From be4410850072678d14252a62f64febb316e2a3d8 Mon Sep 17 00:00:00 2001 From: Cesar Garcia <128240629+Chesars@users.noreply.github.com> Date: Mon, 22 Sep 2025 04:53:28 -0300 Subject: [PATCH 1/4] chore: Add missing space in signup link (#12475) ## Description This branch adds a missing space in the signup footer, **changing**: `Already have an account?Login to Chatwoot` **to** `Already have an account? Login to Chatwoot` ## Type of change Non-breaking change --------- Co-authored-by: Muhsin Keloth --- app/javascript/v3/views/auth/signup/Index.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/v3/views/auth/signup/Index.vue b/app/javascript/v3/views/auth/signup/Index.vue index 508c755f5..86baff55c 100644 --- a/app/javascript/v3/views/auth/signup/Index.vue +++ b/app/javascript/v3/views/auth/signup/Index.vue @@ -62,7 +62,7 @@ export default {
- {{ $t('REGISTER.HAVE_AN_ACCOUNT') }} + {{ $t('REGISTER.HAVE_AN_ACCOUNT') }} {{ replaceInstallationName($t('LOGIN.TITLE')) }} From b5deecc9f9d130139224c696c12b65f75d479c2d Mon Sep 17 00:00:00 2001 From: mix5003 Date: Mon, 22 Sep 2025 16:36:28 +0700 Subject: [PATCH 2/4] feat: Accept file attachment in line channel (#12321) # Pull Request Template ## Description This pull request allow LINE to receive files. ## Type of change Please delete options that are not relevant. - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [ ] This change requires a documentation update ## How Has This Been Tested? add testcase. and test manually by myself. in case you want to test in android, use native share method to share files to LINE. you can share more file types to LINE (native line share only send image,video and audio). ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: mix5003 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Muhsin Keloth --- app/services/line/incoming_message_service.rb | 18 +++++- .../line/incoming_message_service_spec.rb | 64 +++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/app/services/line/incoming_message_service.rb b/app/services/line/incoming_message_service.rb index 4761292d0..0d2f78c7a 100644 --- a/app/services/line/incoming_message_service.rb +++ b/app/services/line/incoming_message_service.rb @@ -76,7 +76,8 @@ class Line::IncomingMessageService response = inbox.channel.client.get_message_content(message['id']) - file_name = "media-#{message['id']}.#{response.content_type.split('/')[1]}" + 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 @@ -93,12 +94,25 @@ class Line::IncomingMessageService ) end + def get_file_extension(response) + if response.content_type&.include?('/') + response.content_type.split('/')[1] + else + 'bin' + end + end + def event_type_message?(event) event['type'] == 'message' || event['type'] == 'sticker' end def message_type_non_text?(type) - [Line::Bot::Event::MessageType::Video, Line::Bot::Event::MessageType::Audio, Line::Bot::Event::MessageType::Image].include?(type) + [ + Line::Bot::Event::MessageType::Video, + Line::Bot::Event::MessageType::Audio, + Line::Bot::Event::MessageType::Image, + Line::Bot::Event::MessageType::File + ].include?(type) end def account diff --git a/spec/services/line/incoming_message_service_spec.rb b/spec/services/line/incoming_message_service_spec.rb index 3bcdf8b04..9160efff2 100644 --- a/spec/services/line/incoming_message_service_spec.rb +++ b/spec/services/line/incoming_message_service_spec.rb @@ -105,6 +105,40 @@ describe Line::IncomingMessageService do }.with_indifferent_access end + let(:file_params) do + { + 'destination': '2342234234', + 'events': [ + { + 'replyToken': '0f3779fba3b349968c5d07db31eab56f', + 'type': 'message', + 'mode': 'active', + 'timestamp': 1_462_629_479_859, + 'source': { + 'type': 'user', + 'userId': 'U4af4980629' + }, + 'message': { + 'type': 'file', + 'id': '354718', + 'fileName': 'contacts.csv', + 'fileSize': 2978 + } + }, + { + 'replyToken': '8cf9239d56244f4197887e939187e19e', + 'type': 'follow', + 'mode': 'active', + 'timestamp': 1_462_629_479_859, + 'source': { + 'type': 'user', + 'userId': 'U4af4980629' + } + } + ] + }.with_indifferent_access + end + let(:sticker_params) do { 'destination': '2342234234', @@ -241,5 +275,35 @@ describe Line::IncomingMessageService do expect(line_channel.inbox.messages.first.attachments.first.file.blob.filename.to_s).to eq('media-354718.mp4') end end + + context 'when valid file message params' do + it 'creates appropriate conversations, message and contacts' do + line_bot = double + line_user_profile = double + allow(Line::Bot::Client).to receive(:new).and_return(line_bot) + allow(line_bot).to receive(:get_profile).and_return(line_user_profile) + file = fixture_file_upload(Rails.root.join('spec/assets/contacts.csv'), 'text/csv') + allow(line_bot).to receive(:get_message_content).and_return( + OpenStruct.new({ + body: Base64.encode64(file.read), + content_type: 'text/csv' + }) + ) + allow(line_user_profile).to receive(:body).and_return( + { + 'displayName': 'LINE Test', + 'userId': 'U4af4980629', + 'pictureUrl': 'https://test.com' + }.to_json + ) + described_class.new(inbox: line_channel.inbox, params: file_params).perform + expect(line_channel.inbox.conversations).not_to eq(0) + expect(Contact.all.first.name).to eq('LINE Test') + expect(Contact.all.first.additional_attributes['social_line_user_id']).to eq('U4af4980629') + expect(line_channel.inbox.messages.first.content).to be_nil + expect(line_channel.inbox.messages.first.attachments.first.file_type).to eq('file') + expect(line_channel.inbox.messages.first.attachments.first.file.blob.filename.to_s).to eq('contacts.csv') + end + end end end From b28c08059f6d0cdb0b36f018117bcc9fcdc341c5 Mon Sep 17 00:00:00 2001 From: Niranjan Patil Date: Mon, 22 Sep 2025 17:05:11 +0530 Subject: [PATCH 3/4] fix: Incorrect contact access in conversations listing (#11797) # Pull Request Template ## Description This PR fixes the incorrect contact access in conversations listing API. Cause: - `undefined method 'conversations' for nil` error because `@contact` is not initialized Solution: - Using `@contact_inbox` to access `@contact` - `@contact_inbox` is properly set in the parent controller's `set_contact_inbox` method Fixes https://linear.app/chatwoot/issue/CW-4185/incorrect-contact-access-pattern-in ## Type of change Please delete options that are not relevant. - [x] Bug fix (non-breaking change which fixes an issue) ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules Co-authored-by: Muhsin Keloth --- .../public/api/v1/inboxes/conversations_controller.rb | 2 +- .../api/v1/inbox/conversations_controller_spec.rb | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/controllers/public/api/v1/inboxes/conversations_controller.rb b/app/controllers/public/api/v1/inboxes/conversations_controller.rb index 4e3b5dca9..242dcde77 100644 --- a/app/controllers/public/api/v1/inboxes/conversations_controller.rb +++ b/app/controllers/public/api/v1/inboxes/conversations_controller.rb @@ -3,7 +3,7 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox before_action :set_conversation, only: [:toggle_typing, :update_last_seen, :show, :toggle_status] def index - @conversations = @contact_inbox.hmac_verified? ? @contact.conversations : @contact_inbox.conversations + @conversations = @contact_inbox.hmac_verified? ? @contact_inbox.contact.conversations : @contact_inbox.conversations end def show; end diff --git a/spec/controllers/public/api/v1/inbox/conversations_controller_spec.rb b/spec/controllers/public/api/v1/inbox/conversations_controller_spec.rb index 2b2bdefc3..6684d18cf 100644 --- a/spec/controllers/public/api/v1/inbox/conversations_controller_spec.rb +++ b/spec/controllers/public/api/v1/inbox/conversations_controller_spec.rb @@ -16,6 +16,17 @@ RSpec.describe 'Public Inbox Contact Conversations API', type: :request do expect(data.first['uuid']).to eq contact_inbox.conversations.first.uuid end + it 'return the conversations when hmac_verified is true' do + contact_inbox.update(hmac_verified: true) + create(:conversation, contact: contact) + get "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations" + + expect(response).to have_http_status(:success) + data = response.parsed_body + expect(data.length).to eq 1 + expect(data.first['uuid']).to eq contact.conversations.first.uuid + end + it 'does not return any private or activity message' do conversation = create(:conversation, contact_inbox: contact_inbox) create(:message, account: conversation.account, inbox: conversation.inbox, conversation: conversation, content: 'message-1') From 0e41263f9c3da2fe34cb1c04fde14620ecb7283a Mon Sep 17 00:00:00 2001 From: mix5003 Date: Mon, 22 Sep 2025 18:35:25 +0700 Subject: [PATCH 4/4] fix: Ensure messages go to correct conversation when receive multi user in 1 LINE webhook (#12322) # Pull Request Template ## Description Ensure messages go to correct conversation when receive multi user in 1 LINE webhook. base on [document](https://developers.line.biz/en/reference/messaging-api/#webhook-event-objects:~:text=There%20is%20not%20necessarily%20one%20user%20per%20webhook). it said ``` There is not necessarily one user per webhook. A message event from person A and a follow event from person B may be in the same webhook. ``` this PR has 1 break changes. In old version. when receive [follow](https://developers.line.biz/en/reference/messaging-api/#follow-event) event, it will create conversation with no messages. After this PR. when receive follow event, it will not create conversation, contact and messages ## Type of change Please delete options that are not relevant. - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [x] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [ ] This change requires a documentation update ## How Has This Been Tested? add test case. and follow event test by delete conversation, and block and unblock line account ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: mix5003 Co-authored-by: Muhsin Keloth Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/services/line/incoming_message_service.rb | 27 ++--- .../line/incoming_message_service_spec.rb | 104 +++++++++++++++++- 2 files changed, 116 insertions(+), 15 deletions(-) diff --git a/app/services/line/incoming_message_service.rb b/app/services/line/incoming_message_service.rb index 0d2f78c7a..6a1192d02 100644 --- a/app/services/line/incoming_message_service.rb +++ b/app/services/line/incoming_message_service.rb @@ -10,11 +10,6 @@ class Line::IncomingMessageService # probably test events return if params[:events].blank? - line_contact_info - return if line_contact_info['userId'].blank? - - set_contact - set_conversation parse_events end @@ -22,6 +17,14 @@ class Line::IncomingMessageService 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'] @@ -30,8 +33,6 @@ class Line::IncomingMessageService end def message_created?(event) - return unless event_type_message?(event) - @message = @conversation.messages.build( content: message_content(event), account_id: @inbox.account_id, @@ -119,13 +120,13 @@ class Line::IncomingMessageService @account ||= inbox.account end - def line_contact_info - @line_contact_info ||= JSON.parse(inbox.channel.client.get_profile(params[:events].first['source']['userId']).body) + def get_line_contact_info(event) + @line_contact_info = JSON.parse(inbox.channel.client.get_profile(event['source']['userId']).body) end def set_contact contact_inbox = ::ContactInboxWithContactBuilder.new( - source_id: line_contact_info['userId'], + source_id: @line_contact_info['userId'], inbox: inbox, contact_attributes: contact_attributes ).perform @@ -152,15 +153,15 @@ class Line::IncomingMessageService def contact_attributes { - name: line_contact_info['displayName'], - avatar_url: line_contact_info['pictureUrl'], + name: @line_contact_info['displayName'], + avatar_url: @line_contact_info['pictureUrl'], additional_attributes: additional_attributes } end def additional_attributes { - social_line_user_id: line_contact_info['userId'] + social_line_user_id: @line_contact_info['userId'] } end diff --git a/spec/services/line/incoming_message_service_spec.rb b/spec/services/line/incoming_message_service_spec.rb index 9160efff2..a7805ce9b 100644 --- a/spec/services/line/incoming_message_service_spec.rb +++ b/spec/services/line/incoming_message_service_spec.rb @@ -35,6 +35,62 @@ describe Line::IncomingMessageService do }.with_indifferent_access end + let(:follow_params) do + { + 'destination': '2342234234', + 'events': [ + { + 'replyToken': '8cf9239d56244f4197887e939187e19e', + 'type': 'follow', + 'mode': 'active', + 'timestamp': 1_462_629_479_859, + 'source': { + 'type': 'user', + 'userId': 'U4af4980629' + } + } + ] + }.with_indifferent_access + end + + let(:multi_user_params) do + { + 'destination': '2342234234', + 'events': [ + { + 'replyToken': '0f3779fba3b349968c5d07db31eab56f1', + 'type': 'message', + 'mode': 'active', + 'timestamp': 1_462_629_479_859, + 'source': { + 'type': 'user', + 'userId': 'U4af4980629' + }, + 'message': { + 'id': '3257081', + 'type': 'text', + 'text': 'Hello, world 1' + } + }, + { + 'replyToken': '0f3779fba3b349968c5d07db31eab56f2', + 'type': 'message', + 'mode': 'active', + 'timestamp': 1_462_629_479_859, + 'source': { + 'type': 'user', + 'userId': 'U4af49806292' + }, + 'message': { + 'id': '3257082', + 'type': 'text', + 'text': 'Hello, world 2' + } + } + ] + }.with_indifferent_access + end + let(:image_params) do { 'destination': '2342234234', @@ -175,8 +231,8 @@ describe Line::IncomingMessageService do end describe '#perform' do - context 'when valid text message params' do - it 'creates appropriate conversations, message and contacts' do + context 'when non-text message params' do + it 'does not create conversations, messages and contacts' do line_bot = double line_user_profile = double allow(Line::Bot::Client).to receive(:new).and_return(line_bot) @@ -188,12 +244,56 @@ describe Line::IncomingMessageService do 'pictureUrl': 'https://test.com' }.to_json ) + described_class.new(inbox: line_channel.inbox, params: follow_params).perform + expect(line_channel.inbox.conversations.size).to eq(0) + expect(Contact.all.size).to eq(0) + expect(line_channel.inbox.messages.size).to eq(0) + end + end + + context 'when valid text message params' do + let(:line_bot) { double } + let(:line_user_profile) { double } + + before do + allow(Line::Bot::Client).to receive(:new).and_return(line_bot) + allow(line_bot).to receive(:get_profile).with('U4af4980629').and_return(line_user_profile) + allow(line_user_profile).to receive(:body).and_return( + { + 'displayName': 'LINE Test', + 'userId': 'U4af4980629', + 'pictureUrl': 'https://test.com' + }.to_json + ) + end + + it 'creates appropriate conversations, message and contacts' do described_class.new(inbox: line_channel.inbox, params: params).perform expect(line_channel.inbox.conversations).not_to eq(0) expect(Contact.all.first.name).to eq('LINE Test') expect(Contact.all.first.additional_attributes['social_line_user_id']).to eq('U4af4980629') expect(line_channel.inbox.messages.first.content).to eq('Hello, world') end + + it 'creates appropriate conversations, message and contacts for multi user' do + line_user_profile2 = double + allow(line_bot).to receive(:get_profile).with('U4af49806292').and_return(line_user_profile2) + allow(line_user_profile2).to receive(:body).and_return( + { + 'displayName': 'LINE Test 2', + 'userId': 'U4af49806292', + 'pictureUrl': 'https://test.com' + }.to_json + ) + described_class.new(inbox: line_channel.inbox, params: multi_user_params).perform + expect(line_channel.inbox.conversations.size).to eq(2) + expect(Contact.all.first.name).to eq('LINE Test') + expect(Contact.all.first.additional_attributes['social_line_user_id']).to eq('U4af4980629') + expect(Contact.all.last.name).to eq('LINE Test 2') + expect(Contact.all.last.additional_attributes['social_line_user_id']).to eq('U4af49806292') + expect(line_channel.inbox.messages.first.content).to eq('Hello, world 1') + expect(line_channel.inbox.messages.last.content).to eq('Hello, world 2') + end end context 'when valid sticker message params' do