kogno 1.0.1

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 (67) hide show
  1. checksums.yaml +7 -0
  2. data/bin/kogno +92 -0
  3. data/lib/boot.rb +9 -0
  4. data/lib/core/bin_helpers/messenger_ctl.rb +48 -0
  5. data/lib/core/bin_helpers/scaffolding.rb +203 -0
  6. data/lib/core/bin_helpers/scheduled_messages_ctl.rb +95 -0
  7. data/lib/core/bin_helpers/sequences_ctl.rb +95 -0
  8. data/lib/core/bin_helpers/server_ctl.rb +127 -0
  9. data/lib/core/bin_helpers/telegram_ctl.rb +48 -0
  10. data/lib/core/bin_helpers/webhook_ctl.rb +96 -0
  11. data/lib/core/db.rb +8 -0
  12. data/lib/core/extensions/array.rb +28 -0
  13. data/lib/core/extensions/hash.rb +12 -0
  14. data/lib/core/extensions/logger.rb +70 -0
  15. data/lib/core/extensions/string.rb +6 -0
  16. data/lib/core/extensions/wit.rb +60 -0
  17. data/lib/core/global_methods.rb +67 -0
  18. data/lib/core/helpers/string.rb +105 -0
  19. data/lib/core/lib/base_config.rb +17 -0
  20. data/lib/core/lib/block_params.rb +4 -0
  21. data/lib/core/lib/context.rb +1573 -0
  22. data/lib/core/lib/error_handler.rb +36 -0
  23. data/lib/core/lib/message.rb +182 -0
  24. data/lib/core/lib/messenger/api.rb +281 -0
  25. data/lib/core/lib/messenger/facebook_graph.rb +32 -0
  26. data/lib/core/lib/messenger/message.rb +202 -0
  27. data/lib/core/lib/messenger/notification.rb +351 -0
  28. data/lib/core/lib/messenger/post_comment.rb +104 -0
  29. data/lib/core/lib/messenger/recurring_notification.rb +81 -0
  30. data/lib/core/lib/nlp.rb +191 -0
  31. data/lib/core/lib/notification.rb +371 -0
  32. data/lib/core/lib/spelling.rb +13 -0
  33. data/lib/core/lib/telegram/api.rb +197 -0
  34. data/lib/core/lib/telegram/chat_activity.rb +111 -0
  35. data/lib/core/lib/telegram/inline_query.rb +112 -0
  36. data/lib/core/lib/telegram/message.rb +327 -0
  37. data/lib/core/lib/telegram/notification.rb +507 -0
  38. data/lib/core/lib/whatsapp/api.rb +153 -0
  39. data/lib/core/lib/whatsapp/message.rb +132 -0
  40. data/lib/core/lib/whatsapp/notification.rb +206 -0
  41. data/lib/core/lib/whatsapp/status_message.rb +58 -0
  42. data/lib/core/loaders/config_files.rb +15 -0
  43. data/lib/core/models/chat_log.rb +4 -0
  44. data/lib/core/models/long_payload.rb +25 -0
  45. data/lib/core/models/matched_message.rb +5 -0
  46. data/lib/core/models/messenger_recurring_notification.rb +16 -0
  47. data/lib/core/models/scheduled_message.rb +40 -0
  48. data/lib/core/models/sequence.rb +29 -0
  49. data/lib/core/models/telegram_chat_group.rb +26 -0
  50. data/lib/core/models/user.rb +285 -0
  51. data/lib/core/web/webhook.rb +198 -0
  52. data/lib/kogno.rb +130 -0
  53. data/scaffolding/new_project/Gemfile +3 -0
  54. data/scaffolding/new_project/application.rb +5 -0
  55. data/scaffolding/new_project/bot/contexts/main_context.rb +10 -0
  56. data/scaffolding/new_project/bot/conversation.rb +14 -0
  57. data/scaffolding/new_project/bot/models/user.rb +3 -0
  58. data/scaffolding/new_project/config/application.rb +28 -0
  59. data/scaffolding/new_project/config/database.yml +8 -0
  60. data/scaffolding/new_project/config/locales/en.yml +4 -0
  61. data/scaffolding/new_project/config/locales/es.yml +3 -0
  62. data/scaffolding/new_project/config/nlp.rb +23 -0
  63. data/scaffolding/new_project/config/platforms/messenger.rb +74 -0
  64. data/scaffolding/new_project/config/platforms/telegram.rb +45 -0
  65. data/scaffolding/new_project/config/platforms/whatsapp.rb +13 -0
  66. data/scaffolding/new_project/web/routes.rb +10 -0
  67. metadata +220 -0
