Skip to content

Commit a9952b2

Browse files
feat: add reddit slash commands (#281)
1 parent ad71781 commit a9952b2

8 files changed

+1212
-142
lines changed

src/reddit/bot.py

Lines changed: 98 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import sys
77
import threading
88
import time
9-
from typing import Optional
109

1110
# lib imports
1211
import praw
@@ -43,8 +42,14 @@ def __init__(self, **kwargs):
4342

4443
# directories
4544
self.data_dir = common.data_dir
45+
self.commands_dir = os.path.join(self.data_dir, "support-bot-commands", "docs")
46+
47+
# files
48+
self.db = os.path.join(self.data_dir, 'reddit_bot_database')
49+
50+
# locks
51+
self.lock = threading.Lock()
4652

47-
self.last_online_file = os.path.join(self.data_dir, 'last_online')
4853
self.reddit = praw.Reddit(
4954
client_id=os.environ['PRAW_CLIENT_ID'],
5055
client_secret=os.environ['PRAW_CLIENT_SECRET'],
@@ -55,6 +60,9 @@ def __init__(self, **kwargs):
5560
)
5661
self.subreddit = self.reddit.subreddit(self.subreddit_name) # "AskReddit" for faster testing of submission loop
5762

63+
self.migrate_shelve()
64+
self.migrate_last_online()
65+
5866
@staticmethod
5967
def validate_env() -> bool:
6068
required_env = [
@@ -70,9 +78,43 @@ def validate_env() -> bool:
7078
return False
7179
return True
7280

81+
def migrate_last_online(self):
82+
if os.path.isfile(os.path.join(self.data_dir, 'last_online')):
83+
os.remove(os.path.join(self.data_dir, 'last_online'))
84+
85+
def migrate_shelve(self):
86+
with self.lock, shelve.open(self.db) as db:
87+
if 'submissions' not in db and 'comments' not in db:
88+
db['comments'] = {}
89+
db['submissions'] = {}
90+
submissions = db['submissions']
91+
for k, v in db.items():
92+
if k not in ['comments', 'submissions']:
93+
submissions[k] = v
94+
assert submissions[k] == v
95+
db['submissions'] = submissions
96+
keys_to_delete = [k for k in db if k not in ['comments', 'submissions']]
97+
for k in keys_to_delete:
98+
del db[k]
99+
assert k not in db
100+
73101
def process_comment(self, comment: models.Comment):
74-
# todo
75-
pass
102+
with self.lock, shelve.open(self.db) as db:
103+
comments = db.get('comments', {})
104+
if comment.id in comments and comments[comment.id].get('processed', False):
105+
return
106+
107+
comments[comment.id] = {
108+
'author': str(comment.author),
109+
'body': comment.body,
110+
'created_utc': comment.created_utc,
111+
'processed': True,
112+
'slash_command': {'project': None, 'command': None},
113+
}
114+
# the shelve doesn't update unless we recreate the main key
115+
db['comments'] = comments
116+
117+
self.slash_commands(comment=comment)
76118

77119
def process_submission(self, submission: models.Submission):
78120
"""
@@ -83,44 +125,28 @@ def process_submission(self, submission: models.Submission):
83125
submission : praw.models.Submission
84126
The submission to process.
85127
"""
86-
last_online = self.get_last_online()
87-
88-
if last_online < submission.created_utc:
128+
with self.lock, shelve.open(self.db) as db:
129+
submissions = db.get('submissions', {})
130+
if submission.id not in submissions:
131+
submissions[submission.id] = {}
132+
submission_exists = False
133+
else:
134+
submission_exists = True
135+
136+
# the shelve doesn't update unless we recreate the main key
137+
submissions[submission.id].update(vars(submission))
138+
db['submissions'] = submissions
139+
140+
if not submission_exists:
89141
print(f'submission id: {submission.id}')
90142
print(f'submission title: {submission.title}')
91143
print('---------')
144+
if os.getenv('DISCORD_WEBHOOK'):
145+
self.discord(submission=submission)
146+
self.flair(submission=submission)
147+
self.karma(submission=submission)
92148

93-
with shelve.open(os.path.join(self.data_dir, 'reddit_bot_database')) as db:
94-
try:
95-
db[submission.id]
96-
except KeyError:
97-
submission_exists = False
98-
db[submission.id] = vars(submission)
99-
else:
100-
submission_exists = True
101-
102-
if submission_exists:
103-
for k, v in vars(submission).items(): # update the database with current values
104-
try:
105-
if db[submission.id][k] != v:
106-
db[submission.id][k] = v
107-
except KeyError:
108-
db[submission.id][k] = v
109-
110-
else:
111-
try:
112-
os.environ['DISCORD_WEBHOOK']
113-
except KeyError:
114-
pass
115-
else:
116-
db = self.discord(db=db, submission=submission)
117-
db = self.flair(db=db, submission=submission)
118-
db = self.karma(db=db, submission=submission)
119-
120-
# re-write the last online time
121-
self.last_online_writer()
122-
123-
def discord(self, db: shelve.Shelf, submission: models.Submission) -> Optional[shelve.Shelf]:
149+
def discord(self, submission: models.Submission):
124150
"""
125151
Send a discord message.
126152
@@ -180,54 +206,45 @@ def discord(self, db: shelve.Shelf, submission: models.Submission) -> Optional[s
180206
r = requests.post(os.environ['DISCORD_WEBHOOK'], json=discord_webhook)
181207

182208
if r.status_code == 204: # successful completion of request, no additional content
183-
# update the database
184-
db[submission.id]['bot_discord'] = {'sent': True, 'sent_utc': int(time.time())}
209+
with self.lock, shelve.open(self.db) as db:
210+
# the shelve doesn't update unless we recreate the main key
211+
submissions = db['submissions']
212+
submissions[submission.id]['bot_discord'] = {'sent': True, 'sent_utc': int(time.time())}
213+
db['submissions'] = submissions
185214

186-
return db
187-
188-
def flair(self, db: shelve.Shelf, submission: models.Submission) -> shelve.Shelf:
189-
# todo
190-
return db
191-
192-
def karma(self, db: shelve.Shelf, submission: models.Submission) -> shelve.Shelf:
215+
def flair(self, submission: models.Submission):
193216
# todo
194-
return db
217+
pass
195218

196-
def commands(self, db: shelve.Shelf, submission: models.Submission) -> shelve.Shelf:
219+
def karma(self, submission: models.Submission):
197220
# todo
198-
return db
199-
200-
def last_online_writer(self) -> int:
201-
"""
202-
Write the current time to the last online file.
203-
204-
Returns
205-
-------
206-
int
207-
The current time.
208-
"""
209-
last_online = int(time.time())
210-
with open(self.last_online_file, 'w') as f:
211-
f.write(str(last_online))
212-
213-
return last_online
214-
215-
def get_last_online(self) -> int:
216-
"""
217-
Get the last online time.
218-
219-
Returns
220-
-------
221-
int
222-
The last online time.
223-
"""
224-
try:
225-
with open(self.last_online_file, 'r') as f:
226-
last_online = int(f.read())
227-
except FileNotFoundError:
228-
last_online = self.last_online_writer()
221+
pass
229222

230-
return last_online
223+
def slash_commands(self, comment: models.Comment):
224+
if comment.body.startswith("/"):
225+
print(f"Processing slash command: {comment.body}")
226+
# Split the comment into project and command
227+
parts = comment.body[1:].split()
228+
project = parts[0]
229+
command = parts[1] if len(parts) > 1 else None
230+
231+
# Check if the command file exists in self.commands_dir
232+
command_file = os.path.join(self.commands_dir, project, f"{command}.md") if command else None
233+
if command_file and os.path.isfile(command_file):
234+
# Open the markdown file and read its contents
235+
with open(command_file, 'r', encoding='utf-8') as file:
236+
file_contents = file.read()
237+
238+
# Reply to the comment with the contents of the file
239+
comment.reply(file_contents)
240+
else:
241+
# Log error message
242+
print(f"Unknown command: {command} in project: {project}")
243+
with self.lock, shelve.open(self.db) as db:
244+
# the shelve doesn't update unless we recreate the main key
245+
comments = db['comments']
246+
comments[comment.id]['slash_command'] = {'project': project, 'command': command}
247+
db['comments'] = comments
231248

232249
def _comment_loop(self, test: bool = False):
233250
# process comments and then keep monitoring

tests/fixtures/cassettes/fixture__submission.json

Lines changed: 302 additions & 0 deletions
Large diffs are not rendered by default.

tests/fixtures/cassettes/test_submission.json renamed to tests/fixtures/cassettes/fixture_slash_command_comment.json

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"http_interactions": [
33
{
4-
"recorded_at": "2024-04-27T01:29:37",
4+
"recorded_at": "2024-05-01T02:35:44",
55
"request": {
66
"body": {
77
"encoding": "utf-8",
@@ -52,7 +52,7 @@
5252
"848"
5353
],
5454
"Date": [
55-
"Sat, 27 Apr 2024 01:29:36 GMT"
55+
"Wed, 01 May 2024 02:35:44 GMT"
5656
],
5757
"NEL": [
5858
"{\"report_to\": \"w3-reporting-nel\", \"max_age\": 14400, \"include_subdomains\": false, \"success_fraction\": 1.0, \"failure_fraction\": 1.0}"
@@ -64,7 +64,7 @@
6464
"snooserv"
6565
],
6666
"Set-Cookie": [
67-
"edgebucket=DwBNm1g2fDYDJH5bDV; Domain=reddit.com; Max-Age=63071999; Path=/; secure"
67+
"edgebucket=ELOnVL1mGMbsRlea17; Domain=reddit.com; Max-Age=63071999; Path=/; secure"
6868
],
6969
"Strict-Transport-Security": [
7070
"max-age=31536000; includeSubdomains"
@@ -96,7 +96,7 @@
9696
}
9797
},
9898
{
99-
"recorded_at": "2024-04-27T01:29:37",
99+
"recorded_at": "2024-05-01T02:35:44",
100100
"request": {
101101
"body": {
102102
"encoding": "utf-8",
@@ -116,19 +116,19 @@
116116
"keep-alive"
117117
],
118118
"Cookie": [
119-
"edgebucket=DwBNm1g2fDYDJH5bDV"
119+
"edgebucket=ELOnVL1mGMbsRlea17"
120120
],
121121
"User-Agent": [
122122
"Test suite PRAW/7.7.1 prawcore/2.4.0"
123123
]
124124
},
125125
"method": "GET",
126-
"uri": "https://oauth.reddit.com/comments/w03cku/?limit=2048&sort=confidence&raw_json=1"
126+
"uri": "https://oauth.reddit.com/api/info/?id=t1_l20s21b&raw_json=1"
127127
},
128128
"response": {
129129
"body": {
130130
"encoding": "UTF-8",
131-
"string": "[{\"kind\": \"Listing\", \"data\": {\"after\": null, \"dist\": 1, \"modhash\": null, \"geo_filter\": \"\", \"children\": [{\"kind\": \"t3\", \"data\": {\"author_flair_background_color\": \"#94e044\", \"approved_at_utc\": null, \"subreddit\": \"LizardByte\", \"selftext\": \"Thank you for joining our LizardByte subreddit! We are still in process of updating our projects on GitHub!\\n\\nLook out for additional updates!\", \"user_reports\": [], \"saved\": false, \"mod_reason_title\": null, \"gilded\": 0, \"clicked\": false, \"title\": \"Welcome to LizardByte!\", \"link_flair_richtext\": [], \"subreddit_name_prefixed\": \"r/LizardByte\", \"hidden\": false, \"pwls\": null, \"link_flair_css_class\": \"\", \"downs\": 0, \"thumbnail_height\": null, \"top_awarded_type\": null, \"parent_whitelist_status\": null, \"hide_score\": false, \"name\": \"t3_w03cku\", \"quarantine\": false, \"link_flair_text_color\": \"dark\", \"upvote_ratio\": 0.82, \"ignore_reports\": false, \"ups\": 7, \"domain\": \"self.LizardByte\", \"media_embed\": {}, \"thumbnail_width\": null, \"author_flair_template_id\": \"5d220538-ff87-11ec-a9c4-56c680cbb67e\", \"is_original_content\": false, \"author_fullname\": \"t2_jwdeap93\", \"secure_media\": null, \"is_reddit_media_domain\": false, \"is_meta\": false, \"category\": null, \"secure_media_embed\": {}, \"link_flair_text\": \"Announcement\", \"can_mod_post\": true, \"score\": 7, \"approved_by\": null, \"is_created_from_ads_ui\": false, \"author_premium\": false, \"thumbnail\": \"self\", \"edited\": false, \"author_flair_css_class\": null, \"previous_visits\": [1714058632.0, 1714062595.0, 1714068499.0, 1714069806.0, 1714079940.0, 1714081381.0, 1714082461.0, 1714084081.0, 1714085322.0, 1714180262.0], \"author_flair_richtext\": [], \"gildings\": {}, \"content_categories\": null, \"is_self\": true, \"subreddit_type\": \"public\", \"created\": 1657930958.0, \"link_flair_type\": \"text\", \"wls\": null, \"removed_by_category\": null, \"banned_by\": null, \"author_flair_type\": \"text\", \"total_awards_received\": 0, \"allow_live_comments\": false, \"selftext_html\": \"\\u003C!-- SC_OFF --\\u003E\\u003Cdiv class=\\\"md\\\"\\u003E\\u003Cp\\u003EThank you for joining our LizardByte subreddit! We are still in process of updating our projects on GitHub!\\u003C/p\\u003E\\n\\n\\u003Cp\\u003ELook out for additional updates!\\u003C/p\\u003E\\n\\u003C/div\\u003E\\u003C!-- SC_ON --\\u003E\", \"likes\": null, \"suggested_sort\": \"new\", \"banned_at_utc\": null, \"view_count\": null, \"archived\": false, \"no_follow\": false, \"spam\": false, \"is_crosspostable\": true, \"pinned\": false, \"over_18\": false, \"all_awardings\": [], \"awarders\": [], \"media_only\": false, \"link_flair_template_id\": \"1c411bfc-ff88-11ec-a3ea-969cbc5b3148\", \"can_gild\": false, \"removed\": false, \"spoiler\": false, \"locked\": false, \"author_flair_text\": \"Developer\", \"treatment_tags\": [], \"visited\": false, \"removed_by\": null, \"mod_note\": null, \"distinguished\": null, \"subreddit_id\": \"t5_6o778z\", \"author_is_blocked\": false, \"mod_reason_by\": null, \"num_reports\": 0, \"removal_reason\": null, \"link_flair_background_color\": \"#ff4500\", \"id\": \"w03cku\", \"is_robot_indexable\": true, \"num_duplicates\": 0, \"report_reasons\": [], \"author\": \"tata_contreras\", \"discussion_type\": null, \"num_comments\": 0, \"send_replies\": true, \"media\": null, \"contest_mode\": false, \"author_patreon_flair\": false, \"approved\": false, \"author_flair_text_color\": \"dark\", \"permalink\": \"/r/LizardByte/comments/w03cku/welcome_to_lizardbyte/\", \"whitelist_status\": null, \"stickied\": false, \"url\": \"https://www.reddit.com/r/LizardByte/comments/w03cku/welcome_to_lizardbyte/\", \"subreddit_subscribers\": 876, \"created_utc\": 1657930958.0, \"num_crossposts\": 0, \"mod_reports\": [], \"is_video\": false}}], \"before\": null}}, {\"kind\": \"Listing\", \"data\": {\"after\": null, \"dist\": null, \"modhash\": null, \"geo_filter\": \"\", \"children\": [], \"before\": null}}]"
131+
"string": "{\"kind\": \"Listing\", \"data\": {\"after\": null, \"dist\": 1, \"modhash\": null, \"geo_filter\": \"\", \"children\": [{\"kind\": \"t1\", \"data\": {\"subreddit_id\": \"t5_6o778z\", \"approved_at_utc\": null, \"author_is_blocked\": false, \"comment_type\": null, \"edited\": false, \"mod_reason_by\": null, \"banned_by\": null, \"ups\": 1, \"num_reports\": 0, \"author_flair_type\": \"text\", \"total_awards_received\": 0, \"subreddit\": \"LizardByte\", \"author_flair_template_id\": \"5d220538-ff87-11ec-a9c4-56c680cbb67e\", \"likes\": null, \"replies\": \"\", \"user_reports\": [], \"saved\": false, \"id\": \"l20s21b\", \"banned_at_utc\": null, \"mod_reason_title\": null, \"gilded\": 0, \"archived\": false, \"collapsed_reason_code\": null, \"no_follow\": true, \"spam\": false, \"can_mod_post\": true, \"gildings\": {}, \"send_replies\": true, \"parent_id\": \"t3_w03cku\", \"score\": 1, \"author_fullname\": \"t2_393wfkmy\", \"created_utc\": 1714522546.0, \"report_reasons\": [], \"approved_by\": null, \"all_awardings\": [], \"ignore_reports\": false, \"body\": \"/sunshine vban\", \"awarders\": [], \"top_awarded_type\": null, \"downs\": 0, \"author_flair_css_class\": null, \"author_patreon_flair\": false, \"collapsed\": false, \"author_flair_richtext\": [], \"is_submitter\": false, \"body_html\": \"\\u003Cdiv class=\\\"md\\\"\\u003E\\u003Cp\\u003E/sunshine vban\\u003C/p\\u003E\\n\\u003C/div\\u003E\", \"removal_reason\": null, \"collapsed_reason\": null, \"associated_award\": null, \"stickied\": false, \"author_premium\": false, \"can_gild\": false, \"removed\": false, \"unrepliable_reason\": null, \"approved\": false, \"author_flair_text_color\": \"dark\", \"score_hidden\": false, \"permalink\": \"/r/LizardByte/comments/w03cku/welcome_to_lizardbyte/l20s21b/\", \"subreddit_type\": \"public\", \"locked\": false, \"name\": \"t1_l20s21b\", \"created\": 1714522546.0, \"author_flair_text\": \"Developer\", \"treatment_tags\": [], \"author\": \"ReenigneArcher\", \"link_id\": \"t3_w03cku\", \"subreddit_name_prefixed\": \"r/LizardByte\", \"controversiality\": 0, \"author_flair_background_color\": \"#94e044\", \"collapsed_because_crowd_control\": null, \"mod_reports\": [], \"mod_note\": null, \"distinguished\": null}}], \"before\": null}}"
132132
},
133133
"headers": {
134134
"Accept-Ranges": [
@@ -138,10 +138,10 @@
138138
"keep-alive"
139139
],
140140
"Content-Length": [
141-
"3683"
141+
"2037"
142142
],
143143
"Date": [
144-
"Sat, 27 Apr 2024 01:29:36 GMT"
144+
"Wed, 01 May 2024 02:35:44 GMT"
145145
],
146146
"NEL": [
147147
"{\"report_to\": \"w3-reporting-nel\", \"max_age\": 14400, \"include_subdomains\": false, \"success_fraction\": 1.0, \"failure_fraction\": 1.0}"
@@ -153,8 +153,8 @@
153153
"snooserv"
154154
],
155155
"Set-Cookie": [
156-
"loid=0000000000ps86k4yd.2.1657372729000.Z0FBQUFBQm1MRlVBMXE0Z2MySTNhSmxhZTBCYzlDcEtFTG5nM1RhM3E5bW1Mckg5T3dmYlhic2JqOHFKUzlXQWRRM3BvZG9kczM3bzJYWU1RZ1lRaXZjNlhaTl9LYVNjcDVnQmdiV2hVVTdNTEMxY0F2V0dudW1HVXdOSURCWEFqd3Voamg5X19uWG0; Domain=reddit.com; Max-Age=63071999; Path=/; expires=Mon, 27-Apr-2026 01:29:36 GMT; secure; SameSite=None; Secure",
157-
"session_tracker=gnralljneqorkibncg.0.1714181376469.Z0FBQUFBQm1MRlVBZEk4cHVFREhtYlZOZVNWYUlLTjRpeUlnZzhiODBLT25HMzFSTE5JZ3o0MndERTc5SlJ4V1NQVEdGSW00akpzMEpVbGJqSGRHSFdxbWVoa1d2VmhIRVp1aTIyaEdTbGgzVENoaG5YVEFJZlg5WXBpWXpsMDJQZmJ4Y1hpbEFFUWU; Domain=reddit.com; Max-Age=7199; Path=/; expires=Sat, 27-Apr-2024 03:29:36 GMT; secure; SameSite=None; Secure",
156+
"loid=0000000000ps86k4yd.2.1657372729000.Z0FBQUFBQm1NYXFBTjF6QmZOUlBkTVRNQUNPdVZNUkxPQkMwY0VRakJfZ09pMVBjNkdVdTRTVVl2YURRanFFQUJXcklpVnZEeU1NXzlNRS03Y1ppOXQxMkpOckhMa1ozem5IMkc5SnZxWjVrV0lwSmZKUDFFSm55MXo5STM3MDNTVDQzdU1ZazRQZVM; Domain=reddit.com; Max-Age=63071999; Path=/; expires=Fri, 01-May-2026 02:35:44 GMT; secure; SameSite=None; Secure",
157+
"session_tracker=goeleipgnjpgmqklcp.0.1714530944660.Z0FBQUFBQm1NYXFBcFRER1ZtRGdHYkVrSUpZQ1JmVTUwNkVWWDNzNzBKSUQtQnVsZ2JHTVladHNsLU5xSFB4SG9hVlZ4YTNQTWZHcDFBek00QlY0Q1V5WTY3cUpfV2ZvU0lBMEZLaXItbGFnTkItVzBMcEtoX2l1eUQyS1BtdS02d19tS1N4WVVKbzI; Domain=reddit.com; Max-Age=7199; Path=/; expires=Wed, 01-May-2024 04:35:44 GMT; secure; SameSite=None; Secure",
158158
"csv=2; Max-Age=63072000; Domain=.reddit.com; Path=/; Secure; SameSite=None"
159159
],
160160
"Strict-Transport-Security": [
@@ -185,13 +185,13 @@
185185
"-1"
186186
],
187187
"x-ratelimit-remaining": [
188-
"989"
188+
"954"
189189
],
190190
"x-ratelimit-reset": [
191-
"24"
191+
"256"
192192
],
193193
"x-ratelimit-used": [
194-
"7"
194+
"42"
195195
],
196196
"x-ua-compatible": [
197197
"IE=edge"
@@ -201,7 +201,7 @@
201201
"code": 200,
202202
"message": "OK"
203203
},
204-
"url": "https://oauth.reddit.com/comments/w03cku/?limit=2048&sort=confidence&raw_json=1"
204+
"url": "https://oauth.reddit.com/api/info/?id=t1_l20s21b&raw_json=1"
205205
}
206206
}
207207
],

0 commit comments

Comments
 (0)