bot_platform 0.1.0 → 0.2.3

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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.idea/.gitignore +8 -0
  3. data/.idea/bot_platform.iml +317 -0
  4. data/.idea/misc.xml +4 -0
  5. data/.idea/modules.xml +8 -0
  6. data/.idea/vcs.xml +6 -0
  7. data/.ruby-version +1 -0
  8. data/CHANGELOG.md +25 -0
  9. data/Gemfile.lock +1 -1
  10. data/LICENSE +21 -0
  11. data/README.md +21 -9
  12. data/bin/cli +34 -0
  13. data/bot_platform.gemspec +3 -2
  14. data/docs/channels.md +13 -0
  15. data/lib/bot_platform/activity.rb +32 -0
  16. data/lib/bot_platform/adapter.rb +87 -0
  17. data/lib/bot_platform/asserts.rb +69 -0
  18. data/lib/bot_platform/boot.rb +12 -0
  19. data/lib/bot_platform/channels/base.rb +26 -0
  20. data/lib/bot_platform/channels/chatwork.rb +1 -0
  21. data/lib/bot_platform/channels/console.rb +60 -0
  22. data/lib/bot_platform/channels/facebook.rb +1 -0
  23. data/lib/bot_platform/channels/line.rb +1 -0
  24. data/lib/bot_platform/channels/lineworks.rb +102 -0
  25. data/lib/bot_platform/channels/skype.rb +1 -0
  26. data/lib/bot_platform/channels/skype_for_business.rb +1 -0
  27. data/lib/bot_platform/channels/slack.rb +15 -0
  28. data/lib/bot_platform/channels/teams.rb +15 -0
  29. data/lib/bot_platform/channels/web.rb +15 -0
  30. data/lib/bot_platform/channels/wechat.rb +1 -0
  31. data/lib/bot_platform/channels.rb +9 -0
  32. data/lib/bot_platform/cli.rb +97 -0
  33. data/lib/bot_platform/conversation_state.rb +32 -0
  34. data/lib/bot_platform/dialogs/dialog.rb +48 -0
  35. data/lib/bot_platform/dialogs/dialog_context.rb +104 -0
  36. data/lib/bot_platform/dialogs/dialog_instance.rb +15 -0
  37. data/lib/bot_platform/dialogs/dialog_result.rb +22 -0
  38. data/lib/bot_platform/dialogs/dialog_set.rb +38 -0
  39. data/lib/bot_platform/dialogs/dialog_state.rb +11 -0
  40. data/lib/bot_platform/dialogs/prompts/prompt.rb +84 -0
  41. data/lib/bot_platform/dialogs/prompts/prompt_options.rb +12 -0
  42. data/lib/bot_platform/dialogs/prompts/prompt_recognizer_result.rb +16 -0
  43. data/lib/bot_platform/dialogs/prompts/text_prompt.rb +36 -0
  44. data/lib/bot_platform/dialogs.rb +17 -0
  45. data/lib/bot_platform/message_factory.rb +9 -0
  46. data/lib/bot_platform/state/bot_state.rb +21 -0
  47. data/lib/bot_platform/state/conversation_state.rb +17 -0
  48. data/lib/bot_platform/state/user_state.rb +1 -0
  49. data/lib/bot_platform/storage/memory_storage.rb +51 -0
  50. data/lib/bot_platform/storage/mysql_storage.rb +1 -0
  51. data/lib/bot_platform/storage/redis_storage.rb +1 -0
  52. data/lib/bot_platform/storage/storageable.rb +33 -0
  53. data/lib/bot_platform/turn_context.rb +105 -0
  54. data/lib/bot_platform/version.rb +1 -1
  55. data/lib/bot_platform.rb +11 -0
  56. data/samples/dialogs/dialog_simple.rb +29 -0
  57. data/samples/echo.rb +5 -0
  58. metadata +56 -5
