hg 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1770164f6ba93c807d497275ee49886066943aae
4
+ data.tar.gz: 03be35378314a8e56f3ca0adba45f2fdd7c5bdfb
5
+ SHA512:
6
+ metadata.gz: b39eabfa70e1bae5b176f2b025f292874c3427320c910e07dbff8b8575fcfcec20a296e9ac57a92b2c5cee4bbb93a832fa99cba62877b85ae0500ad55b835a31
7
+ data.tar.gz: dfee9d3630208bed5eb2c4186c49f6233731e6e032a253ac52dda29d7daf37f7e748ef52d43e9a0ea9286d73c60a01de2a71c2fbce7e530cff736471f086df74
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /.idea/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.13.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in hg.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2016 Voxable
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,41 @@
1
+ # Hg
2
+
3
+ **NB: This gem is still under heavy development, and is likely going to change wildly between releases. You probably shouldn't use it quite yet.**
4
+
5
+ ## Getting Started
6
+
7
+ ### Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'hg'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install hg
22
+
23
+ ### Configuration
24
+
25
+ Create an initializer file within your Rails app at `config/initializers/bot.rb`
26
+
27
+
28
+ ## Usage
29
+
30
+ TODO: Write usage instructions here
31
+
32
+ ## Development
33
+
34
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
35
+
36
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
37
+
38
+ ## Contributing
39
+
40
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/hg.
41
+
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "hg"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,38 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'hg/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'hg'
8
+ spec.version = Hg::VERSION
9
+ spec.authors = ['Matt Buck']
10
+ spec.email = ['matt@voxable.io']
11
+
12
+ spec.summary = %q{A library for building Facebook Messenger bots.}
13
+ spec.homepage = "https://rubygems.org/hg"
14
+
15
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
16
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
17
+ if spec.respond_to?(:metadata)
18
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
19
+ else
20
+ raise "RubyGems 2.0 or newer is required to protect against " \
21
+ "public gem pushes."
22
+ end
23
+
24
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
25
+ f.match(%r{^(test|spec|features)/})
26
+ end
27
+ spec.bindir = 'exe'
28
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ['lib']
30
+
31
+ spec.add_runtime_dependency 'facebook-messenger', '~> 0.11.0'
32
+ spec.add_runtime_dependency 'fuzzy_match', '~> 2.1.0'
33
+ spec.add_runtime_dependency 'rails', '~> 5.0.1'
34
+
35
+ spec.add_development_dependency 'bundler', '~> 1.13'
36
+ spec.add_development_dependency 'rake', '~> 10.0'
37
+ spec.add_development_dependency 'rspec', '~> 3.5'
38
+ end
@@ -0,0 +1,10 @@
1
+ require 'facebook/messenger'
2
+ require 'fuzzy_match'
3
+
4
+ require 'hg/version'
5
+ require 'hg/bot'
6
+ require 'hg/chunk'
7
+
8
+ module Hg
9
+ # Your code goes here...
10
+ end
@@ -0,0 +1,123 @@
1
+ module Hg
2
+ module Bot
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ base.routes = {}
6
+ base.chunks = []
7
+ base.call_to_actions = []
8
+ end
9
+
10
+ module ClassMethods
11
+ attr_accessor :routes
12
+ attr_accessor :chunks
13
+ attr_accessor :default_chunk
14
+ attr_accessor :call_to_actions
15
+ attr_accessor :get_started_content
16
+ attr_accessor :greeting_text
17
+ attr_accessor :image_url_base_portion
18
+
19
+ def init
20
+ initialize_router
21
+ initialize_persistent_menu
22
+ initialize_get_started_button
23
+ initialize_greeting_text
24
+ end
25
+
26
+ def default(chunk)
27
+ @default_chunk = chunk
28
+ end
29
+
30
+ def persistent_menu(&block)
31
+ yield
32
+ end
33
+
34
+ def initialize_persistent_menu
35
+ Facebook::Messenger::Thread.set({
36
+ setting_type: 'call_to_actions',
37
+ thread_state: 'existing_thread',
38
+ call_to_actions: @call_to_actions
39
+ }, access_token: ENV['ACCESS_TOKEN'])
40
+ end
41
+
42
+ def call_to_action(text, options = {})
43
+ call_to_action_content = {
44
+ title: text
45
+ }
46
+
47
+ if options[:to]
48
+ call_to_action_content[:type] = 'postback'
49
+ call_to_action_content[:payload] = options[:to].to_s
50
+ elsif options[:url]
51
+ call_to_action_content[:type] = 'web_url'
52
+ call_to_action_content[:url] = options[:url]
53
+ end
54
+
55
+ @call_to_actions << call_to_action_content
56
+ end
57
+
58
+ def get_started(chunk)
59
+ @get_started_content = {
60
+ setting_type: 'call_to_actions',
61
+ thread_state: 'new_thread',
62
+ call_to_actions: [
63
+ {
64
+ payload: chunk.to_s
65
+ }
66
+ ]
67
+ }
68
+ end
69
+
70
+ def initialize_get_started_button
71
+ Facebook::Messenger::Thread.set @get_started_content, access_token: ENV['ACCESS_TOKEN']
72
+ end
73
+
74
+ def greeting_text(text)
75
+ @greeting_text = text
76
+ end
77
+
78
+ def image_url_base(base)
79
+ @image_url_base_portion = base
80
+ end
81
+
82
+ def initialize_greeting_text
83
+ Facebook::Messenger::Thread.set({
84
+ setting_type: 'greeting',
85
+ greeting: {
86
+ text: @greeting_text
87
+ }
88
+ }, access_token: ENV['ACCESS_TOKEN'])
89
+ end
90
+
91
+ def run_postback_payload(payload, recipient, context)
92
+ # TODO: Shouldn't be constantizing user input. Need a way to sanitize this.
93
+ payload.constantize.new(recipient: recipient, context: context).deliver
94
+ end
95
+
96
+ def initialize_router
97
+ ::Facebook::Messenger::Bot.on :postback do |postback|
98
+ Rails.logger.info 'POSTBACK'
99
+ Rails.logger.info postback.payload
100
+
101
+ run_postback_payload(postback.payload, postback.sender)
102
+ end
103
+
104
+ ::Facebook::Messenger::Bot.on :message do |message|
105
+ Rails.logger.info 'MESSAGE'
106
+ Rails.logger.info message.text
107
+ Rails.logger.info message.quick_reply
108
+
109
+ # Attempt to run a quick reply payload
110
+ if message.quick_reply
111
+ run_postback_payload(message.quick_reply, message.sender)
112
+ # Fall back to fuzzy matching keywords
113
+ elsif fuzzy_match = FuzzyMatch.new(@routes.keys).find(message.text)
114
+ @routes[fuzzy_match].deliver(message.sender)
115
+ # Fallback behavior
116
+ else
117
+ @default_chunk.deliver(message.sender)
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,271 @@
1
+ module Hg
2
+ module Chunk
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ base.prepend Initializer
6
+ base.id = base.to_s
7
+ base.deliverables = []
8
+ base.dynamic = false
9
+ base.add_to_router
10
+ base.add_to_chunks
11
+ base.include_chunks
12
+ end
13
+
14
+ def deliver
15
+ self.class.show_typing(@recipient)
16
+
17
+ Rails.logger.info 'DELIVERABLES'
18
+ Rails.logger.info self.class.deliverables.inspect
19
+
20
+ self.class.deliverables.each do |deliverable|
21
+ # If another chunk, deliver it
22
+ if deliverable.is_a? Class
23
+ deliverable.new(recipient: @recipient, context: @context).deliver
24
+ # If dynamic, then it needs to be evaluated at delivery time. Create a
25
+ # `template` with empty `@deliverables`, then evaluate
26
+ # the dynamic block within it and deliver.
27
+ elsif deliverable.is_a? Proc
28
+ template = self.class.dup
29
+ template.deliverables = []
30
+
31
+ template.class_exec(@recipient, @context, &deliverable)
32
+
33
+ template.new(recipient: @recipient, context: @context).deliver
34
+ # Otherwise, it's just a raw message. Deliver it.
35
+ else
36
+ Facebook::Messenger::Bot.deliver(deliverable.merge(recipient: @recipient), access_token: ENV['ACCESS_TOKEN'])
37
+ end
38
+ end
39
+ end
40
+
41
+ module Initializer
42
+ def initialize(recipient: nil, context: nil)
43
+ @recipient = recipient
44
+ @context = context
45
+ end
46
+ end
47
+
48
+ module ClassMethods
49
+ attr_accessor :id
50
+ attr_accessor :deliverables
51
+ attr_accessor :label
52
+ attr_accessor :recipient
53
+ attr_accessor :context
54
+ attr_accessor :dynamic
55
+
56
+ def bot_class
57
+ self.to_s.split('::').first.constantize
58
+ end
59
+
60
+ def label(text)
61
+ @label = text
62
+ end
63
+
64
+ def add_to_router
65
+ bot_class.routes.merge(@id.to_sym => self)
66
+ end
67
+
68
+ def add_to_chunks
69
+ bot_class.chunks << self
70
+ end
71
+
72
+ def include_chunks
73
+ bot_class.class_eval "include #{bot_class.to_s}::Chunks"
74
+ end
75
+
76
+ def dynamic(&block)
77
+ @dynamic = true
78
+
79
+ @deliverables << block
80
+ end
81
+
82
+ def show_typing(recipient)
83
+ Facebook::Messenger::Bot.deliver({
84
+ recipient: recipient,
85
+ sender_action: 'typing_on'
86
+ }, access_token: ENV['ACCESS_TOKEN'])
87
+ end
88
+
89
+ def keywords(*chunk_keywords)
90
+ chunk_keywords.each do |keyword|
91
+ bot_class.routes[keyword] = self
92
+ end
93
+ end
94
+
95
+ def text(message)
96
+ @deliverables <<
97
+ {
98
+ message: {
99
+ text: message
100
+ }
101
+ }
102
+ end
103
+
104
+ def title(text)
105
+ @card[:title] = text
106
+ end
107
+
108
+ def subtitle(text)
109
+ @card[:subtitle] = text
110
+ end
111
+
112
+ def image_url(path)
113
+ @card[:image_url] = bot_class.instance_variable_get(:@image_url_base_portion) + path
114
+ end
115
+
116
+ def item_url(url)
117
+ @card[:item_url] = url
118
+ end
119
+
120
+ # Build a button template message.
121
+ # See https://developers.facebook.com/docs/messenger-platform/send-api-reference/button-template
122
+ def buttons(&block)
123
+ @card = {}
124
+
125
+ yield
126
+
127
+ deliverable = {
128
+ message: {
129
+ attachment: {
130
+ type: 'template',
131
+ payload: {
132
+ template_type: 'button'
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ # Move buttons to proper location
139
+ deliverable[:message][:attachment][:payload][:buttons] = @card.delete(:buttons)
140
+ deliverable[:message][:attachment][:payload][:text] = @deliverables.pop[:message][:text]
141
+
142
+ @deliverables << deliverable
143
+ end
144
+
145
+ def log_in(url)
146
+ button nil, url: url, type: 'account_link'
147
+ end
148
+
149
+ def button(text, options = {})
150
+ # TODO: text needs a better name
151
+ # If the first argument is a chunk, then make this button a link to that chunk
152
+ if text.is_a? Class
153
+ klass = text
154
+ text = text.instance_variable_get(:@label)
155
+
156
+ button_content = {
157
+ title: text,
158
+ type: 'postback',
159
+ payload: klass.to_s
160
+ }
161
+ else
162
+ button_content = {
163
+ title: text
164
+ }
165
+ end
166
+
167
+ # If a `to` option is present, assume this is a postback link to another chunk.
168
+ if options[:to]
169
+ button_content[:type] = 'postback'
170
+ button_content[:payload] = options[:to].to_s
171
+ # If a different type of button is specified (e.g. "Log in"), then pass
172
+ # through the `type` and `url`.
173
+ elsif options[:type]
174
+ button_content[:type] = options[:type]
175
+
176
+ button_content[:url] = evaluate_option(options[:url])
177
+ # If a `url` option is present, assume this is a webview link button.
178
+ elsif options[:url]
179
+ button_content[:type] = 'web_url'
180
+
181
+ button_content[:url] = evaluate_option(options[:url])
182
+ end
183
+
184
+ # Pass through the `webview_height_ratio` option.
185
+ button_content[:webview_height_ratio] = options[:webview_height_ratio]
186
+
187
+ @card[:buttons] = [] unless @card[:buttons]
188
+
189
+ @card[:buttons] << button_content
190
+ end
191
+
192
+ def quick_replies(*classes)
193
+ classes.each do |klass|
194
+ quick_reply klass.instance_variable_get(:@label), to: klass
195
+ end
196
+ end
197
+
198
+ def quick_reply(title, options = {})
199
+ quick_reply_content = {
200
+ content_type: 'text',
201
+ title: title,
202
+ payload: options[:to].to_s
203
+ }
204
+
205
+ unless @deliverables.last[:message][:quick_replies]
206
+ @deliverables.last[:message][:quick_replies] = []
207
+ end
208
+
209
+ @deliverables.last[:message][:quick_replies] << quick_reply_content
210
+ end
211
+
212
+ def card(&block)
213
+ @card = {}
214
+ yield
215
+ @gallery[:cards] << @card
216
+ end
217
+
218
+ def gallery(&block)
219
+ @gallery = {
220
+ cards: [],
221
+ message: {
222
+ attachment: {
223
+ type: 'template',
224
+ payload: {
225
+ template_type: 'generic',
226
+ elements: []
227
+ }
228
+ }
229
+ }
230
+ }
231
+
232
+ yield
233
+
234
+ @gallery[:message][:attachment][:payload][:elements] = @gallery.delete(:cards)
235
+
236
+ @deliverables << @gallery
237
+ end
238
+
239
+ def image(path)
240
+ @deliverables << {
241
+ message: {
242
+ attachment: {
243
+ type: 'image',
244
+ payload: {
245
+ url: bot_class.instance_variable_get(:@image_url_base_portion) + path
246
+ }
247
+ }
248
+ }
249
+ }
250
+ end
251
+
252
+ def chunk(chunk_class)
253
+ @deliverables << chunk_class
254
+ end
255
+
256
+ private
257
+
258
+ # Take an option, and either call it (if a lambda) or return its value.
259
+ #
260
+ # @param [lambda, String] option Either a lambda to be evaluated, or a value
261
+ # @return [String] The option value
262
+ def evaluate_option(option)
263
+ if option.respond_to?(:call)
264
+ option.call(@context)
265
+ else
266
+ option
267
+ end
268
+ end
269
+ end
270
+ end
271
+ end
@@ -0,0 +1,3 @@
1
+ module Hg
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,142 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hg
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matt Buck
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-01-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: facebook-messenger
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.11.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.11.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: fuzzy_match
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.1.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.1.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 5.0.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 5.0.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.13'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.13'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.5'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.5'
97
+ description:
98
+ email:
99
+ - matt@voxable.io
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - ".rspec"
106
+ - ".travis.yml"
107
+ - Gemfile
108
+ - LICENSE
109
+ - README.md
110
+ - Rakefile
111
+ - bin/console
112
+ - bin/setup
113
+ - hg.gemspec
114
+ - lib/hg.rb
115
+ - lib/hg/bot.rb
116
+ - lib/hg/chunk.rb
117
+ - lib/hg/version.rb
118
+ homepage: https://rubygems.org/hg
119
+ licenses: []
120
+ metadata:
121
+ allowed_push_host: https://rubygems.org
122
+ post_install_message:
123
+ rdoc_options: []
124
+ require_paths:
125
+ - lib
126
+ required_ruby_version: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ required_rubygems_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ requirements: []
137
+ rubyforge_project:
138
+ rubygems_version: 2.5.1
139
+ signing_key:
140
+ specification_version: 4
141
+ summary: A library for building Facebook Messenger bots.
142
+ test_files: []