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,70 @@
|
|
1
|
+
module Chatterbot
|
2
|
+
|
3
|
+
#
|
4
|
+
# routines for optionally interacting with a database for logging
|
5
|
+
# tweets, and storing config data there. Uses Sequel to handle the
|
6
|
+
# heavy lifing.
|
7
|
+
module DB
|
8
|
+
#
|
9
|
+
# connect to the database, and generate any missing tables
|
10
|
+
def db
|
11
|
+
@_db ||= connect_and_validate
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
#
|
17
|
+
# get a DB object from Sequel
|
18
|
+
def get_connection
|
19
|
+
if has_sequel?
|
20
|
+
Sequel.connect(config[:db_uri])
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# try and connect to the DB, and create tables that are missing.
|
26
|
+
def connect_and_validate
|
27
|
+
conn = get_connection
|
28
|
+
return if conn.nil?
|
29
|
+
|
30
|
+
if ! conn.tables.include?(:blacklist)
|
31
|
+
conn.create_table :blacklist do
|
32
|
+
String :user, :primary_key => true
|
33
|
+
DateTime :created_at
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
if ! conn.tables.include?(:tweets)
|
38
|
+
conn.create_table :tweets do
|
39
|
+
primary_key :id
|
40
|
+
String :txt
|
41
|
+
String :bot
|
42
|
+
String :user
|
43
|
+
String :source_id
|
44
|
+
String :source_tweet
|
45
|
+
|
46
|
+
DateTime :created_at
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
if ! conn.tables.include?(:config)
|
51
|
+
conn.create_table :config do
|
52
|
+
String :id, :primary_key => true
|
53
|
+
|
54
|
+
Bignum :since_id
|
55
|
+
|
56
|
+
String :secret
|
57
|
+
String :token
|
58
|
+
String :consumer_secret
|
59
|
+
String :consumer_key
|
60
|
+
|
61
|
+
DateTime :created_at
|
62
|
+
DateTime :updated_at
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
conn
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'chatterbot')
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
module Chatterbot
|
5
|
+
#
|
6
|
+
# very basic DSL to handle the common stuff you would want to do with a bot.
|
7
|
+
module DSL
|
8
|
+
|
9
|
+
#
|
10
|
+
# generate a Bot object. if the DSL is being called from a Bot object, just return it
|
11
|
+
# otherwise create a bot and return that
|
12
|
+
def bot
|
13
|
+
return @bot unless @bot.nil?
|
14
|
+
|
15
|
+
#
|
16
|
+
# parse any command-line options and use them to initialize the bot
|
17
|
+
#
|
18
|
+
params = {}
|
19
|
+
|
20
|
+
opts = OptionParser.new
|
21
|
+
|
22
|
+
opts.banner = "Usage: #{File.basename($0)} [options]"
|
23
|
+
|
24
|
+
opts.separator ""
|
25
|
+
opts.separator "Specific options:"
|
26
|
+
|
27
|
+
opts.on('-d', '--db [ARG]', "Specify a DB connection URI") { |d| ENV["chatterbot_db"] = d }
|
28
|
+
opts.on('-c', '--config [ARG]', "Specify a config file to use") { |c| ENV["chatterbot_config"] = c }
|
29
|
+
opts.on('-t', '--test', "Run the bot without actually sending any tweets") { params[:debug_mode] = true }
|
30
|
+
opts.on('--dry-run', "Run the bot in test mode, and also don't update the database") { params[:debug_mode] = true ; params[:no_update] = true }
|
31
|
+
opts.on('-s', '--since_id [ARG]', "Check for tweets since tweet id #[ARG]") { |s| params[:since_id] = s }
|
32
|
+
|
33
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
34
|
+
puts opts
|
35
|
+
exit
|
36
|
+
end
|
37
|
+
|
38
|
+
opts.parse!(ARGV)
|
39
|
+
|
40
|
+
@bot = Chatterbot::Bot.new(params)
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# specify a bot-specific blacklist of users. accepts an array, or a
|
45
|
+
# comma-delimited string
|
46
|
+
def blacklist(b=nil)
|
47
|
+
if b.is_a?(String)
|
48
|
+
b = b.split(",").collect { |s| s.strip }
|
49
|
+
end
|
50
|
+
|
51
|
+
if b.nil? || b.empty?
|
52
|
+
bot.blacklist = []
|
53
|
+
else
|
54
|
+
bot.blacklist += b
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# specify list of strings we will check when deciding to respond to a tweet or not
|
61
|
+
def exclude(e=nil)
|
62
|
+
if e.is_a?(String)
|
63
|
+
e = e.split(",").collect { |s| s.strip }
|
64
|
+
end
|
65
|
+
|
66
|
+
if e.nil? || e.empty?
|
67
|
+
bot.exclude = []
|
68
|
+
else
|
69
|
+
bot.exclude += e
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
#
|
74
|
+
# search twitter for the specified terms
|
75
|
+
def search(query, &block)
|
76
|
+
@bot.search(query, &block)
|
77
|
+
end
|
78
|
+
|
79
|
+
#
|
80
|
+
# handle replies to the bot
|
81
|
+
def replies(&block)
|
82
|
+
@bot.replies(&block)
|
83
|
+
end
|
84
|
+
|
85
|
+
#
|
86
|
+
# send a tweet
|
87
|
+
def tweet(txt, params = {}, original = nil)
|
88
|
+
@bot.tweet(txt, params, original)
|
89
|
+
end
|
90
|
+
|
91
|
+
#
|
92
|
+
# reply to a tweet
|
93
|
+
def reply(txt, source)
|
94
|
+
@bot.reply(txt, source)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
include Chatterbot::DSL
|
100
|
+
include Chatterbot::Helpers
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Chatterbot
|
2
|
+
|
3
|
+
#
|
4
|
+
# a bunch of helper routines for bots
|
5
|
+
module Helpers
|
6
|
+
|
7
|
+
#
|
8
|
+
# The name of the currently running bot
|
9
|
+
def botname
|
10
|
+
if self.class < Bot
|
11
|
+
self.class.to_s.downcase
|
12
|
+
else
|
13
|
+
File.basename($0,".rb")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# Take the incoming tweet/user name, and turn it into something suitable for replying
|
19
|
+
# to a user. Basically, get their handle and add a '@' to it.
|
20
|
+
def tweet_user(tweet)
|
21
|
+
if ! tweet.is_a?(String)
|
22
|
+
base = tweet.has_key?(:from_user) ? tweet[:from_user] : tweet[:user][:screen_name]
|
23
|
+
else
|
24
|
+
base = tweet
|
25
|
+
end
|
26
|
+
base =~ /^@/ ? base : "@#{base}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Chatterbot
|
4
|
+
|
5
|
+
#
|
6
|
+
# routines for outputting log messages, as well as logging tweets
|
7
|
+
# to the database if desired.
|
8
|
+
module Logging
|
9
|
+
|
10
|
+
#
|
11
|
+
# log a message
|
12
|
+
def debug(s)
|
13
|
+
logger.debug s unless ! logging?
|
14
|
+
end
|
15
|
+
|
16
|
+
#
|
17
|
+
# something really bad happened, print it out and log it
|
18
|
+
def critical(s)
|
19
|
+
puts s
|
20
|
+
debug s
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# log a tweet to the database
|
25
|
+
def log(txt, source=nil)
|
26
|
+
return unless log_tweets?
|
27
|
+
|
28
|
+
data = {:txt => txt, :bot => botname, :created_at => Time.now}
|
29
|
+
if source != nil
|
30
|
+
data = data.merge(:user => source[:from_user],
|
31
|
+
:source_id => source[:id],
|
32
|
+
:source_tweet => source[:text])
|
33
|
+
end
|
34
|
+
|
35
|
+
# populate the table
|
36
|
+
db[:tweets].insert(data)
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
#
|
41
|
+
# initialize a Logger object, writing to log_dest
|
42
|
+
def logger
|
43
|
+
# log to the dest specified in the config file, rollover after 10mb of data
|
44
|
+
@_logger ||= Logger.new(log_dest, 0, 1024 * 1024)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Chatterbot
|
2
|
+
|
3
|
+
#
|
4
|
+
# reply method for responding to tweets
|
5
|
+
module Reply
|
6
|
+
|
7
|
+
# handle replies for the bot
|
8
|
+
def replies(&block)
|
9
|
+
return unless require_login
|
10
|
+
debug "check for replies since #{since_id}"
|
11
|
+
|
12
|
+
results = client.replies(:since_id => since_id)
|
13
|
+
|
14
|
+
if results.is_a?(Hash) && results.has_key?("error")
|
15
|
+
critical results["error"]
|
16
|
+
else
|
17
|
+
results.each { |s|
|
18
|
+
s.symbolize_keys!
|
19
|
+
unless ! block_given? || on_blacklist?(s) || skip_me?(s)
|
20
|
+
update_since_id(s)
|
21
|
+
yield s
|
22
|
+
end
|
23
|
+
}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Chatterbot
|
2
|
+
|
3
|
+
#
|
4
|
+
# handle Twitter searches
|
5
|
+
module Search
|
6
|
+
|
7
|
+
# internal search code
|
8
|
+
def search(queries, &block)
|
9
|
+
return unless init_client
|
10
|
+
|
11
|
+
debug "check for tweets since #{since_id}"
|
12
|
+
|
13
|
+
if queries.is_a?(String)
|
14
|
+
queries = [queries]
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# search twitter
|
19
|
+
#
|
20
|
+
queries.each { |query|
|
21
|
+
search = client.search(query, default_opts)
|
22
|
+
update_since_id(search)
|
23
|
+
|
24
|
+
if search != nil
|
25
|
+
search["results"].each { |s|
|
26
|
+
s.symbolize_keys!
|
27
|
+
yield s unless ! block_given? || on_blacklist?(s) || skip_me?(s)
|
28
|
+
}
|
29
|
+
end
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Chatterbot
|
2
|
+
|
3
|
+
#
|
4
|
+
# routines for sending tweets
|
5
|
+
module Tweet
|
6
|
+
|
7
|
+
# simple wrapper for sending a message
|
8
|
+
def tweet(txt, params = {}, original = nil)
|
9
|
+
return if require_login == false
|
10
|
+
|
11
|
+
if debug_mode?
|
12
|
+
debug "I'm in debug mode, otherwise I would tweet: #{txt}"
|
13
|
+
else
|
14
|
+
debug txt
|
15
|
+
log txt, original
|
16
|
+
client.update txt, params
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# reply to a tweet
|
21
|
+
def reply(txt, source)
|
22
|
+
self.tweet txt, {:in_reply_to_status_id => source["id"]}, source
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe "Chatterbot::Blacklist" do
|
4
|
+
it "has a list of excluded phrases" do
|
5
|
+
@bot = test_bot
|
6
|
+
@bot.exclude = ["junk", "i hate bots", "foobar", "spam"]
|
7
|
+
@bot.skip_me?("did you know that i hate bots?").should == true
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "skip_me?" do
|
11
|
+
before(:each) do
|
12
|
+
@bot = test_bot
|
13
|
+
@bot.stub!(:exclude).and_return(["junk", "i hate bots", "foobar", "spam"])
|
14
|
+
end
|
15
|
+
|
16
|
+
it "blocks tweets with phrases we don't want" do
|
17
|
+
@bot.skip_me?("did you know that i hate bots?").should == true
|
18
|
+
end
|
19
|
+
|
20
|
+
it "isn't case-specific" do
|
21
|
+
@bot.skip_me?("DID YOU KNOW THAT I HATE BOTS?").should == true
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
it "allows users we do want" do
|
26
|
+
@bot.skip_me?("a tweet without any bad content").should == false
|
27
|
+
end
|
28
|
+
|
29
|
+
it "works with result hashes" do
|
30
|
+
@bot.skip_me?({:text => "did you know that i hate bots?"}).should == true
|
31
|
+
@bot.skip_me?({:text => "a tweet without any bad content"}).should == false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "on_blacklist?" do
|
36
|
+
before(:each) do
|
37
|
+
@bot = test_bot
|
38
|
+
@bot.stub!(:blacklist).and_return(["skippy", "blippy", "dippy"])
|
39
|
+
end
|
40
|
+
|
41
|
+
it "blocks users we don't want" do
|
42
|
+
@bot.on_blacklist?("skippy").should == true
|
43
|
+
end
|
44
|
+
|
45
|
+
it "allows users we do want" do
|
46
|
+
@bot.on_blacklist?("flippy").should == false
|
47
|
+
end
|
48
|
+
|
49
|
+
it "isn't case-specific" do
|
50
|
+
@bot.on_blacklist?("FLIPPY").should == false
|
51
|
+
@bot.on_blacklist?("SKIPPY").should == true
|
52
|
+
end
|
53
|
+
|
54
|
+
it "works with result hashes" do
|
55
|
+
@bot.on_blacklist?({:from_user => "skippy"}).should == true
|
56
|
+
@bot.on_blacklist?({:from_user => "flippy"}).should == false
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "on_global_blacklist?" do
|
61
|
+
before(:each) do
|
62
|
+
@bot = test_bot
|
63
|
+
end
|
64
|
+
|
65
|
+
it "doesn't freak out if no db" do
|
66
|
+
@bot.should_receive(:has_db?).and_return(false)
|
67
|
+
@bot.on_global_blacklist?("foobar").should == false
|
68
|
+
end
|
69
|
+
|
70
|
+
it "collects name from the db if it exists" do
|
71
|
+
@bot.stub!(:has_db?).and_return(true)
|
72
|
+
blacklist_table = mock(Object)
|
73
|
+
mock_dataset = mock(Object, {:count => 1})
|
74
|
+
blacklist_table.should_receive(:where).
|
75
|
+
with({ :user => "a"}).
|
76
|
+
and_return( mock_dataset )
|
77
|
+
|
78
|
+
|
79
|
+
missing_dataset = mock(Object, {:count => 0})
|
80
|
+
blacklist_table.should_receive(:where).
|
81
|
+
with({ :user => "b"}).
|
82
|
+
and_return( missing_dataset )
|
83
|
+
|
84
|
+
@bot.stub!(:db).and_return({
|
85
|
+
:blacklist => blacklist_table
|
86
|
+
})
|
87
|
+
@bot.on_global_blacklist?("a").should == true
|
88
|
+
@bot.on_global_blacklist?("b").should == false
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
describe "db interaction" do
|
94
|
+
before(:each) do
|
95
|
+
@db_tmp = Tempfile.new("blacklist.db")
|
96
|
+
@db_uri = "sqlite://#{@db_tmp.path}"
|
97
|
+
|
98
|
+
@bot = Chatterbot::Bot.new
|
99
|
+
@bot.config[:db_uri] = @db_uri
|
100
|
+
@bot.db
|
101
|
+
end
|
102
|
+
|
103
|
+
describe "add_to_blacklist" do
|
104
|
+
it "adds to the blacklist table" do
|
105
|
+
@bot.add_to_blacklist("tester")
|
106
|
+
end
|
107
|
+
|
108
|
+
it "doesn't add a double entry" do
|
109
|
+
@bot.add_to_blacklist("tester")
|
110
|
+
@bot.add_to_blacklist("tester")
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|