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
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:
126
app/services/twitter/direct_message_parser_service.rb
Normal file
126
app/services/twitter/direct_message_parser_service.rb
Normal 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
|
||||
64
app/services/twitter/send_on_twitter_service.rb
Normal file
64
app/services/twitter/send_on_twitter_service.rb
Normal 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
|
||||
92
app/services/twitter/tweet_parser_service.rb
Normal file
92
app/services/twitter/tweet_parser_service.rb
Normal 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
|
||||
57
app/services/twitter/webhook_subscribe_service.rb
Normal file
57
app/services/twitter/webhook_subscribe_service.rb
Normal 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
|
||||
35
app/services/twitter/webhooks_base_service.rb
Normal file
35
app/services/twitter/webhooks_base_service.rb
Normal 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
|
||||
Reference in New Issue
Block a user