chatterbot 0.2.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.
- data/.document +5 -0
- data/.gitignore +52 -0
- data/Gemfile +25 -0
- data/LICENSE.txt +17 -0
- data/README.rdoc +184 -0
- data/Rakefile +26 -0
- data/bin/chatterbot-blacklist +63 -0
- data/chatterbot.gemspec +53 -0
- data/examples/class_bot.rb +9 -0
- data/examples/config.yml.example +6 -0
- data/examples/dsl_test.rb +9 -0
- data/examples/echoes_bot.rb +27 -0
- data/lib/chatterbot.rb +50 -0
- data/lib/chatterbot/blacklist.rb +69 -0
- data/lib/chatterbot/bot.rb +29 -0
- data/lib/chatterbot/client.rb +123 -0
- data/lib/chatterbot/config.rb +259 -0
- data/lib/chatterbot/db.rb +70 -0
- data/lib/chatterbot/dsl.rb +100 -0
- data/lib/chatterbot/helpers.rb +29 -0
- data/lib/chatterbot/logging.rb +48 -0
- data/lib/chatterbot/reply.rb +28 -0
- data/lib/chatterbot/search.rb +35 -0
- data/lib/chatterbot/tweet.rb +25 -0
- data/lib/chatterbot/version.rb +3 -0
- data/spec/blacklist_spec.rb +115 -0
- data/spec/bot_spec.rb +5 -0
- data/spec/client_spec.rb +60 -0
- data/spec/config_spec.rb +204 -0
- data/spec/db_spec.rb +33 -0
- data/spec/dsl_spec.rb +73 -0
- data/spec/helpers_spec.rb +40 -0
- data/spec/logging_spec.rb +65 -0
- data/spec/reply_spec.rb +54 -0
- data/spec/search_spec.rb +72 -0
- data/spec/spec_helper.rb +52 -0
- data/spec/tweet_spec.rb +76 -0
- data/specs.watchr +60 -0
- metadata +162 -0
@@ -0,0 +1,6 @@
|
|
1
|
+
---
|
2
|
+
:consumer_secret: CONSUMER SECRET GOES HERE. THIS IS YOUR TWITTER API SECRET
|
3
|
+
:consumer_key: CONSUMER KEY GOES HERE. THIS IS YOUR TWITTER API KEY
|
4
|
+
:secret: SECRET GOES HERE. YOU GET THIS AFTER AUTHORIZING THE BOT
|
5
|
+
:token: TOKEN GOES HERE. YOU GET THIS AFTER AUTHORIZING THE BOT
|
6
|
+
:since_id: 1234567
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
|
4
|
+
#
|
5
|
+
# require the dsl lib to include all the methods you see below.
|
6
|
+
#
|
7
|
+
require 'chatterbot/dsl'
|
8
|
+
|
9
|
+
puts "Loading echoes_bot.rb"
|
10
|
+
|
11
|
+
##
|
12
|
+
## If I wanted to exclude some terms from triggering this bot, I would list them here.
|
13
|
+
## For now, we'll block URLs to keep this from being a source of spam
|
14
|
+
##
|
15
|
+
exclude "http://"
|
16
|
+
|
17
|
+
blacklist "mean_user, private_user"
|
18
|
+
|
19
|
+
puts "checking for replies to me"
|
20
|
+
replies do |tweet|
|
21
|
+
|
22
|
+
# replace the incoming username with the handle of the user who tweeted us
|
23
|
+
src = tweet[:text].gsub(/@echoes_bot/, tweet_user(tweet))
|
24
|
+
|
25
|
+
# send it back!
|
26
|
+
reply src, tweet
|
27
|
+
end
|
data/lib/chatterbot.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'twitter_oauth'
|
3
|
+
|
4
|
+
#
|
5
|
+
# Try and load Sequel, but don't freak out if it's not there
|
6
|
+
begin
|
7
|
+
require 'sequel'
|
8
|
+
rescue Exception
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
#
|
13
|
+
# extend Hash class to turn keys into symbols
|
14
|
+
#
|
15
|
+
class Hash
|
16
|
+
#
|
17
|
+
# turn keys in this hash into symbols
|
18
|
+
def symbolize_keys!
|
19
|
+
replace(inject({}) do |hash,(key,value)|
|
20
|
+
hash[key.to_sym] = value.is_a?(Hash) ? value.symbolize_keys! : value
|
21
|
+
hash
|
22
|
+
end)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# the big kahuna!
|
28
|
+
module Chatterbot
|
29
|
+
|
30
|
+
#
|
31
|
+
# load in our assorted modules
|
32
|
+
def self.load
|
33
|
+
require "chatterbot/config"
|
34
|
+
require "chatterbot/db"
|
35
|
+
require "chatterbot/logging"
|
36
|
+
require "chatterbot/blacklist"
|
37
|
+
require "chatterbot/client"
|
38
|
+
require "chatterbot/search"
|
39
|
+
require "chatterbot/tweet"
|
40
|
+
require "chatterbot/reply"
|
41
|
+
require "chatterbot/helpers"
|
42
|
+
|
43
|
+
require "chatterbot/bot"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
# mount up
|
49
|
+
Chatterbot.load
|
50
|
+
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Chatterbot
|
2
|
+
|
3
|
+
#
|
4
|
+
# methods for preventing the bot from spamming people who don't want to hear from it
|
5
|
+
module Blacklist
|
6
|
+
attr_accessor :exclude, :blacklist
|
7
|
+
|
8
|
+
# return a list of text strings which we will check for in incoming tweets.
|
9
|
+
# If the text is listed, we won't use this tweet
|
10
|
+
def exclude
|
11
|
+
@exclude || []
|
12
|
+
end
|
13
|
+
|
14
|
+
# A list of Twitter users that don't want to hear from the bot.
|
15
|
+
def blacklist
|
16
|
+
@blacklist || []
|
17
|
+
end
|
18
|
+
def blacklist=(b)
|
19
|
+
@blacklist = b
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# Based on the text of this tweet, should it be skipped?
|
24
|
+
def skip_me?(s)
|
25
|
+
search = s.is_a?(Hash) ? s[:text] : s
|
26
|
+
exclude.detect { |e| search.downcase.include?(e) } != nil
|
27
|
+
end
|
28
|
+
|
29
|
+
#
|
30
|
+
# Pull the username from a tweet hash -- this is different depending on
|
31
|
+
# if we're doing a search, or parsing through replies/mentions.
|
32
|
+
def tweet_user(s)
|
33
|
+
s.has_key?(:from_user) ? s[:from_user] : s[:user][:screen_name]
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# Is this tweet from a user on our blacklist?
|
38
|
+
def on_blacklist?(s)
|
39
|
+
search = (s.is_a?(Hash) ? tweet_user(s) : s).downcase
|
40
|
+
blacklist.any? { |b| search.include?(b.downcase) } ||
|
41
|
+
on_global_blacklist?(search)
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# Is this user on our global blacklist?
|
46
|
+
def on_global_blacklist?(user)
|
47
|
+
return false if ! has_db?
|
48
|
+
db[:blacklist].where(:user => user).count > 0
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# add the specified user to the global blacklist
|
53
|
+
def add_to_blacklist(user)
|
54
|
+
user = user.is_a?(Hash) ? user[:from_user] : user
|
55
|
+
|
56
|
+
# don't try and add if we don't have a DB connection, or if the
|
57
|
+
# user is already on the list
|
58
|
+
return if ! has_db? || on_global_blacklist?(user)
|
59
|
+
|
60
|
+
# make sure we don't have an @ at the beginning of the username
|
61
|
+
user.gsub!(/^@/, "")
|
62
|
+
|
63
|
+
debug "adding #{user} to blacklist"
|
64
|
+
|
65
|
+
db[:blacklist].insert({ :user => user, :created_at => Time.now }) #
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Chatterbot
|
2
|
+
|
3
|
+
#
|
4
|
+
# primary Bot object, includes all the other modules
|
5
|
+
class Bot
|
6
|
+
include Blacklist
|
7
|
+
include Config
|
8
|
+
include Logging
|
9
|
+
include Search
|
10
|
+
include Tweet
|
11
|
+
include Reply
|
12
|
+
include Client
|
13
|
+
include DB
|
14
|
+
include Helpers
|
15
|
+
|
16
|
+
#
|
17
|
+
# Create a new bot. No options for now.
|
18
|
+
def initialize(params={})
|
19
|
+
@config = load_config(params)
|
20
|
+
|
21
|
+
# update config when we exit
|
22
|
+
at_exit do
|
23
|
+
raise $! if $!
|
24
|
+
update_config
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module Chatterbot
|
2
|
+
|
3
|
+
#
|
4
|
+
# routines for connecting to Twitter and validating the bot
|
5
|
+
module Client
|
6
|
+
|
7
|
+
# the Twitter client
|
8
|
+
attr_accessor :client
|
9
|
+
|
10
|
+
#
|
11
|
+
# default options when querying twitter -- this could be extended
|
12
|
+
# with a language, etc.
|
13
|
+
def default_opts
|
14
|
+
{
|
15
|
+
:since_id => since_id
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
#
|
21
|
+
# Initialize the Twitter client
|
22
|
+
def init_client
|
23
|
+
@client ||= TwitterOAuth::Client.new(client_params)
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# Re-initialize with Twitter, handy during the auth process
|
28
|
+
def reset_client
|
29
|
+
@client = nil
|
30
|
+
init_client
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# Call this before doing anything that requires an authorized Twitter
|
35
|
+
# connection.
|
36
|
+
def require_login
|
37
|
+
init_client
|
38
|
+
login
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# print out a message about getting a PIN from twitter, then output
|
43
|
+
# the URL the user needs to visit to authorize
|
44
|
+
#
|
45
|
+
def get_oauth_verifier(request_token)
|
46
|
+
puts "Please go to the following URL and authorize the bot.\n"
|
47
|
+
puts "#{request_token.authorize_url}\n"
|
48
|
+
|
49
|
+
puts "Paste your PIN and hit enter when you have completed authorization."
|
50
|
+
STDIN.readline.chomp
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# Ask the user to get an API key from Twitter.
|
55
|
+
def get_api_key
|
56
|
+
puts "****************************************"
|
57
|
+
puts "****************************************"
|
58
|
+
puts "**** API SETUP TIME! ****"
|
59
|
+
puts "****************************************"
|
60
|
+
puts "****************************************"
|
61
|
+
|
62
|
+
puts "Hey, looks like you need to get an API key from Twitter before you can get started."
|
63
|
+
puts "Please go to this URL: https://twitter.com/apps/new"
|
64
|
+
|
65
|
+
puts "\nChoose 'Client' as the app type."
|
66
|
+
puts "Choose 'Read & Write' access."
|
67
|
+
|
68
|
+
print "\n\n\nPaste the 'Consumer Key' here: "
|
69
|
+
STDOUT.flush
|
70
|
+
config[:consumer_key] = STDIN.readline.chomp
|
71
|
+
|
72
|
+
print "Paste the 'Consumer Secret' here: "
|
73
|
+
STDOUT.flush
|
74
|
+
config[:consumer_secret] = STDIN.readline.chomp
|
75
|
+
|
76
|
+
reset_client
|
77
|
+
|
78
|
+
#
|
79
|
+
# capture ctrl-c and exit without a stack trace
|
80
|
+
#
|
81
|
+
rescue Interrupt => e
|
82
|
+
# exit
|
83
|
+
end
|
84
|
+
|
85
|
+
#
|
86
|
+
# error message for auth
|
87
|
+
def display_oauth_error
|
88
|
+
debug "Oops! Looks like something went wrong there, please try again!"
|
89
|
+
# exit
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# handle oauth for this request. if the client isn't authorized, print
|
94
|
+
# out the auth URL and get a pin code back from the user
|
95
|
+
def login
|
96
|
+
if needs_api_key?
|
97
|
+
get_api_key
|
98
|
+
end
|
99
|
+
|
100
|
+
if needs_auth_token?
|
101
|
+
request_token = client.request_token
|
102
|
+
|
103
|
+
pin = get_oauth_verifier(request_token)
|
104
|
+
access_token = client.authorize(
|
105
|
+
request_token.token,
|
106
|
+
request_token.secret,
|
107
|
+
:oauth_verifier => pin
|
108
|
+
)
|
109
|
+
|
110
|
+
if client.authorized?
|
111
|
+
config[:token] = access_token.token
|
112
|
+
config[:secret] = access_token.secret
|
113
|
+
update_config
|
114
|
+
else
|
115
|
+
display_oauth_error
|
116
|
+
return false
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
true
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,259 @@
|
|
1
|
+
module Chatterbot
|
2
|
+
|
3
|
+
#
|
4
|
+
# routines for storing config information for the bot
|
5
|
+
module Config
|
6
|
+
attr_accessor :config
|
7
|
+
|
8
|
+
#
|
9
|
+
# the entire config for the bot, loaded from YAML files and the DB if applicable
|
10
|
+
def config
|
11
|
+
@config ||= load_config
|
12
|
+
end
|
13
|
+
|
14
|
+
#
|
15
|
+
# has the config been loaded yet?
|
16
|
+
def has_config?
|
17
|
+
! @config.nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# should we log tweets to the database?
|
22
|
+
def log_tweets?
|
23
|
+
config.has_key?(:db_uri)
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# Check to see if Sequel was loaded successfully. If not, we won't make any DB calls
|
28
|
+
def has_sequel?
|
29
|
+
! defined?(Sequel).nil?
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# do we have a DB connection string?
|
34
|
+
def has_db?
|
35
|
+
has_sequel? && config.has_key?(:db_uri)
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# are we in debug mode?
|
40
|
+
def debug_mode?
|
41
|
+
config[:debug_mode] || false
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# Should we run any config updates?
|
46
|
+
def update_config?
|
47
|
+
config[:dry_run] || true
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# should we write to a log file?
|
52
|
+
def logging?
|
53
|
+
has_config? && config.has_key?(:log_dest)
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# destination for log entries
|
58
|
+
def log_dest
|
59
|
+
config[:log_dest]
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
# store since_id to a different key so that it doesn't actually
|
64
|
+
# get updated until the bot is done running
|
65
|
+
def since_id=(x)
|
66
|
+
config[:tmp_since_id] = x
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# return the ID of the most recent tweet pulled up in searches
|
71
|
+
def since_id
|
72
|
+
config[:since_id] || 1
|
73
|
+
end
|
74
|
+
|
75
|
+
#
|
76
|
+
# write out our config file
|
77
|
+
def update_config
|
78
|
+
return if ! update_config?
|
79
|
+
|
80
|
+
# don't update flat file if we can store to the DB instead
|
81
|
+
if has_db?
|
82
|
+
store_database_config
|
83
|
+
else
|
84
|
+
store_local_config
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
#
|
89
|
+
# update the since_id with either the highest ID of the specified
|
90
|
+
# tweets, unless it is lower than what we have already
|
91
|
+
def update_since_id(search)
|
92
|
+
unless search.nil?
|
93
|
+
tmp_id = case
|
94
|
+
# incoming tweets
|
95
|
+
when search.has_key?(:id) then search[:id]
|
96
|
+
|
97
|
+
# incoming searches
|
98
|
+
when search.has_key?("max_id") then search["max_id"]
|
99
|
+
|
100
|
+
# other?
|
101
|
+
else 1
|
102
|
+
end.to_i
|
103
|
+
config[:tmp_since_id] = [config[:tmp_since_id].to_i, tmp_id].max
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
# return a hash of the params we need to connect to the Twitter API
|
109
|
+
def client_params
|
110
|
+
{
|
111
|
+
:consumer_key => config[:consumer_key],
|
112
|
+
:consumer_secret => config[:consumer_secret],
|
113
|
+
:token => config[:token].nil? ? nil : config[:token],
|
114
|
+
:secret => config[:secret].nil? ? nil : config[:secret]
|
115
|
+
}
|
116
|
+
end
|
117
|
+
|
118
|
+
#
|
119
|
+
# do we have an API key specified?
|
120
|
+
def needs_api_key?
|
121
|
+
config[:consumer_key].nil? || config[:consumer_secret].nil?
|
122
|
+
end
|
123
|
+
|
124
|
+
#
|
125
|
+
# has this script validated with Twitter OAuth?
|
126
|
+
def needs_auth_token?
|
127
|
+
config[:token].nil?
|
128
|
+
end
|
129
|
+
|
130
|
+
#
|
131
|
+
# figure out what config file to load based on the name of the bot
|
132
|
+
def config_file
|
133
|
+
File.join(File.expand_path(File.dirname($0)), "#{botname}.yml")
|
134
|
+
end
|
135
|
+
|
136
|
+
#
|
137
|
+
# load in a config file
|
138
|
+
def slurp_file(f)
|
139
|
+
f = File.expand_path(f)
|
140
|
+
|
141
|
+
tmp = {}
|
142
|
+
|
143
|
+
if File.exist?(f)
|
144
|
+
File.open( f ) { |yf|
|
145
|
+
tmp = YAML::load( yf )
|
146
|
+
}
|
147
|
+
end
|
148
|
+
tmp.symbolize_keys! unless tmp == false
|
149
|
+
end
|
150
|
+
|
151
|
+
#
|
152
|
+
# our list of "global config files"
|
153
|
+
def global_config_files
|
154
|
+
[
|
155
|
+
# a system-wide global path
|
156
|
+
"/etc/chatterbot.yml",
|
157
|
+
|
158
|
+
# a file specified in ENV
|
159
|
+
ENV["chatterbot_config"],
|
160
|
+
|
161
|
+
# 'global' config file local to the path of the ruby script
|
162
|
+
File.join(File.dirname(File.expand_path($0)), "global.yml")
|
163
|
+
].compact
|
164
|
+
end
|
165
|
+
|
166
|
+
#
|
167
|
+
# get any config from our global config files
|
168
|
+
def global_config
|
169
|
+
tmp = {}
|
170
|
+
global_config_files.each { |f|
|
171
|
+
tmp.merge!(slurp_file(f) || {})
|
172
|
+
}
|
173
|
+
tmp
|
174
|
+
end
|
175
|
+
|
176
|
+
#
|
177
|
+
# bot-specific config settings
|
178
|
+
def bot_config
|
179
|
+
slurp_file(config_file) || { }
|
180
|
+
end
|
181
|
+
|
182
|
+
#
|
183
|
+
# load the config settings from the db, if possible
|
184
|
+
def db_config
|
185
|
+
return {} if db.nil?
|
186
|
+
db[:config][:id => botname]
|
187
|
+
end
|
188
|
+
|
189
|
+
#
|
190
|
+
# figure out what we should save to the local config file. we don't
|
191
|
+
# save anything that exists in the global config, unless it's been modified
|
192
|
+
# for this particular bot.
|
193
|
+
def config_to_save
|
194
|
+
# remove keys that are duped in the global config
|
195
|
+
tmp = config.delete_if { |k, v| global_config.has_key?(k) && global_config[k] == config[k] }
|
196
|
+
|
197
|
+
# let's not store these, they're just command-line options
|
198
|
+
tmp.delete(:debug_mode)
|
199
|
+
tmp.delete(:dry_run)
|
200
|
+
|
201
|
+
|
202
|
+
# update the since_id now
|
203
|
+
tmp[:since_id] = tmp.delete(:tmp_since_id) unless ! tmp.has_key?(:tmp_since_id)
|
204
|
+
|
205
|
+
tmp
|
206
|
+
end
|
207
|
+
|
208
|
+
|
209
|
+
#
|
210
|
+
# load in the config from the assortment of places it can be specified.
|
211
|
+
def load_config(params={})
|
212
|
+
# load the flat-files first
|
213
|
+
@config = global_config.merge(bot_config).merge(params)
|
214
|
+
@config[:db_uri] ||= ENV["chatterbot_db"] unless ENV["chatterbot_db"].nil?
|
215
|
+
|
216
|
+
# if we have a key to load from the DB, do that now
|
217
|
+
if @config.has_key?(:db_uri) && @config[:db_uri]
|
218
|
+
tmp = db_config
|
219
|
+
@config = @config.merge(tmp) unless tmp.nil?
|
220
|
+
end
|
221
|
+
@config
|
222
|
+
end
|
223
|
+
|
224
|
+
#
|
225
|
+
# write out the config file for this bot
|
226
|
+
def store_local_config
|
227
|
+
File.open(config_file, 'w') { |f| YAML.dump(config_to_save, f) }
|
228
|
+
end
|
229
|
+
|
230
|
+
#
|
231
|
+
# store config settings in the database, if possible
|
232
|
+
def store_database_config
|
233
|
+
return false if db.nil?
|
234
|
+
|
235
|
+
configs = db[:config]
|
236
|
+
data = {
|
237
|
+
:since_id => config[:since_id],
|
238
|
+
:token => config[:token],
|
239
|
+
:secret => config[:secret],
|
240
|
+
:consumer_secret => config[:consumer_secret],
|
241
|
+
:consumer_key => config[:consumer_key],
|
242
|
+
:updated_at => Time.now #:NOW.sql_function
|
243
|
+
}
|
244
|
+
|
245
|
+
row = configs.filter('id = ?', botname)
|
246
|
+
|
247
|
+
if row.count > 0
|
248
|
+
row.update(data)
|
249
|
+
else
|
250
|
+
data[:id] = botname
|
251
|
+
data[:created_at] = Time.now #:NOW.sql_function
|
252
|
+
configs.insert data
|
253
|
+
end
|
254
|
+
|
255
|
+
true
|
256
|
+
end
|
257
|
+
|
258
|
+
end
|
259
|
+
end
|