{{ note }} -
{row.name}
- + {this.$t('CONTACTS_PAGE.LIST.VIEW_DETAILS')}+ {{ text }} +
++ {{ $t('EVENTS.HEADER.TITLE') }} +
++ {{ eventType }} +
+ on {{ eventPath }} +- {{ $t('CONTACT_PANEL.LABELS.NO_AVAILABLE_LABELS') }} -
-- {{ $t('CONTACT_PANEL.LABELS.NO_LABELS_TO_ADD') }} -
-+ {{ integrationName }} +
++ {{ integrationDescription }} +
+| + {{ hookHeader }} + | ++ {{ $t('INTEGRATION_APPS.LIST.INBOX') }} + | + + +|
|---|---|---|
| + {{ property }} + | ++ {{ inboxName(hook) }} + | +
+ |
+
+ {{ + $t('INTEGRATION_APPS.NO_HOOK_CONFIGURED', { + integrationId: integration.id, + }) + }} +
++ {{ integration.name }} +
+ ++ {{ integration.name }} +
++ {{ integration.description }} +
+Welcome, <%= @resource.name %>!
-<% if @resource&.inviter.present? %> -<%= @resource.inviter.name %>, with <%= @resource.inviter.account.name %>, has invited you to try out Chatwoot!
+<% account_user = @resource&.account_users&.first %> +<% if account_user&.inviter.present? %> +<%= account_user.inviter.name %>, with <%= account_user.account.name %>, has invited you to try out Chatwoot!
<% end %>You can confirm your account email through the link below:
diff --git a/app/views/platform/api/v1/agent_bots/create.json.jbuilder b/app/views/platform/api/v1/agent_bots/create.json.jbuilder new file mode 100644 index 000000000..32123ab29 --- /dev/null +++ b/app/views/platform/api/v1/agent_bots/create.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'platform/api/v1/models/agent_bot.json.jbuilder', resource: @resource diff --git a/app/views/platform/api/v1/agent_bots/index.json.jbuilder b/app/views/platform/api/v1/agent_bots/index.json.jbuilder new file mode 100644 index 000000000..9dd6dcce6 --- /dev/null +++ b/app/views/platform/api/v1/agent_bots/index.json.jbuilder @@ -0,0 +1,3 @@ +json.array! @resources do |resource| + json.partial! 'platform/api/v1/models/agent_bot.json.jbuilder', resource: resource.permissible +end diff --git a/app/views/platform/api/v1/agent_bots/show.json.jbuilder b/app/views/platform/api/v1/agent_bots/show.json.jbuilder new file mode 100644 index 000000000..32123ab29 --- /dev/null +++ b/app/views/platform/api/v1/agent_bots/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'platform/api/v1/models/agent_bot.json.jbuilder', resource: @resource diff --git a/app/views/platform/api/v1/agent_bots/update.json.jbuilder b/app/views/platform/api/v1/agent_bots/update.json.jbuilder new file mode 100644 index 000000000..32123ab29 --- /dev/null +++ b/app/views/platform/api/v1/agent_bots/update.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'platform/api/v1/models/agent_bot.json.jbuilder', resource: @resource diff --git a/app/views/platform/api/v1/models/_agent_bot.json.jbuilder b/app/views/platform/api/v1/models/_agent_bot.json.jbuilder new file mode 100644 index 000000000..1aa152186 --- /dev/null +++ b/app/views/platform/api/v1/models/_agent_bot.json.jbuilder @@ -0,0 +1,5 @@ +json.id resource.id +json.name resource.name +json.description resource.description +json.outgoing_url resource.name +json.account_id resource.account_id diff --git a/app/views/public/api/v1/inboxes/contacts/create.json.jbuilder b/app/views/public/api/v1/inboxes/contacts/create.json.jbuilder new file mode 100644 index 000000000..256b588a5 --- /dev/null +++ b/app/views/public/api/v1/inboxes/contacts/create.json.jbuilder @@ -0,0 +1,2 @@ +json.source_id @contact_inbox.source_id +json.partial! 'public/api/v1/models/contact.json.jbuilder', resource: @contact_inbox.contact diff --git a/app/views/public/api/v1/inboxes/contacts/show.json.jbuilder b/app/views/public/api/v1/inboxes/contacts/show.json.jbuilder new file mode 100644 index 000000000..256b588a5 --- /dev/null +++ b/app/views/public/api/v1/inboxes/contacts/show.json.jbuilder @@ -0,0 +1,2 @@ +json.source_id @contact_inbox.source_id +json.partial! 'public/api/v1/models/contact.json.jbuilder', resource: @contact_inbox.contact diff --git a/app/views/public/api/v1/inboxes/contacts/update.json.jbuilder b/app/views/public/api/v1/inboxes/contacts/update.json.jbuilder new file mode 100644 index 000000000..256b588a5 --- /dev/null +++ b/app/views/public/api/v1/inboxes/contacts/update.json.jbuilder @@ -0,0 +1,2 @@ +json.source_id @contact_inbox.source_id +json.partial! 'public/api/v1/models/contact.json.jbuilder', resource: @contact_inbox.contact diff --git a/app/views/public/api/v1/inboxes/conversations/create.json.jbuilder b/app/views/public/api/v1/inboxes/conversations/create.json.jbuilder new file mode 100644 index 000000000..81d59cde5 --- /dev/null +++ b/app/views/public/api/v1/inboxes/conversations/create.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'public/api/v1/models/conversation.json.jbuilder', resource: @conversation diff --git a/app/views/public/api/v1/inboxes/conversations/index.json.jbuilder b/app/views/public/api/v1/inboxes/conversations/index.json.jbuilder new file mode 100644 index 000000000..97942b57b --- /dev/null +++ b/app/views/public/api/v1/inboxes/conversations/index.json.jbuilder @@ -0,0 +1,3 @@ +json.array! @conversations do |conversation| + json.partial! 'public/api/v1/models/conversation.json.jbuilder', resource: conversation +end diff --git a/app/views/public/api/v1/inboxes/messages/create.json.jbuilder b/app/views/public/api/v1/inboxes/messages/create.json.jbuilder new file mode 100644 index 000000000..8b138e948 --- /dev/null +++ b/app/views/public/api/v1/inboxes/messages/create.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'public/api/v1/models/message.json.jbuilder', resource: @message diff --git a/app/views/public/api/v1/inboxes/messages/index.json.jbuilder b/app/views/public/api/v1/inboxes/messages/index.json.jbuilder new file mode 100644 index 000000000..124c79e2d --- /dev/null +++ b/app/views/public/api/v1/inboxes/messages/index.json.jbuilder @@ -0,0 +1,3 @@ +json.array! @messages do |message| + json.partial! 'public/api/v1/models/message.json.jbuilder', resource: message +end diff --git a/app/views/public/api/v1/inboxes/messages/update.json.jbuilder b/app/views/public/api/v1/inboxes/messages/update.json.jbuilder new file mode 100644 index 000000000..8b138e948 --- /dev/null +++ b/app/views/public/api/v1/inboxes/messages/update.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'public/api/v1/models/message.json.jbuilder', resource: @message diff --git a/app/views/public/api/v1/models/_contact.json.jbuilder b/app/views/public/api/v1/models/_contact.json.jbuilder new file mode 100644 index 000000000..aa8abf2e2 --- /dev/null +++ b/app/views/public/api/v1/models/_contact.json.jbuilder @@ -0,0 +1,4 @@ +json.id resource.id +json.name resource.name +json.email resource.email +json.pubsub_token resource.pubsub_token diff --git a/app/views/public/api/v1/models/_conversation.json.jbuilder b/app/views/public/api/v1/models/_conversation.json.jbuilder new file mode 100644 index 000000000..7029238d9 --- /dev/null +++ b/app/views/public/api/v1/models/_conversation.json.jbuilder @@ -0,0 +1,10 @@ +json.id resource.display_id +json.inbox_id resource.inbox_id +json.contact_last_seen_at resource.contact_last_seen_at.to_i +json.status resource.status +json.messages do + json.array! resource.messages do |message| + json.partial! 'public/api/v1/models/message.json.jbuilder', resource: message + end +end +json.contact resource.contact diff --git a/app/views/public/api/v1/models/_message.json.jbuilder b/app/views/public/api/v1/models/_message.json.jbuilder new file mode 100644 index 000000000..8aa42aba3 --- /dev/null +++ b/app/views/public/api/v1/models/_message.json.jbuilder @@ -0,0 +1,9 @@ +json.id resource.id +json.content resource.content +json.message_type resource.message_type_before_type_cast +json.content_type resource.content_type +json.content_attributes resource.content_attributes +json.created_at resource.created_at.to_i +json.conversation_id resource.conversation.display_id +json.attachments resource.attachments.map(&:push_event_data) if resource.attachments.present? +json.sender resource.sender.push_event_data if resource.sender diff --git a/app/views/widgets/show.html.erb b/app/views/widgets/show.html.erb index 85297111a..c63dd08eb 100644 --- a/app/views/widgets/show.html.erb +++ b/app/views/widgets/show.html.erb @@ -8,9 +8,6 @@ window.chatwootWebChannel = { avatarUrl: '<%= @web_widget.inbox.avatar_url %>', hasAConnectedAgentBot: '<%= @web_widget.inbox.agent_bot&.name %>', - <% if @web_widget.inbox.agent_bot %> - hideInputForBotConversations: <%= @web_widget.inbox.agent_bot.hide_input_for_bot_conversations %>, - <% end %> locale: '<%= @web_widget.account.locale %>', websiteName: '<%= @web_widget.inbox.name %>', websiteToken: '<%= @web_widget.website_token %>', diff --git a/app/workers/conversation_reply_email_worker.rb b/app/workers/conversation_reply_email_worker.rb index c352711b0..1f97b43ee 100644 --- a/app/workers/conversation_reply_email_worker.rb +++ b/app/workers/conversation_reply_email_worker.rb @@ -8,9 +8,9 @@ class ConversationReplyEmailWorker # send the email if @conversation.messages.incoming&.last&.content_type == 'incoming_email' || email_inbox? - ConversationReplyMailer.reply_without_summary(@conversation, queued_time).deliver_later + ConversationReplyMailer.with(account: @conversation.account).reply_without_summary(@conversation, queued_time).deliver_later else - ConversationReplyMailer.reply_with_summary(@conversation, queued_time).deliver_later + ConversationReplyMailer.with(account: @conversation.account).reply_with_summary(@conversation, queued_time).deliver_later end # delete the redis set from the first new message on the conversation diff --git a/config/app.yml b/config/app.yml index 55461cba8..258359fd5 100644 --- a/config/app.yml +++ b/config/app.yml @@ -1,5 +1,5 @@ shared: &shared - version: '1.16.2' + version: '1.17.0' development: <<: *shared diff --git a/config/environments/development.rb b/config/environments/development.rb index 557000065..eab9c411d 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -76,4 +76,16 @@ Rails.application.configure do Bullet.bullet_logger = true Bullet.rails_logger = true end + + # ref: https://github.com/cyu/rack-cors + config.middleware.insert_before 0, Rack::Cors do + allow do + origins '*' + resource '/packs/*', headers: :any, methods: [:get, :options] + resource '*', headers: :any, methods: :any, expose: ['access-token', 'client', 'uid', 'expiry'] + end + end + + # ref : https://medium.com/@emikaijuin/connecting-to-action-cable-without-rails-d39a8aaa52d5 + config.action_cable.disable_request_forgery_protection = true end diff --git a/config/environments/production.rb b/config/environments/production.rb index f30ee2239..c60fff3e6 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -42,9 +42,12 @@ Rails.application.configure do # Mount Action Cable outside main process or domain # config.action_cable.mount_path = nil # config.action_cable.url = 'wss://example.com/cable' - if ENV['FRONTEND_URL'].present? - config.action_cable.allowed_request_origins = [ENV['FRONTEND_URL'], %r{https?://#{URI.parse(ENV['FRONTEND_URL']).host}(:[0-9]+)?}] - end + + # to enable connecting to the API channel public APIs + config.action_cable.disable_request_forgery_protection = true + # if ENV['FRONTEND_URL'].present? + # config.action_cable.allowed_request_origins = [ENV['FRONTEND_URL'], %r{https?://#{URI.parse(ENV['FRONTEND_URL']).host}(:[0-9]+)?}] + # end # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = ActiveModel::Type::Boolean.new.cast(ENV.fetch('FORCE_SSL', false)) @@ -110,10 +113,14 @@ Rails.application.configure do # font cors issue with CDN # Ref: https://stackoverflow.com/questions/56960709/rails-font-cors-policy + # ref: https://github.com/cyu/rack-cors config.middleware.insert_before 0, Rack::Cors do allow do origins '*' resource '/packs/*', headers: :any, methods: [:get, :options] + if ActiveModel::Type::Boolean.new.cast(ENV.fetch('CW_API_ONLY_SERVER', false)) + resource '*', headers: :any, methods: :any, expose: ['access-token', 'client', 'uid', 'expiry'] + end end end end diff --git a/config/initializers/bot.rb b/config/initializers/bot.rb index c730c8b1b..00ef3a0ac 100644 --- a/config/initializers/bot.rb +++ b/config/initializers/bot.rb @@ -1,3 +1,7 @@ +# Remember that Rails only eager loads everything in its production environment. +# In the development and test environments, it only requires files as you reference constants. +# You'll need to explicitly load app/bot + unless Rails.env.production? bot_files = Dir[Rails.root.join('app', 'bot', '**', '*.rb')] bot_reloader = ActiveSupport::FileUpdateChecker.new(bot_files) do @@ -11,21 +15,7 @@ unless Rails.env.production? bot_files.each { |file| require_dependency file } end -module Facebook - module Messenger - module Incoming - # The Message class represents an incoming Facebook Messenger message. - class Message - include Facebook::Messenger::Incoming::Common - - def app_id - @messaging['message']['app_id'] - end - end - end - end -end - +# ref: https://github.com/jgorset/facebook-messenger#make-a-configuration-provider class ChatwootFbProvider < Facebook::Messenger::Configuration::Providers::Base def valid_verify_token?(_verify_token) ENV['FB_VERIFY_TOKEN'] @@ -36,13 +26,13 @@ class ChatwootFbProvider < Facebook::Messenger::Configuration::Providers::Base end def access_token_for(page_id) - Channel::FacebookPage.where(page_id: page_id).last.access_token + Channel::FacebookPage.where(page_id: page_id).last.page_access_token end private def bot - MyApp::Bot + Chatwoot::Bot end end diff --git a/config/initializers/devise_token_auth.rb b/config/initializers/devise_token_auth.rb index b70738bf3..07904e621 100644 --- a/config/initializers/devise_token_auth.rb +++ b/config/initializers/devise_token_auth.rb @@ -7,7 +7,7 @@ DeviseTokenAuth.setup do |config| # By default, users will need to re-authenticate after 2 weeks. This setting # determines how long tokens will remain valid after they are issued. - # config.token_lifespan = 2.weeks + config.token_lifespan = 2.months # Sets the max number of concurrent devices per user, which is 10 by default. # After this limit is reached, the oldest tokens will be removed. diff --git a/config/initializers/languages.rb b/config/initializers/languages.rb index bf4d9857d..15e4b4da4 100644 --- a/config/initializers/languages.rb +++ b/config/initializers/languages.rb @@ -32,7 +32,8 @@ LANGUAGES_CONFIG = { 27 => { name: 'Svenska (sv)', iso_639_3_code: 'swe', iso_639_1_code: 'sv', enabled: true }, 28 => { name: 'magyar nyelv (hu)', iso_639_3_code: 'hun', iso_639_1_code: 'hu', enabled: true }, 29 => { name: 'norsk (no)', iso_639_3_code: 'nor', iso_639_1_code: 'no', enabled: true }, - 30 => { name: '中文 (zh-CN)', iso_639_3_code: 'zho', iso_639_1_code: 'zh_CN', enabled: true } + 30 => { name: '中文 (zh-CN)', iso_639_3_code: 'zho', iso_639_1_code: 'zh_CN', enabled: true }, + 31 => { name: 'język polski (pl)', iso_639_3_code: 'pol', iso_639_1_code: 'pl', enabled: true } }.filter { |_key, val| val[:enabled] }.freeze Rails.configuration.i18n.available_locales = LANGUAGES_CONFIG.map { |_index, lang| lang[:iso_639_1_code].to_sym } diff --git a/config/initializers/liquid_handler.rb b/config/initializers/liquid_handler.rb index 7ce86da51..f50093437 100644 --- a/config/initializers/liquid_handler.rb +++ b/config/initializers/liquid_handler.rb @@ -1 +1,3 @@ +require Rails.root.join('lib/action_view/template/handlers/liquid') + ActionView::Template.register_template_handler :liquid, ActionView::Template::Handlers::Liquid diff --git a/config/initializers/secure_password.rb b/config/initializers/secure_password.rb new file mode 100644 index 000000000..139bcfe15 --- /dev/null +++ b/config/initializers/secure_password.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +Devise.setup do |config| + # ==> Configuration for the Devise Secure Password extension + # Module: password_has_required_content + # + # Configure password content requirements including the number of uppercase, + # lowercase, number, and special characters that are required. To configure the + # minimum and maximum length refer to the Devise config.password_length + # standard configuration parameter. + + # The number of uppercase letters (latin A-Z) required in a password: + config.password_required_uppercase_count = 1 + + # The number of lowercase letters (latin A-Z) required in a password: + config.password_required_lowercase_count = 1 + + # The number of numbers (0-9) required in a password: + config.password_required_number_count = 1 + + # The number of special characters (!@#$%^&*()_+-=[]{}|') required in a password: + config.password_required_special_character_count = 1 + + # we are not using the configurations below + # ==> Configuration for the Devise Secure Password extension + # Module: password_disallows_frequent_reuse + # + # The number of previously used passwords that can not be reused: + # config.password_previously_used_count = 8 + + # ==> Configuration for the Devise Secure Password extension + # Module: password_disallows_frequent_changes + # *Requires* password_disallows_frequent_reuse + # + # The minimum time that must pass between password changes: + # config.password_minimum_age = 1.days + + # ==> Configuration for the Devise Secure Password extension + # Module: password_requires_regular_updates + # *Requires* password_disallows_frequent_reuse + # + # The maximum allowed age of a password: + # config.password_maximum_age = 180.days +end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index f3ba72aef..f76e4a409 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -1,3 +1,5 @@ +require Rails.root.join('lib/redis/config') + schedule_file = 'config/schedule.yml' Sidekiq.configure_client do |config| diff --git a/config/installation_config.yml b/config/installation_config.yml index e0ca16637..3f14a9a26 100644 --- a/config/installation_config.yml +++ b/config/installation_config.yml @@ -36,3 +36,7 @@ - name: CHATWOOT_INBOX_HMAC_KEY value: locked: false +- name: API_CHANNEL_NAME + value: +- name: API_CHANNEL_THUMBNAIL + value: diff --git a/config/integration/apps.yml b/config/integration/apps.yml index 1c6276894..a9ae7b4a2 100644 --- a/config/integration/apps.yml +++ b/config/integration/apps.yml @@ -3,7 +3,7 @@ # logo: place the image in /public/dashboard/images/integrations and reference here # i18n_key: the key under which translations for the integration is placed in en.yml # action: if integration requires external redirect url -# hook_type: ( account / inbox ) +# hook_type: ( account / inbox ) # allow_multiple_hooks: whether multiple hooks can be created for the integration # settings_json_schema: the json schema used to validate the settings hash (https://json-schema.org/) # settings_form_schema: the formulate schema used in frontend to render settings form (https://vueformulate.com/) @@ -43,11 +43,38 @@ dialogflow: { "label": "Dialogflow Project ID", "type": "text", - "name": "project_id" + "name": "project_id", + "validation": "required", + "validationName": 'Project Id', }, { "label": "Dialogflow Project Key File", "type": "textarea", "name": "credentials", + "validation": "required|JSON", + "validationName": 'Credentials', + "validation-messages": { + "JSON": "Invalid JSON", + "required": "Credentials is required" + } } ] + visible_properties: ['project_id'] +fullcontact: + id: fullcontact + logo: fullcontact.png + i18n_key: fullcontact + action: /fullcontact + hook_type: account + allow_multiple_hooks: false + settings_json_schema: + { + 'type': 'object', + 'properties': { 'api_key': { 'type': 'string' } }, + 'required': ['api_key'], + 'additionalProperties': false, + } + settings_form_schema: + [{ 'label': 'API Key', 'type': 'text', 'name': 'api_key',"validation": "required", }] + visible_properties: ['api_key'] + diff --git a/config/locales/ar.yml b/config/locales/ar.yml index 173f7dfc0..df67dfd57 100644 --- a/config/locales/ar.yml +++ b/config/locales/ar.yml @@ -78,4 +78,7 @@ ar: description: "أحداث Webhook توفر لك معلومات في الوقت الحقيقي حول ما يحدث في حساب Chatwoot الخاص بك. يمكنك استخدام خاصية الـ Webhook لإيصال الأحداث إلى تطبيقاتك المفضلة مثل Slack أو Github. انقر على \"تهيئة\" لإعداد الـ Webhooks الخاصة بك." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/ca.yml b/config/locales/ca.yml index b1e668cff..ce64fa910 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -78,4 +78,7 @@ ca: description: "Els esdeveniments de Webhook us proporcionen informació en temps real sobre el que passa al vostre compte de Chatwoot. Podeu utilitzar els webhooks per comunicar els esdeveniments a les vostres aplicacions preferides com Slack o Github. Feu clic a Configura per configurar els enllaços web." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/cs.yml b/config/locales/cs.yml index c89ae1d28..8a5340eea 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -78,4 +78,7 @@ cs: description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/da.yml b/config/locales/da.yml index 2e44e61fc..2a2571850 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -78,4 +78,7 @@ da: description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/de.yml b/config/locales/de.yml index 7e3effdb4..54e0f04cb 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -78,4 +78,7 @@ de: description: "Webhook-Ereignisse bieten Ihnen Echtzeitinformationen darüber, was in Ihrem Chatwoot-Konto passiert. Sie können die Webhooks verwenden, um die Ereignisse an Ihre Lieblings-Apps wie Slack oder Github zu kommunizieren. Klicken Sie auf Konfigurieren, um Ihre Webhooks einzurichten." dialogflow: name: "Dialogflow" - description: "Schließen Sie Ihren Dialogflow-Bot an Ihren Posteingang an. Lassen Sie die Bots die Abfragen bearbeiten, bevor Sie ihn an den Kundendienst-Agent übergeben." + description: "Erstellen Sie Chatbots mit Dialogflow und verbinden Sie diesen mit Ihrem Posteingang. Lassen Sie die Bots Anfragen bearbeiten, bevor Sie an einen Kundendienst-Agent weitergeben werden." + fullcontact: + name: "Vollständiger Kontakt" + description: "Die vollständige Kontakt-Integration hilft die Besucherprofile zu erweitern. Identifizieren Sie die Benutzer, sobald diese ihre E-Mail-Adresse speichern um Ihnen maßgeschneiderten Kundenservice anbieten. Verbinden Sie einen Kontakt mit Ihrem Konto, indem Sie den Kontakt-API-Schlüssel freigeben." diff --git a/config/locales/devise.he.yml b/config/locales/devise.he.yml new file mode 100644 index 000000000..ac39c33ae --- /dev/null +++ b/config/locales/devise.he.yml @@ -0,0 +1,63 @@ +#Additional translations at https://github.com/plataformatec/devise/wiki/I18n +he: + devise: + confirmations: + confirmed: "כתובת המייל שלך אושרה בהצלחה." + send_instructions: "אתה תקבל הוראות לאישרור המייל לתיבת המייל שלך תוך כמה דקות." + send_paranoid_instructions: "אם המייל שלך רשום אצלינו, אתה תקבל מייל עם הוראות לאישרור כתובת המייל שלך תוך כמה דקות." + failure: + already_authenticated: "אתה כבר רשום." + inactive: "החשבון שלך עוד לא הופעל." + invalid: "Invalid %{authentication_keys}/password or account is not verified yet." + locked: "Your account is locked." + last_attempt: "You have one more attempt before your account is locked." + not_found_in_database: "Invalid %{authentication_keys} or password." + timeout: "Your session expired. Please sign in again to continue." + unauthenticated: "You need to sign in or sign up before continuing." + unconfirmed: "You have to confirm your email address before continuing." + mailer: + confirmation_instructions: + subject: "Confirmation Instructions" + reset_password_instructions: + subject: "Reset password instructions" + unlock_instructions: + subject: "Unlock instructions" + password_change: + subject: "Password Changed" + omniauth_callbacks: + failure: "Could not authenticate you from %{kind} because \"%{reason}\"." + success: "Successfully authenticated from %{kind} account." + passwords: + no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." + send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." + updated: "Your password has been changed successfully. You are now signed in." + updated_not_active: "Your password has been changed successfully." + registrations: + destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." + signed_up: "Welcome! You have signed up successfully." + signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." + signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." + signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." + update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address." + updated: "Your account has been updated successfully." + sessions: + signed_in: "Signed in successfully." + signed_out: "Signed out successfully." + already_signed_out: "Signed out successfully." + unlocks: + send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." + send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." + unlocked: "Your account has been unlocked successfully. Please sign in to continue." + errors: + messages: + already_confirmed: "was already confirmed, please try signing in" + confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" + expired: "has expired, please request a new one" + not_found: "not found" + not_locked: "was not locked" + not_saved: + one: "1 error prohibited this %{resource} from being saved:" + two: "%{count} errors prohibited this %{resource} from being saved:" + many: "%{count} errors prohibited this %{resource} from being saved:" + other: "%{count} errors prohibited this %{resource} from being saved:" diff --git a/config/locales/el.yml b/config/locales/el.yml index 67d75369f..fd8797c3b 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -79,3 +79,6 @@ el: dialogflow: name: "Dialogflow" description: "Συνδέστε το Dialogflow bot στα εισερχόμενά σας. Αφήστε τα bots να χειριστούν τα ερωτήματα πριν τα παραδώσουν στον πράκτορα εξυπηρέτησης πελατών." + fullcontact: + name: "Πλήρης Επαφή" + description: "Το FullContact Indegration βοηθά στον εμπλουτισμό προφίλ επισκεπτών. Προσδιορίστε τους χρήστες μόλις μοιραστούν τη διεύθυνση ηλεκτρονικού ταχυδρομείου τους και να τους προσφέρουν προσαρμοσμένη εξυπηρέτηση πελατών. Συνδέστε το FullContact με το λογαριασμό σας, κοινοποιώντας το κλειδί API FullContact." diff --git a/config/locales/en.yml b/config/locales/en.yml index ecea2c545..b4bd4357c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -95,4 +95,7 @@ en: description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/es.yml b/config/locales/es.yml index d767c6564..1a2fd3e22 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -78,4 +78,7 @@ es: description: "Los eventos Webhook le proporcionan información en tiempo real sobre lo que está sucediendo en su cuenta de Chatwoot. Puede hacer uso de los webhooks para comunicar los eventos a sus aplicaciones favoritas como Slack o Github. Haga clic en Configurar para configurar sus webhooks." dialogflow: name: "Dialogflow" - description: "Conecte su bot de Dialogflow a su bandeja de entrada. Deje que los bots manejen las preguntas antes de entregarlo al agente de servicio al cliente." + description: "Construya chatboots usando Dialogflow y conéctelos rápidamente a su bandeja de entrada. Permita que los bots gestionen las preguntas and de que pasen a los agentes de servicio al cliente." + fullcontact: + name: "Nombre completo" + description: "La integración con FullContact enriquece los perfiles de los visitantes. Identifique a los usuarios tan pronto compartan la dirección de correo electrónico y ofrezca un servicio al cliente personalizado. Conecte su cuenta de FullContact usando al clave de la API." diff --git a/config/locales/fa.yml b/config/locales/fa.yml index c54f88eee..fe4d296c9 100644 --- a/config/locales/fa.yml +++ b/config/locales/fa.yml @@ -30,7 +30,7 @@ fa: reports: period: زمان گزارش از %{since} تا %{until} agent_csv: - agent_name: Agent name + agent_name: اسم اپراتور conversations_count: Conversations count avg_first_response_time: Avg first response time (Minutes) avg_resolution_time: Avg resolution time (Minutes) @@ -78,4 +78,7 @@ fa: description: "رویدادهای Webhook اطلاعات واقعی در مورد آنچه در حساب شما اتفاق می افتد را به شما ارائه می دهند. برای برقراری ارتباط رویدادها با برنامه های مورد علاقه خود مانند Slack یا Github می توانید از وب بوک ها استفاده کنید. برای تنظیم webhooks خود روی تنظیمات کلیک کنید." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/fi.yml b/config/locales/fi.yml index f4e8acc24..ccaf7f360 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -78,4 +78,7 @@ fi: description: "Webhook-tapahtumat antavat sinulle reaaliaikaista tietoa siitä, mitä Chatwoot-tililläsi tapahtuu. Voit käyttää webhookeja ja välittää tapahtumat suosikkiohjelmillesi, kuten Slackiin tai Githubiiin. Klikkaa \"Määrittele\" määrittääksesi webhookisi." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/fr.yml b/config/locales/fr.yml index be61cd964..ce91d0f14 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -78,4 +78,7 @@ fr: description: "Les événements Webhook vous fournissent des informations en temps réel sur ce qui se passe dans votre compte. Vous pouvez utiliser les webhooks pour communiquer les événements à vos applications préférées comme Slack ou Github. Cliquez sur Configurer pour configurer vos webhooks." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/he.yml b/config/locales/he.yml new file mode 100644 index 000000000..f9522338a --- /dev/null +++ b/config/locales/he.yml @@ -0,0 +1,84 @@ +#Files in the config/locales directory are used for internationalization +#and are automatically loaded by Rails. If you want to use locales other +#than English, add the necessary files in this directory. +#To use the locales, use `I18n.t`: +#I18n.t 'hello' +#In views, this is aliased to just `t`: +#<%= t('hello') %> +#To use a different locale, set it with `I18n.locale`: +#I18n.locale = :es +#This would use the information in config/locales/es.yml. +#The following keys must be escaped otherwise they will not be retrieved by +#the default I18n backend: +#true, false, on, off, yes, no +#Instead, surround them with single quotes. +#en: +#'true': 'foo' +#To learn more, please read the Rails Internationalization guide +#available at https://guides.rubyonrails.org/i18n.html. +he: + hello: "שלום עולם" + messages: + reset_password_success: יאס! בקשה לאיפוס ססמה נשלחה בהצלחה. בדוק תיבת מייל להוראות. + reset_password_failure: אופס! לא מצאנו משתמש עם המייל שצוין. + errors: + signup: + disposable_email: אנחנו לא מאפשרים מיילים חד פעמיים + invalid_email: הכנסת מייל לא תקין + email_already_exists: "כבר נרשמת לחשבון עם %{email}" + failed: הרשמה נכשלה + reports: + period: Reporting period %{since} to %{until} + agent_csv: + agent_name: Agent name + conversations_count: Conversations count + avg_first_response_time: Avg first response time (Minutes) + avg_resolution_time: Avg resolution time (Minutes) + notifications: + notification_title: + conversation_creation: "[New conversation] - #%{display_id} has been created in %{inbox_name}" + conversation_assignment: "[Assigned to you] - #%{display_id} has been assigned to you" + assigned_conversation_new_message: "[New message] - #%{display_id} %{content}" + conversation_mention: "You have been mentioned in conversation [ID - %{display_id}] by %{name}" + conversations: + messages: + deleted: This message was deleted + activity: + status: + resolved: "Conversation was marked resolved by %{user_name}" + open: "Conversation was reopened by %{user_name}" + bot: "Conversation was transferred to bot by %{user_name}" + auto_resolved: "Conversation was marked resolved by system due to %{duration} days of inactivity" + assignee: + self_assigned: "%{user_name} self-assigned this conversation" + assigned: "Assigned to %{assignee_name} by %{user_name}" + removed: "Conversation unassigned by %{user_name}" + team: + assigned: "Assigned to %{team_name} by %{user_name}" + assigned_with_assignee: "Assigned to %{assignee_name} via %{team_name} by %{user_name}" + removed: "Unassigned from %{team_name} by %{user_name}" + labels: + added: "%{user_name} added %{labels}" + removed: "%{user_name} removed %{labels}" + muted: "%{user_name} has muted the conversation" + unmuted: "%{user_name} has unmuted the conversation" + templates: + greeting_message_body: "%{account_name} typically replies in a few hours." + ways_to_reach_you_message_body: "Give the team a way to reach you." + email_input_box_message_body: "Get notified by email" + reply: + email_subject: "New messages on this conversation" + transcript_subject: "Conversation Transcript" + integration_apps: + slack: + name: "Slack" + description: "Slack is a chat tool that brings all your communication together in one place. By integrating Slack, you can get notified of all the new conversations in your account right inside your Slack." + webhooks: + name: "Webhooks" + description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks." + dialogflow: + name: "Dialogflow" + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/hi.yml b/config/locales/hi.yml index e95095e69..47e62700a 100644 --- a/config/locales/hi.yml +++ b/config/locales/hi.yml @@ -78,4 +78,7 @@ hi: description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/hu.yml b/config/locales/hu.yml index 39fba0317..74e9033b2 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -78,4 +78,7 @@ hu: description: "A Webhook események valós idejű információt adnak arról, hogy mi történik a fiókodban. Webhookokat használhatsz arra, hogy az eseményeket a kedvenc appjaidban, pl. a Slackben vagy a Githubban használd. Kattints hogy beállíthast a Webhookjaidat." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/id.yml b/config/locales/id.yml index 17a3dafd0..d8718e188 100644 --- a/config/locales/id.yml +++ b/config/locales/id.yml @@ -30,10 +30,10 @@ id: reports: period: Periode pelaporan %{since} hingga %{until} agent_csv: - agent_name: Agent name - conversations_count: Conversations count - avg_first_response_time: Avg first response time (Minutes) - avg_resolution_time: Avg resolution time (Minutes) + agent_name: Nama agen + conversations_count: Jumlah percakapan + avg_first_response_time: Rata-rata waktu respons pertama (Menit) + avg_resolution_time: Rata-rata waktu resolusi (Menit) notifications: notification_title: conversation_creation: "[Percakapan baru] - #%{display_id} telah dibuat di %{inbox_name}" @@ -47,7 +47,7 @@ id: status: resolved: "Percakapan ditandai selesai oleh %{user_name}" open: "Percakapan telah dibuka kembali oleh %{user_name}" - bot: "Conversation was transferred to bot by %{user_name}" + bot: "Percakapan ditransfer ke bot oleh %{user_name}" auto_resolved: "Percakapan ditandai terselesaikan oleh sistem karena tidak ada aktifitas dalam %{duration} hari" assignee: self_assigned: "%{user_name} menetapkan sendiri percakapan ini" @@ -55,8 +55,8 @@ id: removed: "Percakapan batal ditetapkan oleh %{user_name}" team: assigned: "Ditugaskan ke %{team_name} oleh %{user_name}" - assigned_with_assignee: "Assigned to %{assignee_name} via %{team_name} by %{user_name}" - removed: "Unassigned from %{team_name} by %{user_name}" + assigned_with_assignee: "Ditugaskan ke %{assignee_name} melalui %{team_name} oleh %{user_name}" + removed: "Dibebastugaskan dari %{team_name} oleh %{user_name}" labels: added: "%{user_name} menambahkan %{labels}" removed: "%{user_name} menghapus %{labels}" @@ -78,4 +78,7 @@ id: description: "Webhook event memberi Anda informasi realtime tentang apa yang terjadi di akun Anda. Anda dapat menggunakan webhook untuk mengkomunikasikan acara ke aplikasi favorit Anda seperti Slack atau GitHub. Klik Konfigurasi untuk mengatur webhook Anda." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/it.yml b/config/locales/it.yml index 2f3a35229..71c6e5dd2 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -78,4 +78,7 @@ it: description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 2363623c9..3f78cef21 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -78,4 +78,7 @@ ja: description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/ko.yml b/config/locales/ko.yml index 3855ac9bb..0003ac300 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -64,8 +64,8 @@ ko: unmuted: "%{user_name} has unmuted the conversation" templates: greeting_message_body: "%{account_name} typically replies in a few hours." - ways_to_reach_you_message_body: "Give the team a way to reach you." - email_input_box_message_body: "Get notified by email" + ways_to_reach_you_message_body: "저희가 연락드릴 방법을 알려주세요." + email_input_box_message_body: "이메일로 연락받기" reply: email_subject: "New messages on this conversation" transcript_subject: "Conversation Transcript" @@ -78,4 +78,7 @@ ko: description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/ml.yml b/config/locales/ml.yml index f0468010e..2ab817f9d 100644 --- a/config/locales/ml.yml +++ b/config/locales/ml.yml @@ -78,4 +78,7 @@ ml: description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/ne.yml b/config/locales/ne.yml index 99d2afd98..91970c298 100644 --- a/config/locales/ne.yml +++ b/config/locales/ne.yml @@ -78,4 +78,7 @@ ne: description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 76c42c075..4130041b0 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -78,4 +78,7 @@ nl: description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/no.yml b/config/locales/no.yml index d8a99d271..cf041e5f1 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -78,4 +78,7 @@ description: "Webhook-hendelser gir deg sanntidsinformasjon om hva som skjer med kontoen din. Du kan bruke webhooks for å sende hendelsene til favorittappene dine, som Slack eller Github. Klikk på Konfigurer for å sette opp webhooks." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 38fada5d3..4bca06934 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -78,4 +78,7 @@ pl: description: "Webhooki dostarczają informacji o tym, co dzieje się na Twoim koncie do usług zewnętrznych. Możesz wykorzystać webhooki do przekazywania wydarzeń do ulubionych aplikacji, takich jak Slack lub Github. Kliknij na Konfiguruj, aby skonfigurować webhooki." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/pt.yml b/config/locales/pt.yml index dc41ca866..0c84171b9 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -78,4 +78,7 @@ pt: description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml index a05d1400a..adf67c14d 100644 --- a/config/locales/pt_BR.yml +++ b/config/locales/pt_BR.yml @@ -77,5 +77,8 @@ pt_BR: name: "Webhooks" description: "Webhooks fornecem informações em tempo real sobre o que está acontecendo em sua conta. Você pode usar os webhooks para comunicar eventos com seus aplicativos favoritos como Slack ou Github. Clique em Configurar para configurar seus webhooks." dialogflow: - name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + name: "Fluxo de diálogo" + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Contato completo" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/ro.yml b/config/locales/ro.yml index 17d69bea5..6449fa303 100644 --- a/config/locales/ro.yml +++ b/config/locales/ro.yml @@ -78,4 +78,7 @@ ro: description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/ru.yml b/config/locales/ru.yml index ff3f8693c..8762cc471 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -78,4 +78,7 @@ ru: description: "События webhook позволяют получать информацию о происходящем в вашем аккаунте в реальном времени. Вы можете использовать webhook в связке с вашими приложениями, такими как Slack или Github. Нажмите на Настроить для настройки webhook." dialogflow: name: "Диалог" - description: "Подключите бот к диалогам вашего канала. Предоставьте ботам доступ к обработке запросов, прежде чем для предварительного общения с вашими операторами." + description: "Создавайте чатботы используя Dialogflow и быстро подключайте их к входящим каналам. Пусть боты обрабатывают запросы, прежде чем передать их агенту по обслуживанию клиентов." + fullcontact: + name: "Полная карточка контакта" + description: "Интеграция с FullContact помогает получить всю информацию о профиле посетителей. Определяйте пользователей как только они поделится своим электронным адресом и предложите им индивидуальное обслуживание клиентов. Подключите ваш FullContact к своей учетной записи, поделившись API ключом FullContact." diff --git a/config/locales/secure_password.en.yml b/config/locales/secure_password.en.yml new file mode 100644 index 000000000..ad8bbd074 --- /dev/null +++ b/config/locales/secure_password.en.yml @@ -0,0 +1,74 @@ +en: + secure_password: + character: + one: "character" + other: "characters" + + types: + uppercase: "uppercase" + downcase: "downcase" + lowercase: "lowercase" + number: "number" + special: "special" + + password_has_required_content: + errors: + messages: + unknown_characters: "contains %{count} invalid %{subject}" + minimum_characters: "must contain at least %{count} %{type} %{subject}" + maximum_characters: "must contain less than %{count} %{type} %{subject}" + minimum_length: "must contain at least %{count} %{subject}" + maximum_length: "must contain less than %{count} %{subject}" + password_disallows_frequent_reuse: + errors: + messages: + password_is_recent: "Last %{count} passwords may not be reused" + password_disallows_frequent_changes: + errors: + messages: + password_is_recent: "Password cannot be changed more than once per %{timeframe}" + password_requires_regular_updates: + alerts: + messages: + password_updated: "Your password has been updated." + errors: + messages: + password_expired: "Your password has expired. Passwords must be changed every %{timeframe}" + datetime: + # update distance_in_words translations to remove the determiner words: + # about, almost, over, less than, etc. + precise_distance_in_words: + half_a_minute: "half a minute" + less_than_x_seconds: + one: "1 second" # default was: "less than 1 second" + other: "%{count} seconds" # default was: "less than %{count} seconds" + x_seconds: + one: "1 second" + other: "%{count} seconds" + less_than_x_minutes: + one: "a minute" # default was: "less than a minute" + other: "%{count} minutes" # default was: "less than %{count} minutes" + x_minutes: + one: "1 minute" + other: "%{count} minutes" + about_x_hours: + one: "1 hour" # default was: "about 1 hour" + other: "%{count} hours" # default was: "about %{count} hours" + x_days: + one: "1 day" + other: "%{count} days" + about_x_months: + one: "1 month" # default was: "about 1 month" + other: "%{count} months" # default was: "about %{count} months" + x_months: + one: "1 month" + other: "%{count} months" + about_x_years: + one: "1 year" # default was: "about 1 year" + other: "%{count} years" # default was: "about %{count} years" + over_x_years: + one: "1 year" # default was: "over 1 year" + other: "%{count} years" # default was: "over %{count} years" + almost_x_years: + one: "1 year" # default was: "almost 1 year" + other: "%{count} years" # default was: "almost %{count} years" \ No newline at end of file diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 536a7ee66..2fee5e528 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -78,4 +78,7 @@ sk: description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 7f79452e4..83a198290 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -78,4 +78,7 @@ sv: description: "Webhook-händelser ger dig realtidsinformation om vad som händer i ditt konto. Du kan använda webhooks för att kommunicera händelser till dina favoritappar som Slack eller Github. Klicka på Konfigurera för att konfigurera dina webhooks." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/ta.yml b/config/locales/ta.yml index 7f9426cfc..96f7f7302 100644 --- a/config/locales/ta.yml +++ b/config/locales/ta.yml @@ -78,4 +78,7 @@ ta: description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/th.yml b/config/locales/th.yml index 033ad1621..277f8826c 100644 --- a/config/locales/th.yml +++ b/config/locales/th.yml @@ -78,4 +78,7 @@ th: description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 1f57cc436..29fcaa07c 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -78,4 +78,7 @@ tr: description: "Webhook olayları size hesabınızda gerçekleşen gerçek zamanlı bilgileri getirmenizi sağlar. Bu webhookları kullanarak olaylar ile favori uygulamalarınızı haberleştirebilirsiniz(ör: Slack , \n Github). Yapılandıra basarak webhooklarınızı ayarlayabilirsiniz." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/uk.yml b/config/locales/uk.yml index df363f271..c205a8328 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -78,4 +78,7 @@ uk: description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/vi.yml b/config/locales/vi.yml index 6e05c3011..fdd538e19 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -78,4 +78,7 @@ vi: description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/zh_CN.yml b/config/locales/zh_CN.yml index 6caff7a98..57c94886e 100644 --- a/config/locales/zh_CN.yml +++ b/config/locales/zh_CN.yml @@ -78,4 +78,7 @@ zh_CN: description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/zh_TW.yml b/config/locales/zh_TW.yml index cc2f1bdd5..3cda18b9d 100644 --- a/config/locales/zh_TW.yml +++ b/config/locales/zh_TW.yml @@ -36,39 +36,39 @@ zh_TW: avg_resolution_time: 平均解決時間(分鐘) notifications: notification_title: - conversation_creation: "[New conversation] - #%{display_id} has been created in %{inbox_name}" - conversation_assignment: "[Assigned to you] - #%{display_id} has been assigned to you" - assigned_conversation_new_message: "[New message] - #%{display_id} %{content}" - conversation_mention: "You have been mentioned in conversation [ID - %{display_id}] by %{name}" + conversation_creation: "[新對話] - #%{display_id} 已經在 %{inbox_name} 中被建立" + conversation_assignment: "[已指派給你] - #%{display_id} 已經被指派給你" + assigned_conversation_new_message: "[新訊息] - #%{display_id} %{content}" + conversation_mention: "你在對話 %{name} 的 [ID - %{display_id}] 中被提及" conversations: messages: - deleted: This message was deleted + deleted: 訊息已被刪除 activity: status: resolved: "被%{user_name}標記的對話已解決。" open: "被%{user_name}恢復對話。" - bot: "Conversation was transferred to bot by %{user_name}" - auto_resolved: "Conversation was marked resolved by system due to %{duration} days of inactivity" + bot: "對話已經被 %{user_name} 轉換至機器人模式" + auto_resolved: "由於對話已經 %{duration} 天沒有新活動,已經被系統標記為完成" assignee: - self_assigned: "%{user_name} self-assigned this conversation" + self_assigned: "%{user_name} 將對話指派給自己" assigned: "被%{user_name}分配給%{assignee_name}。" removed: "對話被%{user_name}設定成未分配。" team: assigned: "被%{user_name}分配給%{team_name}。" assigned_with_assignee: "Assigned to %{assignee_name} via %{team_name} by %{user_name}" - removed: "Unassigned from %{team_name} by %{user_name}" + removed: "被 %{user_name} 從 %{team_name} 解除指派" labels: - added: "%{user_name} added %{labels}" - removed: "%{user_name} removed %{labels}" - muted: "%{user_name} has muted the conversation" - unmuted: "%{user_name} has unmuted the conversation" + added: "%{user_name} 新增了 %{labels}" + removed: "%{user_name} 移除了 %{labels}" + muted: "%{user_name} 已將對話靜音" + unmuted: "%{user_name} 將對話解除靜音" templates: - greeting_message_body: "%{account_name} typically replies in a few hours." + greeting_message_body: "%{account_name} 通常在幾小時內回覆" ways_to_reach_you_message_body: "給個聯繫方式讓團隊可以聯繫到您。" email_input_box_message_body: "透過電子郵件得到通知。" reply: - email_subject: "New messages on this conversation" - transcript_subject: "Conversation Transcript" + email_subject: "在對話中的新訊息" + transcript_subject: "對話紀錄" integration_apps: slack: name: "Slack" @@ -78,4 +78,7 @@ zh_TW: description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks." dialogflow: name: "Dialogflow" - description: "Connect your Dialogflow bot to your inbox. Let the bots handle the queries before handing it off to the customer service agent." + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/routes.rb b/config/routes.rb index 140e19400..82876ceba 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -8,14 +8,19 @@ Rails.application.routes.draw do token_validations: 'devise_overrides/token_validations' }, via: [:get, :post] - root to: 'dashboard#index' + ## renders the frontend paths only if its not an api only server + if ActiveModel::Type::Boolean.new.cast(ENV.fetch('CW_API_ONLY_SERVER', false)) + root to: 'api#index' + else + root to: 'dashboard#index' - get '/app', to: 'dashboard#index' - get '/app/*params', to: 'dashboard#index' - get '/app/accounts/:account_id/settings/inboxes/new/twitter', to: 'dashboard#index', as: 'app_new_twitter_inbox' - get '/app/accounts/:account_id/settings/inboxes/new/:inbox_id/agents', to: 'dashboard#index', as: 'app_twitter_inbox_agents' + get '/app', to: 'dashboard#index' + get '/app/*params', to: 'dashboard#index' + get '/app/accounts/:account_id/settings/inboxes/new/twitter', to: 'dashboard#index', as: 'app_new_twitter_inbox' + get '/app/accounts/:account_id/settings/inboxes/new/:inbox_id/agents', to: 'dashboard#index', as: 'app_twitter_inbox_agents' - resource :widget, only: [:show] + resource :widget, only: [:show] + end get '/api', to: 'api#index' namespace :api, defaults: { format: 'json' } do @@ -33,6 +38,8 @@ Rails.application.routes.draw do end resources :agents, except: [:show, :edit, :new] + resources :agent_bots, only: [:index, :create, :show, :update, :destroy] + resources :callbacks, only: [] do collection do post :register_facebook_page @@ -42,7 +49,7 @@ Rails.application.routes.draw do end end resources :canned_responses, except: [:show, :edit, :new] - resources :campaigns, only: [:index, :create, :show, :update] + resources :campaigns, only: [:index, :create, :show, :update, :destroy] namespace :channels do resource :twilio_channel, only: [:create] @@ -81,17 +88,10 @@ Rails.application.routes.draw do end end - resources :facebook_indicators, only: [] do - collection do - post :mark_seen - post :typing_on - post :typing_off - end - end - resources :inboxes, only: [:index, :create, :update, :destroy] do get :assignable_agents, on: :member get :campaigns, on: :member + get :agent_bot, on: :member post :set_agent_bot, on: :member end resources :inbox_members, only: [:create, :show], param: :inbox_id @@ -146,8 +146,6 @@ Rails.application.routes.draw do resource :profile, only: [:show, :update] resource :notification_subscriptions, only: [:create] - resources :agent_bots, only: [:index] - namespace :widget do resources :campaigns, only: [:index] resources :events, only: [:create] @@ -189,6 +187,7 @@ Rails.application.routes.draw do get :login end end + resources :agent_bots, only: [:index, :create, :show, :update, :destroy] resources :accounts, only: [:create, :show, :update, :destroy] do resources :account_users, only: [:index, :create] do collection do @@ -200,6 +199,24 @@ Rails.application.routes.draw do end end + # ---------------------------------------------------------------------- + # Routes for inbox APIs Exposed to contacts + namespace :public, defaults: { format: 'json' } do + namespace :api do + namespace :v1 do + resources :inboxes do + scope module: :inboxes do + resources :contacts, only: [:create, :show, :update] do + resources :conversations, only: [:index, :create] do + resources :messages, only: [:index, :create, :update] + end + end + end + end + end + end + end + # ---------------------------------------------------------------------- # Used in mailer templates resource :app, only: [:index] do @@ -225,6 +242,7 @@ Rails.application.routes.draw do # ---------------------------------------------------------------------- # Routes for external service verifications get 'apple-app-site-association' => 'apple_app#site_association' + get '.well-known/assetlinks.json' => 'android_app#assetlinks' # ---------------------------------------------------------------------- # Internal Monitoring Routes diff --git a/db/migrate/20210520200729_add_account_id_to_agent_bots.rb b/db/migrate/20210520200729_add_account_id_to_agent_bots.rb new file mode 100644 index 000000000..cd4a26073 --- /dev/null +++ b/db/migrate/20210520200729_add_account_id_to_agent_bots.rb @@ -0,0 +1,6 @@ +class AddAccountIdToAgentBots < ActiveRecord::Migration[6.0] + def change + remove_column :agent_bots, :hide_input_for_bot_conversations, :boolean + add_reference :agent_bots, :account, foreign_key: true + end +end diff --git a/db/migrate/20210527173755_remove_super_admin_access_tokes.rb b/db/migrate/20210527173755_remove_super_admin_access_tokes.rb new file mode 100644 index 000000000..18ad7d4f8 --- /dev/null +++ b/db/migrate/20210527173755_remove_super_admin_access_tokes.rb @@ -0,0 +1,5 @@ +class RemoveSuperAdminAccessTokes < ActiveRecord::Migration[6.0] + def change + AccessToken.where(owner_type: 'SuperAdmin').destroy_all + end +end diff --git a/db/migrate/20210602182058_add_hmac_to_api_channel.rb b/db/migrate/20210602182058_add_hmac_to_api_channel.rb new file mode 100644 index 000000000..f441c2829 --- /dev/null +++ b/db/migrate/20210602182058_add_hmac_to_api_channel.rb @@ -0,0 +1,19 @@ +class AddHmacToApiChannel < ActiveRecord::Migration[6.0] + def change + add_column :channel_api, :identifier, :string + add_index :channel_api, :identifier, unique: true + add_column :channel_api, :hmac_token, :string + add_index :channel_api, :hmac_token, unique: true + add_column :channel_api, :hmac_mandatory, :boolean, default: false + add_column :channel_web_widgets, :hmac_mandatory, :boolean, default: false + set_up_existing_api_channels + end + + def set_up_existing_api_channels + ::Channel::Api.find_in_batches do |api_channels_batch| + Rails.logger.info "migrated till #{api_channels_batch.first.id}\n" + api_channels_batch.map(&:regenerate_hmac_token) + api_channels_batch.map(&:regenerate_identifier) + end + end +end diff --git a/db/migrate/20210609133433_add_email_collect_to_inboxes.rb b/db/migrate/20210609133433_add_email_collect_to_inboxes.rb new file mode 100644 index 000000000..f4da1f73e --- /dev/null +++ b/db/migrate/20210609133433_add_email_collect_to_inboxes.rb @@ -0,0 +1,5 @@ +class AddEmailCollectToInboxes < ActiveRecord::Migration[6.0] + def change + add_column :inboxes, :enable_email_collect, :boolean, default: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 60f080d5c..4edb50157 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.define(version: 2021_05_13_083044) do +ActiveRecord::Schema.define(version: 2021_06_09_133433) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" @@ -97,7 +97,8 @@ ActiveRecord::Schema.define(version: 2021_05_13_083044) do t.string "outgoing_url" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false - t.boolean "hide_input_for_bot_conversations", default: false + t.bigint "account_id" + t.index ["account_id"], name: "index_agent_bots_on_account_id" end create_table "attachments", id: :serial, force: :cascade do |t| @@ -142,6 +143,11 @@ ActiveRecord::Schema.define(version: 2021_05_13_083044) do t.string "webhook_url" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false + t.string "identifier" + t.string "hmac_token" + t.boolean "hmac_mandatory", default: false + t.index ["hmac_token"], name: "index_channel_api_on_hmac_token", unique: true + t.index ["identifier"], name: "index_channel_api_on_identifier", unique: true end create_table "channel_email", force: :cascade do |t| @@ -200,6 +206,7 @@ ActiveRecord::Schema.define(version: 2021_05_13_083044) do t.string "hmac_token" t.boolean "pre_chat_form_enabled", default: false t.jsonb "pre_chat_form_options", default: {} + t.boolean "hmac_mandatory", default: false t.index ["hmac_token"], name: "index_channel_web_widgets_on_hmac_token", unique: true t.index ["website_token"], name: "index_channel_web_widgets_on_website_token", unique: true end @@ -321,6 +328,7 @@ ActiveRecord::Schema.define(version: 2021_05_13_083044) do t.boolean "working_hours_enabled", default: false t.string "out_of_office_message" t.string "timezone", default: "UTC" + t.boolean "enable_email_collect", default: true t.index ["account_id"], name: "index_inboxes_on_account_id" end @@ -611,6 +619,7 @@ ActiveRecord::Schema.define(version: 2021_05_13_083044) do add_foreign_key "account_users", "accounts" add_foreign_key "account_users", "users" add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" + add_foreign_key "agent_bots", "accounts" add_foreign_key "campaigns", "accounts" add_foreign_key "campaigns", "inboxes" add_foreign_key "contact_inboxes", "contacts" diff --git a/db/seeds.rb b/db/seeds.rb index 115a27108..6fb8b7970 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -10,13 +10,17 @@ end ## Seeds for Local Development unless Rails.env.production? - SuperAdmin.create!(email: 'john@acme.inc', password: '123456') + SuperAdmin.create!(email: 'john@acme.inc', password: 'Password1!') account = Account.create!( name: 'Acme Inc' ) - user = User.new(name: 'John', email: 'john@acme.inc', password: '123456') + secondary_account = Account.create!( + name: 'Acme Org' + ) + + user = User.new(name: 'John', email: 'john@acme.inc', password: 'Password1!') user.skip_confirmation! user.save! @@ -26,6 +30,18 @@ unless Rails.env.production? role: :administrator ) + AccountUser.create!( + account_id: secondary_account.id, + user_id: user.id, + role: :administrator + ) + + # Enables creating additional accounts from dashboard + installation_config = InstallationConfig.find_by(name: 'CREATE_NEW_ACCOUNT_FROM_DASHBOARD') + installation_config.value = true + installation_config.save! + GlobalConfig.clear_cache + web_widget = Channel::WebWidget.create!(account: account, website_url: 'https://acme.inc') inbox = Inbox.create!(channel: web_widget, account: account, name: 'Acme Support') diff --git a/docker-compose.production.yaml b/docker-compose.production.yaml index 129c93ff4..bda73a089 100644 --- a/docker-compose.production.yaml +++ b/docker-compose.production.yaml @@ -44,6 +44,8 @@ services: redis: image: redis:alpine restart: always + command: ["sh", "-c", "redis-server --requirepass \"$REDIS_PASSWORD\""] + env_file: .env volumes: - /data/redis:/data ports: diff --git a/lib/woot_message_seeder.rb b/lib/woot_message_seeder.rb index 728b60019..7e9aaebe1 100644 --- a/lib/woot_message_seeder.rb +++ b/lib/woot_message_seeder.rb @@ -29,19 +29,19 @@ module WootMessageSeeder def self.sample_card_item { - "media_url": 'https://i.imgur.com/d8Djr4k.jpg', - "title": 'Acme Shoes 2.0', - "description": 'Move with Acme Shoe 2.0', - "actions": [ + media_url: 'https://i.imgur.com/d8Djr4k.jpg', + title: 'Acme Shoes 2.0', + description: 'Move with Acme Shoe 2.0', + actions: [ { - "type": 'link', - "text": 'View More', - "uri": 'http://acme-shoes.inc' + type: 'link', + text: 'View More', + uri: 'http://acme-shoes.inc' }, { - "type": 'postback', - "text": 'Add to cart', - "payload": 'ITEM_SELECTED' + type: 'postback', + text: 'Add to cart', + payload: 'ITEM_SELECTED' } ] } @@ -56,11 +56,11 @@ module WootMessageSeeder content: 'Your favorite food', content_type: 'input_select', content_attributes: { - "items": [ - { "title": '🌯 Burito', "value": 'Burito' }, - { "title": '🍝 Pasta', "value": 'Pasta' }, - { "title": ' 🍱 Sushi', "value": 'Sushi' }, - { "title": ' 🥗 Salad', "value": 'Salad' } + items: [ + { title: '🌯 Burito', value: 'Burito' }, + { title: '🍝 Pasta', value: 'Pasta' }, + { title: ' 🍱 Sushi', value: 'Sushi' }, + { title: ' 🥗 Salad', value: 'Salad' } ] } ) @@ -74,16 +74,25 @@ module WootMessageSeeder message_type: :template, content_type: 'form', content: 'form', - content_attributes: { - "items": [ - { "name": 'email', "placeholder": 'Please enter your email', "type": 'email', "label": 'Email' }, - { "name": 'text_aread', "placeholder": 'Please enter text', "type": 'text_area', "label": 'Large Text' }, - { "name": 'text', "placeholder": 'Please enter text', "type": 'text', "label": 'text', "default": 'defaut value' } - ] - } + content_attributes: sample_form ) end + def self.sample_form + { + "items": [ + { "name": 'email', "placeholder": 'Please enter your email', "type": 'email', "label": 'Email', "required": 'required', + "pattern_error": 'Please fill this field', "pattern": '^[^\s@]+@[^\s@]+\.[^\s@]+$' }, + { "name": 'text_area', "placeholder": 'Please enter text', "type": 'text_area', "label": 'Large Text', "required": 'required', + "pattern_error": 'Please fill this field' }, + { "name": 'text', "placeholder": 'Please enter text', "type": 'text', "label": 'text', "default": 'defaut value', "required": 'required', + "pattern": '^[a-zA-Z ]*$', "pattern_error": 'Only alphabets are allowed' }, + { "name": 'select', "label": 'Select Option', "type": 'select', "options": [{ "label": '🌯 Burito', "value": 'Burito' }, + { "label": '🍝 Pasta', "value": 'Pasta' }] } + ] + } + end + def self.create_sample_articles_message(conversation) Message.create!( account: conversation.account, @@ -93,9 +102,9 @@ module WootMessageSeeder content: 'Tech Companies', content_type: 'article', content_attributes: { - "items": [ - { "title": 'Acme Hardware', "description": 'Hardware reimagined', "link": 'http://acme-hardware.inc' }, - { "title": 'Acme Search', "description": 'The best Search Engine', "link": 'http://acme-search.inc' } + items: [ + { title: 'Acme Hardware', description: 'Hardware reimagined', link: 'http://acme-hardware.inc' }, + { title: 'Acme Search', description: 'The best Search Engine', link: 'http://acme-search.inc' } ] } ) diff --git a/package.json b/package.json index 43766f3de..501cec447 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@chatwoot/chatwoot", - "version": "1.16.2", + "version": "1.17.0", "license": "MIT", "scripts": { "eslint": "eslint app/javascript --fix", @@ -15,10 +15,13 @@ "build-storybook": "build-storybook" }, "dependencies": { + "@braid/vue-formulate": "^2.5.2", "@chatwoot/prosemirror-schema": "https://github.com/chatwoot/prosemirror-schema.git#7e8acadd10d7b932c0dc0bd0a18f804434f83517", + "@chatwoot/utils": "^0.0.3", "@rails/actioncable": "6.1.3", "@rails/webpacker": "5.3.0", - "@sentry/vue": "^5.30.0", + "@sentry/tracing": "^6.4.1", + "@sentry/vue": "^6.4.1", "axios": "^0.21.1", "babel-plugin-syntax-jsx": "^6.18.0", "babel-plugin-transform-vue-jsx": "^3.7.0", diff --git a/public/dashboard/images/integrations/fullcontact.png b/public/dashboard/images/integrations/fullcontact.png new file mode 100644 index 000000000..1e5d9b7f9 Binary files /dev/null and b/public/dashboard/images/integrations/fullcontact.png differ diff --git a/spec/controllers/android_assetlinks_spec.rb b/spec/controllers/android_assetlinks_spec.rb new file mode 100644 index 000000000..a44ef111a --- /dev/null +++ b/spec/controllers/android_assetlinks_spec.rb @@ -0,0 +1,10 @@ +require 'rails_helper' + +describe '.well-known/assetlinks.json', type: :request do + describe 'GET /.well-known/assetlinks.json' do + it 'successfully retrieves assetlinks.json file' do + get '/.well-known/assetlinks.json' + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/api/v1/accounts/agent_bots_controller_spec.rb b/spec/controllers/api/v1/accounts/agent_bots_controller_spec.rb new file mode 100644 index 000000000..60442e6d6 --- /dev/null +++ b/spec/controllers/api/v1/accounts/agent_bots_controller_spec.rb @@ -0,0 +1,180 @@ +require 'rails_helper' + +RSpec.describe 'Agent Bot API', type: :request do + let!(:account) { create(:account) } + let!(:agent_bot) { create(:agent_bot, account: account) } + let(:admin) { create(:user, account: account, role: :administrator) } + let(:agent) { create(:user, account: account, role: :agent) } + + describe 'GET /api/v1/accounts/{account.id}/agent_bots' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + get "/api/v1/accounts/#{account.id}/agent_bots" + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + it 'returns all the agent_bots in account along with global agent bots' do + global_bot = create(:agent_bot) + get "/api/v1/accounts/#{account.id}/agent_bots", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + expect(response.body).to include(agent_bot.name) + expect(response.body).to include(global_bot.name) + end + end + end + + describe 'GET /api/v1/accounts/{account.id}/agent_bots/:id' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + get "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}" + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + it 'shows the agent bot' do + get "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + expect(response.body).to include(agent_bot.name) + end + + it 'will show a global agent bot' do + global_bot = create(:agent_bot) + get "/api/v1/accounts/#{account.id}/agent_bots/#{global_bot.id}", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + expect(response.body).to include(global_bot.name) + end + end + end + + describe 'POST /api/v1/accounts/{account.id}/agent_bots' do + let(:valid_params) { { name: 'test' } } + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + expect { post "/api/v1/accounts/#{account.id}/agent_bots", params: valid_params }.to change(Label, :count).by(0) + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + it 'creates the agent bot when administrator' do + expect do + post "/api/v1/accounts/#{account.id}/agent_bots", headers: admin.create_new_auth_token, + params: valid_params + end.to change(AgentBot, :count).by(1) + + expect(response).to have_http_status(:success) + end + + it 'would not create the agent bot when agent' do + expect do + post "/api/v1/accounts/#{account.id}/agent_bots", headers: agent.create_new_auth_token, + params: valid_params + end.to change(AgentBot, :count).by(0) + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'PATCH /api/v1/accounts/{account.id}/agent_bots/:id' do + let(:valid_params) { { name: 'test_updated' } } + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + patch "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}", + params: valid_params + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + it 'updates the agent bot' do + patch "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}", + headers: admin.create_new_auth_token, + params: valid_params, + as: :json + + expect(response).to have_http_status(:success) + expect(agent_bot.reload.name).to eq('test_updated') + end + + it 'would not update the agent bot when agent' do + patch "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}", + headers: agent.create_new_auth_token, + params: valid_params, + as: :json + + expect(response).to have_http_status(:unauthorized) + expect(agent_bot.reload.name).not_to eq('test_updated') + end + + it 'would not update a global agent bot' do + global_bot = create(:agent_bot) + patch "/api/v1/accounts/#{account.id}/agent_bots/#{global_bot.id}", + headers: admin.create_new_auth_token, + params: valid_params, + as: :json + + expect(response).to have_http_status(:not_found) + expect(agent_bot.reload.name).not_to eq('test_updated') + end + end + end + + describe 'DELETE /api/v1/accounts/{account.id}/agent_bots/:id' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + delete "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}" + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + it 'deletes an agent bot when administrator' do + delete "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}", + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + expect(account.agent_bots.size).to eq(0) + end + + it 'would not delete the agent bot when agent' do + delete "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + expect(account.agent_bots.size).not_to eq(0) + end + + it 'would not delete a global agent bot' do + global_bot = create(:agent_bot) + delete "/api/v1/accounts/#{account.id}/agent_bots/#{global_bot.id}", + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:not_found) + expect(account.agent_bots.size).not_to eq(0) + end + end + end +end diff --git a/spec/controllers/api/v1/accounts/agents_controller_spec.rb b/spec/controllers/api/v1/accounts/agents_controller_spec.rb index f0efad1e1..832840b34 100644 --- a/spec/controllers/api/v1/accounts/agents_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/agents_controller_spec.rb @@ -2,6 +2,8 @@ require 'rails_helper' RSpec.describe 'Agents API', type: :request do let(:account) { create(:account) } + let(:admin) { create(:user, account: account, role: :administrator) } + let(:agent) { create(:user, account: account, role: :agent) } describe 'GET /api/v1/accounts/{account.id}/agents' do context 'when it is an unauthenticated user' do @@ -38,7 +40,13 @@ RSpec.describe 'Agents API', type: :request do end context 'when it is an authenticated user' do - let(:admin) { create(:user, account: account, role: :administrator) } + it 'returns unauthorized for agents' do + delete "/api/v1/accounts/#{account.id}/agents/#{other_agent.id}", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end it 'deletes an agent' do delete "/api/v1/accounts/#{account.id}/agents/#{other_agent.id}", @@ -63,10 +71,17 @@ RSpec.describe 'Agents API', type: :request do end context 'when it is an authenticated user' do - let(:admin) { create(:user, account: account, role: :administrator) } - params = { name: 'TestUser' } + it 'returns unauthorized for agents' do + put "/api/v1/accounts/#{account.id}/agents/#{other_agent.id}", + params: params, + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + it 'modifies an agent' do put "/api/v1/accounts/#{account.id}/agents/#{other_agent.id}", params: params, @@ -91,10 +106,17 @@ RSpec.describe 'Agents API', type: :request do end context 'when it is an authenticated user' do - let(:admin) { create(:user, account: account, role: :administrator) } - params = { name: 'NewUser', email: Faker::Internet.email, role: :agent } + it 'returns unauthorized for agents' do + post "/api/v1/accounts/#{account.id}/agents", + params: params, + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + it 'creates a new agent' do post "/api/v1/accounts/#{account.id}/agents", params: params, diff --git a/spec/controllers/api/v1/accounts/campaigns_controller_spec.rb b/spec/controllers/api/v1/accounts/campaigns_controller_spec.rb index 5a90929d5..ebe7d4dae 100644 --- a/spec/controllers/api/v1/accounts/campaigns_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/campaigns_controller_spec.rb @@ -145,4 +145,40 @@ RSpec.describe 'Campaigns API', type: :request do end end end + + describe 'DELETE /api/v1/accounts/{account.id}/campaigns/:id' do + let(:inbox) { create(:inbox, account: account) } + let!(:campaign) { create(:campaign, account: account) } + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + delete "/api/v1/accounts/#{account.id}/campaigns/#{campaign.display_id}", + as: :json + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + let(:agent) { create(:user, account: account, role: :agent) } + let(:administrator) { create(:user, account: account, role: :administrator) } + + it 'return unauthorized if agent' do + delete "/api/v1/accounts/#{account.id}/campaigns/#{campaign.display_id}", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + + it 'delete campaign if admin' do + delete "/api/v1/accounts/#{account.id}/campaigns/#{campaign.display_id}", + headers: administrator.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + expect(::Campaign.exists?(campaign.display_id)).to eq false + end + end + end end diff --git a/spec/controllers/api/v1/accounts/contacts/contact_inboxes_controller_spec.rb b/spec/controllers/api/v1/accounts/contacts/contact_inboxes_controller_spec.rb index 6bf741347..80de09c40 100644 --- a/spec/controllers/api/v1/accounts/contacts/contact_inboxes_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/contacts/contact_inboxes_controller_spec.rb @@ -5,7 +5,7 @@ RSpec.describe '/api/v1/accounts/{account.id}/contacts/:id/contact_inboxes', typ let(:contact) { create(:contact, account: account) } let(:channel_twilio_sms) { create(:channel_twilio_sms, account: account) } let(:channel_api) { create(:channel_api, account: account) } - let(:user) { create(:user, account: account) } + let(:agent) { create(:user, account: account) } describe 'GET /api/v1/accounts/{account.id}/contacts/:id/contact_inboxes' do context 'when unauthenticated user' do @@ -15,12 +15,13 @@ RSpec.describe '/api/v1/accounts/{account.id}/contacts/:id/contact_inboxes', typ end end - context 'when user is logged in' do + context 'when authenticated user with access to inbox' do it 'creates a contact inbox' do + create(:inbox_member, inbox: channel_api.inbox, user: agent) expect do post "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/contact_inboxes", params: { inbox_id: channel_api.inbox.id }, - headers: user.create_new_auth_token, + headers: agent.create_new_auth_token, as: :json end.to change(ContactInbox, :count).by(1) @@ -29,10 +30,11 @@ RSpec.describe '/api/v1/accounts/{account.id}/contacts/:id/contact_inboxes', typ end it 'throws error for invalid source id' do + create(:inbox_member, inbox: channel_twilio_sms.inbox, user: agent) expect do post "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/contact_inboxes", params: { inbox_id: channel_twilio_sms.inbox.id }, - headers: user.create_new_auth_token, + headers: agent.create_new_auth_token, as: :json end.to change(ContactInbox, :count).by(0) diff --git a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb index 8bc4e5724..12b58c90f 100644 --- a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb @@ -201,18 +201,29 @@ RSpec.describe 'Contacts API', type: :request do end context 'when it is an authenticated user' do - let(:admin) { create(:user, account: account, role: :administrator) } + let(:agent) { create(:user, account: account, role: :agent) } + let!(:twilio_sms) { create(:channel_twilio_sms, account: account) } + let!(:twilio_sms_inbox) { create(:inbox, channel: twilio_sms, account: account) } + let!(:twilio_whatsapp) { create(:channel_twilio_sms, medium: :whatsapp, account: account) } + let!(:twilio_whatsapp_inbox) { create(:inbox, channel: twilio_whatsapp, account: account) } + + it 'shows the contactable inboxes which the user has access to' do + create(:inbox_member, user: agent, inbox: twilio_whatsapp_inbox) - it 'shows the contact' do inbox_service = double allow(Contacts::ContactableInboxesService).to receive(:new).and_return(inbox_service) - allow(inbox_service).to receive(:get).and_return({}) - expect(inbox_service).to receive(:get).and_return({}) + allow(inbox_service).to receive(:get).and_return([ + { source_id: '1123', inbox: twilio_sms_inbox }, + { source_id: '1123', inbox: twilio_whatsapp_inbox } + ]) + expect(inbox_service).to receive(:get) get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/contactable_inboxes", - headers: admin.create_new_auth_token, + headers: agent.create_new_auth_token, as: :json expect(response).to have_http_status(:success) + # only the inboxes which agent has access to are shown + expect(JSON.parse(response.body)['payload'].pluck('inbox').pluck('id')).to eq([twilio_whatsapp_inbox.id]) end end end diff --git a/spec/controllers/api/v1/accounts/conversations/assignments_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations/assignments_controller_spec.rb index ad79a04ff..4e91b8d03 100644 --- a/spec/controllers/api/v1/accounts/conversations/assignments_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/conversations/assignments_controller_spec.rb @@ -14,10 +14,14 @@ RSpec.describe 'Conversation Assignment API', type: :request do end end - context 'when it is an authenticated user' do + context 'when it is an authenticated user with access to the inbox' do let(:agent) { create(:user, account: account, role: :agent) } let(:team) { create(:team, account: account) } + before do + create(:inbox_member, inbox: conversation.inbox, user: agent) + end + it 'assigns a user to the conversation' do params = { assignee_id: agent.id } @@ -48,6 +52,7 @@ RSpec.describe 'Conversation Assignment API', type: :request do before do conversation.update!(assignee: agent) + create(:inbox_member, inbox: conversation.inbox, user: agent) end it 'unassigns the assignee from the conversation' do @@ -69,6 +74,7 @@ RSpec.describe 'Conversation Assignment API', type: :request do before do conversation.update!(team: team) + create(:inbox_member, inbox: conversation.inbox, user: agent) end it 'unassigns the team from the conversation' do diff --git a/spec/controllers/api/v1/accounts/conversations/labels_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations/labels_controller_spec.rb index fad91ffbd..f91b3e3a1 100644 --- a/spec/controllers/api/v1/accounts/conversations/labels_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/conversations/labels_controller_spec.rb @@ -17,9 +17,13 @@ RSpec.describe 'Conversation Label API', type: :request do end end - context 'when it is an authenticated user' do + context 'when it is an authenticated user with access to the conversation' do let(:agent) { create(:user, account: account, role: :agent) } + before do + create(:inbox_member, inbox: conversation.inbox, user: agent) + end + it 'returns all the labels for the conversation' do get api_v1_account_conversation_labels_url(account_id: account.id, conversation_id: conversation.display_id), headers: agent.create_new_auth_token, @@ -49,9 +53,14 @@ RSpec.describe 'Conversation Label API', type: :request do end end - context 'when it is an authenticated user' do + context 'when it is an authenticated user with access to the conversation' do let(:agent) { create(:user, account: account, role: :agent) } + before do + conversation.update_labels('label1, label2') + create(:inbox_member, inbox: conversation.inbox, user: agent) + end + it 'creates labels for the conversation' do post api_v1_account_conversation_labels_url(account_id: account.id, conversation_id: conversation.display_id), params: { labels: %w[label3 label4] }, diff --git a/spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb index a8521a439..d32608ba2 100644 --- a/spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb @@ -15,9 +15,13 @@ RSpec.describe 'Conversation Messages API', type: :request do end end - context 'when it is an authenticated user' do + context 'when it is an authenticated user with access to conversation' do let(:agent) { create(:user, account: account, role: :agent) } + before do + create(:inbox_member, inbox: conversation.inbox, user: agent) + end + it 'creates a new outgoing message' do params = { content: 'test-message', private: true } @@ -31,6 +35,23 @@ RSpec.describe 'Conversation Messages API', type: :request do expect(conversation.messages.first.content).to eq(params[:content]) end + it 'creates an outgoing message with a specific bot sender' do + agent_bot = create(:agent_bot) + time_stamp = Time.now.utc.to_s + params = { content: 'test-message', external_created_at: time_stamp, sender_type: 'AgentBot', sender_id: agent_bot.id } + + post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id), + params: params, + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + response_data = JSON.parse(response.body) + expect(response_data['content_attributes']['external_created_at']).to eq time_stamp + expect(conversation.messages.count).to eq(1) + expect(conversation.messages.last.sender_id).to eq(agent_bot.id) + end + it 'creates a new outgoing message with attachment' do file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png') params = { content: 'test-message', attachments: [file] } @@ -107,9 +128,13 @@ RSpec.describe 'Conversation Messages API', type: :request do end end - context 'when it is an authenticated user' do + context 'when it is an authenticated user with access to conversation' do let(:agent) { create(:user, account: account, role: :agent) } + before do + create(:inbox_member, inbox: conversation.inbox, user: agent) + end + it 'shows the conversation' do get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/messages", headers: agent.create_new_auth_token, @@ -132,9 +157,13 @@ RSpec.describe 'Conversation Messages API', type: :request do end end - context 'when it is an authenticated user' do + context 'when it is an authenticated user with access to conversation' do let(:agent) { create(:user, account: account, role: :agent) } + before do + create(:inbox_member, inbox: conversation.inbox, user: agent) + end + it 'deletes the message' do delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/messages/#{message.id}", headers: agent.create_new_auth_token, @@ -149,6 +178,10 @@ RSpec.describe 'Conversation Messages API', type: :request do context 'when the message id is invalid' do let(:agent) { create(:user, account: account, role: :agent) } + before do + create(:inbox_member, inbox: conversation.inbox, user: agent) + end + it 'returns not found error' do delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/messages/99999", headers: agent.create_new_auth_token, diff --git a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb index 8d4b3de20..6a0c419a2 100644 --- a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb @@ -119,8 +119,27 @@ RSpec.describe 'Conversations API', type: :request do context 'when it is an authenticated user' do let(:agent) { create(:user, account: account, role: :agent) } + let(:administrator) { create(:user, account: account, role: :administrator) } - it 'shows the conversation' do + it 'does not shows the conversation if you do not have access to it' do + get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + + it 'shows the conversation if you are an administrator' do + get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}", + headers: administrator.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + expect(JSON.parse(response.body, symbolize_names: true)[:id]).to eq(conversation.display_id) + end + + it 'shows the conversation if you are an agent with access to inbox' do + create(:inbox_member, user: agent, inbox: conversation.inbox) get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}", headers: agent.create_new_auth_token, as: :json @@ -149,45 +168,71 @@ RSpec.describe 'Conversations API', type: :request do context 'when it is an authenticated user' do let(:agent) { create(:user, account: account, role: :agent) } - it 'creates a new conversation' do + it 'will not create a new conversation if agent does not have access to inbox' do allow(Rails.configuration.dispatcher).to receive(:dispatch) additional_attributes = { test: 'test' } post "/api/v1/accounts/#{account.id}/conversations", headers: agent.create_new_auth_token, params: { source_id: contact_inbox.source_id, additional_attributes: additional_attributes }, as: :json - - expect(response).to have_http_status(:success) - response_data = JSON.parse(response.body, symbolize_names: true) - expect(response_data[:additional_attributes]).to eq(additional_attributes) + expect(response).to have_http_status(:unauthorized) end - it 'creates a new conversation with message when message is passed' do - allow(Rails.configuration.dispatcher).to receive(:dispatch) - post "/api/v1/accounts/#{account.id}/conversations", - headers: agent.create_new_auth_token, - params: { source_id: contact_inbox.source_id, message: { content: 'hi' } }, - as: :json + context 'when it is an authenticated user who has access to the inbox' do + before do + create(:inbox_member, user: agent, inbox: inbox) + end - expect(response).to have_http_status(:success) - response_data = JSON.parse(response.body, symbolize_names: true) - expect(response_data[:additional_attributes]).to eq({}) - expect(account.conversations.find_by(display_id: response_data[:id]).messages.first.content).to eq 'hi' - end + it 'creates a new conversation' do + allow(Rails.configuration.dispatcher).to receive(:dispatch) + additional_attributes = { test: 'test' } + post "/api/v1/accounts/#{account.id}/conversations", + headers: agent.create_new_auth_token, + params: { source_id: contact_inbox.source_id, additional_attributes: additional_attributes }, + as: :json - it 'calls contact inbox builder if contact_id and inbox_id is present' do - builder = double - contact = create(:contact, account: account) - inbox = create(:inbox, account: account) - allow(Rails.configuration.dispatcher).to receive(:dispatch) - allow(ContactInboxBuilder).to receive(:new).and_return(builder) - allow(builder).to receive(:perform) - expect(builder).to receive(:perform) + expect(response).to have_http_status(:success) + response_data = JSON.parse(response.body, symbolize_names: true) + expect(response_data[:additional_attributes]).to eq(additional_attributes) + end - post "/api/v1/accounts/#{account.id}/conversations", - headers: agent.create_new_auth_token, - params: { contact_id: contact.id, inbox_id: inbox.id }, - as: :json + it 'creates a conversation in specificed status' do + allow(Rails.configuration.dispatcher).to receive(:dispatch) + post "/api/v1/accounts/#{account.id}/conversations", + headers: agent.create_new_auth_token, + params: { source_id: contact_inbox.source_id, status: 'bot' }, + as: :json + + expect(response).to have_http_status(:success) + response_data = JSON.parse(response.body, symbolize_names: true) + expect(response_data[:status]).to eq('bot') + end + + it 'creates a new conversation with message when message is passed' do + allow(Rails.configuration.dispatcher).to receive(:dispatch) + post "/api/v1/accounts/#{account.id}/conversations", + headers: agent.create_new_auth_token, + params: { source_id: contact_inbox.source_id, message: { content: 'hi' } }, + as: :json + + expect(response).to have_http_status(:success) + response_data = JSON.parse(response.body, symbolize_names: true) + expect(response_data[:additional_attributes]).to eq({}) + expect(account.conversations.find_by(display_id: response_data[:id]).messages.outgoing.first.content).to eq 'hi' + end + + it 'calls contact inbox builder if contact_id and inbox_id is present' do + builder = double + allow(Rails.configuration.dispatcher).to receive(:dispatch) + allow(ContactInboxBuilder).to receive(:new).and_return(builder) + allow(builder).to receive(:perform) + expect(builder).to receive(:perform) + + post "/api/v1/accounts/#{account.id}/conversations", + headers: agent.create_new_auth_token, + params: { contact_id: contact.id, inbox_id: inbox.id }, + as: :json + end end end end @@ -206,6 +251,10 @@ RSpec.describe 'Conversations API', type: :request do context 'when it is an authenticated user' do let(:agent) { create(:user, account: account, role: :agent) } + before do + create(:inbox_member, user: agent, inbox: conversation.inbox) + end + it 'toggles the conversation status' do expect(conversation.status).to eq('open') @@ -256,6 +305,10 @@ RSpec.describe 'Conversations API', type: :request do context 'when it is an authenticated user' do let(:agent) { create(:user, account: account, role: :agent) } + before do + create(:inbox_member, user: agent, inbox: conversation.inbox) + end + it 'toggles the conversation status' do allow(Rails.configuration.dispatcher).to receive(:dispatch) post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_typing_status", @@ -284,6 +337,10 @@ RSpec.describe 'Conversations API', type: :request do context 'when it is an authenticated user' do let(:agent) { create(:user, account: account, role: :agent) } + before do + create(:inbox_member, user: agent, inbox: conversation.inbox) + end + it 'updates last seen' do post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/update_last_seen", headers: agent.create_new_auth_token, @@ -309,6 +366,10 @@ RSpec.describe 'Conversations API', type: :request do context 'when it is an authenticated user' do let(:agent) { create(:user, account: account, role: :agent) } + before do + create(:inbox_member, user: agent, inbox: conversation.inbox) + end + it 'mutes conversation' do post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/mute", headers: agent.create_new_auth_token, @@ -335,6 +396,10 @@ RSpec.describe 'Conversations API', type: :request do context 'when it is an authenticated user' do let(:agent) { create(:user, account: account, role: :agent) } + before do + create(:inbox_member, user: agent, inbox: conversation.inbox) + end + it 'unmutes conversation' do post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/unmute", headers: agent.create_new_auth_token, @@ -361,15 +426,29 @@ RSpec.describe 'Conversations API', type: :request do let(:agent) { create(:user, account: account, role: :agent) } let(:params) { { email: 'test@test.com' } } + before do + create(:inbox_member, user: agent, inbox: conversation.inbox) + end + it 'mutes conversation' do - allow(ConversationReplyMailer).to receive(:conversation_transcript) + mailer = double + allow(ConversationReplyMailer).to receive(:with).and_return(mailer) + allow(mailer).to receive(:conversation_transcript) post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/transcript", headers: agent.create_new_auth_token, params: params, as: :json expect(response).to have_http_status(:success) - expect(ConversationReplyMailer).to have_received(:conversation_transcript).with(conversation, 'test@test.com') + expect(mailer).to have_received(:conversation_transcript).with(conversation, 'test@test.com') + end + + it 'renders error when parameter missing' do + post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/transcript", + headers: agent.create_new_auth_token, + params: {}, + as: :json + expect(response).to have_http_status(:unprocessable_entity) end end end diff --git a/spec/controllers/api/v1/accounts/facebook_indicators_controller_spec.rb b/spec/controllers/api/v1/accounts/facebook_indicators_controller_spec.rb deleted file mode 100644 index 0b32705f1..000000000 --- a/spec/controllers/api/v1/accounts/facebook_indicators_controller_spec.rb +++ /dev/null @@ -1,143 +0,0 @@ -require 'rails_helper' - -RSpec.describe 'Facebook Indicators API', type: :request do - let(:account) { create(:account) } - let(:facebook_channel) { create(:channel_facebook_page, account: account) } - let(:inbox) { create(:inbox, account: account, channel: facebook_channel) } - let(:contact) { create(:contact, account: account) } - let(:valid_params) { { contact_id: contact.id, inbox_id: inbox.id } } - - before do - allow(Facebook::Messenger::Bot).to receive(:deliver).and_return(true) - allow(Facebook::Messenger::Subscriptions).to receive(:subscribe).and_return(true) - end - - describe 'POST /api/v1/accounts/{account.id}/facebook_indicators/mark_seen' do - context 'when it is an unauthenticated user' do - it 'returns unauthorized' do - post "/api/v1/accounts/#{account.id}/facebook_indicators/mark_seen" - - expect(response).to have_http_status(:unauthorized) - end - end - - context 'when it is an authenticated user' do - let(:agent) { create(:user, account: account, role: :agent) } - - it 'marks a payload as seen' do - contact_inbox = create(:contact_inbox, contact: contact, inbox: inbox) - - expect(Facebook::Messenger::Bot).to receive(:deliver).with( - { recipient: { id: contact_inbox.source_id }, sender_action: 'mark_seen' }, - access_token: inbox.channel.page_access_token - ) - - post "/api/v1/accounts/#{account.id}/facebook_indicators/mark_seen", - headers: agent.create_new_auth_token, - params: valid_params, - as: :json - - expect(response).to have_http_status(:success) - end - - it 'rescues an error' do - create(:contact_inbox, contact: contact, inbox: inbox) - - allow(Facebook::Messenger::Bot).to receive(:deliver).and_raise(Facebook::Messenger::Error) - - post "/api/v1/accounts/#{account.id}/facebook_indicators/mark_seen", - headers: agent.create_new_auth_token, - params: valid_params, - as: :json - - expect(response).to have_http_status(:success) - end - end - end - - describe 'POST /api/v1/accounts/{account.id}/facebook_indicators/typing_on' do - context 'when it is an unauthenticated user' do - it 'returns unauthorized' do - post "/api/v1/accounts/#{account.id}/facebook_indicators/typing_on" - - expect(response).to have_http_status(:unauthorized) - end - end - - context 'when it is an authenticated user' do - let(:agent) { create(:user, account: account, role: :agent) } - - it 'marks a payload as typing_on' do - contact_inbox = create(:contact_inbox, contact: contact, inbox: inbox) - - expect(Facebook::Messenger::Bot).to receive(:deliver).with( - { recipient: { id: contact_inbox.source_id }, sender_action: 'typing_on' }, - access_token: inbox.channel.page_access_token - ) - - post "/api/v1/accounts/#{account.id}/facebook_indicators/typing_on", - headers: agent.create_new_auth_token, - params: valid_params, - as: :json - - expect(response).to have_http_status(:success) - end - - it 'rescues an error' do - create(:contact_inbox, contact: contact, inbox: inbox) - - allow(Facebook::Messenger::Bot).to receive(:deliver).and_raise(Facebook::Messenger::Error) - - post "/api/v1/accounts/#{account.id}/facebook_indicators/typing_on", - headers: agent.create_new_auth_token, - params: valid_params, - as: :json - - expect(response).to have_http_status(:success) - end - end - end - - describe 'POST /api/v1/accounts/{account.id}/facebook_indicators/typing_off' do - context 'when it is an unauthenticated user' do - it 'returns unauthorized' do - post "/api/v1/accounts/#{account.id}/facebook_indicators/typing_off" - - expect(response).to have_http_status(:unauthorized) - end - end - - context 'when it is an authenticated user' do - let(:agent) { create(:user, account: account, role: :agent) } - - it 'marks a payload as typing_off' do - contact_inbox = create(:contact_inbox, contact: contact, inbox: inbox) - - expect(Facebook::Messenger::Bot).to receive(:deliver).with( - { recipient: { id: contact_inbox.source_id }, sender_action: 'typing_off' }, - access_token: inbox.channel.page_access_token - ) - - post "/api/v1/accounts/#{account.id}/facebook_indicators/typing_off", - headers: agent.create_new_auth_token, - params: valid_params, - as: :json - - expect(response).to have_http_status(:success) - end - - it 'rescues an error' do - create(:contact_inbox, contact: contact, inbox: inbox) - - allow(Facebook::Messenger::Bot).to receive(:deliver).and_raise(Facebook::Messenger::Error) - - post "/api/v1/accounts/#{account.id}/facebook_indicators/typing_off", - headers: agent.create_new_auth_token, - params: valid_params, - as: :json - - expect(response).to have_http_status(:success) - end - end - end -end diff --git a/spec/controllers/api/v1/accounts/inbox_members_controller_spec.rb b/spec/controllers/api/v1/accounts/inbox_members_controller_spec.rb index 565b0277f..1361f34b6 100644 --- a/spec/controllers/api/v1/accounts/inbox_members_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/inbox_members_controller_spec.rb @@ -13,14 +13,34 @@ RSpec.describe 'Inbox Member API', type: :request do end end - context 'when it is an authenticated user' do + context 'when it is an authenticated agent' do + let(:agent) { create(:user, account: account, role: :agent) } + + before do + create(:inbox_member, user: agent, inbox: inbox) + end + + it 'returns unauthorized' do + params = { inbox_id: inbox.id, user_ids: [agent.id] } + + post "/api/v1/accounts/#{account.id}/inbox_members", + headers: agent.create_new_auth_token, + params: params, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user with access to inbox' do + let(:administrator) { create(:user, account: account, role: :administrator) } let(:agent) { create(:user, account: account, role: :agent) } it 'modifies inbox members' do params = { inbox_id: inbox.id, user_ids: [agent.id] } post "/api/v1/accounts/#{account.id}/inbox_members", - headers: agent.create_new_auth_token, + headers: administrator.create_new_auth_token, params: params, as: :json @@ -33,7 +53,7 @@ RSpec.describe 'Inbox Member API', type: :request do params = { inbox_id: nil, user_ids: [agent.id] } post "/api/v1/accounts/#{account.id}/inbox_members", - headers: agent.create_new_auth_token, + headers: administrator.create_new_auth_token, params: params, as: :json @@ -44,7 +64,7 @@ RSpec.describe 'Inbox Member API', type: :request do params = { inbox_id: inbox.id, user_ids: ['invalid'] } post "/api/v1/accounts/#{account.id}/inbox_members", - headers: agent.create_new_auth_token, + headers: administrator.create_new_auth_token, params: params, as: :json @@ -65,16 +85,30 @@ RSpec.describe 'Inbox Member API', type: :request do end end - context 'when it is an authenticated user' do + context 'when it is an authenticated user with out access to inbox' do let(:agent) { create(:user, account: account, role: :agent) } it 'returns inbox member' do + get "/api/v1/accounts/#{account.id}/inbox_members/#{inbox.id}", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user with access to inbox' do + let(:agent) { create(:user, account: account, role: :agent) } + + it 'returns inbox member' do + create(:inbox_member, user: agent, inbox: inbox) + get "/api/v1/accounts/#{account.id}/inbox_members/#{inbox.id}", headers: agent.create_new_auth_token, as: :json expect(response).to have_http_status(:success) - expect(JSON.parse(response.body)).to eq({ payload: inbox.inbox_members }.as_json) + expect(JSON.parse(response.body)['payload'].pluck('id')).to eq(inbox.inbox_members.pluck(:user_id)) end end end diff --git a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb index 5efa8049c..1a5c31088 100644 --- a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb @@ -2,6 +2,8 @@ require 'rails_helper' RSpec.describe 'Inboxes API', type: :request do let(:account) { create(:account) } + let(:agent) { create(:user, account: account, role: :agent) } + let(:admin) { create(:user, account: account, role: :administrator) } describe 'GET /api/v1/accounts/{account.id}/inboxes' do context 'when it is an unauthenticated user' do @@ -15,11 +17,11 @@ RSpec.describe 'Inboxes API', type: :request do context 'when it is an authenticated user' do let(:agent) { create(:user, account: account, role: :agent) } let(:admin) { create(:user, account: account, role: :administrator) } + let(:inbox) { create(:inbox, account: account) } before do create(:inbox, account: account) - second_inbox = create(:inbox, account: account) - create(:inbox_member, user: agent, inbox: second_inbox) + create(:inbox_member, user: agent, inbox: inbox) end it 'returns all inboxes of current_account as administrator' do @@ -54,9 +56,6 @@ RSpec.describe 'Inboxes API', type: :request do end context 'when it is an authenticated user' do - let(:agent) { create(:user, account: account, role: :agent) } - let(:admin) { create(:user, account: account, role: :administrator) } - before do create(:inbox_member, user: agent, inbox: inbox) end @@ -92,7 +91,9 @@ RSpec.describe 'Inboxes API', type: :request do let!(:campaign) { create(:campaign, account: account, inbox: inbox) } it 'returns unauthorized for agents' do - get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/campaigns" + get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/campaigns", + headers: agent.create_new_auth_token, + as: :json expect(response).to have_http_status(:unauthorized) end @@ -263,6 +264,46 @@ RSpec.describe 'Inboxes API', type: :request do end end + describe 'GET /api/v1/accounts/{account.id}/inboxes/{inbox.id}/agent_bot' do + let(:inbox) { create(:inbox, account: account) } + + before do + create(:inbox_member, user: agent, inbox: inbox) + end + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/agent_bot" + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + it 'returns empty when no agent bot is present' do + get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/agent_bot", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + inbox_data = JSON.parse(response.body, symbolize_names: true) + expect(inbox_data[:agent_bot].blank?).to eq(true) + end + + it 'returns the agent bot attached to the inbox' do + agent_bot = create(:agent_bot) + create(:agent_bot_inbox, agent_bot: agent_bot, inbox: inbox) + get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/agent_bot", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + inbox_data = JSON.parse(response.body, symbolize_names: true) + expect(inbox_data[:agent_bot][:name]).to eq agent_bot.name + end + end + end + describe 'POST /api/v1/accounts/{account.id}/inboxes/:id/set_agent_bot' do let(:inbox) { create(:inbox, account: account) } let(:agent_bot) { create(:agent_bot) } diff --git a/spec/controllers/api/v1/accounts/integrations/apps_controller_spec.rb b/spec/controllers/api/v1/accounts/integrations/apps_controller_spec.rb index b1ac377bb..8f1716c32 100644 --- a/spec/controllers/api/v1/accounts/integrations/apps_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/integrations/apps_controller_spec.rb @@ -12,7 +12,7 @@ RSpec.describe 'Integration Apps API', type: :request do end context 'when it is an authenticated user' do - let(:agent) { create(:user, account: account, role: :agent) } + let(:agent) { create(:user, account: account, role: :administrator) } it 'returns all active apps' do first_app = Integrations::App.all.find(&:active?) @@ -25,6 +25,21 @@ RSpec.describe 'Integration Apps API', type: :request do expect(apps['id']).to eql(first_app.id) expect(apps['name']).to eql(first_app.name) end + + it 'returns slack app with appropriate redirect url when configured' do + ENV['SLACK_CLIENT_ID'] = 'client_id' + ENV['SLACK_CLIENT_SECRET'] = 'client_secret' + get api_v1_account_integrations_apps_url(account), + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + apps = JSON.parse(response.body)['payload'] + slack_app = apps.find { |app| app['id'] == 'slack' } + expect(slack_app['action']).to include('client_id=client_id') + ENV['SLACK_CLIENT_ID'] = nil + ENV['SLACK_CLIENT_SECRET'] = nil + end end end @@ -37,7 +52,7 @@ RSpec.describe 'Integration Apps API', type: :request do end context 'when it is an authenticated user' do - let(:agent) { create(:user, account: account, role: :agent) } + let(:agent) { create(:user, account: account, role: :administrator) } it 'returns details of the app' do get api_v1_account_integrations_app_url(account_id: account.id, id: 'slack'), diff --git a/spec/controllers/api/v1/accounts/integrations/hooks_controller_spec.rb b/spec/controllers/api/v1/accounts/integrations/hooks_controller_spec.rb index 3c92f9026..63c31d98a 100644 --- a/spec/controllers/api/v1/accounts/integrations/hooks_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/integrations/hooks_controller_spec.rb @@ -36,7 +36,7 @@ RSpec.describe 'Integration Hooks API', type: :request do expect(response).to have_http_status(:success) data = JSON.parse(response.body) - expect(data['app']['id']).to eq params[:app_id] + expect(data['app_id']).to eq params[:app_id] end end end @@ -72,7 +72,7 @@ RSpec.describe 'Integration Hooks API', type: :request do expect(response).to have_http_status(:success) data = JSON.parse(response.body) - expect(data['app']['id']).to eq 'slack' + expect(data['app_id']).to eq 'slack' end end end diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb index 16f36720d..24607f250 100644 --- a/spec/controllers/api/v1/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts_controller_spec.rb @@ -18,13 +18,13 @@ RSpec.describe 'Accounts API', type: :request do it 'calls account builder' do allow(account_builder).to receive(:perform).and_return([user, account]) - params = { account_name: 'test', email: email, user: nil, user_full_name: user_full_name } + params = { account_name: 'test', email: email, user: nil, user_full_name: user_full_name, password: 'Password1!' } post api_v1_accounts_url, params: params, as: :json - expect(AccountBuilder).to have_received(:new).with(params.merge(confirmed: false)) + expect(AccountBuilder).to have_received(:new).with(params.except(:password).merge(user_password: params[:password])) expect(account_builder).to have_received(:perform) expect(response.headers.keys).to include('access-token', 'token-type', 'client', 'expiry', 'uid') end @@ -38,44 +38,11 @@ RSpec.describe 'Accounts API', type: :request do params: params, as: :json - expect(AccountBuilder).to have_received(:new).with(params.merge(confirmed: false)) + expect(AccountBuilder).to have_received(:new).with(params.merge(user_password: params[:password])) expect(account_builder).to have_received(:perform) expect(response).to have_http_status(:forbidden) expect(response.body).to eq({ message: I18n.t('errors.signup.failed') }.to_json) end - - it 'ignores confirmed param when called with out super admin token' do - allow(account_builder).to receive(:perform).and_return(nil) - - params = { account_name: 'test', email: email, confirmed: true, user: nil, user_full_name: user_full_name } - - post api_v1_accounts_url, - params: params, - as: :json - - expect(AccountBuilder).to have_received(:new).with(params.merge(confirmed: false)) - expect(account_builder).to have_received(:perform) - expect(response).to have_http_status(:forbidden) - expect(response.body).to eq({ message: I18n.t('errors.signup.failed') }.to_json) - end - end - - context 'when called with super admin token' do - let(:super_admin) { create(:super_admin) } - - it 'calls account builder with confirmed true when confirmed param is passed' do - params = { account_name: 'test', email: email, confirmed: true, user_full_name: user_full_name } - - post api_v1_accounts_url, - params: params, - headers: { api_access_token: super_admin.access_token.token }, - as: :json - - created_user = User.find_by(email: email) - expect(created_user.confirmed?).to eq(true) - expect(response.headers.keys).to include('access-token', 'token-type', 'client', 'expiry', 'uid') - expect(response.body).to include(created_user.access_token.token) - end end context 'when ENABLE_ACCOUNT_SIGNUP env variable is set to false' do diff --git a/spec/controllers/api/v1/agent_bots_controller_spec.rb b/spec/controllers/api/v1/agent_bots_controller_spec.rb deleted file mode 100644 index 258fad5c3..000000000 --- a/spec/controllers/api/v1/agent_bots_controller_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'rails_helper' - -RSpec.describe 'Profile API', type: :request do - let!(:agent_bot1) { create(:agent_bot) } - let!(:agent_bot2) { create(:agent_bot) } - - describe 'GET /api/v1/agent_bots' do - it 'returns all the agent bots in the system' do - get '/api/v1/agent_bots', - as: :json - - expect(response).to have_http_status(:success) - expect(response.body).to include(agent_bot1.name) - expect(response.body).to include(agent_bot2.name) - end - end -end diff --git a/spec/controllers/api/v1/profiles_controller_spec.rb b/spec/controllers/api/v1/profiles_controller_spec.rb index 4282b02a7..c73194fec 100644 --- a/spec/controllers/api/v1/profiles_controller_spec.rb +++ b/spec/controllers/api/v1/profiles_controller_spec.rb @@ -39,12 +39,12 @@ RSpec.describe 'Profile API', type: :request do end context 'when it is an authenticated user' do - let(:agent) { create(:user, account: account, role: :agent) } + let(:agent) { create(:user, password: 'Test123!', account: account, role: :agent) } it 'updates the name & email' do new_email = Faker::Internet.email put '/api/v1/profile', - params: { profile: { name: 'test', 'email': new_email } }, + params: { profile: { name: 'test', email: new_email } }, headers: agent.create_new_auth_token, as: :json @@ -56,13 +56,23 @@ RSpec.describe 'Profile API', type: :request do expect(agent.email).to eq(new_email) end - it 'updates the password' do + it 'updates the password when current password is provided' do put '/api/v1/profile', - params: { profile: { password: 'test123', password_confirmation: 'test123' } }, + params: { profile: { current_password: 'Test123!', password: 'Test1234!', password_confirmation: 'Test1234!' } }, headers: agent.create_new_auth_token, as: :json expect(response).to have_http_status(:success) + expect(agent.reload.valid_password?('Test1234!')).to eq true + end + + it 'throws error when current password provided is invalid' do + put '/api/v1/profile', + params: { profile: { current_password: 'Test', password: 'test123', password_confirmation: 'test123' } }, + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unprocessable_entity) end it 'updates avatar' do diff --git a/spec/controllers/api/v1/widget/conversations_controller_spec.rb b/spec/controllers/api/v1/widget/conversations_controller_spec.rb index 9ba2d066a..2603d1e15 100644 --- a/spec/controllers/api/v1/widget/conversations_controller_spec.rb +++ b/spec/controllers/api/v1/widget/conversations_controller_spec.rb @@ -104,7 +104,9 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do describe 'POST /api/v1/widget/conversations/transcript' do context 'with a conversation' do it 'sends transcript email' do - allow(ConversationReplyMailer).to receive(:conversation_transcript) + mailer = double + allow(ConversationReplyMailer).to receive(:with).and_return(mailer) + allow(mailer).to receive(:conversation_transcript) post '/api/v1/widget/conversations/transcript', headers: { 'X-Auth-Token' => token }, @@ -112,7 +114,7 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do as: :json expect(response).to have_http_status(:success) - expect(ConversationReplyMailer).to have_received(:conversation_transcript).with(conversation, 'test@test.com') + expect(mailer).to have_received(:conversation_transcript).with(conversation, 'test@test.com') end end end diff --git a/spec/controllers/api/v2/accounts/report_controller_spec.rb b/spec/controllers/api/v2/accounts/report_controller_spec.rb index f72b6639a..e6152dc8b 100644 --- a/spec/controllers/api/v2/accounts/report_controller_spec.rb +++ b/spec/controllers/api/v2/accounts/report_controller_spec.rb @@ -2,6 +2,8 @@ require 'rails_helper' RSpec.describe 'Reports API', type: :request do let(:account) { create(:account) } + let(:admin) { create(:user, account: account, role: :administrator) } + let(:agent) { create(:user, account: account, role: :agent) } let!(:user) { create(:user, account: account) } let!(:inbox) { create(:inbox, account: account) } let(:inbox_member) { create(:inbox_member, user: user, inbox: inbox) } @@ -21,21 +23,28 @@ RSpec.describe 'Reports API', type: :request do end context 'when it is an authenticated user' do - let(:agent) { create(:user, account: account, role: :agent) } - - it 'return timeseries metrics' do - params = { - metric: 'conversations_count', - type: :account, - since: Time.zone.today.to_time.to_i.to_s, - until: Time.zone.today.to_time.to_i.to_s - } + params = { + metric: 'conversations_count', + type: :account, + since: Time.zone.today.to_time.to_i.to_s, + until: Time.zone.today.to_time.to_i.to_s + } + it 'returns unauthorized for agents' do get "/api/v2/accounts/#{account.id}/reports/account", params: params, headers: agent.create_new_auth_token, as: :json + expect(response).to have_http_status(:unauthorized) + end + + it 'return timeseries metrics' do + get "/api/v2/accounts/#{account.id}/reports/account", + params: params, + headers: admin.create_new_auth_token, + as: :json + expect(response).to have_http_status(:success) json_response = JSON.parse(response.body) @@ -56,20 +65,27 @@ RSpec.describe 'Reports API', type: :request do end context 'when it is an authenticated user' do - let(:agent) { create(:user, account: account, role: :agent) } - - it 'returns summary metrics' do - params = { - type: :account, - since: Time.zone.today.to_time.to_i.to_s, - until: Time.zone.today.to_time.to_i.to_s - } + params = { + type: :account, + since: Time.zone.today.to_time.to_i.to_s, + until: Time.zone.today.to_time.to_i.to_s + } + it 'returns unauthorized for agents' do get "/api/v2/accounts/#{account.id}/reports/account_summary", params: params, headers: agent.create_new_auth_token, as: :json + expect(response).to have_http_status(:unauthorized) + end + + it 'returns summary metrics' do + get "/api/v2/accounts/#{account.id}/reports/account_summary", + params: params, + headers: admin.create_new_auth_token, + as: :json + expect(response).to have_http_status(:success) json_response = JSON.parse(response.body) @@ -88,18 +104,24 @@ RSpec.describe 'Reports API', type: :request do end context 'when it is an authenticated user' do - let(:agent) { create(:user, account: account, role: :agent) } - params = { since: 30.days.ago.to_i.to_s, until: Time.zone.today.to_time.to_i.to_s } - it 'returns summary' do + it 'returns unauthorized for agents' do get "/api/v2/accounts/#{account.id}/reports/agents.csv", params: params, headers: agent.create_new_auth_token + expect(response).to have_http_status(:unauthorized) + end + + it 'returns summary' do + get "/api/v2/accounts/#{account.id}/reports/agents.csv", + params: params, + headers: admin.create_new_auth_token + expect(response).to have_http_status(:success) end end @@ -115,18 +137,24 @@ RSpec.describe 'Reports API', type: :request do end context 'when it is an authenticated user' do - let(:agent) { create(:user, account: account, role: :agent) } - params = { since: 30.days.ago.to_i.to_s, until: Time.zone.today.to_time.to_i.to_s } - it 'returns summary' do + it 'returns unauthorized for agents' do get "/api/v2/accounts/#{account.id}/reports/inboxes", params: params, headers: agent.create_new_auth_token + expect(response).to have_http_status(:unauthorized) + end + + it 'returns summary' do + get "/api/v2/accounts/#{account.id}/reports/inboxes", + params: params, + headers: admin.create_new_auth_token + expect(response).to have_http_status(:success) end end diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb index 7ada22702..e797c878a 100644 --- a/spec/controllers/dashboard_controller_spec.rb +++ b/spec/controllers/dashboard_controller_spec.rb @@ -17,4 +17,18 @@ describe '/app/login', type: :request do ENV['DEFAULT_LOCALE'] = 'en' end end + + # Routes are loaded once on app start + # hence Rails.application.reload_routes! is used in this spec + # ref : https://stackoverflow.com/a/63584877/939299 + context 'with CW_API_ONLY_SERVER true' do + it 'returns 404' do + ENV['CW_API_ONLY_SERVER'] = 'true' + Rails.application.reload_routes! + get '/app/login' + expect(response).to have_http_status(:not_found) + ENV['CW_API_ONLY_SERVER'] = nil + Rails.application.reload_routes! + end + end end diff --git a/spec/controllers/devise/confirmations_controller_spec.rb b/spec/controllers/devise/confirmations_controller_spec.rb index 6970b5777..d01ecb196 100644 --- a/spec/controllers/devise/confirmations_controller_spec.rb +++ b/spec/controllers/devise/confirmations_controller_spec.rb @@ -18,12 +18,8 @@ RSpec.describe 'Token Confirmation', type: :request do expect(response.status).to eq 200 end - it 'returns message "Success"' do - expect(response_json[:message]).to eq 'Success' - end - - it 'returns "redirect_url"' do - expect(response_json[:redirect_url]).to include '/app/auth/password/edit?config=default&redirect_url=&reset_password_token' + it 'returns "auth data"' do + expect(response.body).to include('john.doe@gmail.com') end end diff --git a/spec/controllers/devise/session_controller_spec.rb b/spec/controllers/devise/session_controller_spec.rb index ef51f5e4b..957604be3 100644 --- a/spec/controllers/devise/session_controller_spec.rb +++ b/spec/controllers/devise/session_controller_spec.rb @@ -17,10 +17,10 @@ RSpec.describe 'Session', type: :request do end context 'when it is valid credentials' do - let!(:user) { create(:user, password: 'test1234', account: account) } + let!(:user) { create(:user, password: 'Password1!', account: account) } it 'returns successful auth response' do - params = { email: user.email, password: 'test1234' } + params = { email: user.email, password: 'Password1!' } post new_user_session_url, params: params, @@ -32,7 +32,7 @@ RSpec.describe 'Session', type: :request do end context 'when it is invalid sso auth token' do - let!(:user) { create(:user, password: 'test1234', account: account) } + let!(:user) { create(:user, password: 'Password1!', account: account) } it 'returns unauthorized' do params = { email: user.email, sso_auth_token: SecureRandom.hex(32) } @@ -46,7 +46,7 @@ RSpec.describe 'Session', type: :request do end context 'when with valid sso auth token' do - let!(:user) { create(:user, password: 'test1234', account: account) } + let!(:user) { create(:user, password: 'Password1!', account: account) } it 'returns successful auth response' do params = { email: user.email, sso_auth_token: user.generate_sso_auth_token } diff --git a/spec/controllers/platform/api/v1/agent_bots_controller_spec.rb b/spec/controllers/platform/api/v1/agent_bots_controller_spec.rb new file mode 100644 index 000000000..d56e1dfb9 --- /dev/null +++ b/spec/controllers/platform/api/v1/agent_bots_controller_spec.rb @@ -0,0 +1,172 @@ +require 'rails_helper' + +RSpec.describe 'Platform Agent Bot API', type: :request do + let!(:agent_bot) { create(:agent_bot) } + + describe 'GET /platform/api/v1/agent_bots' do + context 'when it is an unauthenticated platform app' do + it 'returns unauthorized' do + get '/platform/api/v1/agent_bots' + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an invalid platform app token' do + it 'returns unauthorized' do + get '/platform/api/v1/agent_bots', headers: { api_access_token: 'invalid' }, as: :json + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated platform app' do + let(:platform_app) { create(:platform_app) } + + it 'returns unauthorized when its not a permissible object' do + get '/platform/api/v1/agent_bots', headers: { api_access_token: platform_app.access_token.token }, as: :json + expect(response).to have_http_status(:success) + data = JSON.parse(response.body) + expect(data.length).to eq(0) + end + + it 'shows a agent_bot when its permissible object' do + create(:platform_app_permissible, platform_app: platform_app, permissible: agent_bot) + + get '/platform/api/v1/agent_bots', + headers: { api_access_token: platform_app.access_token.token }, as: :json + + expect(response).to have_http_status(:success) + data = JSON.parse(response.body) + expect(data.length).to eq(1) + end + end + end + + describe 'GET /platform/api/v1/agent_bots/{agent_bot_id}' do + context 'when it is an unauthenticated platform app' do + it 'returns unauthorized' do + get "/platform/api/v1/agent_bots/#{agent_bot.id}" + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an invalid platform app token' do + it 'returns unauthorized' do + get "/platform/api/v1/agent_bots/#{agent_bot.id}", headers: { api_access_token: 'invalid' }, as: :json + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated platform app' do + let(:platform_app) { create(:platform_app) } + + it 'returns unauthorized when its not a permissible object' do + get "/platform/api/v1/agent_bots/#{agent_bot.id}", headers: { api_access_token: platform_app.access_token.token }, as: :json + expect(response).to have_http_status(:unauthorized) + end + + it 'shows a agent_bot when its permissible object' do + create(:platform_app_permissible, platform_app: platform_app, permissible: agent_bot) + + get "/platform/api/v1/agent_bots/#{agent_bot.id}", + headers: { api_access_token: platform_app.access_token.token }, as: :json + + expect(response).to have_http_status(:success) + data = JSON.parse(response.body) + expect(data['name']).to eq(agent_bot.name) + end + end + end + + describe 'POST /platform/api/v1/agent_bots/' do + context 'when it is an unauthenticated platform app' do + it 'returns unauthorized' do + post '/platform/api/v1/agent_bots' + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an invalid platform app token' do + it 'returns unauthorized' do + post '/platform/api/v1/agent_bots/', headers: { api_access_token: 'invalid' }, as: :json + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated platform app' do + let(:platform_app) { create(:platform_app) } + + it 'creates a new agent bot' do + post '/platform/api/v1/agent_bots/', params: { name: 'test' }, + headers: { api_access_token: platform_app.access_token.token }, as: :json + + expect(response).to have_http_status(:success) + data = JSON.parse(response.body) + expect(data['name']).to eq('test') + expect(platform_app.platform_app_permissibles.first.permissible_id).to eq data['id'] + end + end + end + + describe 'PATCH /platform/api/v1/agent_bots/{agent_bot_id}' do + context 'when it is an unauthenticated platform app' do + it 'returns unauthorized' do + patch "/platform/api/v1/agent_bots/#{agent_bot.id}" + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an invalid platform app token' do + it 'returns unauthorized' do + patch "/platform/api/v1/agent_bots/#{agent_bot.id}", headers: { api_access_token: 'invalid' }, as: :json + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated platform app' do + let(:platform_app) { create(:platform_app) } + + it 'returns unauthorized when its not a permissible object' do + patch "/platform/api/v1/agent_bots/#{agent_bot.id}", params: { name: 'test' }, + headers: { api_access_token: platform_app.access_token.token }, as: :json + expect(response).to have_http_status(:unauthorized) + end + + it 'updates the agent_bot' do + create(:platform_app_permissible, platform_app: platform_app, permissible: agent_bot) + patch "/platform/api/v1/agent_bots/#{agent_bot.id}", params: { name: 'test123' }, + headers: { api_access_token: platform_app.access_token.token }, as: :json + + expect(response).to have_http_status(:success) + data = JSON.parse(response.body) + expect(data['name']).to eq('test123') + end + end + end + + describe 'DELETE /platform/api/v1/agent_bots/{agent_bot_id}' do + context 'when it is an unauthenticated platform app' do + it 'returns unauthorized' do + delete "/platform/api/v1/agent_bots/#{agent_bot.id}", headers: { api_access_token: 'invalid' }, as: :json + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated platform app' do + let(:platform_app) { create(:platform_app) } + + it 'returns unauthorized when its not a permissible object' do + delete "/platform/api/v1/agent_bots/#{agent_bot.id}", headers: { api_access_token: 'invalid' }, as: :json + expect(response).to have_http_status(:unauthorized) + end + + it 'returns deletes the account user' do + create(:platform_app_permissible, platform_app: platform_app, permissible: agent_bot) + + delete "/platform/api/v1/agent_bots/#{agent_bot.id}", headers: { api_access_token: platform_app.access_token.token }, as: :json + + expect(response).to have_http_status(:success) + expect(AgentBot.count).to eq 0 + end + end + end +end diff --git a/spec/controllers/platform/api/v1/public/api/v1/inbox/contacts_controller_spec.rb b/spec/controllers/platform/api/v1/public/api/v1/inbox/contacts_controller_spec.rb new file mode 100644 index 000000000..284d21c83 --- /dev/null +++ b/spec/controllers/platform/api/v1/public/api/v1/inbox/contacts_controller_spec.rb @@ -0,0 +1,40 @@ +require 'rails_helper' + +RSpec.describe 'Public Inbox Contacts API', type: :request do + let!(:api_channel) { create(:channel_api) } + let!(:contact) { create(:contact) } + let!(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: api_channel.inbox) } + + describe 'POST /public/api/v1/inboxes/{identifier}/contact' do + it 'creates a contact and return the source id' do + post "/public/api/v1/inboxes/#{api_channel.identifier}/contacts" + + expect(response).to have_http_status(:success) + data = JSON.parse(response.body) + expect(data['source_id']).not_to eq nil + expect(data['pubsub_token']).not_to eq nil + end + end + + describe 'GET /public/api/v1/inboxes/{identifier}/contact/{source_id}' do + it 'gets a contact when present' do + get "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}" + + expect(response).to have_http_status(:success) + data = JSON.parse(response.body) + expect(data['source_id']).to eq contact_inbox.source_id + expect(data['pubsub_token']).to eq contact.pubsub_token + end + end + + describe 'PATCH /public/api/v1/inboxes/{identifier}/contact/{source_id}' do + it 'updates a contact when present' do + patch "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}", + params: { name: 'John Smith' } + + expect(response).to have_http_status(:success) + data = JSON.parse(response.body) + expect(data['name']).to eq 'John Smith' + end + end +end diff --git a/spec/controllers/platform/api/v1/public/api/v1/inbox/conversations_controller_spec.rb b/spec/controllers/platform/api/v1/public/api/v1/inbox/conversations_controller_spec.rb new file mode 100644 index 000000000..1250ba526 --- /dev/null +++ b/spec/controllers/platform/api/v1/public/api/v1/inbox/conversations_controller_spec.rb @@ -0,0 +1,28 @@ +require 'rails_helper' + +RSpec.describe 'Public Inbox Contact Conversations API', type: :request do + let!(:api_channel) { create(:channel_api) } + let!(:contact) { create(:contact) } + let!(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: api_channel.inbox) } + + describe 'GET /public/api/v1/inboxes/{identifier}/contact/{source_id}/conversations' do + it 'return the conversations for that contact' do + create(:conversation, contact_inbox: contact_inbox) + get "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations" + + expect(response).to have_http_status(:success) + data = JSON.parse(response.body) + expect(data.length).to eq 1 + end + end + + describe 'POST /public/api/v1/inboxes/{identifier}/contact/{source_id}/conversations' do + it 'creates a conversation for that contact' do + post "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations" + + expect(response).to have_http_status(:success) + data = JSON.parse(response.body) + expect(data['id']).not_to eq nil + end + end +end diff --git a/spec/controllers/platform/api/v1/public/api/v1/inbox/messages_controller_spec.rb b/spec/controllers/platform/api/v1/public/api/v1/inbox/messages_controller_spec.rb new file mode 100644 index 000000000..7390d4dd9 --- /dev/null +++ b/spec/controllers/platform/api/v1/public/api/v1/inbox/messages_controller_spec.rb @@ -0,0 +1,57 @@ +require 'rails_helper' + +RSpec.describe 'Public Inbox Contact Conversation Messages API', type: :request do + let!(:api_channel) { create(:channel_api) } + let!(:contact) { create(:contact) } + let!(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: api_channel.inbox) } + let!(:conversation) { create(:conversation, contact: contact, contact_inbox: contact_inbox) } + + describe 'GET /public/api/v1/inboxes/{identifier}/contact/{source_id}/conversations/{conversation_id}/messages' do + it 'return the messages for that conversation' do + 2.times.each { create(:message, account: conversation.account, inbox: conversation.inbox, conversation: conversation) } + + get "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations/#{conversation.display_id}/messages" + + expect(response).to have_http_status(:success) + data = JSON.parse(response.body) + expect(data.length).to eq 2 + end + end + + describe 'POST /public/api/v1/inboxes/{identifier}/contact/{source_id}/conversations/{conversation_id}/messages' do + it 'creates a message in the conversation' do + post "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations/#{conversation.display_id}/messages", + params: { content: 'hello' } + + expect(response).to have_http_status(:success) + data = JSON.parse(response.body) + expect(data['content']).to eq('hello') + end + + it 'creates attachment message in conversation' do + file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png') + post "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations/#{conversation.display_id}/messages", + params: { content: 'hello', attachments: [file] } + + expect(response).to have_http_status(:success) + data = JSON.parse(response.body) + expect(data['content']).to eq('hello') + + expect(conversation.messages.last.attachments.first.file.present?).to eq(true) + expect(conversation.messages.last.attachments.first.file_type).to eq('image') + end + end + + describe 'PATCH /public/api/v1/inboxes/{identifier}/contact/{source_id}/conversations/{conversation_id}/messages/{id}' do + it 'creates a message in the conversation' do + message = create(:message, account: conversation.account, inbox: conversation.inbox, conversation: conversation) + patch "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations/" \ + "#{conversation.display_id}/messages/#{message.id}", + params: { submitted_values: [{ title: 'test' }] } + + expect(response).to have_http_status(:success) + data = JSON.parse(response.body) + expect(data['content_attributes']['submitted_values'].first['title']).to eq 'test' + end + end +end diff --git a/spec/controllers/platform/api/v1/users_controller_spec.rb b/spec/controllers/platform/api/v1/users_controller_spec.rb index 7f9167b14..e25f3cb42 100644 --- a/spec/controllers/platform/api/v1/users_controller_spec.rb +++ b/spec/controllers/platform/api/v1/users_controller_spec.rb @@ -94,7 +94,7 @@ RSpec.describe 'Platform Users API', type: :request do let(:platform_app) { create(:platform_app) } it 'creates a new user and permissible for the user' do - post '/platform/api/v1/users/', params: { name: 'test', email: 'test@test.com', password: 'password123' }, + post '/platform/api/v1/users/', params: { name: 'test', email: 'test@test.com', password: 'Password1!' }, headers: { api_access_token: platform_app.access_token.token }, as: :json expect(response).to have_http_status(:success) @@ -105,7 +105,7 @@ RSpec.describe 'Platform Users API', type: :request do it 'fetch existing user and creates permissible for the user' do create(:user, name: 'old test', email: 'test@test.com') - post '/platform/api/v1/users/', params: { name: 'test', email: 'test@test.com', password: 'password123' }, + post '/platform/api/v1/users/', params: { name: 'test', email: 'test@test.com', password: 'Password1!' }, headers: { api_access_token: platform_app.access_token.token }, as: :json expect(response).to have_http_status(:success) diff --git a/spec/controllers/super_admin/access_tokens_controller_spec.rb b/spec/controllers/super_admin/access_tokens_controller_spec.rb index bb516bc04..ace011663 100644 --- a/spec/controllers/super_admin/access_tokens_controller_spec.rb +++ b/spec/controllers/super_admin/access_tokens_controller_spec.rb @@ -2,6 +2,7 @@ require 'rails_helper' RSpec.describe 'Super Admin access tokens API', type: :request do let(:super_admin) { create(:super_admin) } + let!(:platform_app) { create(:platform_app) } describe 'GET /super_admin/access_tokens' do context 'when it is an unauthenticated super admin' do @@ -16,7 +17,7 @@ RSpec.describe 'Super Admin access tokens API', type: :request do sign_in super_admin get '/super_admin/access_tokens' expect(response).to have_http_status(:success) - expect(response.body).to include(super_admin.access_token.token) + expect(response.body).to include(platform_app.access_token.token) end end end diff --git a/spec/factories/super_admins.rb b/spec/factories/super_admins.rb index 88e488f68..fc0b485c5 100644 --- a/spec/factories/super_admins.rb +++ b/spec/factories/super_admins.rb @@ -1,6 +1,6 @@ FactoryBot.define do factory :super_admin do email { "admin@#{SecureRandom.uuid}.com" } - password { 'password' } + password { 'Password1!' } end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index bbe1e6acd..cbebfa838 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -14,7 +14,7 @@ FactoryBot.define do name { Faker::Name.name } display_name { Faker::Name.first_name } email { display_name + "@#{SecureRandom.uuid}.com" } - password { 'password' } + password { 'Password1!' } after(:build) do |user, evaluator| user.skip_confirmation! if evaluator.skip_confirmation diff --git a/spec/lib/chatwoot_hub_spec.rb b/spec/lib/chatwoot_hub_spec.rb index 26d75365f..2cd5c6be1 100644 --- a/spec/lib/chatwoot_hub_spec.rb +++ b/spec/lib/chatwoot_hub_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' describe ChatwootHub do it 'get latest version from chatwoot hub' do version = '1.1.1' - allow(RestClient).to receive(:get).and_return({ 'version': version }.to_json) + allow(RestClient).to receive(:get).and_return({ version: version }.to_json) expect(described_class.latest_version).to eq version expect(RestClient).to have_received(:get).with(described_class::BASE_URL, { params: described_class.instance_config }) end diff --git a/spec/lib/email_templates/db_resolver_service_spec.rb b/spec/lib/email_templates/db_resolver_service_spec.rb index 7bbb5c508..8648d8514 100644 --- a/spec/lib/email_templates/db_resolver_service_spec.rb +++ b/spec/lib/email_templates/db_resolver_service_spec.rb @@ -15,43 +15,43 @@ describe ::EmailTemplates::DbResolverService do email_template = create(:email_template, name: 'test', body: 'test') handler = ActionView::Template.registered_template_handler(:liquid) template_details = { + locals: [], format: Mime['html'].to_sym, - updated_at: email_template.updated_at, virtual_path: 'test' } expect( - resolver.find_templates('test', '', false, []).first.to_json + resolver.find_templates('test', '', false, []).first.inspect ).to eq( ActionView::Template.new( email_template.body, - "DB Template - #{email_template.id}", handler, template_details - ).to_json + "DB Template - #{email_template.id}", handler, **template_details + ).inspect ) end end context 'when account template exists in db' do let(:account) { create(:account) } - let(:installation_template) { create(:email_template, name: 'test', body: 'test') } - let(:account_template) { create(:email_template, name: 'test', body: 'test2', account: account) } + let!(:installation_template) { create(:email_template, name: 'test', body: 'test') } + let!(:account_template) { create(:email_template, name: 'test', body: 'test2', account: account) } it 'return account template for current account' do Current.account = account handler = ActionView::Template.registered_template_handler(:liquid) template_details = { + locals: [], format: Mime['html'].to_sym, - updated_at: account_template.updated_at, virtual_path: 'test' } expect( - resolver.find_templates('test', '', false, []).first.to_json + resolver.find_templates('test', '', false, []).first.inspect ).to eq( ActionView::Template.new( account_template.body, - "DB Template - #{account_template.id}", handler, template_details - ).to_json + "DB Template - #{account_template.id}", handler, **template_details + ).inspect ) Current.account = nil end @@ -60,18 +60,18 @@ describe ::EmailTemplates::DbResolverService do Current.account = create(:account) handler = ActionView::Template.registered_template_handler(:liquid) template_details = { + locals: [], format: Mime['html'].to_sym, - updated_at: installation_template.updated_at, virtual_path: 'test' } expect( - resolver.find_templates('test', '', false, []).first.to_json + resolver.find_templates('test', '', false, []).first.inspect ).to eq( ActionView::Template.new( installation_template.body, - "DB Template - #{installation_template.id}", handler, template_details - ).to_json + "DB Template - #{installation_template.id}", handler, **template_details + ).inspect ) Current.account = nil end diff --git a/spec/mailers/confirmation_instructions_spec.rb b/spec/mailers/confirmation_instructions_spec.rb index 9914c1585..8310ab714 100644 --- a/spec/mailers/confirmation_instructions_spec.rb +++ b/spec/mailers/confirmation_instructions_spec.rb @@ -5,7 +5,7 @@ require 'rails_helper' RSpec.describe 'Confirmation Instructions', type: :mailer do describe :notify do let(:account) { create(:account) } - let(:confirmable_user) { build(:user, inviter: inviter_val, account: account) } + let(:confirmable_user) { create(:user, inviter: inviter_val, account: account) } let(:inviter_val) { nil } let(:mail) { Devise::Mailer.confirmation_instructions(confirmable_user, nil, {}) } @@ -27,11 +27,9 @@ RSpec.describe 'Confirmation Instructions', type: :mailer do let(:inviter_val) { create(:user, :administrator, skip_confirmation: true, account: account) } it 'refers to the inviter and their account' do - Current.account = account expect(mail.body).to match( - "#{CGI.escapeHTML(inviter_val.name)}, with #{CGI.escapeHTML(inviter_val.account.name)}, has invited you to try out Chatwoot!" + "#{CGI.escapeHTML(inviter_val.name)}, with #{CGI.escapeHTML(account.name)}, has invited you to try out Chatwoot!" ) - Current.account = nil end end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index b219a337c..4c8b38e25 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -66,6 +66,7 @@ RSpec.configure do |config| config.include SlackStubs config.include Devise::Test::IntegrationHelpers, type: :request config.include ActiveSupport::Testing::TimeHelpers + config.include ActionCable::TestHelper end Shoulda::Matchers.configure do |config| diff --git a/spec/requests/api/v1/accounts/integrations/slack_request_spec.rb b/spec/requests/api/v1/accounts/integrations/slack_request_spec.rb index c61c8d376..0815c6eba 100644 --- a/spec/requests/api/v1/accounts/integrations/slack_request_spec.rb +++ b/spec/requests/api/v1/accounts/integrations/slack_request_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' RSpec.describe 'Api::V1::Accounts::Integrations::Slacks', type: :request do let(:account) { create(:account) } - let(:agent) { create(:user, account: account, role: :agent) } + let(:agent) { create(:user, account: account, role: :administrator) } let!(:hook) { create(:integrations_hook, account: account) } describe 'POST /api/v1/accounts/{account.id}/integrations/slack' do diff --git a/spec/services/facebook/send_on_facebook_service_spec.rb b/spec/services/facebook/send_on_facebook_service_spec.rb index e84319a26..8ff842591 100644 --- a/spec/services/facebook/send_on_facebook_service_spec.rb +++ b/spec/services/facebook/send_on_facebook_service_spec.rb @@ -10,7 +10,7 @@ describe Facebook::SendOnFacebookService do end let!(:account) { create(:account) } - let(:bot) { class_double('FacebookBot::Bot').as_stubbed_const } + let(:bot) { class_double('Facebook::Messenger::Bot').as_stubbed_const } let!(:widget_inbox) { create(:inbox, account: account) } let!(:facebook_channel) { create(:channel_facebook_page, account: account) } let!(:facebook_inbox) { create(:inbox, channel: facebook_channel, account: account) } @@ -61,7 +61,7 @@ describe Facebook::SendOnFacebookService do expect(bot).to have_received(:deliver).with({ recipient: { id: contact_inbox.source_id }, message: { text: message.content } - }, { access_token: facebook_channel.page_access_token }) + }, { page_id: facebook_channel.page_id }) expect(bot).to have_received(:deliver).with({ recipient: { id: contact_inbox.source_id }, message: { @@ -72,7 +72,7 @@ describe Facebook::SendOnFacebookService do } } } - }, { access_token: facebook_channel.page_access_token }) + }, { page_id: facebook_channel.page_id }) end end end diff --git a/spec/services/message_templates/hook_execution_service_spec.rb b/spec/services/message_templates/hook_execution_service_spec.rb index 2b8748daf..435d6b4d3 100644 --- a/spec/services/message_templates/hook_execution_service_spec.rb +++ b/spec/services/message_templates/hook_execution_service_spec.rb @@ -7,7 +7,7 @@ describe ::MessageTemplates::HookExecutionService do conversation = create(:conversation, contact: contact) # ensure greeting hook is enabled and greeting_message is present - conversation.inbox.update(greeting_enabled: true, greeting_message: 'Hi, this is a greeting message') + conversation.inbox.update(greeting_enabled: true, enable_email_collect: true, greeting_message: 'Hi, this is a greeting message') email_collect_service = double greeting_service = double @@ -55,7 +55,7 @@ describe ::MessageTemplates::HookExecutionService do contact = create(:contact, email: nil) conversation = create(:conversation, contact: contact) # ensure greeting hook is enabled - conversation.inbox.update(greeting_enabled: true) + conversation.inbox.update(greeting_enabled: true, enable_email_collect: true) email_collect_service = double @@ -70,6 +70,21 @@ describe ::MessageTemplates::HookExecutionService do expect(::MessageTemplates::Template::EmailCollect).to have_received(:new).with(conversation: message.conversation) expect(email_collect_service).to have_received(:perform) end + + it 'doesnot calls ::MessageTemplates::Template::EmailCollect when enable_email_collect form is disabled' do + contact = create(:contact, email: nil) + conversation = create(:conversation, contact: contact) + + conversation.inbox.update(enable_email_collect: false) + # ensure prechat form is enabled + conversation.inbox.channel.update(pre_chat_form_enabled: true) + allow(::MessageTemplates::Template::EmailCollect).to receive(:new).and_return(true) + + # described class gets called in message after commit + message = create(:message, conversation: conversation) + + expect(::MessageTemplates::Template::EmailCollect).not_to have_received(:new).with(conversation: message.conversation) + end end # TODO: remove this if this hook is removed diff --git a/spec/workers/conversation_reply_email_worker_spec.rb b/spec/workers/conversation_reply_email_worker_spec.rb index 11be19b3f..469431c61 100644 --- a/spec/workers/conversation_reply_email_worker_spec.rb +++ b/spec/workers/conversation_reply_email_worker_spec.rb @@ -6,15 +6,18 @@ RSpec.describe ConversationReplyEmailWorker, type: :worker do let(:scheduled_job) { described_class.perform_at(perform_at, 1, Time.zone.now) } let(:conversation) { build(:conversation, display_id: nil) } let(:message) { build(:message, conversation: conversation, content_type: 'incoming_email', inbox: conversation.inbox) } + let(:mailer) { double } + let(:mailer_action) { double } describe 'testing ConversationSummaryEmailWorker' do before do conversation.save! allow(Conversation).to receive(:find).and_return(conversation) - mailer = double - allow(ConversationReplyMailer).to receive(:reply_with_summary).and_return(mailer) - allow(ConversationReplyMailer).to receive(:reply_without_summary).and_return(mailer) - allow(mailer).to receive(:deliver_later).and_return(true) + allow(ConversationReplyMailer).to receive(:with).and_return(mailer) + allow(ConversationReplyMailer).to receive(:with).and_return(mailer) + allow(mailer).to receive(:reply_with_summary).and_return(mailer_action) + allow(mailer).to receive(:reply_without_summary).and_return(mailer_action) + allow(mailer_action).to receive(:deliver_later).and_return(true) end it 'worker jobs are enqueued in the mailers queue' do @@ -32,13 +35,13 @@ RSpec.describe ConversationReplyEmailWorker, type: :worker do context 'with actions performed by the worker' do it 'calls ConversationSummaryMailer#reply_with_summary when last incoming message was not email' do described_class.new.perform(1, Time.zone.now) - expect(ConversationReplyMailer).to have_received(:reply_with_summary) + expect(mailer).to have_received(:reply_with_summary) end it 'calls ConversationSummaryMailer#reply_without_summary when last incoming message was from email' do message.save described_class.new.perform(1, Time.zone.now) - expect(ConversationReplyMailer).to have_received(:reply_without_summary) + expect(mailer).to have_received(:reply_without_summary) end end end diff --git a/stories/Sections/Button.stories.mdx b/stories/Sections/Button.stories.mdx new file mode 100644 index 000000000..ee20ce082 --- /dev/null +++ b/stories/Sections/Button.stories.mdx @@ -0,0 +1,185 @@ +import { Meta, Story, Canvas } from '@storybook/addon-docs/blocks'; + +import WootButton from '../../app/javascript/dashboard/components/ui/WootButton.vue'; + + + +# Buttons + +Buttons can be used by `woot-button` component in `vue`. You can play with all the props here 👉 [woot-button](/story/components-button--primary). The button component is made on top of [foundation button](https://get.foundation/sites/docs/button.html) +so the style of button can be used outside of vue with `button` class. + +Other than declred props the `woot-button` can be customised by providing custom classnames through `class-names` prop. + +
+
-
+
+ {{ eventBody }} +
+