customer_service_im 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +18 -0
- data/app/channels/application_cable/channel.rb +4 -0
- data/app/channels/application_cable/connection.rb +21 -0
- data/app/channels/concerns/customer_service_im/connection.rb +26 -0
- data/app/channels/customer_service_im/conversations_channel.rb +12 -0
- data/app/channels/customer_service_im/messages_channel.rb +15 -0
- data/app/controllers/customer_service_im/application_controller.rb +49 -0
- data/app/controllers/customer_service_im/conversations_controller.rb +34 -0
- data/app/controllers/customer_service_im/messages_controller.rb +65 -0
- data/app/jobs/customer_service_im/application_job.rb +4 -0
- data/app/mailers/customer_service_im/application_mailer.rb +6 -0
- data/app/models/customer_service_im/application_record.rb +5 -0
- data/app/models/customer_service_im/concerns/customer.rb +75 -0
- data/app/models/customer_service_im/conversation.rb +49 -0
- data/app/models/customer_service_im/message.rb +103 -0
- data/app/models/customer_service_im/staff.rb +63 -0
- data/app/views/customer_service_im/conversations/_conversation.json.jbuilder +19 -0
- data/app/views/customer_service_im/conversations/_message.json.jbuilder +3 -0
- data/app/views/customer_service_im/conversations/index.json.jbuilder +5 -0
- data/app/views/customer_service_im/conversations/messages.json.jbuilder +3 -0
- data/app/views/customer_service_im/messages/_message.json.jbuilder +14 -0
- data/app/views/customer_service_im/messages/index.json.jbuilder +1 -0
- data/config/cable.yml +10 -0
- data/config/initializers/tx_asr.rb +28 -0
- data/config/routes.rb +23 -0
- data/db/migrate/20210927065745_create_customer_service_im_staffs.rb +10 -0
- data/db/migrate/20210927074031_create_customer_service_im_conversations.rb +12 -0
- data/db/migrate/20210927074557_create_customer_service_im_messages.rb +13 -0
- data/db/migrate/20210928084420_add_state_to_customer_service_im_messages.rb +5 -0
- data/db/migrate/20210928085514_add_last_messaged_on_to_customer_service_im_conversations.rb +5 -0
- data/db/migrate/20210930025215_add_unread_message_count_to_customer_service_im_conversations.rb +6 -0
- data/lib/customer_service_im/core_ext.rb +11 -0
- data/lib/customer_service_im/engine.rb +6 -0
- data/lib/customer_service_im/version.rb +3 -0
- data/lib/customer_service_im.rb +18 -0
- data/lib/generators/customer_service_im/install/USAGE +8 -0
- data/lib/generators/customer_service_im/install/install_generator.rb +7 -0
- data/lib/generators/customer_service_im/install/templates/initializer.rb +6 -0
- data/lib/tasks/customer_service_im_tasks.rake +4 -0
- data/lib/tsr_recognize_error.rb +2 -0
- metadata +178 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: badd62e6ced06b0e47fde103ffd45abfcf5861ee90882333d77bbda79707138f
|
4
|
+
data.tar.gz: 34b68749a124182c75ac86c8970ef695e1b3f0ff685f25c6dce0f66d02bb7924
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e0f45825d365993e55b5180d90bbc77d738f2d1156d7b876755142f10730c605b9fb55a7b891441df5ff8d9bc4ea40d5f9d914c298fee2ed8043921cb920ed4b
|
7
|
+
data.tar.gz: 83df77a1cafe32031d7581b1e62eaee6cb741eaf9feb2a51a66d1465147d1ecc78f3bd4c2e8ac6ac370d6aba9174461904e6f5cdcd6a6106889c67bf561ef844
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2021 ian
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# CustomerServiceIm
|
2
|
+
Short description and motivation.
|
3
|
+
|
4
|
+
## Usage
|
5
|
+
How to use my plugin.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'customer_service_im'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
```bash
|
16
|
+
$ bundle
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
```bash
|
21
|
+
$ gem install customer_service_im
|
22
|
+
```
|
23
|
+
|
24
|
+
## Contributing
|
25
|
+
Contribution directions go here.
|
26
|
+
|
27
|
+
## License
|
28
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
|
3
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
4
|
+
load "rails/tasks/engine.rake"
|
5
|
+
|
6
|
+
load "rails/tasks/statistics.rake"
|
7
|
+
|
8
|
+
require "bundler/gem_tasks"
|
9
|
+
|
10
|
+
require "rake/testtask"
|
11
|
+
|
12
|
+
Rake::TestTask.new(:test) do |t|
|
13
|
+
t.libs << 'test'
|
14
|
+
t.pattern = 'test/**/*_test.rb'
|
15
|
+
t.verbose = false
|
16
|
+
end
|
17
|
+
|
18
|
+
task default: :test
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ApplicationCable
|
2
|
+
class Connection < ActionCable::Connection::Base
|
3
|
+
identified_by :current_user
|
4
|
+
|
5
|
+
def connect
|
6
|
+
self.current_user = find_user
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def find_user
|
12
|
+
payload, = JWT.decode(request.params[:token], CustomerServiceIm.jwt_secret, true, { algorithm: "HS256" })
|
13
|
+
|
14
|
+
payload["data"]["user_type"].classify.constantize.find(payload["data"]["user_id"])
|
15
|
+
rescue JWT::DecodeError => e
|
16
|
+
logger = Logger.new Rails.root.join("log/jwt.log")
|
17
|
+
logger.error("JWT Decode Error: #{e}")
|
18
|
+
raise e
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
|
2
|
+
module CustomerServiceIm
|
3
|
+
module Connection
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def included(base)
|
7
|
+
base.class_eval do
|
8
|
+
identified_by :current_user
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def connect
|
13
|
+
self.current_user = find_user
|
14
|
+
end
|
15
|
+
|
16
|
+
def find_user
|
17
|
+
payload, = JWT.decode(request.params[:token], CustomerServiceIm.jwt_secret, true, { algorithm: "HS256" })
|
18
|
+
|
19
|
+
payload["data"]["user_type"].classify.constantize.find(payload["data"]["user_id"])
|
20
|
+
rescue JWT::DecodeError => e
|
21
|
+
logger = Logger.new Rails.root.join("log/jwt.log")
|
22
|
+
logger.error("JWT Decode Error: #{e}")
|
23
|
+
raise e
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module CustomerServiceIm
|
2
|
+
class ConversationsChannel < ApplicationCable::Channel
|
3
|
+
def subscribed
|
4
|
+
# stream_from "some_channel"
|
5
|
+
stream_from "customer_service_im/conversations_channel"
|
6
|
+
end
|
7
|
+
|
8
|
+
def unsubscribed
|
9
|
+
# Any cleanup needed when channel is unsubscribed
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module CustomerServiceIm
|
2
|
+
class MessagesChannel < ApplicationCable::Channel
|
3
|
+
def subscribed
|
4
|
+
# stream_from "some_channel"
|
5
|
+
current_user.online!
|
6
|
+
|
7
|
+
conversation = Conversation.find(params[:conversation_id])
|
8
|
+
stream_for conversation
|
9
|
+
end
|
10
|
+
|
11
|
+
def unsubscribed
|
12
|
+
# Any cleanup needed when channel is unsubscribed
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module CustomerServiceIm
|
2
|
+
class ApplicationController < ActionController::API
|
3
|
+
include ActionController::ImplicitRender
|
4
|
+
|
5
|
+
def render_500(errors: nil)
|
6
|
+
render json: { errors: errors }, status: :internal_server_error
|
7
|
+
end
|
8
|
+
|
9
|
+
def render_422(errors: nil)
|
10
|
+
render json: { errors: errors }, status: :unprocessable_entity
|
11
|
+
end
|
12
|
+
|
13
|
+
def render_200(data)
|
14
|
+
render json: data
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def token_user(user)
|
20
|
+
payload = { user_type: user.class.name, user_id: user.id }
|
21
|
+
JWT.encode(payload, hmac_secret, 'HS256')
|
22
|
+
end
|
23
|
+
|
24
|
+
def hmac_secret
|
25
|
+
CustomerServiceIm.jwt_secret
|
26
|
+
end
|
27
|
+
|
28
|
+
def client_has_valid_token?
|
29
|
+
!!logged_user
|
30
|
+
end
|
31
|
+
|
32
|
+
def logged_user
|
33
|
+
begin
|
34
|
+
token = request.headers[:token]
|
35
|
+
payload, = JWT.decode(token, hmac_secret, true, { algorithm: 'HS256' })
|
36
|
+
rescue JWT::DecodeError => e
|
37
|
+
logger = Logger.new Rails.root.join("log/jwt.log")
|
38
|
+
logger.error("JWT Decode Error: #{e}")
|
39
|
+
raise e
|
40
|
+
end
|
41
|
+
|
42
|
+
payload["data"]["user_type"].classify.constantize.find(payload["data"]["user_id"])
|
43
|
+
end
|
44
|
+
|
45
|
+
def authenticate_user!
|
46
|
+
render json: { error: 'Unauthorized' }, status: :unauthorized if !client_has_valid_token?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
|
2
|
+
module CustomerServiceIm
|
3
|
+
class ConversationsController < ApplicationController
|
4
|
+
before_action :set_conversation, only: [:messages, :all_messages_read]
|
5
|
+
|
6
|
+
def index
|
7
|
+
@conversations = Conversation.order('last_messaged_on desc')
|
8
|
+
@total_count = @conversations.size
|
9
|
+
end
|
10
|
+
|
11
|
+
def messages
|
12
|
+
@messages = @conversation.messages
|
13
|
+
.order('created_at desc')
|
14
|
+
.id_less_then(params[:min_id])
|
15
|
+
.tap do |records|
|
16
|
+
@total_count = records.size
|
17
|
+
end
|
18
|
+
.page(params[:page] || 1)
|
19
|
+
.per(params[:per] || 8)
|
20
|
+
.reverse
|
21
|
+
end
|
22
|
+
|
23
|
+
def all_messages_read
|
24
|
+
@conversation.all_messages_read!
|
25
|
+
head :no_content
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def set_conversation
|
31
|
+
@conversation = Conversation.find(params[:id])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module CustomerServiceIm
|
2
|
+
class MessagesController < ApplicationController
|
3
|
+
before_action :set_conversation, only: [ :index ]
|
4
|
+
before_action :set_message, only: [ :receive ]
|
5
|
+
|
6
|
+
def index
|
7
|
+
@messages = @conversation.messages.order("created_at desc")
|
8
|
+
end
|
9
|
+
|
10
|
+
def create
|
11
|
+
@message = Message.new(message_params.merge( user: logged_user, receiver_type: "Staff" ))
|
12
|
+
|
13
|
+
if @message.save
|
14
|
+
|
15
|
+
render_200 message: @message
|
16
|
+
else
|
17
|
+
render_422 errors: @message.errors
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def reply
|
22
|
+
@message = Message.new(message_params.merge( staff: logged_user, receiver_type: "User" ))
|
23
|
+
|
24
|
+
if @message.save
|
25
|
+
render_200 message: @message
|
26
|
+
else
|
27
|
+
render_422 errors: @message.errors
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def receive
|
32
|
+
if @message.received!
|
33
|
+
render_200 message: @message
|
34
|
+
else
|
35
|
+
render_422 @message.errors
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def read
|
40
|
+
if @message.read!
|
41
|
+
render_200 message: @message
|
42
|
+
else
|
43
|
+
render_422 errors: @message.errors
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def message_params
|
50
|
+
params.require(:message).permit(
|
51
|
+
:conversation_id,
|
52
|
+
:content,
|
53
|
+
:attachment
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
def set_conversation
|
58
|
+
@conversation = Conversation.find(message_params[:conversation_id])
|
59
|
+
end
|
60
|
+
|
61
|
+
def set_message
|
62
|
+
@message = Message.find(params[:id])
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
|
2
|
+
module CustomerServiceIm::Concerns::Customer
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
def self.included(klass)
|
6
|
+
klass.class_eval do
|
7
|
+
has_many :conversations, class_name: "CustomerServiceIm::Conversation"
|
8
|
+
has_many :messages, class_name: "CustomerServiceIm::Message"
|
9
|
+
end
|
10
|
+
|
11
|
+
klass.extend ClassMethods
|
12
|
+
end
|
13
|
+
|
14
|
+
def join_conversation(conversation)
|
15
|
+
conversation.update(user: self)
|
16
|
+
end
|
17
|
+
|
18
|
+
def online!
|
19
|
+
redis.sadd online_customers_redis_key, as_redis_record
|
20
|
+
end
|
21
|
+
|
22
|
+
def offline!
|
23
|
+
redis.sadd online_customers_redis_key, as_redis_record
|
24
|
+
end
|
25
|
+
|
26
|
+
# example "User:1"
|
27
|
+
def as_redis_record
|
28
|
+
"#{self.class.name}/#{self.id}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def online?
|
32
|
+
id.in? self.class.online_customers_ids
|
33
|
+
end
|
34
|
+
|
35
|
+
def offline?
|
36
|
+
!online?
|
37
|
+
end
|
38
|
+
|
39
|
+
def send_message(content, attachment)
|
40
|
+
messages.create(
|
41
|
+
conversation: conversation,
|
42
|
+
user: self
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
def online_customers_redis_key
|
47
|
+
self.class.online_customers_redis_key
|
48
|
+
end
|
49
|
+
|
50
|
+
def redis
|
51
|
+
self.class.redis
|
52
|
+
end
|
53
|
+
|
54
|
+
module ClassMethods
|
55
|
+
def online_customers
|
56
|
+
where(id: online_customers_ids)
|
57
|
+
end
|
58
|
+
|
59
|
+
def online_customers_ids
|
60
|
+
online_customer_list_in_redis&.map{|str| str.split("/").second.to_i}
|
61
|
+
end
|
62
|
+
|
63
|
+
def redis
|
64
|
+
@redis ||= Redis.current
|
65
|
+
end
|
66
|
+
|
67
|
+
def online_customers_redis_key
|
68
|
+
"#{CustomerServiceIm.appid}_online_customers"
|
69
|
+
end
|
70
|
+
|
71
|
+
def online_customer_list_in_redis
|
72
|
+
redis.smembers online_customers_redis_key
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module CustomerServiceIm
|
2
|
+
class Conversation < ApplicationRecord
|
3
|
+
has_many :messages
|
4
|
+
belongs_to :user, polymorphic: true
|
5
|
+
belongs_to :staff
|
6
|
+
|
7
|
+
validates_presence_of :title
|
8
|
+
|
9
|
+
def last_message
|
10
|
+
messages.last
|
11
|
+
end
|
12
|
+
|
13
|
+
def message_created!(message)
|
14
|
+
with_lock do
|
15
|
+
increment_unread_message_count(message)
|
16
|
+
update_last_messaged_on(message)
|
17
|
+
broadcast_to_channel
|
18
|
+
|
19
|
+
save!
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def all_messages_read!
|
24
|
+
messages.unread.includes(:user).update(state: :read)
|
25
|
+
update(staff_unread_message_count: 0)
|
26
|
+
broadcast_to_channel
|
27
|
+
end
|
28
|
+
|
29
|
+
def broadcast_to_channel
|
30
|
+
conversation_data = JSON.parse(ApplicationController.render partial: "customer_service_im/conversations/conversation", locals: { conversation: self })
|
31
|
+
ActionCable.server.broadcast "customer_service_im/conversations_channel", conversation_data
|
32
|
+
end
|
33
|
+
|
34
|
+
# @param [CustomerServiceIm::Message] message
|
35
|
+
def increment_unread_message_count(message)
|
36
|
+
if message.from_user?
|
37
|
+
increment(:staff_unread_message_count)
|
38
|
+
elsif message.from_staff?
|
39
|
+
increment(:user_unread_message_count)
|
40
|
+
else
|
41
|
+
# never arrived
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def update_last_messaged_on(message)
|
46
|
+
self.last_messaged_on = message.created_at
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module CustomerServiceIm
|
2
|
+
class Message < ApplicationRecord
|
3
|
+
belongs_to :user, polymorphic: true
|
4
|
+
belongs_to :conversation
|
5
|
+
belongs_to :staff, optional: true
|
6
|
+
|
7
|
+
before_validation :set_default_receiver_type, on: :create
|
8
|
+
before_validation :set_user, on: :create
|
9
|
+
|
10
|
+
after_create :broadcast_to_conversation #, if: -> { target_receivable? }
|
11
|
+
after_create :notice_conversation_message_created
|
12
|
+
|
13
|
+
after_create_commit :speech_recognize, if: ->{ from_user? && speech_unrecognized? }
|
14
|
+
|
15
|
+
has_one_attached :attachment
|
16
|
+
|
17
|
+
scope :from_user, -> {
|
18
|
+
where("receiver_type = 'Staff'")
|
19
|
+
}
|
20
|
+
|
21
|
+
scope :from_staff, -> {
|
22
|
+
where("receiver_type = 'User'")
|
23
|
+
}
|
24
|
+
|
25
|
+
scope :id_less_then, ->( id ){
|
26
|
+
where('id < ?', id) if id.present?
|
27
|
+
}
|
28
|
+
|
29
|
+
scope :unread, ->{
|
30
|
+
where.not(state: "read")
|
31
|
+
}
|
32
|
+
|
33
|
+
enum state: {
|
34
|
+
pending: "pending",
|
35
|
+
sent: "sent",
|
36
|
+
received: "received",
|
37
|
+
read: "read"
|
38
|
+
}, _default: "pending"
|
39
|
+
|
40
|
+
def from_user?
|
41
|
+
receiver_type === 'Staff'
|
42
|
+
end
|
43
|
+
|
44
|
+
def from_staff?
|
45
|
+
receiver_type === 'User'
|
46
|
+
end
|
47
|
+
|
48
|
+
def target_receivable?
|
49
|
+
(from_user? && conversation.staff&.online?) || (from_staff? && user&.online?)
|
50
|
+
end
|
51
|
+
|
52
|
+
def broadcast_to_conversation
|
53
|
+
message_data = JSON.parse(ApplicationController.render partial: "customer_service_im/messages/message", locals: { message: self})
|
54
|
+
CustomerServiceIm::MessagesChannel.broadcast_to(conversation, message_data)
|
55
|
+
|
56
|
+
if target_receivable?
|
57
|
+
sent!
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def read!
|
62
|
+
conversation.message_read!(self)
|
63
|
+
super
|
64
|
+
end
|
65
|
+
|
66
|
+
def attachment_url
|
67
|
+
return if !attachment.blob
|
68
|
+
|
69
|
+
Rails.application.routes.url_helpers.rails_blob_url(attachment)
|
70
|
+
end
|
71
|
+
|
72
|
+
def speech_unrecognized?
|
73
|
+
attachment.blob.present? && content.blank?
|
74
|
+
end
|
75
|
+
|
76
|
+
def speech_recognize
|
77
|
+
result = TxAsr::SentenceRecognition.from(attachment_url)
|
78
|
+
|
79
|
+
if result.success?
|
80
|
+
update( content: result.data["Result"].presence || "[空白语音]" )
|
81
|
+
broadcast_to_conversation
|
82
|
+
else
|
83
|
+
logger = Logger.new Rails.root.join("log/speech_recognition.log")
|
84
|
+
logger.error result.errors
|
85
|
+
raise TsrRecognizeError.new(result.errors)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def set_default_receiver_type
|
92
|
+
self.receiver_type ||= "Staff"
|
93
|
+
end
|
94
|
+
|
95
|
+
def notice_conversation_message_created
|
96
|
+
conversation.message_created!(self)
|
97
|
+
end
|
98
|
+
|
99
|
+
def set_user
|
100
|
+
self.user ||= conversation.user
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module CustomerServiceIm
|
2
|
+
class Staff < ApplicationRecord
|
3
|
+
has_many :conversations
|
4
|
+
has_many :messages
|
5
|
+
|
6
|
+
validates_presence_of :name
|
7
|
+
|
8
|
+
def join_conversation(conversation)
|
9
|
+
conversation.update(staff: self)
|
10
|
+
|
11
|
+
conversation.messages.from_user.each do |message|
|
12
|
+
message.update(staff: self)
|
13
|
+
message.broadcast_to_conversation
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.online_staffs
|
18
|
+
where(id: online_staffs_ids)
|
19
|
+
end
|
20
|
+
|
21
|
+
def online!
|
22
|
+
redis.sadd self.class.online_staff_redis_key, as_redis_record
|
23
|
+
end
|
24
|
+
|
25
|
+
def offline!
|
26
|
+
redis.srem self.class.online_staff_redis_key, as_redis_record
|
27
|
+
end
|
28
|
+
|
29
|
+
def online?
|
30
|
+
id.in? self.class.online_staffs_ids
|
31
|
+
end
|
32
|
+
|
33
|
+
def offline?
|
34
|
+
!online?
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def self.online_staffs_ids
|
40
|
+
online_staff_list_in_redis&.map{|str| str.split("/").second.to_i}
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.online_staff_list_in_redis
|
44
|
+
redis.smembers online_staff_redis_key
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.online_staff_redis_key
|
48
|
+
"#{CustomerServiceIm.appid}_online_staffs"
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.redis
|
52
|
+
@redis ||= Redis.current
|
53
|
+
end
|
54
|
+
|
55
|
+
def as_redis_record
|
56
|
+
"#{self.class.name}/#{self.id}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def redis
|
60
|
+
self.class.redis
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
|
2
|
+
json.extract! conversation, *CustomerServiceIm::Conversation.column_names
|
3
|
+
|
4
|
+
json.user conversation.user
|
5
|
+
json.staff conversation.staff
|
6
|
+
|
7
|
+
if conversation.last_message
|
8
|
+
json.last_message do
|
9
|
+
json.extract! conversation.last_message, *CustomerServiceIm::Message.column_names
|
10
|
+
|
11
|
+
created_at = if conversation.last_message.created_at
|
12
|
+
conversation.last_message.created_at.strftime("%H:%M")
|
13
|
+
else
|
14
|
+
conversation.last_message.created_at.strftime("%m.%d")
|
15
|
+
end
|
16
|
+
|
17
|
+
json.created_at created_at
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
|
2
|
+
json.extract! message, *CustomerServiceIm::Message.column_names
|
3
|
+
|
4
|
+
json.user message.user
|
5
|
+
json.staff message.staff
|
6
|
+
json.attachment_url message.attachment_url
|
7
|
+
|
8
|
+
created_at = if message.created_at.today?
|
9
|
+
message.created_at.strftime("%H:%M")
|
10
|
+
else
|
11
|
+
message.created_at.strftime("%m-%d")
|
12
|
+
end
|
13
|
+
|
14
|
+
json.created_at created_at
|
@@ -0,0 +1 @@
|
|
1
|
+
json.array! @messages, partial: "message", as: :message
|
data/config/cable.yml
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
|
2
|
+
TxAsr.setup do |config|
|
3
|
+
|
4
|
+
# TODO 需要将 secret_key 和 secret_id 改为你实际项目中的配置
|
5
|
+
config.secret_key = "JL6amhMkqfDVWGjuoRp1DI8j48wTnunQ"
|
6
|
+
config.secret_id = "AKIDta1B5i49yXpqb5HCqELBk35qJWS7VcS1"
|
7
|
+
config.region = "ap-guangzhou"
|
8
|
+
|
9
|
+
|
10
|
+
# 声音格式: 支持 mp3 和 wav
|
11
|
+
config.voice_format = "mp3"
|
12
|
+
|
13
|
+
# 可在腾讯云后台查看。如果没有新建项目,那就是使用的默认项目,默认项目 id 为 0
|
14
|
+
config.project_id = 0
|
15
|
+
|
16
|
+
# API 版本号,截止 gem 发布前,下面的值为文档给出的值
|
17
|
+
config.version = "2019-06-14"
|
18
|
+
|
19
|
+
# source type —— 语音来源: 可选 0: url, 1: post body
|
20
|
+
config.source_type = 0
|
21
|
+
|
22
|
+
# 过滤语气词: 0 不过滤,1 部分过滤,2 严格过滤
|
23
|
+
config.filter_model = 1
|
24
|
+
|
25
|
+
# 默认声道数: 1 为单声道,2 为双声道,默认为单声道
|
26
|
+
# config.channel_num = 1
|
27
|
+
|
28
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
CustomerServiceIm::Engine.routes.draw do
|
2
|
+
mount ActionCable.server => "/cable"
|
3
|
+
|
4
|
+
resources :conversations, defaults: { format: :json } do
|
5
|
+
member do
|
6
|
+
get :messages
|
7
|
+
put :all_messages_read
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
resources :messages, defaults: { format: :json } do
|
12
|
+
member do
|
13
|
+
put :receive
|
14
|
+
patch :receive
|
15
|
+
put :read
|
16
|
+
end
|
17
|
+
|
18
|
+
collection do
|
19
|
+
post :create
|
20
|
+
post :reply
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class CreateCustomerServiceImConversations < ActiveRecord::Migration[6.1]
|
2
|
+
def change
|
3
|
+
create_table :customer_service_im_conversations do |t|
|
4
|
+
t.string :title
|
5
|
+
t.string :state
|
6
|
+
t.integer :staff_id
|
7
|
+
t.references :user, polymorphic: true, null: false
|
8
|
+
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class CreateCustomerServiceImMessages < ActiveRecord::Migration[6.1]
|
2
|
+
def change
|
3
|
+
create_table :customer_service_im_messages do |t|
|
4
|
+
t.string :content
|
5
|
+
t.string :receiver_type
|
6
|
+
t.references :user, polymorphic: true, null: false
|
7
|
+
t.integer :staff_id
|
8
|
+
t.integer :conversation_id
|
9
|
+
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/db/migrate/20210930025215_add_unread_message_count_to_customer_service_im_conversations.rb
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
class AddUnreadMessageCountToCustomerServiceImConversations < ActiveRecord::Migration[6.1]
|
2
|
+
def change
|
3
|
+
add_column :customer_service_im_conversations, :user_unread_message_count, :integer
|
4
|
+
add_column :customer_service_im_conversations, :staff_unread_message_count, :integer
|
5
|
+
end
|
6
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "customer_service_im/version"
|
2
|
+
require "customer_service_im/engine"
|
3
|
+
require "customer_service_im/core_ext"
|
4
|
+
require 'shoulda'
|
5
|
+
require "jwt"
|
6
|
+
require 'jbuilder'
|
7
|
+
require "tx_asr"
|
8
|
+
|
9
|
+
module CustomerServiceIm
|
10
|
+
# Your code goes here...
|
11
|
+
mattr_accessor :appid, :app_secret, :jwt_secret
|
12
|
+
|
13
|
+
def self.setup
|
14
|
+
if block_given?
|
15
|
+
yield self
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: customer_service_im
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- ian
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-10-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 6.1.4
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 6.1.4.1
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 6.1.4
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 6.1.4.1
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: shoulda
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '4.0'
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '4.0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: jwt
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: kaminari-activerecord
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :runtime
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: jbuilder
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
type: :runtime
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: tx_asr
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
type: :runtime
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
description: Description of CustomerServiceIm.
|
104
|
+
email:
|
105
|
+
- ianlynxk@gmail.com
|
106
|
+
executables: []
|
107
|
+
extensions: []
|
108
|
+
extra_rdoc_files: []
|
109
|
+
files:
|
110
|
+
- MIT-LICENSE
|
111
|
+
- README.md
|
112
|
+
- Rakefile
|
113
|
+
- app/channels/application_cable/channel.rb
|
114
|
+
- app/channels/application_cable/connection.rb
|
115
|
+
- app/channels/concerns/customer_service_im/connection.rb
|
116
|
+
- app/channels/customer_service_im/conversations_channel.rb
|
117
|
+
- app/channels/customer_service_im/messages_channel.rb
|
118
|
+
- app/controllers/customer_service_im/application_controller.rb
|
119
|
+
- app/controllers/customer_service_im/conversations_controller.rb
|
120
|
+
- app/controllers/customer_service_im/messages_controller.rb
|
121
|
+
- app/jobs/customer_service_im/application_job.rb
|
122
|
+
- app/mailers/customer_service_im/application_mailer.rb
|
123
|
+
- app/models/customer_service_im/application_record.rb
|
124
|
+
- app/models/customer_service_im/concerns/customer.rb
|
125
|
+
- app/models/customer_service_im/conversation.rb
|
126
|
+
- app/models/customer_service_im/message.rb
|
127
|
+
- app/models/customer_service_im/staff.rb
|
128
|
+
- app/views/customer_service_im/conversations/_conversation.json.jbuilder
|
129
|
+
- app/views/customer_service_im/conversations/_message.json.jbuilder
|
130
|
+
- app/views/customer_service_im/conversations/index.json.jbuilder
|
131
|
+
- app/views/customer_service_im/conversations/messages.json.jbuilder
|
132
|
+
- app/views/customer_service_im/messages/_message.json.jbuilder
|
133
|
+
- app/views/customer_service_im/messages/index.json.jbuilder
|
134
|
+
- config/cable.yml
|
135
|
+
- config/initializers/tx_asr.rb
|
136
|
+
- config/routes.rb
|
137
|
+
- db/migrate/20210927065745_create_customer_service_im_staffs.rb
|
138
|
+
- db/migrate/20210927074031_create_customer_service_im_conversations.rb
|
139
|
+
- db/migrate/20210927074557_create_customer_service_im_messages.rb
|
140
|
+
- db/migrate/20210928084420_add_state_to_customer_service_im_messages.rb
|
141
|
+
- db/migrate/20210928085514_add_last_messaged_on_to_customer_service_im_conversations.rb
|
142
|
+
- db/migrate/20210930025215_add_unread_message_count_to_customer_service_im_conversations.rb
|
143
|
+
- lib/customer_service_im.rb
|
144
|
+
- lib/customer_service_im/core_ext.rb
|
145
|
+
- lib/customer_service_im/engine.rb
|
146
|
+
- lib/customer_service_im/version.rb
|
147
|
+
- lib/generators/customer_service_im/install/USAGE
|
148
|
+
- lib/generators/customer_service_im/install/install_generator.rb
|
149
|
+
- lib/generators/customer_service_im/install/templates/initializer.rb
|
150
|
+
- lib/tasks/customer_service_im_tasks.rake
|
151
|
+
- lib/tsr_recognize_error.rb
|
152
|
+
homepage: https://github.com/bkyz/customer_service_im
|
153
|
+
licenses:
|
154
|
+
- MIT
|
155
|
+
metadata:
|
156
|
+
homepage_uri: https://github.com/bkyz/customer_service_im
|
157
|
+
source_code_uri: https://github.com/bkyz/customer_service_im
|
158
|
+
changelog_uri: https://github.com/bkyz/customer_service_im/CHANGELOG.md
|
159
|
+
post_install_message:
|
160
|
+
rdoc_options: []
|
161
|
+
require_paths:
|
162
|
+
- lib
|
163
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
164
|
+
requirements:
|
165
|
+
- - ">="
|
166
|
+
- !ruby/object:Gem::Version
|
167
|
+
version: '0'
|
168
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - ">="
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: '0'
|
173
|
+
requirements: []
|
174
|
+
rubygems_version: 3.2.15
|
175
|
+
signing_key:
|
176
|
+
specification_version: 4
|
177
|
+
summary: Summary of CustomerServiceIm.
|
178
|
+
test_files: []
|