hg 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []