sinbotra 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0160bf49cb6facdd725ea0765152a989d8402748
4
+ data.tar.gz: 13a92dcadd0501e5a192a577f393e7f19e9030f7
5
+ SHA512:
6
+ metadata.gz: d72e76fb8e09d6e553529bd721b87926fb23c7b63cfc77e597459d2e7167764e9e8795121ce7bcfe8fe6c6b3513e5a4f1370c5e6aec3530ca0b9a7de97b9e55f
7
+ data.tar.gz: 2fe7cdcfcf05e887361b39f8bc219fbfca829044befca7f7d3efe85750d4b3afa6296c9df0f958549346bc3dbf9a613b4b1acc062dac83cfc144c71827ef8b07
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ .env*
12
+ .DS_Store
data/.travis.yml ADDED
@@ -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.3
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at aaron@aaroncruz.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in sinbotra.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Aaron Cruz
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # Sinbotra
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/sinbotra`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'sinbotra'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install sinbotra
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ 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).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/sinbotra. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
36
+
37
+
38
+ ## License
39
+
40
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
41
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "sinbotra"
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 'pry'
14
+ require 'pry-byebug'
15
+ Pry.start
data/bin/setup ADDED
@@ -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
data/config.example.ru ADDED
@@ -0,0 +1,91 @@
1
+ require "sinbotra/server"
2
+ require "sucker_punch"
3
+
4
+ logger = Logger.new(STDERR)
5
+ logger.level = :debug
6
+ SuckerPunch.logger = logger
7
+
8
+ class PushupBot
9
+ include Sinbotra::Messenger::Handler
10
+
11
+ enable_get_started!
12
+
13
+ get_started do
14
+ start_conversation(:onboarding)
15
+ end
16
+
17
+ hear(/hi|hello|yo/i) do |bot|
18
+ bot.say "Hello there!"
19
+ bot.start_conversation(:onboarding)
20
+ end
21
+
22
+ hear(/start/i) do |bot|
23
+ bot.start_conversation(:start_day)
24
+ end
25
+
26
+ hear do |bot|
27
+ msg = bot.current_message
28
+ intent = get_intent(msg.text)
29
+ if bot.has_conversation?(intent)
30
+ bot.start_conversation(intent)
31
+ else
32
+ bot.say next_dont_understand_phrase
33
+ end
34
+ end
35
+
36
+ conversation :onboarding do |convo|
37
+ convo.step :ask_name do |_bot|
38
+ convo.say "Hi there!"
39
+ convo.say "What's your name?"
40
+ convo.next_step!
41
+ end
42
+
43
+ convo.step :ask_height do |bot|
44
+ convo.store!(:name, bot.current_message.text)
45
+ convo.say "Great #{convo.recall(:name)}. How tall are you?"
46
+ convo.next_step!
47
+ end
48
+
49
+ convo.step :done do |bot|
50
+ convo.store!(:height, bot.current_message.text)
51
+ convo.say "Thank you for the info"
52
+ typing 1
53
+ convo.say "See you around!"
54
+ convo.done!
55
+ end
56
+ end
57
+
58
+ conversation :start_day do |convo|
59
+ convo.step :how_many do |_bot|
60
+ convo.say "How many pomodoros would you like to do today?"
61
+ convo.next_step!
62
+ end
63
+
64
+ convo.step :ask_start_now do |bot|
65
+ count = bot.current_message.text.to_i
66
+ if count < 1
67
+ convo.say("I totally didn't understand that. How many would you like to do?")
68
+ convo.repeat_step!
69
+ else
70
+ convo.store!(:pomodoro_count, count)
71
+ convo.say "Would you like to start now?"
72
+ convo.next_step!
73
+ end
74
+ end
75
+
76
+ convo.step :now_yes_no do |bot|
77
+ if yes?(bot.current_message.text)
78
+ count = convo.recall(:pomodoro_count)
79
+ convo.say "Great #{convo.recall(:name)}. I'll set you up for #{count} pomodoros."
80
+ convo.say "I'll get back to you in 25 minutes. Cheers"
81
+ else
82
+ bot.quick_replies("Ok, let me know when you'd like to start.", ["Start Pomodoros", "Start Over"])
83
+ end
84
+ convo.done!
85
+ end
86
+ end
87
+ end
88
+
89
+ Sinbotra::Bot.add_handler(:facebook, PushupBot)
90
+
91
+ run Sinbotra::Server
@@ -0,0 +1,47 @@
1
+ class PushupBot
2
+ include Sinbotra::Messenger::Handler
3
+
4
+ enable_get_started!
5
+
6
+ get_started do
7
+ start_conversation(:onboarding)
8
+ end
9
+
10
+ hear(/hi|hello|yo/i) do |bot, _msg|
11
+ bot.say "Hello there!"
12
+ bot.start_conversation(:onboarding, msg)
13
+ end
14
+
15
+ hear do |bot, msg|
16
+ intent = get_intent(msg.text)
17
+ if conversations.include?(intent)
18
+ bot.start_conversation(intent, msg)
19
+ else
20
+ bot.say next_dont_understand_phrase
21
+ end
22
+ end
23
+
24
+ conversation :onboarding do |convo|
25
+ convo.step :ask_name do |_msg|
26
+ convo.say "Hi there!"
27
+ convo.say "What's your name?"
28
+ convo.next_step!
29
+ end
30
+
31
+ convo.step :ask_height do |msg|
32
+ convo.store!(:name, msg.text)
33
+ convo.say "Great #{convo.recall[:name]}. How tall are you?"
34
+ convo.next_step!
35
+ end
36
+
37
+ convo.step :done do |msg|
38
+ convo.store!(:height, msg.text)
39
+ convo.say "Thank you for the info"
40
+ typing 1
41
+ convo.say "See you around!"
42
+ convo.done!
43
+ end
44
+ end
45
+ end
46
+
47
+ Sinbotra::Bot.add_handler(:facebook, PushupBot)
@@ -0,0 +1,38 @@
1
+ class SimpleConversation
2
+ include Sinbotra::Conversation
3
+
4
+ hear /hi|hello/ do |_|
5
+ start_conversation(:onboarding)
6
+ end
7
+
8
+ hear do |msg|
9
+ intent = get_intent(msg.text)
10
+ if conversations.include?(intent)
11
+ start_conversation(intent)
12
+ else
13
+ say next_dont_understand_phrase
14
+ end
15
+ end
16
+
17
+ conversation :onboarding do
18
+ step :ask_name do |convo|
19
+ say "Hi there!"
20
+ say "What's your name?"
21
+ convo.next_step!
22
+ end
23
+
24
+ step :ask_height do |convo|
25
+ convo.store(:name, convo.last_message.text)
26
+ say "Great. How tall are you?"
27
+ convo.next_step!
28
+ end
29
+
30
+ step :done do |convo|
31
+ convo.store(:height, convo.last_message.text)
32
+ say "Thank you for the info"
33
+ typing 1
34
+ say "See you around!"
35
+ convo.done!
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,86 @@
1
+ class Sinbotra::Bot
2
+ class Conversation
3
+ Step = Struct.new(:id, :callback)
4
+
5
+ attr_reader :id
6
+ attr_reader :steps
7
+
8
+ def initialize(id, store={})
9
+ @id = id
10
+ @steps = []
11
+ @store = store
12
+ update_current_step(0)
13
+ store!(:is_active, false)
14
+ end
15
+
16
+ def start
17
+ store!(:is_active, true)
18
+ end
19
+
20
+ def active?
21
+ recall(:is_active)
22
+ end
23
+
24
+ def current_step
25
+ recall(:_current_step)
26
+ end
27
+
28
+ def step(id, &blk)
29
+ @steps << Step.new(id, blk)
30
+ end
31
+
32
+ def store!(k, v)
33
+ @store[k] = v
34
+ end
35
+
36
+ def recall(k)
37
+ @store[k]
38
+ end
39
+
40
+ def say(text)
41
+ puts text
42
+ end
43
+
44
+ def skip_to_next_step!
45
+ puts "NOOP skip_to_next_step!"
46
+ end
47
+
48
+ def next_step!
49
+ new_step = current_step + 1
50
+ update_current_step(new_step)
51
+ done! if completed_last_step?
52
+ end
53
+
54
+ def next_step_now!(bot)
55
+ next_step!
56
+ perform_current_step(bot)
57
+ end
58
+
59
+ def repeat_step!
60
+ puts "Repeating conversation step. Nothing to do here."
61
+ end
62
+
63
+ def done!
64
+ update_current_step(0)
65
+ store!(:is_active, false)
66
+ end
67
+
68
+ def perform_current_step(bot)
69
+ current_action.(bot)
70
+ end
71
+
72
+ private
73
+
74
+ def completed_last_step?
75
+ current_step == @steps.size
76
+ end
77
+
78
+ def update_current_step(n)
79
+ store!(:_current_step, n)
80
+ end
81
+
82
+ def current_action
83
+ @steps[current_step].callback
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,17 @@
1
+ class Sinbotra::Bot
2
+ class ConversationRepo
3
+ class << self
4
+ def connect
5
+ @conversations ||= {}
6
+ end
7
+
8
+ def find(id)
9
+ @conversations[id]
10
+ end
11
+
12
+ def create(id, convo)
13
+ @conversations[id] = convo
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ class Sinbotra::Bot
2
+ class DefaultMessageHistory
3
+ attr_reader :store
4
+
5
+ def initialize(store=[])
6
+ @store = store
7
+ end
8
+
9
+ def add_message(msg)
10
+ @store << msg
11
+ end
12
+
13
+ def all() @store end
14
+ end
15
+ end
@@ -0,0 +1,59 @@
1
+ require "securerandom"
2
+ require "sinbotra/bot/default_message_history"
3
+
4
+ class Sinbotra::Bot
5
+ class User
6
+ attr_reader :id
7
+ attr_reader :session_id
8
+ attr_reader :current_message
9
+ attr_reader :conversations
10
+ attr_reader :history
11
+
12
+ def initialize(id, history=Sinbotra::Bot::DefaultMessageHistory.new)
13
+ @id = id
14
+ @history = history
15
+ @session_id = nil
16
+ @conversations = []
17
+ start_session
18
+ end
19
+
20
+ def handle_message(msg)
21
+ @current_message = msg
22
+ history.add_message(msg)
23
+ end
24
+
25
+ # Sessions
26
+
27
+ def logged_in?() !@session_id.nil? end
28
+
29
+ # remove user while not removing history
30
+ def destroy
31
+ end_session
32
+ end
33
+
34
+ # Conversations
35
+
36
+ def start_conversation(convo)
37
+ conversations.unshift(convo)
38
+ convo.start
39
+ end
40
+
41
+ def in_conversation?
42
+ !current_conversation.nil?
43
+ end
44
+
45
+ def current_conversation
46
+ conversations.find { |c| c.active? }
47
+ end
48
+
49
+ private
50
+
51
+ def start_session
52
+ @session_id = SecureRandom.uuid
53
+ end
54
+
55
+ def end_session
56
+ @session_id = nil
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,23 @@
1
+ class Sinbotra::Bot
2
+ class UserRepo
3
+ class << self
4
+ def connect
5
+ @users ||= {}
6
+ end
7
+
8
+ def find_or_create(user_class, id)
9
+ user = @users[id]
10
+ if user.nil?
11
+ user = user_class.new(id)
12
+ @users[id] = user
13
+ end
14
+ user
15
+ end
16
+
17
+ def messages(id)
18
+ user = @users[id]
19
+ user.history.all
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,35 @@
1
+ require "sinbotra/bot/conversation"
2
+ require "sinbotra/bot/conversation_repo"
3
+ require "sinbotra/bot/user"
4
+ require "sinbotra/bot/user_repo"
5
+
6
+ require "sucker_punch"
7
+
8
+ module Sinbotra
9
+ class Bot
10
+ class << self
11
+ def handle(provider, entry)
12
+ perform_async(provider, entry)
13
+ end
14
+
15
+ def add_handler(provider, handler)
16
+ @handlers ||= {}
17
+ @handlers[provider.to_sym] = handler
18
+ end
19
+
20
+ def receive(provider, msg)
21
+ handler = @handlers[provider.to_sym]
22
+ handler.handle_message(msg)
23
+ end
24
+ end
25
+
26
+ include SuckerPunch::Job
27
+
28
+ def perform(provider, msg)
29
+ # THis needs to be done elsewhere
30
+ msg["entry"].first["messaging"].each do |m|
31
+ Sinbotra::Bot.receive(provider, m)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,91 @@
1
+ module Sinbotra::Messenger
2
+ class Bot
3
+ attr_reader :current_user
4
+
5
+ def initialize(user)
6
+ @current_user = user
7
+ end
8
+
9
+ def say(text)
10
+ client.text(
11
+ recipient_id: recipient_id,
12
+ text: text
13
+ )
14
+ end
15
+ alias_method :text, :say
16
+
17
+ def quick_replies(text, replies)
18
+ client.qr(
19
+ recipient_id: recipient_id,
20
+ text: text,
21
+ replies: replies
22
+ )
23
+ end
24
+
25
+ def typing(seconds=0, &blk)
26
+ client.typing(recipient_id: recipient_id, seconds: seconds, &blk)
27
+ end
28
+
29
+ def qr(text, postback=nil)
30
+ MessengerClient::QuickReply.new(text, postback)
31
+ end
32
+
33
+ def payload_button(text, payload)
34
+ MessengerClient::PayloadButton.new(text, payload)
35
+ end
36
+ def url_button(text, url)
37
+ MessengerClient::URLButton.new(text, url)
38
+ end
39
+
40
+ def generic_template(title, subtitle, image, link, buttons)
41
+ end
42
+
43
+ def text_with_buttons(text, buttons)
44
+ client.button_template(
45
+ recipient_id: recipient_id,
46
+ text: text,
47
+ buttons: buttons
48
+ )
49
+ end
50
+
51
+ def video(url)
52
+ client.video(recipient_id: recipient_id, url: url)
53
+ end
54
+
55
+ def image(url)
56
+ client.image(recipient_id: recipient_id, url: url)
57
+ end
58
+
59
+ def location
60
+ client.location(recipient_id: recipient_id)
61
+ end
62
+
63
+ def current_message
64
+ current_user.current_message
65
+ end
66
+
67
+ def start_conversation(convo_id)
68
+ convo = find_convo(convo_id)
69
+ current_user.start_conversation(convo)
70
+ convo.perform_current_step(self)
71
+ end
72
+
73
+ def has_conversation?(convo_id)
74
+ !!find_convo(convo_id)
75
+ end
76
+
77
+ private
78
+
79
+ def client
80
+ @client ||= MessengerClient::Client.new(ENV.fetch("FACEBOOK_PAGE_TOKEN"))
81
+ end
82
+
83
+ def recipient_id
84
+ current_user.id
85
+ end
86
+
87
+ def find_convo(convo_id)
88
+ Sinbotra::Bot::ConversationRepo.find(convo_id)
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,103 @@
1
+ module Sinbotra::Messenger
2
+ module Handler
3
+ def self.included(base)
4
+ ensure_repos_connected
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ def self.ensure_repos_connected
9
+ Sinbotra::Bot::ConversationRepo.connect
10
+ Sinbotra::Bot::UserRepo.connect
11
+ end
12
+
13
+ module ClassMethods
14
+ GET_STARTED_ID = "GET_STARTED"
15
+
16
+ def handle_message(msg)
17
+ user = Sinbotra::Messenger::User.load(msg)
18
+ bot = Sinbotra::Messenger::Bot.new(user)
19
+ # Next step in conversation
20
+ # or match the `#hear`
21
+ # or get default fallback
22
+ if user.in_conversation?
23
+ convo = user.current_conversation
24
+ convo.perform_current_step(bot)
25
+ elsif intent = match_intent(bot)
26
+ intent.callback.(bot) unless intent.nil?
27
+ else
28
+ intent = default_intent
29
+ intent.callback.(bot) unless intent.nil?
30
+ end
31
+ end
32
+
33
+ def enable_get_started!
34
+ puts "NOOP enable_get_started!"
35
+ end
36
+
37
+ def get_started
38
+ puts "NOOP get_started"
39
+ end
40
+
41
+ def conversation(id, &blk)
42
+ c = Sinbotra::Bot::Conversation.new(id)
43
+ Sinbotra::Bot::ConversationRepo.create(id, c)
44
+ yield c
45
+ end
46
+
47
+ Listener = Struct.new(:matcher, :callback) do
48
+ def default?; false end
49
+ end
50
+ DefaultListener = Struct.new(:callback) do
51
+ def default?; true end
52
+ end
53
+
54
+ def hear(matcher=nil, &blk)
55
+ # TODO: THis feels kinda wrong
56
+ @listeners ||= []
57
+ if matcher.nil?
58
+ return @listeners << DefaultListener.new(blk)
59
+ end
60
+ @listeners << Listener.new(matcher, blk)
61
+ end
62
+
63
+ def action(name, &blk)
64
+ @actions ||= {}
65
+ @actions[name] = blk
66
+ end
67
+
68
+ def get_intent(text)
69
+ puts "NOOP get_intent"
70
+ nil
71
+ end
72
+
73
+ def yes?(str)
74
+ str =~ yes_matcher
75
+ end
76
+
77
+ def typing(delay=1)
78
+ puts "NOOP show typing for #{delay} seconds"
79
+ end
80
+
81
+ def next_dont_understand_phrase
82
+ puts "NOOP next_dont_understand_phrase"
83
+ end
84
+
85
+ private
86
+
87
+ def yes_matcher
88
+ /^(yes|yeah|ya|ok)/i
89
+ end
90
+
91
+ def default_intent
92
+ @listeners.find(&:default?)
93
+ end
94
+
95
+ def match_intent(bot)
96
+ msg = bot.current_message
97
+ txt = msg.text
98
+ @listeners.find { |l| !l.default? && txt.match(l.matcher) }
99
+ end
100
+ end
101
+ end
102
+ end
103
+
@@ -0,0 +1,57 @@
1
+ module Sinbotra::Messenger
2
+ class Message
3
+ Sender = Struct.new(:id)
4
+ Location = Struct.new(:title, :url, :lat, :lng)
5
+
6
+ def self.load(msg)
7
+ new(msg).parse
8
+ end
9
+
10
+ attr_reader :id
11
+ attr_reader :timestamp
12
+ attr_reader :sender
13
+ attr_reader :postback
14
+ attr_reader :text
15
+ attr_reader :location
16
+
17
+ def initialize(msg)
18
+ @raw_message = msg
19
+ end
20
+
21
+ def parse
22
+ parse_message
23
+ self
24
+ end
25
+
26
+ def postback?
27
+ !!postback
28
+ end
29
+
30
+ private
31
+
32
+ def parse_message
33
+ @timestamp = @raw_message["timestamp"]
34
+ @sender = Sender.new(@raw_message["sender"]["id"])
35
+ if pb = @raw_message["postback"]
36
+ # From button postback
37
+ @postback = pb["payload"].to_sym if pb["payload"]
38
+ elsif msg = @raw_message["message"]
39
+ # Message or quick reply (or location)
40
+ @text = msg["text"]
41
+ @id = msg["id"]
42
+ if qr = msg["quick_reply"]
43
+ # Is quick reply
44
+ @postback = qr["payload"].to_sym if qr["payload"]
45
+ end
46
+ attch = msg["attachments"]
47
+ if !attch.nil? && attch["type"] == "location"
48
+ # Is location
49
+ coords = attch["payload"]["coordinates"]
50
+ @location = Location.new(attch["title"], attch["url"], coords.first, coords.last)
51
+ end
52
+ else
53
+ raise ArgumentError, "Dunno what this message is about: #{message.to_s}"
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,10 @@
1
+ module Sinbotra::Messenger
2
+ class User < Sinbotra::Bot::User
3
+ def self.load(msg)
4
+ msg = Sinbotra::Messenger::Message.load(msg)
5
+ user = Sinbotra::Bot::UserRepo.find_or_create(self, msg.sender.id)
6
+ user.handle_message(msg)
7
+ user
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,4 @@
1
+ require "sinbotra/messenger/bot"
2
+ require "sinbotra/messenger/handler"
3
+ require "sinbotra/messenger/message"
4
+ require "sinbotra/messenger/user"
@@ -0,0 +1,51 @@
1
+ require "sinbotra"
2
+ require "sinatra"
3
+ require "json"
4
+
5
+ module Sinbotra
6
+ class Server < Sinatra::Base
7
+ configure do
8
+ raise ArgumentError, "You need to set a FACEBOOK_PAGE_TOKEN environmental variable to run the server!" unless ENV["FACEBOOK_PAGE_TOKEN"]
9
+ raise ArgumentError, "You need to set a FACEBOOK_APP_SECRET environmental variable to run the server!" unless ENV["FACEBOOK_APP_SECRET"]
10
+ raise ArgumentError, "You need to set a FACEBOOK_CHALLENGE environmental variable to run the server!" unless ENV["FACEBOOK_CHALLENGE"]
11
+ end
12
+
13
+ helpers do
14
+ def verify_facebook_signature!(payload_body)
15
+ digest = OpenSSL::HMAC.hexdigest(
16
+ OpenSSL::Digest.new("sha1"),
17
+ ENV["FACEBOOK_APP_SECRET"],
18
+ payload_body
19
+ )
20
+ signature = "sha1=" + digest
21
+ return halt 403, "Signatures didn't match!" unless Rack::Utils.secure_compare(signature, request.env["HTTP_X_HUB_SIGNATURE"])
22
+ end
23
+
24
+ def match_facebook_challenge(params)
25
+ matching = params["hub.mode"] == "subscribe" &&
26
+ params["hub.verify_token"] == ENV.fetch("FACEBOOK_CHALLENGE")
27
+ matching ? params["hub.challenge"] : nil
28
+ end
29
+ end
30
+
31
+ get "/facebook/webhook" do
32
+ if challenge = match_facebook_challenge(params)
33
+ challenge
34
+ else
35
+ status 403
36
+ end
37
+ end
38
+
39
+ post "/facebook/webhook" do
40
+ payload = request.body.read
41
+ verify_facebook_signature!(payload)
42
+ begin
43
+ msg = JSON.load(payload)
44
+ Sinbotra::Bot.handle(:facebook, msg)
45
+ rescue Exception => e
46
+ STDERR.puts e.message
47
+ STDERR.puts e.backtrace
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,3 @@
1
+ module Sinbotra
2
+ VERSION = "0.1.0"
3
+ end
data/lib/sinbotra.rb ADDED
@@ -0,0 +1,10 @@
1
+ # Require messenger client
2
+ require "messenger_client"
3
+
4
+ # Require sinbotra framework
5
+ require "sinbotra/version"
6
+ require "sinbotra/bot"
7
+ require "sinbotra/messenger"
8
+
9
+ module Sinbotra
10
+ end
data/sinbotra.gemspec ADDED
@@ -0,0 +1,43 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sinbotra/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "sinbotra"
8
+ spec.version = Sinbotra::VERSION
9
+ spec.authors = ["Aaron Cruz"]
10
+ spec.email = ["aaron@aaroncruz.com"]
11
+
12
+ spec.summary = %q{A bot framework.}
13
+ spec.description = %q{A bot framework.}
14
+ spec.homepage = "http://computercomputer.computer"
15
+ spec.license = "MIT"
16
+
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
19
+ #if spec.respond_to?(:metadata)
20
+ #spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
21
+ #else
22
+ #raise "RubyGems 2.0 or newer is required to protect against " \
23
+ #"public gem pushes."
24
+ #end
25
+
26
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
27
+ f.match(%r{^(test|spec|features)/})
28
+ end
29
+ spec.bindir = "exe"
30
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
31
+ spec.require_paths = ["lib"]
32
+
33
+ spec.add_dependency "sinatra"
34
+ spec.add_dependency "messenger_client"
35
+ spec.add_dependency "sucker_punch"
36
+ spec.add_dependency "typhoeus"
37
+
38
+ spec.add_development_dependency "puma"
39
+ spec.add_development_dependency "bundler", "~> 1.13"
40
+ spec.add_development_dependency "rake", "~> 10.0"
41
+ spec.add_development_dependency "minitest", "~> 5.0"
42
+ spec.add_development_dependency "pry-byebug"
43
+ end
metadata ADDED
@@ -0,0 +1,197 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sinbotra
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Aaron Cruz
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-01-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sinatra
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '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'
27
+ - !ruby/object:Gem::Dependency
28
+ name: messenger_client
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sucker_punch
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: typhoeus
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: puma
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.13'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.13'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '10.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '10.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: minitest
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '5.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '5.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: pry-byebug
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description: A bot framework.
140
+ email:
141
+ - aaron@aaroncruz.com
142
+ executables: []
143
+ extensions: []
144
+ extra_rdoc_files: []
145
+ files:
146
+ - ".gitignore"
147
+ - ".travis.yml"
148
+ - CODE_OF_CONDUCT.md
149
+ - Gemfile
150
+ - LICENSE.txt
151
+ - README.md
152
+ - Rakefile
153
+ - bin/console
154
+ - bin/setup
155
+ - config.example.ru
156
+ - examples/pushup_bot.rb
157
+ - examples/simple_conversations.rb
158
+ - lib/sinbotra.rb
159
+ - lib/sinbotra/bot.rb
160
+ - lib/sinbotra/bot/conversation.rb
161
+ - lib/sinbotra/bot/conversation_repo.rb
162
+ - lib/sinbotra/bot/default_message_history.rb
163
+ - lib/sinbotra/bot/user.rb
164
+ - lib/sinbotra/bot/user_repo.rb
165
+ - lib/sinbotra/messenger.rb
166
+ - lib/sinbotra/messenger/bot.rb
167
+ - lib/sinbotra/messenger/handler.rb
168
+ - lib/sinbotra/messenger/message.rb
169
+ - lib/sinbotra/messenger/user.rb
170
+ - lib/sinbotra/server.rb
171
+ - lib/sinbotra/version.rb
172
+ - sinbotra.gemspec
173
+ homepage: http://computercomputer.computer
174
+ licenses:
175
+ - MIT
176
+ metadata: {}
177
+ post_install_message:
178
+ rdoc_options: []
179
+ require_paths:
180
+ - lib
181
+ required_ruby_version: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - ">="
184
+ - !ruby/object:Gem::Version
185
+ version: '0'
186
+ required_rubygems_version: !ruby/object:Gem::Requirement
187
+ requirements:
188
+ - - ">="
189
+ - !ruby/object:Gem::Version
190
+ version: '0'
191
+ requirements: []
192
+ rubyforge_project:
193
+ rubygems_version: 2.6.8
194
+ signing_key:
195
+ specification_version: 4
196
+ summary: A bot framework.
197
+ test_files: []