data/bin/cli ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "bot_platform"
6
+ require_relative '../lib/bot_platform/cli'
7
+
8
+ def usage
9
+ <<-USAGE
10
+ usage: bin/cli [bot_ruby_file]
11
+ e.g. bin/cli ./samples/dialog_simple.rb
12
+ USAGE
13
+ end
14
+
15
+ def camelize(str)
16
+ str.split('_').map(&:capitalize).join
17
+ end
18
+
19
+ if ARGV[0].nil?
20
+ puts usage
21
+ exit(0)
22
+ else
23
+ unless File.exist?(ARGV[0])
24
+ puts usage
25
+ exit(-1)
26
+ end
27
+ require ARGV[0]
28
+ base_name = File.basename(ARGV[0],'.rb')
29
+ instance = Object.const_get(camelize(base_name)).new
30
+ end
31
+
32
+
33
+ cli = BotPlatform::Cli.new instance
34
+ cli.run
data/bot_platform.gemspec CHANGED
@@ -5,11 +5,12 @@ require_relative "lib/bot_platform/version"
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "bot_platform"
7
7
  spec.version = BotPlatform::VERSION
8
+ spec.licenses = ['MIT']
8
9
  spec.authors = ["Ningfeng Yang"]
9
10
  spec.email = ["nf.yang@lifevar.com"]
10
11
 
11
12
  spec.summary = "A ruby-based bot platform"
12
- spec.description = "simply write bot codes"
13
+ spec.description = "One bot, multiple channels supported"
13
14
  spec.homepage = "https://github.com/lifevar/bot-platform"
14
15
  spec.required_ruby_version = ">= 3.0.2"
15
16
 
@@ -17,7 +18,7 @@ Gem::Specification.new do |spec|
17
18
 
18
19
  spec.metadata["homepage_uri"] = spec.homepage
19
20
  spec.metadata["source_code_uri"] = "https://github.com/lifevar/bot-platform"
20
- spec.metadata["changelog_uri"] = "https://github.com/lifevar.com/bot-platform/CHANGELOG.md"
21
+ spec.metadata["changelog_uri"] = "https://github.com/Lifevar/bot-platform/blob/main/CHANGELOG.md"
21
22
 
22
23
  # Specify which files should be added to the gem when it is released.
