-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.py
326 lines (280 loc) · 13.4 KB
/
main.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
import datetime
import time
import re
import sys, argparse
import praw
import googleapiclient.discovery
import keyring
# GLOBAL VARIABLES
# PASSWORDS
CLIENT_ID = keyring.get_password('MasterEditor', 'client-id')
CLIENT_SECRET = keyring.get_password('MasterEditor', 'client-secret')
REDDIT_PASSWORD = keyring.get_password('MasterEditor', 'reddit-password')
REDDIT_USERNAME = keyring.get_password('MasterEditor', 'reddit-username')
YOUTUBE_KEY = keyring.get_password('MasterEditor', 'youtube-key')
# UTILITIES
ACTIVITY_CHECK = False
FORCE_DAILY_CHECK = False
# FUNCTIONS
def initialize_reddit():
try:
reddit = praw.Reddit(client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
user_agent='AMVBot:v0.0.0 (by u/Zbynasuper)',
username=REDDIT_USERNAME,
password=REDDIT_PASSWORD)
except praw.exceptions.RedditAPIException as exception:
for subexception in exception.items:
log(f'Reddit initialization failed. Error: {subexception.error_type}')
return reddit
def post_feedback_megathread(subreddit_name='amv'):
reddit = initialize_reddit()
sub = reddit.subreddit(subreddit_name)
with open('megathread_template.txt', mode='r', encoding='utf-8') as megathread_template:
selftext = f'# FEEDBACK MEGATHREAD\n\n# {datetime.date.today().strftime("%B %Y")}\n\n{megathread_template.read()} '
megathread = sub.submit(title=f'Feedback MEGAthread - {datetime.date.today().strftime("%B %Y")}',
selftext=selftext)
widgets = sub.widgets
for widget in widgets.sidebar:
if widget.shortName == 'Megathreads':
for button in widget:
if 'Feedback' in button.text:
old_megathread = reddit.submission(url=button.url)
break
break
if old_megathread.stickied:
old_megathread.mod.sticky(state=False)
megathread.mod.sticky()
megathread.mod.flair(text='Megathread', flair_template_id='23f368e6-f498-11e7-8211-0e87da16ebac')
megathread.mod.suggested_sort(sort='new')
sidebar_before, _, sidebar_after = sub.description.partition(f'{old_megathread.url}')
new_sidebar = sidebar_before + megathread.url + sidebar_after
sub.mod.update(description=new_sidebar)
new_button = button.__dict__
new_button['url'] = megathread.url
new_button.pop('_reddit')
widget.mod.update(buttons=[new_button])
return megathread
def check_youtube_video_length(videoURL):
if args.verbosity:
log(f'Checking length of video at address {videoURL}.')
if '//youtu.be' in videoURL:
_, _, videoID = videoURL.rpartition('//youtu.be/')
elif 'youtube' in videoURL:
_, _, videoID = videoURL.rpartition('v=')
videoID, _, _ = videoID.partition('&')
else:
raise AttributeError('Link is not a youtube video.')
youtube = googleapiclient.discovery.build('youtube', 'v3', developerKey=YOUTUBE_KEY)
request = youtube.videos().list(part='contentDetails', id=videoID)
response = request.execute()
duration = response['items'][0]['contentDetails']['duration']
if args.verbosity:
log(f'Duration of the video: {duration}')
return duration
def remove_submission(submission, reason):
if args.test:
log(f'The submission {submission.title} ({submission.shortlink}) would be removed because of: {reason}, but test mode is ON.')
return True
log(f'The submission {submission.title} ({submission.shortlink}) was removed because: {reason}')
removal_comment = submission.reply(f'Your submission has been removed because of following reason: {reason}'
f'\n '
f'\n Beep Boop, this action was perfomed by a bot. If you believe this was a mistake, please message the moderators of this subreddit with a link to this submission.')
removal_comment.mod.distinguish(how='yes', sticky=True)
submission.mod.remove()
return True
def author_activity_check(submission):
if args.verbosity:
log('Performing check for author\'s past activity on subreddit.')
author = submission.author
comment_count = 0
try:
for comment in author.comments.new(limit=None):
if comment.subreddit_id == 't5_2qpg3':
if args.verbosity:
log(f'A comment has been found on submission {comment.submission.title} - ({comment.submission.shortlink}).')
comment_count += 1
if comment_count == 6:
if args.verbosity:
log('Author has passed the activity check.')
return True
elif int(comment.created_utc) + 15778800 <= int(time.time()): # If the comment is older than 6 months
return False
except StopIteration:
return False
def daily_checks():
if args.verbosity:
log('Performing daily checks.')
# Reset crash counter to avoid unnecessary crashes in a long run
global times_crashed
times_crashed = 0
# Checks for a first day in a month
today = datetime.date.today()
if today.day == 1:
megathread = post_feedback_megathread()
log(f'Feedback MEGAthread posted: ({megathread.shortlink})')
return True
def regular_moderation(submission):
if args.verbosity:
log(f'Running moderation loop on submission {submission.title} - ({submission.shortlink})\n'
f'Checking if the submission is from a mod, approved contributor or already approved manually.')
# If it's approved, from a moderator or approved submitter, then don't moderate it
author = submission.author
mod_check = subreddit.moderator(redditor=author)
contributor_check = subreddit.contributor(redditor=author)
if submission.approved or (mod_check.children.__len__() > 0):
if args.verbosity:
log('Not moderating, moving on...')
return True
try:
next(contributor_check)
if args.verbosity:
log('Not moderating, moving on...')
return True
except StopIteration:
if args.verbosity:
log('Passing submission to moderation.')
pass
if not author_activity_check(submission):
if ACTIVITY_CHECK:
remove_submission(submission, 'You have less than 6 comments in last 6 months on this subreddit.')
return True
elif args.verbosity:
log('Author\'s activity check has failed, but the feature is not yet active so moving on...')
if args.verbosity:
log('Checking account age.')
if (int(time.time()) - author.created_utc) < 259200: # Account younger than 3 days
remove_submission(submission,
f'Your account needs to be at least 3 days old to be able to post on the main page. \n'
f'If you are not posting an AMV and need an exception (e.g. contest announcement), please message the mods.')
return True
# Video length checking
# If submission is a link, hopefully to youtube
if not (submission.is_self or submission.is_video):
if args.verbosity:
log('Submission is a link, checking if it\'s a video and it\'s length.')
try:
duration = check_youtube_video_length(submission.url)
if 'M' not in duration:
remove_submission(submission,'Video is too short. We only allow videos longer than 1 minute on the main page.')
return True
except AttributeError:
submission.report(
'Check manually, link being shared is NOT youtube.') # If not link to youtube, report for manual check
log(f'Submission \"{submission.title}\" ({submission.shortlink}) has been reported as the link is not Youtube.')
except IndexError:
remove_submission(submission, 'Youtube video is being blocked or unaccessible.')
return True
# If submission is a reddit video
elif submission.is_video:
if args.verbosity:
log('Submission is a reddit video, checking length.')
if submission.media['reddit_video']['duration'] <= 60:
remove_submission(submission,
'Video is too short. We only allow videos longer than 1 minute on the main page.')
return True
# Title check
if args.verbosity:
log('Checking title of the submission.')
if re.findall(r'[A-Z]{5}', submission.title):
remove_submission(submission, 'Title contains excessive Caps Lock.')
return True
elif re.findall(r'[^\sa-zA-Z0-9,.“”:;\-\'!?|\"&*+/=^_\[\]()]', submission.title):
remove_submission(submission, 'Non-standard and\or non-english characters used in the title.')
return True
if args.verbosity:
log('Passed all moderation checks, checking if 24 hours passed from last daily check')
# Run daily checks if at least 24 hours since last check
global timer
if time.time() - 86400 > timer or FORCE_DAILY_CHECK:
timer = time.time()
daily_checks()
# TODO copy other stuff from Automod
# TODO account karma/age gate - number of comments in subreddit in last 6 months
def log(log_message):
print(log_message)
if args.logging_file:
log_file = f'{args.logging_file}.txt'
elif args.verbosity or args.test:
log_file = f'bot_logging_test_{datetime.date.today().strftime("%d_%m_%y")}.txt' # if -t then log into separate file
else:
log_file = 'bot_logging.txt'
try:
with open(log_file, 'a') as file:
x = datetime.datetime.now()
file.write(f'{x.strftime("%d %b %Y %H:%M:%S")} - {log_message}\n')
except FileNotFoundError:
with open(log_file, 'w+') as file:
pass
log(log_message)
return True
if __name__ == '__main__':
# Parsing command-line arguments
parser = argparse.ArgumentParser()
parser.add_argument('-t', '--test', action='store_true',
help='Runs in a test mode and won\'t make any modifications. Also logs to a separate log file (unless -l is specified). Implies -v.')
parser.add_argument('-l', '--logging-file',
help='Specifies a separate logging file. If it doesn\' exist, it will be created. If it exists, additional logs will be appended to it.')
parser.add_argument('-s', '--submission',
help='Runs only one test moderation cycle against a submission specified by it\'s ID.')
parser.add_argument('-S', '--submission-test', help='Same as -s with automatic test mode -t (and -v logging)')
parser.add_argument('-v', '--verbosity', action='store_true',
help='Makes the program log more stuff. Useful only for debugging/testing.')
parser.add_argument('-r', '--subreddit-name', help='Specifies a subreddit to run on. Default = r/amv.',
default='amv')
args = parser.parse_args()
if args.submission_test:
args.submission = args.submission_test
args.test = True
# Log bootup message here because logging file depends on args.test that might be changed above by args.submission_test
log(f'\n'
f'********************************************'
f' STARTING UP '
f'********************************************'
f'\n')
if args.test:
args.verbosity = True
log(f'Warning: Running in test mode! There will be no changes done to the subreddit!')
if args.logging_file:
log(f'Logs will be save to file {args.logging_file}.txt')
if args.submission:
log(f'Running only one moderation loop against submission ID {args.submission}')
times_crashed = 0
reddit = initialize_reddit()
subreddit = reddit.subreddit(args.subreddit_name)
if args.verbosity:
log(f'Subreddit {args.subreddit_name} initialized.')
timer = int(time.time())
if args.submission:
submission = initialize_reddit().submission(id=args.submission)
try:
submission.title
except:
log(f'Wrong ID {args.submission} passed as argument to option -s (-submission).')
sys.exit(1)
regular_moderation(submission)
if args.verbosity:
log('Shutting down due to -s option...')
sys.exit(0)
# Running the main loop, restarts itself up to two times before collapsing in blood on the floor for good.
while True:
try:
for submission in subreddit.stream.submissions():
regular_moderation(submission)
except KeyboardInterrupt:
log('Shutting down...')
break
except Exception as e:
times_crashed += 1
if times_crashed <= 2:
log(f'Crashed because of a following error: {e}.')
log(f'Will try to restart in 5 minutes')
time.sleep(300)
log('Restarting...')
continue
else:
log(f'Crashed because of a following error: {e}.')
log(f'Automatic restart disabled because program has crashed {times_crashed} times since last manual check.')
admin = reddit.redditor('Zbynasuper')
admin.message('MasterEditor has crashed.', 'Hey, your awesome bot has crashed for some reason. Check me out plz.')
break