forked from ace3df/AcePictureBot
/
acepicturebot.py
525 lines (470 loc) · 18.1 KB
/
acepicturebot.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
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
from spam_checker import remove_all_limit
from spam_checker import user_spam_check
import xml.etree.ElementTree as etree
from collections import OrderedDict
from utils import printf as print
from threading import Thread
from itertools import islice
from config import settings
from config import update
import functions as func
import urllib.request
import datetime
import random
import tweepy
import utils
import time
import sys
import os
import re
__program__ = "AcePictureBot"
__version__ = "2.6.0"
DEBUG = False
def post_tweet(_API, tweet, media="", command=False, rts=False):
try:
if media:
media = media.replace("\\", "\\\\")
if rts and command:
print("[{0}] Tweeting: {1} ({2}): [{3}] {4}".format(
time.strftime("%Y-%m-%d %H:%M"),
rts.user.screen_name, rts.user.id,
command, tweet))
else:
print("[{0}] Tweeting: {1}".format(
time.strftime("%Y-%m-%d %H:%M"),
tweet))
if rts:
if media:
print("(Image: {0})".format(media))
_API.update_with_media(media, status=tweet,
in_reply_to_status_id=rts.id)
else:
_API.update_status(status=tweet,
in_reply_to_status_id=rts.id)
else:
if media:
print("(Image: {0})".format(media))
_API.update_with_media(media, status=tweet)
else:
_API.update_status(status=tweet)
except:
pass
def tweet_command(_API, status, tweet, command):
tweet_image = False
user = status.user
# Mod command
is_mod = [True if str(user.id) in MOD_IDS else False][0]
if command == "DelLimits":
if is_mod:
their_id, cmd = tweet.split(' ', 2)
remove_all_limit(their_id, cmd)
print("[INFO] Removed limits for {0} - {1}".format(
their_id, cmd))
return False, False
if str(user.id) not in PATREON_IDS:
if not is_mod:
user_is_limited = user_spam_check(user.id,
user.screen_name, command)
if isinstance(user_is_limited, str):
# User hit limit, tweet warning
command = ""
tweet = user_is_limited
elif not user_is_limited:
# User is limited, return
print("[{0}] User is limited! Ignoring...".format(
time.strftime("%Y-%m-%d %H:%M")))
return False
if settings['count_on']:
func.count_trigger(command, user.id)
if command == "DiscordConnect":
tweet = func.DiscordConnect(tweet, user.id)
if command == "DiscordJoin":
tweet = re.sub('http\S+', '', tweet).strip()
for url in status.entities['urls']:
tweet += "" + url['expanded_url']
break
tweet = func.DiscordJoin(tweet)
# Joke Commands
if command == "spook":
tweet, tweet_image = func.spookjoke()
if command == "Spoiler":
tweet = random.choice(utils.file_to_list(
os.path.join(settings['list_loc'],
"spoilers.txt")))
elif command == "!Level":
tweet = func.get_level(user.id)
# Main Commands
if command == "Waifu":
tweet, tweet_image = func.waifu(0, tweet)
elif command == "Husbando":
tweet, tweet_image = func.waifu(1, tweet)
gender = utils.gender(status.text)
if gender == 0:
g_str = "waifu"
else:
g_str = "husbando"
if "Register" in command:
follow_result = is_following(user.id)
if follow_result == "Limited":
tweet = ("The bot is currently limited on checking stuff.\n"
"Try again in 15 minutes!")
func.remove_one_limit(user.id, g_str.lower() + "register")
elif follow_result == "Not Genuine":
tweet = ("Your account wasn't found to be genuine.\n"
"Help: {url}").format(
url=func.config_get('Help URLs', 'not_genuine'))
elif not follow_result:
tweet = ("You must follow @AcePictureBot to register!\n"
"Help: {url}").format(
url=func.config_get('Help URLs', 'must_follow'))
else:
tweet, tweet_image = func.waifuregister(user.id,
user.screen_name,
tweet, gender)
if "My" in command:
skip_dups = False
if "my{g_str}+".format(g_str=g_str) in tweet.lower():
skip_dups = True
if "my{g_str}-".format(g_str=g_str) in tweet.lower():
func.delete_used_imgs(str(user.id), False)
tweet, tweet_image = func.mywaifu(user.id, gender, False, skip_dups)
if "Remove" in command:
tweet = func.waifuremove(user.id, gender)
if command == "OTP":
tweet, tweet_image = func.otp(tweet)
# TODO: Remove this over sometime and change kohai to kouhai on the site
if command == "Kohai":
command = "Kouhai"
list_cmds = ["Shipgirl", "Touhou", "Vocaloid",
"Imouto", "Idol", "Shota",
"Onii", "Onee", "Sensei",
"Monstergirl", "Witchgirl", "Tankgirl",
"Senpai", "Kouhai"]
if command in list_cmds:
tweet, tweet_image = func.random_list(command, tweet)
if command == "Airing":
tweet = func.airing(tweet)
# No results found.
if not tweet:
return False
if command == "Source":
tweet = func.source(_API, status)
if tweet:
tweet = "@{0} {1}".format(user.screen_name, tweet)
post_tweet(_API, tweet, tweet_image, command, status)
def acceptable_tweet(status):
global USER_LAST_COMMAND
global IGNORE_WORDS
global BLOCKED_IDS
tweet = status.text
user = status.user
# Ignore ReTweets.
if tweet.startswith('RT'):
return False, False
if DEBUG:
if str(user.id) not in MOD_IDS:
return False, False
# Reload in case of manual updates.
BLOCKED_IDS = utils.file_to_list(
os.path.join(settings['list_loc'],
"Blocked Users.txt"))
PATREON_IDS = utils.file_to_list(
os.path.join(settings['list_loc'],
"patreon_users.txt"))
IGNORE_WORDS = utils.file_to_list(
os.path.join(settings['list_loc'],
"Blocked Words.txt"))
# Ignore bots and bad boys.
if str(user.id) in BLOCKED_IDS:
return False, False
# Ignore some messages.
if any(word.lower() in tweet.lower()
for word in IGNORE_WORDS):
return False, False
# Make sure the message has @Bot in it.
if not any("@" + a.lower() in tweet.lower()
for a in settings['twitter_track']):
return False, False
# If the user @sauce_plz add "source" to the text
if "sauce" in tweet.lower():
tweet += " source"
# Remove extra spaces.
tweet = re.sub(' +', ' ', tweet).lstrip()
# Remove @UserNames (usernames could trigger commands alone)
tweet = tweet.replace("🚢👧", "Shipgirl")
tweet = ' '.join(
re.sub('(^|\n| )(@[A-Za-z0-9_🚢👧.+-]+)', ' ', tweet).split())
tweet = tweet.replace("#", "")
# Find the command they used.
command = utils.get_command(tweet)
if command == "WaifuRegister" or command == "HusbandoRegister" \
or command == "DiscordConnect":
# Cut the text off after the command word.
reg = "({0})(?i)".format(command)
if len(tweet) > (len(command) +
len(settings['twitter_track'][0]) + 2):
tweet = re.split(reg, tweet)[2].lstrip()
# No command is found see if acceptable for a random waifu.
if not command:
# Ignore quote ReTweets.
if tweet.startswith('"@'):
return False, False
# Ignore if it doesn't mention the main bot only.
if settings['twitter_track'][0] not in status.text:
return False, False
# Last case, check if they're not replying to a tweet.
if status.in_reply_to_status_id is None:
command = "Waifu"
else:
return False, False
if command == "Reroll":
try:
command = utils.get_command(USER_LAST_COMMAND[user.id])
if "Register" in command:
return False, False
elif "My" in command:
return False, False
elif command is False:
return False, False
except (ValueError, KeyError):
return False, False
else:
USER_LAST_COMMAND[user.id] = tweet
if len(USER_LAST_COMMAND) > 30:
USER_LAST_COMMAND = (OrderedDict(
islice(USER_LAST_COMMAND.items(),
20, None)))
# Stop someone limiting the bot on their own.
rate_time = datetime.datetime.now()
rate_limit_secs = 10800
rate_limit_user = 20
if str(user.id) in PATREON_IDS:
# Still a limit just in case
rate_limit_user = 35
if user.id in RATE_LIMIT_DICT:
# User is now limited (3 hours).
if ((rate_time - RATE_LIMIT_DICT[user.id][0])
.total_seconds() < rate_limit_secs)\
and (RATE_LIMIT_DICT[user.id][1] >= rate_limit_user):
return False, False
# User limit is over.
elif ((rate_time - RATE_LIMIT_DICT[user.id][0])
.total_seconds() > rate_limit_secs):
del RATE_LIMIT_DICT[user.id]
else:
# User found, not limited, add one to the trigger count.
RATE_LIMIT_DICT[user.id][1] += 1
else:
# User not found, add them to RATE_LIMIT_DICT.
# Before that quickly go through RATE_LIMIT_DICT
# and remove all the finished unused users.
for person in list(RATE_LIMIT_DICT):
if ((rate_time - RATE_LIMIT_DICT[person][0])
.total_seconds() > rate_limit_secs):
del RATE_LIMIT_DICT[person]
RATE_LIMIT_DICT[user.id] = [rate_time, 1]
# This shouldn't happen but just in case.
if not isinstance(command, str):
return False, False
tweet = tweet.lower().replace(command.lower(), " ", 1).strip()
return tweet, command
def is_following(user_id):
try:
user_info = API.get_user(user_id)
except tweepy.TweepError:
return "Limited"
if user_info.statuses_count < 10:
return "Not Genuine"
elif user_info.followers_count < 5:
return "Not Genuine"
try:
ship = API.lookup_friendships(user_ids=(2910211797, user_id))
except tweepy.TweepError:
return "Limited"
try:
return ship[1].is_followed_by
except TypeError:
# Account doesn't exist anymore.
return False
def status_account(status_api):
""" Read RSS feeds and post them on the status Twitter account.
:param status_api: The Tweepy API object for the status account.
"""
def read_rss(url, name, pre_msg, find_xml):
recent_id = open(os.path.join(settings['ignore_loc'],
name), 'r').read()
try:
rss = urllib.request.urlopen(url).read().decode("utf-8")
xml = etree.fromstring(rss)
except:
# Don't need anymore than this for something like this
print("Failed to read/parse {0} ({1}) RSS".format(name, url))
return False
if bool(find_xml['sub_listing']):
entry = xml[0][find_xml['entries_in']]
else:
entry = xml[find_xml['entries_in']]
current_id = entry.findtext(
find_xml['entry_id'])
if current_id == recent_id:
return False
with open(os.path.join(settings['ignore_loc'], name), "w") as f:
f.write(current_id)
if bool(find_xml['get_href']):
msg_url = entry.find(find_xml['link_id']).get('href')
else:
msg_url = entry.findtext(find_xml['link_id'])
msg_msg = re.sub('<[^<]+?>', '', entry.findtext(find_xml['msg_id']))
msg_msg = re.sub(' +', ' ', os.linesep.join(
[s for s in msg_msg.splitlines() if s])).lstrip()
msg = "{0}{1}\n{2}".format(pre_msg,
utils.short_string(msg_msg, 90),
msg_url)
post_tweet(status_api, msg)
while True:
url = "https://github.com/ace3df/AcePictureBot/commits/master.atom"
name = "GitCommit.txt"
pre_msg = "[Git Commit]\n"
find_xml = {"sub_listing": False,
"entries_in": 5,
"entry_id": "{http://www.w3.org/2005/Atom}id",
"link_id": "{http://www.w3.org/2005/Atom}link",
"get_href": True,
"msg_id": "{http://www.w3.org/2005/Atom}content"}
read_rss(url, name, pre_msg, find_xml)
time.sleep(3 * 60)
class CustomStreamListener(tweepy.StreamListener):
def on_status(self, status):
global HAD_ERROR
global HANG_TIME
global TWEETS_READ
HANG_TIME = time.time()
tweet, command = acceptable_tweet(status)
if not command:
return True
try:
open(update['is_busy_file'], 'w')
except PermissionError:
# This wont happen all the time, the file is probably busy
pass
print("[{0}] Reading: {1} ({2}): {3}".format(
time.strftime("%Y-%m-%d %H:%M"),
status.user.screen_name, status.user.id, status.text))
tweet_command(API, status, tweet, command)
HAD_ERROR = False
TWEETS_READ.append(str(status.id))
with open(os.path.join(settings['ignore_loc'],
"tweets_read.txt"),
'w') as file:
file.write("\n".join(TWEETS_READ))
try:
os.remove(update['is_busy_file'])
except (PermissionError, FileNotFoundError):
# Related to above PermissionError.
pass
def on_error(self, status_code):
global LAST_STATUS_CODE
global HANG_TIME
HANG_TIME = time.time()
if int(status_code) != int(LAST_STATUS_CODE):
LAST_STATUS_CODE = status_code
msg = ("[{0}] Twitter Returning Status Code: {1}.\n"
"More Info: "
"https://dev.twitter.com/overview/api/response-codes"
).format(time.strftime("%Y-%m-%d %H:%M"), status_code)
print(msg)
post_tweet(func.login(status=True), msg)
return True
def start_stream(sapi=None):
if sapi is None:
sapi = func.login(rest=False)
try:
stream_sapi = tweepy.Stream(sapi, CustomStreamListener())
print("[INFO] Reading Twitter Stream!")
stream_sapi.filter(
track=[x.lower() for x in settings['twitter_track']], async=True)
except (KeyboardInterrupt, SystemExit):
stream_sapi.disconnect()
sys.exit(0)
return stream_sapi
def handle_stream(sapi, status_api=False):
stream_sapi = start_stream(sapi)
global HANG_TIME
while True:
time.sleep(5)
elapsed = (time.time() - HANG_TIME)
if elapsed > 600:
# TODO: Temp to try and stop crash tweet spam for now
if os.path.exists(update['last_crash_file']):
if time.time() - os.path.getctime(update['last_crash_file']) > 80000:
os.remove(update['last_crash_file'])
open(update['last_crash_file'], 'w')
msg = """[{0}] Restarting!
The bot will catch up on missed messages now!""".format(
time.strftime("%Y-%m-%d %H:%M"))
if status_api:
post_tweet(status_api, msg)
else:
print(msg)
stream_sapi.disconnect()
time.sleep(3)
if not stream_sapi.running:
stream_sapi = start_stream(sapi)
Thread(target=read_notifications,
args=(API, True, TWEETS_READ)).start()
HANG_TIME = time.time()
def read_notifications(_API, reply, tweets_read):
statuses = _API.mentions_timeline()
print("[INFO] Reading late tweets!")
for status in reversed(statuses):
if str(status.id) in TWEETS_READ:
continue
tweet, command = acceptable_tweet(status)
if not command:
continue
if reply:
print("[{0}] Reading (Late): {1} ({2}): {3}".format(
time.strftime("%Y-%m-%d %H:%M"),
status.user.screen_name, status.user.id,
status.text))
tweet_command(_API, status, tweet, command)
TWEETS_READ.append(str(status.id))
with open(os.path.join(settings['ignore_loc'],
"tweets_read.txt"),
'w') as file:
file.write("\n".join(TWEETS_READ))
print("[INFO] Finished reading late tweets!")
if __name__ == '__main__':
BLOCKED_IDS = utils.file_to_list(
os.path.join(settings['list_loc'],
"Blocked Users.txt"))
PATREON_IDS = utils.file_to_list(
os.path.join(settings['list_loc'],
"patreon_users.txt"))
IGNORE_WORDS = utils.file_to_list(
os.path.join(settings['list_loc'],
"Blocked Words.txt"))
LIMITED = False
HAD_ERROR = False
LAST_STATUS_CODE = 0
TWEETS_READ = []
MOD_IDS = ["2780494890", "121144139"]
RATE_LIMIT_DICT = {}
USER_LAST_COMMAND = OrderedDict()
START_TIME = time.time()
HANG_TIME = time.time()
API = None
STATUS_API = None
SAPI = None
TWEETS_READ = utils.file_to_list(
os.path.join(settings['ignore_loc'],
"tweets_read.txt"))
# TODO: TEMP (Read above)
open(update['last_crash_file'], 'w')
API = func.login(rest=True)
SAPI = func.login(rest=False)
STATUS_API = func.login(status=True)
read_notifications(API, True, TWEETS_READ)
Thread(target=status_account, args=(STATUS_API, )).start()
Thread(target=func.check_website).start()
handle_stream(SAPI, STATUS_API)