Initial commit: Add logistics and order_detail message types
Some checks failed
Lock Threads / action (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Run Linux nightly installer / nightly (push) Has been cancelled

- Add Logistics component with progress tracking
- Add OrderDetail component for order information
- Support data-driven steps and actions
- Add blue color scale to widget SCSS
- Fix node overflow and progress bar rendering issues
- Add English translations for dashboard components

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Liang XJ
2026-01-26 11:16:56 +08:00
commit 092fb2e083
7646 changed files with 975643 additions and 0 deletions

View File

@@ -0,0 +1,126 @@
class Twitter::DirectMessageParserService < Twitter::WebhooksBaseService
pattr_initialize [:payload]
def perform
return if source_app_id == parent_app_id
set_inbox
ensure_contacts
set_conversation
@message = @conversation.messages.create!(
content: message_create_data['message_data']['text'],
account_id: @inbox.account_id,
inbox_id: @inbox.id,
message_type: outgoing_message? ? :outgoing : :incoming,
sender: @contact,
source_id: direct_message_data['id']
)
attach_files
end
private
def attach_files
return if message_create_data['message_data']['attachment'].blank?
save_media
@message
end
def save_media_urls(file)
@message.content_attributes[:media_url] = file['media_url']
@message.content_attributes[:display_url] = file['display_url']
@message.save!
end
def direct_message_events_params
payload['direct_message_events']
end
def direct_message_data
direct_message_events_params.first
end
def message_create_data
direct_message_data['message_create']
end
def source_app_id
message_create_data['source_app_id']
end
def parent_app_id
ENV.fetch('TWITTER_APP_ID', '')
end
def media
message_create_data['message_data']['attachment']['media']
end
def users
payload[:users]
end
def ensure_contacts
users.each do |key, user|
next if key == profile_id
find_or_create_contact(user)
end
end
def conversation_params
{
account_id: @inbox.account_id,
inbox_id: @inbox.id,
contact_id: @contact.id,
contact_inbox_id: @contact_inbox.id,
additional_attributes: {
type: 'direct_message'
}
}
end
def set_conversation
@conversation = @contact_inbox.conversations.where("additional_attributes ->> 'type' = 'direct_message'").first
return if @conversation
@conversation = ::Conversation.create!(conversation_params)
end
def outgoing_message?
message_create_data['sender_id'] == @inbox.channel.profile_id
end
def api_client
@api_client ||= begin
consumer = OAuth::Consumer.new(ENV.fetch('TWITTER_CONSUMER_KEY', nil), ENV.fetch('TWITTER_CONSUMER_SECRET', nil),
{ site: 'https://api.twitter.com' })
token = { oauth_token: @inbox.channel.twitter_access_token, oauth_token_secret: @inbox.channel.twitter_access_token_secret }
OAuth::AccessToken.from_hash(consumer, token)
end
end
def save_media
save_media_urls(media)
response = api_client.get(media['media_url'], [])
temp_file = Tempfile.new('twitter_attachment')
temp_file.binmode
temp_file << response.body
temp_file.rewind
return unless media['type'] == 'photo'
@message.attachments.new(
account_id: @inbox.account_id,
file_type: 'image',
file: {
io: temp_file,
filename: 'twitter_attachment',
content_type: media['type']
}
)
@message.save!
end
end

View File

@@ -0,0 +1,64 @@
class Twitter::SendOnTwitterService < Base::SendOnChannelService
pattr_initialize [:message!]
private
delegate :additional_attributes, to: :contact
def channel_class
Channel::TwitterProfile
end
def perform_reply
conversation_type == 'tweet' ? send_tweet_reply : send_direct_message
end
def twitter_client
Twitty::Facade.new do |config|
config.consumer_key = ENV.fetch('TWITTER_CONSUMER_KEY', nil)
config.consumer_secret = ENV.fetch('TWITTER_CONSUMER_SECRET', nil)
config.access_token = channel.twitter_access_token
config.access_token_secret = channel.twitter_access_token_secret
config.base_url = 'https://api.twitter.com'
config.environment = ENV.fetch('TWITTER_ENVIRONMENT', '')
end
end
def conversation_type
conversation.additional_attributes['type']
end
def screen_name
return "@#{reply_to_message.inbox.name}" if reply_to_message.outgoing?
"@#{reply_to_message.sender&.additional_attributes.try(:[], 'screen_name') || ''}"
end
def send_direct_message
twitter_client.send_direct_message(
recipient_id: contact_inbox.source_id,
message: message.outgoing_content
)
end
def reply_to_message
@reply_to_message ||= if message.in_reply_to
conversation.messages.find(message.in_reply_to)
else
conversation.messages.incoming.last
end
end
def send_tweet_reply
response = twitter_client.send_tweet_reply(
reply_to_tweet_id: reply_to_message.source_id,
tweet: "#{screen_name} #{message.outgoing_content}"
)
if response.status == '200'
tweet_data = response.body
message.update!(source_id: tweet_data['id_str'])
else
Rails.logger.error "TWITTER_TWEET_REPLY_ERROR #{response.body}"
end
end
end

View File

@@ -0,0 +1,92 @@
class Twitter::TweetParserService < Twitter::WebhooksBaseService
pattr_initialize [:payload]
def perform
set_inbox
return if !tweets_enabled? || message_already_exist? || user_has_blocked?
create_message
end
private
def message_type
user['id'] == profile_id ? :outgoing : :incoming
end
def tweet_text
tweet_data['truncated'] ? tweet_data['extended_tweet']['full_text'] : tweet_data['text']
end
def tweet_create_events_params
payload['tweet_create_events']
end
def tweet_data
tweet_create_events_params.first
end
def user
tweet_data['user']
end
def tweet_id
tweet_data['id'].to_s
end
def user_has_blocked?
payload['user_has_blocked'] == true
end
def tweets_enabled?
@inbox.channel.tweets_enabled?
end
def parent_tweet_id
tweet_data['in_reply_to_status_id_str'].nil? ? tweet_data['id'].to_s : tweet_data['in_reply_to_status_id_str']
end
def conversation_params
{
account_id: @inbox.account_id,
inbox_id: @inbox.id,
contact_id: @contact.id,
contact_inbox_id: @contact_inbox.id,
additional_attributes: {
type: 'tweet',
tweet_id: parent_tweet_id,
tweet_source: tweet_data['source']
}
}
end
def set_conversation
tweet_conversations = @contact_inbox.conversations.where("additional_attributes ->> 'tweet_id' = ?", parent_tweet_id)
@conversation = tweet_conversations.first
return if @conversation
tweet_message = @inbox.messages.find_by(source_id: parent_tweet_id)
@conversation = tweet_message.conversation if tweet_message
return if @conversation
@conversation = ::Conversation.create!(conversation_params)
end
def message_already_exist?
@inbox.messages.find_by(source_id: tweet_id)
end
def create_message
find_or_create_contact(user)
set_conversation
@conversation.messages.create!(
account_id: @inbox.account_id,
sender: @contact,
content: tweet_text,
inbox_id: @inbox.id,
message_type: message_type,
source_id: tweet_id
)
end
end

View File

@@ -0,0 +1,57 @@
class Twitter::WebhookSubscribeService
include Rails.application.routes.url_helpers
pattr_initialize [:inbox_id]
def perform
ensure_webhook
unless subscription?
subscribe_response = twitter_client.create_subscription
raise StandardError, 'Twitter Subscription Failed' unless subscribe_response.status == '204'
end
true
end
private
delegate :channel, to: :inbox
delegate :twitter_client, to: :channel
def inbox
Inbox.find(inbox_id)
end
def twitter_url
webhooks_twitter_url(protocol: 'https')
end
def ensure_webhook
webhooks = fetch_webhooks
return true if webhooks&.first&.try(:[], 'url') == twitter_url
# twitter supports only one webhook url per environment
# so we will delete the existing one if its not chatwoot
unregister_webhook(webhooks.first) if webhooks&.first
register_webhook
end
def unregister_webhook(webhook)
unregister_response = twitter_client.unregister_webhook(id: webhook.try(:[], 'id'))
Rails.logger.info "TWITTER_UNREGISTER_WEBHOOK: #{unregister_response.body}"
end
def register_webhook
register_response = twitter_client.register_webhook(url: twitter_url)
Rails.logger.info "TWITTER_REGISTER_WEBHOOK: #{register_response.body}"
end
def subscription?
response = twitter_client.fetch_subscriptions
response.status == '204'
end
def fetch_webhooks
twitter_client.fetch_webhooks.body
end
end

View File

@@ -0,0 +1,35 @@
class Twitter::WebhooksBaseService
private
def profile_id
payload[:for_user_id]
end
def additional_contact_attributes(user)
{
screen_name: user['screen_name'],
location: user['location'],
url: user['url'],
description: user['description'],
followers_count: user['followers_count'],
friends_count: user['friends_count']
}
end
def set_inbox
twitter_profile = ::Channel::TwitterProfile.find_by(profile_id: profile_id)
@inbox = ::Inbox.find_by!(channel: twitter_profile)
end
def find_or_create_contact(user)
@contact_inbox = @inbox.contact_inboxes.where(source_id: user['id']).first
@contact = @contact_inbox.contact if @contact_inbox
return if @contact
@contact_inbox = @inbox.channel.create_contact_inbox(
user['id'], user['name'], additional_contact_attributes(user)
)
@contact = @contact_inbox.contact
Avatar::AvatarFromUrlJob.perform_later(@contact, user['profile_image_url']) if user['profile_image_url']
end
end