From 832df9e851b7978dca0888daad17fd1a2e0b8ae7 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sun, 21 Apr 2024 14:13:00 -0400 Subject: [PATCH] feat: unify discord and reddit bots --- .dockerignore | 3 + .gitignore | 3 + .replit | 3 +- DOCKER_README.md | 60 ++-- Dockerfile | 25 +- README.md | 43 ++- requirements.txt | 1 + sample.env | 10 +- discord_avatar.py => src/discord_avatar.py | 0 discord_bot.py => src/discord_bot.py | 51 +-- .../discord_constants.py | 0 discord_helpers.py => src/discord_helpers.py | 0 discord_modals.py => src/discord_modals.py | 0 discord_views.py => src/discord_views.py | 0 keep_alive.py => src/keep_alive.py | 0 src/main.py | 38 +++ src/reddit_bot.py | 320 ++++++++++++++++++ 17 files changed, 500 insertions(+), 57 deletions(-) rename discord_avatar.py => src/discord_avatar.py (100%) rename discord_bot.py => src/discord_bot.py (95%) rename discord_constants.py => src/discord_constants.py (100%) rename discord_helpers.py => src/discord_helpers.py (100%) rename discord_modals.py => src/discord_modals.py (100%) rename discord_views.py => src/discord_views.py (100%) rename keep_alive.py => src/keep_alive.py (100%) create mode 100644 src/main.py create mode 100644 src/reddit_bot.py diff --git a/.dockerignore b/.dockerignore index 20f9026..a3f4ac4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,3 +10,6 @@ tests/ # ignore venv when building locally venv/ + +data/ +sample.env diff --git a/.gitignore b/.gitignore index 5863785..e0aed2f 100644 --- a/.gitignore +++ b/.gitignore @@ -150,3 +150,6 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ + +# project specific ignores +data/ diff --git a/.replit b/.replit index 636e59f..5fa5899 100644 --- a/.replit +++ b/.replit @@ -1,5 +1,5 @@ # The command that is executed when the run button is clicked. -run = ["python", "discord_bot.py"] +run = ["python", "src/main.py"] # The default file opened in the editor. entrypoint = "README.md" @@ -14,4 +14,3 @@ language = "python3" packageSearch = true # Enabled package guessing guessImports = false - \ No newline at end of file diff --git a/DOCKER_README.md b/DOCKER_README.md index fc39f85..c590081 100644 --- a/DOCKER_README.md +++ b/DOCKER_README.md @@ -5,9 +5,9 @@ Create and run the container (substitute your ``): ```bash docker run -d \ - --name=lizardbyte-discord-bot \ + --name=lizardbyte-support-bot \ --restart=unless-stopped \ - -e BOT_TOKEN= \ + -e DISCORD_BOT_TOKEN= \ -e DAILY_CHANNEL_ID= \ -e DAILY_RELEASES= \ -e DAILY_TASKS= \ @@ -15,18 +15,25 @@ docker run -d \ -e GRAVATAR_EMAIL= \ -e IGDB_CLIENT_ID= \ -e IGDB_CLIENT_SECRET= \ - lizardbyte/discord-bot + -e PRAW_CLIENT_ID= \ + -e PRAW_CLIENT_SECRET= \ + -e PRAW_SUBREDDIT= \ + -e DISCORD_WEBHOOK= \ + -e GRAVATAR_EMAIL= \ + -e REDIRECT_URI= \ + -p 8080:8080 \ + lizardbyte/support-bot ``` To update the container it must be removed and recreated: ```bash # Stop the container -docker stop lizardbyte-discord-bot +docker stop lizardbyte-support-bot # Remove the container -docker rm lizardbyte-discord-bot +docker rm lizardbyte-support-bot # Pull the latest update -docker pull lizardbyte/discord-bot +docker pull lizardbyte/support-bot # Run the container with the same parameters as before docker run -d ... ``` @@ -39,11 +46,11 @@ Create a `docker-compose.yml` file with the following contents (substitute your version: '3' services: lizardbyte-discord-bot: - image: lizardbyte/discord-bot - container_name: lizardbyte-discord-bot + image: lizardbyte/support-bot + container_name: lizardbyte-support-bot restart: unless-stopped environment: - - BOT_TOKEN= + - DISCORD_BOT_TOKEN= - DAILY_CHANNEL_ID= - DAILY_RELEASES= - DAILY_TASKS= @@ -51,6 +58,14 @@ services: - GRAVATAR_EMAIL= - IGDB_CLIENT_ID= - IGDB_CLIENT_SECRET= + - PRAW_CLIENT_ID= + - PRAW_CLIENT_SECRET= + - PRAW_SUBREDDIT= + - DISCORD_WEBHOOK= + - GRAVATAR_EMAIL= + - REDIRECT_URI= + ports: + - 8080:8080 ``` Create and start the container (run the command from the same folder as your `docker-compose.yml` file): @@ -70,13 +85,20 @@ docker-compose up -d ## Parameters You must substitute the `` with your own settings. -| Parameter | Required | Default | Description | -|----------------------|----------|---------|---------------------------------------------------------------| -| BOT_TOKEN | True | None | Token from Bot page on discord developer portal. | -| DAILY_TASKS | False | true | Daily tasks on or off. | -| DAILY_RELEASES | False | true | Send a message for each game released on this day in history. | -| DAILY_CHANNEL_ID | False | None | Required if daily_tasks is enabled. | -| DAILY_TASKS_UTC_HOUR | False | 12 | The hour to run daily tasks. | -| GRAVATAR_EMAIL | False | None | Gravatar email address for bot avatar. | -| IGDB_CLIENT_ID | False | None | Required if daily_releases is enabled. | -| IGDB_CLIENT_SECRET | False | None | Required if daily_releases is enabled. | +| Parameter | Required | Default | Description | +|----------------------|----------|---------|-------------------------------------------------------------------------| +| DISCORD_BOT_TOKEN | True | None | Token from Bot page on discord developer portal. | +| DAILY_TASKS | False | true | Daily tasks on or off. | +| DAILY_RELEASES | False | true | Send a message for each game released on this day in history. | +| DAILY_CHANNEL_ID | False | None | Required if daily_tasks is enabled. | +| DAILY_TASKS_UTC_HOUR | False | 12 | The hour to run daily tasks. | +| GRAVATAR_EMAIL | False | None | Gravatar email address for bot avatar. | +| IGDB_CLIENT_ID | False | None | Required if daily_releases is enabled. | +| IGDB_CLIENT_SECRET | False | None | Required if daily_releases is enabled. | +| PRAW_CLIENT_ID | True | None | `client_id` from reddit app setup page. | +| PRAW_CLIENT_SECRET | True | None | `client_secret` from reddit app setup page. | +| PRAW_SUBREDDIT | True | None | Subreddit to monitor (reddit user should be moderator of the subreddit) | +| DISCORD_WEBHOOK | False | None | URL of webhook to send discord notifications to | +| REDIRECT_URI | True | None | The redirect URI entered during the reddit application setup | + +Further instructions can be found in the main [readme](https://github.com/LizardByte/support-bot/blob/master/README.md). diff --git a/Dockerfile b/Dockerfile index 802381c..e63985a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,26 +9,39 @@ ARG DAILY_RELEASES=true ARG DAILY_TASKS_UTC_HOUR=12 # Secret config -ARG BOT_TOKEN +ARG DISCORD_BOT_TOKEN ARG DAILY_CHANNEL_ID ARG GRAVATAR_EMAIL ARG IGDB_CLIENT_ID ARG IGDB_CLIENT_SECRET +ARG PRAW_CLIENT_ID +ARG PRAW_CLIENT_SECRET +ARG PRAW_SUBREDDIT +ARG DISCORD_WEBHOOK +ARG GRAVATAR_EMAIL +ARG REDIRECT_URI # Environment variables ENV DAILY_TASKS=$DAILY_TASKS ENV DAILY_RELEASES=$DAILY_RELEASES ENV DAILY_CHANNEL_ID=$DAILY_CHANNEL_ID ENV DAILY_TASKS_UTC_HOUR=$DAILY_TASKS_UTC_HOUR -ENV BOT_TOKEN=$BOT_TOKEN +ENV DISCORD_BOT_TOKEN=$DISCORD_BOT_TOKEN ENV GRAVATAR_EMAIL=$GRAVATAR_EMAIL ENV IGDB_CLIENT_ID=$IGDB_CLIENT_ID ENV IGDB_CLIENT_SECRET=$IGDB_CLIENT_SECRET +ENV PRAW_CLIENT_ID=$PRAW_CLIENT_ID +ENV PRAW_CLIENT_SECRET=$PRAW_CLIENT_SECRET +ENV PRAW_SUBREDDIT=$PRAW_SUBREDDIT +ENV DISCORD_WEBHOOK=$DISCORD_WEBHOOK +ENV GRAVATAR_EMAIL=$GRAVATAR_EMAIL +ENV REDIRECT_URI=$REDIRECT_URI + +VOLUME /data WORKDIR /app/ -COPY requirements.txt . -COPY *.py . -RUN pip install --no-cache-dir -r requirements.txt +COPY . . +RUN python -m pip install --no-cache-dir -r requirements.txt -CMD ["python", "discord_bot.py"] +CMD ["python", "./src/main.py"] diff --git a/README.md b/README.md index 2613703..bfb34a1 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ -# discord-bot -Discord bot written in python to help manage the LizardByte discord server. +# support-bot +Support bot written in python to help manage LizardByte communities. The current focus is discord and reddit, but other +platforms such as GitHub discussions/issues could be added. ## Overview -This is a custom discord bot with some slash commands to help with support on the LizardByte discord server. + +### Discord Slash Commands | command | description | argument 1 | |----------|---------------------------------------------------|---------------------| @@ -15,6 +17,9 @@ This is a custom discord bot with some slash commands to help with support on th ## Instructions + +### Discord + * Setup an application at [discord developer portal](https://discord.com/developers/applications). * On `Bot` page enabled these: * Presence Intent @@ -26,7 +31,7 @@ This is a custom discord bot with some slash commands to help with support on th | variable | required | default | description | |----------------------|----------|---------|---------------------------------------------------------------| -| BOT_TOKEN | True | None | Token from Bot page on discord developer portal. | +| DISCORD_BOT_TOKEN | True | None | Token from Bot page on discord developer portal. | | DAILY_TASKS | False | true | Daily tasks on or off. | | DAILY_RELEASES | False | true | Send a message for each game released on this day in history. | | DAILY_CHANNEL_ID | False | None | Required if daily_tasks is enabled. | @@ -36,6 +41,34 @@ This is a custom discord bot with some slash commands to help with support on th | IGDB_CLIENT_SECRET | False | None | Required if daily_releases is enabled. | * Running bot: - * `python discord_bot.py` + * `python ./src/main.py` * Invite bot to server: * `https://discord.com/api/oauth2/authorize?client_id=&permissions=8&scope=bot%20applications.commands` + + +### Reddit + +* Set up an application at [reddit apps](https://www.reddit.com/prefs/apps/). + * The redirect uri must be publicly accessible. + * If using Replit, enter `https://..repl.co` + * Otherwise, it is recommended to use [Nginx Proxy Manager](https://nginxproxymanager.com/) and [Duck DNS](https://www.duckdns.org/) + * Take note of the `client_id` and `client_secret` +* Enter the following as environment variables + + | Parameter | Required | Default | Description | + |--------------------|----------|---------|-------------------------------------------------------------------------| + | PRAW_CLIENT_ID | True | None | `client_id` from reddit app setup page. | + | PRAW_CLIENT_SECRET | True | None | `client_secret` from reddit app setup page. | + | PRAW_SUBREDDIT | True | None | Subreddit to monitor (reddit user should be moderator of the subreddit) | + | DISCORD_WEBHOOK | False | None | URL of webhook to send discord notifications to | + | GRAVATAR_EMAIL | False | None | Gravatar email address to get avatar from | + | REDIRECT_URI | True | None | The redirect URI entered during the reddit application setup | + +* First run (or manually get a new refresh token): + * Delete `./data/refresh_token` file if needed + * `python ./src/main.py` + * Open browser and login to reddit account to use with bot + * Navigate to URL printed in console and accept + * `./data/refresh_token` file is written +* Running after refresh_token already obtained: + * `python ./src/main.py` diff --git a/requirements.txt b/requirements.txt index 2dc1621..b1a8f88 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ beautifulsoup4==4.12.3 Flask==3.0.2 igdb-api-v4==0.3.2 libgravatar==1.0.4 +praw==7.7.1 py-cord==2.4.1 python-dotenv==1.0.1 requests==2.31.0 diff --git a/sample.env b/sample.env index 77064e3..b04274a 100644 --- a/sample.env +++ b/sample.env @@ -5,8 +5,16 @@ DAILY_CHANNEL_ID= DAILY_TASKS_UTC_HOUR=12 # Secret settings -BOT_TOKEN= +DISCORD_BOT_TOKEN= GRAVATAR_EMAIL= IGDB_CLIENT_ID= IGDB_CLIENT_SECRET= READTHEDOCS_TOKEN= + +# reddit bot +PRAW_CLIENT_ID= +PRAW_CLIENT_SECRET= +PRAW_SUBREDDIT=AskReddit +DISCORD_WEBHOOK= +GRAVATAR_EMAIL= +REDIRECT_URI= diff --git a/discord_avatar.py b/src/discord_avatar.py similarity index 100% rename from discord_avatar.py rename to src/discord_avatar.py diff --git a/discord_bot.py b/src/discord_bot.py similarity index 95% rename from discord_bot.py rename to src/discord_bot.py index 99b65cd..15bba3a 100644 --- a/discord_bot.py +++ b/src/discord_bot.py @@ -1,8 +1,10 @@ # standard imports +import asyncio from datetime import datetime import json import os import random +import threading from typing import Union # lib imports @@ -15,18 +17,11 @@ # local imports from discord_constants import org_name, bot_name, bot_url from discord_helpers import igdb_authorization, month_dictionary -import keep_alive - -# development imports -from dotenv import load_dotenv -load_dotenv(override=False) # environment secrets take priority over .env file - -if True: # hack for flake8 - from discord_avatar import avatar, avatar_img - from discord_views import DocsCommandView, DonateCommandView, RefundCommandView +from discord_avatar import avatar, avatar_img +from discord_views import DocsCommandView, DonateCommandView, RefundCommandView # constants -bot_token = os.environ['BOT_TOKEN'] +bot_token = os.environ['DISCORD_BOT_TOKEN'] bot = discord.Bot(intents=discord.Intents.all(), auto_sync_commands=True) user_mention_desc = 'Select the user to mention' @@ -502,21 +497,29 @@ async def daily_task(): print(f'thread created: {thread.name}') -# to run in replit -try: - os.environ['REPL_SLUG'] -except KeyError: - pass # not running in replit -else: - keep_alive.keep_alive() # Start the web server - -try: - bot.loop.run_until_complete(future=bot.start(token=bot_token)) # Login the bot -except KeyboardInterrupt: - print("Keyboard Interrupt Detected") -finally: +bot_thread = threading.Thread(target=lambda: None) + + +def start(): + global bot_thread + try: + # Login the bot in a separate thread + bot_thread = threading.Thread( + target=bot.loop.run_until_complete, + args=(bot.start(token=bot_token),), + daemon=True + ) + bot_thread.start() + except KeyboardInterrupt: + print("Keyboard Interrupt Detected") + stop() + + +def stop(): print("Attempting to stop daily tasks") daily_task.stop() print("Attempting to close bot connection") - bot.loop.run_until_complete(future=bot.close()) + if bot_thread is not None and bot_thread.is_alive(): + asyncio.run_coroutine_threadsafe(bot.close(), bot.loop) + bot_thread.join() print("Closed bot") diff --git a/discord_constants.py b/src/discord_constants.py similarity index 100% rename from discord_constants.py rename to src/discord_constants.py diff --git a/discord_helpers.py b/src/discord_helpers.py similarity index 100% rename from discord_helpers.py rename to src/discord_helpers.py diff --git a/discord_modals.py b/src/discord_modals.py similarity index 100% rename from discord_modals.py rename to src/discord_modals.py diff --git a/discord_views.py b/src/discord_views.py similarity index 100% rename from discord_views.py rename to src/discord_views.py diff --git a/keep_alive.py b/src/keep_alive.py similarity index 100% rename from keep_alive.py rename to src/keep_alive.py diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..3249ea5 --- /dev/null +++ b/src/main.py @@ -0,0 +1,38 @@ +# standard imports +import os +import time + +# development imports +from dotenv import load_dotenv +load_dotenv(override=False) # environment secrets take priority over .env file + +# local imports +if True: # hack for flake8 + import discord_bot + import keep_alive + import reddit_bot + + +def main(): + # to run in replit + try: + os.environ['REPL_SLUG'] + except KeyError: + pass # not running in replit + else: + keep_alive.keep_alive() # Start the web server + + discord_bot.start() # Start the discord bot + reddit_bot.start() # Start the reddit bot + + try: + while discord_bot.bot_thread.is_alive() or reddit_bot.bot_thread.is_alive(): + time.sleep(0.5) + except KeyboardInterrupt: + print("Keyboard Interrupt Detected") + discord_bot.stop() # Stop the discord bot + reddit_bot.stop() + + +if __name__ == '__main__': + main() diff --git a/src/reddit_bot.py b/src/reddit_bot.py new file mode 100644 index 0000000..6b560c6 --- /dev/null +++ b/src/reddit_bot.py @@ -0,0 +1,320 @@ +# standard imports +from datetime import datetime +import os +import random +import requests +import shelve +import socket +import sys +import threading +import time + +# lib imports +from libgravatar import Gravatar +import praw +from praw.util.token_manager import FileTokenManager + +# modify as required +APP = 'lizardbyte-bot' +VERSION = 'v1' +REDDIT_USER = 'ReenigneArcher' +USER_AGENT = f'{APP}/{VERSION} by u/{REDDIT_USER}' + +try: # for running in replit + redirect_uri = f'https://{os.environ["REPL_SLUG"]}.{os.environ["REPL_OWNER"].lower()}.repl.co' +except KeyError: + redirect_uri = os.environ['REDIRECT_URI'] + +bot_thread = threading.Thread(target=lambda: None) + +# directories +# parent directory name of this file, not full path +PARENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))).split(os.sep)[-1] +print(f'PARENT_DIR: {PARENT_DIR}') +if PARENT_DIR == 'app': + # running in Docker container + DATA_DIR = '/data' +else: + # running locally + DATA_DIR = os.path.join(os.getcwd(), 'data') +print(F'DATA_DIR: {DATA_DIR}') +os.makedirs(DATA_DIR, exist_ok=True) + +REFRESH_TOKEN_FILE = os.path.join(DATA_DIR, 'refresh_token') +LAST_ONLINE_FILE = os.path.join(DATA_DIR, 'last_online') + + +def initialize_refresh_token_file(): + if os.path.isfile(REFRESH_TOKEN_FILE): + return True + + # https://www.reddit.com/api/v1/scopes.json + scopes = [ + 'read', # Access posts and comments through my account. + ] + + reddit_auth = praw.Reddit( + client_id=os.environ['PRAW_CLIENT_ID'], + client_secret=os.environ['PRAW_CLIENT_SECRET'], + redirect_uri=redirect_uri, + user_agent=USER_AGENT, + ) + + state = str(random.randint(0, 65000)) + url = reddit_auth.auth.url(scopes=scopes, state=state, duration="permanent") + print(f"Now open this url in your browser: {url}") + + client, data = receive_connection() + param_tokens = data.split(" ", 2)[1].split("?", 1)[1].split("&") + params = { + key: value for (key, value) in [token.split("=") for token in param_tokens] + } + + if state != params["state"]: + send_message( + client, + f"State mismatch. Expected: {state} Received: {params['state']}", + ) + return False + elif "error" in params: + send_message(client, params["error"]) + return False + + refresh_token = reddit_auth.auth.authorize(params["code"]) + with open(REFRESH_TOKEN_FILE, 'w+') as f: + f.write(refresh_token) + + send_message(client, f"Refresh token: {refresh_token}") + print('Refresh token has been written to "refresh_token" file') + return True + + +def receive_connection(): + """Wait for and then return a connected socket. + + Opens a TCP connection on port 8080, and waits for a single client. + """ + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind(('0.0.0.0', 8080)) + server.listen() + # Handle one request from client + while True: + (clientSocket, clientAddress) = server.accept() + data = clientSocket.recv(1024) + + if data != b'': + # Wait until we receive the data from reddit + if data.startswith(b'GET /?state='): + # Send back what you received + clientSocket.send(data) + + break + server.close() + return clientSocket, data.decode("utf-8") + + +def send_message(client, message): + """Send message to client and close the connection.""" + print(f'message: {message}') + client.send(f"HTTP/1.1 200 OK\r\n\r\n{message}".encode("utf-8")) + client.close() + + +def get_bot_avatar(gravatar: str) -> str: + """Return the gravatar of a given email. + + :param gravatar: the gravatar email + :return: url to image + """ + + g = Gravatar(email=gravatar) + image_url = g.get_image() + + return image_url + + +def process_submission(submission): + last_online = get_last_online() + + if last_online < submission.created_utc: + print(f'submission id: {submission.id}') + print(f'submission title: {submission.title}') + print('---------') + + with shelve.open(os.path.join(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 = discord(db, submission) + db = flair(db, submission) + db = karma(db, submission) + + # re-write the last online time + last_online_writer() + + +def discord(db, submission): + # get the flair color + try: + color = int(submission.link_flair_background_color, 16) + except Exception: + color = int('ffffff', 16) + + try: + redditor = reddit.redditor(name=submission.author) + except Exception: + return + + submission_time = datetime.fromtimestamp(submission.created_utc) + + # actually send the message + discord_webhook = { + 'username': 'LizardByte-Bot', + 'avatar_url': avatar, + 'embeds': [ + { + 'author': { + 'name': str(submission.author), + 'url': f'https://www.reddit.com/user/{submission.author}', + 'icon_url': str(redditor.icon_img) + }, + 'title': str(submission.title), + 'url': str(submission.url), + 'description': str(submission.selftext), + 'color': color, + 'thumbnail': { + 'url': 'https://www.redditstatic.com/desktop2x/img/snoo_discovery@1x.png' + }, + 'footer': { + 'text': f'Posted on r/{os.environ["PRAW_SUBREDDIT"]} at {submission_time}', + 'icon_url': 'https://www.redditstatic.com/desktop2x/img/favicon/favicon-32x32.png' + } + } + ] + } + + 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())} + + return db + + +def flair(db, submission): + # todo + return db + + +def karma(db, submission): + # todo + return db + + +def commands(db, submission): + # todo + return db + + +def last_online_writer(): + last_online = int(time.time()) + with open(LAST_ONLINE_FILE, 'w') as f: + f.write(str(last_online)) + + return last_online + + +def get_last_online(): + try: + with open(LAST_ONLINE_FILE, 'r') as f: + last_online = int(f.read()) + except FileNotFoundError: + last_online = last_online_writer() + + return last_online + + +def init(): + required_env = [ + 'DISCORD_WEBHOOK', + 'PRAW_CLIENT_ID', + 'PRAW_CLIENT_SECRET', + 'PRAW_SUBREDDIT', + 'REDIRECT_URI' + ] + for env in required_env: + if env not in os.environ: + if env == 'REDIRECT_URI': + try: + os.environ["REPL_SLUG"] + except KeyError: + sys.stderr.write(f"Environment variable ``{env}`` must be defined\n") + else: + sys.stderr.write(f"Environment variable ``{env}`` must be defined\n") + return False + + # avatar + global avatar + avatar = get_bot_avatar(gravatar=os.environ['GRAVATAR_EMAIL']) + + # verify reddit refresh token or get new + token = initialize_refresh_token_file() + + if not token: + sys.exit(1) + + refresh_token_manager = FileTokenManager(REFRESH_TOKEN_FILE) + + global reddit + reddit = praw.Reddit( + client_id=os.environ['PRAW_CLIENT_ID'], + client_secret=os.environ['PRAW_CLIENT_SECRET'], + token_manager=refresh_token_manager, + user_agent=USER_AGENT, + ) + + subreddit = reddit.subreddit(os.environ['PRAW_SUBREDDIT']) # use "AskReddit" for testing + + # process submissions and then keep monitoring + for submission in subreddit.stream.submissions(): + process_submission(submission) + + +def start(): + global bot_thread + try: + # Start the reddit bot in a separate thread + bot_thread = threading.Thread(target=init, daemon=True) + bot_thread.start() + except KeyboardInterrupt: + print("Keyboard Interrupt Detected") + stop() + + +def stop(): + print("Attempting to stop reddit bot") + if bot_thread is not None and bot_thread.is_alive(): + # There's no built-in way to stop a Python thread directly. + # You'll need to add your own signal mechanism (like a stop flag) to the `init` function. + print("Reddit bot stopped")