Made player controls based on Discord actions.

This commit is contained in:
Jared Kick 2025-07-24 14:30:04 -04:00
parent 3a17d001e1
commit 669339f717
5 changed files with 735 additions and 380 deletions

79
__main__.py Executable file
View File

@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""
BoywifeBot - A Discord bot for the Gayming Group Discord server.
This program provides a bot that plays music in a voice chat and fulfills other
commands in text channels.
Author: Jared Kick <jaredkick@gmail.com>
Version: 0.1.0
For detailed documentation, please refer to:
<url>
Source Code:
https://github.com/jtkick/boywife-bot
"""
PROJECT_VERSION = "0.1.0"
# Standard imports
import logging
import os
import sys
# Third-part imports
import discord
from discord.ext import commands
from dotenv import load_dotenv
from openai import OpenAI
# Project imports
import database
def main():
# Create custom logging handler
console_handler = logging.StreamHandler(sys.stdout)
console_formatter = logging.Formatter(
"[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s")
console_handler.setFormatter(console_formatter)
# Make sure all loggers use this handler
root_logger = logging.getLogger()
root_logger.setLevel(logging.INFO)
root_logger.addHandler(console_handler)
# Get bot logger
logger = logging.getLogger("boywife-bot")
# Load credentials
load_dotenv()
TOKEN = os.getenv('DISCORD_TOKEN')
# Create custom bot with database connection
class BoywifeBot(commands.Bot):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.db = database.Database("boywife_bot.db")
self.ai = OpenAI()
client = BoywifeBot(
command_prefix = '!',
intents=discord.Intents.all(),
log_hander=False
)
# Load all bot cogs in directory
# You need to import os for this method
@client.event
async def on_ready():
logger.info("%s is now running", client.user)
# Load cogs
for filename in os.listdir('./cogs'):
if filename.endswith('.py'):
await client.load_extension(f'cogs.{filename[:-3]}')
logger.info("Loaded %s cog", filename)
client.run(TOKEN, log_handler=None)
if __name__ == "__main__":
main()

View File

@ -1,45 +0,0 @@
#!/usr/bin/env python3
import asyncio
import discord
from discord.ext import commands
from dotenv import load_dotenv
import logging
import os
import sys
import database
# Create custom logging handler
console_handler = logging.StreamHandler(sys.stdout)
console_formatter = logging.Formatter(
"[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s")
console_handler.setFormatter(console_formatter)
# Make sure all loggers use this handler
root_logger = logging.getLogger()
root_logger.setLevel(logging.INFO)
root_logger.addHandler(console_handler)
# Get bot logger
logger = logging.getLogger("boywife-bot")
# Load credentials
load_dotenv()
TOKEN = os.getenv('DISCORD_TOKEN')
# client = discord.Client()
client = commands.Bot(
command_prefix = '!', intents=discord.Intents.all(), log_hander=False)
# You need to import os for this method
@client.event
async def on_ready():
logger.info(f'{client.user} is now running')
# Load cogs
for filename in os.listdir('./cogs'):
if filename.endswith('.py'):
await client.load_extension(f'cogs.{filename[:-3]}')
logger.info(f'Loaded {filename} cog')
client.run(TOKEN, log_handler=None)

View File

@ -7,17 +7,14 @@ import pathlib
import sqlite3
import typing
import database
class Activities(commands.Cog):
"""A cog to track and gather statistics on user activities."""
"""Related commands."""
__slots__ = ("nerd", "nerds", "fword", "fwords")
def __init__(self, bot):
self.bot = bot
self.logger = logging.getLogger("activities")
self.db = database.Database("boywife_bot.db")
async def __local_check(self, ctx):
"""A local check which applies to all commands in this cog."""
@ -29,7 +26,8 @@ class Activities(commands.Cog):
"""A local error handler for all errors arising from commands in this cog."""
if isinstance(error, commands.NoPrivateMessage):
try:
return await ctx.send('This command can not be used in Private Messages.')
return await ctx.send(
'This command can not be used in private messages.')
except discord.HTTPException:
pass
@ -42,15 +40,30 @@ class Activities(commands.Cog):
before: discord.Member,
after: discord.Member):
# Log the activity or status change
self.logger.info("Received activity change; logging")
self.db.insert_activity_change(before, after)
if after.activity:
self.logger.info(
f"User '{before.name}' changed activity to "\
f"'{after.activity.name}'")
else:
self.logger.info(
f"User '{before.name}' changed status to '{after.status}'")
self.bot.db.insert_activity_change(before, after)
@commands.command(name='nerd', aliases=['nerdscale'],
description="Find how nerdy a user is.")
async def nerd_(self, ctx, member: typing.Union[discord.Member, int, None]):
"""Checks the play history of all users in the Discord server, and
based on how many hours they have in League of Legends compared to
other users, declare how nerdy they are."""
"""Clowns on users who play League of Legends.
This command receives a user, computes the amount of time they have
spent playing League of Legends, and will make fun of them if they have
any time in it at all. It optionally takes no argument, and will find
the user in the guild with the most time in League of Legends and call
them out.
Args:
member (discord.Member, int, None, optional): The member to check
for League stats.
"""
# If member is not defined, find the user with the most time
if not member:
members = [member.id for member in ctx.guild.members]

File diff suppressed because it is too large Load Diff

View File

@ -3,82 +3,13 @@ import discord
import sqlite3
import typing
from tqdm import tqdm
from cogs import music_player
class Database:
def __init__(self, path: str = "boywife_bot.db"):
def __init__(self, path: str):
self.path = path
self._ensure_db()
# # TEMP: THIS IS FOR MIGRATING THE PREVIOUS DATABASE SCHEMA
# with sqlite3.connect("ingest.db") as conn:
# cursor = conn.cursor()
# cursor.execute("""
# SELECT
# user_id,
# before_activity_type,
# before_activity_name,
# after_activity_type,
# after_activity_name,
# timestamp
# FROM
# user_activity
# """)
# activities = cursor.fetchall()
# for activity in tqdm(activities):
# # Convert activity type
# if activity[1] == "activity":
# new_before_type = "playing"
# elif activity[1] == "spotify":
# new_before_type = "listening"
# elif activity[1] == "game":
# new_before_type = "playing"
# else:
# new_before_type = None
# if activity[3] == "activity":
# new_after_type = "playing"
# elif activity[3] == "spotify":
# new_after_type = "listening"
# elif activity[3] == "game":
# new_after_type = "playing"
# else:
# new_after_type = None
# new_before_name = None if activity[2] == "other" else activity[2]
# new_after_name = None if activity[4] == "other" else activity[4]
# user_id = self._insert_user(activity[0])
# with sqlite3.connect(self.path) as conn:
# cursor = conn.cursor()
# cursor.execute("""
# INSERT INTO activity_change (
# user_id,
# before_activity_type,
# before_activity_name,
# before_activity_status,
# after_activity_type,
# after_activity_name,
# after_activity_status,
# timestamp
# ) VALUES (
# ?, ?, ?, ?, ?, ?, ?, ?
# )
# """, (
# user_id,
# new_before_type,
# new_before_name,
# "unknown",
# new_after_type,
# new_after_name,
# "unknown",
# datetime.datetime.fromisoformat(activity[5])
# ))
def _ensure_db(self):
with sqlite3.connect(self.path) as conn:
@ -134,22 +65,80 @@ class Database:
)
""")
# # TEMP
# conn.execute("""
# ALTER TABLE song_play ADD COLUMN finished BOOL;
# """)
# Table for songs that actually get played
conn.execute("""
CREATE TABLE IF NOT EXISTS song_play (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
user_id INTEGER,
channel_id INTEGER NOT NULL,
search_term TEXT NOT NULL,
song_title TEXT,
song_artist TEXT,
finished BOOL DEFAULT 0,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
)
""")
# # ############ TEMP ###############
# conn.execute("""DROP TABLE IF EXISTS song_play_old;""")
# conn.execute("""
# ALTER TABLE
# song_play
# RENAME TO
# song_play_old;
# """)
# conn.execute("""
# CREATE TABLE song_play (
# id INTEGER PRIMARY KEY,
# user_id INTEGER,
# channel_id INTEGER NOT NULL,
# search_term TEXT NOT NULL,
# song_title TEXT,
# song_artist TEXT,
# finished BOOLEAN DEFAULT 0,
# timestamp DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
# );
# """)
# conn.execute("""
# INSERT INTO song_play (
# id,
# user_id,
# channel_id,
# search_term,
# song_title,
# song_artist,
# timestamp
# ) SELECT
# id,
# user_id,
# channel_id,
# search_term,
# song_title,
# song_artist,
# timestamp
# FROM song_play_old;
# """)
# conn.execute("""
# DROP TABLE song_play_old;
# """)
# # ##################################
conn.commit()
def _insert_server(self, discord_id: int = None) -> int:
"""Inserts Discord server ID into the 'server' table.
"""
Inserts Discord server ID into the 'server' table.
This method takes an ID for a server used in Discord, and inserts it
into the database. It ignores the case where the server ID is already
@ -187,7 +176,8 @@ class Database:
return row_id
def _insert_channel(self, discord_id: int = None) -> int:
"""Inserts Discord channel ID into the 'channel' table.
"""
Inserts Discord channel ID into the 'channel' table.
This method takes an ID for a channel used in Discord, and inserts it
into the database. It ignores the case where the channel ID is already
@ -225,7 +215,8 @@ class Database:
return row_id
def _insert_user(self, discord_id: int = None) -> int:
"""Inserts Discord user ID into the 'user' table.
"""
Inserts Discord user ID into the 'user' table.
This method takes an ID for a user used in Discord, and inserts it
into the database. It ignores the case where the user ID is already
@ -266,7 +257,8 @@ class Database:
self,
before: discord.Member,
after: discord.Member):
"""Inserts an activity change into the database.
"""
Inserts an activity change into the database.
This method takes two discord.Memeber objects, and records the change
in activity into the 'activity_change' table.
@ -326,7 +318,8 @@ class Database:
self,
message: discord.Message,
source: music_player.YTDLSource):
"""Inserts a song request into the database.
"""
Inserts a song request into the database.
This method takes a message and its derived music source and inserts
the relevant information into the 'song_request' table.
@ -359,7 +352,8 @@ class Database:
self,
channel_id: int,
source: music_player.YTDLSource):
"""Inserts a song play into the database.
"""
Inserts a song play into the database.
This method takes a channel and the song being played and inserts the
relevant information into the 'song_play' table.
@ -367,12 +361,16 @@ class Database:
Args:
channel (int): The Discord channel the song is being played in.
source (music_player.YTDLSource): The audio source.
Returns:
int: The row ID of the entered song. Used to update 'played' value.
"""
user_id = self._insert_user(source.requester.id)
channel_id = self._insert_user(channel.id)
user_id = self._insert_user(source.requester.id) if source.requester else None
channel_id = self._insert_user(channel_id)
# Insert the information
with sqlite3.connect(self.path) as conn:
conn.execute("""
cur = conn.cursor()
cur.execute("""
INSERT INTO song_play (
user_id,
channel_id,
@ -389,13 +387,38 @@ class Database:
source.song_title,
source.artist
))
return cur.lastrowid
def update_song_play(self, song_play_id: int, finished: bool):
"""
Updates a song_play entry on whether or not it was finished.
When a song plays, we want to know if it was finished or not. This
implies that either a user didn't want to hear it anymore, or that the
bot chose the wrong song from the search term.
Args:
song_play_id (int): The row ID within the database for the song
play.
finished (bool): Whether or not the song was completed.
"""
with sqlite3.connect(self.path) as conn:
conn.execute("""
UPDATE
song_play
SET
finished = ?
WHERE
id = ?
""", (finished, song_play_id))
def get_activity_stats(
self,
member: typing.Union[discord.Member, int],
start: datetime = datetime.now() - timedelta(days=30)
) -> dict[str, timedelta]:
"""Gets stats on the activities of the given member.
"""
Gets stats on the activities of the given member.
This method searches the database for activity changes by the given
user and computes the amount of time spent in each activity.