+
+
+
+
+
+
diff --git a/app/jobs/hook_job.rb b/app/jobs/hook_job.rb
new file mode 100644
index 000000000..6fc8934d2
--- /dev/null
+++ b/app/jobs/hook_job.rb
@@ -0,0 +1,11 @@
+class HookJob < ApplicationJob
+ queue_as :integrations
+
+ def perform(hook, message)
+ return unless hook.slack?
+
+ Integrations::Slack::SendOnSlackService.new(message: message, hook: hook).perform
+ rescue StandardError => e
+ Raven.capture_exception(e)
+ end
+end
diff --git a/app/listeners/agent_bot_listener.rb b/app/listeners/agent_bot_listener.rb
index 4c7b4a610..b4b9cc8a3 100644
--- a/app/listeners/agent_bot_listener.rb
+++ b/app/listeners/agent_bot_listener.rb
@@ -56,6 +56,7 @@ class AgentBotListener < BaseListener
agent_bot = inbox.agent_bot_inbox.agent_bot
payload = contact_inbox.webhook_data.merge(event: __method__.to_s)
+ payload[:event_info] = event.data[:event_info]
AgentBotJob.perform_later(agent_bot.outgoing_url, payload)
end
end
diff --git a/app/listeners/hook_listener.rb b/app/listeners/hook_listener.rb
new file mode 100644
index 000000000..efe1c74e8
--- /dev/null
+++ b/app/listeners/hook_listener.rb
@@ -0,0 +1,10 @@
+class HookListener < BaseListener
+ def message_created(event)
+ message = extract_message_and_account(event)[0]
+ return unless message.reportable?
+
+ message.account.hooks.each do |hook|
+ HookJob.perform_later(hook, message)
+ end
+ end
+end
diff --git a/app/listeners/subscription_listener.rb b/app/listeners/subscription_listener.rb
deleted file mode 100644
index 947bcdee7..000000000
--- a/app/listeners/subscription_listener.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# This listener is left over from the initial version of chatwoot
-# We might reuse this later in the hosted version of chatwoot.
-
-class SubscriptionListener < BaseListener
- def subscription_created(event)
- subscription = event.data[:subscription]
- account = subscription.account
- Subscription::ChargebeeService.create_subscription(account)
- end
-
- def account_destroyed(event)
- account = event.data[:account]
- Subscription::ChargebeeService.cancel_subscription(account)
- end
-
- def agent_added(event)
- account = event.data[:account]
- Subscription::ChargebeeService.update_subscription(account)
- end
-
- def agent_removed(event)
- account = event.data[:account]
- Subscription::ChargebeeService.update_subscription(account)
- end
-
- def subscription_reactivated(event)
- account = event.data[:account]
- Subscription::ChargebeeService.reactivate_subscription(account)
- end
-
- def subscription_deactivated(event)
- account = event.data[:account]
- Subscription::ChargebeeService.deactivate_subscription(account)
- end
-end
diff --git a/app/listeners/webhook_listener.rb b/app/listeners/webhook_listener.rb
index 1244df148..14ad91709 100644
--- a/app/listeners/webhook_listener.rb
+++ b/app/listeners/webhook_listener.rb
@@ -38,6 +38,7 @@ class WebhookListener < BaseListener
inbox = contact_inbox.inbox
payload = contact_inbox.webhook_data.merge(event: __method__.to_s)
+ payload[:event_info] = event.data[:event_info]
deliver_webhook_payloads(payload, inbox)
end
diff --git a/app/mailboxes/conversation_mailbox.rb b/app/mailboxes/conversation_mailbox.rb
index b838ef585..da8afd519 100644
--- a/app/mailboxes/conversation_mailbox.rb
+++ b/app/mailboxes/conversation_mailbox.rb
@@ -20,7 +20,7 @@ class ConversationMailbox < ApplicationMailbox
def create_message
@message = @conversation.messages.create(
account_id: @conversation.account_id,
- contact_id: @conversation.contact_id,
+ sender: @conversation.contact,
content: processed_mail.text_content[:reply],
inbox_id: @conversation.inbox_id,
message_type: 'incoming',
diff --git a/app/models/account.rb b/app/models/account.rb
index 42dcf75ae..7e97365e5 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -45,13 +45,13 @@ class Account < ApplicationRecord
has_many :web_widgets, dependent: :destroy, class_name: '::Channel::WebWidget'
has_many :canned_responses, dependent: :destroy
has_many :webhooks, dependent: :destroy
- has_one :subscription, dependent: :destroy
+ has_many :labels, dependent: :destroy
has_many :notification_settings, dependent: :destroy
+ has_many :hooks, dependent: :destroy, class_name: 'Integrations::Hook'
has_flags ACCOUNT_SETTINGS_FLAGS.merge(column: 'settings_flags').merge(DEFAULT_QUERY_SETTING)
enum locale: LANGUAGES_CONFIG.map { |key, val| [val[:iso_639_1_code], key] }.to_h
- after_create :create_subscription
after_create :notify_creation
after_destroy :notify_deletion
@@ -73,22 +73,6 @@ class Account < ApplicationRecord
.map { |_| _.tag.name }
end
- def subscription_data
- agents_count = users.count
- per_agent_price = Plan.paid_plan.price
- {
- state: subscription.state,
- expiry: subscription.expiry.to_i,
- agents_count: agents_count,
- per_agent_cost: per_agent_price,
- total_cost: (per_agent_price * agents_count),
- iframe_url: Subscription::ChargebeeService.hosted_page_url(self),
- trial_expired: subscription.trial_expired?,
- account_suspended: subscription.suspended?,
- payment_source_added: subscription.payment_source_added
- }
- end
-
def webhook_data
{
id: id,
@@ -98,11 +82,6 @@ class Account < ApplicationRecord
private
- def create_subscription
- subscription = build_subscription
- subscription.save
- end
-
def notify_creation
Rails.configuration.dispatcher.dispatch(ACCOUNT_CREATED, Time.zone.now, account: self)
end
diff --git a/app/models/agent_bot.rb b/app/models/agent_bot.rb
index 8b27f0fcc..493c0ed0b 100644
--- a/app/models/agent_bot.rb
+++ b/app/models/agent_bot.rb
@@ -17,12 +17,13 @@ class AgentBot < ApplicationRecord
has_many :agent_bot_inboxes, dependent: :destroy
has_many :inboxes, through: :agent_bot_inboxes
+ has_many :messages, as: :sender, dependent: :restrict_with_exception
- def push_event_data
+ def push_event_data(inbox = nil)
{
id: id,
name: name,
- avatar_url: avatar_url,
+ avatar_url: avatar_url || inbox&.avatar_url,
type: 'agent_bot'
}
end
diff --git a/app/models/channel/web_widget.rb b/app/models/channel/web_widget.rb
index 203049f64..5b3762a5b 100644
--- a/app/models/channel/web_widget.rb
+++ b/app/models/channel/web_widget.rb
@@ -2,16 +2,15 @@
#
# Table name: channel_web_widgets
#
-# id :integer not null, primary key
-# agent_away_message :string
-# website_token :string
-# website_url :string
-# welcome_tagline :string
-# welcome_title :string
-# widget_color :string default("#1f93ff")
-# created_at :datetime not null
-# updated_at :datetime not null
-# account_id :integer
+# id :integer not null, primary key
+# website_token :string
+# website_url :string
+# welcome_tagline :string
+# welcome_title :string
+# widget_color :string default("#1f93ff")
+# created_at :datetime not null
+# updated_at :datetime not null
+# account_id :integer
#
# Indexes
#
diff --git a/app/models/concerns/availability_statusable.rb b/app/models/concerns/availability_statusable.rb
index c7a3541e2..11d1e438e 100644
--- a/app/models/concerns/availability_statusable.rb
+++ b/app/models/concerns/availability_statusable.rb
@@ -1,11 +1,30 @@
module AvailabilityStatusable
extend ActiveSupport::Concern
- def online?
- ::OnlineStatusTracker.subscription_count(pubsub_token) != 0
+ def online_presence?
+ return if user_profile_page_context?
+
+ ::OnlineStatusTracker.get_presence(availability_account_id, self.class.name, id)
end
def availability_status
- online? ? 'online' : 'offline'
+ return availability if user_profile_page_context?
+ return 'offline' unless online_presence?
+ return 'online' if is_a? Contact
+
+ ::OnlineStatusTracker.get_status(availability_account_id, id) || 'online'
+ end
+
+ def user_profile_page_context?
+ # at the moment profile pages aren't account scoped
+ # hence we will return availability attribute instead of true presence
+ # we will revisit this later
+ is_a?(User) && Current.account.blank?
+ end
+
+ def availability_account_id
+ return account_id if is_a? Contact
+
+ Current.account.id
end
end
diff --git a/app/models/contact.rb b/app/models/contact.rb
index 00783f5bc..6433ffcfa 100644
--- a/app/models/contact.rb
+++ b/app/models/contact.rb
@@ -35,11 +35,11 @@ class Contact < ApplicationRecord
has_many :conversations, dependent: :destroy
has_many :contact_inboxes, dependent: :destroy
has_many :inboxes, through: :contact_inboxes
- has_many :messages, dependent: :destroy
+ has_many :messages, as: :sender, dependent: :destroy
before_validation :downcase_email
- after_create :dispatch_create_event
- after_update :dispatch_update_event
+ after_create_commit :dispatch_create_event
+ after_update_commit :dispatch_update_event
def get_source_id(inbox_id)
contact_inboxes.find_by!(inbox_id: inbox_id).source_id
diff --git a/app/models/conversation.rb b/app/models/conversation.rb
index db25c27da..8da783235 100644
--- a/app/models/conversation.rb
+++ b/app/models/conversation.rb
@@ -5,6 +5,7 @@
# id :integer not null, primary key
# additional_attributes :jsonb
# agent_last_seen_at :datetime
+# identifier :string
# locked :boolean default(FALSE)
# status :integer default("open"), not null
# user_last_seen_at :datetime
@@ -106,10 +107,7 @@ class Conversation < ApplicationRecord
end
def webhook_data
- {
- display_id: display_id,
- additional_attributes: additional_attributes
- }
+ Conversations::EventDataPresenter.new(self).push_data
end
def notifiable_assignee_change?
@@ -190,7 +188,7 @@ class Conversation < ApplicationRecord
return unless conversation_status_changed_to_open?
return unless should_round_robin?
- inbox.next_available_agent.then { |new_assignee| update_assignee(new_assignee) }
+ ::RoundRobin::AssignmentService.new(conversation: self).perform
end
def create_status_change_message(user_name)
diff --git a/app/models/inbox.rb b/app/models/inbox.rb
index 4fb395ba5..060dee980 100644
--- a/app/models/inbox.rb
+++ b/app/models/inbox.rb
@@ -7,6 +7,8 @@
# id :integer not null, primary key
# channel_type :string
# enable_auto_assignment :boolean default(TRUE)
+# greeting_enabled :boolean default(FALSE)
+# greeting_message :string
# name :string not null
# created_at :datetime not null
# updated_at :datetime not null
@@ -40,6 +42,7 @@ class Inbox < ApplicationRecord
has_one :agent_bot_inbox, dependent: :destroy
has_one :agent_bot, through: :agent_bot_inbox
has_many :webhooks, dependent: :destroy
+ has_many :hooks, dependent: :destroy, class_name: 'Integrations::Hook'
after_destroy :delete_round_robin_agents
@@ -61,11 +64,6 @@ class Inbox < ApplicationRecord
channel.class.name.to_s == 'Channel::WebWidget'
end
- def next_available_agent
- user_id = Redis::Alfred.rpoplpush(round_robin_key, round_robin_key)
- account.users.find_by(id: user_id)
- end
-
def webhook_data
{
id: id,
@@ -76,10 +74,6 @@ class Inbox < ApplicationRecord
private
def delete_round_robin_agents
- Redis::Alfred.delete(round_robin_key)
- end
-
- def round_robin_key
- format(Constants::RedisKeys::ROUND_ROBIN_AGENTS, inbox_id: id)
+ ::RoundRobin::ManageService.new(inbox: self).clear_queue
end
end
diff --git a/app/models/inbox_member.rb b/app/models/inbox_member.rb
index 4c1e14159..4326b0a8f 100644
--- a/app/models/inbox_member.rb
+++ b/app/models/inbox_member.rb
@@ -26,14 +26,10 @@ class InboxMember < ApplicationRecord
private
def add_agent_to_round_robin
- Redis::Alfred.lpush(round_robin_key, user_id)
+ ::RoundRobin::ManageService.new(inbox: inbox).add_agent_to_queue(user_id)
end
def remove_agent_from_round_robin
- Redis::Alfred.lrem(round_robin_key, user_id)
- end
-
- def round_robin_key
- format(Constants::RedisKeys::ROUND_ROBIN_AGENTS, inbox_id: inbox_id)
+ ::RoundRobin::ManageService.new(inbox: inbox).remove_agent_from_queue(user_id)
end
end
diff --git a/app/models/integrations.rb b/app/models/integrations.rb
new file mode 100644
index 000000000..a6b681795
--- /dev/null
+++ b/app/models/integrations.rb
@@ -0,0 +1,5 @@
+module Integrations
+ def self.table_name_prefix
+ 'integrations_'
+ end
+end
diff --git a/app/models/integrations/app.rb b/app/models/integrations/app.rb
new file mode 100644
index 000000000..645ff3d92
--- /dev/null
+++ b/app/models/integrations/app.rb
@@ -0,0 +1,78 @@
+class Integrations::App
+ attr_accessor :params
+
+ def initialize(params)
+ @params = params
+ end
+
+ def id
+ params[:id]
+ end
+
+ def name
+ params[:name]
+ end
+
+ def description
+ params[:description]
+ end
+
+ def logo
+ params[:logo]
+ end
+
+ def fields
+ params[:fields]
+ end
+
+ def action
+ case params[:id]
+ when 'slack'
+ "#{params[:action]}&client_id=#{ENV['SLACK_CLIENT_ID']}&redirect_uri=#{self.class.slack_integration_url}"
+ else
+ params[:action]
+ end
+ end
+
+ def active?
+ case params[:id]
+ when 'slack'
+ ENV['SLACK_CLIENT_SECRET'].present?
+ else
+ true
+ end
+ end
+
+ def enabled?(account)
+ case params[:id]
+ when 'slack'
+ account.hooks.where(app_id: id).exists?
+ else
+ true
+ end
+ end
+
+ def hooks
+ Current.account.hooks.where(app_id: id)
+ end
+
+ def self.slack_integration_url
+ "#{ENV['FRONTEND_URL']}/app/accounts/#{Current.account.id}/settings/integrations/slack"
+ end
+
+ class << self
+ def apps
+ Hashie::Mash.new(APPS_CONFIG)
+ end
+
+ def all
+ apps.values.each_with_object([]) do |app, result|
+ result << new(app)
+ end
+ end
+
+ def find(params)
+ all.detect { |app| app.id == params[:id] }
+ end
+ end
+end
diff --git a/app/models/integrations/hook.rb b/app/models/integrations/hook.rb
new file mode 100644
index 000000000..82796a2f5
--- /dev/null
+++ b/app/models/integrations/hook.rb
@@ -0,0 +1,36 @@
+# == Schema Information
+#
+# Table name: integrations_hooks
+#
+# id :bigint not null, primary key
+# access_token :string
+# hook_type :integer default("account")
+# settings :text
+# status :integer default("disabled")
+# created_at :datetime not null
+# updated_at :datetime not null
+# account_id :integer
+# app_id :string
+# inbox_id :integer
+# reference_id :string
+#
+class Integrations::Hook < ApplicationRecord
+ validates :account_id, presence: true
+ validates :app_id, presence: true
+
+ enum status: { disabled: 0, enabled: 1 }
+
+ belongs_to :account
+ belongs_to :inbox, optional: true
+ has_secure_token :access_token
+
+ enum hook_type: { account: 0, inbox: 1 }
+
+ def app
+ @app ||= Integrations::App.find(id: app_id)
+ end
+
+ def slack?
+ app_id == 'slack'
+ end
+end
diff --git a/app/models/label.rb b/app/models/label.rb
new file mode 100644
index 000000000..edecf5922
--- /dev/null
+++ b/app/models/label.rb
@@ -0,0 +1,31 @@
+# == Schema Information
+#
+# Table name: labels
+#
+# id :bigint not null, primary key
+# color :string default("#1f93ff"), not null
+# description :text
+# show_on_sidebar :boolean
+# title :string
+# created_at :datetime not null
+# updated_at :datetime not null
+# account_id :bigint
+#
+# Indexes
+#
+# index_labels_on_account_id (account_id)
+# index_labels_on_title_and_account_id (title,account_id) UNIQUE
+#
+class Label < ApplicationRecord
+ include RegexHelper
+ belongs_to :account
+
+ validates :title,
+ presence: { message: 'must not be blank' },
+ format: { with: UNICODE_CHARACTER_NUMBER_HYPHEN_UNDERSCORE },
+ uniqueness: { scope: :account_id }
+
+ before_validation do
+ self.title = title.downcase if attribute_present?('title')
+ end
+end
diff --git a/app/models/message.rb b/app/models/message.rb
index 696ae5a2b..7b368fe93 100644
--- a/app/models/message.rb
+++ b/app/models/message.rb
@@ -8,28 +8,23 @@
# content_type :integer default("text")
# message_type :integer not null
# private :boolean default(FALSE)
+# sender_type :string
# status :integer default("sent")
# created_at :datetime not null
# updated_at :datetime not null
# account_id :integer not null
-# contact_id :bigint
# conversation_id :integer not null
# inbox_id :integer not null
+# sender_id :bigint
# source_id :string
-# user_id :integer
#
# Indexes
#
-# index_messages_on_account_id (account_id)
-# index_messages_on_contact_id (contact_id)
-# index_messages_on_conversation_id (conversation_id)
-# index_messages_on_inbox_id (inbox_id)
-# index_messages_on_source_id (source_id)
-# index_messages_on_user_id (user_id)
-#
-# Foreign Keys
-#
-# fk_rails_... (contact_id => contacts.id)
+# index_messages_on_account_id (account_id)
+# index_messages_on_conversation_id (conversation_id)
+# index_messages_on_inbox_id (inbox_id)
+# index_messages_on_sender_type_and_sender_id (sender_type,sender_id)
+# index_messages_on_source_id (source_id)
#
class Message < ApplicationRecord
@@ -65,17 +60,18 @@ class Message < ApplicationRecord
belongs_to :account
belongs_to :inbox
belongs_to :conversation, touch: true
+
+ # FIXME: phase out user and contact after 1.4 since the info is there in sender
belongs_to :user, required: false
belongs_to :contact, required: false
+ belongs_to :sender, polymorphic: true, required: false
has_many :attachments, dependent: :destroy, autosave: true, before_add: :validate_attachments_limit
after_create :reopen_conversation,
- :execute_message_template_hooks,
:notify_via_mail
- # we need to wait for the active storage attachments to be available
- after_create_commit :dispatch_create_events, :send_reply
+ after_create_commit :execute_after_create_commit_callbacks
after_update :dispatch_update_event
@@ -90,7 +86,8 @@ class Message < ApplicationRecord
conversation_id: conversation.display_id
)
data.merge!(attachments: attachments.map(&:push_event_data)) if attachments.present?
- data.merge!(sender: user.push_event_data) if user
+ data.merge!(sender: sender.push_event_data) if sender && !sender.is_a?(AgentBot)
+ data.merge!(sender: sender.push_event_data(inbox)) if sender&.is_a?(AgentBot)
data
end
@@ -107,8 +104,7 @@ class Message < ApplicationRecord
content_type: content_type,
content_attributes: content_attributes,
source_id: source_id,
- sender: user.try(:webhook_data),
- contact: contact.try(:webhook_data),
+ sender: sender.try(:webhook_data),
inbox: inbox.webhook_data,
conversation: conversation.webhook_data,
account: account.webhook_data
@@ -117,6 +113,14 @@ class Message < ApplicationRecord
private
+ def execute_after_create_commit_callbacks
+ # rails issue with order of active record callbacks being executed
+ # https://github.com/rails/rails/issues/20911
+ dispatch_create_events
+ send_reply
+ execute_message_template_hooks
+ end
+
def dispatch_create_events
Rails.configuration.dispatcher.dispatch(MESSAGE_CREATED, Time.zone.now, message: self)
@@ -132,11 +136,11 @@ class Message < ApplicationRecord
def send_reply
channel_name = conversation.inbox.channel.class.to_s
if channel_name == 'Channel::FacebookPage'
- ::Facebook::SendReplyService.new(message: self).perform
+ ::Facebook::SendOnFacebookService.new(message: self).perform
elsif channel_name == 'Channel::TwitterProfile'
- ::Twitter::SendReplyService.new(message: self).perform
+ ::Twitter::SendOnTwitterService.new(message: self).perform
elsif channel_name == 'Channel::TwilioSms'
- ::Twilio::OutgoingMessageService.new(message: self).perform
+ ::Twilio::SendOnTwilioService.new(message: self).perform
end
end
@@ -149,7 +153,6 @@ class Message < ApplicationRecord
end
def notify_via_mail
- conversation_mail_key = Redis::Alfred::CONVERSATION_MAILER_KEY % conversation.id
if Redis::Alfred.get(conversation_mail_key).nil? && conversation.contact.email? && outgoing?
# set a redis key for the conversation so that we don't need to send email for every
# new message that comes in and we dont enque the delayed sidekiq job for every message
@@ -161,6 +164,10 @@ class Message < ApplicationRecord
end
end
+ def conversation_mail_key
+ format(::Redis::Alfred::CONVERSATION_MAILER_KEY, conversation_id: conversation.id)
+ end
+
def validate_attachments_limit(_attachment)
errors.add(attachments: 'exceeded maximum allowed') if attachments.size >= NUMBER_OF_PERMITTED_ATTACHMENTS
end
diff --git a/app/models/notification.rb b/app/models/notification.rb
index 2e81d07c9..08d7eca28 100644
--- a/app/models/notification.rb
+++ b/app/models/notification.rb
@@ -37,6 +37,35 @@ class Notification < ApplicationRecord
enum notification_type: NOTIFICATION_TYPES
after_create_commit :process_notification_delivery
+ default_scope { order(id: :desc) }
+
+ PRIMARY_ACTORS = ['Conversation'].freeze
+
+ def push_event_data
+ {
+ id: id,
+ notification_type: notification_type,
+ primary_actor_type: primary_actor_type,
+ primary_actor_id: primary_actor_id,
+ primary_actor: primary_actor&.push_event_data,
+ read_at: read_at,
+ secondary_actor: secondary_actor&.push_event_data,
+ user: user&.push_event_data,
+ created_at: created_at,
+ account_id: account_id
+ }
+ end
+
+ # TODO: move to a data presenter
+ def push_message_title
+ if notification_type == 'conversation_creation'
+ return "A new conversation [ID -#{primary_actor.display_id}] has been created in #{primary_actor.inbox.name}"
+ end
+
+ return "A new conversation [ID -#{primary_actor.display_id}] has been assigned to you." if notification_type == 'conversation_assignment'
+
+ ''
+ end
private
diff --git a/app/models/notification_subscription.rb b/app/models/notification_subscription.rb
index 534bced01..dbe148f71 100644
--- a/app/models/notification_subscription.rb
+++ b/app/models/notification_subscription.rb
@@ -22,7 +22,7 @@ class NotificationSubscription < ApplicationRecord
SUBSCRIPTION_TYPES = {
browser_push: 1,
- gcm: 2
+ fcm: 2
}.freeze
enum subscription_type: SUBSCRIPTION_TYPES
diff --git a/app/models/subscription.rb b/app/models/subscription.rb
deleted file mode 100644
index 8a3afebe3..000000000
--- a/app/models/subscription.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# == Schema Information
-#
-# Table name: subscriptions
-#
-# id :integer not null, primary key
-# billing_plan :string default("trial")
-# expiry :datetime
-# payment_source_added :boolean default(FALSE)
-# pricing_version :string
-# state :integer default("trial")
-# created_at :datetime not null
-# updated_at :datetime not null
-# account_id :integer
-# stripe_customer_id :string
-#
-
-class Subscription < ApplicationRecord
- include Events::Types
-
- belongs_to :account
- before_create :set_default_billing_params
- after_create :notify_creation
-
- enum state: [:trial, :active, :cancelled]
-
- def payment_source_added!
- self.payment_source_added = true
- save
- end
-
- def trial_expired?
- (trial? && expiry < Date.current) ||
- (cancelled? && !payment_source_added)
- end
-
- def suspended?
- cancelled? && payment_source_added
- end
-
- def summary
- {
- state: state,
- expiry: expiry.to_i
- }
- end
-
- private
-
- def set_default_billing_params
- self.expiry = Time.now + Plan.default_trial_period
- self.pricing_version = Plan.default_pricing_version
- end
-
- def notify_creation
- Rails.configuration.dispatcher.dispatch(SUBSCRIPTION_CREATED, Time.zone.now, subscription: self)
- end
-end
diff --git a/app/models/user.rb b/app/models/user.rb
index 2476f586e..6a1c53eed 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -3,6 +3,7 @@
# Table name: users
#
# id :integer not null, primary key
+# availability :integer default("online")
# confirmation_sent_at :datetime
# confirmation_token :string
# confirmed_at :datetime
@@ -53,6 +54,8 @@ class User < ApplicationRecord
:validatable,
:confirmable
+ enum availability: { online: 0, offline: 1, busy: 2 }
+
# The validation below has been commented out as it does not
# work because :validatable in devise overrides this.
# validates_uniqueness_of :email, scope: :account_id
@@ -65,7 +68,7 @@ class User < ApplicationRecord
has_many :assigned_conversations, foreign_key: 'assignee_id', class_name: 'Conversation', dependent: :nullify
has_many :inbox_members, dependent: :destroy
has_many :inboxes, through: :inbox_members, source: :inbox
- has_many :messages
+ has_many :messages, as: :sender
has_many :invitees, through: :account_users, class_name: 'User', foreign_key: 'inviter_id', dependent: :nullify
has_many :notifications, dependent: :destroy
@@ -75,6 +78,7 @@ class User < ApplicationRecord
before_validation :set_password_and_uid, on: :create
after_create :create_access_token
+ after_save :update_presence_in_redis, if: :saved_change_to_availability?
def send_devise_notification(notification, *args)
devise_mailer.send(notification, self, *args).deliver_later
@@ -118,7 +122,6 @@ class User < ApplicationRecord
def serializable_hash(options = nil)
serialized_user = super(options).merge(confirmed: confirmed?)
- serialized_user.merge(subscription: account.try(:subscription).try(:summary)) if ENV['BILLING_ENABLED']
serialized_user
end
@@ -127,7 +130,8 @@ class User < ApplicationRecord
id: id,
name: name,
avatar_url: avatar_url,
- type: 'user'
+ type: 'user',
+ availability_status: availability_status
}
end
@@ -139,4 +143,12 @@ class User < ApplicationRecord
type: 'user'
}
end
+
+ private
+
+ def update_presence_in_redis
+ accounts.each do |account|
+ OnlineStatusTracker.set_status(account.id, id, availability)
+ end
+ end
end
diff --git a/app/policies/label_policy.rb b/app/policies/label_policy.rb
new file mode 100644
index 000000000..ad7ebe530
--- /dev/null
+++ b/app/policies/label_policy.rb
@@ -0,0 +1,21 @@
+class LabelPolicy < ApplicationPolicy
+ def index?
+ @account_user.administrator? || @account_user.agent?
+ end
+
+ def update?
+ @account_user.administrator?
+ end
+
+ def show?
+ @account_user.administrator?
+ end
+
+ def create?
+ @account_user.administrator?
+ end
+
+ def destroy?
+ @account_user.administrator?
+ end
+end
diff --git a/app/presenters/conversations/event_data_presenter.rb b/app/presenters/conversations/event_data_presenter.rb
index bf1b4140d..c61792459 100644
--- a/app/presenters/conversations/event_data_presenter.rb
+++ b/app/presenters/conversations/event_data_presenter.rb
@@ -5,10 +5,11 @@ class Conversations::EventDataPresenter < SimpleDelegator
def push_data
{
- id: display_id,
additional_attributes: additional_attributes,
+ id: display_id,
inbox_id: inbox_id,
messages: push_messages,
+ channel: inbox.try(:channel_type),
meta: push_meta,
status: status,
unread_count: unread_incoming_messages.count,
diff --git a/app/presenters/mail_presenter.rb b/app/presenters/mail_presenter.rb
index 31aa294c5..cd3d75fc4 100644
--- a/app/presenters/mail_presenter.rb
+++ b/app/presenters/mail_presenter.rb
@@ -80,7 +80,7 @@ class MailPresenter < SimpleDelegator
{
reply: content[0..(index - 1)].strip,
- quoted_text: content[index..-1].strip
+ quoted_text: content[index..].strip
}
end
diff --git a/app/services/base/send_on_channel_service.rb b/app/services/base/send_on_channel_service.rb
new file mode 100644
index 000000000..2aeb2df09
--- /dev/null
+++ b/app/services/base/send_on_channel_service.rb
@@ -0,0 +1,55 @@
+#######################################
+# To create an external channel reply service
+# - Inherit this as the base class.
+# - Implement `channel_class` method in your child class.
+# - Implement `perform_reply` method in your child class.
+# - Implement additional custom logic for your `perform_reply` method.
+# - When required override the validation_methods.
+# - Use Childclass.new.perform.
+######################################
+class Base::SendOnChannelService
+ pattr_initialize [:message!]
+
+ def perform
+ validate_target_channel
+ return unless outgoing_message?
+ return if invalid_message?
+
+ perform_reply
+ end
+
+ private
+
+ delegate :conversation, to: :message
+ delegate :contact, :contact_inbox, :inbox, to: :conversation
+ delegate :channel, to: :inbox
+
+ def channel_class
+ raise 'Overwrite this method in child class'
+ end
+
+ def perform_reply
+ raise 'Overwrite this method in child class'
+ end
+
+ def outgoing_message_originated_from_channel?
+ # TODO: we need to refactor this logic as more integrations comes by
+ # chatwoot messages won't have source id at the moment
+ # outgoing messages may be created in slack which should be send to the channel
+ message.source_id.present? && !message.source_id.starts_with?('slack_')
+ end
+
+ def outgoing_message?
+ message.outgoing? || message.template?
+ end
+
+ def invalid_message?
+ # private notes aren't send to the channels
+ # we should also avoid the case of message loops, when outgoing messages are created from channel
+ message.private? || outgoing_message_originated_from_channel?
+ end
+
+ def validate_target_channel
+ raise 'Invalid channel service was called' if inbox.channel.class != channel_class
+ end
+end
diff --git a/app/services/facebook/send_reply_service.rb b/app/services/facebook/send_on_facebook_service.rb
similarity index 69%
rename from app/services/facebook/send_reply_service.rb
rename to app/services/facebook/send_on_facebook_service.rb
index 9173ecac7..d27dfcf4d 100644
--- a/app/services/facebook/send_reply_service.rb
+++ b/app/services/facebook/send_on_facebook_service.rb
@@ -1,37 +1,14 @@
-class Facebook::SendReplyService
- pattr_initialize [:message!]
-
- def perform
- return if message.private
- return if inbox.channel.class.to_s != 'Channel::FacebookPage'
- return unless outgoing_message_from_chatwoot?
-
- FacebookBot::Bot.deliver(delivery_params, access_token: message.channel_token)
- end
-
+class Facebook::SendOnFacebookService < Base::SendOnChannelService
private
- delegate :contact, to: :conversation
-
- def inbox
- @inbox ||= message.inbox
+ def channel_class
+ Channel::FacebookPage
end
- def conversation
- @conversation ||= message.conversation
+ def perform_reply
+ FacebookBot::Bot.deliver(delivery_params, access_token: message.channel_token)
end
- def outgoing_message_from_chatwoot?
- # messages sent directly from chatwoot won't have source_id.
- message.outgoing? && !message.source_id
- end
-
- # def reopen_lock
- # if message.incoming? && conversation.locked?
- # conversation.unlock!
- # end
- # end
-
def fb_text_message_params
{
recipient: { id: contact.get_source_id(inbox.id) },
diff --git a/app/services/message_templates/hook_execution_service.rb b/app/services/message_templates/hook_execution_service.rb
index c626c4671..947c0f39f 100644
--- a/app/services/message_templates/hook_execution_service.rb
+++ b/app/services/message_templates/hook_execution_service.rb
@@ -4,6 +4,8 @@ class MessageTemplates::HookExecutionService
def perform
return if inbox.agent_bot_inbox&.active?
+ ::MessageTemplates::Template::Greeting.new(conversation: conversation).perform if should_send_greeting?
+
::MessageTemplates::Template::EmailCollect.new(conversation: conversation).perform if should_send_email_collect?
end
@@ -16,8 +18,16 @@ class MessageTemplates::HookExecutionService
conversation.messages.outgoing.count.zero? && conversation.messages.template.count.zero?
end
+ def should_send_greeting?
+ first_message_from_contact? && conversation.inbox.greeting_enabled?
+ end
+
+ def email_collect_was_sent?
+ conversation.messages.where(content_type: 'input_email').present?
+ end
+
def should_send_email_collect?
- !contact_has_email? && conversation.inbox.web_widget? && first_message_from_contact?
+ !contact_has_email? && conversation.inbox.web_widget? && !email_collect_was_sent?
end
def contact_has_email?
diff --git a/app/services/message_templates/template/email_collect.rb b/app/services/message_templates/template/email_collect.rb
index e9e830d3b..667b6b6cd 100644
--- a/app/services/message_templates/template/email_collect.rb
+++ b/app/services/message_templates/template/email_collect.rb
@@ -3,7 +3,6 @@ class MessageTemplates::Template::EmailCollect
def perform
ActiveRecord::Base.transaction do
- conversation.messages.create!(typical_reply_message_params)
conversation.messages.create!(ways_to_reach_you_message_params)
conversation.messages.create!(email_input_box_template_message_params)
end
@@ -17,21 +16,6 @@ class MessageTemplates::Template::EmailCollect
delegate :contact, :account, to: :conversation
delegate :inbox, to: :message
- def typical_reply_message_params
- content = @conversation.inbox&.channel&.agent_away_message
- if content.blank?
- content = I18n.t('conversations.templates.typical_reply_message_body',
- account_name: account.name)
- end
-
- {
- account_id: @conversation.account_id,
- inbox_id: @conversation.inbox_id,
- message_type: :template,
- content: content
- }
- end
-
def ways_to_reach_you_message_params
content = I18n.t('conversations.templates.ways_to_reach_you_message_body',
account_name: account.name)
diff --git a/app/services/message_templates/template/greeting.rb b/app/services/message_templates/template/greeting.rb
new file mode 100644
index 000000000..478cc9bb7
--- /dev/null
+++ b/app/services/message_templates/template/greeting.rb
@@ -0,0 +1,32 @@
+class MessageTemplates::Template::Greeting
+ pattr_initialize [:conversation!]
+
+ def perform
+ ActiveRecord::Base.transaction do
+ conversation.messages.create!(greeting_message_params)
+ end
+ rescue StandardError => e
+ Raven.capture_exception(e)
+ true
+ end
+
+ private
+
+ delegate :contact, :account, to: :conversation
+ delegate :inbox, to: :message
+
+ def greeting_message_params
+ content = @conversation.inbox&.greeting_message
+ if content.blank?
+ content = I18n.t('conversations.templates.greeting_message_body',
+ account_name: account.name)
+ end
+
+ {
+ account_id: @conversation.account_id,
+ inbox_id: @conversation.inbox_id,
+ message_type: :template,
+ content: content
+ }
+ end
+end
diff --git a/app/services/notification/push_notification_service.rb b/app/services/notification/push_notification_service.rb
index e01d19679..76aa44478 100644
--- a/app/services/notification/push_notification_service.rb
+++ b/app/services/notification/push_notification_service.rb
@@ -7,7 +7,8 @@ class Notification::PushNotificationService
return unless user_subscribed_to_notification?
notification_subscriptions.each do |subscription|
- send_browser_push(subscription) if subscription.browser_push?
+ send_browser_push(subscription)
+ send_fcm_push(subscription)
end
end
@@ -28,21 +29,9 @@ class Notification::PushNotificationService
@conversation ||= notification.primary_actor
end
- def push_message_title
- if notification.notification_type == 'conversation_creation'
- return "A new conversation [ID -#{conversation.display_id}] has been created in #{conversation.inbox.name}"
- end
-
- if notification.notification_type == 'conversation_assignment'
- return "A new conversation [ID -#{conversation.display_id}] has been assigned to you."
- end
-
- ''
- end
-
def push_message
{
- title: push_message_title,
+ title: notification.push_message_title,
tag: "#{notification.notification_type}_#{conversation.display_id}",
url: push_url
}
@@ -52,7 +41,13 @@ class Notification::PushNotificationService
app_account_conversation_url(account_id: conversation.account_id, id: conversation.display_id)
end
+ def send_browser_push?(subscription)
+ ENV['VAPID_PUBLIC_KEY'] && subscription.browser_push?
+ end
+
def send_browser_push(subscription)
+ return unless send_browser_push?(subscription)
+
Webpush.payload_send(
message: JSON.generate(push_message),
endpoint: subscription.subscription_attributes['endpoint'],
@@ -70,4 +65,24 @@ class Notification::PushNotificationService
rescue Webpush::ExpiredSubscription
subscription.destroy!
end
+
+ def send_fcm_push(subscription)
+ return unless ENV['FCM_SERVER_KEY']
+ return unless subscription.fcm?
+
+ fcm = FCM.new(ENV['FCM_SERVER_KEY'])
+ response = fcm.send([subscription.subscription_attributes['push_token']], fcm_options)
+ subscription.destroy! if JSON.parse(response[:body])['results']&.first&.keys&.include?('error')
+ end
+
+ def fcm_options
+ {
+ "notification": {
+ "title": notification.notification_type.titleize,
+ "body": notification.push_message_title
+ },
+ "data": { notification: notification.push_event_data.to_json },
+ "collapse_key": "chatwoot_#{notification.primary_actor_type.downcase}_#{notification.primary_actor_id}"
+ }
+ end
end
diff --git a/app/services/round_robin/assignment_service.rb b/app/services/round_robin/assignment_service.rb
new file mode 100644
index 000000000..26190737f
--- /dev/null
+++ b/app/services/round_robin/assignment_service.rb
@@ -0,0 +1,24 @@
+class RoundRobin::AssignmentService
+ pattr_initialize [:conversation]
+
+ def perform
+ # online agents will get priority
+ new_assignee = round_robin_manage_service.available_agent(priority_list: online_agents)
+ conversation.update(assignee: new_assignee) if new_assignee
+ end
+
+ private
+
+ def online_agents
+ online_agents = OnlineStatusTracker.get_available_users(conversation.account_id)
+ online_agents.select { |_key, value| value.eql?('online') }.keys if online_agents.present?
+ end
+
+ def round_robin_manage_service
+ @round_robin_manage_service ||= RoundRobin::ManageService.new(inbox: conversation.inbox)
+ end
+
+ def round_robin_key
+ format(::Redis::Alfred::ROUND_ROBIN_AGENTS, inbox_id: conversation.inbox_id)
+ end
+end
diff --git a/app/services/round_robin/manage_service.rb b/app/services/round_robin/manage_service.rb
new file mode 100644
index 000000000..3720b1d6d
--- /dev/null
+++ b/app/services/round_robin/manage_service.rb
@@ -0,0 +1,56 @@
+class RoundRobin::ManageService
+ pattr_initialize [:inbox!]
+
+ # called on inbox delete
+ def clear_queue
+ ::Redis::Alfred.delete(round_robin_key)
+ end
+
+ # called on inbox member create
+ def add_agent_to_queue(user_id)
+ ::Redis::Alfred.lpush(round_robin_key, user_id)
+ end
+
+ # called on inbox member delete
+ def remove_agent_from_queue(user_id)
+ ::Redis::Alfred.lrem(round_robin_key, user_id)
+ end
+
+ def available_agent(priority_list: [])
+ reset_queue unless validate_queue?
+ user_id = get_agent_via_priority_list(priority_list)
+ # incase priority list was empty or inbox members weren't present
+ user_id ||= ::Redis::Alfred.rpoplpush(round_robin_key, round_robin_key)
+ inbox.inbox_members.find_by(user_id: user_id)&.user if user_id.present?
+ end
+
+ def reset_queue
+ clear_queue
+ add_agent_to_queue(inbox.inbox_members.map(&:user_id))
+ end
+
+ private
+
+ def get_agent_via_priority_list(priority_list)
+ return if priority_list.blank?
+
+ user_id = queue.intersection(priority_list.map(&:to_s)).pop
+ if user_id.present?
+ remove_agent_from_queue(user_id)
+ add_agent_to_queue(user_id)
+ end
+ user_id
+ end
+
+ def validate_queue?
+ return true if inbox.inbox_members.map(&:user_id).sort == queue.sort.map(&:to_i)
+ end
+
+ def queue
+ ::Redis::Alfred.lrange(round_robin_key)
+ end
+
+ def round_robin_key
+ format(::Redis::Alfred::ROUND_ROBIN_AGENTS, inbox_id: inbox.id)
+ end
+end
diff --git a/app/services/subscription/chargebee_service.rb b/app/services/subscription/chargebee_service.rb
deleted file mode 100644
index 2df2af660..000000000
--- a/app/services/subscription/chargebee_service.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-class Subscription::ChargebeeService
- class << self
- def create_subscription(account)
- result = ChargeBee::Subscription.create(
- plan_id: Plan.paid_plan.id,
- customer: {
- email: account.users.administrator.try(:first).try(:email),
- first_name: account.name,
- company: account.name
- }
- )
- subscription = account.subscription
- subscription.stripe_customer_id = result.subscription.customer_id
- subscription.save
- rescue StandardError => e
- Raven.capture_exception(e)
- end
-
- def update_subscription(account)
- subscription = account.subscription
- agents_count = account.users.count
- ChargeBee::Subscription.update(subscription.stripe_customer_id, plan_quantity: agents_count)
- rescue StandardError => e
- Raven.capture_exception(e)
- end
-
- def cancel_subscription(account)
- subscription = account.subscription
- ChargeBee::Subscription.delete(subscription.stripe_customer_id)
- rescue StandardError => e
- Raven.capture_exception(e)
- end
-
- def reactivate_subscription(account)
- subscription = account.subscription
- ChargeBee::Subscription.reactivate(subscription.stripe_customer_id)
- subscription.active!
- rescue StandardError => e
- Raven.capture_exception(e)
- end
-
- def deactivate_subscription(account)
- subscription = account.subscription
- ChargeBee::Subscription.cancel(subscription.stripe_customer_id)
- subscription.cancelled!
- rescue StandardError => e
- Raven.capture_exception(e)
- end
-
- def hosted_page_url(account)
- subscription = account.subscription
-
- # result = ChargeBee::HostedPage.checkout_existing({
- # :subscription => {
- # :id => subscription.stripe_customer_id,
- # :plan_id => Plan.paid_plan.id
- # }
- # })
-
- result = ChargeBee::HostedPage.update_payment_method(
- customer: {
- id: subscription.stripe_customer_id
- }
- )
- result.hosted_page.url
- rescue StandardError => e
- Raven.capture_exception(e)
- end
- end
-end
diff --git a/app/services/twilio/incoming_message_service.rb b/app/services/twilio/incoming_message_service.rb
index f7a7d0f0c..c50016971 100644
--- a/app/services/twilio/incoming_message_service.rb
+++ b/app/services/twilio/incoming_message_service.rb
@@ -11,7 +11,7 @@ class Twilio::IncomingMessageService
account_id: @inbox.account_id,
inbox_id: @inbox.id,
message_type: :incoming,
- contact_id: @contact.id,
+ sender: @contact,
source_id: params[:SmsSid]
)
attach_files
diff --git a/app/services/twilio/outgoing_message_service.rb b/app/services/twilio/send_on_twilio_service.rb
similarity index 63%
rename from app/services/twilio/outgoing_message_service.rb
rename to app/services/twilio/send_on_twilio_service.rb
index 015b2f75f..684eb31fe 100644
--- a/app/services/twilio/outgoing_message_service.rb
+++ b/app/services/twilio/send_on_twilio_service.rb
@@ -1,22 +1,15 @@
-class Twilio::OutgoingMessageService
- pattr_initialize [:message!]
+class Twilio::SendOnTwilioService < Base::SendOnChannelService
+ private
- def perform
- return if message.private
- return if message.source_id
- return if inbox.channel.class.to_s != 'Channel::TwilioSms'
- return unless message.outgoing?
+ def channel_class
+ Channel::TwilioSms
+ end
+ def perform_reply
twilio_message = client.messages.create(message_params)
message.update!(source_id: twilio_message.sid)
end
- private
-
- delegate :conversation, to: :message
- delegate :contact, to: :conversation
- delegate :contact_inbox, to: :conversation
-
def message_params
params = {
body: message.content,
@@ -39,6 +32,10 @@ class Twilio::OutgoingMessageService
@channel ||= inbox.channel
end
+ def outgoing_message?
+ message.outgoing? || message.template?
+ end
+
def client
::Twilio::REST::Client.new(channel.account_sid, channel.auth_token)
end
diff --git a/app/services/twitter/direct_message_parser_service.rb b/app/services/twitter/direct_message_parser_service.rb
index bf83958ba..04e68e590 100644
--- a/app/services/twitter/direct_message_parser_service.rb
+++ b/app/services/twitter/direct_message_parser_service.rb
@@ -12,7 +12,7 @@ class Twitter::DirectMessageParserService < Twitter::WebhooksBaseService
account_id: @inbox.account_id,
inbox_id: @inbox.id,
message_type: outgoing_message? ? :outgoing : :incoming,
- contact_id: @contact.id,
+ sender: @contact,
source_id: direct_message_data['id']
)
end
diff --git a/app/services/twitter/send_reply_service.rb b/app/services/twitter/send_on_twitter_service.rb
similarity index 72%
rename from app/services/twitter/send_reply_service.rb
rename to app/services/twitter/send_on_twitter_service.rb
index e30786dc0..93088fb41 100644
--- a/app/services/twitter/send_reply_service.rb
+++ b/app/services/twitter/send_on_twitter_service.rb
@@ -1,16 +1,17 @@
-class Twitter::SendReplyService
+class Twitter::SendOnTwitterService < Base::SendOnChannelService
pattr_initialize [:message!]
- def perform
- return if message.private
- return if message.source_id
- return if inbox.channel.class.to_s != 'Channel::TwitterProfile'
- return unless outgoing_message_from_chatwoot?
+ private
- send_reply
+ delegate :additional_attributes, to: :contact
+
+ def channel_class
+ Channel::TwitterProfile
end
- private
+ def perform_reply
+ conversation_type == 'tweet' ? send_tweet_reply : send_direct_message
+ end
def twitter_client
Twitty::Facade.new do |config|
@@ -50,19 +51,4 @@ class Twitter::SendReplyService
Rails.logger.info 'TWITTER_TWEET_REPLY_ERROR' + response.body
end
end
-
- def send_reply
- conversation_type == 'tweet' ? send_tweet_reply : send_direct_message
- end
-
- def outgoing_message_from_chatwoot?
- message.outgoing?
- end
-
- delegate :additional_attributes, to: :contact
- delegate :contact, to: :conversation
- delegate :contact_inbox, to: :conversation
- delegate :conversation, to: :message
- delegate :inbox, to: :conversation
- delegate :channel, to: :inbox
end
diff --git a/app/services/twitter/tweet_parser_service.rb b/app/services/twitter/tweet_parser_service.rb
index 84ea87ff4..def65d35e 100644
--- a/app/services/twitter/tweet_parser_service.rb
+++ b/app/services/twitter/tweet_parser_service.rb
@@ -73,7 +73,7 @@ class Twitter::TweetParserService < Twitter::WebhooksBaseService
set_conversation
@conversation.messages.create(
account_id: @inbox.account_id,
- contact_id: @contact.id,
+ sender: @contact,
content: tweet_text,
inbox_id: @inbox.id,
message_type: message_type,
diff --git a/app/views/api/v1/accounts/contacts/index.json.jbuilder b/app/views/api/v1/accounts/contacts/index.json.jbuilder
index a162fdd82..892e5fbdd 100644
--- a/app/views/api/v1/accounts/contacts/index.json.jbuilder
+++ b/app/views/api/v1/accounts/contacts/index.json.jbuilder
@@ -1,10 +1,5 @@
json.payload do
json.array! @contacts do |contact|
- json.id contact.id
- json.name contact.name
- json.email contact.email
- json.phone_number contact.phone_number
- json.thumbnail contact.avatar_url
- json.additional_attributes contact.additional_attributes
+ json.partial! 'api/v1/models/contact.json.jbuilder', resource: contact
end
end
diff --git a/app/views/api/v1/accounts/contacts/show.json.jbuilder b/app/views/api/v1/accounts/contacts/show.json.jbuilder
index e4cb9b016..276a6323e 100644
--- a/app/views/api/v1/accounts/contacts/show.json.jbuilder
+++ b/app/views/api/v1/accounts/contacts/show.json.jbuilder
@@ -1,9 +1,3 @@
json.payload do
- json.availability_status @contact.availability_status
- json.email @contact.email
- json.id @contact.id
- json.name @contact.name
- json.phone_number @contact.phone_number
- json.thumbnail @contact.avatar_url
- json.additional_attributes @contact.additional_attributes
+ json.partial! 'api/v1/models/contact.json.jbuilder', resource: @contact
end
diff --git a/app/views/api/v1/accounts/contacts/update.json.jbuilder b/app/views/api/v1/accounts/contacts/update.json.jbuilder
index 086375760..276a6323e 100644
--- a/app/views/api/v1/accounts/contacts/update.json.jbuilder
+++ b/app/views/api/v1/accounts/contacts/update.json.jbuilder
@@ -1,8 +1,3 @@
json.payload do
- json.id @contact.id
- json.name @contact.name
- json.email @contact.email
- json.phone_number @contact.phone_number
- json.thumbnail @contact.avatar_url
- json.additional_attributes @contact.additional_attributes
+ json.partial! 'api/v1/models/contact.json.jbuilder', resource: @contact
end
diff --git a/app/views/api/v1/accounts/conversations/messages/create.json.jbuilder b/app/views/api/v1/accounts/conversations/messages/create.json.jbuilder
index 0dc5b827e..4c3eff2a9 100644
--- a/app/views/api/v1/accounts/conversations/messages/create.json.jbuilder
+++ b/app/views/api/v1/accounts/conversations/messages/create.json.jbuilder
@@ -7,4 +7,5 @@ json.content_type @message.content_type
json.content_attributes @message.content_attributes
json.created_at @message.created_at.to_i
json.private @message.private
+json.sender @message.sender.push_event_data
json.attachments @message.attachments.map(&:push_event_data) if @message.attachments.present?
diff --git a/app/views/api/v1/accounts/conversations/messages/index.json.jbuilder b/app/views/api/v1/accounts/conversations/messages/index.json.jbuilder
index e04a7dd9c..a89ea7f0c 100644
--- a/app/views/api/v1/accounts/conversations/messages/index.json.jbuilder
+++ b/app/views/api/v1/accounts/conversations/messages/index.json.jbuilder
@@ -17,6 +17,6 @@ json.payload do
json.private message.private
json.source_id message.source_id
json.attachments message.attachments.map(&:push_event_data) if message.attachments.present?
- json.sender message.user.push_event_data if message.user
+ json.sender message.sender.push_event_data if message.sender
end
end
diff --git a/app/views/api/v1/accounts/conversations/meta.json.jbuilder b/app/views/api/v1/accounts/conversations/meta.json.jbuilder
index 8895ad24d..24fbf10d5 100644
--- a/app/views/api/v1/accounts/conversations/meta.json.jbuilder
+++ b/app/views/api/v1/accounts/conversations/meta.json.jbuilder
@@ -1,7 +1,5 @@
-json.data do
- json.meta do
- json.mine_count @conversations_count[:mine_count]
- json.unassigned_count @conversations_count[:unassigned_count]
- json.all_count @conversations_count[:all_count]
- end
+json.meta do
+ json.mine_count @conversations_count[:mine_count]
+ json.unassigned_count @conversations_count[:unassigned_count]
+ json.all_count @conversations_count[:all_count]
end
diff --git a/app/views/api/v1/accounts/inboxes/create.json.jbuilder b/app/views/api/v1/accounts/inboxes/create.json.jbuilder
index a596c0dd6..c046402bc 100644
--- a/app/views/api/v1/accounts/inboxes/create.json.jbuilder
+++ b/app/views/api/v1/accounts/inboxes/create.json.jbuilder
@@ -2,12 +2,13 @@ json.id @inbox.id
json.channel_id @inbox.channel_id
json.name @inbox.name
json.channel_type @inbox.channel_type
+json.greeting_enabled @inbox.greeting_enabled
+json.greeting_message @inbox.greeting_message
json.avatar_url @inbox.try(:avatar_url)
json.website_token @inbox.channel.try(:website_token)
json.widget_color @inbox.channel.try(:widget_color)
json.website_url @inbox.channel.try(:website_url)
json.welcome_title @inbox.channel.try(:welcome_title)
json.welcome_tagline @inbox.channel.try(:welcome_tagline)
-json.agent_away_message @inbox.channel.try(:agent_away_message)
json.web_widget_script @inbox.channel.try(:web_widget_script)
json.enable_auto_assignment @inbox.enable_auto_assignment
diff --git a/app/views/api/v1/accounts/inboxes/index.json.jbuilder b/app/views/api/v1/accounts/inboxes/index.json.jbuilder
index 7d6879634..da52d6632 100644
--- a/app/views/api/v1/accounts/inboxes/index.json.jbuilder
+++ b/app/views/api/v1/accounts/inboxes/index.json.jbuilder
@@ -4,13 +4,14 @@ json.payload do
json.channel_id inbox.channel_id
json.name inbox.name
json.channel_type inbox.channel_type
+ json.greeting_enabled inbox.greeting_enabled
+ json.greeting_message inbox.greeting_message
json.avatar_url inbox.try(:avatar_url)
json.page_id inbox.channel.try(:page_id)
json.widget_color inbox.channel.try(:widget_color)
json.website_url inbox.channel.try(:website_url)
json.welcome_title inbox.channel.try(:welcome_title)
json.welcome_tagline inbox.channel.try(:welcome_tagline)
- json.agent_away_message inbox.channel.try(:agent_away_message)
json.enable_auto_assignment inbox.enable_auto_assignment
json.web_widget_script inbox.channel.try(:web_widget_script)
json.phone_number inbox.channel.try(:phone_number)
diff --git a/app/views/api/v1/accounts/inboxes/update.json.jbuilder b/app/views/api/v1/accounts/inboxes/update.json.jbuilder
index a596c0dd6..c046402bc 100644
--- a/app/views/api/v1/accounts/inboxes/update.json.jbuilder
+++ b/app/views/api/v1/accounts/inboxes/update.json.jbuilder
@@ -2,12 +2,13 @@ json.id @inbox.id
json.channel_id @inbox.channel_id
json.name @inbox.name
json.channel_type @inbox.channel_type
+json.greeting_enabled @inbox.greeting_enabled
+json.greeting_message @inbox.greeting_message
json.avatar_url @inbox.try(:avatar_url)
json.website_token @inbox.channel.try(:website_token)
json.widget_color @inbox.channel.try(:widget_color)
json.website_url @inbox.channel.try(:website_url)
json.welcome_title @inbox.channel.try(:welcome_title)
json.welcome_tagline @inbox.channel.try(:welcome_tagline)
-json.agent_away_message @inbox.channel.try(:agent_away_message)
json.web_widget_script @inbox.channel.try(:web_widget_script)
json.enable_auto_assignment @inbox.enable_auto_assignment
diff --git a/app/views/api/v1/accounts/integrations/apps/index.json.jbuilder b/app/views/api/v1/accounts/integrations/apps/index.json.jbuilder
new file mode 100644
index 000000000..dca7a4fdc
--- /dev/null
+++ b/app/views/api/v1/accounts/integrations/apps/index.json.jbuilder
@@ -0,0 +1,10 @@
+json.payload do
+ json.array! @apps do |app|
+ json.id app.id
+ json.name app.name
+ json.description app.description
+ json.logo app.logo
+ json.enabled app.enabled?(@current_account)
+ json.action app.action
+ end
+end
diff --git a/app/views/api/v1/accounts/integrations/apps/show.json.jbuilder b/app/views/api/v1/accounts/integrations/apps/show.json.jbuilder
new file mode 100644
index 000000000..93d139e9a
--- /dev/null
+++ b/app/views/api/v1/accounts/integrations/apps/show.json.jbuilder
@@ -0,0 +1,7 @@
+json.id @app.id
+json.name @app.name
+json.logo @app.logo
+json.description @app.description
+json.fields @app.fields
+json.enabled @app.enabled?(@current_account)
+json.button @app.action
diff --git a/app/views/api/v1/accounts/integrations/slack/create.json.jbuilder b/app/views/api/v1/accounts/integrations/slack/create.json.jbuilder
new file mode 100644
index 000000000..0e96489fd
--- /dev/null
+++ b/app/views/api/v1/accounts/integrations/slack/create.json.jbuilder
@@ -0,0 +1,2 @@
+json.id @hook.app_id
+json.enabled true
diff --git a/app/views/api/v1/accounts/labels/create.json.jbuilder b/app/views/api/v1/accounts/labels/create.json.jbuilder
new file mode 100644
index 000000000..edeff661f
--- /dev/null
+++ b/app/views/api/v1/accounts/labels/create.json.jbuilder
@@ -0,0 +1,5 @@
+json.id @label.id
+json.title @label.title
+json.description @label.description
+json.color @label.color
+json.show_on_sidebar @label.show_on_sidebar
diff --git a/app/views/api/v1/accounts/labels/index.json.jbuilder b/app/views/api/v1/accounts/labels/index.json.jbuilder
index 13f99090d..ddff73eec 100644
--- a/app/views/api/v1/accounts/labels/index.json.jbuilder
+++ b/app/views/api/v1/accounts/labels/index.json.jbuilder
@@ -1,8 +1,9 @@
-json.data do
- json.meta do
- end
-
- json.payload do
- json.labels @labels
+json.payload do
+ json.array! @labels do |label|
+ json.id label.id
+ json.title label.title
+ json.description label.description
+ json.color label.color
+ json.show_on_sidebar label.show_on_sidebar
end
end
diff --git a/app/views/api/v1/accounts/labels/most_used.json.jbuilder b/app/views/api/v1/accounts/labels/most_used.json.jbuilder
deleted file mode 100644
index c411b47ed..000000000
--- a/app/views/api/v1/accounts/labels/most_used.json.jbuilder
+++ /dev/null
@@ -1,3 +0,0 @@
-json.payload do
- json.labels @labels
-end
diff --git a/app/views/api/v1/accounts/labels/show.json.jbuilder b/app/views/api/v1/accounts/labels/show.json.jbuilder
new file mode 100644
index 000000000..edeff661f
--- /dev/null
+++ b/app/views/api/v1/accounts/labels/show.json.jbuilder
@@ -0,0 +1,5 @@
+json.id @label.id
+json.title @label.title
+json.description @label.description
+json.color @label.color
+json.show_on_sidebar @label.show_on_sidebar
diff --git a/app/views/api/v1/accounts/labels/update.json.jbuilder b/app/views/api/v1/accounts/labels/update.json.jbuilder
new file mode 100644
index 000000000..edeff661f
--- /dev/null
+++ b/app/views/api/v1/accounts/labels/update.json.jbuilder
@@ -0,0 +1,5 @@
+json.id @label.id
+json.title @label.title
+json.description @label.description
+json.color @label.color
+json.show_on_sidebar @label.show_on_sidebar
diff --git a/app/views/api/v1/accounts/notifications/index.json.jbuilder b/app/views/api/v1/accounts/notifications/index.json.jbuilder
new file mode 100644
index 000000000..b69d869e6
--- /dev/null
+++ b/app/views/api/v1/accounts/notifications/index.json.jbuilder
@@ -0,0 +1,20 @@
+json.data do
+ json.meta do
+ json.unread_count @unread_count
+ end
+
+ json.payload do
+ json.array! @notifications do |notification|
+ json.id notification.id
+ json.notification_type notification.notification_type
+ json.push_message_title notification.push_message_title
+ json.primary_actor_type notification.primary_actor_type
+ json.primary_actor_id notification.primary_actor_id
+ json.primary_actor notification.primary_actor&.push_event_data
+ json.read_at notification.read_at
+ json.secondary_actor notification.secondary_actor&.push_event_data
+ json.user notification.user&.push_event_data
+ json.created_at notification.created_at.to_i
+ end
+ end
+end
diff --git a/app/views/api/v1/accounts/show.json.jbuilder b/app/views/api/v1/accounts/show.json.jbuilder
index 26d1c640e..446d1accb 100644
--- a/app/views/api/v1/accounts/show.json.jbuilder
+++ b/app/views/api/v1/accounts/show.json.jbuilder
@@ -4,4 +4,4 @@ json.locale @account.locale
json.domain @account.domain
json.domain_emails_enabled @account.domain_emails_enabled
json.support_email @account.support_email
-json.features @account.enabled_features
+json.features @account.all_features
diff --git a/app/views/api/v1/conversations/partials/_conversation.json.jbuilder b/app/views/api/v1/conversations/partials/_conversation.json.jbuilder
index 148996898..e9d796c31 100644
--- a/app/views/api/v1/conversations/partials/_conversation.json.jbuilder
+++ b/app/views/api/v1/conversations/partials/_conversation.json.jbuilder
@@ -1,10 +1,8 @@
json.meta do
json.sender do
- json.id conversation.contact.id
- json.name conversation.contact.name
- json.thumbnail conversation.contact.avatar_url
- json.channel conversation.inbox.try(:channel_type)
+ json.partial! 'api/v1/models/contact.json.jbuilder', resource: conversation.contact
end
+ json.channel conversation.inbox.try(:channel_type)
json.assignee conversation.assignee
end
@@ -12,7 +10,7 @@ json.id conversation.display_id
if conversation.unread_incoming_messages.count.zero?
json.messages [conversation.messages.last.try(:push_event_data)]
else
- json.messages conversation.unread_messages.map(&:push_event_data)
+ json.messages conversation.unread_messages.includes([:user, :attachments]).map(&:push_event_data)
end
json.inbox_id conversation.inbox_id
@@ -24,3 +22,4 @@ json.agent_last_seen_at conversation.agent_last_seen_at.to_i
json.unread_count conversation.unread_incoming_messages.count
json.additional_attributes conversation.additional_attributes
json.account_id conversation.account_id
+json.labels conversation.label_list
diff --git a/app/views/api/v1/models/_contact.json.jbuilder b/app/views/api/v1/models/_contact.json.jbuilder
new file mode 100644
index 000000000..a9152712e
--- /dev/null
+++ b/app/views/api/v1/models/_contact.json.jbuilder
@@ -0,0 +1,7 @@
+json.additional_attributes resource.additional_attributes
+json.availability_status resource.availability_status
+json.email resource.email
+json.id resource.id
+json.name resource.name
+json.phone_number resource.phone_number
+json.thumbnail resource.avatar_url
diff --git a/app/views/api/v1/models/_user.json.jbuilder b/app/views/api/v1/models/_user.json.jbuilder
index 5638fc233..3efb6470b 100644
--- a/app/views/api/v1/models/_user.json.jbuilder
+++ b/app/views/api/v1/models/_user.json.jbuilder
@@ -11,6 +11,7 @@ json.inviter_id resource.active_account_user&.inviter_id
json.confirmed resource.confirmed?
json.avatar_url resource.avatar_url
json.access_token resource.access_token.token
+json.availability_status resource.availability_status
json.accounts do
json.array! resource.account_users do |account_user|
json.id account_user.account_id
diff --git a/app/views/api/v1/widget/conversations/index.json.jbuilder b/app/views/api/v1/widget/conversations/index.json.jbuilder
index 5c15a8364..8eb943785 100644
--- a/app/views/api/v1/widget/conversations/index.json.jbuilder
+++ b/app/views/api/v1/widget/conversations/index.json.jbuilder
@@ -1,5 +1,6 @@
if @conversation
json.id @conversation.display_id
json.inbox_id @conversation.inbox_id
+ json.user_last_seen_at @conversation.user_last_seen_at.to_i
json.status @conversation.status
end
diff --git a/app/views/api/v1/widget/messages/create.json.jbuilder b/app/views/api/v1/widget/messages/create.json.jbuilder
index ff442b304..57276bdf1 100644
--- a/app/views/api/v1/widget/messages/create.json.jbuilder
+++ b/app/views/api/v1/widget/messages/create.json.jbuilder
@@ -7,4 +7,4 @@ json.created_at @message.created_at.to_i
json.private @message.private
json.source_id @message.source_id
json.attachments @message.attachments.map(&:push_event_data) if @message.attachments.present?
-json.sender @message.user.push_event_data if @message.user
+json.sender @message.sender.push_event_data if @message.sender
diff --git a/app/views/api/v1/widget/messages/index.json.jbuilder b/app/views/api/v1/widget/messages/index.json.jbuilder
index f6491991c..a0462b545 100644
--- a/app/views/api/v1/widget/messages/index.json.jbuilder
+++ b/app/views/api/v1/widget/messages/index.json.jbuilder
@@ -7,5 +7,5 @@ json.array! @messages do |message|
json.created_at message.created_at.to_i
json.conversation_id message.conversation.display_id
json.attachments message.attachments.map(&:push_event_data) if message.attachments.present?
- json.sender message.user.push_event_data if message.user
+ json.sender message.sender.push_event_data if message.sender
end
diff --git a/app/views/layouts/vueapp.html.erb b/app/views/layouts/vueapp.html.erb
index f93ccb464..c2dae5279 100644
--- a/app/views/layouts/vueapp.html.erb
+++ b/app/views/layouts/vueapp.html.erb
@@ -31,7 +31,6 @@
window.chatwootConfig = {
hostURL: '<%= ENV.fetch('FRONTEND_URL', '') %>',
fbAppId: '<%= ENV.fetch('FB_APP_ID', nil) %>',
- billingEnabled: <%= ActiveModel::Type::Boolean.new.cast(ENV.fetch('BILLING_ENABLED', false)) %>,
signupEnabled: '<%= ENV.fetch('ENABLE_ACCOUNT_SIGNUP', true) %>',
<% if ENV['VAPID_PUBLIC_KEY'] %>
vapidPublicKey: new Uint8Array(<%= Base64.urlsafe_decode64(ENV['VAPID_PUBLIC_KEY']).bytes %>),
diff --git a/app/workers/conversation_reply_email_worker.rb b/app/workers/conversation_reply_email_worker.rb
index 8fed7cc12..e63d8fafb 100644
--- a/app/workers/conversation_reply_email_worker.rb
+++ b/app/workers/conversation_reply_email_worker.rb
@@ -9,7 +9,12 @@ class ConversationReplyEmailWorker
ConversationReplyMailer.reply_with_summary(@conversation, queued_time).deliver_later
# delete the redis set from the first new message on the conversation
- conversation_mail_key = Redis::Alfred::CONVERSATION_MAILER_KEY % @conversation.id
Redis::Alfred.delete(conversation_mail_key)
end
+
+ private
+
+ def conversation_mail_key
+ format(::Redis::Alfred::CONVERSATION_MAILER_KEY, conversation_id: @conversation.id)
+ end
end
diff --git a/config/application.rb b/config/application.rb
index 436a9392f..29943a011 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -27,10 +27,6 @@ module Chatwoot
config.generators.javascripts = false
config.generators.stylesheets = false
- config.action_dispatch.default_headers = {
- 'X-Frame-Options' => 'ALLOWALL'
- }
-
# Custom chatwoot configurations
config.x = config_for(:app).with_indifferent_access
end
diff --git a/config/cable.yml b/config/cable.yml
index cd363f0d5..6bb7a7844 100644
--- a/config/cable.yml
+++ b/config/cable.yml
@@ -1,17 +1,18 @@
-development:
+default: &default
adapter: redis
url: <%= ENV.fetch('REDIS_URL', 'redis://127.0.0.1:6379') %>
password: <%= ENV.fetch('REDIS_PASSWORD', nil).presence %>
+ channel_prefix: <%= "chatwoot_#{Rails.env}_action_cable" %>
+
+development:
+ <<: *default
test:
adapter: test
+ channel_prefix: <%= "chatwoot_#{Rails.env}_action_cable" %>
staging:
- adapter: redis
- url: <%= ENV.fetch('REDIS_URL', 'redis://127.0.0.1:6379') %>
- password: <%= ENV.fetch('REDIS_PASSWORD', nil).presence %>
+ <<: *default
production:
- adapter: redis
- url: <%= ENV.fetch('REDIS_URL', 'redis://127.0.0.1:6379') %>
- password: <%= ENV.fetch('REDIS_PASSWORD', nil).presence %>
+ <<: *default
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 29018c894..1dbfa63ef 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -50,4 +50,5 @@ Rails.application.configure do
# Raises error for missing translations.
# config.action_view.raise_on_missing_translations = true
+ config.log_level = ENV.fetch('LOG_LEVEL', 'debug').to_sym
end
diff --git a/config/features.yml b/config/features.yml
index 0ce1518a1..ab683e29a 100644
--- a/config/features.yml
+++ b/config/features.yml
@@ -5,4 +5,5 @@
enabled: false
- name: channel_facebook
enabled: true
-
+- name: channel_twitter
+ enabled: true
diff --git a/config/initializers/00_init.rb b/config/initializers/00_init.rb
index f2fc3c112..8a12cc226 100644
--- a/config/initializers/00_init.rb
+++ b/config/initializers/00_init.rb
@@ -1,2 +1 @@
-PLAN_CONFIG = YAML.load_file(File.join(Rails.root, 'config', 'plans.yml'))
-$chargebee = ChargeBee.configure(site: ENV['CHARGEBEE_SITE'], api_key: ENV['CHARGEBEE_API_KEY'])
+APPS_CONFIG = YAML.load_file(Rails.root.join('config/integration/apps.yml'))
diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb
index 4b828e80c..351f7b42b 100644
--- a/config/initializers/assets.rb
+++ b/config/initializers/assets.rb
@@ -12,3 +12,4 @@ Rails.application.config.assets.paths << Rails.root.join('node_modules')
# application.js, application.css, and all non-JS/CSS in the app/assets
# folder are already added.
# Rails.application.config.assets.precompile += %w( admin.js admin.css )
+Rails.application.config.assets.precompile += %w[dashboardChart.js]
diff --git a/config/initializers/custom_error_codes.rb b/config/initializers/custom_error_codes.rb
index d5efcb034..e2800b2a3 100644
--- a/config/initializers/custom_error_codes.rb
+++ b/config/initializers/custom_error_codes.rb
@@ -1,4 +1,2 @@
-Rack::Utils::HTTP_STATUS_CODES.merge!(
- 901 => 'Trial Expired',
- 902 => 'Account Suspended'
-)
+Rack::Utils::HTTP_STATUS_CODES[901] = 'Trial Expired'
+Rack::Utils::HTTP_STATUS_CODES[902] = 'Account Suspended'
diff --git a/config/initializers/languages.rb b/config/initializers/languages.rb
index c052cc474..f24571ca7 100644
--- a/config/initializers/languages.rb
+++ b/config/initializers/languages.rb
@@ -8,7 +8,7 @@ LANGUAGES_CONFIG = {
3 => { name: 'French', iso_639_3_code: 'fra', iso_639_1_code: 'fr', enabled: true },
4 => { name: 'German', iso_639_3_code: 'deu', iso_639_1_code: 'de', enabled: true },
5 => { name: 'Hindi', iso_639_3_code: 'hin', iso_639_1_code: 'hi', enabled: false },
- 6 => { name: 'Italian', iso_639_3_code: 'ita', iso_639_1_code: 'it', enabled: false },
+ 6 => { name: 'Italian', iso_639_3_code: 'ita', iso_639_1_code: 'it', enabled: true },
7 => { name: 'Japanese', iso_639_3_code: 'jpn', iso_639_1_code: 'ja', enabled: false },
8 => { name: 'Korean', iso_639_3_code: 'kor', iso_639_1_code: 'ko', enabled: false },
9 => { name: 'Portuguese', iso_639_3_code: 'por', iso_639_1_code: 'pt', enabled: true },
@@ -19,7 +19,8 @@ LANGUAGES_CONFIG = {
14 => { name: 'Catalan', iso_639_3_code: 'cat', iso_639_1_code: 'ca', enabled: true },
15 => { name: 'Greek', iso_639_3_code: 'ell', iso_639_1_code: 'el', enabled: true },
16 => { name: 'Portuguese, Brazilian', iso_639_3_code: '', iso_639_1_code: 'pt_BR', enabled: true },
- 17 => { name: 'Romanian', iso_639_3_code: 'ron', iso_639_1_code: 'ro', enabled: true }
+ 17 => { name: 'Romanian', iso_639_3_code: 'ron', iso_639_1_code: 'ro', enabled: true },
+ 18 => { name: 'Tamil', iso_639_3_code: 'tam', iso_639_1_code: 'ta', 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/redis.rb b/config/initializers/redis.rb
index d893b281c..423e0500f 100644
--- a/config/initializers/redis.rb
+++ b/config/initializers/redis.rb
@@ -4,6 +4,11 @@ app_redis_config = {
}
redis = Rails.env.test? ? MockRedis.new : Redis.new(app_redis_config)
-# Alfred - Used currently for round robin and conversation emails.
+# Alfred
# Add here as you use it for more features
+# Used for Round Robin, Conversation Emails & Online Presence
$alfred = Redis::Namespace.new('alfred', redis: redis, warning: true)
+
+# https://github.com/mperham/sidekiq/issues/4591
+# TODO once sidekiq remove we can remove this
+Redis.exists_returns_integer = false
diff --git a/config/integration/apps.yml b/config/integration/apps.yml
new file mode 100644
index 000000000..a63b5f606
--- /dev/null
+++ b/config/integration/apps.yml
@@ -0,0 +1,13 @@
+slack:
+ id: slack
+ name: Slack
+ logo: slack.png
+ 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."
+ action: https://slack.com/oauth/v2/authorize?scope=commands,chat:write,channels:read,channels:manage,channels:join,groups:write,im:write,mpim:write,users:read,users:read.email,chat:write.customize,channels:history,groups:history,mpim:history,im:history
+webhooks:
+ id: webhook
+ name: Webhooks
+ logo: cable.svg
+ 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.
+ action: /webhook
+
\ No newline at end of file
diff --git a/config/locales/ar.yml b/config/locales/ar.yml
new file mode 100644
index 000000000..5e03eeabf
--- /dev/null
+++ b/config/locales/ar.yml
@@ -0,0 +1,43 @@
+#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.
+ar:
+ hello: "مرحباً بالعالم"
+ messages:
+ reset_password_success: تم إرسال طلب إعادة تعيين كلمة المرور. يرجى مراجعة بريدك الإلكتروني للحصول على التعليمات.
+ reset_password_failure: أوه! لم نتمكن من العثور على أي مستخدم بعنوان البريد الإلكتروني المحدد.
+ errors:
+ signup:
+ disposable_email: نحن لا نسمح باسخدام عناوين البريد الإلكتروني المؤقتة
+ invalid_email: لقد قمت بإدخال عنوان بريد إلكتروني غير صالح
+ email_already_exists: "لقد قمت بالفعل بتسجيل حساب سابقاً بالعنوان %{email}"
+ failed: فشلت عملية التسجيل
+ conversations:
+ activity:
+ status:
+ resolved: "تم تحديث حالة المحادثة لـ\"مغلقة\" بواسطة %{user_name}"
+ open: "تم إعادة فتح المحادثة بواسطة %{user_name}"
+ assignee:
+ assigned: "تم إسنادها إلى %{assignee_name} بواسطة %{user_name}"
+ removed: "المحادثة غير مسندة بواسطة %{user_name}"
+ templates:
+ greeting_message_body: "%{account_name} يرد عادة خلال بضع ساعات."
+ ways_to_reach_you_message_body: "زودنا بوسيلة للتواصل معك."
+ email_input_box_message_body: "احصل على الإشعارات في البريد الإلكتروني"
+ reply:
+ email_subject: "رسائل جديدة في هذه المحادثة"
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index 250b3a003..44aa567a6 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -36,6 +36,8 @@ ca:
assigned: "Assignada a %{assignee_name} per %{user_name}"
removed: "%{user_name} ha tret l'assignació de la conversa"
templates:
- typical_reply_message_body: "%{account_name} normalment respon a les poques hores."
+ greeting_message_body: "%{account_name} normalment respon a les poques hores."
ways_to_reach_you_message_body: "Fes saber a l'equip la forma de posar-nos en contacte amb tu."
email_input_box_message_body: "Rep les notificacions per correu electrònic"
+ reply:
+ email_subject: "New messages on this conversation"
diff --git a/config/locales/cs.yml b/config/locales/cs.yml
index bf9ac59d7..59e4327c8 100644
--- a/config/locales/cs.yml
+++ b/config/locales/cs.yml
@@ -36,6 +36,8 @@ cs:
assigned: "Přiřazeno k %{assignee_name} uživatelem %{user_name}"
removed: "Konverzace zrušena uživatelem %{user_name}"
templates:
- typical_reply_message_body: "%{account_name} obvykle odpovídá za několik hodin."
+ greeting_message_body: "%{account_name} typically replies in a few hours."
ways_to_reach_you_message_body: "Dejte týmu způsob, jak se k vám dostat."
email_input_box_message_body: "Dostat upozornění e-mailem"
+ reply:
+ email_subject: "New messages on this conversation"
diff --git a/config/locales/da.yml b/config/locales/da.yml
new file mode 100644
index 000000000..2d5d6235a
--- /dev/null
+++ b/config/locales/da.yml
@@ -0,0 +1,43 @@
+#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.
+da:
+ hello: "Hello world"
+ messages:
+ reset_password_success: Woot! Request for password reset is successful. Check your mail for instructions.
+ reset_password_failure: Uh ho! We could not find any user with the specified email.
+ errors:
+ signup:
+ disposable_email: We do not allow disposable emails
+ invalid_email: You have entered an invalid email
+ email_already_exists: "You have already signed up for an account with %{email}"
+ failed: Signup failed
+ conversations:
+ activity:
+ status:
+ resolved: "Conversation was marked resolved by %{user_name}"
+ open: "Conversation was reopened by %{user_name}"
+ assignee:
+ assigned: "Assigned to %{assignee_name} by %{user_name}"
+ removed: "Conversation unassigned by %{user_name}"
+ 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"
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 5000c0344..a84cedb6d 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -1,47 +1,32 @@
-# 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.
-
+#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.
de:
hello: "Hello world"
messages:
reset_password_success: Woot! Die Anforderung zum Zurücksetzen des Passworts ist erfolgreich. Überprüfen Sie Ihre E-Mails auf Anweisungen.
reset_password_failure: Uh ho! Wir konnten keinen Benutzer mit der angegebenen E-Mail-Adresse finden.
-
errors:
signup:
disposable_email: Wir erlauben keine Einweg-E-Mails
invalid_email: Sie haben eine ungültige E-Mail-Adresse eingegeben
email_already_exists: "Sie haben sich bereits für ein Konto bei %{email} angemeldet."
failed: Anmeldung gescheitert
-
conversations:
activity:
status:
@@ -51,6 +36,8 @@ de:
assigned: "%{user_name} von %{assignee_name} zugewiesen"
removed: "Gespräch nicht zugewiesen von %{user_name}"
templates:
- typical_reply_message_body: "%{account_name} Antworten in der Regel in wenigen Stunden."
+ greeting_message_body: "%{account_name} Antworten in der Regel in wenigen Stunden."
ways_to_reach_you_message_body: "Geben Sie dem Team einen Weg, Sie zu erreichen."
email_input_box_message_body: "Lassen Sie sich per E-Mail benachrichtigen"
+ reply:
+ email_subject: "New messages on this conversation"
diff --git a/config/locales/devise.ar.yml b/config/locales/devise.ar.yml
new file mode 100644
index 000000000..03cd3353f
--- /dev/null
+++ b/config/locales/devise.ar.yml
@@ -0,0 +1,65 @@
+#Additional translations at https://github.com/plataformatec/devise/wiki/I18n
+ar:
+ devise:
+ confirmations:
+ confirmed: "تم تأكيد عنوان بريدك الإلكتروني بنجاح."
+ send_instructions: "سوف تتلقى رسالة بريد إلكتروني تحتوي على تعليمات لكيفية تأكيد عنوان البريد الإلكتروني الخاص بك خلال بضع دقائق."
+ send_paranoid_instructions: "إذا كان عنوان بريدك الإلكتروني موجود في قاعدة بياناتنا، سوف تتلقى رسالة بريد إلكتروني مع إرشادات لكيفية تأكيد عنوان البريد الإلكتروني الخاص بك خلال بضع دقائق."
+ failure:
+ already_authenticated: "لقد قمت بتسجيل الدخول."
+ inactive: "لم يتم تفعيل حسابك بعد."
+ invalid: "لم يتم التحقق من %{authentication_keys}/كلمة المرور أو أن الحساب غير مُفعّل بعد."
+ locked: "حسابك مقفل."
+ last_attempt: "لديك محاولة أخرى قبل أن يتم غلق حسابك."
+ not_found_in_database: "%{authentication_keys} أو كلمة المرور غير صحيحة."
+ timeout: "انتهت صلاحية جلستك. الرجاء تسجيل الدخول مرة أخرى للمتابعة."
+ unauthenticated: "يجب عليك تسجيل الدخول أو التسجيل قبل المتابعة."
+ unconfirmed: "يجب عليك تأكيد عنوان بريدك الإلكتروني قبل المتابعة."
+ mailer:
+ confirmation_instructions:
+ subject: "تعليمات التأكيد"
+ reset_password_instructions:
+ subject: "تعليمات إعادة تعيين كلمة المرور"
+ unlock_instructions:
+ subject: "إرشادات إلغاء القفل"
+ password_change:
+ subject: "تم تغيير كلمة المرور"
+ omniauth_callbacks:
+ failure: "تعذر المصادقة من %{kind} لأن \"%{reason}\"."
+ success: "تمت المصادقة بنجاح من حساب %{kind}."
+ 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: "سوف تتلقى رسالة بريد إلكتروني تحتوي على تعليمات إعادة ضبط كلمة المرور خلال بضع دقائق."
+ send_paranoid_instructions: "إذا كان عنوان بريدك الإلكتروني موجود في قاعدة بياناتنا، سوف تتلقى رسالة بريد إلكتروني مع إرشادات إعادة ضبط كلمة المرور خلال بضع دقائق."
+ updated: "تم تغيير كلمة المرور الخاصة بك بنجاح وتم تسجيل دخولك الآن."
+ updated_not_active: "تم تغيير كلمة المرور بنجاح."
+ registrations:
+ destroyed: "وداعاً! لقد تم إلغاء حسابك بنجاح. نأمل أن نراك مرة أخرى قريباً."
+ signed_up: "مرحبًا! لقد قمت بالتسجيل بنجاح."
+ signed_up_but_inactive: "لقد قمت بالتسجيل بنجاح. ومع ذلك، لم نستطع تسجيل دخولك لأن حسابك لم يتم تفعيله بعد."
+ 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_out: "تم تسجيل الخروج بنجاح."
+ already_signed_out: "تم تسجيل الخروج بنجاح."
+ 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_locked: "was not locked"
+ not_saved:
+ zero: "%{count} errors prohibited this %{resource} from being saved:"
+ one: "1 error prohibited this %{resource} from being saved:"
+ two: "%{count} errors prohibited this %{resource} from being saved:"
+ few: "%{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/devise.da.yml b/config/locales/devise.da.yml
new file mode 100644
index 000000000..fe5fb7196
--- /dev/null
+++ b/config/locales/devise.da.yml
@@ -0,0 +1,61 @@
+#Additional translations at https://github.com/plataformatec/devise/wiki/I18n
+da:
+ devise:
+ confirmations:
+ confirmed: "Your email address has been successfully confirmed."
+ send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
+ send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
+ failure:
+ already_authenticated: "You are already signed in."
+ inactive: "Your account is not activated yet."
+ 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:"
+ other: "%{count} errors prohibited this %{resource} from being saved:"
diff --git a/config/locales/devise.de.yml b/config/locales/devise.de.yml
new file mode 100644
index 000000000..ce5815a49
--- /dev/null
+++ b/config/locales/devise.de.yml
@@ -0,0 +1,61 @@
+#Additional translations at https://github.com/plataformatec/devise/wiki/I18n
+de:
+ devise:
+ confirmations:
+ confirmed: "Your email address has been successfully confirmed."
+ send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
+ send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
+ failure:
+ already_authenticated: "You are already signed in."
+ inactive: "Your account is not activated yet."
+ 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:"
+ other: "%{count} errors prohibited this %{resource} from being saved:"
diff --git a/config/locales/devise.fi.yml b/config/locales/devise.fi.yml
new file mode 100644
index 000000000..e2aa34829
--- /dev/null
+++ b/config/locales/devise.fi.yml
@@ -0,0 +1,61 @@
+#Additional translations at https://github.com/plataformatec/devise/wiki/I18n
+fi:
+ devise:
+ confirmations:
+ confirmed: "Your email address has been successfully confirmed."
+ send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
+ send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
+ failure:
+ already_authenticated: "You are already signed in."
+ inactive: "Your account is not activated yet."
+ 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:"
+ other: "%{count} errors prohibited this %{resource} from being saved:"
diff --git a/config/locales/devise.hi.yml b/config/locales/devise.hi.yml
new file mode 100644
index 000000000..0804597b9
--- /dev/null
+++ b/config/locales/devise.hi.yml
@@ -0,0 +1,61 @@
+#Additional translations at https://github.com/plataformatec/devise/wiki/I18n
+hi:
+ devise:
+ confirmations:
+ confirmed: "Your email address has been successfully confirmed."
+ send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
+ send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
+ failure:
+ already_authenticated: "You are already signed in."
+ inactive: "Your account is not activated yet."
+ 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:"
+ other: "%{count} errors prohibited this %{resource} from being saved:"
diff --git a/config/locales/devise.hu.yml b/config/locales/devise.hu.yml
new file mode 100644
index 000000000..bc4277617
--- /dev/null
+++ b/config/locales/devise.hu.yml
@@ -0,0 +1,61 @@
+#Additional translations at https://github.com/plataformatec/devise/wiki/I18n
+hu:
+ devise:
+ confirmations:
+ confirmed: "Your email address has been successfully confirmed."
+ send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
+ send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
+ failure:
+ already_authenticated: "You are already signed in."
+ inactive: "Your account is not activated yet."
+ 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:"
+ other: "%{count} errors prohibited this %{resource} from being saved:"
diff --git a/config/locales/devise.ja.yml b/config/locales/devise.ja.yml
new file mode 100644
index 000000000..f3f9b27e1
--- /dev/null
+++ b/config/locales/devise.ja.yml
@@ -0,0 +1,60 @@
+#Additional translations at https://github.com/plataformatec/devise/wiki/I18n
+ja:
+ devise:
+ confirmations:
+ confirmed: "Your email address has been successfully confirmed."
+ send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
+ send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
+ failure:
+ already_authenticated: "You are already signed in."
+ inactive: "Your account is not activated yet."
+ 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:
+ other: "%{count} errors prohibited this %{resource} from being saved:"
diff --git a/config/locales/devise.ko.yml b/config/locales/devise.ko.yml
new file mode 100644
index 000000000..5e55c7c06
--- /dev/null
+++ b/config/locales/devise.ko.yml
@@ -0,0 +1,60 @@
+#Additional translations at https://github.com/plataformatec/devise/wiki/I18n
+ko:
+ devise:
+ confirmations:
+ confirmed: "Your email address has been successfully confirmed."
+ send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
+ send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
+ failure:
+ already_authenticated: "You are already signed in."
+ inactive: "Your account is not activated yet."
+ 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:
+ other: "%{count} errors prohibited this %{resource} from being saved:"
diff --git a/config/locales/devise.ml.yml b/config/locales/devise.ml.yml
new file mode 100644
index 000000000..f70d9d9ef
--- /dev/null
+++ b/config/locales/devise.ml.yml
@@ -0,0 +1,61 @@
+#Additional translations at https://github.com/plataformatec/devise/wiki/I18n
+ml:
+ devise:
+ confirmations:
+ confirmed: "നിങ്ങളുടെ ഇമെയിൽ വിലാസം വിജയകരമായി സ്ഥിരീകരിച്ചു."
+ send_instructions: "കുറച്ച് മിനിറ്റിനുള്ളിൽ നിങ്ങളുടെ ഇമെയിൽ വിലാസം എങ്ങനെ സ്ഥിരീകരിക്കാമെന്നതിനുള്ള നിർദ്ദേശങ്ങളുള്ള ഒരു ഇമെയിൽ നിങ്ങൾക്ക് ലഭിക്കും.\n"
+ send_paranoid_instructions: "നിങ്ങളുടെ ഇമെയിൽ വിലാസം ഞങ്ങളുടെ ഡാറ്റാബേസിൽ ഉണ്ടെങ്കിൽ, കുറച്ച് മിനിറ്റിനുള്ളിൽ നിങ്ങളുടെ ഇമെയിൽ വിലാസം എങ്ങനെ സ്ഥിരീകരിക്കാമെന്നതിനുള്ള നിർദ്ദേശങ്ങളുള്ള ഒരു ഇമെയിൽ നിങ്ങൾക്ക് ലഭിക്കും."
+ failure:
+ already_authenticated: "You are already signed in."
+ inactive: "നിങ്ങളുടെ അക്കൗണ്ട് ഇതുവരെയും സജീവമാക്കിയിട്ടില്ല."
+ invalid: "%{authentication_keys} പാസ്വേഡോ അക്കൗണ്ടോ ഇതുവരെ പരിശോധിച്ചിട്ടില്ല.\n"
+ locked: "നിങ്ങളുടെ അക്കൗണ്ട് ലോക്കുചെയ്തു.\n"
+ last_attempt: "നിങ്ങളുടെ അക്കൗണ്ട് ലോക്കുചെയ്യുന്നതിന് മുമ്പ് നിങ്ങൾക്ക് ഒരു ശ്രമം കൂടി."
+ 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:"
+ other: "%{count} errors prohibited this %{resource} from being saved:"
diff --git a/config/locales/devise.pt_BR.yml b/config/locales/devise.pt_BR.yml
index 82e8287ca..bca956f73 100644
--- a/config/locales/devise.pt_BR.yml
+++ b/config/locales/devise.pt_BR.yml
@@ -1,5 +1,5 @@
#Additional translations at https://github.com/plataformatec/devise/wiki/I18n
-pt-BR:
+pt:
devise:
confirmations:
confirmed: "Seu e-mail foi confirmado com sucesso."
diff --git a/config/locales/devise.ro.yml b/config/locales/devise.ro.yml
new file mode 100644
index 000000000..516082ff1
--- /dev/null
+++ b/config/locales/devise.ro.yml
@@ -0,0 +1,62 @@
+#Additional translations at https://github.com/plataformatec/devise/wiki/I18n
+ro:
+ devise:
+ confirmations:
+ confirmed: "Adresa ta de e-mail a fost confirmată cu succes."
+ send_instructions: "Veţi primi un e-mail cu instrucţiuni despre cum să confirmaţi adresa de e-mail în câteva minute."
+ send_paranoid_instructions: "Dacă adresa ta de e-mail există în baza noastră de date, în câteva minute veți primi un e-mail cu instrucțiuni pentru confirmarea adresei dvs. de e-mail."
+ failure:
+ already_authenticated: "Sunteți deja conectat."
+ inactive: "Contul tău nu este încă activat."
+ invalid: "Invalid %{authentication_keys}/parola sau contul nu este verificat inca."
+ locked: "Contul dvs. este blocat."
+ last_attempt: "Mai ai încă o încercare înainte de a bloca contul."
+ not_found_in_database: "Invalid %{authentication_keys} sau parola."
+ timeout: "Sesiunea ta a expirat. Te rugăm să te autentifici din nou pentru a continua."
+ unauthenticated: "Trebuie să te autentifici sau să te înregistrezi înainte de a continua."
+ unconfirmed: "Trebuie să confirmați adresa dvs. de e-mail înainte de a continua."
+ mailer:
+ confirmation_instructions:
+ subject: "Instrucțiuni pentru confirmare"
+ reset_password_instructions:
+ subject: "Instrucțiuni pentru resetare parolă"
+ unlock_instructions:
+ subject: "Instrucțiuni pentru deblocare"
+ password_change:
+ subject: "Parolă schimbată"
+ omniauth_callbacks:
+ failure: "Nu te-am putut autentifica de la %{kind} deoarece \"%{reason}\"."
+ success: "Autentificat cu succes din contul %{kind}."
+ passwords:
+ no_token: "Nu puteți accesa această pagină fără să o accesati dintr-un e-mail de resetare a parolei. Dacă vii dintr-un e-mail de resetare a parolei, te rugăm să te asiguri că ai folosit URL-ul complet furnizat."
+ send_instructions: "Veţi primi un e-mail cu instrucţiuni despre cum să resetaţi parola în câteva minute."
+ send_paranoid_instructions: "Dacă adresa ta de e-mail există în baza noastră de date, vei primi în câteva minute un link de recuperare a parolei la adresa ta de e-mail."
+ updated: "Parola ta a fost schimbată cu succes. Acum ești conectat."
+ updated_not_active: "Parola ta a fost schimbată cu succes."
+ registrations:
+ destroyed: "La revedere! Contul tău a fost anulat cu succes. Sperăm să te vedem din nou în curând."
+ signed_up: "Bine ați venit! V-ați înregistrat cu succes."
+ signed_up_but_inactive: "V-ați înregistrat cu succes. Cu toate acestea, nu vă putem conecta deoarece contul dvs. nu este încă activat."
+ signed_up_but_locked: "V-ați înregistrat cu succes. Cu toate acestea, nu vă putem conecta deoarece contul dvs. este blocat."
+ signed_up_but_unconfirmed: "Un mesaj cu un link de confirmare a fost trimis la adresa ta de e-mail. Te rugăm să urmezi link-ul pentru a-ți activa contul."
+ update_needs_confirmation: "Ți-ai actualizat contul cu succes, dar trebuie să verificăm noua ta adresă de e-mail. Vă rugăm să verificați adresa de e-mail și să urmați link-ul de confirmare pentru a confirma noua dvs. adresă de e-mail."
+ updated: "Contul dvs. a fost actualizat cu succes."
+ sessions:
+ signed_in: "Conectat cu succes."
+ signed_out: "Deconectat cu succes."
+ already_signed_out: "Deconectat cu succes."
+ unlocks:
+ send_instructions: "Veţi primi un e-mail cu instrucţiuni despre cum să vă deblocaţi contul în câteva minute."
+ send_paranoid_instructions: "Dacă contul tău există, vei primi un e-mail cu instrucțiuni pentru cum să-l deblochezi în câteva minute."
+ unlocked: "Contul tău a fost deblocat cu succes. Te rugăm să te autentifici pentru a continua."
+ errors:
+ messages:
+ already_confirmed: "a fost deja confirmat, încercați să vă conectați"
+ confirmation_period_expired: "trebuie să fie confirmat în %{period}, vă rugăm să solicitați unul nou"
+ expired: "a expirat, vă rugăm să solicitaţi unul nou"
+ not_found: "nu a fost găsit"
+ not_locked: "nu a fost blocat"
+ not_saved:
+ one: "O eroare a împiedicat salvarea acestui %{resource}:"
+ few: "%{count} erori au împiedicat salvarea acestui %{resource}:"
+ other: "%{count} errors prohibited this %{resource} from being saved:"
diff --git a/config/locales/devise.ru.yml b/config/locales/devise.ru.yml
new file mode 100644
index 000000000..64554e0b1
--- /dev/null
+++ b/config/locales/devise.ru.yml
@@ -0,0 +1,63 @@
+#Additional translations at https://github.com/plataformatec/devise/wiki/I18n
+ru:
+ devise:
+ confirmations:
+ confirmed: "Your email address has been successfully confirmed."
+ send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
+ send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
+ failure:
+ already_authenticated: "You are already signed in."
+ inactive: "Your account is not activated yet."
+ 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:"
+ few: "%{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/devise.sk.yml b/config/locales/devise.sk.yml
new file mode 100644
index 000000000..430e8d935
--- /dev/null
+++ b/config/locales/devise.sk.yml
@@ -0,0 +1,63 @@
+#Additional translations at https://github.com/plataformatec/devise/wiki/I18n
+sk:
+ devise:
+ confirmations:
+ confirmed: "Your email address has been successfully confirmed."
+ send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
+ send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
+ failure:
+ already_authenticated: "You are already signed in."
+ inactive: "Your account is not activated yet."
+ 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:"
+ few: "%{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/devise.ta.yml b/config/locales/devise.ta.yml
new file mode 100644
index 000000000..b740b40e0
--- /dev/null
+++ b/config/locales/devise.ta.yml
@@ -0,0 +1,61 @@
+#Additional translations at https://github.com/plataformatec/devise/wiki/I18n
+ta:
+ devise:
+ confirmations:
+ confirmed: "உங்கள் இமெயில் முகவரி வெற்றிகரமாக உறுதிப்படுத்தப்பட்டுள்ளது."
+ send_instructions: "உங்கள் ஈ-மெயில் முகவரியை எவ்வாறு உறுதிப்படுத்துவது என்பதற்கான வழிமுறைகளுடன் ஒரு ஈ-மெயிலை சில நிமிடங்களில் பெறுவீர்கள்."
+ send_paranoid_instructions: "உங்கள் ஈ-மெயில் முகவரி எங்கள் டேட்டாபேசில் இருந்தால், உங்கள் ஈ-மெயில் முகவரியை எவ்வாறு உறுதிப்படுத்துவது என்பதற்கான வழிமுறைகளைக் கொண்ட ஈ-மெயிலை சில நிமிடங்களில் பெறுவீர்கள்."
+ failure:
+ already_authenticated: "நீங்கள் ஏற்கனவே சைன் இன் செய்து விட்டீர்கள்."
+ inactive: "உங்கள் கணக்கு இன்னும் ஆக்டிவேட் செய்யபடவில்லை."
+ invalid: "தவறான %{authentication_keys}/ பாஸ்வேர்ட் அல்லது கணக்கு இன்னும் சரிபார்க்கபடவில்லை."
+ locked: "உங்கள் கணக்கு முடக்கபட்டுள்ளது."
+ last_attempt: "உங்கள் கணக்கு முடக்கபடுவதற்கு முன்பு உங்களுக்கு இன்னும் ஒரு முயற்சி உள்ளது."
+ not_found_in_database: "தவறான %{authentication_keys}/ பாஸ்வேர்ட்."
+ timeout: "உங்கள் அமர்வு காலாவதியாகிவிட்டது. தொடர மீண்டும் சைன் இன் செய்யவும்."
+ unauthenticated: "தொடர்வதற்கு முன் நீங்கள் சைன் இன் செய்ய வேண்டும் அல்லது பதிவுபெற வேண்டும்."
+ unconfirmed: "தொடர்வதற்கு முன் உங்கள் இமெயில் முகவரியை உறுதிப்படுத்த வேண்டும்."
+ mailer:
+ confirmation_instructions:
+ subject: "உறுதிப்படுத்தும் வழிமுறைகள்"
+ reset_password_instructions:
+ subject: "பாஸ்வேர்ட் மீட்டமைக்கும் வழிமுறைகள்"
+ unlock_instructions:
+ subject: "முடக்கத்தை நீக்குவதற்கான வழிமுறைகள்"
+ password_change:
+ subject: "பாஸ்வேர்ட் மாற்றப்பட்டது"
+ omniauth_callbacks:
+ failure: "உங்களை %{kind} வில் இருந்து அங்கீகரிக்க முடியவில்லை, ஏனெனில் %{reason}."
+ success: "%{kind} கணக்கிலிருந்து வெற்றிகரமாக அங்கீகரிக்கப்பட்டது."
+ passwords:
+ no_token: "பாஸ்வேர்டை மீட்டமைக்கும் ஈ-மெயிலில் இருந்து வராமல் இந்த பக்கத்தை அணுக முடியாது. நீங்கள் பாஸ்வேர்டை மீட்டமைக்கும் ஈ-மெயிலில் இருந்து வந்தால், வழங்கப்பட்ட முழு URL ஐ பயன்படுத்தினீர்களா என்பதை உறுதிப்படுத்தவும்."
+ send_instructions: "உங்கள் ஈ-மெயில்மெயில் முகவரியை எவ்வாறு உறுதிப்படுத்துவது என்பதற்கான வழிமுறைகளுடன் ஒரு இமெயிலை சில நிமிடங்களில் பெறுவீர்கள்."
+ send_paranoid_instructions: "உங்கள் ஈ-மெயில் முகவரி எங்கள் டேட்டாபேசில் இருந்தால், உங்கள் ஈ-மெயில் முகவரியை எவ்வாறு உறுதிப்படுத்துவது என்பதற்கான வழிமுறைகளைக் கொண்ட ஈ-மெயிலை சில நிமிடங்களில் பெறுவீர்கள்."
+ updated: "உங்கள் பாஸ்வேர்ட் வெற்றிகரமாக மாற்றப்பட்டுள்ளது. நீங்கள் இப்போது உள்நுழைந்துள்ளீர்கள்."
+ updated_not_active: "உங்கள் பாஸ்வேர்ட் வெற்றிகரமாக மாற்றப்பட்டுள்ளது."
+ registrations:
+ destroyed: "உங்கள் கணக்கு வெற்றிகரமாக ரத்து செய்யப்பட்டது. விரைவில் உங்களை மீண்டும் சந்திப்போம் என்று நம்புகிறோம்."
+ signed_up: "வரவேற்கிறோம்! நீங்கள் வெற்றிகரமாக பதிவு செய்துள்ளீர்கள்."
+ signed_up_but_inactive: "நீங்கள் வெற்றிகரமாக பதிவு செய்துள்ளீர்கள். இருப்பினும், உங்கள் கணக்கு இன்னும் ஆக்ட்டிவேட் செய்யபடாததால் உங்களால் உள்நுழைய முடியவில்லை."
+ signed_up_but_locked: "நீங்கள் வெற்றிகரமாக பதிவு செய்துள்ளீர்கள். இருப்பினும், முடக்கப்பட்டுள்ளதால் உங்கள் கணக்கிற்குள் உள்நுழைய முடியவில்லை."
+ signed_up_but_unconfirmed: "உறுதிப்படுத்தல் இணைப்புடன் ஒரு செய்தி உங்கள் ஈ-மெயில் முகவரிக்கு அனுப்பப்பட்டுள்ளது. உங்கள் கணக்கை செயல்படுத்த இணைப்பைப் பின்தொடரவும்."
+ update_needs_confirmation: "உங்கள் கணக்கை வெற்றிகரமாக புதுப்பித்தீர்கள், ஆனால் உங்கள் புதிய ஈ-மெயில் முகவரியை நாங்கள் சரிபார்க்க வேண்டும். \nஉங்கள் புதிய ஈ-மெயில் முகவரியை உறுதிப்படுத்த உங்கள் ஈ-மெயிலை சரிபார்த்து உறுதிப்படுத்தல் இணைப்பைப் பின்தொடரவும்."
+ updated: "உங்கள் கணக்கு வெற்றிகரமாக புதுப்பிக்கப்பட்டது."
+ sessions:
+ signed_in: "வெற்றிகரமாக உள்நுழைந்தீர்கள்."
+ signed_out: "வெற்றிகரமாக வெளியேறி விட்டீர்கள்."
+ already_signed_out: "வெற்றிகரமாக வெளியேறி விட்டீர்கள்."
+ unlocks:
+ send_instructions: "உங்கள் கணக்கை எவ்வாறு திறப்பது என்பதற்கான வழிமுறைகளுடன் ஒரு ஈ-மெயிலை சில நிமிடங்களில் பெறுவீர்கள்."
+ send_paranoid_instructions: "உங்களுக்கு கணக்கு இருந்தால், சில நிமிடங்களில் அதை எவ்வாறு திறப்பது என்பதற்கான வழிமுறைகளைக் கொண்ட இமெயிலைப் பெறுவீர்கள்."
+ unlocked: "உங்கள் கணக்கு வெற்றிகரமாக திறக்கப்பட்டுள்ளது. தொடர உள்நுழைக."
+ errors:
+ messages:
+ already_confirmed: "ஏற்கனவே உறுதிப்படுத்தப்பட்டது, தயவுசெய்து உள்நுழைய முயற்சிக்கவும்"
+ confirmation_period_expired: "%{period} குள் உறுதிப்படுத்தப்பட வேண்டும், தயவுசெய்து புதியதைக் கோருங்கள்"
+ expired: "காலாவதியானது, புதியதைக் கோருங்கள்"
+ not_found: "கிடைக்கவில்லை"
+ not_locked: "முடக்கபடவில்லை"
+ not_saved:
+ one: "1 பிழை இந்த %{resource} ஐ சேமிப்பதை தடைசெய்தது:"
+ other: "%{count} பிழைகள் இந்த %{resource} ஐ சேமிப்பதை தடைசெய்தது:"
diff --git a/config/locales/devise.tr.yml b/config/locales/devise.tr.yml
new file mode 100644
index 000000000..fae21f441
--- /dev/null
+++ b/config/locales/devise.tr.yml
@@ -0,0 +1,61 @@
+#Additional translations at https://github.com/plataformatec/devise/wiki/I18n
+tr:
+ devise:
+ confirmations:
+ confirmed: "Your email address has been successfully confirmed."
+ send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
+ send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
+ failure:
+ already_authenticated: "You are already signed in."
+ inactive: "Your account is not activated yet."
+ 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:"
+ other: "%{count} errors prohibited this %{resource} from being saved:"
diff --git a/config/locales/devise.uk.yml b/config/locales/devise.uk.yml
new file mode 100644
index 000000000..c34779784
--- /dev/null
+++ b/config/locales/devise.uk.yml
@@ -0,0 +1,63 @@
+#Additional translations at https://github.com/plataformatec/devise/wiki/I18n
+uk:
+ devise:
+ confirmations:
+ confirmed: "Вашу адресу електронної пошти було успішно підтверджено."
+ send_instructions: "Ви отримаєте листа з вказівками щодо підтвердження вашого облікового запису протягом декількох хвилин."
+ send_paranoid_instructions: "Якщо ваша адреса електронної пошти існує в нашій базі даних, протягом декількох хвилин ви отримаєте лист з вказівками щодо підтвердження вашої електронної пошти."
+ failure:
+ already_authenticated: "Ви вже увійшли."
+ inactive: "Ваш обліковий запис ще не активовано."
+ invalid: "Неправильний %{authentication_keys}/пароль або обліковий запис ще не підтверджено."
+ locked: "Ваш обліковий запис заблоковано."
+ last_attempt: "У вас залишилась ще одна спроба, після якої ваш обліковий запис буде заблоковано."
+ not_found_in_database: "Недійсний %{authentication_keys} або пароль."
+ timeout: "Термін дійсності вашого сеансу завершився. Будь ласка, увійдіть знову для продовження."
+ unauthenticated: "Ви повинні увійти або зареєструватися, перш ніж продовжити."
+ unconfirmed: "Вам потрібно підтвердити вашу електронну пошту, перш ніж продовжити."
+ mailer:
+ confirmation_instructions:
+ subject: "Інструкції з підтвердження"
+ reset_password_instructions:
+ subject: "Інструкції по скиданню пароля"
+ unlock_instructions:
+ subject: "Інструкції щодо розблокування"
+ password_change:
+ subject: "Пароль змінено"
+ 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: "Ласкаво просимо! Ви успішно зареєструвалися."
+ signed_up_but_inactive: "Ви успішно зареєструвалися, але не можете увійти в систему, тому що ваш обліковий запис ще не активовано."
+ signed_up_but_locked: "Ви успішно зареєструвались. Але ми не можемо надати вам доступ до системи через те, що ваш обліковий запис заблоковано."
+ signed_up_but_unconfirmed: "Повідомлення з посиланням для підтвердження було надіслано на вашу електронну адресу. Будь ласка, перейдіть за посиланням для активації вашого облікового запису."
+ update_needs_confirmation: "Ви успішно оновили обліковий запис, але нам потрібно перевірити вашу нову адресу електронної пошти. Будь ласка, перевірте свою електронну пошту і перейдіть за посиланням для підтвердження нової адреси."
+ updated: "Ваш обліковий запис було успішно оновлено."
+ sessions:
+ signed_in: "Ви успішно увійшли в систему."
+ signed_out: "Ви успішно вийшли з системи."
+ already_signed_out: "Ви успішно вийшли з системи."
+ unlocks:
+ send_instructions: "За декілька хвилин ви отримаєте лист із вказівками щодо розблокування облікового запису."
+ send_paranoid_instructions: "Якщо ваш обліковий запис існує, за декілька хвилин ви отримаєте лист з вказівками щодо його розблокування."
+ unlocked: "Ваш обліковий запис було успішно розблоковано. Будь ласка, увійдіть в систему для продовження."
+ errors:
+ messages:
+ already_confirmed: "вже було підтверджено, будь ласка спробуйте увійти в систему"
+ confirmation_period_expired: "потребує підтвердження протягом %{period}, будь ласка, подайте новий запит"
+ expired: "has expired, please request a new one"
+ not_found: "не знайдено"
+ not_locked: "не було заблоковано"
+ not_saved:
+ one: "1 error prohibited this %{resource} from being saved:"
+ few: "%{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/devise.vi.yml b/config/locales/devise.vi.yml
new file mode 100644
index 000000000..9fdffdbc0
--- /dev/null
+++ b/config/locales/devise.vi.yml
@@ -0,0 +1,60 @@
+#Additional translations at https://github.com/plataformatec/devise/wiki/I18n
+vi:
+ devise:
+ confirmations:
+ confirmed: "Your email address has been successfully confirmed."
+ send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
+ send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
+ failure:
+ already_authenticated: "You are already signed in."
+ inactive: "Your account is not activated yet."
+ 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:
+ other: "%{count} errors prohibited this %{resource} from being saved:"
diff --git a/config/locales/devise.zh.yml b/config/locales/devise.zh.yml
new file mode 100644
index 000000000..c0e50fc34
--- /dev/null
+++ b/config/locales/devise.zh.yml
@@ -0,0 +1,60 @@
+#Additional translations at https://github.com/plataformatec/devise/wiki/I18n
+zh-TW:
+ devise:
+ confirmations:
+ confirmed: "Your email address has been successfully confirmed."
+ send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
+ send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
+ failure:
+ already_authenticated: "You are already signed in."
+ inactive: "Your account is not activated yet."
+ 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:
+ other: "%{count} errors prohibited this %{resource} from being saved:"
diff --git a/config/locales/el.yml b/config/locales/el.yml
index 6282505cf..9f0a8231b 100644
--- a/config/locales/el.yml
+++ b/config/locales/el.yml
@@ -36,7 +36,7 @@ el:
assigned: "Ανατέθηκε στον %{assignee_name} από τον %{user_name}"
removed: "Η συνομιλία σημάνθηκε ως μη ανατεθειμένη από τον %{user_name}"
templates:
- typical_reply_message_body: "Ο/Η %{account_name} συνήθως απαντάει σε μερικές ώρες."
+ greeting_message_body: "Στον λογαριασμό %{account_name} τυπικά έχετε απάντηση σε μερικές ώρες."
ways_to_reach_you_message_body: "Δώστε στην ομάδα ένα τρόπο να φτάσει σε σας."
email_input_box_message_body: "Ειδοποιηθείτε με email"
reply:
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 3b9afe3e4..f1852aa0c 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -51,7 +51,7 @@ en:
assigned: "Assigned to %{assignee_name} by %{user_name}"
removed: "Conversation unassigned by %{user_name}"
templates:
- typical_reply_message_body: "%{account_name} typically replies in a few hours."
+ 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:
diff --git a/config/locales/es.yml b/config/locales/es.yml
index a85633fc7..5b968a6d9 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -36,7 +36,7 @@ es:
assigned: "Asignado a %{assignee_name} por %{user_name}"
removed: "Conversación no asignada por %{user_name}"
templates:
- typical_reply_message_body: "%{account_name} normalmente responde en unas pocas horas."
+ greeting_message_body: "%{account_name} normalmente responde en unas pocas horas."
ways_to_reach_you_message_body: "Dale al equipo una forma de llegar a ti."
email_input_box_message_body: "Recibir notificaciones por correo electrónico"
reply:
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
new file mode 100644
index 000000000..a154b9380
--- /dev/null
+++ b/config/locales/fi.yml
@@ -0,0 +1,43 @@
+#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.
+fi:
+ hello: "Hello world"
+ messages:
+ reset_password_success: Woot! Request for password reset is successful. Check your mail for instructions.
+ reset_password_failure: Uh ho! We could not find any user with the specified email.
+ errors:
+ signup:
+ disposable_email: We do not allow disposable emails
+ invalid_email: You have entered an invalid email
+ email_already_exists: "You have already signed up for an account with %{email}"
+ failed: Signup failed
+ conversations:
+ activity:
+ status:
+ resolved: "Conversation was marked resolved by %{user_name}"
+ open: "Conversation was reopened by %{user_name}"
+ assignee:
+ assigned: "Assigned to %{assignee_name} by %{user_name}"
+ removed: "Conversation unassigned by %{user_name}"
+ 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"
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 5651c1fd5..9ff3ccbe7 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -36,7 +36,7 @@ fr:
assigned: "Assigné à %{assignee_name} par %{user_name}"
removed: "Responsable de la conversation supprimé par %{user_name}"
templates:
- typical_reply_message_body: "%{account_name} répond généralement en quelques heures."
+ greeting_message_body: "%{account_name} répond généralement en quelques heures."
ways_to_reach_you_message_body: "Donnez à l'équipe un moyen de vous recontacter."
email_input_box_message_body: "Recevez des notifications par courriel"
reply:
diff --git a/config/locales/hi.yml b/config/locales/hi.yml
new file mode 100644
index 000000000..8f38ce459
--- /dev/null
+++ b/config/locales/hi.yml
@@ -0,0 +1,43 @@
+#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.
+hi:
+ hello: "Hello world"
+ messages:
+ reset_password_success: Woot! Request for password reset is successful. Check your mail for instructions.
+ reset_password_failure: Uh ho! We could not find any user with the specified email.
+ errors:
+ signup:
+ disposable_email: We do not allow disposable emails
+ invalid_email: You have entered an invalid email
+ email_already_exists: "You have already signed up for an account with %{email}"
+ failed: Signup failed
+ conversations:
+ activity:
+ status:
+ resolved: "Conversation was marked resolved by %{user_name}"
+ open: "Conversation was reopened by %{user_name}"
+ assignee:
+ assigned: "Assigned to %{assignee_name} by %{user_name}"
+ removed: "Conversation unassigned by %{user_name}"
+ 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"
diff --git a/config/locales/hu.yml b/config/locales/hu.yml
new file mode 100644
index 000000000..06bf64d98
--- /dev/null
+++ b/config/locales/hu.yml
@@ -0,0 +1,43 @@
+#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.
+hu:
+ hello: "Hello world"
+ messages:
+ reset_password_success: Woot! Request for password reset is successful. Check your mail for instructions.
+ reset_password_failure: Uh ho! We could not find any user with the specified email.
+ errors:
+ signup:
+ disposable_email: We do not allow disposable emails
+ invalid_email: You have entered an invalid email
+ email_already_exists: "You have already signed up for an account with %{email}"
+ failed: Signup failed
+ conversations:
+ activity:
+ status:
+ resolved: "Conversation was marked resolved by %{user_name}"
+ open: "Conversation was reopened by %{user_name}"
+ assignee:
+ assigned: "Assigned to %{assignee_name} by %{user_name}"
+ removed: "Conversation unassigned by %{user_name}"
+ 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"
diff --git a/config/locales/it.yml b/config/locales/it.yml
index 1e2d843bc..4d8bdbe10 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -36,6 +36,8 @@ it:
assigned: "Assegnato a %{assignee_name} da %{user_name}"
removed: "Conversazione non assegnata da %{user_name}"
templates:
- typical_reply_message_body: "%{account_name} risponde tipicamente tra qualche ora."
+ greeting_message_body: "%{account_name}, in genere, risponde in poche ore."
ways_to_reach_you_message_body: "Offri alla squadra un modo per raggiungerti."
email_input_box_message_body: "Ricevi una notifica via email"
+ reply:
+ email_subject: "Nuovi messaggi in questa conversazione"
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
new file mode 100644
index 000000000..4040f5b4c
--- /dev/null
+++ b/config/locales/ja.yml
@@ -0,0 +1,43 @@
+#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.
+ja:
+ hello: "Hello world"
+ messages:
+ reset_password_success: Woot! Request for password reset is successful. Check your mail for instructions.
+ reset_password_failure: Uh ho! We could not find any user with the specified email.
+ errors:
+ signup:
+ disposable_email: We do not allow disposable emails
+ invalid_email: You have entered an invalid email
+ email_already_exists: "You have already signed up for an account with %{email}"
+ failed: Signup failed
+ conversations:
+ activity:
+ status:
+ resolved: "Conversation was marked resolved by %{user_name}"
+ open: "Conversation was reopened by %{user_name}"
+ assignee:
+ assigned: "Assigned to %{assignee_name} by %{user_name}"
+ removed: "Conversation unassigned by %{user_name}"
+ 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"
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
new file mode 100644
index 000000000..c6c8ae905
--- /dev/null
+++ b/config/locales/ko.yml
@@ -0,0 +1,43 @@
+#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.
+ko:
+ hello: "Hello world"
+ messages:
+ reset_password_success: Woot! Request for password reset is successful. Check your mail for instructions.
+ reset_password_failure: Uh ho! We could not find any user with the specified email.
+ errors:
+ signup:
+ disposable_email: We do not allow disposable emails
+ invalid_email: You have entered an invalid email
+ email_already_exists: "You have already signed up for an account with %{email}"
+ failed: Signup failed
+ conversations:
+ activity:
+ status:
+ resolved: "Conversation was marked resolved by %{user_name}"
+ open: "Conversation was reopened by %{user_name}"
+ assignee:
+ assigned: "Assigned to %{assignee_name} by %{user_name}"
+ removed: "Conversation unassigned by %{user_name}"
+ 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"
diff --git a/config/locales/ml.yml b/config/locales/ml.yml
index 7cf0efe49..070569897 100644
--- a/config/locales/ml.yml
+++ b/config/locales/ml.yml
@@ -36,6 +36,8 @@ ml:
assigned: "%{assignee_name} %{user_name}-നെ നിയുക്തനാക്കി "
removed: "%{user_name} സംഭാഷണം നിയുക്തമല്ലാതാക്കി"
templates:
- typical_reply_message_body: "%{account_name} സാധാരണ കുറച്ച് മണിക്കൂറിനുള്ളിൽ മറുപടി നൽകുന്നു."
+ greeting_message_body: "%{account_name} സാധാരണ കുറച്ച് മണിക്കൂറിനുള്ളിൽ മറുപടി നൽകുന്നു."
ways_to_reach_you_message_body: "നിങ്ങളിലേക്ക് എത്താൻ ടീമിന് ഒരു വഴി നൽകുക."
email_input_box_message_body: "ഇമെയിൽ വഴി അറിയിപ്പ് നേടുക"
+ reply:
+ email_subject: "New messages on this conversation"
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 370378423..77729ffee 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -36,6 +36,8 @@ nl:
assigned: "Toegewezen aan %{assignee_name} door %{user_name}"
removed: "Gesprek niet toegewezen door %{user_name}"
templates:
- typical_reply_message_body: "%{account_name} reageert meestal binnen een paar uur."
+ greeting_message_body: "%{account_name} reageert meestal binnen een paar uur."
ways_to_reach_you_message_body: "Geef het team een manier om je te bereiken."
email_input_box_message_body: "Ontvang een melding via e-mail"
+ reply:
+ email_subject: "New messages on this conversation"
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index a5f579186..fa1de7ccc 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -36,6 +36,8 @@ pl:
assigned: "Przypisane do %{assignee_name} przez %{user_name}"
removed: "Rozmowa nieprzypisana przez %{user_name}"
templates:
- typical_reply_message_body: "%{account_name} zazwyczaj odpowiada w kilka godzin."
+ greeting_message_body: "%{account_name} typically replies in a few hours."
ways_to_reach_you_message_body: "Daj drużynie możliwość dotarcia do Ciebie."
email_input_box_message_body: "Otrzymuj powiadomienia przez e-mail"
+ reply:
+ email_subject: "New messages on this conversation"
diff --git a/config/locales/pt.yml b/config/locales/pt.yml
index 554fee99a..37cb8a9f1 100644
--- a/config/locales/pt.yml
+++ b/config/locales/pt.yml
@@ -36,6 +36,8 @@ pt:
assigned: "Atribuído a %{assignee_name} por %{user_name}"
removed: "Conversa não atribuída por %{user_name}"
templates:
- typical_reply_message_body: "%{account_name} normalmente responde em algumas horas."
+ greeting_message_body: "%{account_name} typically replies in a few hours."
ways_to_reach_you_message_body: "Dê à equipe um jeito de contatá-lo."
email_input_box_message_body: "Seja notificado por e-mail"
+ reply:
+ email_subject: "New messages on this conversation"
diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml
index 4c496c68a..733a170e1 100644
--- a/config/locales/pt_BR.yml
+++ b/config/locales/pt_BR.yml
@@ -16,7 +16,7 @@
#'true': 'foo'
#To learn more, please read the Rails Internationalization guide
#available at https://guides.rubyonrails.org/i18n.html.
-pt-BR:
+pt:
hello: "Olá, mundo"
messages:
reset_password_success: Legal! A solicitação de alteração de senha foi bem sucedida. Verifique seu e-mail para obter instruções.
@@ -36,6 +36,8 @@ pt-BR:
assigned: "Atribuído a %{assignee_name} por %{user_name}"
removed: "Conversa não atribuída por %{user_name}"
templates:
- typical_reply_message_body: "%{account_name} normalmente responde em algumas horas."
+ greeting_message_body: "%{account_name} normalmente responde em algumas horas."
ways_to_reach_you_message_body: "Informe uma forma para entrarmos em contato com você."
email_input_box_message_body: "Seja notificado por e-mail"
+ reply:
+ email_subject: "Novas mensagens nesta conversa"
diff --git a/config/locales/ro.yml b/config/locales/ro.yml
index db83cb3ef..a685277e2 100644
--- a/config/locales/ro.yml
+++ b/config/locales/ro.yml
@@ -36,7 +36,7 @@ ro:
assigned: "Atribuit lui %{assignee_name} de %{user_name}"
removed: "Conversație neasociată de %{user_name}"
templates:
- typical_reply_message_body: "%{account_name} răspunde de obicei în câteva ore."
+ greeting_message_body: "%{account_name} răspunde de obicei în câteva ore."
ways_to_reach_you_message_body: "Dă-i echipei o modalitate de a te contacta."
email_input_box_message_body: "Primește notificări prin e-mail"
reply:
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
new file mode 100644
index 000000000..21cb41668
--- /dev/null
+++ b/config/locales/ru.yml
@@ -0,0 +1,43 @@
+#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.
+ru:
+ hello: "Hello world"
+ messages:
+ reset_password_success: Woot! Request for password reset is successful. Check your mail for instructions.
+ reset_password_failure: Uh ho! We could not find any user with the specified email.
+ errors:
+ signup:
+ disposable_email: We do not allow disposable emails
+ invalid_email: You have entered an invalid email
+ email_already_exists: "You have already signed up for an account with %{email}"
+ failed: Signup failed
+ conversations:
+ activity:
+ status:
+ resolved: "Conversation was marked resolved by %{user_name}"
+ open: "Conversation was reopened by %{user_name}"
+ assignee:
+ assigned: "Assigned to %{assignee_name} by %{user_name}"
+ removed: "Conversation unassigned by %{user_name}"
+ 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"
diff --git a/config/locales/sk.yml b/config/locales/sk.yml
new file mode 100644
index 000000000..99eca03a4
--- /dev/null
+++ b/config/locales/sk.yml
@@ -0,0 +1,43 @@
+#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.
+sk:
+ hello: "Hello world"
+ messages:
+ reset_password_success: Woot! Request for password reset is successful. Check your mail for instructions.
+ reset_password_failure: Uh ho! We could not find any user with the specified email.
+ errors:
+ signup:
+ disposable_email: We do not allow disposable emails
+ invalid_email: You have entered an invalid email
+ email_already_exists: "You have already signed up for an account with %{email}"
+ failed: Signup failed
+ conversations:
+ activity:
+ status:
+ resolved: "Conversation was marked resolved by %{user_name}"
+ open: "Conversation was reopened by %{user_name}"
+ assignee:
+ assigned: "Assigned to %{assignee_name} by %{user_name}"
+ removed: "Conversation unassigned by %{user_name}"
+ 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"
diff --git a/config/locales/sv.yml b/config/locales/sv.yml
index fa70633f2..6c3cf8e1e 100644
--- a/config/locales/sv.yml
+++ b/config/locales/sv.yml
@@ -36,6 +36,8 @@ sv:
assigned: "Tilldelad till %{assignee_name} av %{user_name}"
removed: "Konversation otilldelad av %{user_name}"
templates:
- typical_reply_message_body: "%{account_name} svarar vanligtvis om några timmar."
+ greeting_message_body: "%{account_name} typically replies in a few hours."
ways_to_reach_you_message_body: "Ge laget ett sätt att nå dig."
email_input_box_message_body: "Få meddelande via e-post"
+ reply:
+ email_subject: "New messages on this conversation"
diff --git a/config/locales/ta.yml b/config/locales/ta.yml
new file mode 100644
index 000000000..3f3d4d548
--- /dev/null
+++ b/config/locales/ta.yml
@@ -0,0 +1,43 @@
+#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.
+ta:
+ hello: "உலக மக்களுக்கு வணக்கம்"
+ messages:
+ reset_password_success: வூட்! பாஸ்வேர்டை மீட்டமைப்பிற்கான கோரிக்கை வெற்றிகரமாக அனுப்பப்பட்டுள்ளது. வழிமுறைகளுக்கு உங்கள் ஈ-மெயிலைப் பார்க்கவும்.
+ reset_password_failure: மன்னிக்கவும்! குறிப்பிட்ட ஈ-மெயிலுடன் எந்த பயனரையும் எங்களால் கண்டுபிடிக்க முடியவில்லை.
+ errors:
+ signup:
+ disposable_email: களைந்துவிடும் இமெயில்களை நாங்கள் அனுமதிக்க மாட்டோம்
+ invalid_email: நீங்கள் தவறான ஈ-மெயிலை உள்ளிட்டுள்ளீர்கள்
+ email_already_exists: "நீங்கள் ஏற்கனவே %{email} கொண்டு கணக்கிற்கு பதிவு செய்துள்ளீர்கள்"
+ failed: உள்நுழையும் முயறிசி தோல்வி அடைந்துள்ளது
+ conversations:
+ activity:
+ status:
+ resolved: "உரையாடலுக்கு %{user_name} தீர்வு வழங்கியுள்ளார்"
+ open: "உரையாடலை %{user_name} மீண்டும் திறந்துள்ளார்"
+ assignee:
+ assigned: "%{user_name} இதை %{assignee_name}க்கு ஒதுக்கியுள்ளார்"
+ removed: "%{user_name} இதை ஒதுக்க படாத உரையாடளாக்கியுள்ளார்"
+ templates:
+ greeting_message_body: "%{account_name} பொதுவாக சில மணிநேரங்களில் பதிலளிப்பார்."
+ ways_to_reach_you_message_body: "உங்களை அடைய அணிக்கு ஒரு வழியைக் கொடுங்கள்."
+ email_input_box_message_body: "இமெயில் மூலம் அறிய"
+ reply:
+ email_subject: "இந்த உரையாடலில் புதிய செய்திகள்"
diff --git a/config/locales/tr.yml b/config/locales/tr.yml
new file mode 100644
index 000000000..1473c7771
--- /dev/null
+++ b/config/locales/tr.yml
@@ -0,0 +1,43 @@
+#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.
+tr:
+ hello: "Hello world"
+ messages:
+ reset_password_success: Woot! Request for password reset is successful. Check your mail for instructions.
+ reset_password_failure: Uh ho! We could not find any user with the specified email.
+ errors:
+ signup:
+ disposable_email: We do not allow disposable emails
+ invalid_email: You have entered an invalid email
+ email_already_exists: "You have already signed up for an account with %{email}"
+ failed: Signup failed
+ conversations:
+ activity:
+ status:
+ resolved: "Conversation was marked resolved by %{user_name}"
+ open: "Conversation was reopened by %{user_name}"
+ assignee:
+ assigned: "Assigned to %{assignee_name} by %{user_name}"
+ removed: "Conversation unassigned by %{user_name}"
+ 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"
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
new file mode 100644
index 000000000..c5a2e54fd
--- /dev/null
+++ b/config/locales/uk.yml
@@ -0,0 +1,43 @@
+#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.
+uk:
+ hello: "Привіт, світе"
+ messages:
+ reset_password_success: Круто! Запит на скидання пароля виконано успішно. Перевірте вашу пошту за подальшими інструкціями.
+ reset_password_failure: Ой-ой! Ми не змогли знайти жодного користувача з цією адресою електронної пошти.
+ errors:
+ signup:
+ disposable_email: Ми не дозволяємо використувати одноразові адреси електронної пошти
+ invalid_email: Ви ввели неправильну адресу електронної пошти
+ email_already_exists: "Ви вже зареєстровані з адресою %{email}"
+ failed: Помилка реєстрації
+ conversations:
+ activity:
+ status:
+ resolved: "Conversation was marked resolved by %{user_name}"
+ open: "Conversation was reopened by %{user_name}"
+ assignee:
+ assigned: "Assigned to %{assignee_name} by %{user_name}"
+ removed: "Conversation unassigned by %{user_name}"
+ templates:
+ greeting_message_body: "%{account_name} typically replies in a few hours."
+ ways_to_reach_you_message_body: "Дайте команді можливість з вами зв'язатися."
+ email_input_box_message_body: "Отримувати сповіщення електронною поштою"
+ reply:
+ email_subject: "New messages on this conversation"
diff --git a/config/locales/vi.yml b/config/locales/vi.yml
new file mode 100644
index 000000000..19d4d0d7f
--- /dev/null
+++ b/config/locales/vi.yml
@@ -0,0 +1,43 @@
+#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.
+vi:
+ hello: "Hello world"
+ messages:
+ reset_password_success: Woot! Request for password reset is successful. Check your mail for instructions.
+ reset_password_failure: Uh ho! We could not find any user with the specified email.
+ errors:
+ signup:
+ disposable_email: We do not allow disposable emails
+ invalid_email: You have entered an invalid email
+ email_already_exists: "You have already signed up for an account with %{email}"
+ failed: Signup failed
+ conversations:
+ activity:
+ status:
+ resolved: "Conversation was marked resolved by %{user_name}"
+ open: "Conversation was reopened by %{user_name}"
+ assignee:
+ assigned: "Assigned to %{assignee_name} by %{user_name}"
+ removed: "Conversation unassigned by %{user_name}"
+ 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"
diff --git a/config/locales/zh.yml b/config/locales/zh.yml
new file mode 100644
index 000000000..08e0c55b5
--- /dev/null
+++ b/config/locales/zh.yml
@@ -0,0 +1,43 @@
+#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.
+zh-TW:
+ hello: "Hello world"
+ messages:
+ reset_password_success: Woot! Request for password reset is successful. Check your mail for instructions.
+ reset_password_failure: Uh ho! We could not find any user with the specified email.
+ errors:
+ signup:
+ disposable_email: We do not allow disposable emails
+ invalid_email: You have entered an invalid email
+ email_already_exists: "You have already signed up for an account with %{email}"
+ failed: Signup failed
+ conversations:
+ activity:
+ status:
+ resolved: "Conversation was marked resolved by %{user_name}"
+ open: "Conversation was reopened by %{user_name}"
+ assignee:
+ assigned: "Assigned to %{assignee_name} by %{user_name}"
+ removed: "Conversation unassigned by %{user_name}"
+ 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"
diff --git a/config/plans.yml b/config/plans.yml
deleted file mode 100644
index a00917dd4..000000000
--- a/config/plans.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-active:
- v1:
- paid:
- name: 'Platinum'
- id: 'v1-platinum'
- price: 29
- trial:
- name: 'Trial'
- id: 'v1Trial'
- price: 0
-inactive:
- v0:
- free:
- name: 'Free'
- price: 0
-trial_period: 365
-default_pricing_version: 'v1'
diff --git a/config/puma.rb b/config/puma.rb
index 0359054fb..1df88f6db 100644
--- a/config/puma.rb
+++ b/config/puma.rb
@@ -4,13 +4,13 @@
# the maximum value specified for Puma. Default is set to 5 threads for minimum
# and maximum; this matches the default thread size of Active Record.
#
-max_threads_count = ENV.fetch('RAILS_MAX_THREADS') { 5 }
+max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5)
min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count }
threads min_threads_count, max_threads_count
# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
#
-port ENV.fetch('PORT') { 3000 }
+port ENV.fetch('PORT', 3000)
# Specifies the `environment` that Puma will run in.
#
diff --git a/config/routes.rb b/config/routes.rb
index 60b5c9736..0023e81ad 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -22,82 +22,85 @@ Rails.application.routes.draw do
namespace :v1 do
# ----------------------------------
# start of account scoped api routes
- resources :accounts, only: [:create, :show, :update], module: :accounts do
+ resources :accounts, only: [:create, :show, :update] do
member do
post :update_active_at
end
- namespace :actions do
- resource :contact_merge, only: [:create]
- end
- resources :agents, except: [:show, :edit, :new]
- resources :callbacks, only: [] do
- collection do
- post :register_facebook_page
- get :register_facebook_page
- post :facebook_pages
- post :reauthorize_page
+ scope module: :accounts do
+ namespace :actions do
+ resource :contact_merge, only: [:create]
+ end
+
+ resources :agents, except: [:show, :edit, :new]
+ resources :callbacks, only: [] do
+ collection do
+ post :register_facebook_page
+ get :register_facebook_page
+ post :facebook_pages
+ post :reauthorize_page
+ end
+ end
+ resources :canned_responses, except: [:show, :edit, :new]
+ namespace :channels do
+ resource :twilio_channel, only: [:create]
+ end
+ resources :conversations, only: [:index, :create, :show] do
+ get 'meta', on: :collection
+ scope module: :conversations do
+ resources :messages, only: [:index, :create]
+ resources :assignments, only: [:create]
+ resources :labels, only: [:create, :index]
+ end
+ member do
+ post :mute
+ post :toggle_status
+ post :toggle_typing_status
+ post :update_last_seen
+ end
+ end
+
+ resources :contacts, only: [:index, :show, :update, :create] do
+ scope module: :contacts do
+ resources :conversations, only: [:index]
+ 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
+ post :set_agent_bot, on: :member
+ end
+ resources :inbox_members, only: [:create, :show], param: :inbox_id
+ resources :labels, only: [:index, :show, :create, :update, :destroy]
+
+ resources :notifications, only: [:index, :update] do
+ collection do
+ post :read_all
+ end
+ end
+ resource :notification_settings, only: [:show, :update]
+
+ resources :webhooks, except: [:show]
+ namespace :integrations do
+ resources :apps, only: [:index, :show]
+ resource :slack, only: [:create, :update, :destroy], controller: 'slack'
end
end
- resources :canned_responses, except: [:show, :edit, :new]
- namespace :channels do
- resource :twilio_channel, only: [:create]
- end
- resources :conversations, only: [:index, :create, :show] do
- get 'meta', on: :collection
- scope module: :conversations do
- resources :messages, only: [:index, :create]
- resources :assignments, only: [:create]
- resources :labels, only: [:create, :index]
- end
- member do
- post :mute
- post :toggle_status
- post :toggle_typing_status
- post :update_last_seen
- end
- end
-
- resources :contacts, only: [:index, :show, :update, :create] do
- scope module: :contacts do
- resources :conversations, only: [:index]
- 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
- post :set_agent_bot, on: :member
- end
- resources :inbox_members, only: [:create, :show], param: :inbox_id
- resources :labels, only: [:index] do
- collection do
- get :most_used
- end
- end
-
- resources :notifications, only: [:index, :update]
- resource :notification_settings, only: [:show, :update]
-
- # this block is only required if subscription via chargebee is enabled
- resources :subscriptions, only: [:index] do
- collection do
- get :summary
- end
- end
-
- resources :webhooks, except: [:show]
end
-
# end of account scoped api routes
# ----------------------------------
+ namespace :integrations do
+ resources :webhooks, only: [:create]
+ end
+
resource :profile, only: [:show, :update]
resource :notification_subscriptions, only: [:create]
@@ -106,8 +109,9 @@ Rails.application.routes.draw do
namespace :widget do
resources :events, only: [:create]
resources :messages, only: [:index, :create, :update]
- resources :conversations do
+ resources :conversations, only: [:index] do
collection do
+ post :update_last_seen
post :toggle_typing
end
end
@@ -115,12 +119,6 @@ Rails.application.routes.draw do
resources :inbox_members, only: [:index]
resources :labels, only: [:create, :destroy]
end
-
- resources :webhooks, only: [] do
- collection do
- post :chargebee
- end
- end
end
namespace :v2 do
diff --git a/config/sidekiq.yml b/config/sidekiq.yml
index 402758056..3e20bd0df 100644
--- a/config/sidekiq.yml
+++ b/config/sidekiq.yml
@@ -13,6 +13,7 @@
:queues:
- [low, 1]
- [webhooks, 1]
+ - [integrations, 2]
- [bots, 1]
- [active_storage_analysis, 1]
- [action_mailbox_incineration, 1]
diff --git a/db/migrate/20170525104650_round_robin.rb b/db/migrate/20170525104650_round_robin.rb
index 3938346c9..e0476438a 100644
--- a/db/migrate/20170525104650_round_robin.rb
+++ b/db/migrate/20170525104650_round_robin.rb
@@ -1,7 +1,7 @@
class RoundRobin < ActiveRecord::Migration[5.0]
def change
InboxMember.find_each do |im|
- round_robin_key = format(Constants::RedisKeys::ROUND_ROBIN_AGENTS, inbox_id: im.inbox_id)
+ round_robin_key = format(::Redis::Alfred::ROUND_ROBIN_AGENTS, inbox_id: im.inbox_id)
Redis::Alfred.lpush(round_robin_key, im.user_id)
end
end
diff --git a/db/migrate/20200418124534_add_sender_to_messages.rb b/db/migrate/20200418124534_add_sender_to_messages.rb
new file mode 100644
index 000000000..02efdc4a5
--- /dev/null
+++ b/db/migrate/20200418124534_add_sender_to_messages.rb
@@ -0,0 +1,25 @@
+class AddSenderToMessages < ActiveRecord::Migration[6.0]
+ def change
+ add_reference :messages, :sender, polymorphic: true, index: true
+ add_sender_from_message
+ remove_index :messages, name: 'index_messages_on_contact_id', column: 'contact_id'
+ remove_index :messages, name: 'index_messages_on_user_id', column: 'user_id'
+ remove_column :messages, :user_id, :integer # rubocop:disable Rails/BulkChangeTable
+ remove_column :messages, :contact_id, :integer
+ end
+
+ def add_sender_from_message
+ ::Message.find_in_batches do |messages_batch|
+ Rails.logger.info "migrated till #{messages_batch.first.id}\n"
+ messages_batch.each do |message|
+ # rubocop:disable Rails/SkipsModelValidations
+ message.update_columns(sender_id: message.user.id, sender_type: 'User') if message.user.present?
+ message.update_columns(sender_id: message.contact.id, sender_type: 'Contact') if message.contact.present?
+ if message.sender.nil?
+ message.update_columns(sender_id: message.conversation.contact.id, sender_type: 'Contact') if message.incoming?
+ end
+ # rubocop:enable Rails/SkipsModelValidations
+ end
+ end
+ end
+end
diff --git a/db/migrate/20200430163438_create_integrations_hooks.rb b/db/migrate/20200430163438_create_integrations_hooks.rb
new file mode 100644
index 000000000..487297bf6
--- /dev/null
+++ b/db/migrate/20200430163438_create_integrations_hooks.rb
@@ -0,0 +1,15 @@
+class CreateIntegrationsHooks < ActiveRecord::Migration[6.0]
+ def change
+ create_table :integrations_hooks do |t|
+ t.integer :status, default: 0
+ t.integer :inbox_id
+ t.integer :account_id
+ t.string :app_id
+ t.text :settings
+ t.integer :hook_type, default: 0
+ t.string :reference_id
+ t.string :access_token
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20200510154151_add_reference_id_to_conversation.rb b/db/migrate/20200510154151_add_reference_id_to_conversation.rb
new file mode 100644
index 000000000..5bc0af68a
--- /dev/null
+++ b/db/migrate/20200510154151_add_reference_id_to_conversation.rb
@@ -0,0 +1,5 @@
+class AddReferenceIdToConversation < ActiveRecord::Migration[6.0]
+ def change
+ add_column :conversations, :reference_id, :string
+ end
+end
diff --git a/db/migrate/20200605130625_agent_away_message_to_auto_reply.rb b/db/migrate/20200605130625_agent_away_message_to_auto_reply.rb
new file mode 100644
index 000000000..9bf1d49c6
--- /dev/null
+++ b/db/migrate/20200605130625_agent_away_message_to_auto_reply.rb
@@ -0,0 +1,21 @@
+class AgentAwayMessageToAutoReply < ActiveRecord::Migration[6.0]
+ def change
+ add_column :inboxes, :greeting_enabled, :boolean, default: false # rubocop:disable Rails/BulkChangeTable
+ add_column :inboxes, :greeting_message, :string
+
+ migrate_agent_away_to_greeting
+
+ remove_column :channel_web_widgets, :agent_away_message, :string
+ end
+
+ def migrate_agent_away_to_greeting
+ ::Channel::WebWidget.find_in_batches do |widget_batch|
+ widget_batch.each do |widget|
+ inbox = widget.inbox
+ inbox.greeting_enabled = true
+ inbox.greeting_message = widget.agent_away_message
+ widget.save!
+ end
+ end
+ end
+end
diff --git a/db/migrate/20200606132552_create_labels.rb b/db/migrate/20200606132552_create_labels.rb
new file mode 100644
index 000000000..81ac698e9
--- /dev/null
+++ b/db/migrate/20200606132552_create_labels.rb
@@ -0,0 +1,13 @@
+class CreateLabels < ActiveRecord::Migration[6.0]
+ def change
+ create_table :labels do |t|
+ t.string :title
+ t.text :description
+ t.string :color
+ t.boolean :show_on_sidebar
+ t.references :account, index: true
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20200607140737_remove_subscriptions.rb b/db/migrate/20200607140737_remove_subscriptions.rb
new file mode 100644
index 000000000..33f756083
--- /dev/null
+++ b/db/migrate/20200607140737_remove_subscriptions.rb
@@ -0,0 +1,15 @@
+class RemoveSubscriptions < ActiveRecord::Migration[6.0]
+ def change
+ drop_table :subscriptions do |t|
+ t.string 'pricing_version'
+ t.integer 'account_id'
+ t.datetime 'expiry'
+ t.string 'billing_plan', default: 'trial'
+ t.string 'stripe_customer_id'
+ t.datetime 'created_at', null: false
+ t.datetime 'updated_at', null: false
+ t.integer 'state', default: 0
+ t.boolean 'payment_source_added', default: false
+ end
+ end
+end
diff --git a/db/migrate/20200610143132_rename_reference_id.rb b/db/migrate/20200610143132_rename_reference_id.rb
new file mode 100644
index 000000000..e15e642a2
--- /dev/null
+++ b/db/migrate/20200610143132_rename_reference_id.rb
@@ -0,0 +1,5 @@
+class RenameReferenceId < ActiveRecord::Migration[6.0]
+ def change
+ rename_column :conversations, :reference_id, :identifier
+ end
+end
diff --git a/db/migrate/20200625124400_migrate_and_add_unique_index_to_labels.rb b/db/migrate/20200625124400_migrate_and_add_unique_index_to_labels.rb
new file mode 100644
index 000000000..02e9fcd49
--- /dev/null
+++ b/db/migrate/20200625124400_migrate_and_add_unique_index_to_labels.rb
@@ -0,0 +1,20 @@
+class MigrateAndAddUniqueIndexToLabels < ActiveRecord::Migration[6.0]
+ def change
+ add_index :labels, [:title, :account_id], unique: true
+ migrate_existing_tags
+ end
+
+ private
+
+ def migrate_existing_tags
+ ::ActsAsTaggableOn::Tag.all.each do |tag|
+ tag.taggings.each do |tagging|
+ ensure_label_for_account(tag.name, tagging.taggable.account)
+ end
+ end
+ end
+
+ def ensure_label_for_account(name, account)
+ account.labels.where(title: name.downcase).first_or_create
+ end
+end
diff --git a/db/migrate/20200625154254_add_default_value_to_color.rb b/db/migrate/20200625154254_add_default_value_to_color.rb
new file mode 100644
index 000000000..9257cc3ae
--- /dev/null
+++ b/db/migrate/20200625154254_add_default_value_to_color.rb
@@ -0,0 +1,11 @@
+class AddDefaultValueToColor < ActiveRecord::Migration[6.0]
+ def up
+ Label.where(color: nil).find_each { |u| u.update(color: '#1f93ff') }
+
+ change_column :labels, :color, :string, default: '#1f93ff', null: false
+ end
+
+ def down
+ change_column :labels, :color, :string, default: nil, null: true
+ end
+end
diff --git a/db/migrate/20200629122646_add_availability_to_user.rb b/db/migrate/20200629122646_add_availability_to_user.rb
new file mode 100644
index 000000000..dd062af15
--- /dev/null
+++ b/db/migrate/20200629122646_add_availability_to_user.rb
@@ -0,0 +1,5 @@
+class AddAvailabilityToUser < ActiveRecord::Migration[6.0]
+ def change
+ add_column :users, :availability, :integer, default: 0
+ end
+end
diff --git a/db/migrate/20200704173104_add_twitter_feature_flag.rb b/db/migrate/20200704173104_add_twitter_feature_flag.rb
new file mode 100644
index 000000000..047313d70
--- /dev/null
+++ b/db/migrate/20200704173104_add_twitter_feature_flag.rb
@@ -0,0 +1,5 @@
+class AddTwitterFeatureFlag < ActiveRecord::Migration[6.0]
+ def change
+ ConfigLoader.new.process
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 1509a912a..b6337dbcd 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: 2020_05_22_115645) do
+ActiveRecord::Schema.define(version: 2020_07_04_173104) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
@@ -161,7 +161,6 @@ ActiveRecord::Schema.define(version: 2020_05_22_115645) do
t.string "widget_color", default: "#1f93ff"
t.string "welcome_title"
t.string "welcome_tagline"
- t.string "agent_away_message"
t.index ["website_token"], name: "index_channel_web_widgets_on_website_token", unique: true
end
@@ -208,6 +207,7 @@ ActiveRecord::Schema.define(version: 2020_05_22_115645) do
t.jsonb "additional_attributes"
t.bigint "contact_inbox_id"
t.uuid "uuid", default: -> { "gen_random_uuid()" }, null: false
+ t.string "identifier"
t.index ["account_id", "display_id"], name: "index_conversations_on_account_id_and_display_id", unique: true
t.index ["account_id"], name: "index_conversations_on_account_id"
t.index ["contact_inbox_id"], name: "index_conversations_on_contact_inbox_id"
@@ -245,6 +245,8 @@ ActiveRecord::Schema.define(version: 2020_05_22_115645) do
t.datetime "updated_at", null: false
t.string "channel_type"
t.boolean "enable_auto_assignment", default: true
+ t.boolean "greeting_enabled", default: false
+ t.string "greeting_message"
t.index ["account_id"], name: "index_inboxes_on_account_id"
end
@@ -256,6 +258,31 @@ ActiveRecord::Schema.define(version: 2020_05_22_115645) do
t.index ["name", "created_at"], name: "index_installation_configs_on_name_and_created_at", unique: true
end
+ create_table "integrations_hooks", force: :cascade do |t|
+ t.integer "status", default: 0
+ t.integer "inbox_id"
+ t.integer "account_id"
+ t.string "app_id"
+ t.text "settings"
+ t.integer "hook_type", default: 0
+ t.string "reference_id"
+ t.string "access_token"
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
+ end
+
+ create_table "labels", force: :cascade do |t|
+ t.string "title"
+ t.text "description"
+ t.string "color", default: "#1f93ff", null: false
+ t.boolean "show_on_sidebar"
+ t.bigint "account_id"
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
+ t.index ["account_id"], name: "index_labels_on_account_id"
+ t.index ["title", "account_id"], name: "index_labels_on_title_and_account_id", unique: true
+ end
+
create_table "messages", id: :serial, force: :cascade do |t|
t.text "content"
t.integer "account_id", null: false
@@ -265,18 +292,17 @@ ActiveRecord::Schema.define(version: 2020_05_22_115645) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "private", default: false
- t.integer "user_id"
t.integer "status", default: 0
t.string "source_id"
t.integer "content_type", default: 0
t.json "content_attributes", default: {}
- t.bigint "contact_id"
+ t.string "sender_type"
+ t.bigint "sender_id"
t.index ["account_id"], name: "index_messages_on_account_id"
- t.index ["contact_id"], name: "index_messages_on_contact_id"
t.index ["conversation_id"], name: "index_messages_on_conversation_id"
t.index ["inbox_id"], name: "index_messages_on_inbox_id"
+ t.index ["sender_type", "sender_id"], name: "index_messages_on_sender_type_and_sender_id"
t.index ["source_id"], name: "index_messages_on_source_id"
- t.index ["user_id"], name: "index_messages_on_user_id"
end
create_table "notification_settings", force: :cascade do |t|
@@ -317,18 +343,6 @@ ActiveRecord::Schema.define(version: 2020_05_22_115645) do
t.index ["user_id"], name: "index_notifications_on_user_id"
end
- create_table "subscriptions", id: :serial, force: :cascade do |t|
- t.string "pricing_version"
- t.integer "account_id"
- t.datetime "expiry"
- t.string "billing_plan", default: "trial"
- t.string "stripe_customer_id"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.integer "state", default: 0
- t.boolean "payment_source_added", default: false
- end
-
create_table "super_admins", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
@@ -399,6 +413,7 @@ ActiveRecord::Schema.define(version: 2020_05_22_115645) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "pubsub_token"
+ t.integer "availability", default: 0
t.index ["email"], name: "index_users_on_email"
t.index ["pubsub_token"], name: "index_users_on_pubsub_token", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
@@ -421,5 +436,4 @@ ActiveRecord::Schema.define(version: 2020_05_22_115645) do
add_foreign_key "contact_inboxes", "contacts"
add_foreign_key "contact_inboxes", "inboxes"
add_foreign_key "conversations", "contact_inboxes"
- add_foreign_key "messages", "contacts"
end
diff --git a/db/seeds.rb b/db/seeds.rb
index 1a169c966..b56da8239 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -1,4 +1,5 @@
# loading installation configs
+GlobalConfig.clear_cache
ConfigLoader.new.process
account = Account.create!(
@@ -34,3 +35,4 @@ conversation = Conversation.create!(
additional_attributes: {}
)
Message.create!(content: 'Hello', account: account, inbox: inbox, conversation: conversation, message_type: :incoming)
+CannedResponse.create!(account: account, short_code: 'start', content: 'Hello welcome to chatwoot.')
diff --git a/docs/README.md b/docs/README.md
index e7edfa83c..879451bfa 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -18,3 +18,11 @@ This guide will help you get started with Chatwoot!
* [Environment Variables](/docs/environment-variables)
* [Conversation Continuity with Email](/docs/conversation-continuity)
* [Common Errors](/docs/common-errors)
+
+### Deployment
+
+* [Architecture](/docs/deployment/architecture)
+* [Heroku](/docs/deployment/deploy-chatwoot-with-heroku) (recommended)
+* [Caprover](/docs/deployment/deploy-chatwoot-with-caprover) (recommended)
+* [Docker](/docs/deployment/deploy-chatwoot-with-docker)
+* [Linux](/docs/deployment/deploy-chatwoot-in-linux-vm)
diff --git a/docs/channels/facebook-channel.md b/docs/channels/facebook-channel.md
index 48991997e..08f11d0aa 100644
--- a/docs/channels/facebook-channel.md
+++ b/docs/channels/facebook-channel.md
@@ -31,7 +31,7 @@ If you are using self-hosted Chatwoot installation, please configure the Faceboo

-**Step 6**. Hooray! You have sucessfully created a Facebook inbox. Whenever a customer sends a message to your Facebook page, you will be able to see it here and manage it.
+**Step 6**. Hooray! You have successfully created a Facebook inbox. Whenever a customer sends a message to your Facebook page, you will be able to see it here and manage it.

diff --git a/docs/channels/twitter-channel.md b/docs/channels/twitter-channel.md
index 4d7726204..db65060fe 100644
--- a/docs/channels/twitter-channel.md
+++ b/docs/channels/twitter-channel.md
@@ -23,7 +23,7 @@ title: "How to create Twitter channel?"

-**Step 6**. Hooray! You have sucessfully created a Twitter inbox. You will be able to manage Twitter DMs and tweets mentioning you in the Chatwoot Inbox.
+**Step 6**. Hooray! You have successfully created a Twitter inbox. You will be able to manage Twitter DMs and tweets mentioning you in the Chatwoot Inbox.

diff --git a/docs/channels/website-channel.md b/docs/channels/website-channel.md
index f730a4668..52cbb3736 100644
--- a/docs/channels/website-channel.md
+++ b/docs/channels/website-channel.md
@@ -19,7 +19,7 @@ title: "How to create website channel?"

-**Step 5**. Hooray! You have sucessfully created a website inbox. Copy and paste the code shown in the page to your website and start supporting your customers.
+**Step 5**. Hooray! You have successfully created a website inbox. Copy and paste the code shown in the page to your website and start supporting your customers.

diff --git a/docs/channels/whatsapp-sms-twilio.md b/docs/channels/whatsapp-sms-twilio.md
index 7f00516fd..96a036a72 100644
--- a/docs/channels/whatsapp-sms-twilio.md
+++ b/docs/channels/whatsapp-sms-twilio.md
@@ -15,6 +15,8 @@ title: "How to create a Whatsapp/SMS channel with Twilio?"
These are input required to create this channel
+
+
| Input | Description | Where can I find it |
| -- | -- | -- |
| Channel Name | This is the name inbox, this will used across the application | N/A |
@@ -23,13 +25,15 @@ These are input required to create this channel
| Account SID | Account SID in Twilio Console | Login to Twilio Console, you would be able to see Account SID and Auth Token |
| Auth Token | Auth token for the account | Login to Twilio Console, you would be able to see Account SID and Auth Token |
+
+

**Step 4**. "Add agents" to your inbox.

-**Step 6**. Hooray! You have sucessfully created a whatsapp/sms inbox.
+**Step 6**. Hooray! You have successfully created a whatsapp/sms inbox.

diff --git a/docs/deployment/production/architecture.md b/docs/deployment/production/architecture.md
new file mode 100644
index 000000000..59d441017
--- /dev/null
+++ b/docs/deployment/production/architecture.md
@@ -0,0 +1,41 @@
+---
+path: "/docs/deployment/architecture"
+title: "Chatwoot Production deployment guide"
+---
+
+This guide will help you to deploy Chatwoot to production!
+
+### Architecture
+
+Running Chatwoot in production requires the following set of services.
+
+* Chatwoot web servers
+* Chatwoot workers
+* PostgreSQL Database
+* Redis Database
+* Email service (SMTP servers / sendgrid / mailgun etc)
+* Object Storage ( S3, Azure Storage, GCS, etc)
+
+
+### Updating your Chatwoot installation
+
+A new version of Chatwoot is released around the first monday of every month. We also release minor versions when there is a need for Hotfixes or security updates.
+
+You can stay tuned to our [Roadmap](https://github.com/chatwoot/chatwoot/milestones) and [releases](https://github.com/chatwoot/chatwoot/releases) on github. We recommend you to stay upto date with our releases to enjoy the lastest features and security updates.
+
+The deployment process for a newer version involves updating your app servers and workers with the latest code. Most updates would involve database migrations as well which can be executed through the following rails command.
+
+```
+bundle exec rails db:migrate
+```
+
+The detailed instructions can be found in respective deployment guides.
+
+### Available deployment options
+
+If you want to self host Chatwoot, the recommended approach is to use one of the recommended one click installation options from the below list. If you are comfortable with ruby on rails applications, you can also make use of the other deployment options mentioned below.
+
+* [Heroku](/docs/deployment/deploy-chatwoot-with-heroku) (recommended)
+* [Caprover](/docs/deployment/deploy-chatwoot-with-caprover) (recommended)
+* [Docker](/docs/deployment/deploy-chatwoot-with-docker)
+* [Linux](/docs/deployment/deploy-chatwoot-in-linux-vm)
diff --git a/docs/deployment/production/caprover.md b/docs/deployment/production/caprover.md
new file mode 100644
index 000000000..c9e81438c
--- /dev/null
+++ b/docs/deployment/production/caprover.md
@@ -0,0 +1,57 @@
+---
+path: "/docs/deployment/deploy-chatwoot-with-caprover"
+title: "Caprover Chatwoot Production deployment guide"
+---
+
+### Caprover Overview
+
+Caprover is an extremely easy to use application server management tool. It is blazing fast and uses Docker under the hood. Chatwoot has been made available as a one-click app in Chatwoot and hence the deployment process is very easy.
+
+### Install Caprover on your VM
+
+Finish your caprover installation by referring to [Getting started guid](https://caprover.com/docs/get-started.html).
+
+### Installing Chatwoot in Caprover
+
+Chatwoot is available in the one-click apps option in caprover, find Chatwoot by searching and clicking on it. Replace the default `version` with the latest `version` of chatwoot. User appropriate values for the Postgres and Redis passwords and click install. It should only take a few minutes.
+
+### Configure the necessary environment variables
+
+Caprover will take care of the installation of Postgres and Redis along with the app and worker servers. We would advise you to replace the database/Redis services with managed/standalone servers once you start scaling.
+
+Also, ensure to set the appropriate environment variables for E-mail, Object Store service etc referring to our [Environment variables guide](./environment-variables)
+
+### Upgrading Chatwoot installation
+
+To update your chatwoot installation to the latest version in caprover, Run the following command in deployment tab for web and worker in the method 5: deploy captain-definition
+
+### web
+
+```json
+{
+ "schemaVersion": 2,
+ "dockerfileLines": [
+ "FROM chatwoot/chatwoot:latest",
+ "RUN chmod +x docker/entrypoints/rails.sh",
+ "ENTRYPOINT [\"docker/entrypoints/rails.sh\"]",
+ "CMD bundle exec rake db:setup; bundle exec rake db:migrate; bundle exec rails s -b 0.0.0.0 -p 3000"
+ ]
+}
+```
+
+### worker
+```json
+{
+ "schemaVersion": 2,
+ "dockerfileLines": [
+ "FROM chatwoot/chatwoot:latest",
+ "RUN chmod +x docker/entrypoints/rails.sh",
+ "ENTRYPOINT [\"docker/entrypoints/rails.sh\"]",
+ "CMD bundle exec sidekiq -C config/sidekiq.yml"
+ ]
+}
+```
+
+### Further references
+
+- https://isotropic.co/how-to-install-chatwoot-to-a-digitalocean-droplet/
diff --git a/docs/deployment/production/docker.md b/docs/deployment/production/docker.md
new file mode 100644
index 000000000..92249c6c9
--- /dev/null
+++ b/docs/deployment/production/docker.md
@@ -0,0 +1,32 @@
+---
+path: "/docs/deployment/deploy-chatwoot-with-docker"
+title: "Docker Chatwoot Production deployment guide"
+---
+
+### Deploying with docker
+
+We publish our base images to docker hub. Build your web/worker images from these base images
+
+### Web
+
+```
+FROM chatwoot/chatwoot:latest
+RUN chmod +x docker/entrypoints/rails.sh
+ENTRYPOINT [\"docker/entrypoints/rails.sh\"]
+CMD bundle exec bundle exec rails s -b 0.0.0.0 -p 3000"
+```
+
+### worker
+
+```
+FROM chatwoot/chatwoot:latest
+RUN chmod +x docker/entrypoints/rails.sh
+ENTRYPOINT [\"docker/entrypoints/rails.sh\"]
+CMD bundle exec sidekiq -C config/sidekiq.yml"
+```
+
+The app servers will available on port `3000`. Ensure the images are connected to the same database and Redis servers. Provide the configuration for these services via environment variables.
+
+### Upgrading
+
+Update the images using the latest image from chatwoot. Run the `rails db:migrate` option after accessing console from one of the containers running latest image.
diff --git a/docs/deployment/production/heroku.md b/docs/deployment/production/heroku.md
new file mode 100644
index 000000000..16b4a3b80
--- /dev/null
+++ b/docs/deployment/production/heroku.md
@@ -0,0 +1,22 @@
+---
+path: "/docs/deployment/deploy-chatwoot-with-heroku"
+title: "Heroku Chatwoot Production deployment guide"
+---
+
+### Deploying on Heroku
+Deploy chatwoot on Heroku through the following steps
+
+1. Click on the [one click deploy button](https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master) and deploy your app.
+2. Go to the Resources tab in the Heroku app dashboard and ensure the worker dynos is turned on.
+3. Head over to settings tabs in Heroku app dashboard and click reveal config vars.
+4. Configure the environment variables for [mailer](https://www.chatwoot.com/docs/environment-variables#configure-emails) and [storage](https://www.chatwoot.com/docs/configuring-cloud-storage) as per the [documentation](https://www.chatwoot.com/docs/environment-variables).
+5. Head over to `yourapp.herokuapp.com` and enjoy using Chatwoot.
+
+
+### Updating the deployment on Heroku
+
+Whenever a new version is out for chatwoot, you update your Heroku deployment through following steps.
+
+1. In the deploy tab, choose `Github` as the deployment option.
+2. Connect chatwoot repo to the app.
+3. Head over to the manual deploy option, choose `master` branch and hit deploy.
diff --git a/docs/deployment/production/linux-vm.md b/docs/deployment/production/linux-vm.md
new file mode 100644
index 000000000..79e940074
--- /dev/null
+++ b/docs/deployment/production/linux-vm.md
@@ -0,0 +1,124 @@
+---
+path: "/docs/deployment/deploy-chatwoot-in-linux-vm"
+title: "Linux VM Chatwoot Production deployment guide"
+---
+
+
+### Deploying to Linux VM
+
+We have prepared a deployment script for ubuntu 18.04. Run the script or refer the script and make changes accordingly to OS.
+
+https://github.com/chatwoot/chatwoot/blob/develop/deployment/setup.sh
+
+### After logging in to your Linux VM as the root user perform the following steps for initial set up
+
+1. Create the `chatwoot.sh` file and copy the content from [deployment script](https://github.com/chatwoot/chatwoot/blob/develop/deployment/setup.sh).
+2. Execute the script and it will take care of the initial Chatwoot setup
+3. Chatwoot Installation will now be accessible at `http://{your_ip}:3000`
+
+### Configure ngix and letsencrypt
+
+1. configure Nginx to serve as a frontend proxy by following steps in your shell
+
+```bash
+cd /etc/nginx/sites-enabled
+nano yourdomain.com.conf
+```
+
+2. Add the required Nginx config after replacing the `yourdomain.com` in `server_name`.
+
+```bash
+server {
+ server_name yourdomain.com;
+ # where rails app is running
+ set $upstream 127.0.0.1:3000;
+
+ # Here we define the web-root for our SSL proof
+ location /.well-known {
+ # Note that a request for /.well-known/test.html will be made
+ alias /var/www/ssl-proof/chatwoot/.well-known;
+ }
+
+ location / {
+ proxy_pass_header Authorization;
+ proxy_pass http://$upstream;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Ssl on; # Optional
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_http_version 1.1;
+ proxy_set_header Connection “”;
+ proxy_buffering off;
+ client_max_body_size 0;
+ proxy_read_timeout 36000s;
+ proxy_redirect off;
+ }
+ listen 80;
+}
+```
+
+3. Verify and reload your Nginx config by running following command.
+
+```sh
+nginx -t
+systemctl reload nginx
+```
+
+4. Run Letsencrypt and configure SSL
+
+```sh
+mkdir -p /var/www/ssl-proof/chatwoot/.well-known
+certbot --webroot -w /var/www/ssl-proof/chatwoot/ -d yourdomain.com -i nginx
+```
+
+5. You Chatwoot installation should be accessible from the `https://yourdomain.com` now.
+
+### Configure the required environment variables
+
+For your chatwoot installation to properly function you would need to configure some of the essential environment variables like `FRONTEND_URL`, mailer and storage config etc. refer [environment variables](https://www.chatwoot.com/docs/environment-variables) for the full list.
+
+1. Login as chatwoot and edit the .env file.
+
+```shell
+# login as chatwoot user
+sudo -i -u chatwoot
+cd chatwoot
+nano .env
+```
+2. Refer [environment variables](https://www.chatwoot.com/docs/environment-variables) and update the required variables. Save the `.env` file.
+3. Restart the Chatwoot server and enjoy using Chatwoot.
+
+```sh
+systemctl restart chatwoot.target
+```
+
+### Updating your Chatwoot Installation on a Linux VM
+
+Run the following steps on your VM if you made use of our installation script Or making changes accordingly to your OS
+
+```
+# login as chatwoot user
+sudo -i -u chatwoot
+
+# navigate to the chatwoot directory
+cd chatwoot
+
+# pull the latest version of the master branch
+git pull
+
+# update dependencies
+bundle
+yarn
+
+# recompile the assets
+rake assets:precompile RAILS_ENV=production
+
+# migrate the database schema
+RAILS_ENV=production bundle exec rake db:migrate
+
+#Restart the chatwoot server
+systemctl restart chatwoot.target
+```
diff --git a/docs/development/environment-setup/windows.md b/docs/development/environment-setup/windows.md
index d2b3266b6..c1f379b5a 100644
--- a/docs/development/environment-setup/windows.md
+++ b/docs/development/environment-setup/windows.md
@@ -7,7 +7,7 @@ title: 'Windows 10 Installation Guide'
You need to install Linux Subsystem for Windows.
-1. The first step is to enable "Developer mode" in Windows. You can do this by opening up Settings and navigating to to Update & Security, then "For Developers". Click the "Developer mode" option to enable it.
+1. The first step is to enable "Developer mode" in Windows. You can do this by opening up Settings and navigating to Update & Security, then "For Developers". Click the "Developer mode" option to enable it.
diff --git a/docs/development/project-setup/environment-variables.md b/docs/development/project-setup/environment-variables.md
index 899abebaf..e92523355 100644
--- a/docs/development/project-setup/environment-variables.md
+++ b/docs/development/project-setup/environment-variables.md
@@ -41,6 +41,42 @@ SMTP_USERNAME=
SMTP_PASSWORD=
```
+If you would like to use Sendgrid to send your emails, use the following environment variables:
+```bash
+SMTP_ADDRESS=smtp.sendgrid.net
+SMTP_AUTHENTICATION=plain
+SMTP_DOMAIN=
+SMTP_ENABLE_STARTTLS_AUTO=true
+SMTP_PORT=587
+SMTP_USERNAME=apikey
+SMTP_PASSWORD=
+```
+
+If you would like to use Mailgun to send your emails, use the following environment variables:
+```bash
+SMTP_ADDRESS=smtp.mailgun.org
+SMTP_AUTHENTICATION=plain
+SMTP_DOMAIN=
+SMTP_ENABLE_STARTTLS_AUTO=true
+SMTP_PORT=587
+SMTP_USERNAME=
+SMTP_PASSWORD=
+```
+
+
+If you would like to use Mailchimp to send your emails, use the following environment variables:
+Note: Mandrill is the transactional email service for Mailchimp. You need to enable transactional email and login to mandrillapp.com.
+
+```bash
+SMTP_ADDRESS=smtp.mandrillapp.com
+SMTP_AUTHENTICATION=plain
+SMTP_DOMAIN=
+SMTP_ENABLE_STARTTLS_AUTO=true
+SMTP_PORT=587
+SMTP_USERNAME= SMTP & API info>
+SMTP_PASSWORD= SMTP & API Info>
+```
+
### Configure frontend URL
Provide the following value as frontend url
diff --git a/docs/development/project-setup/slack-integration-setup.md b/docs/development/project-setup/slack-integration-setup.md
new file mode 100644
index 000000000..264238423
--- /dev/null
+++ b/docs/development/project-setup/slack-integration-setup.md
@@ -0,0 +1,38 @@
+---
+path: "/docs/slack-integration-setup"
+title: "Setting Up Slack Integration"
+---
+
+### Register a Slack app
+
+To use Slack Integration, you have to create a Slack app in the developer portal. You can find more details about creating Slack apps [here](https://api.slack.com/)
+
+Once you register your Slack App, you will have to obtain the `Client Id` and `Client Secret`. These values will be available in the app settings and will be required while setting up Chatwoot environment variables.
+
+### Configure the Slack app
+
+1) Create a slack app and add it to your development workspace.
+2) Obtain the `Client Id` and `Client Secret` for the app and configure it in your Chatwoot environment variables.
+3) Head over to the `OAuth & permissions` section under `features` tab.
+4) In the redirect URLs, Add your Chatwoot installation base url.
+5) In the scopes section configure the given scopes for bot token scopes. `commands,chat:write,channels:read,channels:manage,channels:join,groups:write,im:write,mpim:write,users:read,users:read.email,chat:write.customize,channels:history,groups:history,mpim:history,im:history`
+6) Head over to the `events subscriptions` section under `features` tab.
+7) Enable events and configure the given request url `{chatwoot installation url}/api/v1/integrations/webhooks`
+8) Subscribe to the following bot events `message.channels` , `message.groups`, `message.im`, `message.mpim`
+9) Connect slack integration on Chatwoot app and get productive.
+
+### Configuring the environment variables in Chatwoot
+
+Configure the following Chatwoot environment variables with the values you have obtained during the slack app setup.
+
+```bash
+SLACK_CLIENT_ID=
+SLACK_CLIENT_SECRET=
+```
+
+### Test your setup
+
+1. Ensure that you are receiving the Chatwoot messages in the `customer-conversations` channel.
+2. Add a message to that thread and ensure it is coming back on to Chatwoot
+3. Add `note:` or `private:` in front on the Slack message see if it is coming out as private notes
+4. If your Slack member's email matches their email on Chatwoot, the messages will be associated with their Chatwoot user account.
diff --git a/lib/constants/redis_keys.rb b/lib/constants/redis_keys.rb
deleted file mode 100644
index 7138e4f24..000000000
--- a/lib/constants/redis_keys.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-module Constants::RedisKeys
- ROUND_ROBIN_AGENTS = 'ROUND_ROBIN_AGENTS:%{inbox_id}'.freeze
-end
diff --git a/lib/integrations/slack/channel_builder.rb b/lib/integrations/slack/channel_builder.rb
new file mode 100644
index 000000000..aa2aa992a
--- /dev/null
+++ b/lib/integrations/slack/channel_builder.rb
@@ -0,0 +1,32 @@
+class Integrations::Slack::ChannelBuilder
+ attr_reader :params, :channel
+
+ def initialize(params)
+ @params = params
+ end
+
+ def perform
+ find_or_create_channel
+ update_reference_id
+ end
+
+ private
+
+ def hook
+ @hook ||= params[:hook]
+ end
+
+ def slack_client
+ @slack_client ||= Slack::Web::Client.new(token: hook.access_token)
+ end
+
+ def find_or_create_channel
+ exisiting_channel = slack_client.conversations_list.channels.find { |channel| channel['name'] == params[:channel] }
+ @channel = exisiting_channel || slack_client.conversations_create(name: params[:channel])['channel']
+ end
+
+ def update_reference_id
+ slack_client.conversations_join(channel: channel[:id])
+ @hook.update(reference_id: channel[:id])
+ end
+end
diff --git a/lib/integrations/slack/hook_builder.rb b/lib/integrations/slack/hook_builder.rb
new file mode 100644
index 000000000..93bff9656
--- /dev/null
+++ b/lib/integrations/slack/hook_builder.rb
@@ -0,0 +1,43 @@
+class Integrations::Slack::HookBuilder
+ attr_reader :params
+
+ def initialize(params)
+ @params = params
+ end
+
+ def perform
+ token = fetch_access_token
+
+ hook = account.hooks.new(
+ access_token: token,
+ status: 'enabled',
+ inbox_id: params[:inbox_id],
+ hook_type: hook_type,
+ app_id: 'slack'
+ )
+
+ hook.save!
+ hook
+ end
+
+ private
+
+ def account
+ params[:account]
+ end
+
+ def hook_type
+ params[:inbox_id] ? 'inbox' : 'account'
+ end
+
+ def fetch_access_token
+ client = Slack::Web::Client.new
+ slack_access = client.oauth_v2_access(
+ client_id: ENV.fetch('SLACK_CLIENT_ID', 'TEST_CLIENT_ID'),
+ client_secret: ENV.fetch('SLACK_CLIENT_SECRET', 'TEST_CLIENT_SECRET'),
+ code: params[:code],
+ redirect_uri: Integrations::App.slack_integration_url
+ )
+ slack_access['access_token']
+ end
+end
diff --git a/lib/integrations/slack/incoming_message_builder.rb b/lib/integrations/slack/incoming_message_builder.rb
new file mode 100644
index 000000000..e7ebd735f
--- /dev/null
+++ b/lib/integrations/slack/incoming_message_builder.rb
@@ -0,0 +1,94 @@
+class Integrations::Slack::IncomingMessageBuilder
+ attr_reader :params
+
+ SUPPORTED_EVENT_TYPES = %w[event_callback url_verification].freeze
+ SUPPORTED_EVENTS = %w[message].freeze
+ SUPPORTED_MESSAGE_TYPES = %w[rich_text].freeze
+
+ def initialize(params)
+ @params = params
+ end
+
+ def perform
+ return unless valid_event?
+
+ if hook_verification?
+ verify_hook
+ elsif create_message?
+ create_message
+ end
+ end
+
+ private
+
+ def valid_event?
+ supported_event_type? && supported_event?
+ end
+
+ def supported_event_type?
+ SUPPORTED_EVENT_TYPES.include?(params[:type])
+ end
+
+ def supported_event?
+ hook_verification? || SUPPORTED_EVENTS.include?(params[:event][:type])
+ end
+
+ def supported_message?
+ SUPPORTED_MESSAGE_TYPES.include?(message[:type]) if message.present?
+ end
+
+ def hook_verification?
+ params[:type] == 'url_verification'
+ end
+
+ def create_message?
+ supported_message? && integration_hook
+ end
+
+ def message
+ params[:event][:blocks]&.first
+ end
+
+ def verify_hook
+ {
+ challenge: params[:challenge]
+ }
+ end
+
+ def integration_hook
+ @integration_hook ||= Integrations::Hook.find_by(reference_id: params[:event][:channel])
+ end
+
+ def conversation
+ @conversation ||= Conversation.where(identifier: params[:event][:thread_ts]).first
+ end
+
+ def sender
+ user_email = slack_client.users_info(user: params[:event][:user])[:user][:profile][:email]
+ conversation.account.users.find_by(email: user_email)
+ end
+
+ def private_note?
+ params[:event][:text].strip.starts_with?('note:', 'private:')
+ end
+
+ def create_message
+ return unless conversation
+
+ conversation.messages.create(
+ message_type: :outgoing,
+ account_id: conversation.account_id,
+ inbox_id: conversation.inbox_id,
+ content: params[:event][:text],
+ source_id: "slack_#{params[:event][:ts]}",
+ private: private_note?,
+ sender: sender
+ )
+
+ { status: 'success' }
+ end
+
+ def slack_client
+ @slack_client ||= Slack::Web::Client.new(token: @integration_hook.access_token)
+ end
+end
diff --git a/lib/integrations/slack/send_on_slack_service.rb b/lib/integrations/slack/send_on_slack_service.rb
new file mode 100644
index 000000000..4c5bb46c6
--- /dev/null
+++ b/lib/integrations/slack/send_on_slack_service.rb
@@ -0,0 +1,58 @@
+class Integrations::Slack::SendOnSlackService < Base::SendOnChannelService
+ pattr_initialize [:message!, :hook!]
+
+ def perform
+ # overriding the base class logic since the validations are different in this case.
+ # FIXME: for now we will only send messages from widget to slack
+ return unless channel.is_a?(Channel::WebWidget)
+ # we don't want message loop in slack
+ return if message.source_id.try(:starts_with?, 'slack_')
+ # we don't want to start slack thread from agent conversation as of now
+ return if message.outgoing? && conversation.identifier.blank?
+
+ perform_reply
+ end
+
+ private
+
+ def perform_reply
+ send_message
+ update_reference_id
+ end
+
+ def message_content
+ private_indicator = message.private? ? 'private: ' : ''
+ if conversation.identifier.present?
+ "#{private_indicator}#{message.content}"
+ else
+ "*Inbox: #{message.inbox.name}* \n\n #{message.content}"
+ end
+ end
+
+ def avatar_url(sender)
+ sender.try(:avatar_url) || "#{ENV['FRONTEND_URL']}/admin/avatar_square.png"
+ end
+
+ def send_message
+ sender = message.sender
+ sender_type = sender.class == Contact ? 'Contact' : 'Agent'
+
+ @slack_message = slack_client.chat_postMessage(
+ channel: hook.reference_id,
+ text: message_content,
+ username: "#{sender_type}: #{sender.try(:name)}",
+ thread_ts: conversation.identifier,
+ icon_url: avatar_url(sender)
+ )
+ end
+
+ def update_reference_id
+ return if conversation.identifier
+
+ conversation.update!(identifier: @slack_message['ts'])
+ end
+
+ def slack_client
+ @slack_client ||= Slack::Web::Client.new(token: hook.access_token)
+ end
+end
diff --git a/lib/integrations/widget/incoming_message_builder.rb b/lib/integrations/widget/incoming_message_builder.rb
index abc14aafa..82bc303f3 100644
--- a/lib/integrations/widget/incoming_message_builder.rb
+++ b/lib/integrations/widget/incoming_message_builder.rb
@@ -51,7 +51,8 @@ class Integrations::Widget::IncomingMessageBuilder
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
message_type: 0,
- content: options[:content]
+ content: options[:content],
+ sender: contact
}
end
end
diff --git a/lib/integrations/widget/outgoing_message_builder.rb b/lib/integrations/widget/outgoing_message_builder.rb
index fb76f67d6..5791ea64e 100644
--- a/lib/integrations/widget/outgoing_message_builder.rb
+++ b/lib/integrations/widget/outgoing_message_builder.rb
@@ -46,7 +46,7 @@ class Integrations::Widget::OutgoingMessageBuilder
inbox_id: @conversation.inbox_id,
message_type: 1,
content: options[:content],
- user_id: user.id
+ sender: user
}
end
end
diff --git a/lib/online_status_tracker.rb b/lib/online_status_tracker.rb
index fa66453ad..622585dff 100644
--- a/lib/online_status_tracker.rb
+++ b/lib/online_status_tracker.rb
@@ -1,19 +1,52 @@
module OnlineStatusTracker
- def self.add_subscription(channel_id)
- count = subscription_count(channel_id)
- ::Redis::Alfred.setex(channel_id, count + 1)
+ PRESENCE_DURATION = 60.seconds
+
+ # presence : sorted set with timestamp as the score & object id as value
+
+ # obj_type: Contact | User
+ def self.update_presence(account_id, obj_type, obj_id)
+ ::Redis::Alfred.zadd(presence_key(account_id, obj_type), Time.now.to_i, obj_id)
end
- def self.remove_subscription(channel_id)
- count = subscription_count(channel_id)
- if count == 1
- ::Redis::Alfred.delete(channel_id)
- elsif count != 0
- ::Redis::Alfred.setex(channel_id, count - 1)
+ def self.get_presence(account_id, obj_type, obj_id)
+ connected_time = ::Redis::Alfred.zscore(presence_key(account_id, obj_type), obj_id)
+ connected_time && connected_time > (Time.zone.now - PRESENCE_DURATION).to_i
+ end
+
+ def self.presence_key(account_id, type)
+ case type
+ when 'Contact'
+ format(::Redis::Alfred::ONLINE_PRESENCE_CONTACTS, account_id: account_id)
+ else
+ format(::Redis::Alfred::ONLINE_PRESENCE_USERS, account_id: account_id)
end
end
- def self.subscription_count(channel_id)
- ::Redis::Alfred.get(channel_id).to_i
+ # online status : online | busy | offline
+ # redis hash with obj_id key && status as value
+
+ def self.set_status(account_id, user_id, status)
+ ::Redis::Alfred.hset(status_key(account_id), user_id, status)
+ end
+
+ def self.get_status(account_id, user_id)
+ ::Redis::Alfred.hget(status_key(account_id), user_id)
+ end
+
+ def self.status_key(account_id)
+ format(::Redis::Alfred::ONLINE_STATUS, account_id: account_id)
+ end
+
+ def self.get_available_contacts(account_id)
+ contact_ids = ::Redis::Alfred.zrangebyscore(presence_key(account_id, 'Contact'), (Time.zone.now - PRESENCE_DURATION).to_i, Time.now.to_i)
+ contact_ids.index_with { |_id| 'online' }
+ end
+
+ def self.get_available_users(account_id)
+ user_ids = ::Redis::Alfred.zrangebyscore(presence_key(account_id, 'User'), (Time.zone.now - PRESENCE_DURATION).to_i, Time.now.to_i)
+ return {} if user_ids.blank?
+
+ user_availabilities = ::Redis::Alfred.hmget(status_key(account_id), user_ids)
+ user_ids.map.with_index { |id, index| [id, (user_availabilities[index] || 'online')] }.to_h
end
end
diff --git a/lib/redis/alfred.rb b/lib/redis/alfred.rb
index d811df028..d023f9fa3 100644
--- a/lib/redis/alfred.rb
+++ b/lib/redis/alfred.rb
@@ -1,21 +1,11 @@
module Redis::Alfred
- CONVERSATION_MAILER_KEY = 'CONVERSATION::%d'.freeze
+ include Redis::RedisKeys
class << self
- def rpoplpush(source, destination)
- $alfred.rpoplpush(source, destination)
- end
+ # key operations
- def lpush(key, value)
- $alfred.lpush(key, value)
- end
-
- def delete(key)
- $alfred.del(key)
- end
-
- def lrem(key, value, count = 0)
- $alfred.lrem(key, count, value)
+ def set(key, value)
+ $alfred.set(key, value)
end
def setex(key, value, expiry = 1.day)
@@ -25,5 +15,69 @@ module Redis::Alfred
def get(key)
$alfred.get(key)
end
+
+ def delete(key)
+ $alfred.del(key)
+ end
+
+ # list operations
+
+ def llen(key)
+ $alfred.llen(key)
+ end
+
+ def lrange(key, start_index = 0, end_index = -1)
+ $alfred.lrange(key, start_index, end_index)
+ end
+
+ def rpop(key)
+ $alfred.rpop(key)
+ end
+
+ def lpush(key, values)
+ $alfred.lpush(key, values)
+ end
+
+ def rpoplpush(source, destination)
+ $alfred.rpoplpush(source, destination)
+ end
+
+ def lrem(key, value, count = 0)
+ $alfred.lrem(key, count, value)
+ end
+
+ # hash operations
+
+ # add a key value to redis hash
+ def hset(key, field, value)
+ $alfred.hset(key, field, value)
+ end
+
+ # get value from redis hash
+ def hget(key, field)
+ $alfred.hget(key, field)
+ end
+
+ # get values of multiple keys from redis hash
+ def hmget(key, fields)
+ $alfred.hmget(key, *fields)
+ end
+
+ # sorted set operations
+
+ # add score and value for a key
+ def zadd(key, score, value)
+ $alfred.zadd(key, score, value)
+ end
+
+ # get score of a value for key
+ def zscore(key, value)
+ $alfred.zscore(key, value)
+ end
+
+ # get values by score
+ def zrangebyscore(key, range_start, range_end)
+ $alfred.zrangebyscore(key, range_start, range_end)
+ end
end
end
diff --git a/lib/redis/redis_keys.rb b/lib/redis/redis_keys.rb
new file mode 100644
index 000000000..417978d7c
--- /dev/null
+++ b/lib/redis/redis_keys.rb
@@ -0,0 +1,13 @@
+module Redis::RedisKeys
+ ROUND_ROBIN_AGENTS = 'ROUND_ROBIN_AGENTS:%d'.freeze
+
+ CONVERSATION_MAILER_KEY = 'CONVERSATION::%d'.freeze
+
+ ## Online Status Keys
+ # hash containing user_id key and status as value
+ ONLINE_STATUS = 'ONLINE_STATUS::%d'.freeze
+ # sorted set storing online presense of account contacts
+ ONLINE_PRESENCE_CONTACTS = 'ONLINE_PRESENCE::%d::CONTACTS'.freeze
+ # sorted set storing online presense of account users
+ ONLINE_PRESENCE_USERS = 'ONLINE_PRESENCE::%d::USERS'.freeze
+end
diff --git a/lib/regex_helper.rb b/lib/regex_helper.rb
new file mode 100644
index 000000000..88f2b2835
--- /dev/null
+++ b/lib/regex_helper.rb
@@ -0,0 +1,8 @@
+module RegexHelper
+ # user https://rubular.com/ to quickly validate your regex
+
+ # the following regext needs atleast one character which should be
+ # valid unicode letter, unicode number, underscore, hyphen
+ # shouldn't start with a underscore or hyphen
+ UNICODE_CHARACTER_NUMBER_HYPHEN_UNDERSCORE = Regexp.new('\A[\p{L}\p{N}]+[\p{L}\p{N}_-]+\Z')
+end
diff --git a/lib/webhooks/chargebee.rb b/lib/webhooks/chargebee.rb
deleted file mode 100644
index dd28a110b..000000000
--- a/lib/webhooks/chargebee.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-# frozen_string_literal: true
-
-class Webhooks::Chargebee
- SUPPORTED_EVENTS = [:subscription_created, :subscription_trial_end_reminder,
- :subscription_activated, :subscription_cancelled,
- :subscription_reactivated, :subscription_deleted,
- :subscription_renewed, :payment_source_added, :subscription_changed].freeze
-
- attr_accessor :params, :account
-
- def initialize(params)
- @params = params
- end
-
- def consume
- send(event_name) if supported_event?
- end
-
- private
-
- def event_name
- @params[:event_type]
- end
-
- def customer_id
- @params[:content][:customer][:id]
- end
-
- def trial_ends_on
- trial_end = @params[:content][:subscription][:trial_end]
- DateTime.strptime(trial_end, '%s')
- end
-
- def supported_event?
- SUPPORTED_EVENTS.include?(event_name.to_sym)
- end
-
- def subscription
- @subscriptiom ||= Subscription.find_by(stripe_customer_id: customer_id)
- end
-
- def subscription_created
- Raven.capture_message("subscription created for #{customer_id}")
- end
-
- def subscription_changed
- if subscription.expiry != trial_ends_on
- subscription.expiry = trial_ends_on
- subscription.save
- subscription.trial!
- end
- end
-
- def subscription_trial_end_reminder
- # Raven.capture_message("subscription trial end reminder for #{customer_id}")
- end
-
- def subscription_activated
- subscription.active!
- Raven.capture_message("subscription activated for #{customer_id}")
- end
-
- def subscription_cancelled
- # if there is a reason field in response. Then cancellation is due to payment failure
- subscription.cancelled!
- Raven.capture_message("subscription cancelled for #{customer_id}")
- end
-
- def subscription_reactivated
- # TODO: send mail to user that account is reactivated
- subscription.active!
- Raven.capture_message("subscription reactivated for #{customer_id}")
- end
-
- def subscription_renewed
- # TODO: Needs this to show payment history.
- Raven.capture_message("subscription renewed for #{customer_id}")
- end
-
- def subscription_deleted; end
-
- def payment_source_added
- Raven.capture_message("payment source added for #{customer_id}")
- subscription.payment_source_added!
- end
-end
diff --git a/public/admin/avatar_square.png b/public/admin/avatar_square.png
new file mode 100644
index 000000000..f9eb765c9
Binary files /dev/null and b/public/admin/avatar_square.png differ
diff --git a/app/javascript/dashboard/assets/images/integrations/cable.svg b/public/assets/dashboard/integrations/cable.svg
similarity index 100%
rename from app/javascript/dashboard/assets/images/integrations/cable.svg
rename to public/assets/dashboard/integrations/cable.svg
diff --git a/public/assets/dashboard/integrations/slack.png b/public/assets/dashboard/integrations/slack.png
new file mode 100644
index 000000000..41fc5e45c
Binary files /dev/null and b/public/assets/dashboard/integrations/slack.png differ
diff --git a/spec/actions/contact_merge_action_spec.rb b/spec/actions/contact_merge_action_spec.rb
index 8afbdf6b4..815518ae3 100644
--- a/spec/actions/contact_merge_action_spec.rb
+++ b/spec/actions/contact_merge_action_spec.rb
@@ -10,7 +10,7 @@ describe ::ContactMergeAction do
before do
2.times.each { create(:conversation, contact: base_contact) }
2.times.each { create(:conversation, contact: mergee_contact) }
- 2.times.each { create(:message, contact: mergee_contact) }
+ 2.times.each { create(:message, sender: mergee_contact) }
end
describe '#perform' do
diff --git a/spec/channels/room_channel_spec.rb b/spec/channels/room_channel_spec.rb
index e50aaebe4..8cac85ce6 100644
--- a/spec/channels/room_channel_spec.rb
+++ b/spec/channels/room_channel_spec.rb
@@ -1,15 +1,15 @@
require 'rails_helper'
RSpec.describe RoomChannel, type: :channel do
- let!(:user) { create(:user) }
+ let!(:contact) { create(:contact) }
before do
stub_connection
end
it 'subscribes to a stream when pubsub_token is provided' do
- subscribe(pubsub_token: user.uid)
+ subscribe(pubsub_token: contact.pubsub_token)
expect(subscription).to be_confirmed
- expect(subscription).to have_stream_for(user.uid)
+ expect(subscription).to have_stream_for(contact.pubsub_token)
end
end
diff --git a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb
index 27e5dfd37..1a1b82442 100644
--- a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb
@@ -53,7 +53,7 @@ RSpec.describe 'Contacts API', type: :request do
end
describe 'POST /api/v1/accounts/{account.id}/contacts' do
- let(:valid_params) { { contact: { account_id: account.id } } }
+ let(:valid_params) { { contact: { name: 'test' } } }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
diff --git a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb
index 9ea04c78c..af9eeaa4f 100644
--- a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb
@@ -54,7 +54,7 @@ RSpec.describe 'Conversations API', type: :request do
as: :json
expect(response).to have_http_status(:success)
- expect(JSON.parse(response.body, symbolize_names: true)[:data][:meta][:all_count]).to eq(1)
+ expect(JSON.parse(response.body, symbolize_names: true)[:meta][:all_count]).to eq(1)
end
end
end
diff --git a/spec/controllers/api/v1/accounts/integrations/apps_controller_spec.rb b/spec/controllers/api/v1/accounts/integrations/apps_controller_spec.rb
new file mode 100644
index 000000000..b1ac377bb
--- /dev/null
+++ b/spec/controllers/api/v1/accounts/integrations/apps_controller_spec.rb
@@ -0,0 +1,54 @@
+require 'rails_helper'
+
+RSpec.describe 'Integration Apps API', type: :request do
+ let(:account) { create(:account) }
+
+ describe 'GET /api/v1/integrations/apps' do
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ get api_v1_account_integrations_apps_url(account)
+ 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 'returns all active apps' do
+ first_app = Integrations::App.all.find(&:active?)
+ 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'].first
+ expect(apps['id']).to eql(first_app.id)
+ expect(apps['name']).to eql(first_app.name)
+ end
+ end
+ end
+
+ describe 'GET /api/v1/integrations/apps/:id' do
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ get api_v1_account_integrations_app_url(account_id: account.id, id: 'slack')
+ 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 'returns details of the app' do
+ get api_v1_account_integrations_app_url(account_id: account.id, id: 'slack'),
+ headers: agent.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ app = JSON.parse(response.body)
+ expect(app['id']).to eql('slack')
+ expect(app['name']).to eql('Slack')
+ end
+ end
+ end
+end
diff --git a/spec/controllers/api/v1/accounts/labels_controller_spec.rb b/spec/controllers/api/v1/accounts/labels_controller_spec.rb
index a3d6fb347..083d1ec86 100644
--- a/spec/controllers/api/v1/accounts/labels_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/labels_controller_spec.rb
@@ -1,12 +1,8 @@
require 'rails_helper'
RSpec.describe 'Label API', type: :request do
- let(:account) { create(:account) }
- let(:conversation) { create(:conversation, account: account) }
-
- before do
- conversation.update_labels('label1, label2')
- end
+ let!(:account) { create(:account) }
+ let!(:label) { create(:label, account: account) }
describe 'GET /api/v1/accounts/{account.id}/labels' do
context 'when it is an unauthenticated user' do
@@ -18,7 +14,7 @@ RSpec.describe 'Label 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 the labels in account' do
get "/api/v1/accounts/#{account.id}/labels",
@@ -26,33 +22,82 @@ RSpec.describe 'Label API', type: :request do
as: :json
expect(response).to have_http_status(:success)
- expect(response.body).to include('label1')
- expect(response.body).to include('label2')
+ expect(response.body).to include(label.title)
end
end
end
- describe 'GET /api/v1/accounts/{account.id}/labels/most_used' do
+ describe 'GET /api/v1/accounts/{account.id}/labels/:id' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
- get "/api/v1/accounts/#{account.id}/labels"
+ get "/api/v1/accounts/#{account.id}/labels/#{label.id}"
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(:admin) { create(:user, account: account, role: :administrator) }
- it 'returns most used labels' do
- get "/api/v1/accounts/#{account.id}/labels/most_used",
- headers: agent.create_new_auth_token,
- params: { count: 1 },
+ it 'shows the contact' do
+ get "/api/v1/accounts/#{account.id}/labels/#{label.id}",
+ headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
- expect(response.body).to include('label1')
- expect(response.body).not_to include('label2')
+ expect(response.body).to include(label.title)
+ end
+ end
+ end
+
+ describe 'POST /api/v1/accounts/{account.id}/labels' do
+ let(:valid_params) { { label: { title: 'test' } } }
+
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ expect { post "/api/v1/accounts/#{account.id}/labels", 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
+ let(:admin) { create(:user, account: account, role: :administrator) }
+
+ it 'creates the contact' do
+ expect do
+ post "/api/v1/accounts/#{account.id}/labels", headers: admin.create_new_auth_token,
+ params: valid_params
+ end.to change(Label, :count).by(1)
+
+ expect(response).to have_http_status(:success)
+ end
+ end
+ end
+
+ describe 'PATCH /api/v1/accounts/{account.id}/labels/:id' do
+ let(:valid_params) { { title: 'Test_2' } }
+
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ put "/api/v1/accounts/#{account.id}/labels/#{label.id}",
+ params: valid_params
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an authenticated user' do
+ let(:admin) { create(:user, account: account, role: :administrator) }
+
+ it 'updates the label' do
+ patch "/api/v1/accounts/#{account.id}/labels/#{label.id}",
+ headers: admin.create_new_auth_token,
+ params: valid_params,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ expect(label.reload.title).to eq('test_2')
end
end
end
diff --git a/spec/controllers/api/v1/accounts/notifications_controller_spec.rb b/spec/controllers/api/v1/accounts/notifications_controller_spec.rb
index fac2df97b..8f61cd803 100644
--- a/spec/controllers/api/v1/accounts/notifications_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/notifications_controller_spec.rb
@@ -14,15 +14,62 @@ RSpec.describe 'Notifications API', type: :request do
context 'when it is an authenticated user' do
let(:admin) { create(:user, account: account, role: :administrator) }
- let!(:notification) { create(:notification, account: account, user: admin) }
+ let!(:notification1) { create(:notification, account: account, user: admin) }
+ let!(:notification2) { create(:notification, account: account, user: admin) }
it 'returns all notifications' do
get "/api/v1/accounts/#{account.id}/notifications",
headers: admin.create_new_auth_token,
as: :json
+ response_json = JSON.parse(response.body)
expect(response).to have_http_status(:success)
- expect(response.body).to include(notification.notification_type)
+ expect(response.body).to include(notification1.notification_type)
+ expect(response_json['data']['meta']['unread_count']).to eq 2
+ # notification appear in descending order
+ expect(response_json['data']['payload'].first['id']).to eq notification2.id
+ end
+ end
+ end
+
+ describe 'POST /api/v1/accounts/{account.id}/notifications/read_all' do
+ let(:admin) { create(:user, account: account, role: :administrator) }
+ let!(:notification1) { create(:notification, account: account, user: admin) }
+ let!(:notification2) { create(:notification, account: account, user: admin) }
+
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ post "/api/v1/accounts/#{account.id}/notifications/read_all"
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an authenticated user' do
+ let(:admin) { create(:user, account: account, role: :administrator) }
+
+ it 'updates all the notifications read at' do
+ post "/api/v1/accounts/#{account.id}/notifications/read_all",
+ headers: admin.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ expect(notification1.reload.read_at).not_to eq('')
+ expect(notification2.reload.read_at).not_to eq('')
+ end
+
+ it 'updates only the notifications read at for primary actor when param is passed' do
+ post "/api/v1/accounts/#{account.id}/notifications/read_all",
+ headers: admin.create_new_auth_token,
+ params: {
+ primary_actor_id: notification1.primary_actor_id,
+ primary_actor_type: notification1.primary_actor_type
+ },
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ expect(notification1.reload.read_at).not_to eq('')
+ expect(notification2.reload.read_at).to eq nil
end
end
end
diff --git a/spec/controllers/api/v1/accounts/subscriptions_controller_spec.rb b/spec/controllers/api/v1/accounts/subscriptions_controller_spec.rb
deleted file mode 100644
index af7071f21..000000000
--- a/spec/controllers/api/v1/accounts/subscriptions_controller_spec.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe 'Subscriptions API', type: :request do
- let(:account) { create(:account) }
-
- describe 'GET /api/v1/accounts/{account.id}/subscriptions' do
- context 'when it is an unauthenticated user' do
- it 'returns unauthorized' do
- ENV['BILLING_ENABLED'] = 'true'
-
- get "/api/v1/accounts/#{account.id}/subscriptions"
-
- expect(response).to have_http_status(:unauthorized)
-
- ENV['BILLING_ENABLED'] = nil
- end
- end
-
- context 'when it is an authenticated user' do
- let(:agent) { create(:user, account: account, role: :agent) }
-
- it 'returns all subscriptions' do
- ENV['BILLING_ENABLED'] = 'true'
-
- get "/api/v1/accounts/#{account.id}/subscriptions",
- headers: agent.create_new_auth_token,
- as: :json
-
- expect(response).to have_http_status(:success)
- expect(JSON.parse(response.body)).to eq(account.subscription_data.as_json)
-
- ENV['BILLING_ENABLED'] = nil
- end
-
- it 'throws 404 error if env variable is not set' do
- ENV['BILLING_ENABLED'] = nil
-
- get "/api/v1/accounts/#{account.id}/subscriptions",
- headers: agent.create_new_auth_token,
- as: :json
-
- expect(response).to have_http_status(:not_found)
- end
- end
- end
-end
diff --git a/spec/controllers/api/v1/accounts/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb
similarity index 100%
rename from spec/controllers/api/v1/accounts/accounts_controller_spec.rb
rename to spec/controllers/api/v1/accounts_controller_spec.rb
diff --git a/spec/controllers/api/v1/profiles_controller_spec.rb b/spec/controllers/api/v1/profiles_controller_spec.rb
index a2226d68b..b8e2b60f9 100644
--- a/spec/controllers/api/v1/profiles_controller_spec.rb
+++ b/spec/controllers/api/v1/profiles_controller_spec.rb
@@ -77,6 +77,16 @@ RSpec.describe 'Profile API', type: :request do
agent.reload
expect(agent.avatar.attached?).to eq(true)
end
+
+ it 'updates the availability status' do
+ put '/api/v1/profile',
+ params: { profile: { availability: 'offline' } },
+ headers: agent.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ expect(::OnlineStatusTracker.get_status(account.id, agent.id)).to eq('offline')
+ end
end
end
end
diff --git a/spec/controllers/api/v1/widget/conversations_controller_spec.rb b/spec/controllers/api/v1/widget/conversations_controller_spec.rb
index 7b3cc6538..8056b552f 100644
--- a/spec/controllers/api/v1/widget/conversations_controller_spec.rb
+++ b/spec/controllers/api/v1/widget/conversations_controller_spec.rb
@@ -9,6 +9,24 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
let(:token) { ::Widget::TokenService.new(payload: payload).generate_token }
+ describe 'GET /api/v1/widget/conversations' do
+ context 'with a conversation' do
+ it 'returns the correct conversation params' do
+ allow(Rails.configuration.dispatcher).to receive(:dispatch)
+ get '/api/v1/widget/conversations',
+ headers: { 'X-Auth-Token' => token },
+ params: { website_token: web_widget.website_token },
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ json_response = JSON.parse(response.body)
+
+ expect(json_response['id']).to eq(conversation.display_id)
+ expect(json_response['status']).to eq(conversation.status)
+ end
+ end
+ end
+
describe 'POST /api/v1/widget/conversations/toggle_typing' do
context 'with a conversation' do
it 'dispatches the correct typing status' do
@@ -25,20 +43,20 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
end
end
- describe 'POST /api/v1/widget/conversations' do
+ describe 'POST /api/v1/widget/conversations/update_last_seen' do
context 'with a conversation' do
it 'returns the correct conversation params' do
allow(Rails.configuration.dispatcher).to receive(:dispatch)
- get '/api/v1/widget/conversations',
- headers: { 'X-Auth-Token' => token },
- params: { website_token: web_widget.website_token },
- as: :json
+ expect(conversation.user_last_seen_at).to eq(nil)
+
+ post '/api/v1/widget/conversations/update_last_seen',
+ headers: { 'X-Auth-Token' => token },
+ params: { website_token: web_widget.website_token },
+ as: :json
expect(response).to have_http_status(:success)
- json_response = JSON.parse(response.body)
- expect(json_response['id']).to eq(conversation.display_id)
- expect(json_response['status']).to eq(conversation.status)
+ expect(conversation.reload.user_last_seen_at).not_to eq(nil)
end
end
end
diff --git a/spec/controllers/api/v1/widget/events_controller_spec.rb b/spec/controllers/api/v1/widget/events_controller_spec.rb
index e99686830..a1b8e4af9 100644
--- a/spec/controllers/api/v1/widget/events_controller_spec.rb
+++ b/spec/controllers/api/v1/widget/events_controller_spec.rb
@@ -30,7 +30,9 @@ RSpec.describe '/api/v1/widget/events', type: :request do
as: :json
expect(response).to have_http_status(:success)
- expect(Rails.configuration.dispatcher).to have_received(:dispatch).with(params[:name], anything, contact_inbox: contact_inbox)
+ expect(Rails.configuration.dispatcher).to have_received(:dispatch)
+ .with(params[:name], anything, contact_inbox: contact_inbox,
+ event_info: { browser_language: nil, widget_language: nil })
end
end
end
diff --git a/spec/controllers/api/v1/widget/messages_controller_spec.rb b/spec/controllers/api/v1/widget/messages_controller_spec.rb
index e4983fa57..a8120d93e 100644
--- a/spec/controllers/api/v1/widget/messages_controller_spec.rb
+++ b/spec/controllers/api/v1/widget/messages_controller_spec.rb
@@ -24,8 +24,8 @@ RSpec.describe '/api/v1/widget/messages', type: :request do
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
- # 2 messages created + 3 messages by the template hook
- expect(json_response.length).to eq(5)
+ # 2 messages created + 2 messages by the email hook
+ expect(json_response.length).to eq(4)
end
end
end
diff --git a/spec/factories/conversations.rb b/spec/factories/conversations.rb
index a14845e7f..e3647cc64 100644
--- a/spec/factories/conversations.rb
+++ b/spec/factories/conversations.rb
@@ -4,9 +4,9 @@ FactoryBot.define do
factory :conversation do
status { 'open' }
display_id { rand(10_000_000) }
- user_last_seen_at { Time.current }
agent_last_seen_at { Time.current }
locked { false }
+ identifier { SecureRandom.hex }
after(:build) do |conversation|
conversation.account ||= create(:account)
diff --git a/spec/factories/integrations/hooks.rb b/spec/factories/integrations/hooks.rb
new file mode 100644
index 000000000..6af74af9c
--- /dev/null
+++ b/spec/factories/integrations/hooks.rb
@@ -0,0 +1,12 @@
+FactoryBot.define do
+ factory :integrations_hook, class: 'Integrations::Hook' do
+ status { 1 }
+ inbox_id { 1 }
+ account_id { 1 }
+ app_id { 'slack' }
+ settings { 'MyText' }
+ hook_type { 1 }
+ access_token { SecureRandom.hex }
+ reference_id { SecureRandom.hex }
+ end
+end
diff --git a/spec/factories/labels.rb b/spec/factories/labels.rb
new file mode 100644
index 000000000..67d4d572e
--- /dev/null
+++ b/spec/factories/labels.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :label do
+ account
+ sequence(:title) { |n| "Label_#{n}" }
+ end
+end
diff --git a/spec/factories/messages.rb b/spec/factories/messages.rb
index 6f9e37a5d..236095a0a 100644
--- a/spec/factories/messages.rb
+++ b/spec/factories/messages.rb
@@ -9,7 +9,7 @@ FactoryBot.define do
account { create(:account) }
after(:build) do |message|
- message.user ||= create(:user, account: message.account)
+ message.sender ||= create(:user, account: message.account)
message.conversation ||= create(:conversation, account: message.account)
message.inbox ||= create(:inbox, account: message.account)
end
diff --git a/spec/finders/message_finder_spec.rb b/spec/finders/message_finder_spec.rb
index e51c7c406..58dafcef6 100644
--- a/spec/finders/message_finder_spec.rb
+++ b/spec/finders/message_finder_spec.rb
@@ -15,7 +15,7 @@ describe ::MessageFinder do
create(:message, account: account, inbox: inbox, conversation: conversation)
create(:message, message_type: 'activity', account: account, inbox: inbox, conversation: conversation)
create(:message, message_type: 'activity', account: account, inbox: inbox, conversation: conversation)
- # this outgoing message creates 3 additional messages because of the hook execution service
+ # this outgoing message creates 2 additional messages because of the email hook execution service
create(:message, message_type: 'outgoing', account: account, inbox: inbox, conversation: conversation)
end
@@ -25,7 +25,7 @@ describe ::MessageFinder do
it 'filter conversations by status' do
result = message_finder.perform
- expect(result.count).to be 7
+ expect(result.count).to be 6
end
end
@@ -34,7 +34,7 @@ describe ::MessageFinder do
it 'filter conversations by status' do
result = message_finder.perform
- expect(result.count).to be 5
+ expect(result.count).to be 4
end
end
@@ -44,7 +44,7 @@ describe ::MessageFinder do
it 'filter conversations by status' do
result = message_finder.perform
- expect(result.count).to be 7
+ expect(result.count).to be 6
end
end
end
diff --git a/spec/fixtures/subscriptions.yml b/spec/fixtures/subscriptions.yml
deleted file mode 100644
index f279a5c48..000000000
--- a/spec/fixtures/subscriptions.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
-
-one:
- pricing_version: MyString
- account_id: 1
- expiry: 2017-04-24 22:47:08
- billing_plan: MyString
- stripe_customer_id: MyString
-
-two:
- pricing_version: MyString
- account_id: 1
- expiry: 2017-04-24 22:47:08
- billing_plan: MyString
- stripe_customer_id: MyString
diff --git a/spec/jobs/hook_job_spec.rb b/spec/jobs/hook_job_spec.rb
new file mode 100644
index 000000000..b852db7ad
--- /dev/null
+++ b/spec/jobs/hook_job_spec.rb
@@ -0,0 +1,15 @@
+require 'rails_helper'
+
+RSpec.describe HookJob, type: :job do
+ subject(:job) { described_class.perform_later(hook, message) }
+
+ let(:account) { create(:account) }
+ let(:hook) { create(:integrations_hook, account: account) }
+ let(:message) { create(:message) }
+
+ it 'queues the job' do
+ expect { job }.to have_enqueued_job(described_class)
+ .with(hook, message)
+ .on_queue('integrations')
+ end
+end
diff --git a/spec/lib/integrations/slack/hook_builder_spec.rb b/spec/lib/integrations/slack/hook_builder_spec.rb
new file mode 100644
index 000000000..c3670b723
--- /dev/null
+++ b/spec/lib/integrations/slack/hook_builder_spec.rb
@@ -0,0 +1,22 @@
+require 'rails_helper'
+
+describe Integrations::Slack::HookBuilder do
+ let(:account) { create(:account) }
+ let(:code) { SecureRandom.hex }
+ let(:token) { SecureRandom.hex }
+
+ describe '#perform' do
+ it 'creates hook' do
+ hooks_count = account.hooks.count
+
+ builder = described_class.new(account: account, code: code)
+ allow(builder).to receive(:fetch_access_token).and_return(token)
+
+ builder.perform
+ expect(account.hooks.count).to eql(hooks_count + 1)
+
+ hook = account.hooks.last
+ expect(hook.access_token).to eql(token)
+ end
+ end
+end
diff --git a/spec/lib/integrations/slack/incoming_message_builder_spec.rb b/spec/lib/integrations/slack/incoming_message_builder_spec.rb
new file mode 100644
index 000000000..fbbffb687
--- /dev/null
+++ b/spec/lib/integrations/slack/incoming_message_builder_spec.rb
@@ -0,0 +1,47 @@
+require 'rails_helper'
+
+describe Integrations::Slack::IncomingMessageBuilder do
+ let(:account) { create(:account) }
+ let(:message_params) { slack_message_stub }
+ let(:verification_params) { slack_url_verification_stub }
+
+ let!(:hook) { create(:integrations_hook, account: account, reference_id: message_params[:event][:channel]) }
+ let!(:conversation) { create(:conversation, identifier: message_params[:event][:thread_ts]) }
+
+ describe '#perform' do
+ context 'when url verification' do
+ it 'return challenge code as response' do
+ builder = described_class.new(verification_params)
+ response = builder.perform
+ expect(response[:challenge]).to eql(verification_params[:challenge])
+ end
+ end
+
+ context 'when message creation' do
+ it 'creates message' do
+ expect(hook).not_to eq nil
+ messages_count = conversation.messages.count
+ builder = described_class.new(message_params)
+ allow(builder).to receive(:sender).and_return(nil)
+ builder.perform
+ expect(conversation.messages.count).to eql(messages_count + 1)
+ end
+
+ it 'does not create message for invalid event type' do
+ messages_count = conversation.messages.count
+ message_params[:type] = 'invalid_event_type'
+ builder = described_class.new(message_params)
+ builder.perform
+ expect(conversation.messages.count).to eql(messages_count)
+ end
+
+ it 'does not create message for invalid event name' do
+ messages_count = conversation.messages.count
+ message_params[:event][:type] = 'invalid_event_name'
+ builder = described_class.new(message_params)
+ builder.perform
+ expect(conversation.messages.count).to eql(messages_count)
+ end
+ end
+ end
+end
diff --git a/spec/lib/integrations/slack/send_on_slack_service_spec.rb b/spec/lib/integrations/slack/send_on_slack_service_spec.rb
new file mode 100644
index 000000000..a709c2ce0
--- /dev/null
+++ b/spec/lib/integrations/slack/send_on_slack_service_spec.rb
@@ -0,0 +1,31 @@
+require 'rails_helper'
+
+describe Integrations::Slack::SendOnSlackService do
+ let(:account) { create(:account) }
+ let!(:inbox) { create(:inbox, account: account) }
+ let!(:contact) { create(:contact) }
+
+ let!(:hook) { create(:integrations_hook, account: account) }
+ let!(:conversation) { create(:conversation, account: account, inbox: inbox, contact: contact) }
+ let!(:message) { create(:message, account: account, inbox: inbox, conversation: conversation) }
+
+ describe '#perform' do
+ it 'sent message to slack' do
+ builder = described_class.new(message: message, hook: hook)
+ stub_request(:post, 'https://slack.com/api/chat.postMessage')
+ .to_return(status: 200, body: '', headers: {})
+ slack_client = double
+ expect(builder).to receive(:slack_client).and_return(slack_client)
+
+ expect(slack_client).to receive(:chat_postMessage).with(
+ channel: hook.reference_id,
+ text: message.content,
+ username: "Agent: #{message.sender.name}",
+ thread_ts: conversation.identifier,
+ icon_url: anything
+ )
+
+ builder.perform
+ end
+ end
+end
diff --git a/spec/lib/webhooks/twitter_spec.rb b/spec/lib/webhooks/twitter_spec.rb
index 28220406b..cb01d03da 100644
--- a/spec/lib/webhooks/twitter_spec.rb
+++ b/spec/lib/webhooks/twitter_spec.rb
@@ -7,7 +7,7 @@ describe Webhooks::Twitter do
let!(:account) { create(:account) }
# FIX ME: recipient id is set to 1 inside event factories
let!(:twitter_channel) { create(:channel_twitter_profile, account: account, profile_id: '1') }
- let!(:twitter_inbox) { create(:inbox, channel: twitter_channel, account: account) }
+ let!(:twitter_inbox) { create(:inbox, channel: twitter_channel, account: account, greeting_enabled: false) }
let!(:dm_params) { build(:twitter_message_create_event).with_indifferent_access }
let!(:tweet_params) { build(:tweet_create_event).with_indifferent_access }
diff --git a/spec/listeners/hook_listener_spec.rb b/spec/listeners/hook_listener_spec.rb
new file mode 100644
index 000000000..f22a074ab
--- /dev/null
+++ b/spec/listeners/hook_listener_spec.rb
@@ -0,0 +1,32 @@
+require 'rails_helper'
+describe HookListener do
+ let(:listener) { described_class.instance }
+ let!(:account) { create(:account) }
+ let!(:user) { create(:user, account: account) }
+ let!(:inbox) { create(:inbox, account: account) }
+ let!(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: user) }
+ let!(:message) do
+ create(:message, message_type: 'outgoing',
+ account: account, inbox: inbox, conversation: conversation)
+ end
+ let!(:event) { Events::Base.new(event_name, Time.zone.now, message: message) }
+
+ describe '#message_created' do
+ let(:event_name) { :'conversation.created' }
+
+ context 'when hook is not configured' do
+ it 'does not trigger hook job' do
+ expect(HookJob).to receive(:perform_later).exactly(0).times
+ listener.message_created(event)
+ end
+ end
+
+ context 'when hook is configured' do
+ it 'triggers hook job' do
+ hook = create(:integrations_hook, account: account)
+ expect(HookJob).to receive(:perform_later).with(hook, message).once
+ listener.message_created(event)
+ end
+ end
+ end
+end
diff --git a/spec/mailboxes/conversation_mailbox_spec.rb b/spec/mailboxes/conversation_mailbox_spec.rb
index c3e5dfa2a..65a757058 100644
--- a/spec/mailboxes/conversation_mailbox_spec.rb
+++ b/spec/mailboxes/conversation_mailbox_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe ConversationMailbox, type: :mailbox do
describe 'add mail as reply in a conversation' do
let(:agent) { create(:user, email: 'agent1@example.com') }
let(:reply_mail) { create_inbound_email_from_fixture('reply.eml') }
- let(:conversation) { create(:conversation, assignee: agent) }
+ let(:conversation) { create(:conversation, assignee: agent, inbox: create(:inbox, greeting_enabled: false)) }
let(:described_subject) { described_class.receive reply_mail }
let(:serialized_attributes) { %w[text_content html_content number_of_attachments subject date to from in_reply_to cc bcc message_id] }
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index 821eb31ba..59929ae8f 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -14,7 +14,6 @@ RSpec.describe Account do
it { is_expected.to have_many(:canned_responses).dependent(:destroy) }
it { is_expected.to have_many(:facebook_pages).class_name('::Channel::FacebookPage').dependent(:destroy) }
it { is_expected.to have_many(:web_widgets).class_name('::Channel::WebWidget').dependent(:destroy) }
- it { is_expected.to have_one(:subscription).dependent(:destroy) }
it { is_expected.to have_many(:webhooks).dependent(:destroy) }
it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
it { is_expected.to have_many(:events) }
diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb
index c92ed10d1..1c5cc2939 100644
--- a/spec/models/conversation_spec.rb
+++ b/spec/models/conversation_spec.rb
@@ -108,6 +108,7 @@ RSpec.describe Conversation, type: :model do
end
before do
+ create(:inbox_member, inbox: inbox, user: agent)
allow(Redis::Alfred).to receive(:rpoplpush).and_return(agent.id)
end
@@ -141,9 +142,11 @@ RSpec.describe Conversation, type: :model do
conversation.status = 'resolved'
conversation.save!
expect(conversation.reload.assignee).to eq(agent)
+ inbox.inbox_members.where(user_id: agent.id).first.destroy!
# round robin changes assignee in this case since agent doesn't have access to inbox
agent2 = create(:user, email: 'agent2@example.com', account: account)
+ create(:inbox_member, inbox: inbox, user: agent2)
allow(Redis::Alfred).to receive(:rpoplpush).and_return(agent2.id)
conversation.status = 'open'
conversation.save!
@@ -254,7 +257,7 @@ RSpec.describe Conversation, type: :model do
conversation: conversation,
account: conversation.account,
inbox: conversation.inbox,
- user: conversation.assignee
+ sender: conversation.assignee
}
end
let!(:message) do
@@ -279,7 +282,7 @@ RSpec.describe Conversation, type: :model do
conversation: conversation,
account: conversation.account,
inbox: conversation.inbox,
- user: conversation.assignee,
+ sender: conversation.assignee,
created_at: 1.minute.ago
}
end
@@ -312,6 +315,7 @@ RSpec.describe Conversation, type: :model do
inbox_id: conversation.inbox_id,
status: conversation.status,
timestamp: conversation.created_at.to_i,
+ channel: 'Channel::WebWidget',
user_last_seen_at: conversation.user_last_seen_at.to_i,
agent_last_seen_at: conversation.agent_last_seen_at.to_i,
unread_count: 0
diff --git a/spec/models/inbox_spec.rb b/spec/models/inbox_spec.rb
index 65dd9ee9e..e92e66c31 100644
--- a/spec/models/inbox_spec.rb
+++ b/spec/models/inbox_spec.rb
@@ -29,6 +29,8 @@ RSpec.describe Inbox do
it { is_expected.to have_many(:webhooks).dependent(:destroy) }
it { is_expected.to have_many(:events) }
+
+ it { is_expected.to have_many(:hooks) }
end
describe '#add_member' do
diff --git a/spec/models/integrations/hook_spec.rb b/spec/models/integrations/hook_spec.rb
new file mode 100644
index 000000000..da5f1ff20
--- /dev/null
+++ b/spec/models/integrations/hook_spec.rb
@@ -0,0 +1,12 @@
+require 'rails_helper'
+
+RSpec.describe Integrations::Hook, type: :model do
+ context 'with validations' do
+ it { is_expected.to validate_presence_of(:app_id) }
+ it { is_expected.to validate_presence_of(:account_id) }
+ end
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:account) }
+ end
+end
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
new file mode 100644
index 000000000..215e9d5a6
--- /dev/null
+++ b/spec/models/label_spec.rb
@@ -0,0 +1,42 @@
+require 'rails_helper'
+
+RSpec.describe Label, type: :model do
+ describe 'associations' do
+ it { is_expected.to belong_to(:account) }
+ end
+
+ describe 'title validations' do
+ it 'would not let you start title without numbers or letters' do
+ label = FactoryBot.build(:label, title: '_12')
+ expect(label.valid?).to eq false
+ end
+
+ it 'would not let you use special characters' do
+ label = FactoryBot.build(:label, title: 'jell;;2_12')
+ expect(label.valid?).to eq false
+ end
+
+ it 'would not allow space' do
+ label = FactoryBot.build(:label, title: 'heeloo _12')
+ expect(label.valid?).to eq false
+ end
+
+ it 'allows foreign charactes' do
+ label = FactoryBot.build(:label, title: '学中文_12')
+ expect(label.valid?).to eq true
+ end
+
+ it 'converts uppercase letters to lowercase' do
+ label = FactoryBot.build(:label, title: 'Hello_World')
+ expect(label.valid?).to eq true
+ expect(label.title).to eq 'hello_world'
+ end
+
+ it 'validates uniqueness of label name for account' do
+ account = create(:account)
+ label = FactoryBot.create(:label, account: account)
+ duplicate_label = FactoryBot.build(:label, title: label.title, account: account)
+ expect(duplicate_label.valid?).to eq false
+ end
+ end
+end
diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb
new file mode 100644
index 000000000..7920df919
--- /dev/null
+++ b/spec/models/notification_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Notification do
+ context 'with associations' do
+ it { is_expected.to belong_to(:account) }
+ it { is_expected.to belong_to(:user) }
+ end
+
+ context 'with default order by' do
+ it 'sort by primary id desc' do
+ notification1 = create(:notification)
+ create(:notification)
+ notification3 = create(:notification)
+
+ expect(described_class.all.first).to eq notification3
+ expect(described_class.all.last).to eq notification1
+ end
+ end
+end
diff --git a/spec/presenters/conversations/event_data_presenter_spec.rb b/spec/presenters/conversations/event_data_presenter_spec.rb
index 82a9dc771..76326b616 100644
--- a/spec/presenters/conversations/event_data_presenter_spec.rb
+++ b/spec/presenters/conversations/event_data_presenter_spec.rb
@@ -22,6 +22,7 @@ RSpec.describe Conversations::EventDataPresenter do
messages: [],
inbox_id: conversation.inbox_id,
status: conversation.status,
+ channel: conversation.inbox.channel_type,
timestamp: conversation.created_at.to_i,
user_last_seen_at: conversation.user_last_seen_at.to_i,
agent_last_seen_at: conversation.agent_last_seen_at.to_i,
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index ef39ac0ef..bb4475cca 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -22,7 +22,9 @@ require 'sidekiq/testing'
# directory. Alternatively, in the individual `*_spec.rb` files, manually
# require only the support files necessary.
#
-# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
+# rubocop:disable Rails/FilePath
+Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
+# rubocop:enable Rails/FilePath
# Checks for pending migrations and applies them before tests are run.
# If you are not using ActiveRecord, you can remove these lines.
@@ -61,7 +63,7 @@ RSpec.configure do |config|
config.filter_rails_from_backtrace!
# arbitrary gems may also be filtered via:
# config.filter_gems_from_backtrace("gem name")
-
+ config.include SlackStubs
config.include Devise::Test::IntegrationHelpers, type: :request
end
diff --git a/spec/requests/api/v1/accounts/integrations/slack_request_spec.rb b/spec/requests/api/v1/accounts/integrations/slack_request_spec.rb
new file mode 100644
index 000000000..e68eabf73
--- /dev/null
+++ b/spec/requests/api/v1/accounts/integrations/slack_request_spec.rb
@@ -0,0 +1,80 @@
+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!(:hook) { create(:integrations_hook, account: account) }
+
+ describe 'POST /api/v1/accounts/{account.id}/integrations/slack' do
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ post "/api/v1/accounts/#{account.id}/integrations/slack", params: {}
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an authenticated user' do
+ it 'creates hook' do
+ hook_builder = Integrations::Slack::HookBuilder.new(account: account, code: SecureRandom.hex)
+ expect(hook_builder).to receive(:fetch_access_token).and_return(SecureRandom.hex)
+ expect(Integrations::Slack::HookBuilder).to receive(:new).and_return(hook_builder)
+
+ channel_builder = Integrations::Slack::ChannelBuilder.new(hook: hook, channel: 'channel')
+ expect(channel_builder).to receive(:perform)
+ expect(Integrations::Slack::ChannelBuilder).to receive(:new).and_return(channel_builder)
+
+ post "/api/v1/accounts/#{account.id}/integrations/slack",
+ params: { code: SecureRandom.hex },
+ headers: agent.create_new_auth_token
+
+ expect(response).to have_http_status(:success)
+ json_response = JSON.parse(response.body)
+ expect(json_response['id']).to eql('slack')
+ end
+ end
+
+ describe 'PUT /api/v1/accounts/{account.id}/integrations/slack/' do
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ put "/api/v1/accounts/#{account.id}/integrations/slack/", params: {}
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an authenticated user' do
+ it 'updates hook' do
+ channel_builder = Integrations::Slack::ChannelBuilder.new(hook: hook, channel: 'channel')
+ expect(channel_builder).to receive(:perform)
+
+ expect(Integrations::Slack::ChannelBuilder).to receive(:new).and_return(channel_builder)
+
+ put "/api/v1/accounts/#{account.id}/integrations/slack",
+ params: { channel: SecureRandom.hex },
+ headers: agent.create_new_auth_token
+
+ expect(response).to have_http_status(:success)
+ json_response = JSON.parse(response.body)
+ expect(json_response['app_id']).to eql('slack')
+ end
+ end
+ end
+
+ describe 'DELETE /api/v1/accounts/{account.id}/integrations/slack' do
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ delete "/api/v1/accounts/#{account.id}/integrations/slack", params: {}
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an authenticated user' do
+ it 'deletes hook' do
+ delete "/api/v1/accounts/#{account.id}/integrations/slack",
+ headers: agent.create_new_auth_token
+ expect(response).to have_http_status(:success)
+ expect(Integrations::Hook.find_by(id: hook.id)).to be nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v1/integrations/webhooks_request_spec.rb b/spec/requests/api/v1/integrations/webhooks_request_spec.rb
new file mode 100644
index 000000000..ea870d45c
--- /dev/null
+++ b/spec/requests/api/v1/integrations/webhooks_request_spec.rb
@@ -0,0 +1,15 @@
+require 'rails_helper'
+
+RSpec.describe 'Api::V1::Integrations::Webhooks', type: :request do
+ describe 'POST /api/v1/integrations/webhooks' do
+ it 'consumes webhook' do
+ builder = Integrations::Slack::IncomingMessageBuilder.new({})
+ expect(builder).to receive(:perform).and_return(true)
+
+ expect(Integrations::Slack::IncomingMessageBuilder).to receive(:new).and_return(builder)
+
+ post '/api/v1/integrations/webhooks', params: {}
+ expect(response).to have_http_status(:success)
+ end
+ end
+end
diff --git a/spec/services/facebook/send_reply_service_spec.rb b/spec/services/facebook/send_on_facebook_service_spec.rb
similarity index 98%
rename from spec/services/facebook/send_reply_service_spec.rb
rename to spec/services/facebook/send_on_facebook_service_spec.rb
index a74f1e826..742044abd 100644
--- a/spec/services/facebook/send_reply_service_spec.rb
+++ b/spec/services/facebook/send_on_facebook_service_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe Facebook::SendReplyService do
+describe Facebook::SendOnFacebookService do
subject(:send_reply_service) { described_class.new(message: message) }
before do
diff --git a/spec/services/message_templates/hook_execution_service_spec.rb b/spec/services/message_templates/hook_execution_service_spec.rb
index 90a7893ad..4b5b8537f 100644
--- a/spec/services/message_templates/hook_execution_service_spec.rb
+++ b/spec/services/message_templates/hook_execution_service_spec.rb
@@ -5,17 +5,22 @@ describe ::MessageTemplates::HookExecutionService do
it 'calls ::MessageTemplates::Template::EmailCollect' do
contact = create(:contact, email: nil)
conversation = create(:conversation, contact: contact)
- message = create(:message, conversation: conversation)
- # this hook will only get executed for conversations with out any template messages
- message.conversation.messages.template.destroy_all
+ # ensure greeting hook is enabled
+ conversation.inbox.update(greeting_enabled: true)
email_collect_service = double
+ greeting_service = double
allow(::MessageTemplates::Template::EmailCollect).to receive(:new).and_return(email_collect_service)
allow(email_collect_service).to receive(:perform).and_return(true)
+ allow(::MessageTemplates::Template::Greeting).to receive(:new).and_return(greeting_service)
+ allow(greeting_service).to receive(:perform).and_return(true)
- described_class.new(message: message).perform
+ # described class gets called in message after commit
+ message = create(:message, conversation: conversation)
+ expect(::MessageTemplates::Template::Greeting).to have_received(:new).with(conversation: message.conversation)
+ expect(greeting_service).to have_received(:perform)
expect(::MessageTemplates::Template::EmailCollect).to have_received(:new).with(conversation: message.conversation)
expect(email_collect_service).to have_received(:perform)
end
diff --git a/spec/services/message_templates/template/email_collect_spec.rb b/spec/services/message_templates/template/email_collect_spec.rb
index 1d688e37a..4fe5256c7 100644
--- a/spec/services/message_templates/template/email_collect_spec.rb
+++ b/spec/services/message_templates/template/email_collect_spec.rb
@@ -6,7 +6,7 @@ describe ::MessageTemplates::Template::EmailCollect do
it 'creates the email collect messages' do
described_class.new(conversation: conversation).perform
- expect(conversation.messages.count).to eq(3)
+ expect(conversation.messages.count).to eq(2)
end
end
end
diff --git a/spec/services/message_templates/template/greeting_spec.rb b/spec/services/message_templates/template/greeting_spec.rb
new file mode 100644
index 000000000..7d1f45d8c
--- /dev/null
+++ b/spec/services/message_templates/template/greeting_spec.rb
@@ -0,0 +1,12 @@
+require 'rails_helper'
+
+describe ::MessageTemplates::Template::Greeting do
+ context 'when this hook is called' do
+ let(:conversation) { create(:conversation) }
+
+ it 'creates the email collect messages' do
+ described_class.new(conversation: conversation).perform
+ expect(conversation.messages.count).to eq(1)
+ end
+ end
+end
diff --git a/spec/services/notification/push_notification_service_spec.rb b/spec/services/notification/push_notification_service_spec.rb
new file mode 100644
index 000000000..44a5dbaf5
--- /dev/null
+++ b/spec/services/notification/push_notification_service_spec.rb
@@ -0,0 +1,36 @@
+require 'rails_helper'
+
+describe Notification::PushNotificationService do
+ let!(:account) { create(:account) }
+ let!(:user) { create(:user, account: account) }
+ let!(:notification) { create(:notification, user: user, account: user.accounts.first) }
+ let(:fcm_double) { double }
+
+ before do
+ allow(Webpush).to receive(:payload_send).and_return(true)
+ allow(FCM).to receive(:new).and_return(fcm_double)
+ allow(fcm_double).to receive(:send).and_return({ body: { 'results': [] }.to_json })
+ end
+
+ describe '#perform' do
+ it 'sends webpush notifications for webpush subscription' do
+ ENV['VAPID_PUBLIC_KEY'] = 'test'
+ create(:notification_subscription, user: notification.user)
+
+ described_class.new(notification: notification).perform
+ expect(Webpush).to have_received(:payload_send)
+ expect(FCM).not_to have_received(:new)
+ ENV['ENABLE_ACCOUNT_SIGNUP'] = nil
+ end
+
+ it 'sends a fcm notification for firebase subscription' do
+ ENV['FCM_SERVER_KEY'] = 'test'
+ create(:notification_subscription, user: notification.user, subscription_type: 'fcm')
+
+ described_class.new(notification: notification).perform
+ expect(FCM).to have_received(:new)
+ expect(Webpush).not_to have_received(:payload_send)
+ ENV['ENABLE_ACCOUNT_SIGNUP'] = nil
+ end
+ end
+end
diff --git a/spec/services/round_robin/manage_service_spec.rb b/spec/services/round_robin/manage_service_spec.rb
new file mode 100644
index 000000000..8d0696b26
--- /dev/null
+++ b/spec/services/round_robin/manage_service_spec.rb
@@ -0,0 +1,41 @@
+require 'rails_helper'
+
+describe RoundRobin::ManageService do
+ let!(:account) { create(:account) }
+ let!(:inbox) { create(:inbox, account: account) }
+ let!(:inbox_members) { create_list(:inbox_member, 5, inbox: inbox) }
+ let(:subject) { ::RoundRobin::ManageService.new(inbox: inbox) }
+
+ describe '#available_agent' do
+ it 'gets the first available agent and move agent to end of the list' do
+ expected_queue = [inbox_members[0].user_id, inbox_members[4].user_id, inbox_members[3].user_id, inbox_members[2].user_id,
+ inbox_members[1].user_id].map(&:to_s)
+ subject.available_agent
+ expect(subject.send(:queue)).to eq(expected_queue)
+ end
+
+ it 'gets intersection of priority list and agent queue. get and move agent to the end of the list' do
+ expected_queue = [inbox_members[2].user_id, inbox_members[4].user_id, inbox_members[3].user_id, inbox_members[1].user_id,
+ inbox_members[0].user_id].map(&:to_s)
+ expect(subject.available_agent(priority_list: [inbox_members[3].user_id, inbox_members[2].user_id])).to eq inbox_members[2].user
+ expect(subject.send(:queue)).to eq(expected_queue)
+ end
+
+ it 'constructs round_robin_queue if queue is not present' do
+ subject.clear_queue
+ expect(subject.send(:queue)).to eq([])
+ subject.available_agent
+ # the service constructed the redis queue before performing
+ expect(subject.send(:queue).sort.map(&:to_i)).to eq(inbox_members.map(&:user_id).sort)
+ end
+
+ it 'validates the queue and correct it before performing round robin' do
+ # adding some invalid ids to queue
+ subject.add_agent_to_queue([2, 3, 5, 9])
+ expect(subject.send(:queue).sort.map(&:to_i)).not_to eq(inbox_members.map(&:user_id).sort)
+ subject.available_agent
+ # the service have refreshed the redis queue before performing
+ expect(subject.send(:queue).sort.map(&:to_i)).to eq(inbox_members.map(&:user_id).sort)
+ end
+ end
+end
diff --git a/spec/services/twilio/incoming_message_service_spec.rb b/spec/services/twilio/incoming_message_service_spec.rb
index 7b895bd35..277a4f683 100644
--- a/spec/services/twilio/incoming_message_service_spec.rb
+++ b/spec/services/twilio/incoming_message_service_spec.rb
@@ -3,7 +3,8 @@ require 'rails_helper'
describe Twilio::IncomingMessageService do
let!(:account) { create(:account) }
let!(:twilio_sms) do
- create(:channel_twilio_sms, account: account, phone_number: '+1234567890', account_sid: 'ACxxx')
+ create(:channel_twilio_sms, account: account, phone_number: '+1234567890', account_sid: 'ACxxx',
+ inbox: create(:inbox, account: account, greeting_enabled: false))
end
let!(:contact) { create(:contact, account: account, phone_number: '+12345') }
let(:contact_inbox) { create(:contact_inbox, source_id: '+12345', contact: contact, inbox: twilio_sms.inbox) }
diff --git a/spec/services/twilio/outgoing_message_service_spec.rb b/spec/services/twilio/send_on_twilio_service_spec.rb
similarity index 98%
rename from spec/services/twilio/outgoing_message_service_spec.rb
rename to spec/services/twilio/send_on_twilio_service_spec.rb
index 064f3bced..56a8283c3 100644
--- a/spec/services/twilio/outgoing_message_service_spec.rb
+++ b/spec/services/twilio/send_on_twilio_service_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe Twilio::OutgoingMessageService do
+describe Twilio::SendOnTwilioService do
subject(:outgoing_message_service) { described_class.new(message: message) }
let(:twilio_client) { instance_double(::Twilio::REST::Client) }
diff --git a/spec/services/twitter/send_reply_service_spec.rb b/spec/services/twitter/send_on_twitter_service_spec.rb
similarity index 98%
rename from spec/services/twitter/send_reply_service_spec.rb
rename to spec/services/twitter/send_on_twitter_service_spec.rb
index 140144e83..017fcf27c 100644
--- a/spec/services/twitter/send_reply_service_spec.rb
+++ b/spec/services/twitter/send_on_twitter_service_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe Twitter::SendReplyService do
+describe Twitter::SendOnTwitterService do
subject(:send_reply_service) { described_class.new(message: message) }
let(:twitter_client) { instance_double(::Twitty::Facade) }
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index ee6aa71b9..d017eb0da 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,5 +1,8 @@
require 'simplecov'
+require 'webmock/rspec'
+
SimpleCov.start 'rails'
+WebMock.allow_net_connect!
RSpec.configure do |config|
config.expect_with :rspec do |expectations|
@@ -11,6 +14,4 @@ RSpec.configure do |config|
end
config.shared_context_metadata_behavior = :apply_to_host_groups
-
- # config.include Rails.application.routes.url_helpers
end
diff --git a/spec/support/slack_stubs.rb b/spec/support/slack_stubs.rb
new file mode 100644
index 000000000..18af884ed
--- /dev/null
+++ b/spec/support/slack_stubs.rb
@@ -0,0 +1,57 @@
+module SlackStubs
+ def slack_url_verification_stub
+ {
+ "token": 'Jhj5dZrVaK7ZwHHjRyZWjbDl',
+ "challenge": '3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P',
+ "type": 'url_verification'
+ }
+ end
+
+ def slack_message_stub
+ {
+ "token": '[FILTERED]',
+ "team_id": 'TLST3048H',
+ "api_app_id": 'A012S5UETV4',
+ "event": message_event,
+ "type": 'event_callback',
+ "event_id": 'Ev013QUX3WV6',
+ "event_time": 1_588_623_033,
+ "authed_users": '[FILTERED]',
+ "webhook": {}
+ }
+ end
+
+ def message_event
+ { "client_msg_id": 'ffc6e64e-6f0c-4a3d-b594-faa6b44e48ab',
+ "type": 'message',
+ "text": 'this is test',
+ "user": 'ULYPAKE5S',
+ "ts": '1588623033.006000',
+ "team": 'TLST3048H',
+ "blocks": message_blocks,
+ "thread_ts": '1588623023.005900',
+ "channel": 'G01354F6A6Q',
+ "event_ts": '1588623033.006000',
+ "channel_type": 'group' }
+ end
+
+ def message_blocks
+ [
+ {
+ "type": 'rich_text',
+ "block_id": 'jaIv3',
+ "elements": [
+ {
+ "type": 'rich_text_section',
+ "elements": [
+ {
+ "type": 'text',
+ "text": 'this is test'
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ end
+end
diff --git a/swagger/index.yml b/swagger/index.yml
index 5fd289f37..b00c78da7 100644
--- a/swagger/index.yml
+++ b/swagger/index.yml
@@ -17,6 +17,24 @@ produces:
- application/json; charset=utf-8
consumes:
- application/json; charset=utf-8
+securityDefinitions:
+ userApiKey:
+ type: apiKey
+ in: header
+ name: api_access_token
+ description: This token can be obtained by visiting the profile page or via rails console. Provides access to endpoints based on the user permissions levels. This token can be saved by an external system when user is created via API, to perform activities on behalf of the user.
+ agentBotApiKey:
+ type: apiKey
+ in: header
+ name: api_access_token
+ description: This token should be provided by system admin or obtained via rails console. This token can be used to build bot integrations and can only access limited apis.
+ superAdminApiKey:
+ type: apiKey
+ in: header
+ name: api_access_token
+ description: This token is only for the system admin or obtained via rails console. This token is to be used rarely for cases like creating a pre verified user through api from external system.
+security:
+ - userApiKey: []
paths:
$ref: ./paths/index.yml
diff --git a/swagger/paths/conversation/index_or_create.yml b/swagger/paths/conversation/index_or_create.yml
index b1a2dbdd4..2afba74c8 100644
--- a/swagger/paths/conversation/index_or_create.yml
+++ b/swagger/paths/conversation/index_or_create.yml
@@ -45,6 +45,9 @@ post:
operationId: newConversation
summary: Create New Conversation
description: Create conversation
+ security:
+ - userApiKey: []
+ - agentBotApiKey: []
parameters:
- name: data
in: body
diff --git a/swagger/paths/conversation/messages/index_create.yml b/swagger/paths/conversation/messages/index_create.yml
index a1f9e3279..d7ccf44fe 100644
--- a/swagger/paths/conversation/messages/index_create.yml
+++ b/swagger/paths/conversation/messages/index_create.yml
@@ -30,6 +30,9 @@ post:
operationId: conversationNewMessage
summary: Create New Message
description: All the agent replies are created as new messages through this endpoint
+ security:
+ - userApiKey: []
+ - agentBotApiKey: []
parameters:
- name: id
in: path
diff --git a/swagger/paths/conversation/toggle_status.yml b/swagger/paths/conversation/toggle_status.yml
index e9b6be822..3ec5b43ba 100644
--- a/swagger/paths/conversation/toggle_status.yml
+++ b/swagger/paths/conversation/toggle_status.yml
@@ -4,6 +4,9 @@ post:
operationId: conversationToggleStatus
summary: Toggle Status
description: Toggles the status of the conversation between open and resolved
+ security:
+ - userApiKey: []
+ - agentBotApiKey: []
parameters:
- name: id
in: path
diff --git a/swagger/swagger.json b/swagger/swagger.json
index 50a255d32..2ac8cc1f2 100644
--- a/swagger/swagger.json
+++ b/swagger/swagger.json
@@ -24,6 +24,33 @@
"consumes": [
"application/json; charset=utf-8"
],
+ "securityDefinitions": {
+ "userApiKey": {
+ "type": "apiKey",
+ "in": "header",
+ "name": "api_access_token",
+ "description": "This token can be obtained by visiting the profile page or via rails console. Provides access to endpoints based on the user permissions levels. This token can be saved by an external system when user is created via API, to perform activities on behalf of the user."
+ },
+ "agentBotApiKey": {
+ "type": "apiKey",
+ "in": "header",
+ "name": "api_access_token",
+ "description": "This token should be provided by system admin or obtained via rails console. This token can be used to build bot integrations and can only access limited apis."
+ },
+ "superAdminApiKey": {
+ "type": "apiKey",
+ "in": "header",
+ "name": "api_access_token",
+ "description": "This token is only for the system admin or obtained via rails console. This token is to be used rarely for cases like creating a pre verified user through api from external system."
+ }
+ },
+ "security": [
+ {
+ "userApiKey": [
+
+ ]
+ }
+ ],
"paths": {
"/accounts/{account_id}/inboxes": {
"post": {
@@ -325,6 +352,18 @@
"operationId": "newConversation",
"summary": "Create New Conversation",
"description": "Create conversation",
+ "security": [
+ {
+ "userApiKey": [
+
+ ]
+ },
+ {
+ "agentBotApiKey": [
+
+ ]
+ }
+ ],
"parameters": [
{
"name": "data",
@@ -409,6 +448,18 @@
"operationId": "conversationToggleStatus",
"summary": "Toggle Status",
"description": "Toggles the status of the conversation between open and resolved",
+ "security": [
+ {
+ "userApiKey": [
+
+ ]
+ },
+ {
+ "agentBotApiKey": [
+
+ ]
+ }
+ ],
"parameters": [
{
"name": "id",
@@ -428,7 +479,8 @@
"type": "string",
"enum": [
"open",
- "resolved"
+ "resolved",
+ "bot"
],
"required": true,
"description": "The status of the conversation"
@@ -500,6 +552,18 @@
"operationId": "conversationNewMessage",
"summary": "Create New Message",
"description": "All the agent replies are created as new messages through this endpoint",
+ "security": [
+ {
+ "userApiKey": [
+
+ ]
+ },
+ {
+ "agentBotApiKey": [
+
+ ]
+ }
+ ],
"parameters": [
{
"name": "id",
@@ -1035,6 +1099,14 @@
"type": "number",
"description": "ID of the inbox"
},
+ "name": {
+ "type": "string",
+ "description": "The name of the inbox"
+ },
+ "website_url": {
+ "type": "string",
+ "description": "Website URL"
+ },
"channel_type": {
"type": "string",
"description": "The type of the inbox"
diff --git a/yarn.lock b/yarn.lock
index 60dfd4fec..d78e760f4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10555,9 +10555,9 @@ websocket-driver@>=0.5.1:
websocket-extensions ">=0.1.1"
websocket-extensions@>=0.1.1:
- version "0.1.3"
- resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
- integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
+ integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.5:
version "1.0.5"