-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathpinabot.rb
153 lines (128 loc) · 5.53 KB
/
pinabot.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
require 'bundler/setup'
require 'i18n'
Bundler.require
Dotenv.load
# open our SQLite database for storing replies and managing unsubscribe requests
DB = SQLite3::Database.new('db/development.sqlite3')
### THESE VARIABLES MUST BE CONFIGURED PRIOR TO RUNNING ###
VERSION = '0.0.1' # you should increment this on major changes
AUTHOR = '' # reddit username of bot's author/handler
BOTNAME = '' # you can use this to stylize the bot's name since reddit doesn't allow accents in usernames
OPTOUT = '' # bot handles opt-outs and this is the trigger word
SUB = '' # subreddit to monitor (unfortunately only one at this time)
# these are ideally set via environment variables or using the .env file (see README.md), but can be hardwired here
REDD_CLIENT = ENV['REDD_CLIENT']
REDD_SECRET = ENV['REDD_SECRET']
REDD_USERNAME = ENV['REDD_USERNAME']
REDD_PASSWORD = ENV['REDD_PASSWORD']
# here's the general template for what the bot posts when triggered (uses Reddit markdown syntax)
REPLY_TEMPLATE = <<-ENDTEMPLATE
You activated #{BOTNAME}, %{commenter}!
%{reply}
---
^^Suggestions? ^^Shoot ^^a ^^message ^^to ^^[#{AUTHOR}](https://www.reddit.com/message/compose/?to=#{AUTHOR}). ^^Reply ^^`#{OPTOUT}` ^^to ^^this ^^message ^^to ^^unsubscribe. ^^Thanks!
ENDTEMPLATE
# set up a session using environment variables or hardcoded variables above
SESSION = Redd.it(
user_agent: "redd:#{I18n.transliterate(BOTNAME).downcase}:v#{VERSION} (by /u/#{AUTHOR})",
client_id: REDD_CLIENT,
secret: REDD_SECRET,
username: REDD_USERNAME,
password: REDD_PASSWORD
)
# here are the sets of triggers and their replies
DICTIONARY = [
{
# Yuli the Piña
triggers: [
'pina',
'piña',
'pineapple',
'yuli',
'gurriel'
],
replies: [
'[Mmm... pineapple!](https://i.imgur.com/gw7lBej.jpg)',
'[Did somebody mention *la piña*?](https://i.imgur.com/LNpv5pL.jpg)',
'[(⌐■_■)](https://i.imgur.com/GkpzeKx.jpg)',
'[Piña!](https://i.imgur.com/VkpGOCw.jpg)',
'[Piña Power?](https://i.imgur.com/QqGHuuq.jpg)',
'[Piña Power!](https://i.imgur.com/8z7Ds5g.jpg)',
'[Beach piña!](https://i.imgur.com/7aMkyX7.jpg)',
'[Suited up?](https://i.imgur.com/LCrfDbw.jpg)'
]
},
{
# Take It Back!
triggers: [
'#takeitback',
'takeitback',
'take it back'
],
replies: [
'[Verlander wants to \#TakeItBack!](https://i.imgur.com/ycNuaUv.jpg)',
'[Altuve wants to \#TakeItBack!](https://i.imgur.com/A9te6cQ.jpg)',
'[Houston wants to \#TakeItBack!](https://i.imgur.com/W4BG341.jpg)',
'[Reddick wants to \#TakeItBack!](https://i.imgur.com/Iblpwpb.jpg)',
'[Diaz wants to \#TakeItBack!](https://i.imgur.com/aSs3ozC.jpg)',
'[Verlander wants to \#TakeItBack!](https://i.imgur.com/yiifafW.jpg)'
]
}
]
# convert the triggers into useable regular expression object
DICTIONARY.each { |d| d[:regex] = Regexp.union(d[:triggers]) }
# rotate the comments array and pick the first (roundrobin instead of random)
def trigger_a_comment(body)
DICTIONARY.each { |d| return d[:replies].rotate!.first if body =~ /\b#{d[:regex].source}\b/i }
nil # return nil since we didn't find any trigger words
end
# save space in the database by trimming off '/r/subreddit/comments/'
def trim_permalink(link)
link.sub!(/^(\/r.*comments\/)/, '')
link # return the trimmed link
end
# don't reply to the bot's comments or users who have unsubscribed
def unsubscribed?(commenter)
return true if commenter.downcase == REDD_USERNAME.downcase ||
DB.get_first_value('select count(*) from unsubscribed where LOWER(username) = ?', commenter.downcase) > 0
end
# wait ten seconds from script start to actually start processing comments (we only want new comments)
READY_TIME = Time.now.utc + 10
SESSION.subreddit(SUB).comment_stream( {limit: 0} ) do |comment|
next if Time.now.utc < READY_TIME # we delay because we only want new comments, not existing ones
commenter = comment.author.name
# handle unsubscribes
if comment.body =~ /#{OPTOUT}/i && !unsubscribed?(commenter)
begin
DB.execute 'insert into unsubscribed (username) values (?)', [commenter.downcase]
puts "#{commenter} unsubscribed."
rescue SQLite3::ConstraintException
puts "#{commenter} already unsubscribed."
rescue
puts "SQLite error looking up #{commenter} in unsubscribed usernames."
end
next
end
# only reply if user hasn't unsubscribed and the comment includes a trigger word
if (reply = trigger_a_comment(comment.body)) && !unsubscribed?(commenter)
permalink = trim_permalink(comment.permalink) # save space in database by trimming permalink
# only reply if we haven't replied already
unless DB.get_first_value('select count(*) from permalinks where link = ?', permalink) > 0
puts "#{commenter} triggered."
# keep track of this comment, so we don't reply to it again
begin
DB.execute 'insert into permalinks (link) values (?)', [permalink]
rescue
puts "SQLite error saving #{permalink}"
end
# attempt to post comment -- retry if problem with reddit API
handler = Proc.new do |exception, attempt_number, total_delay|
puts "Handler saw a #{exception.class}; retry attempt #{attempt_number}; #{total_delay} seconds have passed."
end
with_retries(:max_tries => 5, :handler => handler) do |attempt|
# interpolate our custom reply into the reply template and reply to the comment
comment.reply REPLY_TEMPLATE % {commenter: commenter, reply: reply}
end
end
end
end