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.
@@ -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,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'chatterbot/dsl'
4
+
5
+ exclude ["foo", "bar"]
6
+ search("foo") do |tweet|
7
+ # # reply "@#{tweet['from_user']} I am serious, and don't call me Shirley!", tweet
8
+ puts tweet.inspect
9
+ end
@@ -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