Skip to content

Commit

Permalink
feat: add reddit slash commands (#281)
Browse files Browse the repository at this point in the history
  • Loading branch information
ReenigneArcher authored May 1, 2024
1 parent ad71781 commit a9952b2
Show file tree
Hide file tree
Showing 8 changed files with 1,212 additions and 142 deletions.
179 changes: 98 additions & 81 deletions src/reddit/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import sys
import threading
import time
from typing import Optional

# lib imports
import praw
Expand Down Expand Up @@ -43,8 +42,14 @@ def __init__(self, **kwargs):

# directories
self.data_dir = common.data_dir
self.commands_dir = os.path.join(self.data_dir, "support-bot-commands", "docs")

# files
self.db = os.path.join(self.data_dir, 'reddit_bot_database')

# locks
self.lock = threading.Lock()

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

self.migrate_shelve()
self.migrate_last_online()

@staticmethod
def validate_env() -> bool:
required_env = [
Expand All @@ -70,9 +78,43 @@ def validate_env() -> bool:
return False
return True

def migrate_last_online(self):
if os.path.isfile(os.path.join(self.data_dir, 'last_online')):
os.remove(os.path.join(self.data_dir, 'last_online'))

def migrate_shelve(self):
with self.lock, shelve.open(self.db) as db:
if 'submissions' not in db and 'comments' not in db:
db['comments'] = {}
db['submissions'] = {}
submissions = db['submissions']
for k, v in db.items():
if k not in ['comments', 'submissions']:
submissions[k] = v
assert submissions[k] == v
db['submissions'] = submissions
keys_to_delete = [k for k in db if k not in ['comments', 'submissions']]
for k in keys_to_delete:
del db[k]
assert k not in db

def process_comment(self, comment: models.Comment):
# todo
pass
with self.lock, shelve.open(self.db) as db:
comments = db.get('comments', {})
if comment.id in comments and comments[comment.id].get('processed', False):
return

comments[comment.id] = {
'author': str(comment.author),
'body': comment.body,
'created_utc': comment.created_utc,
'processed': True,
'slash_command': {'project': None, 'command': None},
}
# the shelve doesn't update unless we recreate the main key
db['comments'] = comments

self.slash_commands(comment=comment)

def process_submission(self, submission: models.Submission):
"""
Expand All @@ -83,44 +125,28 @@ def process_submission(self, submission: models.Submission):
submission : praw.models.Submission
The submission to process.
"""
last_online = self.get_last_online()

if last_online < submission.created_utc:
with self.lock, shelve.open(self.db) as db:
submissions = db.get('submissions', {})
if submission.id not in submissions:
submissions[submission.id] = {}
submission_exists = False
else:
submission_exists = True

# the shelve doesn't update unless we recreate the main key
submissions[submission.id].update(vars(submission))
db['submissions'] = submissions

if not submission_exists:
print(f'submission id: {submission.id}')
print(f'submission title: {submission.title}')
print('---------')
if os.getenv('DISCORD_WEBHOOK'):
self.discord(submission=submission)
self.flair(submission=submission)
self.karma(submission=submission)

with shelve.open(os.path.join(self.data_dir, 'reddit_bot_database')) as db:
try:
db[submission.id]
except KeyError:
submission_exists = False
db[submission.id] = vars(submission)
else:
submission_exists = True

if submission_exists:
for k, v in vars(submission).items(): # update the database with current values
try:
if db[submission.id][k] != v:
db[submission.id][k] = v
except KeyError:
db[submission.id][k] = v

else:
try:
os.environ['DISCORD_WEBHOOK']
except KeyError:
pass
else:
db = self.discord(db=db, submission=submission)
db = self.flair(db=db, submission=submission)
db = self.karma(db=db, submission=submission)

# re-write the last online time
self.last_online_writer()

def discord(self, db: shelve.Shelf, submission: models.Submission) -> Optional[shelve.Shelf]:
def discord(self, submission: models.Submission):
"""
Send a discord message.
Expand Down Expand Up @@ -180,54 +206,45 @@ def discord(self, db: shelve.Shelf, submission: models.Submission) -> Optional[s
r = requests.post(os.environ['DISCORD_WEBHOOK'], json=discord_webhook)

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

return db

def flair(self, db: shelve.Shelf, submission: models.Submission) -> shelve.Shelf:
# todo
return db

def karma(self, db: shelve.Shelf, submission: models.Submission) -> shelve.Shelf:
def flair(self, submission: models.Submission):
# todo
return db
pass

def commands(self, db: shelve.Shelf, submission: models.Submission) -> shelve.Shelf:
def karma(self, submission: models.Submission):
# todo
return db

def last_online_writer(self) -> int:
"""
Write the current time to the last online file.
Returns
-------
int
The current time.
"""
last_online = int(time.time())
with open(self.last_online_file, 'w') as f:
f.write(str(last_online))

return last_online

def get_last_online(self) -> int:
"""
Get the last online time.
Returns
-------
int
The last online time.
"""
try:
with open(self.last_online_file, 'r') as f:
last_online = int(f.read())
except FileNotFoundError:
last_online = self.last_online_writer()
pass

return last_online
def slash_commands(self, comment: models.Comment):
if comment.body.startswith("/"):
print(f"Processing slash command: {comment.body}")
# Split the comment into project and command
parts = comment.body[1:].split()
project = parts[0]
command = parts[1] if len(parts) > 1 else None

# Check if the command file exists in self.commands_dir
command_file = os.path.join(self.commands_dir, project, f"{command}.md") if command else None
if command_file and os.path.isfile(command_file):
# Open the markdown file and read its contents
with open(command_file, 'r', encoding='utf-8') as file:
file_contents = file.read()

# Reply to the comment with the contents of the file
comment.reply(file_contents)
else:
# Log error message
print(f"Unknown command: {command} in project: {project}")
with self.lock, shelve.open(self.db) as db:
# the shelve doesn't update unless we recreate the main key
comments = db['comments']
comments[comment.id]['slash_command'] = {'project': project, 'command': command}
db['comments'] = comments

def _comment_loop(self, test: bool = False):
# process comments and then keep monitoring
Expand Down
302 changes: 302 additions & 0 deletions tests/fixtures/cassettes/fixture__submission.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"http_interactions": [
{
"recorded_at": "2024-04-27T01:29:37",
"recorded_at": "2024-05-01T02:35:44",
"request": {
"body": {
"encoding": "utf-8",
Expand Down Expand Up @@ -52,7 +52,7 @@
"848"
],
"Date": [
"Sat, 27 Apr 2024 01:29:36 GMT"
"Wed, 01 May 2024 02:35:44 GMT"
],
"NEL": [
"{\"report_to\": \"w3-reporting-nel\", \"max_age\": 14400, \"include_subdomains\": false, \"success_fraction\": 1.0, \"failure_fraction\": 1.0}"
Expand All @@ -64,7 +64,7 @@
"snooserv"
],
"Set-Cookie": [
"edgebucket=DwBNm1g2fDYDJH5bDV; Domain=reddit.com; Max-Age=63071999; Path=/; secure"
"edgebucket=ELOnVL1mGMbsRlea17; Domain=reddit.com; Max-Age=63071999; Path=/; secure"
],
"Strict-Transport-Security": [
"max-age=31536000; includeSubdomains"
Expand Down Expand Up @@ -96,7 +96,7 @@
}
},
{
"recorded_at": "2024-04-27T01:29:37",
"recorded_at": "2024-05-01T02:35:44",
"request": {
"body": {
"encoding": "utf-8",
Expand All @@ -116,19 +116,19 @@
"keep-alive"
],
"Cookie": [
"edgebucket=DwBNm1g2fDYDJH5bDV"
"edgebucket=ELOnVL1mGMbsRlea17"
],
"User-Agent": [
"Test suite PRAW/7.7.1 prawcore/2.4.0"
]
},
"method": "GET",
"uri": "https://oauth.reddit.com/comments/w03cku/?limit=2048&sort=confidence&raw_json=1"
"uri": "https://oauth.reddit.com/api/info/?id=t1_l20s21b&raw_json=1"
},
"response": {
"body": {
"encoding": "UTF-8",
"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}}]"
"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}}"
},
"headers": {
"Accept-Ranges": [
Expand All @@ -138,10 +138,10 @@
"keep-alive"
],
"Content-Length": [
"3683"
"2037"
],
"Date": [
"Sat, 27 Apr 2024 01:29:36 GMT"
"Wed, 01 May 2024 02:35:44 GMT"
],
"NEL": [
"{\"report_to\": \"w3-reporting-nel\", \"max_age\": 14400, \"include_subdomains\": false, \"success_fraction\": 1.0, \"failure_fraction\": 1.0}"
Expand All @@ -153,8 +153,8 @@
"snooserv"
],
"Set-Cookie": [
"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",
"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",
"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",
"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",
"csv=2; Max-Age=63072000; Domain=.reddit.com; Path=/; Secure; SameSite=None"
],
"Strict-Transport-Security": [
Expand Down Expand Up @@ -185,13 +185,13 @@
"-1"
],
"x-ratelimit-remaining": [
"989"
"954"
],
"x-ratelimit-reset": [
"24"
"256"
],
"x-ratelimit-used": [
"7"
"42"
],
"x-ua-compatible": [
"IE=edge"
Expand All @@ -201,7 +201,7 @@
"code": 200,
"message": "OK"
},
"url": "https://oauth.reddit.com/comments/w03cku/?limit=2048&sort=confidence&raw_json=1"
"url": "https://oauth.reddit.com/api/info/?id=t1_l20s21b&raw_json=1"
}
}
],
Expand Down
Loading

0 comments on commit a9952b2

Please sign in to comment.