@@ -0,0 +1,132 @@
1
+ module Kogno
2
+ module WhatsApp
3
+ class Message < Kogno::Message
4
+
5
+ @overwritten_payload = nil
6
+
7
+ def initialize(data, type=nil)
8
+ @data = data
9
+ @type = type
10
+ end
11
+
12
+ def type
13
+ @type
14
+ end
15
+
16
+ def platform
17
+ :whatsapp
18
+ end
19
+
20
+ def metadata
21
+ @data[:metadata]
22
+ end
23
+
24
+ def sender_id
25
+ return @data[:contacts][0][:wa_id]
26
+ end
27
+
28
+ def sender_name
29
+ return @data[:contacts][0][:profile][:name] rescue ""
30
+ end
31
+
32
+ def attachments
33
+ message = @data[:messages][0]
34
+ a = nil
35
+ a = message[:audio]
36
+ a = message[:document] if a.nil?
37
+ a = message[:image] if a.nil?
38
+ a = message[:video] if a.nil?
39
+ return a
40
+ end
41
+
42
+
43
+ def raw_message
44
+ t = @data[:messages][0][:text][:body] rescue nil
45
+ unless t.nil?
46
+ return({
47
+ :type => :text,
48
+ :value => t
49
+ })
50
+ end
51
+ end
52
+
53
+ def raw_payload
54
+ payload = @data[:messages][0][:button][:payload] rescue nil
55
+ payload = (@data[:messages][0][:interactive][:button_reply][:id] rescue nil) if payload.nil?
56
+ payload = (@data[:messages][0][:interactive][:list_reply][:id] rescue nil) if payload.nil?
57
+ payload = @overwritten_payload if payload.nil?
58
+ return(payload)
59
+ end
60
+
61
+ def text
62
+ return self.raw_message[:value].to_s rescue ""
63
+ end
64
+
65
+
66
+ def handle_event(debug=false)
67
+
68
+ begin
69
+
70
+ user = User.find_or_create_by_psid(self.sender_id, :whatsapp)
71
+ user.get_session_vars
72
+
73
+ self.set_nlp(user.locale)
74
+
75
+ I18n.locale = user.locale unless user.locale.nil?
76
+
77
+ self.set_nlp(I18n.locale)
78
+
79
+ unless user.vars[:nlp_context_ref].nil?
80
+ self.nlp.set_context_reference(user.vars[:nlp_context_ref])
81
+ user.vars.delete(:nlp_context_ref) # context references will only be used once
82
+ end
83
+
84
+ notification = Notification.new(user,self)
85
+
86
+ self.log_message_info(user)
87
+
88
+ context = get_context(user,self,notification)
89
+
90
+ return({msg: self, user: user, notification: notification, context: context}) if debug
91
+
92
+ unless empty_thread_from_ad?
93
+
94
+ called_action = context.run
95
+ if Kogno::Application.config.store_log_in_database
96
+ message_log_id = user.log_message(self).id
97
+ else
98
+ message_chat_log_id = 0
99
+ end
100
+
101
+ notification.send
102
+
103
+ response_log_id = 0
104
+ if Kogno::Application.config.store_log_in_database
105
+ response_log = user.log_response(notification)
106
+ response_log_id = response_log.id unless response_log.nil?
107
+ end
108
+
109
+ # user.set_last_usage
110
+ user.save_session_vars
111
+ context.handle_message_from_memory
112
+
113
+ else
114
+ context.run_class_callbacks_only
115
+ user.save_session_vars
116
+ user.log_message(self) if Kogno::Application.config.store_log_in_database
117
+ end
118
+ logger.write "- Current user context: #{user.context}", :blue unless user.context.nil?
119
+
120
+ rescue StandardError => e
121
+ error_token = Digest::MD5.hexdigest("#{Time.now}#{rand(1000)}") # This helps to identify the error that arrives to Slack in order to search it in logs/http.log
122
+ logger.write e.message, :red
123
+ logger.write "Error Token: #{error_token}", :red
124
+ logger.write "Backtrace:\n\t#{e.backtrace.join("\n\t")}", :red
125
+ ErrorHandler.notify_by_slack(Kogno::Application.config.app_name,e, error_token) if Kogno::Application.config.error_notifier.slack[:enable] rescue false
126
+ end
127
+
128
+ end
129
+
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,206 @@
1
+ module Kogno
2
+ module WhatsApp
3
+ class Notification < Kogno::Notification
4
+
5
+ def send(recipient_id=nil,delete=true)
6
+ recipient_id = @recipient.psid if recipient_id.nil?
7
+ page_id = @recipient.page_id if page_id.nil?
8
+ messages = @before_messages+@messages+@after_messages
9
+ @message_log = messages
10
+ messages.each do |message|
11
+ if message[:type].to_sym == :action
12
+ sleep(message[:value][:duration]) if message[:value][:action].to_sym == :typing_on
13
+ else
14
+ message = self.replace_place_holders(message[:value])
15
+ @response_log << Api::send(recipient_id,message)
16
+ end
17
+ end
18
+ self.delete_messages() if delete
19
+ @recipient.mark_last_message_as_unread unless @recipient.nil?
20
+ end
21
+
22
+ def text(text, extra_params={})
23
+ extra_params = {preview_url: false}.merge(extra_params)
24
+ params = {
25
+ type: :text,
26
+ text: {
27
+ body: text
28
+ }.merge(extra_params)
29
+ }
30
+ self.push_message(params, :message)
31
+ end
32
+
33
+
34
+ def typing_on(duration)
35
+ self.push_message({:action => :typing_on, :duration => duration}, :action)
36
+ end
37
+
38
+ def whatsapp_template(name, components=[], lang="en_US")
39
+ self.push_message(Api::template_message(name, components, lang), :message)
40
+ end
41
+
42
+ def button(text, replies, extra_settings={})
43
+ replies = [replies] if replies.class == Hash
44
+ settings = {typed_postbacks: Kogno::Application.config.typed_postbacks}.merge(extra_settings) # defaults
45
+ replies = replies.replace_keys({payload: :id})
46
+ buttons = replies.map do |reply|
47
+ {
48
+ type: :reply,
49
+ reply: reply
50
+ }
51
+ end
52
+
53
+ set_typed_postbacks(replies.map{|button|
54
+ [button[:title].to_payload, button[:id]] unless button[:title].nil?
55
+ }.compact.to_h) if settings[:typed_postbacks]
56
+
57
+ self.push_message(Api::interactive_buttons(text, buttons), :message)
58
+ end
59
+
60
+ def quick_reply(text, replies, extra_settings={})
61
+ self.button(text, replies, extra_settings)
62
+ end
63
+
64
+ def list(params,header={},footer={})
65
+ params[:sections] = params[:sections].map{|s|
66
+ {
67
+ title: s[:title],
68
+ rows: s[:rows].replace_keys({payload: :id})
69
+ }
70
+ }
71
+
72
+ params[:header] = header unless header.empty?
73
+ params[:footer] = footer unless footer.empty?
74
+
75
+ self.push_message(Api::interactive_list(params), :message)
76
+ end
77
+
78
+ def url(params)
79
+ self.raw(
80
+ {
81
+ type: :image,
82
+ image: {
83
+ link: params[:image],
84
+ caption: "#{params[:title]}\n#{params[:sub_title]}\n#{params[:url]}"
85
+ }
86
+ }
87
+ )
88
+ end
89
+
90
+ def location(params)
91
+ self.push_message(Api::location(params), :message)
92
+ end
93
+
94
+ def image(params)
95
+ # params = params.replace_keys({url: :link})
96
+ if params[:buttons].nil?
97
+
98
+ self.raw(
99
+ {
100
+ type: :image,
101
+ image: {
102
+ link: params[:url],
103
+ caption: params[:caption]
104
+ }
105
+ }
106
+ )
107
+
108
+ else
109
+
110
+ buttons = params[:buttons]
111
+ buttons = [buttons] if buttons.class == Hash
112
+ buttons = buttons.replace_keys({payload: :id})
113
+ replies = buttons.map do |reply|
114
+ {
115
+ type: :reply,
116
+ reply: reply
117
+ }
118
+ end
119
+ self.raw(
120
+ {
121
+ type: :interactive,
122
+ interactive: {
123
+ type: :button,
124
+ header: {
125
+ type: :image,
126
+ image: {
127
+ link: params[:url]
128
+ }
129
+ },
130
+ body:{
131
+ text: params[:caption]
132
+ },
133
+ action:{
134
+ buttons: replies
135
+ }
136
+ }
137
+ }
138
+ )
139
+
140
+ end
141
+
142
+ end
143
+
144
+ def video(params)
145
+
146
+ if params[:buttons].nil?
147
+
148
+ self.raw(
149
+ {
150
+ type: :video,
151
+ video: {
152
+ link: params[:url],
153
+ caption: params[:caption]
154
+ }
155
+ }
156
+ )
157
+
158
+ else
159
+
160
+ buttons = params[:buttons]
161
+ buttons = [buttons] if buttons.class == Hash
162
+ buttons = buttons.replace_keys({payload: :id})
163
+ replies = buttons.map do |reply|
164
+ {
165
+ type: :reply,
166
+ reply: reply
167
+ }
168
+ end
169
+ self.raw(
170
+ {
171
+ type: :interactive,
172
+ interactive: {
173
+ type: :button,
174
+ header: {
175
+ type: :video,
176
+ video: {
177
+ link: params[:url]
178
+ }
179
+ },
180
+ body:{
181
+ text: params[:caption]
182
+ },
183
+ action:{
184
+ buttons: replies
185
+ }
186
+ }
187
+ }
188
+ )
189
+
190
+ end
191
+
192
+ end
193
+
194
+ def document(params)
195
+ self.push_message(Api::media(:document, params), :message)
196
+ end
197
+
198
+ def contact(params)
199
+ self.push_message(Api::contacts(params), :message)
200
+ end
201
+
202
+
203
+
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,58 @@
1
+ module Kogno
2
+ module WhatsApp
3
+ class StatusMessage < Kogno::Message
4
+
5
+ @overwritten_payload = nil
6
+
7
+ def initialize(data, type=nil)
8
+ @data = data
9
+ @type = type
10
+ end
11
+
12
+ def type
13
+ @type
14
+ end
15
+
16
+ def platform
17
+ :whatsapp
18
+ end
19
+
20
+ def status_raw
21
+ @data[:statuses][0]
22
+ end
23
+
24
+ def status
25
+ self.status_raw[:status]
26
+ end
27
+
28
+ def metadata
29
+ @data[:metadata]
30
+ end
31
+
32
+ def sender_id
33
+ return self.status_raw[:recipient_id]
34
+ end
35
+
36
+
37
+ def handle_event(debug=false)
38
+
39
+ begin
40
+
41
+ user = User.find_or_create_by_psid(self.sender_id, :whatsapp)
42
+ if self.status == "read"
43
+ user.mark_last_message_as_read
44
+ end
45
+
46
+ rescue StandardError => e
47
+ error_token = Digest::MD5.hexdigest("#{Time.now}#{rand(1000)}") # This helps to identify the error that arrives to Slack in order to search it in logs/http.log
48
+ logger.write e.message, :red
49
+ logger.write "Error Token: #{error_token}", :red
50
+ logger.write "Backtrace:\n\t#{e.backtrace.join("\n\t")}", :red
51
+ ErrorHandler.notify_by_slack(Kogno::Application.config.app_name,e, error_token) if Kogno::Application.config.error_notifier.slack[:enable] rescue false
52
+ end
53
+
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,15 @@
1
+ module Kogno
2
+
3
+ Dir[File.join(Application.project_path,'config','*.rb')].each do |required_file|
4
+ require required_file
5
+ end
6
+
7
+ Dir[File.join(Application.project_path,'config','platforms','*.rb')].each do |required_file|
8
+ require required_file
9
+ end
10
+
11
+ Dir[File.join(Application.project_path,'config','initializers','*.rb')].each do |required_file|
12
+ require required_file
13
+ end
14
+
15
+ end
@@ -0,0 +1,4 @@
1
+ class ChatLog < ActiveRecord::Base
2
+ self.table_name = "kogno_chat_logs"
3
+ belongs_to :user
4
+ end
@@ -0,0 +1,25 @@
1
+ class LongPayload < ActiveRecord::Base
2
+ self.table_name = "kogno_long_payloads"
3
+ before_create :generate_token
4
+
5
+ def generate_token
6
+ self.token = Digest::MD5.hexdigest "#{Time.now}#{rand(100000)}"
7
+ end
8
+
9
+ def self.set(data)
10
+ data = data.split(":",2)
11
+ payload = data[0]
12
+ params = data[1]
13
+ payload_param = create(payload: payload, params: params)
14
+ return payload_param.token
15
+ end
16
+
17
+ def self.get(token)
18
+ payload = find_by_token(token)
19
+ unless payload.nil?
20
+ return ("#{payload.payload}:#{payload.params}")
21
+ else
22
+ return {}
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ class MatchedMessage < ActiveRecord::Base
2
+ self.table_name = "kogno_matched_messages"
3
+ belongs_to :user
4
+
5
+ end
@@ -0,0 +1,16 @@
1
+ class MessengerRecurringNotification < ActiveRecord::Base
2
+ self.table_name = "kogno_messenger_recurring_notifications"
3
+ belongs_to :user
4
+
5
+ def data
6
+ {
7
+ token: self.token,
8
+ frecuency: self.frecuency,
9
+ expires_at: self.expires_at,
10
+ token_status: self.token_status,
11
+ timezone: self.timezone,
12
+ status: self.active ? :active : :stopped
13
+ }
14
+ end
15
+
16
+ end
@@ -0,0 +1,40 @@
1
+ class ScheduledMessage < ActiveRecord::Base
2
+ self.table_name = "kogno_scheduled_messages"
3
+ belongs_to :user
4
+
5
+ def self.process_all(sleep=60)
6
+ loop do
7
+ scheduled_messages = where("'#{Time.now.utc}' > send_at").includes(:user).order(:send_at)
8
+ scheduled_messages.each do |scheduled_message|
9
+ if scheduled_message.user.last_usage > Kogno::Application.config.sequences.time_elapsed_after_last_usage
10
+ scheduled_message.execute
11
+ else
12
+ logger.write "User ID #{scheduled_message.user.psid} wrote us recently, let's wait #{Kogno::Application.config.sequences.time_elapsed_after_last_usage - scheduled_message.user.last_usage} seconds before bother him.", :green
13
+ end
14
+ end
15
+ scheduled_messages.destroy_all
16
+ sleep(sleep)
17
+ end
18
+ end
19
+
20
+ def execute
21
+ if self.user.platform == "messenger"
22
+ notification = Kogno::Messenger::Notification.new(self.user)
23
+ elsif self.user.platform == "telegram"
24
+ notification = Kogno::Telegram::Notification.new(self.user)
25
+ elsif self.user.platform == "whatsapp"
26
+ notification = Kogno::WhatsApp::Notification.new(self.user)
27
+ else
28
+ logger.write "Platform '#{self.user.platform} not supported. User ID: #{self.user.id}"
29
+ notification = nil
30
+ end
31
+ unless notification.nil?
32
+ notification.import_messages(self.messages)
33
+ logger.write "Sending scheduled messages to #{self.user.psid}..", :green
34
+ logger.write_json JSON.parse(self.messages), :bright
35
+ notification.send
36
+ self.user.log_response(notification,true) if Kogno::Application.config.store_log_in_database
37
+ end
38
+ end
39
+
40
+ end
@@ -0,0 +1,29 @@
1
+ class Sequence < ActiveRecord::Base
2
+ self.table_name = "kogno_sequences"
3
+ belongs_to :user
4
+ before_create :set_last_hit_at
5
+
6
+ def set_last_hit_at
7
+ self.last_hit_at = Time.now.utc
8
+ end
9
+
10
+ def route
11
+ "#{context}.#{stage}"
12
+ end
13
+
14
+
15
+ def self.process_all(sleep=60)
16
+ loop do
17
+ actions = where("'#{Time.now.utc}' > execution_time or execution_time is null").includes(:user).order(:execution_time)
18
+ actions.each do |action|
19
+ if action.user.last_usage > Kogno::Application.config.sequences.time_elapsed_after_last_usage
20
+ Kogno::Context.run_sequence(action)
21
+ else
22
+ logger.write "User ID #{action.user.psid} wrote us recently, let's wait #{Kogno::Application.config.sequences.time_elapsed_after_last_usage - action.user.last_usage} seconds before bother him.", :green
23
+ end
24
+ end
25
+ sleep(sleep)
26
+ end
27
+ end
28
+
29
+ end
@@ -0,0 +1,26 @@
1
+ class TelegramChatGroup < ActiveRecord::Base
2
+ self.table_name = "kogno_telegram_chat_groups"
3
+ belongs_to :user, foreign_key: :inviter_user_id
4
+
5
+ def type
6
+ self.kind.to_sym
7
+ end
8
+
9
+ def self.find_or_create(args)
10
+ chat = find_by_chat_id(args[:chat_id])
11
+ if chat.nil?
12
+ chat = create(args)
13
+ else
14
+ chat.update(args)
15
+ end
16
+ return chat
17
+ end
18
+
19
+ def notification
20
+ if @notification.nil?
21
+ @notification = Kogno::Telegram::Notification.new(self)
22
+ end
23
+ @notification
24
+ end
25
+
26
+ end