gotta archive this lmao

This commit is contained in:
ForeverPyrite
2025-08-12 11:50:18 -05:00
parent 2ae3e4af7d
commit 3fb6390570
10 changed files with 841 additions and 741 deletions

10
.dockerignore Normal file → Executable file
View File

@@ -1,6 +1,6 @@
.gitignore .gitignore
/logs /logs
/.venv /.venv
/__pycache__ /__pycache__
*.pyc *.pyc
/data /data

0
.gitignore vendored Normal file → Executable file
View File

30
.vscode/launch.json vendored Normal file → Executable file
View File

@@ -1,16 +1,16 @@
{ {
// Use IntelliSense to learn about possible attributes. // Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes. // Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Python Debugger: Current File", "name": "Python Debugger: Current File",
"type": "debugpy", "type": "debugpy",
"request": "launch", "request": "launch",
"program": "${file}", "program": "${file}",
"console": "integratedTerminal" "console": "integratedTerminal"
} }
] ]
} }

32
Dockerfile Normal file → Executable file
View File

@@ -1,17 +1,17 @@
# Use the official Python image from the Docker Hub # Use the official Python image from the Docker Hub
FROM python:3.12-slim FROM python:3.12-slim
# Set working directory in the container # Set working directory in the container
WORKDIR /app WORKDIR /app
# Copy requirements.txt # Copy requirements.txt
COPY requirements.txt . COPY requirements.txt .
# Install the required packages # Install the required packages
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# Copy the rest of the application files # Copy the rest of the application files
COPY . . COPY . .
# Command to run the application # Command to run the application
CMD ["python", "app.py"] CMD ["python", "app.py"]

378
ai_logic.py Normal file → Executable file
View File

