Files
gochat/docs/requirements/M3-codegraph-context.md
T
2026-06-04 15:44:48 +08:00

572 lines
14 KiB
Markdown

## Code Context
**Query:** conversation, message, conversation participant, attachment, message template, incoming message service, outgoing message service, conversation status, message type
### Entry Points
- **Sms::IncomingMessageService** (class) - app/services/sms/incoming_message_service.rb:1
- **Line::IncomingMessageService** (class) - app/services/line/incoming_message_service.rb:4
- **Twilio::IncomingMessageService** (class) - app/services/twilio/incoming_message_service.rb:1
### Related Symbols
- app/services/sms/incoming_message_service.rb: perform:6, account:23, channel:27, phone_number:31, formatted_phone_number:35, set_contact:39, conversation_params:50, set_conversation:59, contact_attributes:72, attach_files:79
### Code
#### Sms::IncomingMessageService (app/services/sms/incoming_message_service.rb:1)
```ruby
class Sms::IncomingMessageService
include ::FileTypeHelper
pattr_initialize [:inbox!, :params!]
def perform
set_contact
set_conversation
@message = @conversation.messages.create!(
content: params[:text],
account_id: @inbox.account_id,
inbox_id: @inbox.id,
message_type: :incoming,
sender: @contact,
source_id: params[:id]
)
attach_files
@message.save!
end
private
def account
@account ||= @inbox.account
end
def channel
@channel ||= @inbox.channel
end
def phone_number
params[:from]
end
def formatted_phone_number
TelephoneNumber.parse(phone_number).international_number
end
def set_contact
contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: params[:from],
inbox: @inbox,
contact_attributes: contact_attributes
).perform
@contact_inbox = contact_inbox
@contact = contact_inbox.contact
end
def conversation_params
{
account_id: @inbox.account_id,
inbox_id: @inbox.id,
contact_id: @contact.id,
contact_inbox_id: @contact_inbox.id
}
end
def set_conversation
# if lock to single conversation is disabled, we will create a new conversation if previous conversation is resolved
@conversation = if @inbox.lock_to_single_conversation
@contact_inbox.conversations.last
else
@contact_inbox.conversations.where
// ... truncated ...
```
#### Line::IncomingMessageService (app/services/line/incoming_message_service.rb:4)
```ruby
class Line::IncomingMessageService
include ::FileTypeHelper
pattr_initialize [:inbox!, :params!]
LINE_STICKER_IMAGE_URL = 'https://stickershop.line-scdn.net/stickershop/v1/sticker/%s/android/sticker.png'.freeze
def perform
# probably test events
return if params[:events].blank?
parse_events
end
private
def parse_events
params[:events].each do |event|
next unless event_type_message?(event)
get_line_contact_info(event)
next if @line_contact_info['userId'].blank?
set_contact
set_conversation
next unless message_created? event
attach_files event['message']
@message.save!
end
end
def message_created?(event)
@message = @conversation.messages.build(
content: message_content(event),
account_id: @inbox.account_id,
content_type: message_content_type(event),
inbox_id: @inbox.id,
message_type: :incoming,
sender: @contact,
source_id: event['message']['id'].to_s
)
@message
end
def message_content(event)
message_type = event.dig('message', 'type')
case message_type
when 'text'
event.dig('message', 'text')
when 'sticker'
sticker_id = event.dig('message', 'stickerId')
sticker_image_url(sticker_id)
end
end
# Currently, Chatwoot doesn't support stickers. As a temporary solution,
# we're displaying stickers as images using the sticker ID in markdown format.
# This is subject to change in the future
// ... truncated ...
```
#### Twilio::IncomingMessageService (app/services/twilio/incoming_message_service.rb:1)
```ruby
class Twilio::IncomingMessageService
include ::FileTypeHelper
include ::Twilio::WhatsappIdentifierHelper
pattr_initialize [:params!]
def perform
return if twilio_channel.blank?
set_contact
set_conversation
@message = @conversation.messages.build(
content: message_body,
account_id: @inbox.account_id,
inbox_id: @inbox.id,
message_type: :incoming,
sender: @contact,
source_id: params[:SmsSid]
)
attach_files
attach_location if location_message?
@message.save!
end
private
def twilio_channel
@twilio_channel ||= ::Channel::TwilioSms.find_by(messaging_service_sid: params[:MessagingServiceSid]) if params[:MessagingServiceSid].present?
if params[:AccountSid].present? && params[:To].present?
@twilio_channel ||= ::Channel::TwilioSms.find_by(account_sid: params[:AccountSid],
phone_number: params[:To])
end
log_channel_not_found if @twilio_channel.blank?
@twilio_channel
end
def log_channel_not_found
Rails.logger.warn(
'[TWILIO] Incoming message channel lookup failed ' \
"account_sid=#{params[:AccountSid]} " \
"to=#{params[:To]} " \
"messaging_service_sid=#{params[:MessagingServiceSid]} " \
"sms_sid=#{params[:SmsSid]}"
)
end
def inbox
@inbox ||= twilio_channel.inbox
end
def account
@account ||= inbox.account
end
# Twilio WhatsApp phone payloads arrive as `whatsap
// ... truncated ...
```
#### perform (app/services/sms/incoming_message_service.rb:6)
```ruby
def perform
set_contact
set_conversation
@message = @conversation.messages.create!(
content: params[:text],
account_id: @inbox.account_id,
inbox_id: @inbox.id,
message_type: :incoming,
sender: @contact,
source_id: params[:id]
)
attach_files
@message.save!
end
```
#### account (app/services/sms/incoming_message_service.rb:23)
```ruby
def account
@account ||= @inbox.account
end
```
#### channel (app/services/sms/incoming_message_service.rb:27)
```ruby
def channel
@channel ||= @inbox.channel
end
```
#### phone_number (app/services/sms/incoming_message_service.rb:31)
```ruby
def phone_number
params[:from]
end
```
#### formatted_phone_number (app/services/sms/incoming_message_service.rb:35)
```ruby
def formatted_phone_number
TelephoneNumber.parse(phone_number).international_number
end
```
#### set_contact (app/services/sms/incoming_message_service.rb:39)
```ruby
def set_contact
contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: params[:from],
inbox: @inbox,
contact_attributes: contact_attributes
).perform
@contact_inbox = contact_inbox
@contact = contact_inbox.contact
end
```
#### conversation_params (app/services/sms/incoming_message_service.rb:50)
```ruby
def conversation_params
{
account_id: @inbox.account_id,
inbox_id: @inbox.id,
contact_id: @contact.id,
contact_inbox_id: @contact_inbox.id
}
end
```
#### set_conversation (app/services/sms/incoming_message_service.rb:59)
```ruby
def set_conversation
# if lock to single conversation is disabled, we will create a new conversation if previous conversation is resolved
@conversation = if @inbox.lock_to_single_conversation
@contact_inbox.conversations.last
else
@contact_inbox.conversations.where
.not(status: :resolved).last
end
return if @conversation
@conversation = ::Conversation.create!(conversation_params)
end
```
#### contact_attributes (app/services/sms/incoming_message_service.rb:72)
```ruby
def contact_attributes
{
name: formatted_phone_number,
phone_number: phone_number
}
end
```
#### attach_files (app/services/sms/incoming_message_service.rb:79)
```ruby
def attach_files
return if params[:media].blank?
params[:media].each do |media_url|
# we don't need to process this files since chatwoot doesn't support it
next if media_url.end_with?('.smil', '.xml')
attachment_file = Down.download(
media_url,
http_basic_authentication: [channel.provider_config['api_key'], channel.provider_config['api_secret']]
)
@message.attachments.new(
account_id: @message.account_id,
file_type: file_type(attachment_file.content_type),
file: {
io: attachment_file,
filename: attachment_file.original_filename,
content_type: attachment_file.content_type
}
)
end
end
```
#### process_event_params (app/jobs/webhooks/sms_events_job.rb:17)
```ruby
def process_event_params(channel, params)
if delivery_event?(params)
Sms::DeliveryStatusService.new(channel: channel, params: params[:message].with_indifferent_access).perform
else
Sms::IncomingMessageService.new(inbox: channel.inbox, params: params[:message].with_indifferent_access).perform
end
end
```
#### perform (app/services/line/incoming_message_service.rb:9)
```ruby
def perform
# probably test events
return if params[:events].blank?
parse_events
end
```
#### parse_events (app/services/line/incoming_message_service.rb:18)
```ruby
def parse_events
params[:events].each do |event|
next unless event_type_message?(event)
get_line_contact_info(event)
next if @line_contact_info['userId'].blank?
set_contact
set_conversation
next unless message_created? event
attach_files event['message']
@message.save!
end
end
```
#### message_created? (app/services/line/incoming_message_service.rb:35)
```ruby
def message_created?(event)
@message = @conversation.messages.build(
content: message_content(event),
account_id: @inbox.account_id,
content_type: message_content_type(event),
inbox_id: @inbox.id,
message_type: :incoming,
sender: @contact,
source_id: event['message']['id'].to_s
)
@message
end
```
#### message_content (app/services/line/incoming_message_service.rb:48)
```ruby
def message_content(event)
message_type = event.dig('message', 'type')
case message_type
when 'text'
event.dig('message', 'text')
when 'sticker'
sticker_id = event.dig('message', 'stickerId')
sticker_image_url(sticker_id)
end
end
```
#### sticker_image_url (app/services/line/incoming_message_service.rb:65)
```ruby
def sticker_image_url(sticker_id)
"![sticker-#{sticker_id}](#{LINE_STICKER_IMAGE_URL % sticker_id})"
end
```
#### message_content_type (app/services/line/incoming_message_service.rb:69)
```ruby
def message_content_type(event)
return 'sticker' if event['message']['type'] == 'sticker'
'text'
end
```
#### attach_files (app/services/line/incoming_message_service.rb:75)
```ruby
def attach_files(message)
return unless message_type_non_text?(message['type'])
response = inbox.channel.client.get_message_content(message['id'])
extension = get_file_extension(response)
file_name = message['fileName'] || "media-#{message['id']}.#{extension}"
temp_file = Tempfile.new(file_name)
temp_file.binmode
temp_file << response.body
temp_file.rewind
@message.attachments.new(
account_id: @message.account_id,
file_type: file_content_type(response),
file: {
io: temp_file,
filename: file_name,
content_type: response.content_type
}
)
end
```
#### get_file_extension (app/services/line/incoming_message_service.rb:98)
```ruby
def get_file_extension(response)
if response.content_type&.include?('/')
response.content_type.split('/')[1]
else
'bin'
end
end
```
#### event_type_message? (app/services/line/incoming_message_service.rb:106)
```ruby
def event_type_message?(event)
event['type'] == 'message' || event['type'] == 'sticker'
end
```
#### message_type_non_text? (app/services/line/incoming_message_service.rb:110)
```ruby
def message_type_non_text?(type)
[
Line::Bot::Event::MessageType::Video,
Line::Bot::Event::MessageType::Audio,
Line::Bot::Event::MessageType::Image,
Line::Bot::Event::MessageType::File
].include?(type)
end
```
#### account (app/services/line/incoming_message_service.rb:119)
```ruby
def account
@account ||= inbox.account
end
```
#### get_line_contact_info (app/services/line/incoming_message_service.rb:123)
```ruby
def get_line_contact_info(event)
@line_contact_info = JSON.parse(inbox.channel.client.get_profile(event['source']['userId']).body)
end
```
#### set_contact (app/services/line/incoming_message_service.rb:127)
```ruby
def set_contact
contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: @line_contact_info['userId'],
inbox: inbox,
contact_attributes: contact_attributes
).perform
@contact_inbox = contact_inbox
@contact = contact_inbox.contact
end
```
#### conversation_params (app/services/line/incoming_message_service.rb:138)
```ruby
def conversation_params
{
account_id: @inbox.account_id,
inbox_id: @inbox.id,
contact_id: @contact.id,
contact_inbox_id: @contact_inbox.id
}
end
```
#### set_conversation (app/services/line/incoming_message_service.rb:147)
```ruby
def set_conversation
# if lock to single conversation is disabled, we will create a new conversation if previous conversation is resolved
@conversation = if @inbox.lock_to_single_conversation
@contact_inbox.conversations.last
else
@contact_inbox.conversations.where.not(status: :resolved).last
end
return if @conversation
@conversation = ::Conversation.create!(conversation_params)
end
```
#### contact_attributes (app/services/line/incoming_message_service.rb:159)
```ruby
def contact_attributes
{
name: @line_contact_info['displayName'],
avatar_url: @line_contact_info['pictureUrl'],
additional_attributes: additional_attributes
}
end
```