From 0974b1e705d197a5f77f093ca325a79e14900a9f Mon Sep 17 00:00:00 2001 From: Anthony Meirlaen Date: Fri, 1 Dec 2023 01:22:34 +0100 Subject: [PATCH 1/4] fix: Install node 20.x in non-production docker (#8441) (#8442) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 676b5bf74..6503530b7 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -104,7 +104,7 @@ RUN apk update && apk add --no-cache \ && gem install bundler RUN if [ "$RAILS_ENV" != "production" ]; then \ - apk add --no-cache nodejs yarn; \ + apk add --no-cache nodejs-current yarn; \ fi COPY --from=pre-builder /gems/ /gems/ From fdc1123b18864b3602e02fe4e0000eaf3fcd43de Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Fri, 1 Dec 2023 07:23:35 +0530 Subject: [PATCH 2/4] feat: Add support for attachments(image and video) in LINE channel (#8425) --- .../widgets/WootWriter/ReplyBottomPanel.vue | 7 +++ .../widgets/conversation/ReplyBox.vue | 3 +- app/javascript/shared/constants/messages.js | 2 + app/services/line/send_on_line_service.rb | 34 +++++++++++- .../line/send_on_line_service_spec.rb | 52 +++++++++++++++++++ 5 files changed, 96 insertions(+), 2 deletions(-) diff --git a/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue b/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue index eb9b26f30..b5022c9a2 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue @@ -144,6 +144,7 @@ import { FEATURE_FLAGS } from 'dashboard/featureFlags'; import { ALLOWED_FILE_TYPES, ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP, + ALLOWED_FILE_TYPES_FOR_LINE, } from 'shared/constants/messages'; import VideoCallButton from '../VideoCallButton.vue'; import AIAssistanceButton from '../AIAssistanceButton.vue'; @@ -270,6 +271,9 @@ export default { return this.showFileUpload || this.isNote; }, showAudioRecorderButton() { + if (this.isALineChannel) { + return false; + } // Disable audio recorder for safari browser as recording is not supported const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test( navigator.userAgent @@ -291,6 +295,9 @@ export default { if (this.isATwilioWhatsAppChannel) { return ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP; } + if (this.isALineChannel) { + return ALLOWED_FILE_TYPES_FOR_LINE; + } return ALLOWED_FILE_TYPES; }, enableDragAndDrop() { diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index 3712a08e6..e18e1c0e2 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -392,7 +392,8 @@ export default { this.isAPIInbox || this.isAnEmailChannel || this.isASmsInbox || - this.isATelegramChannel + this.isATelegramChannel || + this.isALineChannel ); }, replyButtonLabel() { diff --git a/app/javascript/shared/constants/messages.js b/app/javascript/shared/constants/messages.js index 1cf24f6a4..b53ca6826 100644 --- a/app/javascript/shared/constants/messages.js +++ b/app/javascript/shared/constants/messages.js @@ -55,6 +55,8 @@ export const ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP = 'audio/mpeg, audio/opus, audio/ogg, audio/amr,' + 'video/mp4,' + 'application/pdf,'; +// https://developers.line.biz/en/reference/messaging-api/#image-message, https://developers.line.biz/en/reference/messaging-api/#video-message +export const ALLOWED_FILE_TYPES_FOR_LINE = 'image/png, image/jpeg,video/mp4'; export const CSAT_RATINGS = [ { diff --git a/app/services/line/send_on_line_service.rb b/app/services/line/send_on_line_service.rb index a30de4ab4..f8d704128 100644 --- a/app/services/line/send_on_line_service.rb +++ b/app/services/line/send_on_line_service.rb @@ -6,7 +6,8 @@ class Line::SendOnLineService < Base::SendOnChannelService end def perform_reply - response = channel.client.push_message(message.conversation.contact_inbox.source_id, [{ type: 'text', text: message.content }]) + response = channel.client.push_message(message.conversation.contact_inbox.source_id, build_payload) + return if response.blank? parsed_json = JSON.parse(response.body) @@ -20,6 +21,37 @@ class Line::SendOnLineService < Base::SendOnChannelService end end + def build_payload + if message.content && message.attachments.any? + [text_message, *attachments] + elsif message.content.nil? && message.attachments.any? + attachments + else + text_message + end + end + + def attachments + message.attachments.map do |attachment| + # Support only image and video for now, https://developers.line.biz/en/reference/messaging-api/#image-message + next unless attachment.file_type == 'image' || attachment.file_type == 'video' + + { + type: attachment.file_type, + originalContentUrl: attachment.download_url, + previewImageUrl: attachment.download_url + } + end + end + + # https://developers.line.biz/en/reference/messaging-api/#text-message + def text_message + { + type: 'text', + text: message.content + } + end + # https://developers.line.biz/en/reference/messaging-api/#error-responses def external_error(error) # Message containing information about the error. See https://developers.line.biz/en/reference/messaging-api/#error-messages diff --git a/spec/services/line/send_on_line_service_spec.rb b/spec/services/line/send_on_line_service_spec.rb index 2320f8679..bed8917b8 100644 --- a/spec/services/line/send_on_line_service_spec.rb +++ b/spec/services/line/send_on_line_service_spec.rb @@ -92,5 +92,57 @@ describe Line::SendOnLineService do expect(message.status).to eq('delivered') end end + + context 'with message attachments' do + it 'sends the message with text and attachments' do + attachment = message.attachments.new(account_id: message.account_id, file_type: :image) + attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png') + expected_url_regex = %r{rails/active_storage/disk/[a-zA-Z0-9=_\-+]+/avatar\.png} + + expect(line_client).to receive(:push_message).with( + message.conversation.contact_inbox.source_id, + [ + { type: 'text', text: message.content }, + { + type: 'image', + originalContentUrl: match(expected_url_regex), + previewImageUrl: match(expected_url_regex) + } + ] + ) + + described_class.new(message: message).perform + end + + it 'sends the message with attachments only' do + attachment = message.attachments.new(account_id: message.account_id, file_type: :image) + attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png') + message.update!(content: nil) + expected_url_regex = %r{rails/active_storage/disk/[a-zA-Z0-9=_\-+]+/avatar\.png} + + expect(line_client).to receive(:push_message).with( + message.conversation.contact_inbox.source_id, + [ + { + type: 'image', + originalContentUrl: match(expected_url_regex), + previewImageUrl: match(expected_url_regex) + } + ] + ) + + described_class.new(message: message).perform + end + + it 'sends the message with text only' do + message.attachments.destroy_all + expect(line_client).to receive(:push_message).with( + message.conversation.contact_inbox.source_id, + { type: 'text', text: message.content } + ) + + described_class.new(message: message).perform + end + end end end From aad18e1ca4d08fa9a15ea2f92bbf5189cf2d8b76 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Fri, 1 Dec 2023 23:06:22 +0530 Subject: [PATCH 3/4] feat: Add `notification_deleted` action cable event (#8431) --- app/listeners/action_cable_listener.rb | 6 ++++++ app/models/notification.rb | 5 +++++ lib/events/types.rb | 1 + spec/listeners/action_cable_listener_spec.rb | 21 ++++++++++++++++++++ spec/models/notification_spec.rb | 2 +- 5 files changed, 34 insertions(+), 1 deletion(-) diff --git a/app/listeners/action_cable_listener.rb b/app/listeners/action_cable_listener.rb index ba5f2ed31..01e6b2735 100644 --- a/app/listeners/action_cable_listener.rb +++ b/app/listeners/action_cable_listener.rb @@ -7,6 +7,12 @@ class ActionCableListener < BaseListener broadcast(account, tokens, NOTIFICATION_CREATED, { notification: notification.push_event_data, unread_count: unread_count, count: count }) end + def notification_deleted(event) + notification, account, unread_count, count = extract_notification_and_account(event) + tokens = [event.data[:notification].user.pubsub_token] + broadcast(account, tokens, NOTIFICATION_DELETED, notification: notification, unread_count: unread_count, count: count) + end + def account_cache_invalidated(event) account = event.data[:account] tokens = user_tokens(account, account.agents) diff --git a/app/models/notification.rb b/app/models/notification.rb index 8fad75618..30c93c763 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -41,6 +41,7 @@ class Notification < ApplicationRecord enum notification_type: NOTIFICATION_TYPES after_create_commit :process_notification_delivery, :dispatch_create_event + after_destroy_commit :dispatch_destroy_event # TODO: Get rid of default scope # https://stackoverflow.com/a/1834250/939299 @@ -135,4 +136,8 @@ class Notification < ApplicationRecord def dispatch_create_event Rails.configuration.dispatcher.dispatch(NOTIFICATION_CREATED, Time.zone.now, notification: self) end + + def dispatch_destroy_event + Rails.configuration.dispatcher.dispatch(NOTIFICATION_DELETED, Time.zone.now, notification: self) + end end diff --git a/lib/events/types.rb b/lib/events/types.rb index 2693f5216..6e34fc358 100644 --- a/lib/events/types.rb +++ b/lib/events/types.rb @@ -48,6 +48,7 @@ module Events::Types # notification events NOTIFICATION_CREATED = 'notification.created' + NOTIFICATION_DELETED = 'notification.deleted' # agent events AGENT_ADDED = 'agent.added' diff --git a/spec/listeners/action_cable_listener_spec.rb b/spec/listeners/action_cable_listener_spec.rb index 276fadbf9..35983ab91 100644 --- a/spec/listeners/action_cable_listener_spec.rb +++ b/spec/listeners/action_cable_listener_spec.rb @@ -131,6 +131,27 @@ describe ActionCableListener do end end + describe '#notification_deleted' do + let(:event_name) { :'notification.deleted' } + let!(:notification) { create(:notification, account: account, user: agent) } + let!(:event) { Events::Base.new(event_name, Time.zone.now, notification: notification) } + + it 'sends message to account admins, inbox agents' do + expect(ActionCableBroadcastJob).to receive(:perform_later).with( + [agent.pubsub_token], + 'notification.deleted', + { + account_id: notification.account_id, + notification: notification, + unread_count: 1, + count: 1 + } + ) + + listener.notification_deleted(event) + end + end + describe '#conversation_updated' do let(:event_name) { :'conversation.updated' } let!(:event) { Events::Base.new(event_name, Time.zone.now, conversation: conversation, user: agent, is_private: false) } diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb index 71a6209ad..00de30cfb 100644 --- a/spec/models/notification_spec.rb +++ b/spec/models/notification_spec.rb @@ -134,7 +134,7 @@ Hey @John, @Alisha Peter can you check this ticket?" end end - context 'when primary actory is deleted' do + context 'when primary actor is deleted' do let!(:conversation) { create(:conversation) } it 'clears notifications' do From 449503bb94fd8b33c7e53247c33b39fbc103a2f8 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Mon, 4 Dec 2023 12:32:35 +0530 Subject: [PATCH 4/4] feat: Snooze notification API (#8439) --- .../v1/accounts/notifications_controller.rb | 8 +++++- app/models/notification.rb | 1 + config/routes.rb | 3 ++ ...1149_add_snoozed_until_to_notifications.rb | 5 ++++ db/schema.rb | 3 +- .../accounts/notifications_controller_spec.rb | 28 +++++++++++++++++++ 6 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20231129091149_add_snoozed_until_to_notifications.rb diff --git a/app/controllers/api/v1/accounts/notifications_controller.rb b/app/controllers/api/v1/accounts/notifications_controller.rb index fb23370c5..0d8cf6a47 100644 --- a/app/controllers/api/v1/accounts/notifications_controller.rb +++ b/app/controllers/api/v1/accounts/notifications_controller.rb @@ -1,7 +1,8 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseController RESULTS_PER_PAGE = 15 + include DateRangeHelper - before_action :fetch_notification, only: [:update, :destroy] + before_action :fetch_notification, only: [:update, :destroy, :snooze] before_action :set_primary_actor, only: [:read_all] before_action :set_current_page, only: [:index] @@ -38,6 +39,11 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro render json: @unread_count end + def snooze + @notification.update(snoozed_until: parse_date_time(params[:snoozed_until].to_s)) if params[:snoozed_until] + render json: @notification + end + private def set_primary_actor diff --git a/app/models/notification.rb b/app/models/notification.rb index 30c93c763..de6d6790c 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -7,6 +7,7 @@ # primary_actor_type :string not null # read_at :datetime # secondary_actor_type :string +# snoozed_until :datetime # created_at :datetime not null # updated_at :datetime not null # account_id :bigint not null diff --git a/config/routes.rb b/config/routes.rb index fc45c895c..fb071c91b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -171,6 +171,9 @@ Rails.application.routes.draw do post :read_all get :unread_count end + member do + post :snooze + end end resource :notification_settings, only: [:show, :update] diff --git a/db/migrate/20231129091149_add_snoozed_until_to_notifications.rb b/db/migrate/20231129091149_add_snoozed_until_to_notifications.rb new file mode 100644 index 000000000..14a52a9a7 --- /dev/null +++ b/db/migrate/20231129091149_add_snoozed_until_to_notifications.rb @@ -0,0 +1,5 @@ +class AddSnoozedUntilToNotifications < ActiveRecord::Migration[7.0] + def change + add_column :notifications, :snoozed_until, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 081030b11..0e4ba980c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_11_14_111614) do +ActiveRecord::Schema[7.0].define(version: 2023_11_29_091149) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -729,6 +729,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_11_14_111614) do t.datetime "read_at", precision: nil t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.datetime "snoozed_until" t.index ["account_id"], name: "index_notifications_on_account_id" t.index ["primary_actor_type", "primary_actor_id"], name: "uniq_primary_actor_per_account_notifications" t.index ["secondary_actor_type", "secondary_actor_id"], name: "uniq_secondary_actor_per_account_notifications" diff --git a/spec/controllers/api/v1/accounts/notifications_controller_spec.rb b/spec/controllers/api/v1/accounts/notifications_controller_spec.rb index e922d613a..cadc212d4 100644 --- a/spec/controllers/api/v1/accounts/notifications_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/notifications_controller_spec.rb @@ -153,4 +153,32 @@ RSpec.describe 'Notifications API', type: :request do end end end + + describe 'PATCH /api/v1/accounts/{account.id}/notifications/:id/snooze' do + let(:admin) { create(:user, account: account, role: :administrator) } + let!(:notification) { create(:notification, account: account, user: admin) } + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/notifications/#{notification.id}/snooze", + params: { snoozed_until: DateTime.now.utc + 1.day } + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + let(:admin) { create(:user, account: account, role: :administrator) } + + it 'updates the notification snoozed until' do + post "/api/v1/accounts/#{account.id}/notifications/#{notification.id}/snooze", + headers: admin.create_new_auth_token, + params: { snoozed_until: DateTime.now.utc + 1.day }, + as: :json + + expect(response).to have_http_status(:success) + expect(notification.reload.snoozed_until).not_to eq('') + end + end + end end