@@ -1,182 +1,196 @@
import logging # ----- Configuration & Setup -----
import os import logging
import asyncio import os
import discord # For type hintings import asyncio
from json import loads as load_json import discord # For type hintings
from json import loads as load_json
from openai import OpenAI
from openai.types.beta import Assistant, Thread from openai import OpenAI
from openai.types.beta.threads import Run, Message, RequiredActionFunctionToolCall from openai.types.beta import Assistant, Thread
from quotes import QUOTES from openai.types.beta.threads import Run, Message, RequiredActionFunctionToolCall
from students import * from quotes import QUOTES
from students import *
logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__)
OPENAI_KEY = os.getenv('OPENAI_KEY')
OPENAI_KEY = os.getenv('OPENAI_KEY')
# OpenAI & Assistant configuration
client: OpenAI = OpenAI( client: OpenAI = OpenAI(
api_key=OPENAI_KEY, api_key=OPENAI_KEY,
project="proj_vUQ7duhKKc1jxIqllRhy6DVJ", project="proj_vUQ7duhKKc1jxIqllRhy6DVJ",
) )
mr_jacobs: Assistant = client.beta.assistants.retrieve("asst_KdPdwqNAKijujfyCRrJCOgJN") mr_jacobs: Assistant = client.beta.assistants.retrieve("asst_KdPdwqNAKijujfyCRrJCOgJN")
base_instructions: str = mr_jacobs.instructions base_instructions: str = mr_jacobs.instructions
logger.info(f"Base instructions: {base_instructions}") logger.info(f"Base instructions: {base_instructions}")
instructions: str = base_instructions + f"\n\nHere is a dictionary containing some of your most well known quotes, organized by their frequency. DO NOT USE THE QUOTES FREQUENTLY! JUST UTILIZE THEM TO KNOW THE TYPES OF THINGS YOU SAY! \n{QUOTES}\n\n" instructions: str = base_instructions + f"\n\nHere is a dictionary containing some of your most well known quotes, organized by their frequency. DO NOT USE THE QUOTES FREQUENTLY! JUST UTILIZE THEM TO KNOW THE TYPES OF THINGS YOU SAY! \n{QUOTES}\n\n"
def get_run_status(run: Run) -> str: # ----- Helper Functions -----
""" def get_run_status(run: Run) -> str:
Retrieves the status of a run. """
Retrieves the status of a run.
Args:
run: The run object. Args:
run: The run object.
Returns:
str: The status of the run. Returns:
""" str: The status of the run.
status: str = client.beta.threads.runs.retrieve( """
run.id, status: str = client.beta.threads.runs.retrieve(
thread_id=run.thread_id run.id,
).status thread_id=run.thread_id
logger.info(f"Status of run {run.id} is {status}") ).status
return status logger.info(f"Status of run {run.id} is {status}")
return status
def get_instructions(context: discord.abc.GuildChannel | discord.Message = None) -> str:
""" def get_instructions(context: discord.abc.GuildChannel | discord.Message = None) -> str:
Retrieves the instructions for the AI based on the context. """
Retrieves the instructions for the AI based on the context.
Args:
context (discord.abc.GuildChannel | discord.Message | None): The context from which to derive additional instructions. Args:
If it's a GuildChannel, includes channel name and category. context (discord.abc.GuildChannel | discord.Message | None): The context from which to derive additional instructions.
If it's a Message, includes channel name and category from the message. If it's a GuildChannel, includes channel name and category.
If it's a Message, includes channel name and category from the message.
Returns:
str: The instructions for the AI. Returns:
""" str: The instructions for the AI.
tuned_instructions: str = instructions """
tuned_instructions: str = instructions
if context:
tuned_instructions += "\n\nNow I will give you the context of the channel that the message is sent in, keep it in mind. Please try to remain conversational depending on the topic of the channel, and don't be worshiping Cisco so religiously outside of on-topic. It's okay to joke about it, but you still wanna goof around with the students sometimes. However, if they have an essay, you should definately get on them about it outside of on-topic, so that you can still be helpful when they are talking about things on-topic, but can scold them to do their essay in any other channel. You should take the channel info fairly seriously, please. I really hate having to make such extreme channel descriptions to see a slight change in behavior from the Cisco worshipping. :\n" if context:
if isinstance(context, discord.Message): tuned_instructions += "\n\nNow I will give you the context of the channel that the message is sent in, keep it in mind. Please try to remain conversational depending on the topic of the channel, and don't be worshiping Cisco so religiously outside of on-topic. It's okay to joke about it, but you still wanna goof around with the students sometimes. However, if they have an essay, you should definately get on them about it outside of on-topic, so that you can still be helpful when they are talking about things on-topic, but can scold them to do their essay in any other channel. You should take the channel info fairly seriously, please. I really hate having to make such extreme channel descriptions to see a slight change in behavior from the Cisco worshipping. :\n"
context = context.channel if isinstance(context, discord.Message):
if not isinstance(context, discord.abc.Messageable): context = context.channel
logger.critical(f"Literally how are we going to send a message in a channel that is not messageable? (bro how did we get here, wtf was passed as context? oh: {context}") if not isinstance(context, discord.abc.Messageable):
return tuned_instructions logger.critical(f"Literally how are we going to send a message in a channel that is not messageable? (bro how did we get here, wtf was passed as context? oh: {context}")
if isinstance(context, discord.TextChannel): return tuned_instructions
tuned_instructions += f"\nChannel Category: {context.category.name}\nChannel Name:{context.name}\nChannel Description: {context.topic if isinstance(context, discord.TextChannel) else 'No Description'}" if isinstance(context, discord.TextChannel):
elif isinstance(context, discord.DMChannel): tuned_instructions += f"\nChannel Category: {context.category.name}\nChannel Name:{context.name}\nChannel Description: {context.topic if isinstance(context, discord.TextChannel) else 'No Description'}"
logger.warning(f"DM Channel detected, adding context to instructions.\nDM channel with {ID_TO_NAME.get(context.recipient.id, context.recipient.name)}") elif isinstance(context, discord.DMChannel):
tuned_instructions += f"\nThis is a direct message with {STUDENTS.get(context.recipient.id) if context.recipient.id in STUDENTS else context.recipient.name + 'No extra known info on user.'}" logger.warning(f"DM Channel detected, adding context to instructions.\nDM channel with {ID_TO_NAME.get(context.recipient.id, context.recipient.name)}")
tuned_instructions += f"\nThis is a direct message with {STUDENTS.get(context.recipient.id) if context.recipient.id in STUDENTS else context.recipient.name + 'No extra known info on user.'}"
return tuned_instructions
return tuned_instructions
async def handle_action(run: Run) -> bool:
# Define the list to store tool outputs # ----- Action Handlers -----
tool_outputs = [] async def handle_action(run: Run) -> bool:
tool: RequiredActionFunctionToolCall # Define the list to store tool outputs
# Loop through each tool in the required action section tool_outputs = []
try: tool: RequiredActionFunctionToolCall
logger.debug(f"Run.require_action is currently: {run.required_action} and tool calls are {run.required_action.submit_tool_outputs.tool_calls}") # Loop through each tool in the required action section
for tool in run.required_action.submit_tool_outputs.tool_calls: try:
logger.info(f"Handling action for tool {tool.id}, function {tool.function.name}") logger.debug(f"Run.require_action is currently: {run.required_action} and tool calls are {run.required_action.submit_tool_outputs.tool_calls}")
if tool.function.name == "assign_essay": for tool in run.required_action.submit_tool_outputs.tool_calls:
logger.debug(f"Handling action for assign_essay tool. Received arguments {tool.function.arguments}") logger.info(f"Handling action for tool {tool.id}, function {tool.function.name}")
args = load_json(tool.function.arguments) if tool.function.name == "assign_essay":
tool_outputs.append({ logger.debug(f"Handling action for assign_essay tool. Received arguments {tool.function.arguments}")
"tool_call_id": tool.id, args = load_json(tool.function.arguments)
"output": assign_essay(int(run.metadata.get("user_id"))) if not tool.function.arguments else assign_essay(int(run.metadata.get("user_id")), args["topic"]) tool_outputs.append({
}) "tool_call_id": tool.id,
elif tool.function.name == "clear_essay": "output": assign_essay(int(run.metadata.get("user_id"))) if not tool.function.arguments else assign_essay(int(run.metadata.get("user_id")), args["topic"])
logger.debug(f"Clearing the essay for student {run.metadata.get('user_id')}") })
clear_essay(int(run.metadata.get("user_id"))) elif tool.function.name == "clear_essay":
tool_outputs.append({ logger.debug(f"Clearing the essay for student {run.metadata.get('user_id')}")
"tool_call_id": tool.id, clear_essay(int(run.metadata.get("user_id")))
"output": "Essay cleared sucessfully, feel free to still give the student feedback on their essay because it couldn't have been perfect." tool_outputs.append({
}) "tool_call_id": tool.id,
except AttributeError as e: "output": "Essay cleared sucessfully, feel free to still give the student feedback on their essay because it couldn't have been perfect."
logger.error(f"Failed to handle action: {e}") })
except Exception as e: except AttributeError as e:
logger.critical(f"An unexpected error occurred while handling action: {e}") logger.error(f"Failed to handle action: {e}")
finally: except Exception as e:
logger.info(f"Tool outputs: {tool_outputs}") logger.critical(f"An unexpected error occurred while handling action: {e}")
if tool_outputs: finally:
try: logger.info(f"Tool outputs: {tool_outputs}")
run = client.beta.threads.runs.submit_tool_outputs_and_poll( if tool_outputs:
thread_id=run.thread_id, try:
run_id=run.id, run = client.beta.threads.runs.submit_tool_outputs_and_poll(
tool_outputs=tool_outputs thread_id=run.thread_id,
) run_id=run.id,
logger.info("Tool outputs submitted successfully.") tool_outputs=tool_outputs
except Exception as e: )
logger.error("Failed to submit tool outputs:", e) logger.info("Tool outputs submitted successfully.")
else: except Exception as e:
logger.warning("No tool outputs to submit.") logger.error("Failed to submit tool outputs:", e)
else:
logger.warning("No tool outputs to submit.")
async def handle_run(run: Run) -> bool:
while True:
run = client.beta.threads.runs.retrieve( # ----- Run Handlers -----
run.id, async def handle_run(run: Run) -> bool:
thread_id=run.thread_id """Manages a run and all it's different possible states.
)
match run.status: Args:
case "completed": run (Run): The run to maintain.
return True
case "failed": Returns:
logger.error(f"Run {run.id} failed.") bool: Whether or not the run completed successfully.
return False """
case "requires_action": while True:
logger.info(f"Run {run.id} requires action.") run = client.beta.threads.runs.retrieve(
await handle_action(run) run.id,
case _: thread_id=run.thread_id
await asyncio.sleep(1) )
await asyncio.sleep(1) match run.status:
case "completed":
return True
async def run(messages: list[dict], instructions: str = instructions, user_id : int = None) -> str: case "failed":
""" logger.error(f"Run {run.id} failed.")
Runs the AI with the given messages and instructions. return False
case "requires_action":
Args: logger.info(f"Run {run.id} requires action.")
messages (list[dict]): The list of messages. await handle_action(run)
instructions (str): The instructions for the AI. case "expired":
user_id (int): The Discord ID of the user who initiated the run. logger.error(f"Run {run.id} expired.")
return False
Returns: case _:
str: The response from the AI. await asyncio.sleep(1)
""" await asyncio.sleep(1)
logger.debug(f"Running AI assistant with the following parameters:\nmessages={messages}\ninstructions={instructions}\nmetadata={str(user_id if user_id else -1)}")
run = client.beta.threads.create_and_run(
assistant_id=mr_jacobs.id, async def run(messages: list[dict], instructions: str = instructions, user_id : int = None) -> str:
instructions=instructions, """
thread={ Runs the AI with the given messages and instructions.
"messages": messages
}, Args:
metadata={ messages (list[dict]): The list of messages.
"user_id": str(user_id if user_id else -1) instructions (str): The instructions for the AI.
} user_id (int): The Discord ID of the user who initiated the run.
)
response = await run_message(run) Returns:
return response str: The response from the AI.
"""
logger.debug(f"Running AI assistant with the following parameters:\nmessages={messages}\ninstructions={instructions}\nmetadata={str(user_id if user_id else -1)}")
async def run_message(run) -> str: run = client.beta.threads.create_and_run(
""" assistant_id=mr_jacobs.id,
Retrieves the response message from a run. instructions=instructions,
thread={
Args: "messages": messages
run: The run object. },
metadata={
Returns: "user_id": str(user_id if user_id else -1)
str: The response message. }
""" )
logger.debug(f"Retrieving response message for run ID {run.id}") response = await run_message(run)
# ew but Python doesn't have a do while and it's less ugly than the same statement twice. return response
await handle_run(run)
thread_messages = client.beta.threads.messages.list(run.thread_id)
for msg_ob in thread_messages.data: async def run_message(run) -> str:
if msg_ob.id == thread_messages.first_id: """
response = msg_ob.content[0].text.value Retrieves the response message from a run.
return response
logger.critical(f"Couldn't find the msg that matched with the first message ID:\nThread Messages List:\n{thread_messages}") Args:
run: The run object.
Returns:
str: The response message.
"""
logger.debug(f"Retrieving response message for run ID {run.id}")
# ew but Python doesn't have a do while and it's less ugly than the same statement twice.
await handle_run(run)
thread_messages = client.beta.threads.messages.list(run.thread_id)
for msg_ob in thread_messages.data:
if msg_ob.id == thread_messages.first_id:
response = msg_ob.content[0].text.value
return response
logger.critical(f"Couldn't find the msg that matched with the first message ID:\nThread Messages List:\n{thread_messages}")

78
app.py Normal file → Executable file
View File

@@ -1,40 +1,40 @@
import os import os
import logging import logging
from dotenv import load_dotenv from dotenv import load_dotenv
import pytz import pytz
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from discord_logic import bot, setup_discord_bot, send_quote, after_class from discord_logic import bot, setup_discord_bot, send_quote, after_class
# Load environment variables # Load environment variables
load_dotenv() load_dotenv()
DISCORD_TOKEN = os.getenv('DISCORD_TOKEN') DISCORD_TOKEN = os.getenv('DISCORD_TOKEN')
# Logging. # Logging.
logging.basicConfig( logging.basicConfig(
filename=f"./logs/jacobs.log", filename=f"./logs/jacobs.log",
filemode="at+", filemode="at+",
level=logging.DEBUG if os.getenv('DEBUG') == "True" else logging.INFO, level=logging.DEBUG if os.getenv('DEBUG') == "True" else logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
setup_discord_bot(bot) setup_discord_bot(bot)
@bot.event @bot.event
async def on_ready() -> None: async def on_ready() -> None:
"""Event handler for when the bot is initialized.""" """Event handler for when the bot is initialized."""
logger.info(f"{bot.user.name} has connected to Discord and is ready.") logger.info(f"{bot.user.name} has connected to Discord and is ready.")
print(f"{bot.user.name} is ready.") print(f"{bot.user.name} is ready.")
# After successful initialization, schedule tasks. # After successful initialization, schedule tasks.
task_scheduler = BackgroundScheduler(timezone=pytz.timezone('EDT')) task_scheduler = BackgroundScheduler(timezone=pytz.timezone('EST'))
task_scheduler.add_job(lambda: bot.loop.create_task(send_quote()), 'cron', day_of_week='mon-fri', hour=8, minute=50) task_scheduler.add_job(lambda: bot.loop.create_task(send_quote()), 'cron', day_of_week='mon-fri', hour=8, minute=50)
task_scheduler.add_job(lambda: bot.loop.create_task(send_quote()), 'cron', day_of_week='mon-fri', hour='8-14', minute="*/20", jitter=180) task_scheduler.add_job(lambda: bot.loop.create_task(send_quote()), 'cron', day_of_week='mon-fri', hour='8-14', minute="*/20", jitter=180)
task_scheduler.add_job(lambda: bot.loop.create_task(after_class()), 'cron', day_of_week='mon-fri', hour=10, minute=55) task_scheduler.add_job(lambda: bot.loop.create_task(after_class()), 'cron', day_of_week='mon-fri', hour=10, minute=55)
task_scheduler.start() task_scheduler.start()
logger.info("Presumably successfully initialized, starting bot.") logger.info("Presumably successfully initialized, starting bot.")
bot.run(DISCORD_TOKEN) bot.run(DISCORD_TOKEN)

908
discord_logic.py Normal file → Executable file
View File

@@ -1,411 +1,497 @@
import logging # ----- Imports & Setup -----
import regex as re import logging
from random import randint import regex as re
from asyncio import run as asyncio_run, sleep from random import randint
import discord from asyncio import run as asyncio_run, sleep
from discord import Bot, Message from datetime import timedelta
from discord.abc import GuildChannel as Channel import discord
from discord import Bot, Message
from ai_logic import run, get_instructions from discord.abc import GuildChannel as Channel
from quotes import select_quote, select_student from discord.ext import commands
from students import *
from ai_logic import run, get_instructions
logger = logging.getLogger(__name__) from quotes import select_quote, select_student
from students import *
SERVER_ID: int = 1339601046745645148
GENERAL_ID: int = 1339601047294840876 logger = logging.getLogger(__name__)
bot = Bot(intents=discord.Intents.all()) SERVER_ID: int = 1339601046745645148
GENERAL_ID: int = 1339601047294840876
# ---------- Discord Helper Functions ----------
async def get_channel(channel_id: int) -> Channel: bot = Bot(intents=discord.Intents.all())
"""
Tries to get a cached channel object, if it fails it will send an API request to retrieve a new one. # ----- Discord Helper Functions -----
async def get_channel(channel_id: int) -> Channel:
Args: """
channel_id (int): The ID of the channel to retrieve. Tries to get a cached channel object, if it fails it will send an API request to retrieve a new one.
Raises: Args:
ValueError: If the channel cannot be retrieved. channel_id (int): The ID of the channel to retrieve.
Returns: Raises:
discord.abc.GuildChannel: The retrieved discord channel object. ValueError: If the channel cannot be retrieved.
"""
logger.debug(f"Attempting to get channel with ID {channel_id}") Returns:
channel = bot.get_channel(channel_id) discord.abc.GuildChannel: The retrieved discord channel object.
if not channel: """
logger.debug(f"Channel with ID {channel_id} not found in cache, fetching from API.") logger.debug(f"Attempting to get channel with ID {channel_id}")
channel = await bot.fetch_channel(channel_id) channel = bot.get_channel(channel_id)
if not channel: if not channel:
logger.critical(f"Could not fetch channel with channel_id {channel_id}, fetch_channel returned None.") logger.debug(f"Channel with ID {channel_id} not found in cache, fetching from API.")
return None channel = await bot.fetch_channel(channel_id)
logger.info(f"Successfully retrieved {channel_id}, #{channel.name}") if not channel:
return channel logger.critical(f"Could not fetch channel with channel_id {channel_id}, fetch_channel returned None.")
return None
async def get_message(message: Message | int, channel_id: int = None, attempts: int = 3) -> Message: logger.info(f"Successfully retrieved {channel_id}, #{channel.name}")
""" return channel
Retrieves a message object either from cache or by fetching it from the channel.
async def get_message(message: Message | int, channel_id: int = None, attempts: int = 3) -> Message:
Args: """
message (Message | int): The message object or message ID to retrieve. Retrieves a message object either from cache or by fetching it from the channel.
channel_id (int, optional): The ID of the channel to fetch the message from if not in cache.
attempts (int, optional): The number of attempts to retrieve the message. Defaults to 3. Args:
message (Message | int): The message object or message ID to retrieve.
Returns: channel_id (int, optional): The ID of the channel to fetch the message from if not in cache.
Message: The retrieved message object. attempts (int, optional): The number of attempts to retrieve the message. Defaults to 3.
"""
logger.debug(f"Attempting to get message with ID {message if isinstance(message, int) else message.id}") Returns:
for attempt in range(attempts): Message: The retrieved message object.
if isinstance(message, int): """
got_message: Message = bot.get_message(message) logger.debug(f"Attempting to get message with ID {message if isinstance(message, int) else message.id}")
if not got_message and channel_id: for attempt in range(attempts):
logger.debug(f"Message with ID {message} not found in cache, fetching from channel ID {channel_id}.") if isinstance(message, int):
channel: discord.abc.GuildChannel = await get_channel(channel_id) got_message: Message = bot.get_message(message)
got_message: Message = await channel.fetch_message(message) if not got_message and channel_id:
elif not got_message: logger.debug(f"Message with ID {message} not found in cache, fetching from channel ID {channel_id}.")
logger.error(f"Message with ID {message} not found in cache and no channel ID provided.") channel: discord.abc.GuildChannel = await get_channel(channel_id)
elif isinstance(message, Message): got_message: Message = await channel.fetch_message(message)
got_message: Message = bot.get_message(message.id) elif not got_message:
if not got_message: logger.error(f"Message with ID {message} not found in cache and no channel ID provided.")
logger.debug(f"Message with ID {message.id} not found in cache, fetching from channel.") elif isinstance(message, Message):
got_message: Message = await message.channel.fetch_message(message.id) got_message: Message = bot.get_message(message.id)
else: if not got_message:
logger.error(f"Couldn't retrieve message:\nMessage: {message}\nChannel ID: {channel_id}") logger.debug(f"Message with ID {message.id} not found in cache, fetching from channel.")
return None got_message: Message = await message.channel.fetch_message(message.id)
else:
if got_message: logger.error(f"Couldn't retrieve message:\nMessage: {message}\nChannel ID: {channel_id}")
return got_message return None
if attempt < attempts - 1: if got_message:
logger.warning(f"Attempt {attempt + 1} failed. Retrying in 2 seconds...") return got_message
await sleep(2)
if attempt < attempts - 1:
logger.error(f"Failed to retrieve message after {attempts} attempts.") logger.warning(f"Attempt {attempt + 1} failed. Retrying in 2 seconds...")
return None await sleep(2)
async def send_message(message: str, channel: int | Channel = GENERAL_ID) -> Message: logger.error(f"Failed to retrieve message after {attempts} attempts.")
""" return None
Has the bot send a message to a specified channel. If no channel is specified, the bot sends it to general.
async def send_message(message: str, channel: int | Channel = GENERAL_ID) -> Message:
Args: """
message (str): The message to send. Has the bot send a message to a specified channel. If no channel is specified, the bot sends it to general.
channel (int | Message, optional): Channel to send message to, Defaults to GENERAL_ID.
Args:
Returns: message (str): The message to send.
Message: The sent message object. channel (int | Message, optional): Channel to send message to, Defaults to GENERAL_ID.
"""
logger.debug(f"Attempting to send message: {message}") Returns:
if isinstance(channel, int): Message: The sent message object.
channel = await get_channel(channel) """
if isinstance(channel, Channel): logger.debug(f"Attempting to send message: {message}")
sent_message: Message = await channel.send(message) if isinstance(channel, int):
if isinstance(sent_message, Message): channel = await get_channel(channel)
logger.info(f"Message '{message}' successfully sent to {sent_message.channel}") if isinstance(channel, Channel):
return sent_message sent_message: Message = await channel.send(message)
else: if isinstance(sent_message, Message):
logger.error(f"Message likely wasn't successfully sent, as channel.send did not return a Message.") logger.info(f"Message '{message}' successfully sent to {sent_message.channel}")
elif isinstance(channel, None): return sent_message
logger.error(f"Message couldn't be sent as a channel wasn't found/messagable.") else:
logger.error(f"Message likely wasn't successfully sent, as channel.send did not return a Message.")
async def edit_message(message: Message | int, channel: int = None): elif isinstance(channel, None):
""" logger.error(f"Message couldn't be sent as a channel wasn't found/messagable.")
Edits a message in a channel.
async def edit_message(message: Message | int, channel: int = None):
Args: """
message (Message | int): The message to edit. Edits a message in a channel.
channel (int, optional): The channel ID of the message. Defaults to None.
""" Args:
logger.debug(f"Attempting to edit message {message if isinstance(message, int) else message.id}") message (Message | int): The message to edit.
if isinstance(message, int): channel (int, optional): The channel ID of the message. Defaults to None.
message = await get_message(message, channel) """
if isinstance(message, Message): logger.debug(f"Attempting to edit message {message if isinstance(message, int) else message.id}")
await message.edit(content=message.content) if isinstance(message, int):
logger.info(f"Message {message.id} successfully edited.") message = await get_message(message, channel)
else: if isinstance(message, Message):
logger.error(f"Message couldn't be edited as it was not found.") await message.edit(content=message.content)
logger.info(f"Message {message.id} successfully edited.")
# ---------- Message Utility Functions ---------- else:
async def msg_is_reply(message: Message) -> tuple[bool, Message]: logger.error(f"Message couldn't be edited as it was not found.")
"""
Checks if a message is a reply to another message. # ----- Message Utility Functions -----
async def msg_is_reply(message: Message) -> tuple[bool, Message]:
Args: """
message (Message): The message to check. Checks if a message is a reply to another message.
Returns: Args:
tuple[bool, Message]: A tuple containing a boolean indicating if the message is a reply and the replied message. message (Message): The message to check.
"""
logger.debug(f"Checking if message ID {message.id} is a reply.") Returns:
if message.reference is not None: tuple[bool, Message]: A tuple containing a boolean indicating if the message is a reply and the replied message.
replied_msg = message.reference.cached_message """
logger.debug(f"Checking if message ID {message.id} is a reply.")
if replied_msg is None: if message.reference is not None:
logger.debug(f"Replied message not found in cache, fetching from channel ID {message.reference.channel_id}.") replied_msg = message.reference.cached_message
channel = await get_channel(message.reference.channel_id)
replied_msg = await channel.fetch_message(message.reference.message_id) if replied_msg is None:
logger.debug(f"Replied message not found in cache, fetching from channel ID {message.reference.channel_id}.")
if isinstance(replied_msg, Message): channel = await get_channel(message.reference.channel_id)
return True, replied_msg replied_msg = await channel.fetch_message(message.reference.message_id)
return False, None
if isinstance(replied_msg, Message):
async def get_reply_chain(msg: Message) -> list[Message]: return True, replied_msg
""" return False, None
Retrieves the chain of replies for a given message.
async def get_reply_chain(msg: Message) -> list[Message]:
Args: """
msg (Message): The message to get the reply chain for. Retrieves the chain of replies for a given message.
Returns: Args:
list[Message]: A list of messages in the reply chain. msg (Message): The message to get the reply chain for.
"""
logger.debug(f"Getting reply chain for message ID {msg.id}.") Returns:
i = 0 list[Message]: A list of messages in the reply chain.
reply_chain = [msg] """
while i < 10: logger.debug(f"Getting reply chain for message ID {msg.id}.")
is_reply, msg = await msg_is_reply(msg) i = 0
if not is_reply: reply_chain = [msg]
break while i < 10:
reply_chain.insert(0, msg) is_reply, msg = await msg_is_reply(msg)
i += 1 if not is_reply:
return reply_chain break
reply_chain.insert(0, msg)
async def gen_message_list(discord_messages: list[Message]) -> list[dict]: i += 1
""" return reply_chain
Generates a list of message dictionaries from a list of Discord messages.
async def gen_message_list(discord_messages: list[Message]) -> list[dict]:
Args: """
discord_messages (list[Message]): The list of Discord messages. Generates a list of message dictionaries from a list of Discord messages.
Returns: Args:
list[dict]: A list of message dictionaries. discord_messages (list[Message]): The list of Discord messages.
"""
logger.debug("Generating message list from Discord messages.") Returns:
messages = [] list[dict]: A list of message dictionaries.
if not isinstance(discord_messages, list): """
discord_messages = [discord_messages] logger.debug("Generating message list from Discord messages.")
messages = []
for message in discord_messages: if not isinstance(discord_messages, list):
students_mentioned = None discord_messages = [discord_messages]
if isinstance(message, Message):
message_copy = { for message in discord_messages:
"id": message.id, students_mentioned = None
"content": message.content, if isinstance(message, Message):
"author": message.author, message_copy = {
"mentions": message.mentions, "id": message.id,
"created_at": message.created_at, "content": message.content,
"jump_url": message.jump_url "author": message.author,
} "mentions": message.mentions,
role = "assistant" if message_copy["author"].id == bot.user.id else "user" "created_at": message.created_at,
students_mentioned = None "jump_url": message.jump_url
}
if message_copy["mentions"] and role == "user": role = "assistant" if message_copy["author"].id == bot.user.id else "user"
students_mentioned = [student.id for student in message_copy["mentions"] if student.id in STUDENT_IDS] students_mentioned = None
mentions = re.findall(r'<@\d{17,19}>', message_copy["content"])
for mention in mentions: if message_copy["mentions"] and role == "user":
mention_id = mention.replace("<@", "").replace(">", "") students_mentioned = [student.id for student in message_copy["mentions"] if student.id in STUDENT_IDS]
student_info = ID_TO_NAME.get(int(mention_id)) mentions = re.findall(r'<@\d{17,19}>', message_copy["content"])
if student_info: for mention in mentions:
message_copy["content"] = message_copy["content"].replace(mention, student_info.split("\n")[0]) mention_id = mention.replace("<@", "").replace(">", "")
message_copy["content"] += '\n{' student_info = ID_TO_NAME.get(int(mention_id))
if students_mentioned: if student_info:
message_copy["content"] += f"\n\nInfo on students mentioned (DO NOT REPEAT!): " message_copy["content"] = message_copy["content"].replace(mention, student_info.split("\n")[0])
for student in students_mentioned: message_copy["content"] += '\n{'
message_copy["content"] += f"\n{STUDENTS.get(student)}" if students_mentioned:
message_copy["content"] += f"\n\nSent by: {STUDENTS.get(message_copy['author'].id) if message_copy['author'].id in STUDENT_IDS else message_copy['author'].name + ' (Author\'s details not found)'}" + '}' message_copy["content"] += f"\n\nInfo on students mentioned (DO NOT REPEAT!): "
for student in students_mentioned:
if not message_copy["content"]: message_copy["content"] += f"\n{STUDENTS.get(student)}"
debug = True message_copy["content"] += f"\n\nSent by: {STUDENTS.get(message_copy['author'].id) if message_copy['author'].id in STUDENT_IDS else message_copy['author'].name + ' (Author\'s details not found)'}" + '}'
logger.error(f"No message content for message {message_copy['id']}.")
fetched_message = await get_message(message_copy["id"]) if not message_copy["content"]:
message_copy["content"] = fetched_message.content debug = True
logger.error(f"No message content for message {message_copy['id']}.")
thread_message = { fetched_message = await get_message(message_copy["id"])
"role": role, message_copy["content"] = fetched_message.content
"content": message_copy["content"]
} thread_message = {
debug = True # Delete after fix "role": role,
if debug: "content": message_copy["content"]
debug_dump = "Messages Dump:\n" }
for msg_copy in discord_messages: debug = True # Delete after fix
debug_dump += f""" if debug:
Message: {msg_copy} debug_dump = "Messages Dump:\n"
Message Time: {msg_copy.created_at} for msg_copy in discord_messages:
Message ID: {msg_copy.id} debug_dump += f"""
Message Author: {msg_copy.author} Message: {msg_copy}
Message Content: {msg_copy.content} Message Time: {msg_copy.created_at}
Link: {msg_copy.jump_url} Message ID: {msg_copy.id}
""" Message Author: {msg_copy.author}
logger.debug(debug_dump) Message Content: {msg_copy.content}
messages.append(thread_message) Link: {msg_copy.jump_url}
else: """
logger.warning(f"Argument {message} is not of type Message and will be skipped.") logger.debug(debug_dump)
messages.append(thread_message)
return messages else:
logger.warning(f"Argument {message} is not of type Message and will be skipped.")
# ---------- AI Interaction Functions ----------
async def send_quote(quote: str = None) -> None: return messages
"""
Sends a quote to the general channel. # ----- AI Interaction Functions -----
async def send_quote(quote: str = None) -> None:
Args: """
quote (str, optional): The quote to send. If not provided, a random quote is selected. Sends a quote to the general channel.
"""
if not quote: Args:
quote = select_quote() quote (str, optional): The quote to send. If not provided, a random quote is selected.
logger.info(f"Sending quote '{quote}' in #general...") """
await send_message(quote) if not quote:
quote = select_quote()
async def after_class(student: int = None) -> None: logger.info(f"Sending quote '{quote}' in #general...")
""" await send_message(quote)
Sends a message to a student to see the bot after class.
async def after_class(student: int = None) -> None:
Args: """
student (int, optional): The ID of the student. If not provided, a random student is selected. Sends a message to a student to see the bot after class.
"""
if not student: Args:
student = select_student() student (int, optional): The ID of the student. If not provided, a random student is selected.
logger.info(f"Sending mention to see {student} after class to #general...") """
await send_message(f"Come see me after class <@{student}>") if not student:
student = select_student()
async def has_essay(message: Message) -> bool: logger.info(f"Sending mention to see {student} after class to #general...")
if message.author.id in ASSIGNED_ESSAY: await send_message(f"Come see me after class <@{student}>")
async with message.channel.typing():
message = await get_message(message) if not message else message async def has_essay(message: Message) -> bool:
message_list = await gen_message_list(message) if message.author.id in ASSIGNED_ESSAY:
response = await run( async with message.channel.typing():
message_list, message = await get_message(message) if not message else message
f"{get_instructions(message)}\n\nTHIS USER HAS AN ESSAY!!! Yell at them and tell them to finish their essay on {ASSIGNED_ESSAY.get(message.author.id)}!!! Unless...they sent you the essay, in that case, you can decide whether or not it is satisfactory (it needs to be in MLA format and 300-2000 characters long, and to be relevant of course). If it is satisfactory, you can tell them they are free to go, after you run the function \"clear_essay\".", message_list = await gen_message_list(message)
message.author.id response = await run(
) message_list,
await message.reply(response) f"{get_instructions(message)}\n\nTHIS USER HAS AN ESSAY!!! Yell at them and tell them to finish their essay on {ASSIGNED_ESSAY.get(message.author.id)}!!! Unless...they sent you the essay, in that case, you can decide whether or not it is satisfactory (it needs to be in MLA format and 300-2000 characters long, and to be relevant of course). If it is satisfactory, you can tell them they are free to go, after you run the function \"clear_essay\".",
return True message.author.id
else: )
return False await message.reply(response)
return True
# ---------- Discord Commands & Event Handlers ---------- else:
def setup_discord_bot(bot: Bot) -> None: return False
"""
Sets up the Discord bot with commands and event listeners. # ----- Discord Commands & Event Handlers -----
def setup_discord_bot(bot: Bot) -> None:
Args: """
bot (Bot): The Discord bot instance. Sets up the Discord bot with commands and event listeners.
"""
@bot.slash_command(description="Talk to Mr. Jacobs!!!") Args:
async def message(ctx: discord.ApplicationContext, text: str) -> None: bot (Bot): The Discord bot instance.
""" """
Slash command to talk to Mr. Jacobs. The bot will respond to the user's message using AI. @bot.slash_command(description="Talk to Mr. Jacobs!!!")
async def message(ctx: discord.ApplicationContext, text: str) -> None:
Args: """
ctx (discord.ApplicationContext): The context of the command. Slash command to talk to Mr. Jacobs. The bot will respond to the user's message using AI.
text (str): The message text.
""" Args:
logging.info(f"User {ctx.author.global_name} sent message {text}") ctx (discord.ApplicationContext): The context of the command.
await ctx.defer() text (str): The message text.
instructions = get_instructions(ctx.channel) """
thread_messages = [{"role": "user", "content": text}] logging.info(f"User {ctx.author.global_name} sent message {text}")
response = await run(thread_messages, instructions, ctx.author.id) await ctx.defer()
await ctx.respond(content=response) instructions = get_instructions(ctx.channel)
thread_messages = [{"role": "user", "content": text}]
@bot.slash_command( response = await run(thread_messages, instructions, ctx.author.id)
name="rps", await ctx.respond(content=response)
description="Play \"Rock, Paper, Scissors, Essay\" with Mr. Jacobs!!!",
) @bot.slash_command(name="clear_essay", description="Clear the essay assigned to a student")
async def rps_essay( async def clear_essay_command(ctx: discord.ApplicationContext, student: str) -> None:
ctx: discord.ApplicationContext, """
choice: str = discord.Option( Slash command to clear the essay assigned to a student.
description="Your selection for the game",
choices=[ Args:
discord.OptionChoice("Rock"), ctx (discord.ApplicationContext): The context of the command.
discord.OptionChoice("Paper"), student (str): The ID of the student whose essay is to be cleared.
discord.OptionChoice("Scissors") """
] try:
) student = int(student)
) -> None: except Exception as e:
""" logging.error(f"Failed to convert student ID to int: {e}")
Play Rock Paper Scissors with Mr. Jacobs. await ctx.respond("Invalid student ID format.", ephemeral=True)
return
Args: if not ctx.author.id == 620319269233885226:
ctx: Application command context await ctx.respond("You don't have permission to use this command.")
choice: Your selection for the game return
""" if student not in STUDENT_IDS:
logging.info(f"{ctx.author} chose {choice}") await ctx.respond("Invalid student ID.", ephemeral=True)
outcomes = {"Rock": "Paper", "Paper": "Scissors", "Scissors": "Rock"} return
await ctx.respond( ASSIGNED_ESSAY.pop(student, None)
f"I choose {outcomes[choice]}, you owe me a {randint(1,15)} page essay " await ctx.respond(f"Cleared essay for <@{student}>")
f"on {assign_essay(ctx.author.id)}. MLA format, double spaced, "
"12pt Times New Roman!" @bot.slash_command(name="get_essay", description="Get the essay assigned to a student, or all students")
) async def get_essay_command(ctx: discord.ApplicationContext, student: str = None) -> None:
if student:
@bot.event try:
async def on_message(message: Message) -> None: student = int(student)
""" except Exception as e:
Event listener for new messages. The bot will respond to messages that mention it or reply to its messages. logging.error(f"Failed to convert student ID to int: {e}")
Bot disregards its own messages. await ctx.respond("Invalid student ID format.", ephemeral=True)
return
Args: await ctx.respond(get_essay(student))
message (Message): The new message. else:
""" await ctx.respond(get_essay(), ephemeral=True)
logger.debug(f"New message received: {message.content}")
@bot.slash_command(name="assign_essay", description="Assign an essay to a student")
if message.author.id == bot.user.id or message.interaction_metadata: @commands.has_permissions(moderate_members=True)
return async def assign_essay_command(
ctx: discord.ApplicationContext,
if await handle_reply(message): student: str,
return timeout: int | None = 0,
topic: str | None = None
if await handle_mentions(message): ) -> None:
return """
Slash command to assign an essay to a student.
async def handle_reply(message: Message) -> bool:
""" Args:
Handles replies to the bot's messages. ctx (discord.ApplicationContext): The context of the command.
student (int): The ID of the student to assign the essay to.
Args: timeout (int | None, optional): The timeout in seconds before the essay is cleared. Defaults to 0.
message (Message): The new message. topic (str | None, optional): The topic of the essay. If not provided, a random topic is selected.
"""
Returns: try:
bool: True if the message was handled, False otherwise. student = int(student)
""" except Exception as e:
is_reply, first_reply = await msg_is_reply(message) logging.error(f"Failed to convert student ID to int: {e}")
if is_reply and first_reply.author.id == bot.user.id: await ctx.respond("Invalid student ID format.")
async with message.channel.typing(): return
if await has_essay(message): if not ctx.author.id == 620319269233885226:
return True await ctx.respond("You don't have permission to use this command.")
reply_chain = await get_reply_chain(message) return
thread_messages = await gen_message_list(reply_chain) if timeout <= 0:
await process_thread_messages(thread_messages, message) timeout = None
return True logging.info(f"Assigning essay to student {student} with timeout {timeout} and topic {topic}")
return False if student not in STUDENT_IDS:
await ctx.respond("Invalid student ID.", ephemeral=True)
async def handle_mentions(message: Message) -> bool: return
""" assign_essay(student, topic)
Handles messages that mention the bot. await ctx.respond(f"Assigned essay to <@{student}>: {ASSIGNED_ESSAY[student]}")
if timeout and topic:
Args: timeout_until = discord.utils.utcnow() + timedelta(seconds=timeout)
message (Message): The new message. try:
member: discord.Member = await ctx.interaction.guild.fetch_member(student)
Returns: await member.timeout(until=timeout_until, reason=f"Assigned essay: {topic}")
bool: True if the message was handled, False otherwise. await ctx.respond(f'{member.mention} has been timed out for {timeout}.', ephemeral=True)
""" except discord.Forbidden:
if message.mentions: await ctx.respond(f'Failed to timeout {member.mention}. Missing permissions.', ephemeral=True)
for user in message.mentions: except discord.HTTPException as e:
if user.id == bot.user.id: await ctx.respond(f'Failed to timeout {member.mention}. {e.text}', ephemeral=True)
async with message.channel.typing(): return
if await has_essay(message):
return True @bot.slash_command(
thread_messages = await gen_message_list(message) name="rps",
await process_thread_messages(thread_messages, message) description="Play \"Rock, Paper, Scissors, Essay\" with Mr. Jacobs!!!",
return True )
return False async def rps_essay(
ctx: discord.ApplicationContext,
async def process_thread_messages(thread_messages: list, message: Message) -> None: choice: str = discord.Option(
""" description="Your selection for the game",
Processes a list of thread messages and sends a response. choices=[
discord.OptionChoice("Rock"),
Args: discord.OptionChoice("Paper"),
thread_messages (list): The list of thread messages. discord.OptionChoice("Scissors")
message (Message): The original message. ]
""" )
instructions = get_instructions(message) ) -> None:
response = await run(thread_messages, instructions, message.author.id) """
await message.reply(response) Play Rock Paper Scissors with Mr. Jacobs.
Args:
ctx: Application command context
choice: Your selection for the game
"""
logging.info(f"{ctx.author} chose {choice}")
outcomes = {"Rock": "Paper", "Paper": "Scissors", "Scissors": "Rock"}
await ctx.respond(
f"I choose {outcomes[choice]}, you owe me a {randint(1,15)} page essay "
f"on {assign_essay(ctx.author.id)}. MLA format, double spaced, "
"12pt Times New Roman!"
)
@bot.event
async def on_message(message: Message) -> None:
"""
Event listener for new messages. The bot will respond to messages that mention it or reply to its messages.
Bot disregards its own messages.
Args:
message (Message): The new message.
"""
logger.debug(f"New message received: {message.content}")
if message.author.id == bot.user.id or message.interaction_metadata:
return
if await handle_reply(message):
return
if await handle_mentions(message):
return
async def handle_reply(message: Message) -> bool:
"""
Handles replies to the bot's messages.
Args:
message (Message): The new message.
Returns:
bool: True if the message was handled, False otherwise.
"""
is_reply, first_reply = await msg_is_reply(message)
if is_reply and first_reply.author.id == bot.user.id:
async with message.channel.typing():
if await has_essay(message):
return True
reply_chain = await get_reply_chain(message)
thread_messages = await gen_message_list(reply_chain)
await process_thread_messages(thread_messages, message)
return True
return False
async def handle_mentions(message: Message) -> bool:
"""
Handles messages that mention the bot.
Args:
message (Message): The new message.
Returns:
bool: True if the message was handled, False otherwise.
"""
if message.mentions:
for user in message.mentions:
if user.id == bot.user.id:
async with message.channel.typing():
if await has_essay(message):
return True
thread_messages = await gen_message_list(message)
await process_thread_messages(thread_messages, message)
return True
return False
async def process_thread_messages(thread_messages: list, message: Message) -> None:
"""
Processes a list of thread messages and sends a response.
Args:
thread_messages (list): The list of thread messages.
message (Message): The original message.
"""
instructions = get_instructions(message)
response = await run(thread_messages, instructions, message.author.id)
await message.reply(response)

16
docker-compose.yml Normal file → Executable file
View File

@@ -1,9 +1,9 @@
services: services:
discord-bot: discord-bot:
container_name: jacobs-bot container_name: jacobs-bot
image: docker.foreverpyrite.com/jacobs-bot:beta image: docker.foreverpyrite.com/jacobs-bot:beta
volumes: volumes:
- ./logs/:/app/logs/ - ./logs/:/app/logs/
- ./data/:/app/data/ - ./data/:/app/data/
env_file: .env env_file: .env
restart: unless-stopped restart: unless-stopped

130
quotes.py Normal file → Executable file
View File

@@ -1,65 +1,65 @@
import random import random
import datetime import datetime
from students import STUDENT_IDS from students import STUDENT_IDS
QUOTES = { QUOTES = {
"NORMAL" : ( "NORMAL" : (
"Oh boy, we are about to wake up!", "Oh boy, we are about to wake up!",
"All right 👏 🫰", "All right 👏 🫰",
"How we doing, Paul?", "How we doing, Paul?",
"*Drops pen*", "*Drops pen*",
"How we doing, guys?", "How we doing, guys?",
"Killing it!!!", "Killing it!!!",
"*Bounces ball off wall*", "*Bounces ball off wall*",
"Ugggghhh...", "Ugggghhh...",
"Hmm... I see.", "Hmm... I see.",
"What are we doing over here?", "What are we doing over here?",
"Mmm... Okay! :clap:", "Mmm... Okay! :clap:",
"*LOUDLY* We don't like stupid prizes, now do we? Then, don't play stupid games!", "*LOUDLY* We don't like stupid prizes, now do we? Then, don't play stupid games!",
"You guys are killing it!", "You guys are killing it!",
"Do we need to go back over the module again?", "Do we need to go back over the module again?",
"Let's get it done!", "Let's get it done!",
"Sorry, I can't hear over Devan and Connor talking.", "Sorry, I can't hear over Devan and Connor talking.",
"That's what I like to hear!", "That's what I like to hear!",
"*Paces*", "*Paces*",
"*Sprints to the ringing phone*", "*Sprints to the ringing phone*",
"*Spins lanyard*", "*Spins lanyard*",
"Come on guys, you should know this already!", "Come on guys, you should know this already!",
"Let me just say this...", "Let me just say this...",
"Not trying to be mean or anything...but..." "Not trying to be mean or anything...but..."
), ),
"RARE" : ( "RARE" : (
"Play stupid games, win big prizes! 🤑🤑", "Play stupid games, win big prizes! 🤑🤑",
"Oooooo raahahah!", "Oooooo raahahah!",
"It's cherry-pickin' time, y'all!", "It's cherry-pickin' time, y'all!",
"What does the fox say?" "What does the fox say?"
), ),
"MYTHIC" : ( "MYTHIC" : (
"I'm proud of you.", "I'm proud of you.",
"You can take a 5-minute break.", "You can take a 5-minute break.",
"I have somewhere to be at 9:30, so you guys will have a sub." "I have somewhere to be at 9:30, so you guys will have a sub."
), ),
"LEGENDARY": ( "LEGENDARY": (
) )
} }
def select_quote() -> str: def select_quote() -> str:
rarity = random.randint(0, 99) rarity = random.randint(0, 99)
if rarity < 1: if rarity < 1:
quote = random.choice(QUOTES.get("MYTHIC")) quote = random.choice(QUOTES.get("MYTHIC"))
elif rarity < 15: elif rarity < 15:
quote = random.choice(QUOTES.get("RARE")) quote = random.choice(QUOTES.get("RARE"))
else: else:
quote = random.choice(QUOTES.get("NORMAL")) quote = random.choice(QUOTES.get("NORMAL"))
# Append log to a file (quotes.log) # Append log to a file (quotes.log)
with open("./logs/quotes.log", "at+") as log_file: with open("./logs/quotes.log", "at+") as log_file:
log_file.write(f"At {datetime.datetime.now()}, rarity was rolled as {rarity} and selected: {quote}\n") log_file.write(f"At {datetime.datetime.now()}, rarity was rolled as {rarity} and selected: {quote}\n")
return quote return quote
def select_student() -> int: def select_student() -> int:
student = random.choice(STUDENT_IDS) student = random.choice(STUDENT_IDS)
with open("./logs/quotes.log", "at+") as log_file: with open("./logs/quotes.log", "at+") as log_file:
log_file.write(f"At {datetime.datetime.now()}, wanted to see {student} after class.\n") log_file.write(f"At {datetime.datetime.now()}, wanted to see {student} after class.\n")
return student return student

0
requirements.txt Normal file → Executable file
View File