Made player controls based on Discord actions.
This commit is contained in:
parent
3a17d001e1
commit
669339f717
79
__main__.py
Executable file
79
__main__.py
Executable 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()
|
||||||
@ -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)
|
|
||||||
@ -7,17 +7,14 @@ import pathlib
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
import database
|
|
||||||
|
|
||||||
class Activities(commands.Cog):
|
class Activities(commands.Cog):
|
||||||
|
"""A cog to track and gather statistics on user activities."""
|
||||||
|
|
||||||
"""Related commands."""
|
|
||||||
__slots__ = ("nerd", "nerds", "fword", "fwords")
|
__slots__ = ("nerd", "nerds", "fword", "fwords")
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.logger = logging.getLogger("activities")
|
self.logger = logging.getLogger("activities")
|
||||||
self.db = database.Database("boywife_bot.db")
|
|
||||||
|
|
||||||
async def __local_check(self, ctx):
|
async def __local_check(self, ctx):
|
||||||
"""A local check which applies to all commands in this cog."""
|
"""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."""
|
"""A local error handler for all errors arising from commands in this cog."""
|
||||||
if isinstance(error, commands.NoPrivateMessage):
|
if isinstance(error, commands.NoPrivateMessage):
|
||||||
try:
|
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:
|
except discord.HTTPException:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -42,15 +40,30 @@ class Activities(commands.Cog):
|
|||||||
before: discord.Member,
|
before: discord.Member,
|
||||||
after: discord.Member):
|
after: discord.Member):
|
||||||
# Log the activity or status change
|
# Log the activity or status change
|
||||||
self.logger.info("Received activity change; logging")
|
if after.activity:
|
||||||
self.db.insert_activity_change(before, after)
|
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'],
|
@commands.command(name='nerd', aliases=['nerdscale'],
|
||||||
description="Find how nerdy a user is.")
|
description="Find how nerdy a user is.")
|
||||||
async def nerd_(self, ctx, member: typing.Union[discord.Member, int, None]):
|
async def nerd_(self, ctx, member: typing.Union[discord.Member, int, None]):
|
||||||
"""Checks the play history of all users in the Discord server, and
|
"""Clowns on users who play League of Legends.
|
||||||
based on how many hours they have in League of Legends compared to
|
|
||||||
other users, declare how nerdy they are."""
|
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 member is not defined, find the user with the most time
|
||||||
if not member:
|
if not member:
|
||||||
members = [member.id for member in ctx.guild.members]
|
members = [member.id for member in ctx.guild.members]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
185
database.py
185
database.py
@ -3,82 +3,13 @@ import discord
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from tqdm import tqdm
|
|
||||||
|
|
||||||
from cogs import music_player
|
from cogs import music_player
|
||||||
|
|
||||||
class Database:
|
class Database:
|
||||||
def __init__(self, path: str = "boywife_bot.db"):
|
def __init__(self, path: str):
|
||||||
self.path = path
|
self.path = path
|
||||||
self._ensure_db()
|
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):
|
def _ensure_db(self):
|
||||||
with sqlite3.connect(self.path) as conn:
|
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
|
# Table for songs that actually get played
|
||||||
conn.execute("""
|
conn.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS song_play (
|
CREATE TABLE IF NOT EXISTS song_play (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
user_id INTEGER NOT NULL,
|
user_id INTEGER,
|
||||||
channel_id INTEGER NOT NULL,
|
channel_id INTEGER NOT NULL,
|
||||||
search_term TEXT NOT NULL,
|
search_term TEXT NOT NULL,
|
||||||
song_title TEXT,
|
song_title TEXT,
|
||||||
song_artist TEXT,
|
song_artist TEXT,
|
||||||
|
finished BOOL DEFAULT 0,
|
||||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
|
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()
|
conn.commit()
|
||||||
|
|
||||||
def _insert_server(self, discord_id: int = None) -> int:
|
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
|
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
|
into the database. It ignores the case where the server ID is already
|
||||||
@ -187,7 +176,8 @@ class Database:
|
|||||||
return row_id
|
return row_id
|
||||||
|
|
||||||
def _insert_channel(self, discord_id: int = None) -> int:
|
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
|
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
|
into the database. It ignores the case where the channel ID is already
|
||||||
@ -225,7 +215,8 @@ class Database:
|
|||||||
return row_id
|
return row_id
|
||||||
|
|
||||||
def _insert_user(self, discord_id: int = None) -> int:
|
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
|
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
|
into the database. It ignores the case where the user ID is already
|
||||||
@ -266,7 +257,8 @@ class Database:
|
|||||||
self,
|
self,
|
||||||
before: discord.Member,
|
before: discord.Member,
|
||||||
after: 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
|
This method takes two discord.Memeber objects, and records the change
|
||||||
in activity into the 'activity_change' table.
|
in activity into the 'activity_change' table.
|
||||||
@ -326,7 +318,8 @@ class Database:
|
|||||||
self,
|
self,
|
||||||
message: discord.Message,
|
message: discord.Message,
|
||||||
source: music_player.YTDLSource):
|
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
|
This method takes a message and its derived music source and inserts
|
||||||
the relevant information into the 'song_request' table.
|
the relevant information into the 'song_request' table.
|
||||||
@ -359,7 +352,8 @@ class Database:
|
|||||||
self,
|
self,
|
||||||
channel_id: int,
|
channel_id: int,
|
||||||
source: music_player.YTDLSource):
|
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
|
This method takes a channel and the song being played and inserts the
|
||||||
relevant information into the 'song_play' table.
|
relevant information into the 'song_play' table.
|
||||||
@ -367,12 +361,16 @@ class Database:
|
|||||||
Args:
|
Args:
|
||||||
channel (int): The Discord channel the song is being played in.
|
channel (int): The Discord channel the song is being played in.
|
||||||
source (music_player.YTDLSource): The audio source.
|
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)
|
user_id = self._insert_user(source.requester.id) if source.requester else None
|
||||||
channel_id = self._insert_user(channel.id)
|
channel_id = self._insert_user(channel_id)
|
||||||
# Insert the information
|
# Insert the information
|
||||||
with sqlite3.connect(self.path) as conn:
|
with sqlite3.connect(self.path) as conn:
|
||||||
conn.execute("""
|
cur = conn.cursor()
|
||||||
|
cur.execute("""
|
||||||
INSERT INTO song_play (
|
INSERT INTO song_play (
|
||||||
user_id,
|
user_id,
|
||||||
channel_id,
|
channel_id,
|
||||||
@ -389,13 +387,38 @@ class Database:
|
|||||||
source.song_title,
|
source.song_title,
|
||||||
source.artist
|
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(
|
def get_activity_stats(
|
||||||
self,
|
self,
|
||||||
member: typing.Union[discord.Member, int],
|
member: typing.Union[discord.Member, int],
|
||||||
start: datetime = datetime.now() - timedelta(days=30)
|
start: datetime = datetime.now() - timedelta(days=30)
|
||||||
) -> dict[str, timedelta]:
|
) -> 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
|
This method searches the database for activity changes by the given
|
||||||
user and computes the amount of time spent in each activity.
|
user and computes the amount of time spent in each activity.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user