23
24
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
data/docs/channels.md ADDED
@@ -0,0 +1,13 @@
1
+ ## Current Supported Channels
2
+
3
+ ### Pahse 1 (developing)
4
+ - Console
5
+ - Line Works
6
+
7
+ ### Phase 2 (Oct. 2021)
8
+ - Web
9
+ - Slack
10
+ - Teams
11
+ - Line
12
+ - WeChat
13
+ - ChatWork
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BotPlatform
4
+ class Activity
5
+ attr_reader :type, :text, :content, :options, :from, :prefix, :preview_url, :resource_id, :resource_url, :channel_id
6
+ attr_accessor :to
7
+ TYPES = {
8
+ typing: 1,
9
+ message: 2,
10
+ image: 3,
11
+ confirm: 4,
12
+ options: 5,
13
+ carousel: 6,
14
+ command: 7,
15
+ card: 9
16
+ }.freeze
17
+
18
+ def initialize(type, opts={})
19
+ @type = type
20
+ @resource_id = opts[:resource_id]
21
+ @content = opts[:content]
22
+ @options = opts[:options]
23
+ @prefix = opts[:prefix]
24
+ @text = opts[:text]
25
+ @preview_url = opts[:preview_url]
26
+ @resource_url = opts[:resource_url]
27
+ @from = opts[:from]
28
+ @to = opts[:to]
29
+ @channel_id = opts[:channel_id]
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+ require_relative 'asserts'
5
+ module BotPlatform
6
+ class Adapter
7
+ include Asserts
8
+ include Singleton
9
+
10
+
11
+ attr_reader :channels, :channel_map
12
+
13
+ def initialize
14
+ @channels = []
15
+ @channel_map = {}
16
+ channels = ENV['BOT_CHANNELS']
17
+ raise 'No BOT_CHANNELS found in environment variables.' if channels.nil? || channels.length == 0
18
+ channels.split(',').each do |ch|
19
+ channel = Object.const_get("BotPlatform").const_get("Channels").const_get(ch.capitalize).new
20
+ @channels << channel
21
+ @channel_map[ch] = channel
22
+ end
23
+
24
+ end
25
+
26
+ def send_activities(turn_context, activities)
27
+ assert_context_is_not_null turn_context
28
+ assert_activity_is_not_null activities
29
+ assert_activity_is_not_null activities[0]
30
+
31
+ activities.each do |activity|
32
+ @channel_map[turn_context.channel_id].send_activity(activity)
33
+ end
34
+ end
35
+
36
+ def update_activity(turn_context, activity, cancel_token)
37
+
38
+ end
39
+
40
+ def delete_activity(turn_context, conversation, cancel_token)
41
+ end
42
+
43
+ def continue_conversation(bot_id, bot_cb_handler)
44
+ end
45
+
46
+ def create_conversation(bot_id, channel_id, service_url, audience, bot_callback_handler, cancel_token)
47
+ end
48
+
49
+ def process_activity_async(claims_identity, activity, bot_callback_handler, cancel_token)
50
+ end
51
+
52
+ def process_activity(headers, body, &block)
53
+ channel = nil
54
+ raise 'No channel registered.' if @channels.nil? || @channels.length==0
55
+ @channels.each do |ch|
56
+ if ch.match_request(headers, body)
57
+ channel = ch
58
+ break
59
+ end
60
+
61
+ end
62
+ raise 'No channel found' if channel.nil?
63
+
64
+ activity = channel.parse_incoming_to_activity(headers, body)
65
+
66
+ context = BotPlatform::TurnContext.new self, activity
67
+ block.call(context)
68
+ end
69
+
70
+ def run_pipeline_async(turn_context, callback, cancel_token)
71
+ assert_context_is_not_null(turn_context)
72
+
73
+ if turn_context.activity != nil
74
+ # set locale
75
+ # run middleware
76
+ # rescue custom onTurnError
77
+ else
78
+ unless callback.nil?
79
+ # proactive
80
+ callback(turn_context, cancel_token)
81
+ end
82
+ end
83
+ end
84
+
85
+
86
+ end
87
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BotPlatform
4
+ module Asserts
5
+ def assert_is_not_empty(param)
6
+ raise "#{param.name} is empty" if param.nil?
7
+ end
8
+
9
+ def assert_dialog_id_is_valid(dialog_id)
10
+ raise "dialog_id is not valid" unless (dialog_id.is_a? String) && !dialog_id.empty?
11
+ end
12
+
13
+ def assert_dialog_context_is_valid(ctx)
14
+ raise "dialog context is not valid" unless !ctx.nil? && (ctx.is_a? BotPlatform::Dialogs::DialogContext)
15
+ end
16
+
17
+ def assert_prompt_options_is_valid(options)
18
+ raise "prompt options is not valid" unless !options.nil? && (options.is_a? BotPlatform::Dialogs::Prompts::PromptOptions)
19
+ end
20
+
21
+ def assert_dialog_set_is_valid(dialogs)
22
+ raise "dialogs is not valid" if dialogs.nil? || !(dialogs.is_a? Dialogs::DialogSet)
23
+ end
24
+
25
+ def assert_dialog_state_is_valid(state)
26
+ raise "dialog state is not valid" if state.nil? || !(state.is_a? Dialogs::DialogState)
27
+ end
28
+
29
+ def assert_dialog_is_valid(dialog)
30
+ raise "dialog is not valid" if dialog.nil? || !(dialog.is_a? Dialogs::Dialog)
31
+ end
32
+
33
+ def assert_dialog_is_uniq(hash, id)
34
+ raise "dialog is aready added" if !hash[id.to_sym].nil?
35
+ end
36
+
37
+ def assert_activity_is_not_null(activity)
38
+ raise "activity cannot be null" if activity.nil?
39
+ end
40
+
41
+ def assert_activity_type_is_not_null(type)
42
+ raise "activity type cannot be null" if type.nil?
43
+ end
44
+
45
+ def assert_turn_context_is_valid(turn_context)
46
+ raise "turn context is not valid" if turn_context.nil? || !(turn_context.is_a? TurnContext)
47
+ end
48
+
49
+ def assert_context_is_not_null(turn_context)
50
+ raise "turn context cannot be null" if turn_context.nil?
51
+ end
52
+
53
+ def assert_conversation_reference_is_not_null(conversation_ref)
54
+ raise "conversation reference cannot be null" if conversation_ref.nil?
55
+ end
56
+
57
+ def assert_activity_list_is_not_null(activities)
58
+ raise "activity list cannot be null" if activities.nil?
59
+ end
60
+
61
+ def assert_middleware_is_not_null(middleware)
62
+ raise "middleware cannot be null" if middleware.nil?
63
+ end
64
+
65
+ def assert_middleware_list_is_not_null(middlewares)
66
+ raise "middleware list cannot be null" if middlewares.nil?
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BotPlatform
4
+ class Boot
5
+ def self.set_env
6
+ puts "Set bot environment"
7
+ # initialize channels
8
+ BotPlatform::Adapter.instance
9
+
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BotPlatform
4
+ module Channels
5
+ module Base
6
+ class NeedImplementation < StandardError; end
7
+
8
+ def channel_id
9
+ raise NeedImplementation
10
+ end
11
+
12
+ def send_activity(activity)
13
+ raise NeedImplementation
14
+ end
15
+
16
+ def as_command(activity)
17
+ raise NeedImplementation
18
+ end
19
+
20
+ def match_request(headers,body)
21
+ raise NeedImplementation
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1 @@
1
+ # frozen_string_literal: true
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BotPlatform::Channels
4
+ class Console
5
+ include BotPlatform::Channels::Base
6
+
7
+ def channel_id
8
+ "console"
9
+ end
10
+
11
+ def key
12
+ "X-Bot-Platform-Bot".intern
13
+ end
14
+
15
+ def match_request(headers, body)
16
+ return false if headers.nil?
17
+ return !(headers[key].nil? || headers[key].empty?)
18
+ end
19
+
20
+ def send_activity(activity)
21
+ case activity.type
22
+ when BotPlatform::Activity::TYPES[:message] then
23
+ puts "bot> #{activity.text}"
24
+ when BotPlatform::Activity::TYPES[:carousel] then
25
+ puts "bot> select from the list:"
26
+ content = activity.content
27
+ content[:columns].each_with_index do |col, idx|
28
+ puts "#{idx+1}: #{col[:title]}(#{col[:text]}) [/#{col[:defaultAction][:data]}]"
29
+ end
30
+ when BotPlatform::Activity::TYPES[:options] then
31
+ puts "bot> #{activity.text}"
32
+ activity.options.each_with_index{|opt, idx| puts "#{idx+1}: #{opt} [/#{activity.prefix}-opt-#{idx}]"}
33
+ when BotPlatform::Activity::TYPES[:image] then
34
+ `open -a '/Applications/Google Chrome.app' #{activity.resource_url}`
35
+ else
36
+ puts "bot[debug]> activity.inspect"
37
+ end
38
+ end
39
+
40
+ def parse_incoming_to_activity(headers, body)
41
+ user_id = body[:bot_id] || ""
42
+ room_id = body[:room_id] || ""
43
+ activity = nil
44
+ cmd = as_command(headers, body)
45
+ if cmd
46
+ activity = BotPlatform::Activity.new ::BotPlatform::Activity::TYPES[:command], {from: {user_id: user_id, room_id: room_id}, text: cmd, channel_id: channel_id}
47
+ else
48
+ activity = BotPlatform::Activity.new ::BotPlatform::Activity::TYPES[:message], {from: {user_id: user_id, room_id: room_id}, text: body[:text], channel_id: channel_id}
49
+ end
50
+
51
+ return activity
52
+ end
53
+
54
+ def as_command(headers, body)
55
+ return body[:cmd] if body[:type] == "cmd_back"
56
+ return false
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1 @@
1
+ # frozen_string_literal: true
@@ -0,0 +1 @@
1
+ # frozen_string_literal: true
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BotPlatform::Channels
4
+ class Lineworks
5
+ include Base
6
+
7
+ def initialize
8
+ @queue = []
9
+ @api_uri = URI.parse("https://apis.worksmobile.com/r/#{ENV['LINE_API_ID']}/message/v1/bot/#{ENV['BOT_NO']}/message/push")
10
+ @headers = {
11
+ "Content-type": "application/json",
12
+ "consumerKey": ENV['LINE_SERVER_CONSUMER_KEY'],
13
+ "Authorization": ENV['LINE_SERVER_TOKEN']
14
+ }
15
+ end
16
+
17
+ def channel_id
18
+ "lineworks"
19
+ end
20
+
21
+ def parse_incoming_to_activity(headers, body)
22
+ puts "Headers: #{headers.inspect}"
23
+ puts "body: #{body.inspect}"
24
+ user_id = body["source"]["accountId"] || ""
25
+ room_id = body["source"]["roomId"] || ""
26
+ activity = nil
27
+ if cmd = as_command(headers, body)
28
+ activity = ChatBot::Activity.new ::ChatBot::Activity::TYPES[:command], {from: {user_id: user_id, room_id: room_id}, text: cmd, channel_id: channel_id}
29
+ elsif body["type"] == "message" && body["content"]["type"] == "image"
30
+ activity = ChatBot::Activity.new ::ChatBot::Activity::TYPES[:image], {from: {user_id: user_id, room_id: room_id}, resource_id: body["content"]["resourceId"], channel_id: channel_id}
31
+ else
32
+ activity = ChatBot::Activity.new ::ChatBot::Activity::TYPES[:message], {from: {user_id: user_id, room_id: room_id}, text: body["content"]["text"], channel_id: channel_id}
33
+ end
34
+ puts "parse_incoming_to_activity activity:#{activity.to_json}"
35
+
36
+ return activity
37
+ end
38
+
39
+ def send_activity(activity)
40
+ http = Net::HTTP.new(@api_uri.host, @api_uri.port)
41
+ http.use_ssl = true
42
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
43
+
44
+ content = {}
45
+ if !activity.to[:room_id].blank?
46
+ content["roomId"] = activity.to[:room_id]
47
+ else
48
+ content["accountId"] = activity.to[:user_id]
49
+ end
50
+ case activity.type
51
+ when ChatBot::Activity::TYPES[:message] then
52
+ content["content"] = {
53
+ "type": "text",
54
+ "text": activity.text
55
+ }
56
+ when ChatBot::Activity::TYPES[:image] then
57
+ content["content"] = {
58
+ "type": "image",
59
+ "previewUrl": activity.preview_url,
60
+ "resourceUrl": activity.resource_url,
61
+ }
62
+ when ChatBot::Activity::TYPES[:confirm] then
63
+ content["content"] = {
64
+ "type": "button_template",
65
+ "contentText": activity.text,
66
+ "actions": [
67
+ { "type":"message", "label": "はい", "postback": "#{activity.prefix}-yes"},
68
+ { "type":"message", "label": "いいえ","postback":"#{activity.prefix}-no"}
69
+ ]
70
+ }
71
+ when ChatBot::Activity::TYPES[:options] then
72
+ content["content"] = {
73
+ "type": "button_template",
74
+ "contentText": activity.text,
75
+ "actions": activity.options.map{|opt| {"type":"message", "label":opt, "postback": "#{activity.prefix}-opt-#{opt}"}}
76
+ }
77
+ when ChatBot::Activity::TYPES[:carousel] then
78
+ content["content"] = activity.content
79
+ else
80
+ end
81
+
82
+ http.start do
83
+ req = Net::HTTP::Post.new(@api_uri.path)
84
+ req.initialize_http_header(@headers)
85
+ req.body = content.to_json
86
+ http.request(req)
87
+ end
88
+ end
89
+
90
+ def match_request(headers, body)
91
+ return !headers["X-Works-Signature"].blank?
92
+ end
93
+
94
+ private
95
+
96
+ def as_command(headers, body)
97
+ return body["data"] if body["type"] == "postback"
98
+ return body["content"]["postback"] if body["type"] == "message" && !body["content"]["postback"].blank?
99
+ return false
100
+ end
101
+ end
102
+ end