Compare commits

...
103 Commits
Author SHA1 Message Date
sony-mathew 0d7c1e5e11 Hotfix: Fix for private note in emails - Merge branch 'hotfix/1.4.1' 2020-05-10 16:21:30 +05:30
sony-mathew 35b2bdd074 Removed the un-neccessary attribute from msg create factory 2020-05-10 16:21:02 +05:30
sony-mathew 1a123a4f56 Spec for private messages not to be included in mails (#837) 2020-05-10 16:21:02 +05:30
sony-mathew a75f49ddcc Bug: Fixed private note being sent to customer (#837) 2020-05-10 16:21:02 +05:30
Sojan 628e0c8423 Merge branch 'release/1.4.0' 2020-05-06 14:26:24 +05:30
217f33b2b2 Chore: Add ro, fr, pt_BR translation files (#827)
Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
2020-05-06 14:24:20 +05:30
c7120e9637 Chore: Add translated languages to account settings (#826)
* Add available languages in account settings

Co-authored-by: Sojan <sojan@pepalo.com>
2020-05-06 13:38:36 +05:30
Sojan Jose d6d15c8c3c Update Crowdin configuration file 2020-05-06 13:20:31 +05:30
Pranav Raj SandGitHub b50aeb7ae4 Chore: Fix broken conversation mailer layout (#825) 2020-05-06 12:40:41 +05:30
e9131ea558 Feature: Add web push notification permission in frontend (#766)
Add webpush notification permission in frontend

Co-authored-by: Sojan <sojan@pepalo.com>
2020-05-06 00:10:56 +05:30
Vishnu NarayananandGitHub 5bd7a4c511 Feature: Add installation script (#776)
* Feature: Add installation script
* Add systemd service files for chatwoot
* Enable static file serving by rails on prod
* Fix systemd files path
2020-05-05 01:59:38 +05:30
Pranav Raj SandGitHub 44fae4a4a1 Feature: Add default value to ChatForm (#816)
* Feature: Add adefault value to ChatForm
2020-05-04 23:48:47 +05:30
5bc8219db5 Feature: Typing Indicator on widget and dashboard (#811)
* Adds typing indicator for widget
* typing indicator for agents in dashboard

Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
2020-05-04 23:07:56 +05:30
Sony MathewandGitHub fabc3170b7 Feature: Feature flags on account (#814) (#815)
* Feature: Feature flags on account (#814)
* Added the new column on account model for managing feature flags
* Added the inbound email flag
* Locked the settings of this eature in account settings based on this
* Encapsulated the feature flaging as a concern. With this, we can re-use the same concern if we introduce this in other models like user or inbox or so.
* Added the features in the account api

* Changed Rails/FilePath style - rubocop issue

* Revert "Changed Rails/FilePath style - rubocop issue"

This reverts commit 3a42d3b9c9b3a2fde8bc7256fd704b6fcaf54040.

* Disabling rubocop on codeclimate as we already have this in CICD
2020-05-04 23:06:42 +05:30
Pranav Raj SandGitHub 98af47aae2 Fix missing translation key (#810) 2020-05-04 19:07:31 +05:30
Sojan JoseandGitHub ae3f8c02e1 Chore: Add pt and pt_BR translations (#813) 2020-05-04 17:33:55 +05:30
Pranav Raj SandGitHub 19886b40bd Chore: Add enter key listener for email input box (#809)
* Chore: Add enter key listener for email input box
2020-05-03 14:45:33 +05:30
Sojan JoseandGitHub dc6398ab56 Feature: Api to toggle typing status on a conversation (#807)
- api to toggle typing status on a conversation
- clients receive webhook events on the same

Addresses: #718 , #719 , #775
2020-05-03 12:17:27 +05:30
Sojan JoseandGitHub ba1e0dbda0 Chore: Apple site association file for deep linking (#805) 2020-05-03 12:16:11 +05:30
Sojan JoseandGitHub db6e3fb43e Chore: API to get conversations count (#808)
Addresses: #497
2020-05-03 12:15:10 +05:30
Sony MathewandGitHub d8131e450c Chore: Moved the email ingress method to an env variable (#777) (#806)
* Chore: Moved the email ingress method to an env variable (#777)
* Moved the email ingress method in config to an environment variable
* Updated documentation to reflect this

* Added ingress config to staging env config (#777)
* Re-arranged the order in env configs so that email related configs are together
2020-05-02 23:29:09 +05:30
Pranav Raj SandGitHub 8655466fa5 Chore: Add docs for Twilio Whatsapp/SMS channel (#804) 2020-05-02 16:25:57 +05:30
Subin T PandGitHub 7e62000e1b Chore: Remove redis backed reporting (#669) 2020-05-02 16:04:52 +05:30
Pranav Raj SandGitHub ddbc612f0b Bug: Use beginAtZero option for reports (#803) 2020-05-02 15:03:43 +05:30
a951fb20cb Chore: UI fixes and enhancements (#796)
Co-authored-by: Pranav Raj S <pranavrajs@gmail.com>
2020-05-02 09:47:36 +05:30
Pranav Raj SandGitHub 8906fd808d Bug: Fix missing contact_id in conversation list (#798) 2020-05-01 23:00:33 +05:30
Pranav Raj SandGitHub 8f0c4281c7 Chore: Fix data migration in notification settings (#797) 2020-05-01 22:30:57 +05:30
Sojan JoseandGitHub d4ff9f5b45 Chore: Add additional information to conversation meta (#795)
Chore: Add additional information to conversation meta
2020-05-01 16:49:28 +05:30
96da27f1f6 Feature: User Notification Objects (#752)
Co-authored-by: Pranav Raj S <pranavrajs@gmail.com>
2020-05-01 14:53:43 +05:30
Sojan JoseandGitHub 66aace7c13 Chore: Add Romanian, Portuguese, Brazilian, Ukrainian languages (#790)
* New translations integrations.json (Romanian)

* New translations integrations.json (Portuguese, Brazilian)

* New translations integrations.json (Ukrainian)

* New translations integrations.json (Portuguese)

* New translations inboxMgmt.json (French)

* New translations report.json (Portuguese, Brazilian)

* New translations resetPassword.json (Portuguese, Brazilian)

* New translations login.json (Portuguese)

* New translations report.json (Ukrainian)

* New translations report.json (Portuguese)

* New translations cannedMgmt.json (Portuguese)

* New translations billing.json (Portuguese)

* New translations agentMgmt.json (Spanish)

* New translations agentMgmt.json (Portuguese, Brazilian)

* New translations generalSettings.json (French)

* New translations conversation.json (Portuguese, Brazilian)

* New translations conversation.json (Portuguese)

* New translations chatlist.json (Portuguese)

* New translations contact.json (Portuguese)

* New translations chatlist.json (Portuguese, Brazilian)

* New translations en.yml (Portuguese, Brazilian)

* New translations devise.en.yml (Portuguese, Brazilian)

* New translations en.yml (French)

* New translations en.yml (Spanish)

* New translations settings.json (Romanian)

* New translations setNewPassword.json (Portuguese)

* New translations setNewPassword.json (Ukrainian)

* New translations signup.json (Portuguese, Brazilian)

* New translations settings.json (Ukrainian)

* New translations settings.json (Portuguese, Brazilian)
2020-05-01 12:03:46 +05:30
29ae232e54 Feature: Add spanish (Spain) language (#671)
Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
2020-05-01 11:45:46 +05:30
Nithin David ThomasandGitHub 1ebe9fdba9 Fixes broken seeds issue (#788) 2020-05-01 01:22:13 +05:30
0690d3191d Chore: Review docker issues (#787)
* Fix docker issues

Addresses: #701

* Add action cable configurations

Co-authored-by: Sony Mathew <ynos1234@gmail.com>
2020-04-30 21:32:34 +05:30
Sony MathewandGitHub 0b65526b85 Feature: Conversation Continuity with Email (#770)
* Added POC for mail inbox reply email
* created mailbox and migratuion for the same
* cleaned up sidekiq queues and added the queues for action mailbox and active storage
* created conversation mailbox and functionlaity to create a message on the conversation when it's replied

* Added UUID to conversation to be used in email replies

* added migration to add uuid for conversation
* changed parsing and resource fetching to reflect matching uuid and
  loading conversation alone
* cleaned up conversation mailbox.rb

* Added content type & attribute for message

* Added the new reply email to outgoing emails
* Added migration to accounts for adding domain and settings
* Modified seeds to reflect this changes
* Added the flag based column on account for boolean settings
* Added the new reply to email in outgoing conversation emails based on conditions

* Added dynamic email routing in application mailbox
* Added dynamic email routing in application mailbox
* Added a catch all deafult empty mailbox
* Added annotation for account

* Added the complete email details & attachments to the message
* Added the complete email details to the message in content_attributes, like subject, to, cc, bcc etc
* Modified the mail extractor to give a serilaized version of email
* Handled storing attachments of email on the message

* Added incoming email settings, env variables

* [#138] Added documentation regarding different email settings and variables

* Fixed the mail attachments blob issue (#138)
* Decoided attachments were strings and had to construct blobs out fo them to work with active storage
* Fixed the content encoding issue with mail body
* Fixed issue with Proc used in apllication mailbox routing
* Fixed couple of typos and silly mistakes

* Set appropriate from email for conversation reply mails (#138)
* From email was taken from a env variable, changed it to take from account settings if enabled
* Set the reply to email correctly based on conversation UUID
* Added commented config ind development.rb for mailbox ingress

* Added account settings for domain and support email (#138)
* Added the new attributes in accounts controller params whitelisting, api responses
* Added options for the the new fields in account settings

* Fixed typos in email continuity docs and warnings

* Added specs for conversation reply mailer changes (#138)
* Added specs for
  * conversation reply mailer
  * Accounts controller
  * Account and Conversation models

* Added tests for email presenter (#138)

* Specs for inbound email routing and mailboxes (#138)
2020-04-30 20:20:26 +05:30
0cb7333977 Feature: Twilio Whatsapp Integration (#779)
Twilio Whatsapp Integration

Co-authored-by: Sojan <sojan@pepalo.com>
2020-04-30 01:41:13 +05:30
Nithin David ThomasandGitHub 168042f9a4 Feature: Support an Emoji selector in the chat widget (#773)
* Adds emoji widget to web widget
* Style fixes for the send button
* Adds cursor to emoji widget action buttons
2020-04-29 13:54:56 +05:30
Pranav Raj SandGitHub fde4f9271b Bug: Redirect user to set new password screen (#772) 2020-04-27 15:15:05 +05:30
6770126de0 Feature: Customise the position of messenger (#767)
Co-authored-by: Nithin David Thomas <webofnithin@gmail.com>
2020-04-25 23:18:00 +05:30
Andrei PopoviciandGitHub 94192e8184 Remove deprecated scope from FB Channel (#761)
Remove deprecated scope from FB Channel
2020-04-25 22:43:54 +05:30
Sojan JoseandGitHub 3fcd367952 Chore: New Crowdin translations (#747)
Brings in new translation files from Crowdin.
2020-04-23 23:52:16 +05:30
Pranav Raj SandGitHub 7d41b7a5dc Bug: Add account scoping in CTA (#754) 2020-04-23 12:21:45 +05:30
Pranav Raj SandGitHub 645d53db1c Bug: Fix webwidget.triggered event (#750) 2020-04-22 13:14:09 +05:30
Subin T PandGitHub dba6866b6d Bug: Force scoping message metrics to account_id (#748) 2020-04-22 12:49:46 +05:30
Pranav Raj SandGitHub b98a64d2dd Chore: Add S3 bucket policy (#746) 2020-04-20 15:40:45 +05:30
Pranav Raj SandGitHub 06153a96ed Chore: Update swagger API documentation (#745)
* Chore: Update swagger documentation
2020-04-20 11:55:06 +05:30
Vishnu NarayananandGitHub 5736f687e8 Chore: Bump ruby version to 2.7.0 in docs (#744) 2020-04-19 23:41:16 +05:30
941fbb0d72 Chore: Web widget Inbox Tech Debts (#738)
* Chore: Webwidget Inbox Tech Debts

* Additional customization options creating Web Widget
* Changes to edit Page for Web Widget
* Remove the WebWidget API end points
* Minor chores

Address: #680, #502

Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
2020-04-19 23:40:28 +05:30
c0ce70e87b Chore: Events Cleanup (#739)
* Chore: Events Cleanup

- fix failing tests
- add additional webhook events
- clean up event logic

* Fix conversation status update in action cable

Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
2020-04-18 20:25:58 +05:30
Sojan JoseandGitHub ecccb103a0 Chore: Add a real-time event for contact resolution/update (#696)
* Chore: Add a real-time event for contact resolution/update
* Chore: Ensure Events are sent to administrators

Addresses: #419
2020-04-18 13:47:51 +05:30
818c769bb7 Chore: Message to support multiple attachments (#730)
* Changes for the message to have multiple attachments
* changed the message association to attachments from has_one to has_many
* changed all the references of this association in building and fetching to reflect this change

* Added number of attachments validation to the message model

* Modified the backend responses and endpoints to reflect multiple attachment support (#737)

* Changing the frontend components for multiple attachments
* changed the request structure to reflect the multiple attachment structures
* changed the message bubbles to support multiple attachments
* bugfix: agent side attachment was not showing because of a missing await
* broken message was shown because of the store filtering
* Added documentation for ImageMagick

* spec fixes

* refactored code to reflect more apt namings

* Added updated message listener for the dashboard (#727)
* Added the publishing for message updated event
* Implemented the listener for dashboard

Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
2020-04-17 21:15:20 +05:30
Pranav Raj SandGitHub 0817414957 Chore: Upgrade babel packages (#736)
Chore: Upgrade babel
2020-04-17 18:49:14 +05:30
Pranav Raj SandGitHub e3ccfc4662 Bug: Fix IE issues in Widget (#734) 2020-04-17 16:45:12 +05:30
Sony MathewandGitHub 2aa92d46fc Fixed confirmation email flaky tests (#534) (#735) 2020-04-17 15:48:23 +05:30
c2167fcaaf Feature: Add catalan (Catalonia) language (#672)
Co-authored-by: Pranav Raj S <pranavrajs@gmail.com>
2020-04-16 15:28:23 +05:30
Sojan JoseandGitHub 743bddc065 Chore: Housekeeping tasks (#721)
- End point to return api version
- Move agent bot listener to sync dispatcher
- gem update
2020-04-16 15:05:01 +05:30
55892e37f9 Feature: Add Malayalam(മലയാളം) localization (#707) (#708)
* Add Malayalam localization

* Added Malayalam translations for dashboard locales (UI)

 Translations for
* webhooks, signup, settings, set a new password, reset password, report, login, integrations

* Added Malayalam translations for dashboard translations for UI

* Added dashboard Malayalam translations for contact, conversation, general settings and inbox management.

* [707] Add Malayalam translations for dashboard

* [707] Add support for Malayalam

* [707] Fix review comments

* [707] fix review commetns

Co-authored-by: sony-mathew <ynos1234@gmail.com>
2020-04-13 21:14:17 +05:30
Sojan Jose 9db8adab5f Update Crowdin configuration file 2020-04-13 19:01:00 +05:30
Sojan Jose 991585264e Update Crowdin configuration file 2020-04-13 16:47:31 +05:30
b0950d6880 Feature: Rich Message Types (#610)
Co-authored-by: Pranav Raj S <pranavrajs@gmail.com>
Co-authored-by: Nithin David Thomas <webofnithin@gmail.com>
2020-04-10 16:42:37 +05:30
Pranav Raj SandGitHub 48f603798b Chore: Fix URLs in README 2020-04-08 22:46:58 +05:30
1cfa756d49 Chore: APIs for agent bots (#676)
Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
2020-04-07 10:41:18 +05:30
4feca1d88d Feature: Business logo API for web widget (#674)
Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
2020-04-07 10:19:19 +05:30
Sojan e04b37dfcf Merge branch 'release/1.3.0' 2020-04-06 22:58:20 +05:30
99eaf59509 Feature: Ability to set an account name (#667)
* Ability to change the account name 
* Ability to set a language to the account

Addresses: #667  #307  

Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
2020-04-06 22:17:07 +05:30
Pranav Raj SandGitHub a1a81e3799 Feature: Twilio SMS Channel (#658)
Twilio SMS Channel
Fixes :  #350
2020-04-05 22:11:27 +05:30
Sojan JoseandGitHub 8e59564793 Chore: Update ruby version (#665) 2020-04-05 12:11:50 +05:30
cb22b396eb Feature: Website SDK (#653)
Add SDK functions

Co-authored-by: Sojan <sojan@pepalo.com>
2020-04-03 13:04:58 +05:30
7fcd2d0e85 Feature: Support file type messages on widget and dashboard (#659)
- Adds support for file upload

Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
Co-authored-by: Sojan <sojan@pepalo.com>
2020-04-02 12:28:38 +05:30
Sony MathewandGitHub 0afa5c297f Bug: Fixed conversation reply mail issue when an attachment is sent (#655)
* When an attachment was sent in the reply mailer it was breaking due to the
truncating function applied on the last message content. Here the content was empty
and so it broke.
* Fixed this breakage
* Also addressed the issue that if the attachment was there the content was coming empty.
With these changes, made the content to have a link to the attachment.
2020-03-30 16:07:01 +05:30
Nithin David ThomasandGitHub 6c4e1fdaac Feature: Send images from widget 2020-03-30 12:15:06 +05:30
Subin T PandGitHub e56132c506 Non blocking event dispatch (#652)
- Performance improvements for event dispatch
2020-03-29 19:18:30 +05:30
Sojan JoseandGitHub bab9d663d2 Feature: API for updating account settings (#645)
* Feature: API for updating account settings

- API to update account locale
- API to update account name
- API to show account info
2020-03-29 12:16:31 +05:30
Pranav Raj SandGitHub 6c48f2e789 Bug: Update conversation urls in email (#649)
Update the conversation URLs in email templates.
2020-03-29 09:34:57 +05:30
50ebbc13b6 Chore: Add Twitter documentation (#648)
* Chore: Add Twitter documentation

Co-authored-by: Sojan <sojan@pepalo.com>
2020-03-28 11:51:42 +05:30
a3c2d4e5bd Chore: Add Facebook app set up documentation (#647)
Co-authored-by: Pranav Raj S <pranavrajs@gmail.com>
2020-03-28 11:43:02 +05:30
Sony MathewandGitHub 209e0a0fb4 [Feature] Change conversation ordering (#640)
* Update conversation's updated_at when a message is created/saved
* changed teh ordering of conversation to be based on `updated_at` rather than `created_at`
2020-03-24 23:34:22 +05:30
Pranav Raj Sreepuram 2eac97c705 Merge branch 'hotfix/1.2.4' into develop 2020-03-22 20:43:58 +05:30
Pranav Raj Sreepuram bf80a37cfb Merge branch 'hotfix/1.2.4' 2020-03-22 20:41:51 +05:30
Pranav Raj SandGitHub a94f14ca22 Bug: Disable logging to file on asset precompile (#637) 2020-03-22 20:38:49 +05:30
Nithin David ThomasandGitHub f7e5f1fabf Feature: Send attachments to widget user (#621) 2020-03-22 15:54:36 +05:30
Nithin David ThomasandGitHub fe70843fae Feature: Improve Dashboard UI (#630) 2020-03-22 11:44:40 +05:30
Pranav Raj SandGitHub 29bb2bff3a Security: Upgrade eslint, eslint-config-airbnb (#635) 2020-03-21 21:01:55 +05:30
Pranav Raj SandGitHub 4c40d738a5 Security: Upgrade rails to 6.0.2.2 (#636) 2020-03-21 20:54:04 +05:30
Pranav Raj SandGitHub 1c4566df66 Bug: Migrate messages while merging contact (#624)
Bug: Migrate messages while merging contact
2020-03-19 01:52:38 +05:30
Subin T PandGitHub 8f6f07177d Enhancement: Move reporting metrics to postgres (#606) 2020-03-18 16:53:35 +05:30
Sojan f69eb7e542 Merge branch 'hotfix/1.2.3' into develop 2020-03-18 01:53:24 +05:30
Sojan 9d170fc727 Merge branch 'hotfix/1.2.3' 2020-03-18 01:53:13 +05:30
Sojan 8d65e7db95 HotFix: Tune the chatwoot logs
Fixes : #616
2020-03-18 01:40:34 +05:30
Arief LuthfiandGitHub 71d48854b1 Chore: Refactor Facebook send reply service
Refactor Facebook send reply service
2020-03-17 21:09:20 +05:30
Pranav Raj SandGitHub 4d0f46176b Chore: Add web_widget_script in the Inboxes API (#615)
* Chore: Add web_widget_script in the Inboxes API
2020-03-16 12:32:34 +05:30
Nithin David ThomasandGitHub d8599c62dd Feature: Support options message type on widget and dashboard (#614) 2020-03-16 12:08:40 +05:30
ghosteathumanandGitHub bb47b5f086 Reduce long peer dependencies warning (#568)
Co-authored-by: ghosteathuman <46582963+ghosteathuman@users.noreply.github.com>
2020-03-12 22:00:58 +05:30
Nithin David ThomasandGitHub fb17d34b8a Feature: Support card options in widget and dashboard #594 (#609) 2020-03-12 21:23:52 +05:30
Pranav Raj SandGitHub 260e40831a Chore: Update Swagger API docs (#608) 2020-03-11 16:19:13 +05:30
a5b1e2b650 Feature: Access tokens for API access (#604)
Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
2020-03-11 00:02:15 +05:30
19ab0fe108 Chore: Scope URLs with account_id (#601)
* Chore: Enable Users to create multiple accounts

Addresses: #402
- migrations to split roles and other attributes from users table
- make changes in code to accommodate this change

Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
2020-03-09 23:27:10 +05:30
Pranav Raj Sreepuram 2a6670f0da Merge branch 'hotfix/1.2.2' into develop 2020-03-09 10:01:15 +05:30
Pranav Raj Sreepuram 91bccd3c6d Merge branch 'hotfix/1.2.2' 2020-03-09 10:00:42 +05:30
Pranav Raj Sreepuram ef6c695bcb Chore: Add additional_attributes to conversation seed 2020-03-09 10:00:11 +05:30
Pranav Raj Sreepuram 1db35caa56 Merge branch 'release/1.2.1' into develop 2020-03-08 22:15:58 +05:30
60dc564f37 Bug: Fix missing close button on mobile chat window (#600)
Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
2020-03-07 23:39:41 +05:30
8b4df986bf Chore: Enable Users to create multiple accounts (#440)
Addresses: #402
- migrations to split roles and other attributes from users table
- make changes in code to accommodate this change

Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
2020-03-07 12:18:16 +05:30
b2d5cc7b05 Feature: Introduce bots (#545)
Co-authored-by: Pranav Raj S <pranavrajs@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2020-03-06 01:43:12 +05:30
848 changed files with 27169 additions and 5014 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ defaults: &defaults
working_directory: ~/build
docker:
# specify the version you desire here
- image: circleci/ruby:2.6.5-node-browsers
- image: circleci/ruby:2.7.0-node-browsers
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
+5 -1
View File
@@ -1,7 +1,7 @@
version: "2"
plugins:
rubocop:
enabled: true
enabled: false
channel: rubocop-0-73
eslint:
enabled: false
@@ -26,3 +26,7 @@ exclude_patterns:
- "node_modules/**/*"
- "lib/tasks/auto_annotate_models.rake"
- "app/test-matchers.js"
- "docs/*"
- "**/*.md"
- "**/*.yml"
- "app/javascript/dashboard/i18n/locale"
+56 -20
View File
@@ -1,14 +1,19 @@
SECRET_KEY_BASE=
# Used to verify the integrity of signed cookies. so ensure a secure value is set
SECRET_KEY_BASE=replace_with_lengthy_secure_hex
# Replace with the URL you are planning to use for your app
FRONTEND_URL=http://0.0.0.0:3000
# Force all access to the app over SSL, default is set to false
FORCE_SSL=
FORCE_SSL=false
# This lets you control new sign ups on your chatwoot installation
# true : default option, allows sign ups
# false : disables all the end points related to sign ups
# api_only: disables the UI for signup, but you can create sign ups via the account apis
ENABLE_ACCOUNT_SIGNUP=
ENABLE_ACCOUNT_SIGNUP=true
#redis config
# Redis config
REDIS_URL=redis://redis:6379
# If you are using docker-compose, set this variable's value to be any string,
# which will be the password for the redis service running inside the docker-compose
@@ -22,18 +27,7 @@ POSTGRES_PASSWORD=
RAILS_ENV=development
RAILS_MAX_THREADS=5
#fb app
FB_VERIFY_TOKEN=
FB_APP_SECRET=
FB_APP_ID=
#twitter app
TWITTER_APP_ID=
TWITTER_CONSUMER_KEY=
TWITTER_CONSUMER_SECRET=
TWITTER_ENVIRONMENT=
#mail
# Mail outgoing
MAILER_SENDER_EMAIL=accounts@chatwoot.com
SMTP_PORT=1025
SMTP_DOMAIN=chatwoot.com
@@ -45,23 +39,60 @@ SMTP_PASSWORD=
SMTP_AUTHENTICATION=
SMTP_ENABLE_STARTTLS_AUTO=
#misc
FRONTEND_URL=http://0.0.0.0:3000
# Mail Incoming
# Set this to appropriate ingress channel with regards to incoming emails
# Possible values are :
# :relay for Exim, Postfix, Qmail
# :mailgun for Mailgun
# :mandrill for Mandrill
# :postmark for Postmark
# :sendgrid for Sendgrid
RAILS_INBOUND_EMAIL_SERVICE=
# Use one of the following based on the email ingress service
# Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html
RAILS_INBOUND_EMAIL_PASSWORD=
MAILGUN_INGRESS_SIGNING_KEY=
MANDRILL_INGRESS_API_KEY=
# Storage
ACTIVE_STORAGE_SERVICE=local
#s3
# Amazon S3
S3_BUCKET_NAME=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=
#sentry
# Sentry
SENTRY_DSN=
# Log settings
# Disable if you want to write logs to a file
RAILS_LOG_TO_STDOUT=true
LOG_LEVEL=info
LOG_SIZE=500
# Credentials to access sidekiq dashboard in production
SIDEKIQ_AUTH_USERNAME=
SIDEKIQ_AUTH_PASSWORD=
### This environment variables are only required if you are setting up social media channels
#facebook
FB_VERIFY_TOKEN=
FB_APP_SECRET=
FB_APP_ID=
# Twitter
TWITTER_APP_ID=
TWITTER_CONSUMER_KEY=
TWITTER_CONSUMER_SECRET=
TWITTER_ENVIRONMENT=
### Change this env variable only if you are using a custom build mobile app
## Mobile app env variables
IOS_APP_ID=6C953F3RX2.com.chatwoot.app
#### This environment variables are only required in hosted version which has billing
ENABLE_BILLING=
@@ -70,3 +101,8 @@ CHARGEBEE_API_KEY=
CHARGEBEE_SITE=
CHARGEBEE_WEBHOOK_USERNAME=
CHARGEBEE_WEBHOOK_PASSWORD=
## Push Notification
## generate a new key value here : https://d3v.one/vapid-key-generator/
# VAPID_PUBLIC_KEY=
# VAPID_PRIVATE_KEY=
+3 -3
View File
@@ -1,8 +1,8 @@
module.exports = {
extends: ['airbnb/base', 'prettier', 'plugin:vue/recommended'],
extends: ['airbnb-base/legacy', 'prettier', 'plugin:vue/recommended'],
parserOptions: {
parser: 'babel-eslint',
ecmaVersion: 2017,
ecmaVersion: 2020,
sourceType: 'module',
},
plugins: ['html', 'prettier', 'babel'],
@@ -28,7 +28,7 @@ module.exports = {
}],
'vue/html-self-closing': 'off',
"vue/no-v-html": 'off',
'import/extensions': ['never']
'import/extensions': ['off']
},
settings: {
+1
View File
@@ -0,0 +1 @@
12.16.1
+64 -10
View File
@@ -4,18 +4,28 @@ require:
- rubocop-rspec
inherit_from: .rubocop_todo.yml
Lint/RaiseException:
Enabled: true
Lint/StructNewOverride:
Enabled: true
Layout/LineLength:
Max: 150
Metrics/ClassLength:
Max: 125
RSpec/ExampleLength:
Max: 15
Max: 25
Style/Documentation:
Enabled: false
Style/FrozenStringLiteralComment:
Enabled: false
Style/SymbolArray:
Enabled: false
Style/HashEachMethods:
Enabled: true
Style/HashTransformKeys:
Enabled: true
Style/HashTransformValues:
Enabled: true
Style/GlobalVars:
Exclude:
- 'config/initializers/redis.rb'
@@ -41,14 +51,58 @@ RSpec/NestedGroups:
Max: 4
RSpec/MessageSpies:
Enabled: false
Metrics/MethodLength:
Exclude:
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
Rails/CreateTableWithTimestamps:
Exclude:
- 'db/migrate/20170207092002_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb'
Style/GuardClause:
Exclude:
- 'app/builders/account_builder.rb'
- 'app/models/attachment.rb'
- 'app/models/message.rb'
- 'lib/webhooks/chargebee.rb'
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
Metrics/AbcSize:
Exclude:
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
Metrics/CyclomaticComplexity:
Exclude:
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
Rails/ReversibleMigration:
Exclude:
- 'db/migrate/20161025070152_removechannelsfrommodels.rb'
- 'db/migrate/20161025070645_remchannel.rb'
- 'db/migrate/20161025070645_remchannel.rb'
- 'db/migrate/20161110102609_removeinboxid.rb'
- 'db/migrate/20170519091539_add_avatar_to_fb.rb'
- 'db/migrate/20191020085608_rename_old_tables.rb'
- 'db/migrate/20191126185833_update_user_invite_foreign_key.rb'
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
Rails/BulkChangeTable:
Exclude:
- 'db/migrate/20161025070152_removechannelsfrommodels.rb'
- 'db/migrate/20200121190901_create_account_users.rb'
- 'db/migrate/20170211092540_notnullableusers.rb'
- 'db/migrate/20170403095203_contactadder.rb'
- 'db/migrate/20170406104018_add_default_status_conv.rb'
- 'db/migrate/20170511134418_latlong.rb'
- 'db/migrate/20191027054756_create_contact_inboxes.rb'
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
Rails/UniqueValidationWithoutIndex:
Exclude:
- 'app/models/channel/twitter_profile.rb'
- 'app/models/webhook.rb'
AllCops:
Exclude:
- db/*
- bin/**/*
- db/**/*
- config/**/*
- public/**/*
- vendor/**/*
- node_modules/**/*
- lib/tasks/auto_annotate_models.rake
- config/environments/**/*
- 'bin/**/*'
- 'db/schema.rb'
- 'config/**/*'
- 'public/**/*'
- 'vendor/**/*'
- 'node_modules/**/*'
- 'lib/tasks/auto_annotate_models.rake'
- 'config/environments/**/*'
- 'tmp/**/*'
- 'storage/**/*'
-9
View File
@@ -282,15 +282,6 @@ Style/GlobalVars:
Exclude:
- 'lib/redis/alfred.rb'
# Offense count: 7
# Configuration parameters: MinBodyLength.
Style/GuardClause:
Exclude:
- 'app/builders/account_builder.rb'
- 'app/models/attachment.rb'
- 'app/models/message.rb'
- 'lib/webhooks/chargebee.rb'
# Offense count: 4
Style/IdenticalConditionalBranches:
Exclude:
+1 -1
View File
@@ -1 +1 @@
2.6.5
2.7.0
+2 -2
View File
@@ -82,7 +82,7 @@ linters:
enabled: true
ImportantRule:
enabled: true
enabled: false
ImportPath:
enabled: true
@@ -252,7 +252,7 @@ linters:
enabled: false
UnnecessaryParentReference:
enabled: true
enabled: false
UrlFormat:
enabled: true
+9 -7
View File
@@ -1,6 +1,6 @@
source 'https://rubygems.org'
ruby '2.6.5'
ruby '2.7.0'
##-- base gems for rails --##
gem 'rack-cors', require: 'rack/cors'
@@ -17,6 +17,7 @@ gem 'jbuilder'
gem 'kaminari'
gem 'responders'
gem 'rest-client'
gem 'telephone_number'
gem 'time_diff'
gem 'tzinfo-data'
gem 'valid_email2'
@@ -25,11 +26,12 @@ gem 'uglifier'
##-- for active storage --##
gem 'aws-sdk-s3', require: false
gem 'azure-storage', require: false
gem 'azure-storage-blob', require: false
gem 'google-cloud-storage', require: false
gem 'mini_magick'
##-- gems for database --#
gem 'groupdate'
gem 'pg'
gem 'redis'
gem 'redis-namespace'
@@ -52,18 +54,15 @@ gem 'pundit'
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
gem 'wisper', '2.0.0'
##--- gems for reporting ---##
gem 'nightfury'
##--- gems for billing ---##
gem 'chargebee'
##--- gems for channels ---##
gem 'facebook-messenger'
gem 'telegram-bot-ruby'
gem 'twilio-ruby', '~> 5.32.0'
# twitty will handle subscription of twitter account events
gem 'twitty', git: 'https://github.com/chatwoot/twitty'
# facebook client
gem 'koala'
# Random name generator
@@ -81,6 +80,9 @@ gem 'sidekiq'
##-- used for single column multiple binary flags in notification settings/feature flagging --##
gem 'flag_shih_tzu'
##-- Push notification service --##
gem 'webpush'
group :development do
gem 'annotate'
gem 'bullet'
@@ -99,7 +101,7 @@ group :development, :test do
gem 'factory_bot_rails'
gem 'faker'
gem 'listen'
gem 'mock_redis'
gem 'mock_redis', git: 'https://github.com/sds/mock_redis', ref: '16d00789f0341a3aac35126c0ffe97a596753ff9'
gem 'pry-rails'
gem 'rspec-rails', '~> 4.0.0.beta2'
gem 'rubocop', require: false
+159 -137
View File
@@ -1,10 +1,17 @@
GIT
remote: https://github.com/chatwoot/twitty
revision: c1edd557401d1e8a197b19e738f82e39507a8e2d
revision: af4f3e45dca55e42c64f7741a1fedfaa94d36419
specs:
twitty (0.1.0)
oauth
GIT
remote: https://github.com/sds/mock_redis
revision: 16d00789f0341a3aac35126c0ffe97a596753ff9
ref: 16d00789f0341a3aac35126c0ffe97a596753ff9
specs:
mock_redis (0.22.0)
GIT
remote: https://github.com/tzmfreedom/json_refs
revision: e32deb073ce9aef39bdd63556bffd7fe7c2a803d
@@ -16,58 +23,58 @@ GIT
GEM
remote: https://rubygems.org/
specs:
action-cable-testing (0.6.0)
action-cable-testing (0.6.1)
actioncable (>= 5.0)
actioncable (6.0.2.1)
actionpack (= 6.0.2.1)
actioncable (6.0.2.2)
actionpack (= 6.0.2.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.0.2.1)
actionpack (= 6.0.2.1)
activejob (= 6.0.2.1)
activerecord (= 6.0.2.1)
activestorage (= 6.0.2.1)
activesupport (= 6.0.2.1)
actionmailbox (6.0.2.2)
actionpack (= 6.0.2.2)
activejob (= 6.0.2.2)
activerecord (= 6.0.2.2)
activestorage (= 6.0.2.2)
activesupport (= 6.0.2.2)
mail (>= 2.7.1)
actionmailer (6.0.2.1)
actionpack (= 6.0.2.1)
actionview (= 6.0.2.1)
activejob (= 6.0.2.1)
actionmailer (6.0.2.2)
actionpack (= 6.0.2.2)
actionview (= 6.0.2.2)
activejob (= 6.0.2.2)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.0.2.1)
actionview (= 6.0.2.1)
activesupport (= 6.0.2.1)
actionpack (6.0.2.2)
actionview (= 6.0.2.2)
activesupport (= 6.0.2.2)
rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.0.2.1)
actionpack (= 6.0.2.1)
activerecord (= 6.0.2.1)
activestorage (= 6.0.2.1)
activesupport (= 6.0.2.1)
actiontext (6.0.2.2)
actionpack (= 6.0.2.2)
activerecord (= 6.0.2.2)
activestorage (= 6.0.2.2)
activesupport (= 6.0.2.2)
nokogiri (>= 1.8.5)
actionview (6.0.2.1)
activesupport (= 6.0.2.1)
actionview (6.0.2.2)
activesupport (= 6.0.2.2)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (6.0.2.1)
activesupport (= 6.0.2.1)
activejob (6.0.2.2)
activesupport (= 6.0.2.2)
globalid (>= 0.3.6)
activemodel (6.0.2.1)
activesupport (= 6.0.2.1)
activerecord (6.0.2.1)
activemodel (= 6.0.2.1)
activesupport (= 6.0.2.1)
activestorage (6.0.2.1)
actionpack (= 6.0.2.1)
activejob (= 6.0.2.1)
activerecord (= 6.0.2.1)
activemodel (6.0.2.2)
activesupport (= 6.0.2.2)
activerecord (6.0.2.2)
activemodel (= 6.0.2.2)
activesupport (= 6.0.2.2)
activestorage (6.0.2.2)
actionpack (= 6.0.2.2)
activejob (= 6.0.2.2)
activerecord (= 6.0.2.2)
marcel (~> 0.3.1)
activesupport (6.0.2.1)
activesupport (6.0.2.2)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
@@ -77,46 +84,44 @@ GEM
activerecord (>= 5.0, < 6.1)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
annotate (3.0.3)
annotate (3.1.1)
activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 14.0)
ast (2.4.0)
attr_extras (6.2.3)
aws-eventstream (1.0.3)
aws-partitions (1.269.0)
aws-sdk-core (3.89.1)
aws-eventstream (~> 1.0, >= 1.0.2)
aws-eventstream (1.1.0)
aws-partitions (1.296.0)
aws-sdk-core (3.94.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.28.0)
aws-sdk-kms (1.30.0)
aws-sdk-core (~> 3, >= 3.71.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.60.1)
aws-sdk-s3 (1.61.2)
aws-sdk-core (~> 3, >= 3.83.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.1.0)
aws-sigv4 (1.1.1)
aws-eventstream (~> 1.0, >= 1.0.2)
axiom-types (0.1.1)
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
azure-core (0.1.15)
faraday (~> 0.9)
faraday_middleware (~> 0.10)
nokogiri (~> 1.6)
azure-storage (0.15.0.preview)
azure-core (~> 0.1)
faraday (~> 0.9)
faraday_middleware (~> 0.10)
nokogiri (~> 1.6, >= 1.6.8)
azure-storage-blob (2.0.0)
azure-storage-common (~> 2.0)
nokogiri (~> 1.10.4)
azure-storage-common (2.0.1)
faraday (~> 1.0)
faraday_middleware (~> 1.0.0.rc1)
nokogiri (~> 1.10.4)
bcrypt (3.1.13)
bindex (0.8.1)
bootsnap (1.4.5)
bootsnap (1.4.6)
msgpack (~> 1.0)
brakeman (4.7.2)
browser (3.0.3)
brakeman (4.8.1)
browser (4.0.0)
builder (3.2.4)
bullet (6.1.0)
activesupport (>= 3.0.0)
@@ -127,13 +132,13 @@ GEM
bundler (>= 1.2.0, < 3)
thor (~> 0.18)
byebug (11.1.1)
chargebee (2.7.3)
chargebee (2.7.5)
json_pure (~> 2.1)
rest-client (>= 1.8, < 3.0)
coderay (1.1.2)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
concurrent-ruby (1.1.5)
concurrent-ruby (1.1.6)
connection_pool (2.2.2)
crass (1.0.6)
declarative (0.0.10)
@@ -151,7 +156,7 @@ GEM
devise (> 3.5.2, < 5)
rails (>= 4.2.0, < 6.1)
diff-lcs (1.3)
digest-crc (0.4.1)
digest-crc (0.5.1)
docile (1.3.2)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
@@ -165,23 +170,23 @@ GEM
facebook-messenger (1.4.1)
httparty (~> 0.13, >= 0.13.7)
rack (>= 1.4.5)
factory_bot (5.1.1)
factory_bot (5.1.2)
activesupport (>= 4.2.0)
factory_bot_rails (5.1.1)
factory_bot (~> 5.1.0)
railties (>= 4.2.0)
faker (2.10.1)
faker (2.11.0)
i18n (>= 1.6, < 2)
faraday (0.17.3)
faraday (1.0.1)
multipart-post (>= 1.2, < 3)
faraday_middleware (0.14.0)
faraday (>= 0.7.4, < 1.0)
faraday_middleware (1.0.0)
faraday (~> 1.0)
ffi (1.12.2)
flag_shih_tzu (0.3.23)
foreman (0.87.0)
foreman (0.87.1)
globalid (0.4.2)
activesupport (>= 4.2.0)
google-api-client (0.36.4)
google-api-client (0.38.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0)
@@ -192,30 +197,33 @@ GEM
google-cloud-core (1.5.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.3.0)
faraday (~> 0.11)
google-cloud-env (1.3.1)
faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.0.0)
google-cloud-storage (1.25.1)
google-cloud-storage (1.26.0)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-api-client (~> 0.33)
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
mini_mime (~> 1.0)
googleauth (0.10.0)
faraday (~> 0.12)
googleauth (0.12.0)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.12)
signet (~> 0.14)
groupdate (5.0.0)
activesupport (>= 5)
haikunator (1.1.0)
hana (1.3.5)
hashie (4.1.0)
hkdf (0.3.0)
http-accept (1.7.0)
http-cookie (1.0.3)
domain_name (~> 0.5)
httparty (0.17.3)
httparty (0.18.0)
mime-types (~> 3.0)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
@@ -224,11 +232,11 @@ GEM
ice_nine (0.11.2)
inflecto (0.0.2)
jaro_winkler (1.5.4)
jbuilder (2.9.1)
activesupport (>= 4.2.0)
jbuilder (2.10.0)
activesupport (>= 5.0.0)
jmespath (1.4.0)
json (2.3.0)
json_pure (2.2.0)
json_pure (2.3.0)
jwt (2.2.1)
kaminari (1.2.0)
activesupport (>= 4.1.0)
@@ -246,14 +254,14 @@ GEM
addressable
faraday
json (>= 1.8)
launchy (2.4.3)
addressable (~> 2.3)
launchy (2.5.0)
addressable (~> 2.7)
letter_opener (1.7.0)
launchy (~> 2.2)
listen (3.2.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.4.0)
loofah (2.5.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
@@ -261,7 +269,7 @@ GEM
marcel (0.3.3)
mimemagic (~> 0.3.2)
memoist (0.16.2)
method_source (0.9.2)
method_source (1.0.0)
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2019.1009)
@@ -270,35 +278,33 @@ GEM
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.14.0)
mock_redis (0.22.0)
msgpack (1.3.1)
msgpack (1.3.3)
multi_json (1.14.1)
multi_xml (0.6.0)
multipart-post (2.1.1)
netrc (0.11.0)
nightfury (1.0.1)
nio4r (2.5.2)
nokogiri (1.10.8)
nokogiri (1.10.9)
mini_portile2 (~> 2.4.0)
oauth (0.5.4)
orm_adapter (0.5.0)
os (1.0.1)
os (1.1.0)
parallel (1.19.1)
parser (2.7.0.2)
parser (2.7.1.1)
ast (~> 2.4.0)
pg (1.2.2)
pry (0.12.2)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
pg (1.2.3)
pry (0.13.1)
coderay (~> 1.1)
method_source (~> 1.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.3)
public_suffix (4.0.4)
puma (4.3.3)
nio4r (~> 2.0)
pundit (2.1.0)
activesupport (>= 3.0.0)
rack (2.1.2)
rack-cache (1.11.0)
rack (2.2.2)
rack-cache (1.11.1)
rack (>= 0.4)
rack-cors (1.1.1)
rack (>= 2.0.0)
@@ -308,29 +314,29 @@ GEM
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (6.0.2.1)
actioncable (= 6.0.2.1)
actionmailbox (= 6.0.2.1)
actionmailer (= 6.0.2.1)
actionpack (= 6.0.2.1)
actiontext (= 6.0.2.1)
actionview (= 6.0.2.1)
activejob (= 6.0.2.1)
activemodel (= 6.0.2.1)
activerecord (= 6.0.2.1)
activestorage (= 6.0.2.1)
activesupport (= 6.0.2.1)
rails (6.0.2.2)
actioncable (= 6.0.2.2)
actionmailbox (= 6.0.2.2)
actionmailer (= 6.0.2.2)
actionpack (= 6.0.2.2)
actiontext (= 6.0.2.2)
actionview (= 6.0.2.2)
activejob (= 6.0.2.2)
activemodel (= 6.0.2.2)
activerecord (= 6.0.2.2)
activestorage (= 6.0.2.2)
activesupport (= 6.0.2.2)
bundler (>= 1.3.0)
railties (= 6.0.2.1)
railties (= 6.0.2.2)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0)
loofah (~> 2.3)
railties (6.0.2.1)
actionpack (= 6.0.2.1)
activesupport (= 6.0.2.1)
railties (6.0.2.2)
actionpack (= 6.0.2.2)
activesupport (= 6.0.2.2)
method_source
rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0)
@@ -345,7 +351,7 @@ GEM
redis-rack-cache (2.2.1)
rack-cache (>= 1.10, < 2)
redis-store (>= 1.6, < 2)
redis-store (1.8.1)
redis-store (1.8.2)
redis (>= 4, < 5)
representable (3.0.4)
declarative (< 0.1.0)
@@ -360,15 +366,16 @@ GEM
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
retriable (3.1.2)
rexml (3.2.4)
rspec-core (3.9.1)
rspec-support (~> 3.9.1)
rspec-expectations (3.9.0)
rspec-expectations (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-mocks (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-rails (4.0.0.beta4)
rspec-rails (4.0.0)
actionpack (>= 4.2)
activesupport (>= 4.2)
railties (>= 4.2)
@@ -377,19 +384,21 @@ GEM
rspec-mocks (~> 3.9)
rspec-support (~> 3.9)
rspec-support (3.9.2)
rubocop (0.79.0)
rubocop (0.81.0)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
parser (>= 2.7.0.1)
rainbow (>= 2.2.2, < 4.0)
rexml
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.7)
unicode-display_width (>= 1.4.0, < 2.0)
rubocop-performance (1.5.2)
rubocop (>= 0.71.0)
rubocop-rails (2.4.2)
rubocop-rails (2.5.2)
activesupport
rack (>= 1.1)
rubocop (>= 0.72.0)
rubocop-rspec (1.37.1)
rubocop-rspec (1.38.1)
rubocop (>= 0.68.1)
ruby-progressbar (1.10.1)
sass (3.7.4)
@@ -397,25 +406,26 @@ GEM
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
scout_apm (2.6.6)
scout_apm (2.6.7)
parser
scss_lint (0.59.0)
sass (~> 3.5, >= 3.5.5)
seed_dump (3.3.1)
activerecord (>= 4)
activesupport (>= 4)
sentry-raven (2.13.0)
faraday (>= 0.7.6, < 1.0)
shoulda-matchers (4.2.0)
semantic_range (2.3.0)
sentry-raven (3.0.0)
faraday (>= 1.0)
shoulda-matchers (4.3.0)
activesupport (>= 4.2.0)
sidekiq (6.0.4)
sidekiq (6.0.6)
connection_pool (>= 2.2.2)
rack (>= 2.0.0)
rack (~> 2.0)
rack-protection (>= 2.0.0)
redis (>= 4.1.0)
signet (0.12.0)
signet (0.14.0)
addressable (~> 2.3)
faraday (~> 0.9)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simplecov (0.17.1)
@@ -438,12 +448,17 @@ GEM
faraday
inflecto
virtus
telephone_number (1.4.6)
thor (0.20.3)
thread_safe (0.3.6)
time_diff (0.3.0)
activesupport
i18n
tzinfo (1.2.6)
twilio-ruby (5.32.0)
faraday (~> 1.0.0)
jwt (>= 1.5, <= 2.5)
nokogiri (>= 1.6, < 2.0)
tzinfo (1.2.7)
thread_safe (~> 0.1)
tzinfo-data (1.2019.3)
tzinfo (>= 1.0.0)
@@ -452,10 +467,10 @@ GEM
execjs (>= 0.3.0, < 3)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.6)
unicode-display_width (1.6.1)
unf_ext (0.0.7.7)
unicode-display_width (1.7.0)
uniform_notifier (1.13.0)
valid_email2 (3.1.3)
valid_email2 (3.2.2)
activemodel (>= 3.2)
mail (~> 2.5)
virtus (1.0.5)
@@ -470,15 +485,19 @@ GEM
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webpacker (4.2.2)
activesupport (>= 4.2)
webpacker (5.0.1)
activesupport (>= 5.2)
rack-proxy (>= 0.6.1)
railties (>= 4.2)
railties (>= 5.2)
semantic_range (>= 2.3.0)
webpush (1.0.0)
hkdf (~> 0.2)
jwt (~> 2.0)
websocket-driver (0.7.1)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.4)
wisper (2.0.0)
zeitwerk (2.2.2)
zeitwerk (2.3.0)
PLATFORMS
ruby
@@ -489,7 +508,7 @@ DEPENDENCIES
annotate
attr_extras
aws-sdk-s3
azure-storage
azure-storage-blob
bootsnap
brakeman
browser
@@ -506,6 +525,7 @@ DEPENDENCIES
flag_shih_tzu
foreman
google-cloud-storage
groupdate
haikunator
hashie
jbuilder
@@ -516,8 +536,7 @@ DEPENDENCIES
letter_opener
listen
mini_magick
mock_redis
nightfury
mock_redis!
pg
pry-rails
puma
@@ -544,17 +563,20 @@ DEPENDENCIES
spring
spring-watcher-listen
telegram-bot-ruby
telephone_number
time_diff
twilio-ruby (~> 5.32.0)
twitty!
tzinfo-data
uglifier
valid_email2
web-console
webpacker
webpush
wisper (= 2.0.0)
RUBY VERSION
ruby 2.6.5p114
ruby 2.7.0p0
BUNDLED WITH
2.0.2
2.1.2
+2 -2
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="https://storage.googleapis.com/chatwoot-assets/woot-logo.svg" alt="Woot-logo" width="240">
<img src="https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/brand.svg" alt="Woot-logo" width="240">
<div align="center">A simple and elegant live chat software</div>
<div align="center">An opensource alternative to Intercom, Zendesk, Drift, Crisp etc.</div>
@@ -23,7 +23,7 @@ ___
<a href="https://discord.gg/cJXdrwS"><img src="https://img.shields.io/badge/chat-Discord-violet?logo=discord" alt="Chat on Discord"></a>
</p>
![ChatUI progess](https://storage.googleapis.com/chatwoot-assets/dashboard-screen.png)
![ChatUI progess](https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/chatwoot-dashboard-assets.png)
## Background
+47
View File
@@ -0,0 +1,47 @@
class ContactIdentifyAction
pattr_initialize [:contact!, :params!]
def perform
ActiveRecord::Base.transaction do
@contact = merge_contact(existing_identified_contact, @contact) if merge_contacts?(existing_identified_contact, @contact)
@contact = merge_contact(existing_email_contact, @contact) if merge_contacts?(existing_email_contact, @contact)
update_contact
end
@contact
end
private
def account
@account ||= @contact.account
end
def existing_identified_contact
return if params[:identifier].blank?
@existing_identified_contact ||= Contact.where(account_id: account.id).find_by(identifier: params[:identifier])
end
def existing_email_contact
return if params[:email].blank?
@existing_email_contact ||= Contact.where(account_id: account.id).find_by(email: params[:email])
end
def merge_contacts?(existing_contact, _contact)
existing_contact && existing_contact.id != @contact.id
end
def update_contact
@contact.update!(params.slice(:name, :email, :identifier))
ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
end
def merge_contact(base_contact, merge_contact)
ContactMergeAction.new(
account: account,
base_contact: base_contact,
mergee_contact: merge_contact
).perform
end
end
+7 -1
View File
@@ -5,9 +5,11 @@ class ContactMergeAction
ActiveRecord::Base.transaction do
validate_contacts
merge_conversations
merge_messages
merge_contact_inboxes
remove_mergee_contact
end
@base_contact
end
private
@@ -15,7 +17,7 @@ class ContactMergeAction
def validate_contacts
return if belongs_to_account?(@base_contact) && belongs_to_account?(@mergee_contact)
raise Exception, 'contact does not belong to the account'
raise StandardError, 'contact does not belong to the account'
end
def belongs_to_account?(contact)
@@ -26,6 +28,10 @@ class ContactMergeAction
Conversation.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
end
def merge_messages
Message.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
end
def merge_contact_inboxes
ContactInbox.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
end
+13 -5
View File
@@ -42,18 +42,26 @@ class AccountBuilder
def create_and_link_user
password = Time.now.to_i
@user = @account.users.new(email: @email,
password: password,
password_confirmation: password,
role: User.roles['administrator'],
name: email_to_name(@email))
@user = User.new(email: @email,
password: password,
password_confirmation: password,
name: email_to_name(@email))
if @user.save!
link_user_to_account(@user, @account)
@user
else
raise UserErrors.new(errors: @user.errors)
end
end
def link_user_to_account(user, account)
AccountUser.create!(
account_id: account.id,
user_id: user.id,
role: AccountUser.roles['administrator']
)
end
def email_to_name(email)
name = email[/[^@]+/]
name.split('.').map(&:capitalize).join(' ')
+38
View File
@@ -0,0 +1,38 @@
class ContactBuilder
pattr_initialize [:source_id!, :inbox!, :contact_attributes!]
def perform
contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id)
return contact_inbox if contact_inbox
build_contact
end
private
def account
@account ||= inbox.account
end
def build_contact
ActiveRecord::Base.transaction do
contact = account.contacts.create!(
name: contact_attributes[:name],
phone_number: contact_attributes[:phone_number],
email: contact_attributes[:email],
identifier: contact_attributes[:identifier],
additional_attributes: contact_attributes[:additional_attributes]
)
contact_inbox = ::ContactInbox.create!(
contact_id: contact.id,
inbox_id: inbox.id,
source_id: source_id
)
::ContactAvatarJob.perform_later(contact, contact_attributes[:avatar_url]) if contact_attributes[:avatar_url]
contact_inbox
rescue StandardError => e
Rails.logger e
end
end
end
+2 -6
View File
@@ -1,5 +1,3 @@
require 'open-uri'
# This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo`
# Assumptions
# 1. Incase of an outgoing message which is echo, source_id will NOT be nil,
@@ -36,16 +34,14 @@ class Messages::MessageBuilder
return if contact.present?
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
avatar_resource = LocalResource.new(contact_params[:remote_avatar_url])
@contact.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding)
ContactAvatarJob.perform_later(@contact, contact_params[:remote_avatar_url]) if contact_params[:remote_avatar_url]
@contact_inbox = ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id)
end
def build_message
@message = conversation.messages.create!(message_params)
(response.attachments || []).each do |attachment|
attachment_obj = @message.build_attachment(attachment_params(attachment).except(:remote_file_url))
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
attachment_obj.save!
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
end
@@ -1,16 +1,31 @@
class Messages::Outgoing::NormalBuilder
include ::FileTypeHelper
attr_reader :message
def initialize(user, conversation, params)
@content = params[:message]
@private = ['1', 'true', 1, true].include? params[:private]
@content = params[:content]
@private = params[:private] || false
@conversation = conversation
@user = user
@fb_id = params[:fb_id]
@content_type = params[:content_type]
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
@attachments = params[:attachments]
end
def perform
@message = @conversation.messages.create!(message_params)
@message = @conversation.messages.build(message_params)
if @attachments.present?
@attachments.each do |uploaded_attachment|
attachment = @message.attachments.new(
account_id: @message.account_id,
file_type: file_type(uploaded_attachment&.content_type)
)
attachment.file.attach(uploaded_attachment)
end
end
@message.save
@message
end
private
@@ -22,8 +37,10 @@ class Messages::Outgoing::NormalBuilder
message_type: :outgoing,
content: @content,
private: @private,
user_id: @user.id,
source_id: @fb_id
user_id: @user&.id,
source_id: @fb_id,
content_type: @content_type,
items: @items
}
end
end
+32
View File
@@ -0,0 +1,32 @@
class NotificationBuilder
pattr_initialize [:notification_type!, :user!, :account!, :primary_actor!]
def perform
return unless user_subscribed_to_notification?
build_notification
end
private
def secondary_actor
Current.user
end
def user_subscribed_to_notification?
notification_setting = user.notification_settings.find_by(account_id: account.id)
return true if notification_setting.public_send("email_#{notification_type}?")
return true if notification_setting.public_send("push_#{notification_type}?")
false
end
def build_notification
user.notifications.create!(
notification_type: notification_type,
account: account,
primary_actor: primary_actor,
secondary_actor: secondary_actor
)
end
end
@@ -0,0 +1,28 @@
class NotificationSubscriptionBuilder
pattr_initialize [:params, :user!]
def perform
# if multiple accounts were used to login in same browser
move_subscription_to_user if identifier_subscription && identifier_subscription.user_id != user.id
build_identifier_subscription if identifier_subscription.blank?
identifier_subscription
end
private
def identifier
@identifier ||= params[:subscription_attributes][:endpoint] if params[:subscription_type] == 'browser_push'
end
def identifier_subscription
@identifier_subscription ||= NotificationSubscription.find_by(identifier: identifier)
end
def move_subscription_to_user
@identifier_subscription.update(user_id: user.id)
end
def build_identifier_subscription
user.notification_subscriptions.create(params.merge(identifier: identifier))
end
end
-77
View File
@@ -1,77 +0,0 @@
class ReportBuilder
include CustomExceptions::Report
# Usage
# rb = ReportBuilder.new a, { metric: 'conversations_count', type: :account, id: 1}
# rb = ReportBuilder.new a, { metric: 'avg_first_response_time', type: :agent, id: 1}
IDENTITY_MAPPING = {
account: AccountIdentity,
agent: AgentIdentity
}.freeze
def initialize(account, params)
@account = account
@params = params
@identity = get_identity
@start_time, @end_time = validate_times
end
def build
metric = @identity.send(@params[:metric])
if metric.get.nil?
metric.delete
result = {}
else
result = metric.get_padded_range(@start_time, @end_time) || {}
end
formatted_hash(result)
end
private
def get_identity
identity_class = IDENTITY_MAPPING[@params[:type]]
raise InvalidIdentity if identity_class.nil?
@params[:id] = @account.id if identity_class == AccountIdentity
identity_id = @params[:id]
raise IdentityNotFound if identity_id.nil?
tags = identity_class == AccountIdentity ? nil : { account_id: @account.id }
identity = identity_class.new(identity_id, tags: tags)
raise MetricNotFound if @params[:metric].blank?
raise MetricNotFound unless identity.respond_to?(@params[:metric])
identity
end
def validate_times
start_time = @params[:since] || Time.now.end_of_day - 30.days
end_time = @params[:until] || Time.now.end_of_day
start_time = begin
parse_date_time(start_time)
rescue StandardError
raise(InvalidStartTime)
end
end_time = begin
parse_date_time(end_time)
rescue StandardError
raise(InvalidEndTime)
end
[start_time, end_time]
end
def parse_date_time(datetime)
return datetime if datetime.is_a?(DateTime)
return datetime.to_datetime if datetime.is_a?(Time) || datetime.is_a?(Date)
DateTime.strptime(datetime, '%s')
end
def formatted_hash(hash)
hash.each_with_object([]) do |p, arr|
arr << { value: p[1], timestamp: p[0] }
end
end
end
+111
View File
@@ -0,0 +1,111 @@
class V2::ReportBuilder
attr_reader :account, :params
def initialize(account, params)
@account = account
@params = params
end
def timeseries
send(params[:metric])
end
# For backward compatible with old report
def build
timeseries.each_with_object([]) do |p, arr|
arr << { value: p[1], timestamp: p[0].to_time.to_i }
end
end
def summary
{
conversations_count: conversations_count.values.sum,
incoming_messages_count: incoming_messages_count.values.sum,
outgoing_messages_count: outgoing_messages_count.values.sum,
avg_first_response_time: avg_first_response_time_summary,
avg_resolution_time: avg_resolution_time_summary,
resolutions_count: resolutions_count.values.sum
}
end
private
def scope
return account if params[:type].match?('account')
return inbox if params[:type].match?('inbox')
return user if params[:type].match?('agent')
end
def inbox
@inbox ||= account.inboxes.where(id: params[:id]).first
end
def user
@user ||= account.users.where(id: params[:id]).first
end
def conversations_count
scope.conversations
.group_by_day(:created_at, range: range, default_value: 0)
.count
end
# unscoped removes all scopes added to a model previously
def incoming_messages_count
scope.messages.unscoped.where(account_id: account.id).incoming
.group_by_day(:created_at, range: range, default_value: 0)
.count
end
def outgoing_messages_count
scope.messages.unscoped.where(account_id: account.id).outgoing
.group_by_day(:created_at, range: range, default_value: 0)
.count
end
def resolutions_count
scope.conversations
.resolved
.group_by_day(:created_at, range: range, default_value: 0)
.count
end
def avg_first_response_time
scope.events
.where(name: 'first_response')
.group_by_day(:created_at, range: range, default_value: 0)
.average(:value)
end
def avg_resolution_time
scope.events.where(name: 'conversation_resolved')
.group_by_day(:created_at, range: range, default_value: 0)
.average(:value)
end
def range
parse_date_time(params[:since])..parse_date_time(params[:until])
end
# Taking average of average is not too accurate
# https://en.wikipedia.org/wiki/Simpson's_paradox
# TODO: Will optimize this later
def avg_resolution_time_summary
return 0 if avg_resolution_time.values.empty?
(avg_resolution_time.values.sum / avg_resolution_time.values.length)
end
def avg_first_response_time_summary
return 0 if avg_first_response_time.values.empty?
(avg_first_response_time.values.sum / avg_first_response_time.values.length)
end
def parse_date_time(datetime)
return datetime if datetime.is_a?(DateTime)
return datetime.to_datetime if datetime.is_a?(Time) || datetime.is_a?(Date)
DateTime.strptime(datetime, '%s')
end
end
+8 -1
View File
@@ -1,9 +1,16 @@
class Api::BaseController < ApplicationController
include AccessTokenAuthHelper
respond_to :json
before_action :authenticate_user!
before_action :authenticate_access_token!, if: :authenticate_by_access_token?
before_action :validate_bot_access_token!, if: :authenticate_by_access_token?
before_action :authenticate_user!, unless: :authenticate_by_access_token?
private
def authenticate_by_access_token?
request.headers[:api_access_token].present? || request.headers[:HTTP_API_ACCESS_TOKEN].present?
end
def set_conversation
@conversation ||= current_account.conversations.find_by(display_id: params[:conversation_id])
end
@@ -1,10 +1,12 @@
class Api::V1::AccountsController < Api::BaseController
class Api::V1::Accounts::AccountsController < Api::BaseController
include AuthHelper
skip_before_action :verify_authenticity_token, only: [:create]
skip_before_action :authenticate_user!, :set_current_user, :check_subscription, :handle_with_exception,
only: [:create], raise: false
before_action :check_signup_enabled
before_action :check_signup_enabled, only: [:create]
before_action :check_authorization, except: [:create]
before_action :fetch_account, except: [:create]
rescue_from CustomExceptions::Account::InvalidEmail,
CustomExceptions::Account::UserExists,
@@ -18,18 +20,32 @@ class Api::V1::AccountsController < Api::BaseController
).perform
if @user
send_auth_headers(@user)
render json: {
data: @user.token_validation_response
}
render 'devise/auth.json', locals: { resource: @user }
else
render_error_response(CustomExceptions::Account::SignupFailed.new({}))
end
end
def show
render 'api/v1/accounts/show.json'
end
def update
@account.update!(account_params.slice(:name, :locale, :domain, :support_email, :domain_emails_enabled))
end
private
def check_authorization
authorize(Account)
end
def fetch_account
@account = current_user.accounts.find(params[:id])
end
def account_params
params.permit(:account_name, :email)
params.permit(:account_name, :email, :name, :locale, :domain, :support_email, :domain_emails_enabled)
end
def check_signup_enabled
@@ -1,4 +1,4 @@
class Api::V1::Actions::ContactMergesController < Api::BaseController
class Api::V1::Accounts::Actions::ContactMergesController < Api::BaseController
before_action :set_base_contact, only: [:create]
before_action :set_mergee_contact, only: [:create]
@@ -0,0 +1,69 @@
class Api::V1::Accounts::AgentsController < Api::BaseController
before_action :fetch_agent, except: [:create, :index]
before_action :check_authorization
before_action :find_user, only: [:create]
before_action :create_user, only: [:create]
before_action :save_account_user, only: [:create]
def index
@agents = agents
end
def destroy
@agent.account_user.destroy
head :ok
end
def update
@agent.update!(agent_params.except(:role))
@agent.account_user.update!(role: agent_params[:role]) if agent_params[:role]
render 'api/v1/models/user.json', locals: { resource: @agent }
end
def create
render 'api/v1/models/user.json', locals: { resource: @user }
end
private
def check_authorization
authorize(User)
end
def fetch_agent
@agent = agents.find(params[:id])
end
def find_user
@user = User.find_by(email: new_agent_params[:email])
end
def create_user
return if @user
@user = User.create!(new_agent_params.slice(:email, :name, :password, :password_confirmation))
end
def save_account_user
AccountUser.create!(
account_id: current_account.id,
user_id: @user.id,
role: new_agent_params[:role],
inviter_id: current_user.id
)
end
def agent_params
params.require(:agent).permit(:email, :name, :role)
end
def new_agent_params
time = Time.now.to_i
params.require(:agent).permit(:email, :name, :role)
.merge!(password: time, password_confirmation: time, inviter: current_user)
end
def agents
@agents ||= current_account.users
end
end
@@ -1,24 +1,24 @@
require 'rest-client'
require 'telegram/bot'
class Api::V1::CallbacksController < Api::BaseController
class Api::V1::Accounts::CallbacksController < Api::BaseController
before_action :inbox, only: [:reauthorize_page]
def register_facebook_page
user_access_token = params[:user_access_token]
page_access_token = params[:page_access_token]
page_name = params[:page_name]
page_id = params[:page_id]
inbox_name = params[:inbox_name]
facebook_channel = current_account.facebook_pages.create!(
name: page_name, page_id: page_id, user_access_token: user_access_token,
page_access_token: page_access_token
)
set_avatar(facebook_channel, page_id)
inbox = current_account.inboxes.create!(name: inbox_name, channel: facebook_channel)
render json: inbox
ActiveRecord::Base.transaction do
facebook_channel = current_account.facebook_pages.create!(
page_id: page_id, user_access_token: user_access_token,
page_access_token: page_access_token
)
@facebook_inbox = current_account.inboxes.create!(name: inbox_name, channel: facebook_channel)
set_avatar(@facebook_inbox, page_id)
rescue StandardError => e
Rails.logger e
end
end
def get_facebook_pages
def facebook_pages
@page_details = mark_already_existing_facebook_pages(fb_object.get_connections('me', 'accounts'))
end
@@ -61,24 +61,26 @@ class Api::V1::CallbacksController < Api::BaseController
def long_lived_token(omniauth_token)
koala = Koala::Facebook::OAuth.new(ENV['FB_APP_ID'], ENV['FB_APP_SECRET'])
koala.exchange_access_token_info(omniauth_token)['access_token']
rescue StandardError => e
Rails.logger e
end
def mark_already_existing_facebook_pages(data)
return [] if data.empty?
data.inject([]) do |result, page_detail|
current_account.facebook_pages.exists?(page_id: page_detail['id']) ? page_detail.merge!(exists: true) : page_detail.merge!(exists: false)
page_detail[:exists] = current_account.facebook_pages.exists?(page_id: page_detail['id']) ? true : false
result << page_detail
end
end
def set_avatar(facebook_channel, page_id)
def set_avatar(facebook_inbox, page_id)
uri = get_avatar_url(page_id)
return unless uri
avatar_resource = LocalResource.new(uri)
facebook_channel.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding)
facebook_inbox.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding)
end
def get_avatar_url(page_id)
@@ -90,11 +92,12 @@ class Api::V1::CallbacksController < Api::BaseController
response = uri.open(redirect: false)
rescue OpenURI::HTTPRedirect => e
uri = e.uri # assigned from the "Location" response header
retry if (tries -= 1) > 0
retry if (tries -= 1).positive?
raise
end
pic_url = response.base_uri.to_s
rescue StandardError => e
Rails.logger.debug "Rescued: #{e.inspect}"
pic_url = nil
end
pic_url
@@ -1,4 +1,4 @@
class Api::V1::CannedResponsesController < Api::BaseController
class Api::V1::Accounts::CannedResponsesController < Api::BaseController
before_action :fetch_canned_response, only: [:update, :destroy]
def index
@@ -0,0 +1,57 @@
class Api::V1::Accounts::Channels::TwilioChannelsController < Api::BaseController
before_action :authorize_request
def create
ActiveRecord::Base.transaction do
authenticate_twilio
build_inbox
setup_webhooks if @twilio_channel.sms?
rescue Twilio::REST::TwilioError => e
render_could_not_create_error(e.message)
rescue StandardError => e
render_could_not_create_error(e.message)
end
end
private
def authorize_request
authorize ::Inbox
end
def authenticate_twilio
client = Twilio::REST::Client.new(permitted_params[:account_sid], permitted_params[:auth_token])
client.messages.list(limit: 1)
end
def setup_webhooks
::Twilio::WebhookSetupService.new(inbox: @inbox).perform
end
def phone_number
medium == 'sms' ? permitted_params[:phone_number] : "whatsapp:#{permitted_params[:phone_number]}"
end
def medium
permitted_params[:medium]
end
def build_inbox
@twilio_channel = current_account.twilio_sms.create!(
account_sid: permitted_params[:account_sid],
auth_token: permitted_params[:auth_token],
phone_number: phone_number,
medium: medium
)
@inbox = current_account.inboxes.create(
name: permitted_params[:name],
channel: @twilio_channel
)
end
def permitted_params
params.require(:twilio_channel).permit(
:account_id, :phone_number, :account_sid, :auth_token, :name, :medium
)
end
end
@@ -1,4 +1,4 @@
class Api::V1::Contacts::ConversationsController < Api::BaseController
class Api::V1::Accounts::Contacts::ConversationsController < Api::BaseController
def index
@conversations = current_account.conversations.includes(
:assignee, :contact, :inbox
@@ -1,14 +1,9 @@
class Api::V1::ContactsController < Api::BaseController
class Api::V1::Accounts::ContactsController < Api::BaseController
protect_from_forgery with: :null_session
before_action :check_authorization
before_action :fetch_contact, only: [:show, :update]
skip_before_action :authenticate_user!, only: [:create]
skip_before_action :set_current_user, only: [:create]
skip_before_action :check_subscription, only: [:create]
skip_around_action :handle_with_exception, only: [:create]
def index
@contacts = current_account.contacts
end
@@ -1,7 +1,8 @@
class Api::V1::Conversations::AssignmentsController < Api::BaseController
class Api::V1::Accounts::Conversations::AssignmentsController < Api::BaseController
before_action :set_conversation, only: [:create]
def create # assign agent to a conversation
# assign agent to a conversation
def create
# if params[:assignee_id] is not a valid id, it will set to nil, hence unassigning the conversation
assignee = current_account.users.find_by(id: params[:assignee_id])
@conversation.update_assignee(assignee)
@@ -1,4 +1,4 @@
class Api::V1::Conversations::LabelsController < Api::BaseController
class Api::V1::Accounts::Conversations::LabelsController < Api::BaseController
before_action :set_conversation, only: [:create, :index]
def create
@@ -6,7 +6,8 @@ class Api::V1::Conversations::LabelsController < Api::BaseController
@labels = @conversation.label_list
end
def index # all labels of the current conversation
# all labels of the current conversation
def index
@labels = @conversation.label_list
end
end
@@ -1,4 +1,4 @@
class Api::V1::Conversations::MessagesController < Api::BaseController
class Api::V1::Accounts::Conversations::MessagesController < Api::BaseController
before_action :set_conversation, only: [:index, :create]
def index
@@ -0,0 +1,73 @@
class Api::V1::Accounts::ConversationsController < Api::BaseController
include Events::Types
before_action :conversation, except: [:index]
before_action :contact_inbox, only: [:create]
def index
result = conversation_finder.perform
@conversations = result[:conversations]
@conversations_count = result[:count]
end
def meta
result = conversation_finder.perform
@conversations_count = result[:count]
end
def create
@conversation = ::Conversation.create!(conversation_params)
end
def show; end
def toggle_status
@status = @conversation.toggle_status
end
def toggle_typing_status
if params[:typing_status] == 'on'
trigger_typing_event(CONVERSATION_TYPING_ON)
elsif params[:typing_status] == 'off'
trigger_typing_event(CONVERSATION_TYPING_OFF)
end
head :ok
end
def update_last_seen
@conversation.agent_last_seen_at = parsed_last_seen_at
@conversation.save!
head :ok
end
private
def trigger_typing_event(event)
user = current_user.presence || @resource
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user)
end
def parsed_last_seen_at
DateTime.strptime(params[:agent_last_seen_at].to_s, '%s')
end
def conversation
@conversation ||= current_account.conversations.find_by(display_id: params[:id])
end
def contact_inbox
@contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id])
end
def conversation_params
{
account_id: current_account.id,
inbox_id: @contact_inbox.inbox_id,
contact_id: @contact_inbox.contact_id,
contact_inbox_id: @contact_inbox.id
}
end
def conversation_finder
@conversation_finder ||= ConversationFinder.new(current_user, params)
end
end
@@ -1,4 +1,4 @@
class Api::V1::FacebookIndicatorsController < Api::BaseController
class Api::V1::Accounts::FacebookIndicatorsController < Api::BaseController
before_action :set_access_token
around_action :handle_with_exception
@@ -26,6 +26,7 @@ class Api::V1::FacebookIndicatorsController < Api::BaseController
def handle_with_exception
yield
rescue Facebook::Messenger::Error => e
Rails.logger.debug "Rescued: #{e.inspect}"
true
end
@@ -1,4 +1,4 @@
class Api::V1::InboxMembersController < Api::BaseController
class Api::V1::Accounts::InboxMembersController < Api::BaseController
before_action :fetch_inbox, only: [:create, :show]
before_action :current_agents_ids, only: [:create]
@@ -0,0 +1,66 @@
class Api::V1::Accounts::InboxesController < Api::BaseController
before_action :check_authorization
before_action :fetch_inbox, except: [:index, :create]
before_action :fetch_agent_bot, only: [:set_agent_bot]
def index
@inboxes = policy_scope(current_account.inboxes)
end
def create
ActiveRecord::Base.transaction do
channel = web_widgets.create!(permitted_params[:channel].except(:type)) if permitted_params[:channel][:type] == 'web_widget'
@inbox = current_account.inboxes.build(name: permitted_params[:name], channel: channel)
@inbox.avatar.attach(permitted_params[:avatar])
@inbox.save!
end
end
def update
@inbox.update(inbox_update_params.except(:channel))
@inbox.channel.update!(inbox_update_params[:channel]) if @inbox.channel.is_a?(Channel::WebWidget) && inbox_update_params[:channel].present?
end
def set_agent_bot
if @agent_bot
agent_bot_inbox = @inbox.agent_bot_inbox || AgentBotInbox.new(inbox: @inbox)
agent_bot_inbox.agent_bot = @agent_bot
agent_bot_inbox.save!
elsif @inbox.agent_bot_inbox.present?
@inbox.agent_bot_inbox.destroy!
end
head :ok
end
def destroy
@inbox.destroy
head :ok
end
private
def fetch_inbox
@inbox = current_account.inboxes.find(params[:id])
end
def fetch_agent_bot
@agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot]
end
def web_widgets
current_account.web_widgets
end
def check_authorization
authorize(Inbox)
end
def permitted_params
params.permit(:id, :avatar, :name, channel: [:type, :website_url, :widget_color, :welcome_title, :welcome_tagline, :agent_away_message])
end
def inbox_update_params
params.permit(:enable_auto_assignment, :name, :avatar, channel: [:website_url, :widget_color, :welcome_title,
:welcome_tagline, :agent_away_message])
end
end
@@ -1,5 +1,6 @@
class Api::V1::LabelsController < Api::BaseController
def index # list all labels in account
class Api::V1::Accounts::LabelsController < Api::BaseController
# list all labels in account
def index
@labels = current_account.all_conversation_tags
end
@@ -1,4 +1,4 @@
class Api::V1::User::NotificationSettingsController < Api::BaseController
class Api::V1::Accounts::NotificationSettingsController < Api::BaseController
before_action :set_user, :load_notification_setting
def show; end
@@ -20,10 +20,11 @@ class Api::V1::User::NotificationSettingsController < Api::BaseController
end
def notification_setting_params
params.require(:notification_settings).permit(selected_email_flags: [])
params.require(:notification_settings).permit(selected_email_flags: [], selected_push_flags: [])
end
def update_flags
@notification_setting.selected_email_flags = notification_setting_params[:selected_email_flags]
@notification_setting.selected_push_flags = notification_setting_params[:selected_push_flags]
end
end
@@ -0,0 +1,21 @@
class Api::V1::Accounts::NotificationsController < Api::BaseController
protect_from_forgery with: :null_session
before_action :fetch_notification, only: [:update]
def index
@notifications = current_user.notifications.where(account_id: current_account.id)
render json: @notifications
end
def update
@notification.update(read_at: DateTime.now.utc)
render json: @notification
end
private
def fetch_notification
@notification = current_user.notifications.find(params[:id])
end
end
@@ -1,4 +1,4 @@
class Api::V1::SubscriptionsController < Api::BaseController
class Api::V1::Accounts::SubscriptionsController < Api::BaseController
skip_before_action :check_subscription
before_action :check_billing_enabled
@@ -1,4 +1,4 @@
class Api::V1::Account::WebhooksController < Api::BaseController
class Api::V1::Accounts::WebhooksController < Api::BaseController
before_action :check_authorization
before_action :fetch_webhook, only: [:update, :destroy]
@@ -0,0 +1,8 @@
class Api::V1::AgentBotsController < Api::BaseController
skip_before_action :authenticate_user!
skip_before_action :check_subscription
def index
render json: AgentBot.all
end
end
@@ -1,52 +0,0 @@
class Api::V1::AgentsController < Api::BaseController
before_action :fetch_agent, except: [:create, :index]
before_action :check_authorization
before_action :build_agent, only: [:create]
def index
@agents = agents
end
def destroy
@agent.destroy
head :ok
end
def update
@agent.update!(agent_params)
render json: @agent
end
def create
@agent.save!
render json: @agent
end
private
def check_authorization
authorize(User)
end
def fetch_agent
@agent = agents.find(params[:id])
end
def build_agent
@agent = agents.new(new_agent_params)
end
def agent_params
params.require(:agent).permit(:email, :name, :role)
end
def new_agent_params
time = Time.now.to_i
params.require(:agent).permit(:email, :name, :role)
.merge!(password: time, password_confirmation: time, inviter: current_user)
end
def agents
@agents ||= current_account.users
end
end
@@ -1,35 +0,0 @@
class Api::V1::ConversationsController < Api::BaseController
before_action :set_conversation, except: [:index]
def index
result = conversation_finder.perform
@conversations = result[:conversations]
@conversations_count = result[:count]
end
def show; end
def toggle_status
@status = @conversation.toggle_status
end
def update_last_seen
@conversation.agent_last_seen_at = parsed_last_seen_at
@conversation.save!
head :ok
end
private
def parsed_last_seen_at
DateTime.strptime(params[:agent_last_seen_at].to_s, '%s')
end
def set_conversation
@conversation ||= current_account.conversations.find_by(display_id: params[:id])
end
def conversation_finder
@conversation_finder ||= ConversationFinder.new(current_user, params)
end
end
@@ -1,31 +0,0 @@
class Api::V1::InboxesController < Api::BaseController
before_action :check_authorization
before_action :fetch_inbox, only: [:destroy, :update]
def index
@inboxes = policy_scope(current_account.inboxes)
end
def destroy
@inbox.destroy
head :ok
end
def update
@inbox.update(inbox_update_params)
end
private
def fetch_inbox
@inbox = current_account.inboxes.find(params[:id])
end
def check_authorization
authorize(Inbox)
end
def inbox_update_params
params.require(:inbox).permit(:enable_auto_assignment)
end
end
@@ -0,0 +1,19 @@
class Api::V1::NotificationSubscriptionsController < Api::BaseController
before_action :set_user
def create
notification_subscription = NotificationSubscriptionBuilder.new(user: @user, params: notification_subscription_params).perform
render json: notification_subscription
end
private
def set_user
@user = current_user
end
def notification_subscription_params
params.require(:notification_subscription).permit(:subscription_type, subscription_attributes: {})
end
end
@@ -1,103 +0,0 @@
class Api::V1::ReportsController < Api::BaseController
include CustomExceptions::Report
include Constants::Report
around_action :report_exception
def account
builder = ReportBuilder.new(current_account, account_report_params)
data = builder.build
render json: data
end
def agent
builder = ReportBuilder.new(current_account, agent_report_params)
data = builder.build
render json: data
end
def account_summary
render json: account_summary_metrics
end
def agent_summary
render json: agent_summary_metrics
end
private
def report_exception
yield
rescue InvalidIdentity, IdentityNotFound, MetricNotFound, InvalidStartTime, InvalidEndTime => e
render_error_response(e)
end
def current_account
current_user.account
end
def agent
@agent ||= current_account.users.find(params[:agent_id])
end
def account_summary_metrics
summary_metrics(ACCOUNT_METRICS, :account_summary_params, AVG_ACCOUNT_METRICS)
end
def agent_summary_metrics
summary_metrics(AGENT_METRICS, :agent_summary_params, AVG_AGENT_METRICS)
end
def summary_metrics(metrics, calc_function, avg_metrics)
metrics.each_with_object({}) do |metric, result|
data = ReportBuilder.new(current_account, send(calc_function, metric)).build
if avg_metrics.include?(metric)
sum = data.inject(0) { |sum, hash| sum + hash[:value].to_i }
sum /= data.length unless sum.zero?
else
sum = data.inject(0) { |sum, hash| sum + hash[:value].to_i }
end
result[metric] = sum
end
end
def account_summary_params(metric)
{
metric: metric.to_s,
type: :account,
since: params[:since],
until: params[:until]
}
end
def agent_summary_params(metric)
{
metric: metric.to_s,
type: :agent,
since: params[:since],
until: params[:until],
id: params[:id]
}
end
def account_report_params
{
metric: params[:metric],
type: :account,
since: params[:since],
until: params[:until]
}
end
def agent_report_params
{
metric: params[:metric],
type: :agent,
id: params[:id],
since: params[:since],
until: params[:until]
}
end
end
@@ -5,6 +5,7 @@ class Api::V1::WebhooksController < ApplicationController
before_action :login_from_basic_auth, only: [:chargebee]
before_action :check_billing_enabled, only: [:chargebee]
def chargebee
chargebee_consumer.consume
head :ok
@@ -2,9 +2,9 @@ class Api::V1::Widget::BaseController < ApplicationController
private
def conversation
@conversation ||= @contact_inbox.conversations.find_by(
@conversation ||= @contact_inbox.conversations.where(
inbox_id: auth_token_params[:inbox_id]
)
).last
end
def auth_token_params
@@ -18,6 +18,7 @@ class Api::V1::Widget::BaseController < ApplicationController
def set_web_widget
@web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token])
@account = @web_widget.account
switch_locale @account
end
def set_contact
@@ -0,0 +1,18 @@
class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
before_action :set_web_widget
before_action :set_contact
def update
contact_identify_action = ContactIdentifyAction.new(
contact: @contact,
params: permitted_params.to_h.deep_symbolize_keys
)
render json: contact_identify_action.perform
end
private
def permitted_params
params.permit(:website_token, :identifier, :email, :name, :avatar_url)
end
end
@@ -0,0 +1,27 @@
class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
include Events::Types
before_action :set_web_widget
before_action :set_contact
def toggle_typing
head :ok && return if conversation.nil?
if permitted_params[:typing_status] == 'on'
trigger_typing_event(CONVERSATION_TYPING_ON)
elsif permitted_params[:typing_status] == 'off'
trigger_typing_event(CONVERSATION_TYPING_OFF)
end
head :ok
end
private
def trigger_typing_event(event)
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: conversation, user: @contact)
end
def permitted_params
params.permit(:id, :typing_status, :website_token)
end
end
@@ -0,0 +1,16 @@
class Api::V1::Widget::EventsController < Api::V1::Widget::BaseController
include Events::Types
before_action :set_web_widget
before_action :set_contact
def create
Rails.configuration.dispatcher.dispatch(permitted_params[:name], Time.zone.now, contact_inbox: @contact_inbox)
head :no_content
end
private
def permitted_params
params.permit(:name, :website_token)
end
end
@@ -1,48 +0,0 @@
class Api::V1::Widget::InboxesController < Api::BaseController
before_action :authorize_request
before_action :set_web_widget_channel, only: [:update]
before_action :set_inbox, only: [:update]
def create
ActiveRecord::Base.transaction do
channel = web_widgets.create!(
website_name: permitted_params[:website][:website_name],
website_url: permitted_params[:website][:website_url],
widget_color: permitted_params[:website][:widget_color]
)
@inbox = inboxes.create!(name: permitted_params[:website][:website_name], channel: channel)
end
end
def update
@channel.update!(
widget_color: permitted_params[:website][:widget_color]
)
end
private
def authorize_request
authorize ::Inbox
end
def inboxes
current_account.inboxes
end
def web_widgets
current_account.web_widgets
end
def set_web_widget_channel
@channel = web_widgets.find_by(id: permitted_params[:id])
end
def set_inbox
@inbox = @channel.inbox
end
def permitted_params
params.permit(:id, website: [:website_name, :website_url, :widget_color])
end
end
@@ -0,0 +1,24 @@
class Api::V1::Widget::LabelsController < Api::V1::Widget::BaseController
before_action :set_web_widget
before_action :set_contact
def create
conversation.label_list.add(permitted_params[:label])
conversation.save!
head :no_content
end
def destroy
conversation.label_list.remove(permitted_params[:id])
conversation.save!
head :no_content
end
private
def permitted_params
params.permit(:id, :label, :website_token)
end
end
@@ -10,20 +10,36 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
def create
@message = conversation.messages.new(message_params)
@message.save!
render json: @message
@message.save
build_attachment
end
def update
@message.update!(input_submitted_email: contact_email)
update_contact(contact_email)
head :no_content
if @message.content_type == 'input_email'
@message.update!(submitted_email: contact_email)
update_contact(contact_email)
else
@message.update!(message_update_params[:message])
end
rescue StandardError => e
render json: { error: @contact.errors, message: e.message }.to_json, status: 500
end
private
def build_attachment
return if params[:message][:attachments].blank?
params[:message][:attachments].each do |uploaded_attachment|
attachment = @message.attachments.new(
account_id: @message.account_id,
file_type: helpers.file_type(uploaded_attachment&.content_type)
)
attachment.file.attach(uploaded_attachment)
end
@message.save!
end
def set_conversation
@conversation = ::Conversation.create!(conversation_params) if conversation.nil?
end
@@ -86,7 +102,11 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
def update_contact(email)
contact_with_email = @account.contacts.find_by(email: email)
if contact_with_email
::ContactMergeAction.new(account: @account, base_contact: contact_with_email, mergee_contact: @contact).perform
@contact = ::ContactMergeAction.new(
account: @account,
base_contact: contact_with_email,
mergee_contact: @contact
).perform
else
@contact.update!(
email: email,
@@ -103,6 +123,10 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
contact_email.split('@')[0]
end
def message_update_params
params.permit(message: [submitted_values: [:name, :title, :value]])
end
def permitted_params
params.permit(:id, :before, :website_token, contact: [:email], message: [:content, :referer_url, :timestamp])
end
@@ -0,0 +1,39 @@
class Api::V2::Accounts::ReportsController < Api::BaseController
def account
builder = V2::ReportBuilder.new(current_account, account_report_params)
data = builder.build
render json: data
end
def account_summary
render json: account_summary_metrics
end
private
def current_account
current_user.account
end
def account_summary_params
{
type: :account,
since: params[:since],
until: params[:until]
}
end
def account_report_params
{
metric: params[:metric],
type: :account,
since: params[:since],
until: params[:until]
}
end
def account_summary_metrics
builder = V2::ReportBuilder.new(current_account, account_summary_params)
builder.summary
end
end
+8
View File
@@ -0,0 +1,8 @@
class ApiController < ApplicationController
skip_before_action :set_current_user, only: [:index]
skip_before_action :check_subscription, only: [:index]
def index
render json: { version: Chatwoot.config[:version], timestamp: Time.now.utc.to_formatted_s(:db) }
end
end
+6
View File
@@ -0,0 +1,6 @@
class AppleAppController < ApplicationController
def site_association
site_association_json = render_to_string action: 'site_association', layout: false
send_data site_association_json, filename: 'apple-app-site-association', type: 'application/json'
end
end
+24 -1
View File
@@ -14,7 +14,30 @@ class ApplicationController < ActionController::Base
private
def current_account
@_ ||= current_user.account
@_ ||= find_current_account
end
def find_current_account
account = Account.find(params[:account_id])
if current_user
account_accessible_for_user?(account)
elsif @resource&.is_a?(AgentBot)
account_accessible_for_bot?(account)
end
switch_locale account
account
end
def switch_locale(account)
I18n.locale = (I18n.available_locales.map(&:to_s).include?(account.locale) ? account.locale : nil) || I18n.default_locale
end
def account_accessible_for_user?(account)
render_unauthorized('You are not authorized to access this account') unless account.account_users.find_by(user_id: current_user.id)
end
def account_accessible_for_bot?(account)
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
end
def handle_with_exception
@@ -0,0 +1,26 @@
module AccessTokenAuthHelper
BOT_ACCESSIBLE_ENDPOINTS = {
'api/v1/accounts/conversations' => %w[toggle_status create],
'api/v1/accounts/conversations/messages' => ['create']
}.freeze
def authenticate_access_token!
token = request.headers[:api_access_token] || request.headers[:HTTP_API_ACCESS_TOKEN]
access_token = AccessToken.find_by(token: token)
render_unauthorized('Invalid Access Token') && return unless access_token
token_owner = access_token.owner
@resource = token_owner
end
def validate_bot_access_token!
return if current_user.is_a?(User)
return if agent_bot_accessible?
render_unauthorized('Access to this endpoint is not authorized for bots')
end
def agent_bot_accessible?
BOT_ACCESSIBLE_ENDPOINTS.fetch(params[:controller], []).include?(params[:action])
end
end
@@ -11,9 +11,7 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
@recoverable = User.find_by(reset_password_token: reset_password_token)
if @recoverable && reset_password_and_confirmation(@recoverable)
send_auth_headers(@recoverable)
render json: {
data: @recoverable.token_validation_response
}
render 'devise/auth.json', locals: { resource: @recoverable }
else
render json: { "message": 'Invalid token', "redirect_url": '/' }, status: 422
end
@@ -4,6 +4,6 @@ class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsControlle
wrap_parameters format: []
def render_create_success
render 'devise/auth.json'
render 'devise/auth.json', locals: { resource: @resource }
end
end
-9
View File
@@ -1,9 +0,0 @@
require 'rest-client'
require 'telegram/bot'
class HomeController < ApplicationController
skip_before_action :verify_authenticity_token, only: [:telegram]
skip_before_action :authenticate_user!, only: [:telegram], raise: false
skip_before_action :set_current_user
skip_before_action :check_subscription
end
@@ -0,0 +1,31 @@
class Twilio::CallbackController < ApplicationController
def create
::Twilio::IncomingMessageService.new(params: permitted_params).perform
head :no_content
end
private
def permitted_params
params.permit(
:ApiVersion,
:SmsSid,
:From,
:ToState,
:ToZip,
:AccountSid,
:MessageSid,
:FromCountry,
:ToCity,
:FromCity,
:To,
:FromZip,
:Body,
:ToCountry,
:FromState,
:MediaUrl0,
:MediaContentType0
)
end
end
@@ -6,7 +6,7 @@ class Twitter::AuthorizationsController < Twitter::BaseController
::Redis::Alfred.setex(oauth_token, account.id)
redirect_to oauth_authorize_endpoint(oauth_token)
else
redirect_to app_new_twitter_inbox_url
redirect_to app_new_twitter_inbox_url(account_id: account.id)
end
end
@@ -1,6 +1,6 @@
class Twitter::CallbacksController < Twitter::BaseController
def show
return redirect_to app_new_twitter_inbox_url if permitted_params[:denied]
return redirect_to twitter_app_redirect_url if permitted_params[:denied]
@response = twitter_client.access_token(
oauth_token: permitted_params[:oauth_token],
@@ -10,9 +10,9 @@ class Twitter::CallbacksController < Twitter::BaseController
inbox = build_inbox
::Redis::Alfred.delete(permitted_params[:oauth_token])
::Twitter::WebhookSubscribeService.new(inbox_id: inbox.id).perform
redirect_to app_twitter_inbox_agents_url(inbox_id: inbox.id)
redirect_to app_twitter_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
else
redirect_to app_new_twitter_inbox_url
redirect_to twitter_app_redirect_url
end
end
@@ -30,13 +30,16 @@ class Twitter::CallbacksController < Twitter::BaseController
@account ||= Account.find_by!(id: account_id)
end
def twitter_app_redirect_url
app_new_twitter_inbox_url(account_id: account.id)
end
def build_inbox
ActiveRecord::Base.transaction do
twitter_profile = account.twitter_profiles.create(
twitter_access_token: parsed_body['oauth_token'],
twitter_access_token_secret: parsed_body['oauth_token_secret'],
profile_id: parsed_body['user_id'],
name: parsed_body['screen_name']
profile_id: parsed_body['user_id']
)
account.inboxes.create(
name: parsed_body['screen_name'],
+5 -1
View File
@@ -1,11 +1,15 @@
class AsyncDispatcher < BaseDispatcher
def dispatch(event_name, timestamp, data)
EventDispatcherJob.perform_later(event_name, timestamp, data)
end
def publish_event(event_name, timestamp, data)
event_object = Events::Base.new(event_name, timestamp, data)
publish(event_object.method_name, event_object)
end
def listeners
listeners = [EmailNotificationListener.instance, ReportingListener.instance, WebhookListener.instance]
listeners = [EventListener.instance, WebhookListener.instance]
listeners << SubscriptionListener.instance if ENV['BILLING_ENABLED']
listeners
end
+1 -1
View File
@@ -5,6 +5,6 @@ class SyncDispatcher < BaseDispatcher
end
def listeners
[ActionCableListener.instance]
[ActionCableListener.instance, AgentBotListener.instance, NotificationListener.instance]
end
end
+1 -1
View File
@@ -11,7 +11,7 @@ class MessageFinder
private
def conversation_messages
@conversation.messages.includes(:attachment, user: { avatar_attachment: :blob })
@conversation.messages.includes(:attachments, user: { avatar_attachment: :blob })
end
def messages
+3
View File
@@ -1,2 +1,5 @@
module ApplicationHelper
def available_locales_with_name
LANGUAGES_CONFIG.map { |_key, val| val.slice(:name, :iso_639_1_code) }
end
end
+14
View File
@@ -0,0 +1,14 @@
module FileTypeHelper
def file_type(content_type)
return :image if [
'image/jpeg',
'image/png',
'image/svg+xml',
'image/gif',
'image/tiff',
'image/bmp'
].include?(content_type)
:file
end
end
-10
View File
@@ -1,10 +0,0 @@
class AccountIdentity < Nightfury::Identity::Base
metric :conversations_count, :count_time_series, store_as: :b, step: :day
metric :incoming_messages_count, :count_time_series, step: :day
metric :outgoing_messages_count, :count_time_series, step: :day
metric :avg_first_response_time, :avg_time_series, store_as: :d, step: :day
metric :avg_resolution_time, :avg_time_series, store_as: :f, step: :day
metric :resolutions_count, :count_time_series, store_as: :g, step: :day
end
AccountIdentity.store_as = :ci
-8
View File
@@ -1,8 +0,0 @@
class AgentIdentity < Nightfury::Identity::Base
metric :avg_first_response_time, :avg_time_series, store_as: :d, step: :day
metric :avg_resolution_time, :avg_time_series, store_as: :f, step: :day
metric :resolutions_count, :count_time_series, store_as: :g, step: :day
tag :account_id, store_as: :co
end
AgentIdentity.store_as = :ai
+23
View File
@@ -8,7 +8,10 @@
</template>
<script>
import Vue from 'vue';
import { mapGetters } from 'vuex';
import WootSnackbarBox from './components/SnackbarContainer';
import { accountIdFromPathname } from './helper/URLHelper';
export default {
name: 'App',
@@ -17,8 +20,28 @@ export default {
WootSnackbarBox,
},
computed: {
...mapGetters({
getAccount: 'accounts/getAccount',
}),
},
mounted() {
this.$store.dispatch('setUser');
this.initializeAccount();
},
methods: {
async initializeAccount() {
const { pathname } = window.location;
const accountId = accountIdFromPathname(pathname);
if (accountId) {
await this.$store.dispatch('accounts/get');
const { locale } = this.getAccount(accountId);
Vue.config.lang = locale;
}
},
},
};
</script>
+18 -2
View File
@@ -3,9 +3,25 @@
const API_VERSION = `/api/v1`;
class ApiClient {
constructor(url) {
constructor(resource, options = {}) {
this.apiVersion = API_VERSION;
this.url = `${this.apiVersion}/${url}`;
this.options = options;
this.resource = resource;
}
get url() {
let url = this.apiVersion;
if (this.options.accountScoped) {
const isInsideAccountScopedURLs = window.location.pathname.includes(
'/app/accounts'
);
if (isInsideAccountScopedURLs) {
const accountId = window.location.pathname.split('/')[3];
url = `${url}/accounts/${accountId}`;
}
}
return `${url}/${this.resource}`;
}
get() {
+9
View File
@@ -0,0 +1,9 @@
import ApiClient from './ApiClient';
class AccountAPI extends ApiClient {
constructor() {
super('', { accountScoped: true });
}
}
export default new AccountAPI();
+1 -1
View File
@@ -2,7 +2,7 @@ import ApiClient from './ApiClient';
class Agents extends ApiClient {
constructor() {
super('agents');
super('agents', { accountScoped: true });
}
}
@@ -4,7 +4,7 @@ import ApiClient from './ApiClient';
class CannedResponse extends ApiClient {
constructor() {
super('canned_responses');
super('canned_responses', { accountScoped: true });
}
get({ searchKey }) {
@@ -3,7 +3,7 @@ import ApiClient from '../ApiClient';
class FBChannel extends ApiClient {
constructor() {
super('facebook_indicators');
super('facebook_indicators', { accountScoped: true });
}
markSeen({ inboxId, contactId }) {
@@ -22,7 +22,7 @@ class FBChannel extends ApiClient {
create(params) {
return axios.post(
`${this.apiVersion}/callbacks/register_facebook_page`,
`${this.url.replace(this.resource, '')}callbacks/register_facebook_page`,
params
);
}
@@ -0,0 +1,9 @@
import ApiClient from '../ApiClient';
class TwilioChannel extends ApiClient {
constructor() {
super('channels/twilio_channel', { accountScoped: true });
}
}
export default new TwilioChannel();
@@ -2,7 +2,7 @@ import ApiClient from '../ApiClient';
class WebChannel extends ApiClient {
constructor() {
super('widget/inboxes');
super('inboxes', { accountScoped: true });
}
}
+2 -2
View File
@@ -5,9 +5,9 @@
import endPoints from './endPoints';
export default {
fetchFacebookPages(token) {
fetchFacebookPages(token, accountId) {
const urlData = endPoints('fetchFacebookPages');
urlData.params.omniauth_token = token;
return axios.post(urlData.url, urlData.params);
return axios.post(urlData.url(accountId), urlData.params);
},
};
+1 -1
View File
@@ -3,7 +3,7 @@ import ApiClient from './ApiClient';
class ContactAPI extends ApiClient {
constructor() {
super('contacts');
super('contacts', { accountScoped: true });
}
getConversations(contactId) {
@@ -3,7 +3,7 @@ import ApiClient from './ApiClient';
class ConversationApi extends ApiClient {
constructor() {
super('conversations');
super('conversations', { accountScoped: true });
}
getLabels(conversationID) {
+3 -14
View File
@@ -28,23 +28,12 @@ const endPoints = {
},
fetchFacebookPages: {
url: 'api/v1/callbacks/get_facebook_pages.json',
url(accountId) {
return `api/v1/accounts/${accountId}/callbacks/facebook_pages.json`;
},
params: { omniauth_token: '' },
},
reports: {
account(metric, from, to) {
return {
url: `/api/v1/reports/account?metric=${metric}&since=${from}&to=${to}`,
};
},
accountSummary(accountId, from, to) {
return {
url: `/api/v1/reports/${accountId}/account_summary?since=${from}&to=${to}`,
};
},
},
subscriptions: {
get() {
return {
@@ -3,7 +3,7 @@ import ApiClient from '../ApiClient';
class ConversationApi extends ApiClient {
constructor() {
super('conversations');
super('conversations', { accountScoped: true });
}
get({ inboxId, status, assigneeType, page }) {
@@ -33,6 +33,12 @@ class ConversationApi extends ApiClient {
agent_last_seen_at: lastSeen,
});
}
toggleTyping({ conversationId, status }) {
return axios.post(`${this.url}/${conversationId}/toggle_typing_status`, {
typing_status: status,
});
}
}
export default new ConversationApi();
+12 -2
View File
@@ -4,12 +4,12 @@ import ApiClient from '../ApiClient';
class MessageApi extends ApiClient {
constructor() {
super('conversations');
super('conversations', { accountScoped: true });
}
create({ conversationId, message, private: isPrivate }) {
return axios.post(`${this.url}/${conversationId}/messages`, {
message,
content: message,
private: isPrivate,
});
}
@@ -19,6 +19,16 @@ class MessageApi extends ApiClient {
params: { before },
});
}
sendAttachment([conversationId, { file }]) {
const formData = new FormData();
formData.append('attachments[]', file, file.name);
return axios({
method: 'post',
url: `${this.url}/${conversationId}/messages`,
data: formData,
});
}
}
export default new MessageApi();
+1 -1
View File
@@ -3,7 +3,7 @@ import ApiClient from './ApiClient';
class InboxMembers extends ApiClient {
constructor() {
super('inbox_members');
super('inbox_members', { accountScoped: true });
}
create({ inboxId, agentList }) {
+1 -1
View File
@@ -2,7 +2,7 @@ import ApiClient from './ApiClient';
class Inboxes extends ApiClient {
constructor() {
super('inboxes');
super('inboxes', { accountScoped: true });
}
}
@@ -0,0 +1,9 @@
import ApiClient from './ApiClient';
class NotificationSubscriptions extends ApiClient {
constructor() {
super('notification_subscriptions');
}
}
export default new NotificationSubscriptions();
+19 -11
View File
@@ -1,14 +1,22 @@
/* global axios */
import ApiClient from './ApiClient';
import endPoints from './endPoints';
class ReportsAPI extends ApiClient {
constructor() {
super('reports', { accountScoped: true });
}
export default {
getAccountReports(metric, from, to) {
const { url } = endPoints('reports').account(metric, from, to);
return axios.get(url);
},
getAccountSummary(accountId, from, to) {
const urlData = endPoints('reports').accountSummary(accountId, from, to);
return axios.get(urlData.url);
},
};
getAccountReports(metric, since, until) {
return axios.get(`${this.url}/account`, {
params: { metric, since, until },
});
}
getAccountSummary(accountId, since, until) {
return axios.get(`${this.url}/${accountId}/account_summary`, {
params: { since, until },
});
}
}
export default new ReportsAPI();
@@ -0,0 +1,15 @@
import fbChannel from '../../channel/fbChannel';
import ApiClient from '../../ApiClient';
describe('#FBChannel', () => {
it('creates correct instance', () => {
expect(fbChannel).toBeInstanceOf(ApiClient);
expect(fbChannel).toHaveProperty('get');
expect(fbChannel).toHaveProperty('show');
expect(fbChannel).toHaveProperty('create');
expect(fbChannel).toHaveProperty('update');
expect(fbChannel).toHaveProperty('delete');
expect(fbChannel).toHaveProperty('markSeen');
expect(fbChannel).toHaveProperty('toggleTyping');
});
});
@@ -3,7 +3,7 @@ import ApiClient from './ApiClient';
class UserNotificationSettings extends ApiClient {
constructor() {
super('user/notification_settings');
super('notification_settings', { accountScoped: true });
}
update(params) {
+1 -1
View File
@@ -2,7 +2,7 @@ import ApiClient from './ApiClient';
class WebHooks extends ApiClient {
constructor() {
super('account/webhooks');
super('webhooks', { accountScoped: true });
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -49,10 +49,10 @@ $global-font-size: 10px;
$global-width: 100%;
$global-lineheight: 1.5;
$foundation-palette: (primary: $color-woot,
secondary: #777,
success: #13ce66,
warning: #ffc82c,
alert: #ff4949);
secondary: #35c5ff,
success: #44ce4b,
warning: #ffc532,
alert: #ff382d);
$light-gray: #c0ccda;
$medium-gray: #8492a6;
$dark-gray: $color-gray;
@@ -127,7 +127,7 @@ $header-styles: (small: ("h1": ("font-size": 24),
$header-text-rendering: optimizeLegibility;
$small-font-size: 80%;
$header-small-font-color: $medium-gray;
$paragraph-lineheight: 1.6;
$paragraph-lineheight: 1.45;
$paragraph-margin-bottom: 1rem;
$paragraph-text-rendering: optimizeLegibility;
$code-color: $black;
@@ -377,8 +377,8 @@ $form-button-radius: $global-radius;
// 20. Label
// ---------
$label-background: $primary-color;
$label-color: $white;
$label-background: lighten($primary-color, 40%);
$label-color: $primary-color;
$label-color-alt: $black;
$label-palette: $foundation-palette;
$label-font-size: $font-size-micro;

Some files were not shown because too many files have changed in this diff Show More