-
Notifications
You must be signed in to change notification settings - Fork 7
/
bot.py
288 lines (246 loc) · 11.3 KB
/
bot.py
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
import requests
import random
import json
import praw
import re
import os
def remove_duplicates(seq):
seen = set()
seen_add = seen.add
return [x for x in seq if not (x in seen or seen_add(x))]
class Card:
CARD_IMAGE_BASE_URL = 'http://www.legends-decks.com/img_cards/{}.png'
CARD_IMAGE_404_URL = 'http://imgur.com/1Lxy3DA'
JSON_DATA = []
KEYWORDS = ['Prophecy', 'Breakthrough', 'Guard', 'Regenerate', 'Charge', 'Ward', 'Shackle',
'Lethal', 'Pilfer', 'Last Gasp', 'Summon', 'Drain']
PARTIAL_MATCH_END_LENGTH = 20
@staticmethod
def preload_card_data(path='data/cards.json'):
dir = os.path.dirname(__file__)
filename = os.path.join(dir, path)
with open(filename) as f:
Card.JSON_DATA = json.load(f)
@staticmethod
def _escape_name(card):
return re.sub(r'[\s_\-"\',;{\}]', '', card).lower()
@staticmethod
def _img_exists(url):
req = requests.get(url)
return req.headers['content-type'] == 'image/png'
@staticmethod
def _extract_keywords(text):
expr = re.compile(r'((?<!Gasp:\s)\w+(?:\sGasp)?)', re.I)
# If the card is an item, remove the +x/+y from its text.
text = re.sub(r'\+\d/\+\d', '', text)
words = expr.findall(text)
# Keywords are extracted until a non-keyword word is found
keywords = []
for word in words:
word = word.title()
if word in Card.KEYWORDS:
keywords.append(word)
else:
break
return remove_duplicates(keywords)
@staticmethod
def _fetch_data_partial(name):
i = 0
matches = ['', '']
while len(matches) > 1 and i <= Card.PARTIAL_MATCH_END_LENGTH:
matches = [s for s in Card.JSON_DATA if Card._escape_name(s['name']).startswith(Card._escape_name(name[:i]))]
i += 1
if len(matches) == 0:
return None
match = matches[0]
if Card._escape_name(match['name'])[:len(name)] == Card._escape_name(name):
return match
return None
@staticmethod
def get_info(name):
name = Card._escape_name(name)
if name == 'teslcardbot': # I wonder...
return Card('TESLCardBot', 'https://imgs.xkcd.com/comics/tabletop_roleplaying.png',
type='Bot',
attribute_1='Python',
attribute_2='JSON',
rarity='Legendary',
text='If your have more health than your opponent, win the game.',
cost='∞', power='∞', health='∞')
# If JSON_DATA hasn't been populated yet, try to do it now or fail miserably.
if len(Card.JSON_DATA) <= 0:
Card.preload_card_data()
assert (len(Card.JSON_DATA) > 0)
data = Card._fetch_data_partial(name)
if data is None:
return None
img_url = Card.CARD_IMAGE_BASE_URL.format(Card._escape_name(data['name']))
# Unlikely, but possible?
if not Card._img_exists(img_url):
img_url = Card.CARD_IMAGE_404_URL
name = data['name']
type = data['type']
attr_1 = data['attribute_1']
if 'attribute_2' in data:
attr_2 = data['attribute_2']
else:
attr_2 = ''
rarity = data['rarity']
unique = data['isunique'] == 'true'
cost = int(data['cost'])
text = data['text']
power = ''
health = ''
if type == 'creature':
power = int(data['attack'])
health = int(data['health'])
elif type == 'item':
# Stats granted by items are extracted from their text
stats = re.findall(r'\+(\d)/\+(\d)', text)
power, health = stats[0]
return Card(name=name,
img_url=img_url,
type=type,
attribute_1=attr_1,
attribute_2=attr_2,
rarity=rarity,
unique=unique,
cost=cost,
power=power,
health=health,
text=text)
def __init__(self, name, img_url, type='Creature', attribute_1='neutral',
attribute_2='', text='', rarity='Common', unique=False, cost=0, power=0, health=0):
self.name = name
self.img_url = img_url
self.type = type
self.attributes = [attribute_1.title(), attribute_2.title()] if len(attribute_2) > 0 else [attribute_1.title()]
self.rarity = rarity
self.unique = unique
self.cost = cost
self.power = power
self.health = health
self.text = text
self.keywords = Card._extract_keywords(text)
def __str__(self):
template = '[📷]({url} "{text}") {name} ' \
'| {type} | {stats} | {keywords} | {attrs} | {unique}{rarity}'
def _format_stats(t):
if self.type == 'creature':
return t.format(self.cost, self.power, self.health)
elif self.type == 'item':
return t.format(self.cost, '+{}'.format(self.power), '+{}'.format(self.health))
else:
return t.format(self.cost, '?', '?')
return template.format(
attrs='/'.join(map(str, self.attributes)),
unique='' if not self.unique else 'Unique ',
rarity=self.rarity.title(),
name=self.name,
url=self.img_url,
type=self.type.title(),
mana=self.cost,
stats=_format_stats('{} - {}/{}'),
keywords=', '.join(map(str, self.keywords)) + '' if len(self.keywords) > 0 else 'None',
text=self.text if len(self.text) > 0 else 'This card\'s name isn\'t in the database. Possible typo?'
)
class TESLCardBot:
CARD_MENTION_REGEX = re.compile(r'\{\{((?:.*?)+)\}\}')
@staticmethod
def find_card_mentions(s):
return remove_duplicates(TESLCardBot.CARD_MENTION_REGEX.findall(s))
def _get_praw_instance(self):
r = praw.Reddit('TES:L Card Fetcher by /u/{}.'.format(self.author))
r.login(username=os.environ['REDDIT_USERNAME'], password=os.environ['REDDIT_PASSWORD'], disable_warning=True)
return r
def _process_submission(self, s):
cards = TESLCardBot.find_card_mentions(s.selftext)
if len(cards) > 0 and not s.saved:
try:
self.log('Commenting in {} about the following cards: {}'.format(s.title, cards))
response = self.build_response(cards)
s.add_comment(response)
s.save()
self.log('Done commenting and saved thread.')
except:
self.log('There was an error while trying to leave a comment.')
raise
def _process_comment(self, c):
cards = TESLCardBot.find_card_mentions(c.body)
if len(cards) > 0 and not c.saved and c.author != os.environ['REDDIT_USERNAME']:
try:
self.log('Replying to {} about the following cards: {}'.format(c.id, cards))
response = self.build_response(cards)
c.reply(response)
c.save()
self.log('Done replying and saved comment.')
except:
self.log('There was an error while trying to reply.')
raise
# TODO: Make this template-able, maybe?
def build_response(self, cards):
response = 'Name | Type | Stats | Keywords | Attribute | ' \
'Rarity \n--|--|--|--|--|--|--\n'
cards_not_found = []
for name in cards:
card = Card.get_info(name)
if card is None:
cards_not_found.append(name)
else:
response += '{}\n'.format(str(card))
did_you_know = random.choice(['You can hover the camera emoji to read a card\'s text!',
'I can do partial matches!',
'I was made in Python 🐍',
'My code is open-source and anyone can contribute to it.',
'I might hide a few easter eggs.',
'You can send your suggestions to my creator, no matter how insignificant. '
'Or you can open an issue on GitHub.',
'My author doesn\'t actively monitor this sub, or my replies, so PM him if you need anything.',
])
auto_word = random.choice(['automatically', 'automagically'])
if len(cards_not_found) == len(cards):
response = 'I\'m sorry, but none of the cards you mentioned were matched. ' \
'Tokens and other generated cards will be included soon.\n'
elif len(cards_not_found) > 0:
response += '\n^(Some of the cards you mentioned were not matched: _{}._ ' \
'Tokens and other generated cards will be included soon.)\n'.format(', '.join(cards_not_found))
response += '\n**Did you know?** _{}_\n\n' \
'\n\n \n\n^(_I am a bot, and this action was performed {}. Made by user G3Kappa. ' \
'Special thanks to Jeremy at legends-decks._)' \
'\n\n[^Source ^Code](https://github.com/G3Kappa/TESLCardBot/) ^| [^Send ^PM](https://www.reddit.com/' \
'message/compose/?to={})'.format(did_you_know, auto_word, self.author)
return response
def log(self, msg):
print('TESLCardBot # {}'.format(msg))
def start(self, batch_limit=10, buffer_size=1000):
r = None
try:
r = self._get_praw_instance()
except praw.errors.HTTPException as e:
self.log('Reddit seems to be down! Aborting.')
self.log(e)
return
already_done = []
subreddit = r.get_subreddit(self.target_sub)
while True:
try:
new_submissions = [s for s in subreddit.get_new(limit=batch_limit) if s.id not in already_done]
new_comments = [c for c in r.get_comments(subreddit) if c.id not in already_done]
except praw.errors.HTTPException as e:
self.log('Reddit seems to be down! Aborting.')
self.log(e)
return
for s in new_submissions:
self._process_submission(s)
# The bot will also save submissions it replies to to prevent double-posting.
already_done.append(s.id)
for c in new_comments:
self._process_comment(c)
# The bot will also save comments it replies to to prevent double-posting.
already_done.append(c.id)
# If we're using too much memory, remove the bottom elements
if len(already_done) >= buffer_size:
already_done = already_done[batch_limit:]
def __init__(self, author='Anonymous', target_sub='all'):
self.author = author
self.target_sub = target_sub