-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcontest_bot.pyw
232 lines (198 loc) · 9.08 KB
/
contest_bot.pyw
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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Select winners from top level comments in a Reddit post"""
import csv
import praw
import string
import random
import logging
import configparser
import tkinter as tk
import tkinter.font as tkf
from tkinter import ttk
from tkinter import scrolledtext
from pathlib import Path
from logging.handlers import RotatingFileHandler
class MainWindow(tk.Frame):
def __init__(self, root, reddit, blacklist):
"""Initialize the window and construct its widgets.
Parameters
----------
root : Tk instance
This is a wrapper around a Tcl interpreter.
reddit : Reddit instance
This is used to access the Reddit API.
blacklist : list of str
A list of usernames to never select as winners
"""
super().__init__(root)
self.root = root
self.reddit = reddit
self.blacklist = blacklist
self.root.title("Contest Bot v1.2")
self.root.columnconfigure(1, weight=1)
self.root.rowconfigure(0, weight=1)
self.root.rowconfigure(1, weight=1)
self.root.rowconfigure(2, weight=999)
self.grid()
# Number of winners entry
tk.Label(self.root, text="Winners:").grid(row=0, column=0, sticky='W')
self.entry_winners = ttk.Entry(self.root)
self.entry_winners.grid(row=0, column=1, sticky="EW")
self.entry_winners.insert(0, '1')
# URL entry
tk.Label(self.root, text="URL:").grid(row=1, column=0, sticky='W')
self.entry_url = ttk.Entry(self.root)
self.entry_url.grid(row=1, column=1, sticky="EW")
self.entry_url.configure(state="readonly")
# Winners list
self.selected_winners = scrolledtext.ScrolledText(root)
self.selected_winners.grid(row=2, column=0, columnspan=3, sticky="NESW")
self.selected_winners.insert("1.0", "Winners will be listed here")
self.selected_winners.configure(state="disabled")
# Button to run program with URL in clipboard
self.button_url = ttk.Button(self.root, text="Paste URL", command=self.run, width=17)
self.button_url.grid(row=1, column=2)
# Set padding and minimal window size
self.vertical_padding = 2
self.horizontal_padding = 2
for child in self.root.winfo_children():
child.grid_configure(padx=self.horizontal_padding, pady=self.vertical_padding)
self.root.update()
self.root.minsize(*self.calculate_window_size(3))
self.root.geometry("0x0")
def calculate_window_size(self, viewable_lines):
"""
Calculate the window size necessary to view n lines in the output
textbox.
Parameters
----------
viewable_lines : int
This is the amount of lines required to be viewable in the output
textbox.
Returns
-------
tuple of ints
The new width and height to be passed to self.root.geometry.
"""
selected_winners_y_offset = self.selected_winners.winfo_rooty() - self.root.winfo_rooty() + self.vertical_padding + 4
font_height = tkf.Font(font=self.selected_winners["font"]).metrics("linespace")
return (400, selected_winners_y_offset + viewable_lines * font_height)
def set_selected_winners_text(self, text):
"""Sets the output textbox text.
Parameters
----------
text : string
This is the new text for the output textbox.
"""
self.selected_winners.configure(state="normal")
self.selected_winners.delete("1.0", tk.END)
self.selected_winners.insert("1.0", text)
self.selected_winners.configure(state="disabled")
self.selected_winners.update()
def get_winners(self, post_URL, n):
"""
Select winners from a reddit post, based only on the top level
comments, removing any duplications or blacklisted users. The
results are saved in a csv file.
Parameters
----------
post_URL : string
This is the URL of the contest, which is expected to be a Reddit
post.
n : int
The amount of winners to select without replacement.
Returns
-------
list of strings
Each string represents a line in the output with the format
"comment index) username".
"""
# Get all root comments from URL
submission = reddit.submission(url=post_URL)
submission.comments.replace_more(limit=None, threshold=1)
root_comments = list(submission.comments)
root_comments.sort(key=lambda comment: comment.created_utc) # Only take the latest root comment from an author
root_comments = filter(lambda comment: comment.author not in self.blacklist, root_comments)
deduplicated_comments = {comment.author : comment.body for comment in root_comments}
# Select n winners
winners = random.sample(list(deduplicated_comments.keys()), min(len(deduplicated_comments), n))
winners.sort(key=lambda author: list(deduplicated_comments.keys()).index(author))
# Store contest results in CSV file
records_path = Path(__file__).parent.resolve() / "Excel Contest Records"
records_path.mkdir(exist_ok=True)
with open(records_path / f"Contest_{submission.id}.csv", 'w', newline='') as records_fp:
writer = csv.writer(records_fp, escapechar = '\\', quoting=csv.QUOTE_NONE)
writer.writerow(['Won?', '#', 'Name', 'Comment'])
# NOTE: keys() is used instead of items(), as the order between them is not guaranteed to be the same
for i, author in enumerate(deduplicated_comments.keys()):
body = ''.join(list(filter(lambda x: x in string.printable and x not in "\t\n,", deduplicated_comments[author])))
writer.writerow(['WINNER' if author in winners else '', str(i), author, body])
return [str(list(deduplicated_comments.keys()).index(author)) + ") " + str(author) for i, author in enumerate(winners)]
def run(self):
"""
Sanitize user input and pass it to get_winners
"""
try:
post_URL = root.selection_get(selection="CLIPBOARD")
except tk.TclError as e:
self.set_selected_winners_text(f"ERROR: {str(e)}\nNOTE: Probably no text in clipboard")
self.root.geometry(f"0x0")
raise
self.entry_url.configure(state="normal")
self.entry_url.delete(0, tk.END)
self.entry_url.insert(0, post_URL)
self.entry_url.configure(state="readonly")
# Verify user entered a number
amount = self.entry_winners.get()
if amount.isdigit() and int(amount) == 0:
self.set_selected_winners_text(f"ERROR: Winners must be more than 0")
self.root.geometry("0x0")
elif amount.isdigit():
self.set_selected_winners_text("Processing...")
try:
winners = self.get_winners(post_URL, int(amount))
except Exception as e:
self.set_selected_winners_text(f"ERROR: {str(e)}")
self.root.geometry(f"0x0")
raise
if len(winners) == 1:
self.set_selected_winners_text(f"And the winner is:\n{winners[0]}")
else:
self.set_selected_winners_text(f"And the winners are:\n" + '\n'.join(winners))
window_size = self.calculate_window_size(min(10, len(winners)) + 1) # Show up to 10 winners at once
self.root.geometry(f"0x{window_size[1]}")
else:
self.set_selected_winners_text(f"ERROR: \"{amount}\" is not a number...")
self.root.geometry("0x0")
if __name__ == "__main__":
# Initialize logging
logging.basicConfig(level=logging.NOTSET)
logs_path = Path(__file__).parent.resolve() / "Logs"
logs_path.mkdir(exist_ok=True)
file_handler = RotatingFileHandler(logs_path / "contest_log.txt", maxBytes=1024*1024, backupCount=1) # 2 x 1M log files
file_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s - %(message)s"))
logging.root.addHandler(file_handler)
logger = logging.getLogger(__name__)
logger.info("User started program.")
try:
# Initialize PRAW
config = configparser.ConfigParser(allow_no_value=True)
config.read("account.ini")
reddit = praw.Reddit(
client_id=config["ACCOUNT INFO"]["client id"],
client_secret=config["ACCOUNT INFO"]["client secret"],
password=config["ACCOUNT INFO"]["password"],
user_agent="contest_bot/v1.2",
username=config["ACCOUNT INFO"]["username"],
)
blacklist = list(config["BLACKLIST"])
class FaultTolerantTk(tk.Tk):
def report_callback_exception(self, exc, val, tb):
logger.exception("Critical error occured in Tk.")
root = FaultTolerantTk()
app = MainWindow(root, reddit, blacklist)
app.mainloop()
except Exception as e:
logger.exception("Critical error occured in main.")
raise