diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 64f4b569..bd71fdcd 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,5 +1,5 @@ --- -name: Bug report +name: Bug reports about: Create a report to help us improve title: "[BUG] " labels: bug diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 661ebb3b..eecf74a2 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -8,7 +8,7 @@ assignees: refekt --- **Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] +A clear and concise description of what the issue is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. diff --git a/.gitignore b/.gitignore index 780f32bf..6beb889f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,15 @@ +*.json +*.key +*.log *.pyc *.xml .gitignore .idea +/changelog.md /discord_surveys/ +/husk_messages.txt __pycache__ discord_surveys/__init__.py discord_surveys/survey.py -resources/key.key -resources/mammals.json -resources/variables.json -survey_chart.jpg -resources/images -resources/images/make_slowking.png -/changelog.md -/husk_messages.txt -/HB Temp Files/ -*.key -*.json +resources/* +survey_chart.jpg \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 00000000..07920112 --- /dev/null +++ b/__init__.py @@ -0,0 +1,101 @@ +import logging +import sys +from time import perf_counter + +import discord # noqa # Beta version thing +from discord.app_commands import CommandInvokeError + +from objects.Client import HuskerClient + +logging.basicConfig( + format="[%(asctime)s] %(levelname)s :: %(name)s :: %(module)s :: func/%(funcName)s :: Ln/%(lineno)d :: %(message)s", + datefmt="%X %x", + level=logging.INFO, + encoding="utf-8", + stream=sys.stdout, +) +logger = logging.getLogger(__name__) + +start = perf_counter() + +logger.info("Loading helpers") + +# Helper Functions +from helpers.constants import * # noqa +from helpers.embed import * # noqa +from helpers.encryption import * # noqa +from helpers.fryer import * # noqa +from helpers.misc import * # noqa +from helpers.mysql import * # noqa +from helpers.slowking import * # noqa + +logger.info("Helpers laoded. Loading objects") + +# Objects/classes +from objects.Bets import * # noqa +from objects.Exceptions import * # noqa +from objects.Karma import * # noqa +from objects.Paginator import * # noqa +from objects.Prediction import * # noqa +from objects.Recruits import * # noqa +from objects.Schedule import * # noqa +from objects.Thread import * # noqa +from objects.TweepyStreamListener import * # noqa +from objects.Weather import * # noqa +from objects.Winsipedia import * # noqa + +# Start the bot +logger.info("Objects loaded. Starting the bot!") + +intents = discord.Intents.all() +intents.typing = False +intents.presences = False + +client = HuskerClient( + command_prefix="$", + fetch_offline_members=True, + intents=intents, + owner_id=MEMBER_GEE, +) + +tree = client.tree + + +@tree.error +async def on_app_command_error( + interaction: discord.Interaction, error: CommandInvokeError +) -> None: + logger.exception(str(error.original.args[0]), exc_info=True) + embed = buildEmbed( + title="Command Error Received", + fields=[ + dict( + name=f"Error Type: {type(error.original)}", + value=str(error.original.args[0]), + ), + dict( + name="Input", + value=f"This error originated from '{error.command.qualified_name}'{' with the following data passed: ' + str(interaction.data['options']) if interaction.data.get('options', False) else ''}", + ), + ], + ) + if interaction.response.is_done(): + await interaction.followup.send(content="", embed=embed) + else: + await interaction.response.send_message(content="", embed=embed, ephemeral=True) + + +end = perf_counter() +logger.info(f"The bot initialized in {end - start:,.2f} seconds") + +__all__ = ["client"] + + +# v2.0 loop +async def main() -> None: + async with client: + await client.start(PROD_TOKEN) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/__version__.py b/__version__.py new file mode 100644 index 00000000..a527e703 --- /dev/null +++ b/__version__.py @@ -0,0 +1,2 @@ +_version = "3.5.0.b" +_author = "u/refekt" diff --git a/commands/admin.py b/commands/admin.py index 4bfd3083..e0075b3b 100644 --- a/commands/admin.py +++ b/commands/admin.py @@ -1,1201 +1,690 @@ -import asyncio +import logging import pathlib import platform +import socket from datetime import datetime, timedelta +from enum import Enum +from typing import Any, Union -import discord -import nest_asyncio +import discord.ext.commands import paramiko -from dinteractions_Paginator import Paginator +from discord import app_commands, Forbidden, HTTPException from discord.ext import commands -from discord_slash import ButtonStyle, cog_ext -from discord_slash.context import SlashContext, ComponentContext -from discord_slash.model import SlashCommandPermissionType -from discord_slash.utils.manage_commands import ( - create_option, - create_permission, +from paramiko.ssh_exception import ( + AuthenticationException, + BadHostKeyException, + SSHException, ) -from discord_slash.utils.manage_components import ( - create_actionrow, - create_button, - create_select, - create_select_option, - spread_to_rows, -) -from utilities.constants import pretty_time_delta -from objects.Thread import end_timeout -from utilities.constants import ( +from __version__ import _version +from helpers.constants import ( BOT_FOOTER_SECRET, CAT_GAMEDAY, CAT_GENERAL, - CHAN_BANNED, + CHAN_ADMIN, CHAN_DISCUSSION_LIVE, CHAN_DISCUSSION_STREAMING, CHAN_GENERAL, CHAN_HYPE_GROUP, CHAN_IOWA, CHAN_RECRUITING, - CHAN_WAR_ROOM, - CommandError, - ROLE_ADMIN_PROD, - ROLE_AIRPOD, - ROLE_ALDIS, - ROLE_ASPARAGUS, + DISCORD_USER_TYPES, + GUILD_PROD, ROLE_EVERYONE_PROD, - ROLE_HYPE_MAX, - ROLE_HYPE_NO, - ROLE_HYPE_SOME, - ROLE_ISMS, - ROLE_MEME, - ROLE_MOD_PROD, - ROLE_PACKER, - ROLE_PIXEL, - ROLE_POTATO, - ROLE_QDOBA, - ROLE_RUNZA, - ROLE_TARMAC, ROLE_TIME_OUT, SSH_HOST, SSH_PASSWORD, SSH_USERNAME, - UserError, - guild_id_list, ) -from utilities.embed import build_embed as build_embed -from utilities.mysql import Process_MySQL, sqlInsertIowa, sqlRemoveIowa, sqlRetrieveIowa +from helpers.embed import buildEmbed +from helpers.misc import discordURLFormatter +from helpers.mysql import processMySQL, sqlInsertIowa, sqlRetrieveIowa, sqlRemoveIowa +from objects.Exceptions import CommandException, UserInputException + +logger = logging.getLogger(__name__) + + +class ConfirmButtons(discord.ui.View): + def __init__(self): + super().__init__() + self.value = None + + @discord.ui.button(label="Confirm", style=discord.ButtonStyle.green) + async def confirm( + self, interaction: discord.Interaction, button: discord.ui.Button + ) -> None: + self.value = True + self.stop() + + @discord.ui.button(label="Cancel", style=discord.ButtonStyle.grey) + async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button): + self.value = False + self.stop() + + +class AdminCog(commands.Cog, name="Admin Commands"): + class MammaleChannels(Enum): + general = 1 + recruiting = 2 + admin = 3 + + group_purge = app_commands.Group( + name="purge", + description="Purge messages from channel", + default_permissions=discord.Permissions(manage_messages=True), + guild_ids=[GUILD_PROD], + ) + group_submit = app_commands.Group( + name="submit", + description="Sbumit a bug or feature request for the bot", + guild_ids=[GUILD_PROD], + ) + group_gameday = app_commands.Group( + name="gameday", + description="Turn game day mode on or off", + default_permissions=discord.Permissions(manage_messages=True), + guild_ids=[GUILD_PROD], + ) + # noinspection PyMethodMayBeStatic + async def alert_gameday_channels( + self, client: Union[discord.ext.commands.Bot, discord.Client], on: bool + ) -> None: + chan_general = await client.fetch_channel(CHAN_GENERAL) + chan_live = await client.fetch_channel(CHAN_DISCUSSION_LIVE) + chan_streaming = await client.fetch_channel(CHAN_DISCUSSION_STREAMING) -def log(level: int, message: str): - import datetime + if on: + embed = buildEmbed( + title="Game Day Mode", + description="Game day mode is now on!", + fields=[ + dict( + name="Live TV", + value=f"{chan_live.mention} text and voice channels are for users who are watching live.", + ), + dict( + name="Streaming", + value=f"{chan_streaming.mention} text and voice channels are for users who are streaming the game.", + ), + dict( + name="Info", + value="All channels in the Huskers category will be turned off until the game day mode is disabled.", + ), + ], + ) + else: + embed = buildEmbed( + title="Game Day Mode", + description="Game day mode is now off!", + fields=[ + dict( + name="Info", + value=f"Game day channels have been disabled and General categories channels have been enabled. Regular discussion may continue in {chan_general.mention}.", + ) + ], + ) - if level == 0: - print(f"[{datetime.datetime.now()}] ### Admin: {message}") - elif level == 1: - print(f"[{datetime.datetime.now()}] ### ~~~ Admin: {message}") + await chan_general.send(embed=embed) + await chan_live.send(embed=embed) + await chan_streaming.send(embed=embed) + # noinspection PyMethodMayBeStatic + async def process_gameday(self, mode: bool, guild: discord.Guild) -> None: + gameday_category = guild.get_channel(CAT_GAMEDAY) + general_category = guild.get_channel(CAT_GENERAL) + everyone = guild.get_role(ROLE_EVERYONE_PROD) -console_buttons = [ - create_button(style=ButtonStyle.primary, label="SMMS", emoji="๐Ÿฆ", custom_id="SMMS") -] + logger.info(f"Creating permissions to be [{mode}]") -console_chan_select = create_select( - options=[ - create_select_option(label="General", value="SMMS_general"), - create_select_option(label="Recruiting", value="SMMS_recruiting"), - create_select_option(label="War Room", value="SMMS_war"), - ], - custom_id="SMMS_select", - min_values=1, - max_values=1, - placeholder="What channel do you want to send to?", -) + perms_text = discord.PermissionOverwrite() -buttons_roles_hype = [ - create_button( - style=ButtonStyle.gray, label="Max", custom_id="role_hype_max", emoji="๐Ÿ“ˆ" - ), - create_button( - style=ButtonStyle.gray, label="Some", custom_id="role_hype_some", emoji="โš " - ), - create_button( - style=ButtonStyle.gray, label="No", custom_id="role_hype_no", emoji="โ›”" - ), - create_button( - style=ButtonStyle.gray, label="Tarmac", custom_id="role_hype_tarmac", emoji="๐Ÿ›ซ" - ), - create_button( - style=ButtonStyle.gray, - label="Remove Hype Roles", - custom_id="role_hype_none", - emoji="๐Ÿ•ณ", - ), -] - -buttons_roles_food = [ - create_button( - style=ButtonStyle.gray, - label="Potato Gang", - custom_id="role_food_potato", - emoji="๐Ÿฅ”", - ), - create_button( - style=ButtonStyle.gray, - label="Asparagang", - custom_id="role_food_asparagang", - emoji="๐Ÿ’š", - ), - create_button( - style=ButtonStyle.gray, - label="Runza", - custom_id="role_food_runza", - emoji="๐Ÿฅช", - ), - create_button( - style=ButtonStyle.gray, - label="Qdoba's Witness", - custom_id="role_food_qdoba", - emoji="๐ŸŒฏ", - ), - create_button( - style=ButtonStyle.gray, - label="Aldi's Nuts", - custom_id="role_food_aldi", - emoji="๐Ÿฅœ", - ), - create_button( - style=ButtonStyle.gray, - label="Remove Food Roles", - custom_id="roles_food_remove", - emoji="๐Ÿ•ณ", - ), -] - -buttons_roles_culture = [ - create_button( - style=ButtonStyle.gray, - label="Meme Team", - custom_id="role_culture_meme", - emoji="๐Ÿ˜น", - ), - create_button( - style=ButtonStyle.gray, - label="He Man Isms Hater Club", - custom_id="role_culture_isms", - emoji="โ™ฃ", - ), - create_button( - style=ButtonStyle.gray, - label="Packer Backer", - custom_id="role_culture_packer", - emoji="๐Ÿง€", - ), - create_button( - style=ButtonStyle.gray, - label="Pixel Gang", - custom_id="role_culture_pixel", - emoji="๐Ÿ“ฑ", - ), - create_button( - style=ButtonStyle.gray, - label="Airpod Gang", - custom_id="role_culture_airpod", - emoji="๐ŸŽง", - ), - create_button( - style=ButtonStyle.gray, - label="Remove Culture Roles", - custom_id="roles_culture_remove", - emoji="๐Ÿ•ณ", - ), -] - - -# noinspection PyUnresolvedReferences -async def process_gameday(mode: bool, guild: discord.Guild): - gameday_category = guild.get_channel(CAT_GAMEDAY) - general_category = guild.get_channel(CAT_GENERAL) - everyone = guild.get_role(ROLE_EVERYONE_PROD) - - log(1, f"Creating permissions to be [{mode}]") - - perms_text = discord.PermissionOverwrite() - - perms_text.view_channel = mode # noqa - perms_text.send_messages = mode # noqa - perms_text.read_messages = mode # noqa - - perms_text_opposite = discord.PermissionOverwrite() - perms_text_opposite.send_messages = not mode # noqa - - perms_voice = discord.PermissionOverwrite() - perms_voice.view_channel = mode # noqa - perms_voice.connect = mode # noqa - perms_voice.speak = mode # noqa - - log(1, f"Permissions created") - - for channel in general_category.channels: - - if channel.id in CHAN_HYPE_GROUP: - continue - - # noinspection PyBroadException - try: - log(1, f"Attempting to changes permissions for [{channel}] to [{not mode}]") + perms_text.view_channel = mode # noqa + perms_text.send_messages = mode # noqa + perms_text.read_messages = mode # noqa - if channel.type == discord.ChannelType.text: - await channel.set_permissions(everyone, overwrite=perms_text_opposite) - log(1, f"Changed permissions for [{channel}] to [{not mode}]") - except: # noqa - log(1, f"Unable to change permissions for [{channel}] to [{not mode}]") - continue + perms_text_opposite = discord.PermissionOverwrite() + perms_text_opposite.send_messages = not mode # noqa - for channel in gameday_category.channels: - try: - log(1, f"Attempting to changes permissions for [{channel}] to [{mode}]") - - if channel.type == discord.ChannelType.text: - await channel.set_permissions(everyone, overwrite=perms_text) - elif channel.type == discord.ChannelType.voice: - await channel.set_permissions(everyone, overwrite=perms_voice) - - # TODO Trying to kick people from voice channels if game day mode is turned off. - # if not mode: - # for member in channel.members: - # try: - # await member.voice.kick() - # except: # noqa - # pass - else: - log(1, f"Unable to change permissions for [{channel}] to [{mode}]") - continue - log(1, f"Changed permissions for [{channel}] to [{mode}]") - except discord.errors.Forbidden: - raise CommandError("The bot does not have access to change permissions!") - except: # noqa - continue + perms_voice = discord.PermissionOverwrite() + perms_voice.view_channel = mode # noqa + perms_voice.connect = mode # noqa + perms_voice.speak = mode # noqa - log(0, f"All permissions changes applied") + logger.info(f"Permissions created") + for channel in general_category.channels: -async def process_nebraska(ctx, who: discord.Member): - assert who, UserError("You must include a user!") + if channel.id in CHAN_HYPE_GROUP: + continue - role_timeout = ctx.guild.get_role(ROLE_TIME_OUT) - await who.remove_roles(role_timeout) + try: + logger.info( + f"Attempting to changes permissions for [{channel}] to [{not mode}]" + ) + if channel.type == discord.ChannelType.text: + await channel.set_permissions( + everyone, overwrite=perms_text_opposite + ) + except: # noqa + logger.info( + f"Unable to change permissions for [{channel}] to [{not mode}]" + ) + continue - log(1, f"Removed [{role_timeout}] role") + logger.info(f"Changed permissions for [{channel}] to [{not mode}]") - previous_roles_raw = Process_MySQL( - query=sqlRetrieveIowa, values=who.id, fetch="all" - ) + for channel in gameday_category.channels: + try: + logger.info( + f"Attempting to changes permissions for [{channel}] to [{mode}]" + ) - if previous_roles_raw is not None: - previous_roles = previous_roles_raw[0]["previous_roles"].split(",") - log(1, f"Gathered all the roles to store") + if channel.type == discord.ChannelType.text: + await channel.set_permissions(everyone, overwrite=perms_text) + elif channel.type == discord.ChannelType.voice: + await channel.set_permissions(everyone, overwrite=perms_voice) - if previous_roles: - for role in previous_roles: - try: - new_role = ctx.guild.get_role(int(role)) - log(1, f"Attempting to add [{new_role}] role...") - await who.add_roles(new_role, reason="Returning from Iowa") - log(1, f"Added [{new_role}] role") - except ( - discord.Forbidden, - discord.HTTPException, - discord.ext.commands.MissingPermissions, - ) as e: - log(1, f"Unable to add role! {e}") + # TODO Trying to kick people from voice channels if game day mode is turned off. + else: + logger.info( + f"Unable to change permissions for [{channel}] to [{mode}]" + ) continue + logger.info(f"Changed permissions for [{channel}] to [{mode}]") + except discord.errors.Forbidden: + logger.exception( + "The bot does not have access to change permissions!", exc_info=True + ) + except: # noqa + continue - Process_MySQL(query=sqlRemoveIowa, values=who.id) - return True + logger.info(f"All permissions changes applied") + # noinspection PyMethodMayBeStatic + async def college_purge_messages( + self, channel: Any, all_messages: bool = False + ) -> list: + msgs = [] + max_age = datetime.now() - timedelta( + days=13, hours=23, minutes=59 + ) # Discord only lets you delete 14 day old messages -class AdminCommands(commands.Cog): - def __init__(self, bot: discord.Client): - self.bot = bot + try: + async for message in channel.history(limit=100): + if ( + message.created_at >= max_age.astimezone() and message.author.bot + if not all_messages + else True + ): + msgs.append(message) - @cog_ext.cog_slash( - name="about", description="All about Bot Frost!", guild_ids=guild_id_list() - ) - async def _about(self, ctx: SlashContext): + except discord.ClientException: + logger.exception( + "Cannot delete more than 100 messages at a time.", exc_info=True + ) + except discord.Forbidden: + logger.exception("Missing permissions.", exc_info=True) + except discord.HTTPException: + logger.exception( + "Deleting messages failed. Bulk messages possibly include messages over 14 days old.", + exc_info=True, + ) + + return msgs + + # noinspection PyMethodMayBeStatic + async def confirm_purge(self, interaction: discord.Interaction) -> bool: + + view = ConfirmButtons() + await interaction.response.send_message( + "Do you want to continue?", view=view, ephemeral=True + ) + await view.wait() + + if view.value is None: + logger.exception("Purge confirmation timed out!", exc_info=True) + elif view.value: + return True + else: + return False + + @app_commands.command(name="about", description="Learn all about Bot Frost") + @app_commands.guilds(GUILD_PROD) + async def about(self, interaction: discord.Interaction) -> None: """All about Bot Frost""" - await ctx.send( - embed=build_embed( + + await interaction.response.send_message( + # TODO Change this to use dict() + embed=buildEmbed( title="About Me", - inline=False, fields=[ - [ - "History", - "Bot Frost was created and developed by [/u/refekt](https://reddit.com/u/refekt) and [/u/psyspoop](https://reddit.com/u/psyspoop). Jeyrad and ModestBeaver assisted with the creation greatly!", - ], - [ - "Source Code", - "[GitHub](https://www.github.com/refekt/Husker-Bot)", - ], - [ - "Hosting Location", - f"{'Local Machine' if 'Windows' in platform.platform() else 'Virtual Private Server'}", - ], - ["Hosting Status", "https://status.hyperexpert.com/"], - ["Latency", f"{self.bot.latency * 1000:.2f} ms"], - ["Username", self.bot.user.mention], - ["Birthday", f"I was born on {self.bot.user.created_at}"], - [ - "Feeling generous?", - f"Check out `/donate` to help out the production and upkeep of the bot.", - ], + { + "name": "History", + "value": "Bot Frost was created and developed by [/u/refekt](https://reddit.com/u/refekt) and [/u/psyspoop](https://reddit.com/u/psyspoop). Jeyrad and ModestBeaver assisted with the creation greatly!", + }, + { + "name": "Source Code", + "value": discordURLFormatter( + "GitHub", "https://www.github.com/refekt/Husker-Bot" + ), + }, + {"name": "Version", "value": _version, "inline": False}, + { + "name": "Hosting Location", + "value": f"{'Local Machine' if 'Windows' in platform.platform() else 'Virtual Private Server'}", + }, + { + "name": "Hosting Status", + "value": "https://status.hyperexpert.com/", + }, + { + "name": "Latency", + "value": f"{interaction.client.latency * 1000:.2f} ms", + }, + { + "name": "Username", + "value": interaction.client.user.mention, + }, + { + "name": "Feeling generous?", + "value": f"Check out `/donate` to help out the production and upkeep of the bot.", + }, ], ) ) - @cog_ext.cog_slash( - name="quit", - description="Admin or mod only: Turn off the bot", - guild_ids=guild_id_list(), + @app_commands.command( + name="donate", description="Contribute to the development of Bot Frost" ) - @cog_ext.permission( - guild_id=guild_id_list()[0], - permissions=[ - create_permission(ROLE_ADMIN_PROD, SlashCommandPermissionType.ROLE, True), - create_permission(ROLE_MOD_PROD, SlashCommandPermissionType.ROLE, True), - create_permission( - ROLE_EVERYONE_PROD, SlashCommandPermissionType.ROLE, False - ), - ], - ) - async def _quit(self, ctx: SlashContext): - await ctx.send(f"Good bye world! ๐Ÿ˜ญ I was turned off by [{ctx.author}].") - await self.bot.logout() + @app_commands.guilds(GUILD_PROD) + async def donate(self, interaction: discord.Interaction) -> None: + """Contribute to the development of Bot Frost""" - @cog_ext.cog_slash( - name="donate", description="Donate to the cause!", guild_ids=guild_id_list() - ) - async def _donate(self, ctx: SlashContext): - """Donate to the cause""" - - await ctx.send( - embed=build_embed( + await interaction.response.send_message( + # TODO change this to use dict() + embed=buildEmbed( title="Donation Information", - inline=False, + thumbnail="https://i.imgur.com/53GeCvm.png", fields=[ - [ - "About", - "I hate asking for donations; however, the bot has grown to the point where official server hosting is required. Server hosting provides 99% uptime and hardware performance I cannot provide with my own hardware. I will be paying for upgraded hosting but donations will help offset any costs.", - ], - [ - "Terms", - "(1) Final discretion of donation usage is up to the creator(s). " + { + "name": "About", + "value": "I hate asking for donations; however, the bot has grown to the point where official server hosting is required. Server hosting provides 99% uptime and hardware performance I cannot provide with my own hardware. I will be paying for upgraded hosting but donations will help offset any costs.", + }, + { + "name": "Terms", + "value": "(1) Final discretion of donation usage is up to the creator(s). " "(2) Making a donation to the product(s) and/or service(s) does not garner any control or authority over product(s) or service(s). " "(3) No refunds. " "(4) Monthly subscriptions can be terminated by either party at any time. " "(5) These terms can be changed at any time. Please read before each donation. " "(6) Clicking the donation link signifies your agreement to these terms.", - ], - [ - "Donation Link", - "[Click Me](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=refekt%40gmail.com¤cy_code=USD&source=url)", - ], + }, + { + "name": "Donation Link", + "value": discordURLFormatter( + "click me", + "https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=refekt%40gmail.com¤cy_code=USD&source=url", + ), + }, ], - ), - hidden=True, + ) ) - @cog_ext.cog_subcommand( - base="purge", - base_description="Admin only: Delete messages", - name="everything", - description="Admin only: Deletes up to 100 of the previous messages", - guild_ids=guild_id_list(), - ) - @cog_ext.permission( - guild_id=guild_id_list()[0], - permissions=[ - create_permission(ROLE_ADMIN_PROD, SlashCommandPermissionType.ROLE, True), - create_permission(ROLE_MOD_PROD, SlashCommandPermissionType.ROLE, True), - create_permission( - ROLE_EVERYONE_PROD, SlashCommandPermissionType.ROLE, False - ), - ], + @app_commands.command( + name="commands", description="Lists all commands within the bot" ) - async def _everything(self, ctx: SlashContext): - if ctx.subcommand_passed is not None: - return - - if ctx.channel.id in CHAN_BANNED: - return - - await ctx.defer(hidden=True) - - try: - max_age = datetime.now() - timedelta( - days=13, hours=23, minutes=59 - ) # Discord only lets you delete 14 day old messages - deleted = await ctx.channel.purge(after=max_age, bulk=True) - log(0, f"Bulk delete of {len(deleted)} messages successful.") - except discord.ClientException: - log(1, f"Cannot delete more than 100 messages at a time.") - except discord.Forbidden: - log(1, f"Missing permissions.") - except discord.HTTPException: - log( - 1, - f"Deleting messages failed. Bulk messages possibly include messages over 14 days old.", + @app_commands.guilds(GUILD_PROD) + async def commands( + self, interaction: discord.Interaction + ) -> None: # TODO All of this apparently + """Lists all commands within the bot""" + embed_fields_commands = [ + dict( + name=cmd.name, + value=cmd.description if cmd.description else "TBD", ) + for cmd in interaction.client.commands + ] + embed = buildEmbed(title="Bot Commands", fields=embed_fields_commands) + await interaction.response.send_message(embed=embed) - await ctx.send(hidden=True, content="Done!") - - @cog_ext.cog_subcommand( - base="purge", - base_description="Admin only: Delete messages", - name="bot", - description="Admin only: Deletes previous bot messages", - guild_ids=guild_id_list(), - ) - @cog_ext.permission( - guild_id=guild_id_list()[0], - permissions=[ - create_permission(ROLE_ADMIN_PROD, SlashCommandPermissionType.ROLE, True), - create_permission( - ROLE_EVERYONE_PROD, SlashCommandPermissionType.ROLE, False - ), - ], + @group_purge.command( + name="bot", description="Purge the 100 most recent bot messages" ) - async def _bot(self, ctx: SlashContext): - if ctx.subcommand_passed is not None: - return + async def purge_bot(self, interaction: discord.Interaction) -> None: + assert type(interaction.channel) is discord.TextChannel, CommandException( + "Unable to run this command outside text channels." + ) - if ctx.channel.id in CHAN_BANNED: + if not await self.confirm_purge(interaction): + await interaction.edit_original_message(content="Purge declined", view=None) return - await ctx.defer(hidden=True) + await interaction.edit_original_message(content="Working...") - try: - - def is_bot(message: discord.Message): - return message.author.bot - - max_age = datetime.now() - timedelta( - days=13, hours=23, minutes=59 - ) # Discord only lets you delete 14 day old messages - deleted = await ctx.channel.purge(after=max_age, bulk=True, check=is_bot) - log(0, f"Bulk delete of {len(deleted)} messages successful.") - except discord.ClientException: - log(1, f"Cannot delete more than 100 messages at a time.") - except discord.Forbidden: - log(1, f"Missing permissions.") - except discord.HTTPException: - log( - 1, - f"Deleting messages failed. Bulk messages possibly include messages over 14 days old.", - ) - - await ctx.send(hidden=True, content="Done!") - - @cog_ext.cog_slash( - name="submit", - description="Report something on GitHub", - guild_ids=guild_id_list(), - ) - async def _submit(self, ctx: SlashContext): - pass - - @cog_ext.cog_subcommand( - base="submit", - name="bug", - description="Submit a bug report", - guild_ids=guild_id_list(), - ) - async def _submit_bug(self, ctx: SlashContext): - embed = build_embed( - title=f"Bug Reporter", - description="[Submit a bug report here](https://github.com/refekt/Bot-Frost/issues/new?assignees=refekt&labels=bug&template=bug_report.md&title=%5BBUG%5D+)", - author=None, - image=None, - thumbnail=None, + msgs = await self.college_purge_messages( + channel=interaction.channel, all_messages=False ) - await ctx.send(embed=embed) - @cog_ext.cog_subcommand( - base="submit", - name="feature", - description="Submit feature request", - guild_ids=guild_id_list(), - ) - async def _submit_feature(self, ctx: SlashContext): - embed = build_embed( - title=f"Feature Request", - description="[Submit a feature request here](https://github.com/refekt/Bot-Frost/issues/new?assignees=refekt&labels=request&template=feature_request.md&title=%5BREQUEST%5D+)", - author=None, - image=None, - thumbnail=None, + await interaction.channel.delete_messages(msgs) + await interaction.edit_original_message( + content=f"Bulk delete of {len(msgs)} messages successful.", view=None ) - await ctx.send(embed=embed) - - @cog_ext.cog_subcommand( - base="roles", - base_description="Manage your roles", - name="hype", - description="Assign hype roles", - guild_ids=guild_id_list(), - ) - async def _roles_hype(self, ctx: SlashContext): - log(1, f"Roles: Hype Squad") - - hype_action_row = create_actionrow(*buttons_roles_hype) + logger.info(f"Bulk delete of {len(msgs)} messages successful.") - embed = build_embed( - title="Which Nebraska hype squad do you belong to?", - description="Selecting a squad assigns you a role", - inline=False, - fields=[ - ["๐Ÿ“ˆ Max Hype", "Believe Nebraska will be great always"], - ["โš  Some Hype", "Little hype or uncertain of Nebraska's performance"], - ["โ›” No Hype", "Nebraska will not be good and I expect this"], - ["๐Ÿ›ซ Tarmac Gang", "FROST MUST BE LEFT ON THE TARMAC"], - ["๐Ÿ•ณ None", "Remove hype roles"], - ], + @group_purge.command(name="all", description="Purge the 100 most recent messages") + async def purge_all(self, interaction: discord.Interaction) -> None: + assert type(interaction.channel) is discord.TextChannel, CommandException( + "Unable to run this command outside text channels." ) - await ctx.send(embed=embed, components=[hype_action_row]) - - @cog_ext.cog_component(components=buttons_roles_hype) - async def process_roles_hype(self, ctx: ComponentContext): - await ctx.defer() - - log(0, f"Gathering roles") - - hype_max = ctx.guild.get_role(ROLE_HYPE_MAX) - hype_some = ctx.guild.get_role(ROLE_HYPE_SOME) - hype_no = ctx.guild.get_role(ROLE_HYPE_NO) - hype_tarmac = ctx.guild.get_role(ROLE_TARMAC) - - if any([hype_max, hype_some, hype_no]) is None: - raise CommandError("Unable to locate role!") - - if ctx.custom_id == "role_hype_max": - await ctx.author.add_roles(hype_max, reason="Hype squad") - await ctx.author.remove_roles( - hype_tarmac, hype_some, hype_no, reason="Hype squad" - ) - chosen_hype = hype_max.mention - elif ctx.custom_id == "role_hype_some": - await ctx.author.add_roles(hype_some, reason="Hype squad") - await ctx.author.remove_roles( - hype_tarmac, hype_max, hype_no, reason="Hype squad" - ) - chosen_hype = hype_some.mention - elif ctx.custom_id == "role_hype_no": - await ctx.author.add_roles(hype_no, reason="Hype squad") - await ctx.author.remove_roles( - hype_tarmac, hype_some, hype_max, reason="Hype squad" - ) - chosen_hype = hype_no.mention - elif ctx.custom_id == "role_hype_tarmac": - await ctx.author.add_roles(hype_tarmac, reason="Hype squad") - await ctx.author.remove_roles( - hype_no, hype_some, hype_max, reason="Hype squad" - ) - chosen_hype = hype_tarmac.mention - elif ctx.custom_id == "role_hype_none": - await ctx.author.remove_roles( - hype_tarmac, hype_no, hype_some, hype_max, reason="Hype squad" - ) - embed = build_embed( - description=f"{ctx.author.mention} has left all hype roles.", - image=None, - thumbnail=None, - author=None, - footer="This message will disappear after 10 seconds.", - ) - await ctx.send(embed=embed, delete_after=10) - return - else: + if not await self.confirm_purge(interaction): + await interaction.edit_original_message(content="Purge declined", view=None) return - embed = build_embed( - description=f"{ctx.author.mention} has joined: {chosen_hype}.", - image=None, - thumbnail=None, - author=None, - footer="This message will disappear after 10 seconds.", - ) - await ctx.send(embed=embed, delete_after=10) - - log(0, f"Roles: Hype Squad") + await interaction.edit_original_message(content="Working...") - @cog_ext.cog_subcommand( - base="roles", - base_description="Manage your roles", - name="food", - description="Assign food roles", - guild_ids=guild_id_list(), - ) - async def _roles_food(self, ctx: SlashContext): - log(0, f"Roles: Food") + msgs = await self.college_purge_messages( + channel=interaction.channel, all_messages=True + ) - embed = build_embed( - title="Which food roles do you want?", - description="Assign yourself a role", - inline=False, - fields=[ - ["๐Ÿฅ” Potato Gang", "Potatoes are better than asparagus"], - ["๐Ÿ’š Asparagang", "Asparagus are better than potatoes"], - ["๐Ÿฅช Runza", "r/unza"], - ["๐ŸŒฏ Qdoba's Witness", "Qdoba is better than Chipotle"], - ["๐Ÿฅœ Aldi's Nuts", "Aldi Super Fan"], - ["๐Ÿ•ณ None", "Remove food roles"], - ], + await interaction.channel.delete_messages(msgs) + await interaction.edit_original_message( + content=f"Bulk delete of {len(msgs)} messages successful.", view=None + ) + logger.info(f"Bulk delete of {len(msgs)} messages successful.") + + @app_commands.command(name="quit", description="Turn off Bot Frost") + @app_commands.default_permissions(manage_messages=True) + @app_commands.guilds(GUILD_PROD) + async def quit(self, interaction: discord.Interaction) -> None: + await interaction.response.send_message( + f"Goodbye for now! {interaction.user.mention} has turned me off!" + ) + await interaction.client.close() + logger.info( + f"User {interaction.user.name}#{interaction.user.discriminator} turned off the bot." ) - food_action_row = spread_to_rows(*buttons_roles_food, max_in_row=2) + @app_commands.command( + name="restart", description="Restart the bot (Linux host only)" + ) # TODO Test on Linux + @app_commands.guilds(GUILD_PROD) + @app_commands.default_permissions(manage_messages=True) + async def restart(self, interaction: discord.Interaction) -> None: + interaction.response.defer(ephemeral=True, thinking=True) - await ctx.send(embed=embed, components=food_action_row) + assert "Windows" not in platform.platform(), CommandException( + "Cannot run this command while hosted on Windows" + ) - @cog_ext.cog_component(components=buttons_roles_food) - async def process_roles_food(self, ctx: ComponentContext): - await ctx.defer() + logger.info("Restarting the bot via SSH") - log(0, f"Gathering roles") + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - food_potato = ctx.guild.get_role(ROLE_POTATO) - food_asparagus = ctx.guild.get_role(ROLE_ASPARAGUS) - food_runza = ctx.guild.get_role(ROLE_RUNZA) - food_qdoba = ctx.guild.get_role(ROLE_QDOBA) - food_aldi = ctx.guild.get_role(ROLE_ALDIS) + logger.info("SSH Client established") - if ( - any([food_potato, food_asparagus, food_runza, food_qdoba, food_aldi]) - is None - ): - raise CommandError("Unable to locate role!") - - if ctx.custom_id == "role_food_potato": - await ctx.author.add_roles(food_potato, reason="Hype squad") - chosen_food = food_potato.mention - elif ctx.custom_id == "role_food_asparagang": - await ctx.author.add_roles(food_asparagus, reason="Hype squad") - chosen_food = food_asparagus.mention - elif ctx.custom_id == "role_food_runza": - await ctx.author.add_roles(food_runza, reason="Hype squad") - chosen_food = food_runza.mention - elif ctx.custom_id == "role_food_qdoba": - await ctx.author.add_roles(food_qdoba, reason="Hype squad") - chosen_food = food_qdoba.mention - elif ctx.custom_id == "role_food_aldi": - await ctx.author.add_roles(food_aldi, reason="Hype squad") - chosen_food = food_aldi.mention - elif ctx.custom_id == "roles_food_remove": - await ctx.author.remove_roles( - food_potato, - food_asparagus, - food_runza, - food_qdoba, - food_aldi, - reason="Hype squad", - ) - embed = build_embed( - description=f"{ctx.author.mention} has left all food roles.", - image=None, - thumbnail=None, - author=None, - footer="This message will disappear after 10 seconds.", + try: + client.connect( + hostname=SSH_HOST, username=SSH_USERNAME, password=SSH_PASSWORD ) - await ctx.send(embed=embed, delete_after=10) - return - else: - return - - embed = build_embed( - description=f"{ctx.author.mention} has joined: {chosen_food}.", - image=None, - thumbnail=None, - author=None, - footer="This message will disappear after 10 seconds.", - ) - await ctx.send(embed=embed, delete_after=10) - - log(0, f"Roles: Food") + except ( + BadHostKeyException, + AuthenticationException, + SSHException, + socket.error, + ): + logger.exception("Unable to restart the bot!", exc_info=True) - @cog_ext.cog_subcommand( - base="roles", - base_description="Manage your roles", - name="culture", - description="Assign culture roles", - guild_ids=guild_id_list(), - ) - async def _roles_culture(self, ctx: SlashContext): - log(0, f"Roles: Culture") + logger.info("SSH Client connected to host") - embed = build_embed( - title="Which culture roles do you want?", - description="Assign yourself a role", - inline=False, - fields=[ - ["๐Ÿ˜น Meme team", "Memes are life"], - ["โ™ฃ He Man Isms Hater Club", "Idontbelieveinisms sucks"], - ["๐Ÿง€ Packer Backer", "Green Bay fan"], - ["๐Ÿ“ฑ Pixel Gang", "Android fan"], - ["๐ŸŽง Airpod Gang", "Apple fan"], - ["๐Ÿ•ณ None", "Remove food roles"], - ], + # Update change log + bash_script_path = pathlib.PurePosixPath( + f"{pathlib.Path(__file__).parent.parent.parent.resolve()}/changelog.sh" ) + bash_script = open(bash_script_path).read() - culture_action_row = spread_to_rows(*buttons_roles_culture, max_in_row=2) - - await ctx.send(embed=embed, components=culture_action_row) - - @cog_ext.cog_component(components=buttons_roles_culture) - async def process_roles_culture(self, ctx: ComponentContext): - await ctx.defer() - - log(0, f"Gathering roles") - - culture_meme = ctx.guild.get_role(ROLE_MEME) - culture_isms = ctx.guild.get_role(ROLE_ISMS) - culiture_packer = ctx.guild.get_role(ROLE_PACKER) - culture_pixel = ctx.guild.get_role(ROLE_PIXEL) - culture_airpod = ctx.guild.get_role(ROLE_AIRPOD) + stdin, stdout, stderr = client.exec_command(bash_script) + logger.info(stdout.read().decode()) - if ( - any( - [ - culture_meme, - culture_isms, - culiture_packer, - culture_pixel, - culture_airpod, - ] - ) - is None - ): - raise CommandError("Unable to locate role!") - - if ctx.custom_id == "role_culture_meme": - await ctx.author.add_roles(culture_meme, reason="Hype squad") - chosen_food = culture_meme.mention - elif ctx.custom_id == "role_culture_isms": - await ctx.author.add_roles(culture_isms, reason="Hype squad") - chosen_food = culture_isms.mention - elif ctx.custom_id == "role_culture_packer": - await ctx.author.add_roles(culiture_packer, reason="Hype squad") - chosen_food = culiture_packer.mention - elif ctx.custom_id == "role_culture_pixel": - await ctx.author.add_roles(culture_pixel, reason="Hype squad") - chosen_food = culture_pixel.mention - elif ctx.custom_id == "role_culture_airpod": - await ctx.author.add_roles(culture_airpod, reason="Hype squad") - chosen_food = culture_airpod.mention - elif ctx.custom_id == "roles_culture_remove": - await ctx.author.remove_roles( - culture_meme, - culture_isms, - culiture_packer, - culture_pixel, - culture_airpod, - reason="Hype squad", - ) - embed = build_embed( - description=f"{ctx.author.mention} has left all culture roles.", - image=None, - thumbnail=None, - author=None, - footer="This message will disappear after 10 seconds.", - ) - await ctx.send(embed=embed, delete_after=10) - return - else: - return + err = stderr.read().decode() + assert err is None, CommandException(err) - embed = build_embed( - description=f"{ctx.author.mention} has joined: {chosen_food}.", - image=None, - thumbnail=None, - author=None, - footer="This message will disappear after 10 seconds.", + # Restart + bash_script_path = pathlib.PurePosixPath( + f"{pathlib.Path(__file__).parent.parent.parent.resolve()}/restart.sh" ) - await ctx.send(embed=embed, delete_after=10) + bash_script = open(bash_script_path).read() - log(0, f"Roles: Culture") + stdin, stdout, stderr = client.exec_command(bash_script) + logger.info(stdout.read().decode()) - async def alert_gameday_channels(self, on: bool): - chan_general = self.bot.get_channel(id=CHAN_GENERAL) - chan_live = self.bot.get_channel(id=CHAN_DISCUSSION_LIVE) - chan_streaming = self.bot.get_channel(id=CHAN_DISCUSSION_STREAMING) + err = stderr.read().decode() + assert err is None, CommandException(err) - if on: - embed = build_embed( - title="Game Day Mode", - inline=False, - description="Game day mode is now on!", - fields=[ - [ - "Live TV", - f"{chan_live.mention} text and voice channels are for users who are watching live.", - ], - [ - "Streaming", - f"{chan_streaming.mention} text and voice channels are for users who are streaming the game.", - ], - [ - "Info", - "All channels in the Huskers category will be turned off until the game day mode is disabled.", - ], - ], - ) - else: - embed = build_embed( - title="Game Day Mode", - inline=False, - description="Game day mode is now off!", - fields=[ - [ - "Info", - f"Game day channels have been disabled and General categories channels have been enabled. Regular discussion may continue in {chan_general.mention}.", - ] - ], - ) + client.close() + logger.info("SSH Client is closed.") - await chan_general.send(embed=embed) - await chan_live.send(embed=embed) - await chan_streaming.send(embed=embed) + await interaction.channel.send("Bot restart complete!") - @cog_ext.cog_subcommand( - base="gameday", - base_description="Admin and mod only: Turn game day mode on or off", - name="on", - description="Turn game day mode on", - guild_ids=guild_id_list(), - ) - @cog_ext.permission( - guild_id=guild_id_list()[0], - permissions=[ - create_permission(ROLE_ADMIN_PROD, SlashCommandPermissionType.ROLE, True), - create_permission(ROLE_MOD_PROD, SlashCommandPermissionType.ROLE, True), - create_permission( - ROLE_EVERYONE_PROD, SlashCommandPermissionType.ROLE, False + @group_submit.command(name="bug", description="Submit a bug") + async def submit_bug(self, interaction: discord.Interaction) -> None: + embed = buildEmbed( + title="Bug Reporter", + description=discordURLFormatter( + "Submit a bug report here", + "https://github.com/refekt/Bot-Frost/issues/new?assignees=refekt&labels=bug&template=bug_report.md&title=%5BBUG%5D+", ), - ], - ) - async def _gameday_on(self, ctx: SlashContext): - log(0, f"Game Day: On") - await ctx.defer(hidden=True) - await ctx.send("Processing!", hidden=True) - await process_gameday(True, ctx.guild) - await self.alert_gameday_channels(True) - - @cog_ext.cog_subcommand( - base="gameday", - base_description="Admin and mod only: Turn game day mode on or off", - name="off", - description="Turn game day mode off", - guild_ids=guild_id_list(), - ) - @cog_ext.permission( - guild_id=guild_id_list()[0], - permissions=[ - create_permission(ROLE_ADMIN_PROD, SlashCommandPermissionType.ROLE, True), - create_permission(ROLE_MOD_PROD, SlashCommandPermissionType.ROLE, True), - create_permission( - ROLE_EVERYONE_PROD, SlashCommandPermissionType.ROLE, False - ), - ], - ) - async def _gameday_off(self, ctx: SlashContext): - log(0, f"Game Day: Off") - await ctx.defer(hidden=True) - await ctx.send("Processing!", hidden=True) - await process_gameday(False, ctx.guild) - await self.alert_gameday_channels(False) - - @cog_ext.cog_slash( - name="commands", - description="Show all slash commands. This replaced $help", - guild_ids=guild_id_list(), - options=[ - create_option( - name="command_name", - description="Name of the command you want to view", - option_type=3, - required=False, - ) - ], - ) - async def _commands(self, ctx: SlashContext, command_name: str = None): - def command_embed(cur_cmd, cur_options) -> discord.Embed: - opts = "" - for copt in cur_options: - opts += f"ยป {copt['name']} ({'Required' if copt['required'] else 'Optional'}): {copt['description']}\n" - return build_embed( - title="Bot Frost Commands", - fields=[ - ["Command Name", cur_cmd.name], - ["Description", cur_cmd.description], - ["Options", opts if opts != "" else "N/A"], - ], - ) - - if not command_name: - pages = [] - for command in ctx.slash.commands: - if not type(ctx.slash.commands[command]) == dict: - pages.append( - command_embed( - ctx.slash.commands[command], - ctx.slash.commands[command].options, - ) - ) - await Paginator( - bot=ctx.bot, ctx=ctx, pages=pages, useSelect=False, useIndexButton=True - ).run() - else: - command_name = command_name.lower() - await ctx.send( - embed=command_embed( - ctx.slash.commands[command_name], - ctx.slash.commands[command_name].options, - ), - hidden=True, - ) - - @cog_ext.cog_slash( - name="iowa", - description="Admin and mod only: Sends members to Iowa", - guild_ids=guild_id_list(), - ) - @cog_ext.permission( - guild_id=guild_id_list()[0], - permissions=[ - create_permission(ROLE_ADMIN_PROD, SlashCommandPermissionType.ROLE, True), - create_permission(ROLE_MOD_PROD, SlashCommandPermissionType.ROLE, True), - create_permission( - ROLE_EVERYONE_PROD, SlashCommandPermissionType.ROLE, False + ) + await interaction.response.send_message(embed=embed) + + @group_submit.command(name="feature", description="Submit a feature") + async def submit_feature(self, interaction: discord.Interaction) -> None: + embed = buildEmbed( + title="Feature Request", + description=discordURLFormatter( + "Submit a feature request here", + "https://github.com/refekt/Bot-Frost/issues/new?assignees=refekt&labels=request&template=feature_request.md&title=%5BREQUEST%5D+", ), - ], - ) - async def _iowa( - self, ctx: SlashContext, who: discord.Member, reason: str, duration: int = None - ): - await ctx.defer() - log(0, f"Starting the Iowa command and banishing {who}") + ) + await interaction.response.send_message(embed=embed) + + @app_commands.command(name="iowa", description="Send someone to Iowa") + @app_commands.describe(who="User to send to Iowa", reason="The reason why") + @app_commands.guilds(GUILD_PROD) + @app_commands.default_permissions(manage_messages=True) + async def iowa( + self, + interaction: discord.Interaction, + who: DISCORD_USER_TYPES, + reason: str, + ) -> None: + await interaction.response.defer(thinking=True) + + logger.info( + f"Starting the Iowa command and banishing {who.name}#{who.discriminator}" + ) - assert who, UserError("You must include a user!") - assert reason, UserError("You must include a reason why!") + assert who, UserInputException("You must include a user!") + assert reason, UserInputException("You must include a reason why!") - role_timeout = ctx.guild.get_role(ROLE_TIME_OUT) - channel_iowa = ctx.guild.get_channel(CHAN_IOWA) - full_reason = f"Time Out by {ctx.author}: " + reason + role_timeout = interaction.guild.get_role(ROLE_TIME_OUT) + channel_iowa = interaction.guild.get_channel(CHAN_IOWA) + full_reason = ( + f"Time Out by {interaction.user.name}#{interaction.user.discriminator}: " + + reason + ) previous_roles = [str(role.id) for role in who.roles[1:]] if previous_roles: previous_roles = ",".join(previous_roles) - log(1, f"Gathered all the roles to store") + logger.info(f"Gathered all the roles to store") roles = who.roles for role in roles: try: await who.remove_roles(role, reason=full_reason) - log(1, f"Removed [{role}] role") + logger.info(f"Removed [{role}] role") except (discord.Forbidden, discord.HTTPException): - pass + continue try: await who.add_roles(role_timeout, reason=full_reason) - log(1, f"Added [{role_timeout}] role") except (discord.Forbidden, discord.HTTPException): - pass + logger.exception( + f"Unable to add role to {who.name}#{who.discriminator}!", exc_info=True + ) - Process_MySQL(query=sqlInsertIowa, values=(who.id, full_reason, previous_roles)) + logger.info(f"Added [{role_timeout}] role to {who.name}#{who.discriminator}") - if duration: - nest_asyncio.apply() - asyncio.create_task(end_timeout(duration=duration, ctx=ctx, who=who)) + processMySQL(query=sqlInsertIowa, values=(who.id, full_reason, previous_roles)) - embed = build_embed( - title="Banished to Iowa", - inline=False, - fields=[ - [ - "Statement", - f"[{who.mention}] has had all roles removed and been sent to Iowa. Their User ID has been recorded and {role_timeout.mention} will be reapplied on rejoining the server. The user will be brought back in {pretty_time_delta(duration)}.", - ], - ["Reason", full_reason], - ], - ) - else: - embed = build_embed( - title="Banished to Iowa", - inline=False, - fields=[ - [ - "Statement", - f"[{who.mention}] has had all roles removed and been sent to Iowa. Their User ID has been recorded and {role_timeout.mention} will be reapplied on rejoining the server.", - ], - ["Reason", full_reason], - ], - ) + embed = buildEmbed( + title="Banished to Iowa", + fields=[ + { + "name": "Statement", + "value": f"[{who.mention}] has had all roles removed and been sent to Iowa. Their User ID has been recorded and {role_timeout.mention} will be reapplied on rejoining the server.", + }, + {"name": "Reason", "value": full_reason, "inline": False}, + ], + ) - await ctx.send(embed=embed) + await interaction.followup.send(embed=embed) await who.send( f"You have been moved to [ {channel_iowa.mention} ] for the following reason: {reason}." ) - log(0, "Iowa command complete") - - @cog_ext.cog_slash( - name="nebraska", - description="Admin and mod only: Bring a member back from Iowa", - guild_ids=guild_id_list(), - ) - @cog_ext.permission( - guild_id=guild_id_list()[0], - permissions=[ - create_permission(ROLE_ADMIN_PROD, SlashCommandPermissionType.ROLE, True), - create_permission(ROLE_MOD_PROD, SlashCommandPermissionType.ROLE, True), - create_permission( - ROLE_EVERYONE_PROD, SlashCommandPermissionType.ROLE, False - ), - ], - ) - async def _nebraska(self, ctx: SlashContext, who: discord.Member): - await ctx.defer() - log(0, f"Starting the Nebraska command and banishing {who}") - - if await process_nebraska(ctx, who): - embed = build_embed( - title="Return to Nebraska", - inline=False, - fields=[ - ["Welcome back!", f"[{who.mention}] is welcomed back to Nebraska!"], - ["Welcomed by", ctx.author.mention], - ], - ) - await ctx.send(embed=embed) - log(0, "Nebraska command complete") - - @cog_ext.cog_slash( - name="console", - description="Admin or mod only", - guild_ids=guild_id_list(), - ) - @cog_ext.permission( - guild_id=guild_id_list()[0], - permissions=[ - create_permission(ROLE_ADMIN_PROD, SlashCommandPermissionType.ROLE, True), - create_permission(ROLE_MOD_PROD, SlashCommandPermissionType.ROLE, True), - create_permission( - ROLE_EVERYONE_PROD, SlashCommandPermissionType.ROLE, False - ), - ], - ) - async def _console(self, ctx: SlashContext): - - console_actionrow = create_actionrow(*console_buttons) - await ctx.send(content="Shh..", hidden=True) - await ctx.send(content="Shh...", components=[console_actionrow], hidden=True) - - @cog_ext.cog_slash( - name="restart", - description="Admin only: Restart the Twitter stream", - guild_ids=guild_id_list(), - ) - @cog_ext.permission( - guild_id=guild_id_list()[0], - permissions=[ - create_permission(ROLE_ADMIN_PROD, SlashCommandPermissionType.ROLE, True), - create_permission(ROLE_MOD_PROD, SlashCommandPermissionType.ROLE, False), - create_permission( - ROLE_EVERYONE_PROD, SlashCommandPermissionType.ROLE, False - ), - ], - ) - async def _restart(self, ctx: SlashContext): - if "Windows" in platform.platform(): - return - - await ctx.defer(hidden=True) - - log(0, "Restarting the bot via SSH") - - client = paramiko.SSHClient() - client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - - log(1, "SSH Client established") - + logger.info("Iowa command complete") + + @app_commands.command(name="nebraska", description="Bring someone back to Nebraska") + @app_commands.describe(who="User to bring back to Nebraska") + @app_commands.guilds(GUILD_PROD) + @app_commands.default_permissions(manage_messages=True) + async def nebraska( + self, + interaction: discord.Interaction, + who: DISCORD_USER_TYPES, + ) -> None: + assert who, UserInputException("You must include a user!") + + await interaction.response.defer(thinking=True) + + role_timeout = interaction.guild.get_role(ROLE_TIME_OUT) try: - client.connect( - hostname=SSH_HOST, username=SSH_USERNAME, password=SSH_PASSWORD - ) - log(1, "SSH Client connected to host") - except: # noqa - log(0, "SSH Client was unable to connect ot host") - await ctx.send("Unable to restart the bot!", hidden=True) - return - - # Update change log - bash_script_path = pathlib.PurePosixPath( - f"{pathlib.Path(__file__).parent.parent.parent.resolve()}/changelog.sh" - ) - bash_script = open(bash_script_path).read() - - stdin, stdout, stderr = client.exec_command(bash_script) - log(1, stdout.read().decode()) + await who.remove_roles(role_timeout) + except (Forbidden, HTTPException) as e: + logger.exception(f"Unable to remove the timeout role!\n{e}", exc_info=True) - err = stderr.read().decode() - if err: - log(1, err) + logger.info(f"Removed [{role_timeout}] role") - # Restart - bash_script_path = pathlib.PurePosixPath( - f"{pathlib.Path(__file__).parent.parent.parent.resolve()}/restart.sh" + previous_roles_raw = processMySQL( + query=sqlRetrieveIowa, values=who.id, fetch="all" ) - bash_script = open(bash_script_path).read() - - stdin, stdout, stderr = client.exec_command(bash_script) - log(1, stdout.read().decode()) - - err = stderr.read().decode() - if err: - log(1, err) - client.close() - log(0, "SSH Client is closed.") - - await ctx.send("Bot restart complete!", hidden=True) - - @cog_ext.cog_component(components=console_buttons) - async def process_console(self, ctx: ComponentContext): - if ctx.custom_id == "SMMS": - chan_select_actionrow = create_actionrow(console_chan_select) - await ctx.send( - "Choose a channel.", hidden=True, components=[chan_select_actionrow] - ) - - @cog_ext.cog_component(components=console_chan_select) - async def process_console_channel(self, ctx: ComponentContext): - await ctx.send("What is your message?", hidden=True) - try: + processMySQL(query=sqlRemoveIowa, values=who.id) + + if previous_roles_raw is not None: + previous_roles = previous_roles_raw[0]["previous_roles"].split(",") + logger.info(f"Gathered all the roles to store") + + if previous_roles: + for role in previous_roles: + try: + new_role = interaction.guild.get_role(int(role)) + logger.info(f"Attempting to add [{new_role}] role...") + await who.add_roles(new_role, reason="Returning from Iowa") + except ( + discord.Forbidden, + discord.HTTPException, + discord.ext.commands.MissingPermissions, + ) as e: + logger.info(f"Unable to add role!\n{e}") + continue + + logger.info(f"Added [{new_role}] role") + + embed = buildEmbed( + title="Return to Nebraska", + fields=[ + { + "name": "Welcome back!", + "value": f"[{who.mention}] is welcomed back to Nebraska!", + }, + { + "name": "Welcomed by", + "value": interaction.user.mention, + }, + ], + ) + await interaction.followup.send(embed=embed) - def validate(messsage): - if ( - messsage.channel.id == ctx.channel_id - and messsage.author.id == ctx.author_id - ): - return True - else: - return False + logger.info("Nebraska command complete") - user_input = await self.bot.wait_for("message", check=validate) - user_msg = user_input.clean_content - await user_input.delete() - del user_input - except asyncio.TimeoutError: - return + @group_gameday.command( + name="on", + description="WIP: Turn game day mode on. Restricts access to server channels.", + ) + @app_commands.default_permissions(manage_messages=True) + async def gameday_on(self, interaction: discord.Interaction) -> None: + logger.info(f"Game Day: On") + await interaction.response.defer(ephemeral=True) + await interaction.followup.send("Processing!") + await self.process_gameday(True, interaction.guild) + await self.alert_gameday_channels(client=interaction.client, on=True) + + @group_gameday.command( + name="off", + description="WIP: Turn game day mode off. Restores access to server channels.", + ) + @app_commands.default_permissions(manage_messages=True) + async def gameday_off(self, interaction: discord.Interaction) -> None: + logger.info(f"Game Day: Off") + await interaction.response.defer(ephemeral=True) + await interaction.followup.send("Processing!") + await self.process_gameday(False, interaction.guild) + await self.alert_gameday_channels(client=interaction.client, on=False) + + @app_commands.command(name="smms", description="Tee hee") # TODO Make hidden + @app_commands.default_permissions(manage_messages=True) + async def smms( + self, + interaction: discord.Interaction, + destination: MammaleChannels, + message: str, + ) -> None: + assert message, CommandException("You cannot have a blank message!") + + await interaction.response.defer(ephemeral=True, thinking=True) - embed = build_embed( + chan = None + if destination.name == "general": + chan = await interaction.guild.fetch_channel(CHAN_GENERAL) + elif destination.name == "recruiting": + chan = await interaction.guild.fetch_channel(CHAN_RECRUITING) + elif destination.name == "admin": + chan = await interaction.guild.fetch_channel(CHAN_ADMIN) + + embed = buildEmbed( title="Secret Mammal Message System (SMMS)", + description="These messages have no way to be verified to be accurate.", thumbnail="https://i.imgur.com/EGC1qNt.jpg", footer=BOT_FOOTER_SECRET, - fields=[["Back Channel Communication", user_msg]], + fields=[ + dict( + name="Back Channel Communication", + value=message, + ) + ], ) + await chan.send(embed=embed) - chan = None - - if ctx.values[0] == "SMMS_general": - chan = ctx.guild.get_channel(CHAN_GENERAL) - elif ctx.values[0] == "SMMS_recruiting": - chan = ctx.guild.get_channel(CHAN_RECRUITING) - elif ctx.values[0] == "SMMS_war": - chan = ctx.guild.get_channel(CHAN_WAR_ROOM) - - if chan is not None: - await chan.send(embed=embed) - else: - await ctx.send("Hiccup!", hidden=True) + await interaction.followup.send( + f"Back channel communication successfully sent to {chan.mention}!" + ) -def setup(bot): - bot.add_cog(AdminCommands(bot)) +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(AdminCog(bot), guilds=[discord.Object(id=GUILD_PROD)]) diff --git a/commands/croot_bot.py b/commands/croot_bot.py deleted file mode 100644 index 5e2e5d73..00000000 --- a/commands/croot_bot.py +++ /dev/null @@ -1,259 +0,0 @@ -import datetime - -from discord.ext import commands -from discord_slash import ButtonStyle, ComponentContext, SlashContext, cog_ext -from discord_slash.utils.manage_commands import create_option -from discord_slash.utils.manage_components import ( - create_actionrow, - create_button, - wait_for_component, -) - -from objects.FAPing import individual_predictions, initiate_fap -from objects.Recruits import FootballRecruit -from utilities.constants import CROOT_SEARCH_LIMIT, TZ, UserError, guild_id_list -from utilities.embed import build_embed, build_recruit_embed - - -def log(message: str, level: int): - import datetime - - if level == 0: - print(f"[{datetime.datetime.now()}] ### Croot Bot: {message}") - elif level == 1: - print(f"[{datetime.datetime.now()}] ### ~~~ Croot Bot: {message}") - - -fap_buttons = [ - create_button( - style=ButtonStyle.gray, label="๐Ÿ”ฎ", custom_id="crystal_ball", disabled=True - ), - create_button( - style=ButtonStyle.gray, label="๐Ÿ“œ", custom_id="scroll", disabled=False - ), -] -fap_action_row = create_actionrow(*fap_buttons) - -croot_search = [] -fap_search = [] -search_reactions = {"1๏ธโƒฃ": 0, "2๏ธโƒฃ": 1, "3๏ธโƒฃ": 2, "4๏ธโƒฃ": 3, "5๏ธโƒฃ": 4} - -search_buttons = [ - create_button(style=ButtonStyle.blue, label="1", custom_id="result_1"), - create_button(style=ButtonStyle.blue, label="2", custom_id="result_2"), - create_button(style=ButtonStyle.blue, label="3", custom_id="result_3"), - create_button(style=ButtonStyle.blue, label="4", custom_id="result_4"), - create_button(style=ButtonStyle.blue, label="5", custom_id="result_5"), -] - - -def search_result_info(new_search) -> str: - result_info = "" - for index, recruit in enumerate(new_search): - if index < CROOT_SEARCH_LIMIT: - result_info += ( - f"{list(search_reactions.keys())[index]}: " - f"{recruit.year} - " - f"{'โญ' * recruit.rating_stars if recruit.rating_stars else 'N/R'} - " - f"{recruit.position} - " - f"{recruit.name}\n" - ) - return result_info - - -async def final_send_embed_fap_loop(ctx, target_recruit, bot, edit: bool = False): - embed = build_recruit_embed(target_recruit) - - if not target_recruit.committed == "Enrolled": - fap_buttons[0]["disabled"] = False - - if edit: - log(f"Editing message", 1) - await ctx.edit_origin(content="", embed=embed, components=[fap_action_row]) - else: - log(f"Sending message", 1) - embed = build_recruit_embed(target_recruit) - await ctx.send(embed=embed, components=[fap_action_row]) - - button_context: ComponentContext = await wait_for_component( - bot, components=fap_action_row - ) - - if button_context.custom_id == "crystal_ball": - log(f"Crystal ball pressed for [{target_recruit.name.capitalize()}]", 0) - await initiate_fap(ctx=ctx, user=ctx.author, recruit=target_recruit, client=bot) - return - - elif button_context.custom_id == "scroll": - log(f"Scroll pressed for [{target_recruit.name.capitalize()}]", 0) - await individual_predictions(ctx=ctx, recruit=target_recruit) - return - - -def checking_reaction(search_reactions, reaction_used, user_initiated): - if not user_initiated.bot: - return reaction_used.emoji in search_reactions - - -class RecruitCog(commands.Cog): - def __init__(self, bot): - self.bot = bot - - @cog_ext.cog_slash( - name="crootbot", - description="Retreive information about a recruit", - guild_ids=guild_id_list(), - ) - async def _crootbot(self, ctx: SlashContext, year: int, search_name: str): - print(f"[{datetime.datetime.now().astimezone(tz=TZ)}] ### Crootbot") - - if len(search_name) == 0: - raise UserError("A player's first and/or last search_name is required.") - - if len(str(year)) == 2: - year += 2000 - elif len(str(year)) == 1 or len(str(year)) == 3: - raise UserError("The search year must be two or four digits long.") - - if year > datetime.datetime.now().year + 5: - raise UserError( - "The search year must be within five years of the current class." - ) - - if year < 1869: - raise UserError( - "The search year must be after the first season of college football--1869." - ) - - await ctx.defer() # Similar to sending a message with a loading screen to edit later on - - log(f"Searching for [{year} {search_name.capitalize()}]", 1) - - global croot_search - croot_search = FootballRecruit(year, search_name) - - log(f"Found [{len(croot_search)}] results", 1) - - if len(croot_search) == 1: - return await final_send_embed_fap_loop( - ctx=ctx, target_recruit=croot_search[0], bot=self.bot - ) - - result_info = search_result_info(croot_search) - action_row = create_actionrow(*search_buttons) - - embed = build_embed( - title=f"Search Results for [{year} {search_name.capitalize()}]", - fields=[["Search Results", result_info]], - ) - - await ctx.send(embed=embed, components=[action_row]) - - log(f"Sent search results for [{year} {search_name.capitalize()}]", 1) - - @cog_ext.cog_subcommand( - name="predict", - description="Place a FAP for a recruit's commitment", - base="fap", - base_description="Frost approved predictions", - guild_ids=guild_id_list(), - options=[ - create_option( - name="year", - option_type=4, - description="Year of the recruit", - required=True, - ), - create_option( - name="search_name", - option_type=3, - description="Name of the recruit", - required=True, - ), - ], - ) - async def _fap_predict(self, ctx: SlashContext, year: int, search_name: str): - if len(str(year)) == 2: - year += 2000 - - if year > datetime.datetime.now().year + 5: - raise UserError( - "The search year must be within five years of the current class." - ) - - if year < 1869: - raise UserError( - "The search year must be after the first season of college football--1869." - ) - - await ctx.defer(hidden=True) - - global fap_search - fap_search = FootballRecruit(year, search_name) - - if type(fap_search) == commands.UserInputError: - return await ctx.send(content=fap_search, hidden=True) - - async def send_fap_convo(target_recruit): - await initiate_fap( - ctx=ctx, user=ctx.author, recruit=target_recruit, client=ctx.bot - ) - - if len(fap_search) == 1: - return await send_fap_convo(fap_search[0]) - - result_info = search_result_info(fap_search) - action_row = create_actionrow(*search_buttons) - - embed = build_embed( - title=f"Search Results for [{year} {search_name.capitalize()}]", - fields=[["Search Results", result_info]], - ) - - await ctx.send(embed=embed, components=[action_row], hidden=True) - - @cog_ext.cog_subcommand( - name="leaderboard", - description="The FAP leaderboard", - base="fap", - base_description="Frost approved predictions", - guild_ids=guild_id_list(), - ) - async def _fap_leaderboard(self, ctx: SlashContext): - # TODO All of this. - await ctx.send("Work in progress!", hidden=True) - - @cog_ext.cog_component(components=search_buttons) - async def process_croot_bot(self, ctx: ComponentContext): - button_to_index = { - "result_1": 0, - "result_2": 1, - "result_3": 2, - "result_4": 3, - "result_5": 4, - } - log(f"Button [{ctx.custom_id}] was pressed", 1) - - global croot_search, fap_search - - if croot_search is not None: - await final_send_embed_fap_loop( - ctx=ctx, - target_recruit=croot_search[button_to_index[ctx.custom_id]], - bot=self.bot, - edit=True, - ) - del croot_search - if fap_search is not None: - await initiate_fap( - ctx=ctx, - user=ctx.author, - recruit=fap_search[button_to_index[ctx.custom_id]], - client=ctx.bot, - ) - - del fap_search - - -def setup(bot): - bot.add_cog(RecruitCog(bot)) diff --git a/commands/example_commands.py b/commands/example_commands.py new file mode 100644 index 00000000..169b6e37 --- /dev/null +++ b/commands/example_commands.py @@ -0,0 +1,58 @@ +# https://gist.github.com/AbstractUmbra/a9c188797ae194e592efe05fa129c57f +# https://discordpy.readthedocs.io/en/latest/interactions/api.html#discord.Interaction + +import discord +from discord import app_commands +from discord.ext import commands + +from helpers.constants import GUILD_PROD + + +class MyCog(commands.Cog): + # for simplicity, these commands are all global. You can add `guild=` or `guilds=` to `Bot.add_cog` in `setup` to add them to a guild. + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + super().__init__() # this is now required in this context. + + group_example = app_commands.Group(name="example-command", description="TBD") + + @group_example.command(name="sub-command") + async def sub_command(self, interaction: discord.Interaction) -> None: + """/example-command sub-command""" + ... + + @app_commands.command(name="command-1", description="Descript #1") + @app_commands.describe(example="Description for example variable") + @app_commands.guilds(GUILD_PROD) + @app_commands.default_permissions( + manage_messages=True + ) # https://discordpy.readthedocs.io/en/latest/api.html#discord.Permissions + async def my_command(self, interaction: discord.Interaction, example: str) -> None: + """/command-1""" + await interaction.response.send_message("Hello from command 1!", ephemeral=True) + + @app_commands.command(name="command-2") + @app_commands.guilds(discord.Object(id=...), ...) + async def my_private_command(self, interaction: discord.Interaction) -> None: + """/command-2""" + await interaction.response.send_message( + "Hello from private command!", ephemeral=True + ) + + @app_commands.command(name="sub-1") + async def my_sub_command_1(self, interaction: discord.Interaction) -> None: + """/parent sub-1""" + await interaction.response.send_message( + "Hello from sub command 1", ephemeral=True # ephemeral hides messages + ) + + @app_commands.command(name="sub-2") + async def my_sub_command_2(self, interaction: discord.Interaction) -> None: + """/parent sub-2""" + await interaction.response.send_message( + "Hello from sub command 2", ephemeral=True + ) + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(MyCog(bot), guilds=[discord.Object(id=GUILD_PROD)]) diff --git a/commands/football_stats.py b/commands/football_stats.py index 30fd5435..ef8ea002 100644 --- a/commands/football_stats.py +++ b/commands/football_stats.py @@ -1,65 +1,90 @@ import calendar +import logging from datetime import datetime +from typing import Union, Any -from cfbd import ApiClient, BettingApi, Configuration, GamesApi, StatsApi +import discord +from cfbd import ApiClient, BettingApi, Configuration, GamesApi from cfbd.rest import ApiException -from dinteractions_Paginator import Paginator +from discord import app_commands from discord.ext import commands -from discord_slash import cog_ext, SlashContext -from discord_slash.model import ButtonStyle -from discord_slash.utils.manage_commands import create_option +from helpers.constants import ( + CFBD_KEY, + DT_OBJ_FORMAT, + DT_OBJ_FORMAT_TBA, + DT_TBA_HR, + DT_TBA_MIN, + GUILD_PROD, + TZ, +) +from helpers.embed import buildEmbed, collectScheduleEmbeds +from objects.Exceptions import StatsException +from objects.Paginator import EmbedPaginatorView from objects.Schedule import HuskerSchedule -from objects.Winsipedia import CompareWinsipedia, TeamStatsWinsipediaTeam -from utilities.constants import CFBD_KEY, TZ, guild_id_list, pretty_time_delta -from utilities.embed import build_countdown_embed, build_embed, return_schedule_embeds -import urllib.parse +from objects.Winsipedia import CompareWinsipedia + +logger = logging.getLogger(__name__) + cfbd_config = Configuration() cfbd_config.api_key["Authorization"] = CFBD_KEY cfbd_config.api_key_prefix["Authorization"] = "Bearer" - -def log(message: str, level: int): - import datetime - - if level == 0: - print(f"[{datetime.datetime.now()}] ### Football Stats: {message}") - elif level == 1: - print(f"[{datetime.datetime.now()}] ### ~~~ Football Stats: {message}") +__all__ = ["FootballStatsCog"] -def convert_seconds(n): +def convert_seconds(n) -> Union[int, Any]: + logger.info(f"Converting {n:,} seconds to hours and minutes") secs = n % (24 * 3600) hour = secs // 3600 secs %= 3600 mins = secs // 60 - return hour, mins def get_current_week(year: int, team: str) -> int: + logger.info(f"Getting the current week for the {year} {team} game") api = GamesApi(ApiClient(cfbd_config)) try: - games = api.get_games(year=year, team=team) + games = api.get_games( + year=year, team="nebraska" + ) # We only care about Nebraska's schedule except ApiException: - return -1 + logger.exception("CFBD API unable to get games", exc_info=True) + raise StatsException("CFBD API unable to get games") for index, game in enumerate(games): - if team == "Nebraska": - if game.away_points is None and game.home_points is None: - return game.week - else: - if any( - [game.away_team == "Nebraska", game.home_team == "Nebraska"] - ) and any([game.away_team == team, game.home_team == team]): + logger.info(f"Checking the Week {game.week} game.") + if team.lower() == "nebraska": + if game.week == games[1].week: # Week 0 game + return 0 + + if not (game.away_points and game.home_points): return game.week + else: + logger.exception( + "Unknown error occured when getting week for Nebraska game", + exc_info=True, + ) + elif ( + game.away_team.lower() == "nebraska" or game.home_team.lower() == "nebraska" + ) and ( + game.away_team.lower() == team.lower() + or game.home_team.lower() == team.lower() + ): + return game.week + + logger.exception(f"Unable to find week for {team}") + raise StatsException(f"Unable to find week for {team}") def get_consensus_line( team_name: str, year: int = datetime.now().year, week: int = None -): +) -> Union[None, str]: + logger.info(f"Getting the conensus line for {year} Week {week} {team_name} game") + cfb_api = BettingApi(ApiClient(cfbd_config)) if week is None: @@ -70,178 +95,110 @@ def get_consensus_line( except (ApiException, TypeError): return None - log(f"Results: {api_response}", 1) + logger.info(f"Results: {api_response}") try: - # Hard code Week 0 - lines = None - if len(api_response) > 1: - for resp in api_response: - if resp.away_team == "Nebraska": - lines = resp.lines[0] - break - else: - lines = api_response[0].lines[0] - - if lines is None: - return None + lines = None # Hard code Week 0 + # if len(api_response) > 1: + # for game in api_response: + # if game.away_team == "Nebraska": + # lines = game.lines[0] + # break + # else: + # lines = api_response[0].lines[0] + # + # if lines is None: + # return None + for game in api_response: + if game.away_score is None and game.home_score is None: + lines = game.lines[0] + break - log(f"Lines: {lines}", 1) + logger.info(f"Lines: {lines}") formattedSpread = ( - spreadOpen - ) = overUnder = overUnderOpen = homeMoneyline = awayMoneyline = "" - - if lines.get("formattedSpread", None): - formattedSpread = lines.get("formattedSpread") - if lines.get("spreadOpen", None): - spreadOpen = lines.get("spreadOpen") - if lines.get("overUnder", None): - overUnder = lines.get("overUnder") - if lines.get("overUnderOpen", None): - overUnderOpen = lines.get("overUnderOpen") - if lines.get("homeMoneyline", None): - homeMoneyline = str(lines.get("homeMoneyline")) - if lines.get("awayMoneyline", None): - awayMoneyline = str(lines.get("awayMoneyline")) + lines.get("formattedSpread") if lines.get("formattedSpread", None) else "" + ) + spreadOpen = lines.get("spreadOpen") if lines.get("spreadOpen", None) else "N/A" + overUnder = lines.get("overUnder") if lines.get("overUnder", None) else "" + overUnderOpen = ( + lines.get("overUnderOpen") if lines.get("overUnderOpen", None) else "N/A" + ) + homeMoneyline = ( + str(lines.get("homeMoneyline")) if lines.get("homeMoneyline", None) else "" + ) + awayMoneyline = ( + str(lines.get("awayMoneyline")) if lines.get("awayMoneyline", None) else "" + ) + new_line = "\n" consensus_line = ( - f"{'Spread: ' + formattedSpread + ' (Opened: ' + spreadOpen + ')' + new_line if formattedSpread else ''}" - f"{'Over/Under: ' + overUnder + ' (Opened: ' + overUnderOpen + ')' + new_line if overUnder else ''}" - f"{'Home Moneyline: ' + homeMoneyline + new_line if homeMoneyline else ''}" - f"{'Away Moneyline: ' + awayMoneyline + new_line if awayMoneyline else ''}" + f"{'Spread: ' + formattedSpread + ' (Opened: ' + spreadOpen + ')' + new_line}" + f"{'Over/Under: ' + overUnder + ' (Opened: ' + overUnderOpen + ')' + new_line}" + f"{'Home Moneyline: ' + homeMoneyline + new_line}" + f"{'Away Moneyline: ' + awayMoneyline + new_line }" ) except IndexError: - consensus_line = None - - log(f"Consensus Line: {consensus_line}", 1) + return None return consensus_line -class FootballStatsCommands(commands.Cog): - @cog_ext.cog_slash( - name="lines", - description="Get lines for a game", - guild_ids=guild_id_list(), - options=[ - create_option( - name="week", - description="Week of the season", - required=False, - option_type=4, - ), - create_option( - name="year", - description="Year of the season", - required=False, - option_type=4, - ), - create_option( - name="team_name", - description="Name of the team in which to get lines", - required=False, - option_type=3, - ), - ], +class FootballStatsCog(commands.Cog, name="Football Stats Commands"): + @app_commands.command( + name="countdown", description="Get the time until the next game!" ) - async def _lines( + @app_commands.describe( + opponent="Name of opponent to lookup", year="Year of the game to look up" + ) + @app_commands.guilds(GUILD_PROD) + async def countdown( self, - ctx: SlashContext, - week: int = None, - team_name: str = "Nebraska", + interaction: discord.Interaction, + opponent: str = None, year: int = datetime.now().year, - ): - log(f"Gathering info for lines", 0) - games, stats = HuskerSchedule(sport="football", year=year) - del stats - - lines = None - - if week is None: - week = get_current_week(year=year, team=team_name) - - week += 1 # account for week 0 - - log(f"Current week: {week}", 1) - - icon = None - for game in games: - if game.week == week: - lines = get_consensus_line( - team_name=team_name, year=year, week=week - 1 - ) - icon = game.icon - break - - if lines is None: - lines = "TBD" - - embed = build_embed( - title=f"Betting lines for [{team_name.title()}]", - fields=[["Year", year], ["Week", week - 1], ["Lines", lines]], - thumbnail=icon, - ) - await ctx.send(embed=embed) - log(f"Lines completed", 0) - - @cog_ext.cog_slash( - name="countdown", - description="Countdown to the most current or specific Husker game", - guild_ids=guild_id_list(), - options=[ - create_option( - name="team", - description="Name of the opponent you want to search", - required=False, - option_type=3, - ), - create_option( - name="sport", - description="The name of the sport. Uses Huskers.com's naming convention", - required=False, - option_type=3, - ), - ], - ) - async def _countdown( - self, ctx: SlashContext, team: str = None, sport: str = "football" - ): - log(f"Starting countdown", 0) - await ctx.defer() + ) -> None: + logger.info(f"Starting countdown") + # await interaction.original_message.defer() now_cst = datetime.now().astimezone(tz=TZ) - log(f"Now CST is... {now_cst}", 1) + logger.info(f"Now CST is... {now_cst}") - games, stats = HuskerSchedule(sport=sport, year=now_cst.year) + games, stats = HuskerSchedule(year=year) - if not games: - log("No games found!", 1) - return await ctx.send(content="No games found!") + assert games, StatsException("No games found!") + # if not games: + # raise StatsException("No games found!") last_game = len(games) - 1 now_cst_orig = None if games[last_game].game_date_time < now_cst: - log("The current season is over! Looking to next year...", 1) + logger.info("The current season is over! Looking to next year...") del games, stats - games, stats = HuskerSchedule(sport=sport, year=now_cst.year + 1) + games, stats = HuskerSchedule(year=now_cst.year + 1) now_cst_orig = now_cst now_cst = datetime(datetime.now().year + 1, 3, 1).astimezone(tz=TZ) game_compared = None for game in games: - if team: # Specific team - if game.opponent.lower() == team.lower(): + if opponent: # Specific team + if game.opponent.lower() == opponent.lower(): game_compared = game break elif game.game_date_time > now_cst: # Next future game game_compared = game break - log(f"Game compared: {game_compared.opponent}", 1) - del games, sport, team, game, stats + + if game_compared is None: + raise StatsException( + f"Unable to find the {year} {opponent.capitalize()} game!" + ) + + logger.info(f"Game compared: {game_compared.opponent}") + del games, opponent, game, stats if now_cst_orig: dt_game_time_diff = game_compared.game_date_time - now_cst_orig @@ -252,8 +209,8 @@ async def _countdown( ) # datetime object does not have hours or minutes year_days = 0 - log(f"Time diff: {dt_game_time_diff}", 1) - log(f"Time diff mins: {diff_hours_minutes}", 1) + logger.info(f"Time diff: {dt_game_time_diff}") + logger.info(f"Time diff mins: {diff_hours_minutes}") if dt_game_time_diff.days < 0: if calendar.isleap(now_cst.year): @@ -261,319 +218,189 @@ async def _countdown( else: year_days = 365 - embed = build_countdown_embed( - days=dt_game_time_diff.days + year_days, - hours=diff_hours_minutes[0], - minutes=diff_hours_minutes[1], - opponent=game_compared.opponent, - thumbnail=game_compared.icon, - date_time=game_compared.game_date_time, - consensus=get_consensus_line( - team_name=game_compared.opponent, year=now_cst.year - ), - location=game_compared.location, + days = dt_game_time_diff.days + year_days + hours = diff_hours_minutes[0] + minutes = diff_hours_minutes[1] + opponent = game_compared.opponent + thumbnail = game_compared.icon + date_time = game_compared.game_date_time + consensus = get_consensus_line( + team_name=game_compared.opponent, year=now_cst.year ) - await ctx.send(embed=embed) - log(f"Countdown done", 0) - - @cog_ext.cog_slash( - name="compare", - description="Compare two teams stats", - guild_ids=guild_id_list(), - options=[ - create_option( - name="comparison_team", - option_type=3, - required=True, - description="The main team you want to compare stats", - ), - create_option( - name="comparison_against", - option_type=3, - required=True, - description="The team you want to compare stats again", - ), - ], - ) - async def _compare( - self, ctx: SlashContext, comparison_team: str, comparison_against: str - ): - await ctx.defer() + location = game_compared.location - comparison_team = comparison_team.replace(" ", "-") - comparison_against = comparison_against.replace(" ", "-") - - comparison = CompareWinsipedia( - compare=comparison_team, against=comparison_against - ) - embed = build_embed( - title=f"Historical records for [{comparison_team.title()}] vs. [{comparison_against.title()}]", - inline=False, - fields=[ - [ - "Links", - f"[All Games ]({comparison.full_games_url}) | " - f"[{comparison_team.title()}'s Games]({'http://www.winsipedia.com/' + comparison_team.lower()}) | " - f"[{comparison_against.title()}'s Games]({'http://www.winsipedia.com/' + comparison_against.lower()})", - ], - [ - f"{comparison_team.title()}'s Recoard vs. {comparison_against.title()}", - comparison.all_time_record, - ], - [ - f"{comparison_team.title()}'s Largest MOV", - f"{comparison.compare.largest_mov} ({comparison.compare.largest_mov_date})", - ], - [ - f"{comparison_team.title()}'s Longest Win Streak", - f"{comparison.compare.longest_win_streak} ({comparison.compare.largest_win_streak_date})", - ], - [ - f"{comparison_against.title()}'s Largest MOV", - f"{comparison.against.largest_mov} ({comparison.against.largest_mov_date})", + if date_time.hour == DT_TBA_HR and date_time.minute == DT_TBA_MIN: + embed = buildEmbed( + title="Countdown town...", + thumbnail=thumbnail, + fields=[ + dict(name="Opponent", value=opponent), + dict( + name="Scheduled Time", + value=date_time.strftime(DT_OBJ_FORMAT_TBA), + ), + dict(name="Location", value=location), + dict(name="Time Remaining", value=days), + dict( + name="Betting Info", + value=consensus if consensus else "Line TBD", + ), ], - [ - f"{comparison_against.title()}'s Longest Win Streak", - f"{comparison.against.longest_win_streak} ({comparison.against.largest_win_streak_date})", + ) + else: + embed = buildEmbed( + title="Countdown town...", + thumbnail=thumbnail, + fields=[ + dict(name="Opponent", value=opponent), + dict( + name="Scheduled Time", value=date_time.strftime(DT_OBJ_FORMAT) + ), + dict(name="Location", value=location), + dict( + name="Time Remaining", + value=f"{days} days, {hours} hours, and {minutes} minutes", + ), + dict( + name="Betting Info", + value=consensus if consensus else "Line TBD", + ), ], + ) + + await interaction.response.send_message(embed=embed) + logger.info(f"Countdown done") + + @app_commands.command(name="lines", description="Get the betting lines for a game") + @app_commands.guilds(GUILD_PROD) + async def lines( + self, + interaction: discord.Interaction, + team_name: str = "Nebraska", + week: int = None, + year: int = datetime.now().year, + ) -> None: + logger.info(f"Gathering info for lines") + + await interaction.response.defer() + + if week is None: + week = get_current_week(year=year, team=team_name) + + week = 1 if week == 0 else week + logger.info(f"Current week: {week}") + + games, _ = HuskerSchedule(year=year) + del _ + + lines = None + icon = None + + for game in games: + if ( + not team_name.lower() == "nebraska" + and team_name.lower() == game.opponent.lower() + ): # When a team_name is provided + lines = get_consensus_line(team_name=team_name, year=year, week=week) + icon = game.icon + break + elif ( + game.week == week and game.outcome == "" + ): # When a team_name is omitted + lines = get_consensus_line(team_name=team_name, year=year, week=week) + icon = game.icon + break + + lines = "TBD" if lines is None else lines + + embed = buildEmbed( + title=f"Betting lines for [{team_name.title()}]", + fields=[ + dict(name="Year", value=f"{year}"), + dict(name="Week", value=f"{week - 1}"), + dict(name="Lines", value=lines), ], + thumbnail=icon, ) - await ctx.send(embed=embed) - - @cog_ext.cog_slash( - name="schedule", - description="Husker schedule", - guild_ids=guild_id_list(), - options=[ - create_option( - name="year", - required=False, - option_type=4, - description="The year of the schedule you want to search", - ), - create_option( - name="sport", - required=False, - option_type=3, - description="The name of the sport. Uses Huskers.com's naming convention", - ), - ], + + await interaction.followup.send(embed=embed) + logger.info(f"Lines completed") + + @app_commands.command( + name="compare-teams-stats", description="Compare two team's season stats" ) - async def _schedule( - self, - ctx: SlashContext, - year: int = datetime.now().year, - sport: str = "football", - ): - await ctx.defer() - - pages = return_schedule_embeds(year, sport=sport) - await Paginator( - bot=ctx.bot, - ctx=ctx, - pages=pages, - useIndexButton=True, - firstStyle=ButtonStyle.gray, - nextStyle=ButtonStyle.gray, - prevStyle=ButtonStyle.gray, - lastStyle=ButtonStyle.gray, - indexStyle=ButtonStyle.gray, - ).run() - - @cog_ext.cog_slash( - name="teamstats", - description="Historical stats for a team", - guild_ids=guild_id_list(), - options=[ - create_option( - name="team_name", - required=True, - option_type=3, - description="Name of the team you want to search", - ) - ], + @app_commands.describe( + team_for="The main team", + team_against="The team you want to compare the main team against", ) - async def _teamstats(self, ctx: SlashContext, team_name: str): - await ctx.defer() + @app_commands.guilds(GUILD_PROD) + async def compare_team_stats( + self, interaction: discord.Interaction, team_for: str, team_against: str + ) -> None: + logger.info(f"Comparing {team_for} against {team_against} stats") + await interaction.response.defer() + + team_for = team_for.replace(" ", "-") + team_against = team_against.replace(" ", "-") - team = TeamStatsWinsipediaTeam(team_name=team_name) + logger.info("Creating a comparison object") + comparison = CompareWinsipedia(compare=team_for, against=team_against) - embed = build_embed( - title=f"{team_name.title()} Historical Stats", + embed = buildEmbed( + title=f"Historical records for [{team_for.title()}] vs. [{team_against.title()}]", + inline=False, fields=[ - [ - "All Time Record", - f"{team.all_time_record} ({team.all_time_record_rank})", - ], - ["All Time Wins", f"{team.all_time_wins} ({team.all_time_wins_rank})"], - ["Bowl Games", f"{team.bowl_games} ({team.bowl_games_rank})"], - ["Bowl Record", f"{team.bowl_record} ({team.bowl_record_rank})"], - ["Championships", f"{team.championships} ({team.championships_rank})"], - [ - "Conference Championships", - f"{team.conf_championships} ({team.conf_championships_rank})", - ], - [ - "Consensus All American", - f"{team.conf_championships} ({team.conf_championships_rank})", - ], - [ - "Heisman Winners", - f"{team.heisman_winners} ({team.heisman_winners_rank})", - ], - [ - "NFL Draft Picks", - f"{team.nfl_draft_picks} ({team.nfl_draft_picks_rank})", - ], - [ - "Weeks in AP Poll", - f"{team.week_in_ap_poll} ({team.week_in_ap_poll_rank})", - ], + dict( + name="Links", + value="[All Games ]({comparison.full_games_url}) | " + f"[{team_for.title()}'s Games]({'http://www.winsipedia.com/' + team_for.lower()}) | " + f"[{team_against.title()}'s Games]({'http://www.winsipedia.com/' + team_against.lower()})", + ), + dict( + name=f"{team_for.title()}'s Recoard vs. {team_against.title()}", + value=comparison.all_time_record, + ), + dict( + name=f"{team_for.title()}'s Largest MOV", + value=f"{comparison.compare.largest_mov} ({comparison.compare.largest_mov_date})", + ), + dict( + name=f"{team_for.title()}'s Longest Win Streak", + value=f"{comparison.compare.longest_win_streak} ({comparison.compare.largest_win_streak_date})", + ), + dict( + name=f"{team_against.title()}'s Largest MOV", + value=f"{comparison.against.largest_mov} ({comparison.against.largest_mov_date})", + ), + dict( + name=f"{team_against.title()}'s Longest Win Streak", + value=f"{comparison.against.longest_win_streak} ({comparison.against.largest_win_streak_date})", + ), ], - inline=False, ) - await ctx.send(embed=embed) - - @cog_ext.cog_slash( - name="seasonstats", - description="Season stats for a team", - guild_ids=guild_id_list(), - options=[ - create_option( - name="team_name", - required=False, - option_type=3, - description="Name of the team you want to search", - ), - create_option( - name="year", - required=False, - option_type=4, - description="Year of the season you want to search", - ), - ], + await interaction.followup.send(embed=embed) + + @app_commands.command( + name="team-schedule", description="Retrieve the team's schedule" ) - async def _seasonstats( - self, - ctx: SlashContext, - team_name: str = "Nebraska", - year: int = datetime.now().year, - ): - cfb_api = StatsApi(ApiClient(cfbd_config)) + @app_commands.describe(year="The year of the schedule") + @app_commands.guilds(GUILD_PROD) + async def taem_schedule( + self, interaction: discord.Interaction, year: int = datetime.now().year + ) -> None: + await interaction.response.defer() + + pages = collectScheduleEmbeds(year) + view = EmbedPaginatorView( + embeds=pages, original_message=await interaction.original_message() + ) - try: - api_response = cfb_api.get_team_season_stats_with_http_info( - year=year, team=team_name - ) - except ApiException: - return None + await interaction.followup.send(embed=view.initial, view=view) - season_stats = {} - for stat in api_response[0]: - season_stats[stat.stat_name] = stat.stat_value + # TODO team-stats - pages = [ - # Offense - build_embed( - fields=[ - [ - "Offense", - f"**Total Yards**: {season_stats.get('totalYards', 0):,}\n" - f"---\n" - f"**Rushing Attempts**: {season_stats.get('rushingAttempts', 0):,}\n" - f"**Rushing Yards**: {season_stats.get('rushingYards', 0):,}\n" - f"**Rushing TDs**: {season_stats.get('rushingTDs', 0)}\n" - f"---\n" - f"**Pass Attempts**: {season_stats.get('passAttempts', 0):,}\n" - f"**Pass Completions**: {season_stats.get('passCompletions', 0):,}\n" - f"**Passing Yards**: {season_stats.get('netPassingYards', 0):,}\n" - f"**Passing TDs**: {season_stats.get('passingTDs', 0)}\n" - f"---\n" - f"**First Downs**: {season_stats.get('firstDowns', 0):,}\n" - f"**Third Downs**: {season_stats.get('thirdDowns', 0):,}\n" - f"**Third Down Conversions**: {season_stats.get('thirdDownConversions', 0):,}\n" - f"**Fourth Downs**: {season_stats.get('fourthDowns', 0)}\n" - f"**Fourth Down Conversions**: {season_stats.get('fourthDownConversions', 0)}\n" - f"---\n" - f"**Interceptions Thrown**: {season_stats.get('interceptions', 0)}\n" - f"---\n" - f"**Time of Possession**: {pretty_time_delta(season_stats.get('possessionTime', 0))}\n", - ], - ], - ), - # Defense - build_embed( - fields=[ - [ - "Defense", - f"**Tackles for Loss**: {season_stats.get('tacklesForLoss', 0)}\n" - f"**Sacks**: {season_stats.get('sacks', 0)}\n" - f"**Passes Intercepted**: {season_stats.get('passesIntercepted', 0)}\n" - f"**Interception Yards**: {season_stats.get('interceptionYards', 0)}\n" - f"**Interception TDs**: {season_stats.get('interceptionTDs', 0)}\n", - ], - ], - ), - # Special Teams - build_embed( - fields=[ - [ - "Special Teams", - f"**Kick Returns**: {season_stats.get('kickReturns', 0)}\n" - f"**Kick Return Yards**: {season_stats.get('kickReturnYards', 0):,}\n" - f"**Kick Return TDs**: {season_stats.get('kickReturnTDs', 0)}\n" - f"---\n" - f"**Punt Returns**: {season_stats.get('puntReturns', 0)}\n" - f"**Punt Return Yards**: {season_stats.get('puntReturnYards', 0):,}\n" - f"**Punt Return TDs**: {season_stats.get('puntReturnTDs', 0)}\n", - ], - ], - ), - # Penalties - build_embed( - fields=[ - [ - "Penalties", - f"**Penalties**: {season_stats.get('penalties', 0)}\n" - f"**Penalty Yards**: {season_stats.get('penaltyYards', 0):,}\n", - ], - ], - ), - # Fumbles and Turnovers - build_embed( - fields=[ - [ - "Fumbles and Turnovers", - f"**Fumbles Lost**: {season_stats.get('fumblesLost', 0)}\n" - f"**Fumbles Recovered**: {season_stats.get('fumblesRecovered', 0)}\n" - f"**Turnovers**: {season_stats.get('turnovers', 0)}\n", - ], - ], - ), - ] - - content = [ - f"{team_name.title()}'s {year} Offensive Stats", - f"{team_name.title()}'s {year} Defensive Stats", - f"{team_name.title()}'s {year} Special Teams Stats", - f"{team_name.title()}'s {year} Penalties Stats", - f"{team_name.title()}'s {year} Fumble and Turnover Stats", - ] - - await Paginator( - bot=ctx.bot, - ctx=ctx, - pages=pages, - content=content, - useIndexButton=True, - useSelect=False, - firstStyle=ButtonStyle.gray, - nextStyle=ButtonStyle.gray, - prevStyle=ButtonStyle.gray, - lastStyle=ButtonStyle.gray, - indexStyle=ButtonStyle.gray, - ).run() - - -def setup(bot): - bot.add_cog(FootballStatsCommands(bot)) + # TODO season-stats + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(FootballStatsCog(bot), guilds=[discord.Object(id=GUILD_PROD)]) diff --git a/commands/image.py b/commands/image.py index b53de39a..bd14d2c6 100644 --- a/commands/image.py +++ b/commands/image.py @@ -1,316 +1,118 @@ import io +import logging import pathlib import platform import random from os import listdir from os.path import isfile, join +from typing import Optional, Any, Union -import discord -import markovify +import discord.ext.commands import requests import validators -from PIL import Image +from PIL import Image, ImageOps +from discord import app_commands from discord.ext import commands -from discord_slash import cog_ext -from discord_slash.context import SlashContext -from discord_slash.utils.manage_commands import create_option - -import utilities.fryer as fryer -from utilities.constants import ( - CommandError, - HEADERS, - ROLE_ADMIN_PROD, - UserError, - guild_id_list, - make_slowking, -) -from utilities.embed import build_embed -from utilities.mysql import ( - Process_MySQL, + +from helpers.constants import GUILD_PROD, ROLE_ADMIN_PROD, HEADERS +from helpers.embed import buildEmbed +from helpers.fryer import fry_image +from helpers.mysql import ( + processMySQL, sqlCreateImageCommand, sqlDeleteImageCommand, sqlSelectAllImageCommand, sqlSelectImageCommand, ) +from objects.Exceptions import ImageException -image_formats = (".jpg", ".jpeg", ".png", ".gif", ".gifv", ".mp4") - - -def create_img(author: int, image_name: str, image_url: str): - if not validators.url(image_url): - raise UserError( - "Invalid image URL format. The URL must begin with 'http' or 'https'." - ) - - if not any(sub_str in image_url for sub_str in image_formats): - raise UserError( - f"Invalid image URL format. The URL must end with a [{', '.join(image_formats)}] extension." - ) - - try: - Process_MySQL( - query=sqlCreateImageCommand, values=[author, image_name, image_url] - ) - except: # noqa - raise CommandError("Unable to create image command in MySQL database!") +logger = logging.getLogger(__name__) +image_formats = (".jpg", ".jpeg", ".png", ".gif", ".gifv", ".mp4") -def retrieve_img(image_name: str): - try: - return Process_MySQL( - query=sqlSelectImageCommand, values=image_name, fetch="one" - ) - except: # noqa - raise UserError(f"Unable to locate an image command named [{image_name}].") +__all__ = ["ImageCog"] def retrieve_all_img(): try: - return Process_MySQL(query=sqlSelectAllImageCommand, fetch="all") + return processMySQL(query=sqlSelectAllImageCommand, fetch="all") except: # noqa - raise CommandError(f"Unable to retrieve image commands.") + raise ImageException(f"Unable to retrieve image commands.") all_imgs = retrieve_all_img() -image_options = [] -for img in all_imgs: - image_options.append( - create_option( - name=img["img_name"], - description="Custom image command", - required=False, - option_type=1, - ) - ) - - -def is_valid(image): - if image is None: - return False - else: - return True +def retrieve_img(image_name: str) -> Union[None, Any]: + try: + return processMySQL(query=sqlSelectImageCommand, values=image_name, fetch="one") + except: # noqa + raise ImageException(f"Unable to locate an image command named [{image_name}].") -class ImageCommands(commands.Cog): - def __init__(self, bot): - self.bot = bot - @cog_ext.cog_slash( - name="imgcreate", - description="Create an image command", - guild_ids=guild_id_list(), +def create_img(author: int, image_name: str, image_url: str) -> Union[bool, Any]: + assert validators.url(image_url), ImageException( + "Invalid image URL format. The URL must begin with 'http' or 'https'." ) - async def _imgcreate(self, ctx: SlashContext, image_name: str, image_url: str): - if is_valid(retrieve_img(image_name)): - raise UserError("An image with that name already exists. Try again!") - - image_name = image_name.replace(" ", "") - - create_img(ctx.author_id, image_name, image_url) - - embed = build_embed( - title="Create an Image Command", - image=image_url, - fields=[ - [ - "Image Created!", - f"Congratulations, [{ctx.author.mention}] created the [{image_name}] command!", - ] - ], - ) - await ctx.send(embed=embed) - - @cog_ext.cog_slash( - name="imgdelete", - description="Delete image commands you've created", - guild_ids=guild_id_list(), + assert any(sub_str in image_url for sub_str in image_formats), ImageException( + f"Invalid image URL format. The URL must end with a [{', '.join(image_formats)}] extension." ) - async def _imgdelete(self, ctx: SlashContext, image_name: str): - try: - img_author = int(retrieve_img(image_name)["author"]) - except TypeError: - raise UserError(f"Unable to locate image [{image_name}]") - if not is_valid(img_author): - raise UserError(f"Unable to locate image [{image_name}]") - - admin = ctx.guild.get_role(ROLE_ADMIN_PROD) - admin_delete = False - - if admin in ctx.author.roles: - admin_delete = True - elif not ctx.author_id == img_author: - raise UserError( - f"This command was created by [{ctx.guild.get_member(img_author).mention}] and can only be deleted by them" - ) - - try: - if admin_delete: - Process_MySQL( - query=sqlDeleteImageCommand, values=[image_name, str(img_author)] - ) - else: - Process_MySQL( - query=sqlDeleteImageCommand, values=[image_name, str(ctx.author_id)] - ) - except: # noqa - raise CommandError("Unable to delete this image command!") - - embed = build_embed( - title="Delete Image Command", - fields=[["Deleted", f"You have deleted the image command [{image_name}]."]], + try: + processMySQL( + query=sqlCreateImageCommand, values=[author, image_name, image_url] ) - await ctx.send(embed=embed) - - @cog_ext.cog_slash( - name="imglist", - description="Show a list of all available image commands", - guild_ids=guild_id_list(), - ) - async def _imglist(self, ctx: SlashContext): - global all_imgs - all_imgs = retrieve_all_img() - - img_list = [] - for img in all_imgs: - img_list.append(img["img_name"]) - - img_list.sort() - - await ctx.send(", ".join(img_list), hidden=True) - - # pages = [] - # - # for image in all_imgs: - # try: - # author = ctx.guild.get_member(user_id=int(image["author"])).mention - # except: # noqa - # author = "N/A" - # - # created_at = image["created_at"] - # - # pages.append( - # build_embed( - # title=f"Image: {image['img_name']}", - # inline=False, - # image=image["img_url"], - # fields=[ - # ["Command Name", f"`/img img_name:{image['img_name']}`"], - # ["Image URL", f"[URL]({image['img_url']})"], - # ["Author", f"{author}"], - # ["Created At", f"{created_at.strftime(DT_OBJ_FORMAT)}"], - # ], - # ) - # ) - # - # await Paginator( - # bot=ctx.bot, - # ctx=ctx, - # pages=pages, - # useIndexButton=True, - # useSelect=False, - # firstStyle=ButtonStyle.gray, - # nextStyle=ButtonStyle.gray, - # prevStyle=ButtonStyle.gray, - # lastStyle=ButtonStyle.gray, - # indexStyle=ButtonStyle.gray, - # hidden=True, - # ).run() - - @cog_ext.cog_slash( - name="imgrandom", - description="Show a random image", - guild_ids=guild_id_list(), - ) - async def _imgrandom(self, ctx: SlashContext): - global all_imgs - all_imgs = retrieve_all_img() - image = random.choice(all_imgs) + logger.info(f"Image {image_name} sucessfully created!") + return True + except: # noqa + raise ImageException("Unable to create image command in MySQL database!") - author = ctx.guild.get_member(user_id=int(image["author"])) - if author is None: - author = "Unknown" - await ctx.send(content=f"{image['img_url']}") +def retrieve_all_img() -> None: + try: + return processMySQL(query=sqlSelectAllImageCommand, fetch="all") + except: # noqa + raise ImageException(f"Unable to retrieve image commands.") - del image - @cog_ext.cog_slash( - name="img", description="Use an image command", guild_ids=guild_id_list() +class ImageCog(commands.Cog, name="Image Commands"): + group_img = app_commands.Group( + name="img", + description="Get creative with images", + guild_ids=[GUILD_PROD], ) - async def _img(self, ctx: SlashContext, image_name: str): - # TODO Attempt to download image files upon creation in a way that Discord plays nicely - - image = retrieve_img(image_name) - - if not is_valid(image): - raise UserError(f"Unable to locate an image command named [{image_name}].") - - author = ctx.guild.get_member(user_id=int(image["author"])) - if author is None: - author = "Unknown" - await ctx.send( - content=f"{ctx.author.mention} used [{image_name}]: \n{image['img_url']}" - ) - - @cog_ext.cog_slash( - name="slowking", - description="Turn a user into Slow King", - guild_ids=guild_id_list(), - options=[ - create_option( - name="user", - description="User you want to turn into Slow King", - option_type=6, - required=True, - ) - ], + @app_commands.command( + name="deep-fry", + description="Deep fry a picture into a unique creation", ) - async def _slowking(self, ctx: SlashContext, user: discord.Member): - await ctx.defer() - - await ctx.send(file=make_slowking(user)) - - @cog_ext.cog_slash( - name="deepfry", - description="Deep fry an image", - guild_ids=guild_id_list(), - options=[ - create_option( - name="url", - description="The URL of the image you want to deep fry.", - option_type=3, - required=False, - ), - create_option( - name="avatar", - description="The avatar you want to deep fry.", - option_type=6, - required=False, - ), - ], + @app_commands.describe( + source_url="URL of an iamge you want to deepfry", + source_avatar="Avatar of member you want to deefry", ) - async def _deepfry( - self, ctx: SlashContext, url: str = None, avatar: discord.Member = None - ): - if url == avatar: - raise UserError("You must provide either a URL or an avatar!") - - if ( - url is not None - and not validators.url(url) - and not any(sub_str in url for sub_str in image_formats) - ): - raise UserError("You must provide a valid URL!") - - await ctx.defer() - - def load(url): + @app_commands.guilds(GUILD_PROD) + async def deepfry( + self, + interaction: discord.Interaction, + source_url: str = None, + source_avatar: discord.Member = None, + ) -> None: + logger.info("Attempting to create a deep fried image!") + await interaction.response.defer() + + if source_url is not None and source_avatar is not None: + raise ImageException("You can only provide one source!") + + if source_url is not None: + assert validators.url(source_url) and any( + sub_str in source_url for sub_str in image_formats + ), ImageException("You must provide a valid URL!") + + def load_image_from_url(url: str): image_response = requests.get(url=url, stream=True, headers=HEADERS) return Image.open(io.BytesIO(image_response.content)).convert("RGBA") + # TODO Play with these variables to see if we can improve the output emote_amount = random.randrange(1, 6) noise = random.uniform(0.4, 0.65) contrast = random.randrange(1, 99) @@ -318,24 +120,30 @@ def load(url): image = None - if url: - image = load(url) - elif avatar: - image = load(avatar.avatar_url) + if source_url: + logger.info("Loading image from URL") + image = load_image_from_url(source_url) + elif source_avatar: + logger.info("Loading image from member avatar") + image = load_image_from_url(source_avatar.avatar.url) + else: + ImageException("Unknown source used!") - if image is None: - raise CommandError("Unable to load image") + assert image, ImageException("Unable to load image") try: - fried = fryer.fry(image, emote_amount, noise, contrast) + logger.info("Frying loaded image") + fried = fry_image(image, emote_amount, noise, contrast) + logger.info("Adding emotes, noise, and contrast") for layer in range(layers): emote_amount = random.randrange(1, 6) noise = random.uniform(0.4, 0.65) contrast = random.randrange(1, 99) - fried = fryer.fry(fried, emote_amount, noise, contrast) + fried = fry_image(fried, emote_amount, noise, contrast) + logger.info("Saving file") with io.BytesIO() as image_binary: fried.save(image_binary, "PNG") if image_binary.tell() > 8000000: @@ -345,67 +153,213 @@ def load(url): ) image_binary.seek(0) - await ctx.send( + await interaction.followup.send( file=discord.File(fp=image_binary, filename="deepfry.png") ) + image_binary.close() except Exception: - raise CommandError("Something went wrong. Blame my creators.") - - @cog_ext.cog_slash( - name="inspireme", - description="The bot will send you an inspirational message", - guild_ids=guild_id_list(), - options=[ - create_option( - name="person", - description="Person you want to inspire", - required=False, - option_type=6, - ) - ], + raise ImageException("Something went wrong. Blame my creators.") + + @app_commands.command( + name="inspire-me", + description="Get random inspiration", ) - async def _inspireme(self, ctx: SlashContext, person: discord.Member = None): + @app_commands.describe(person="Person you want to inspire") + @app_commands.guilds(GUILD_PROD) + async def inspireme( + self, interaction: discord.Interaction, person: Optional[discord.Member] = None + ) -> None: image = requests.get("https://inspirobot.me/api?generate=true") if person: - try: - await ctx.send( - f"{ctx.author.mention} wants to inspire {person.mention}\n{image.text}" - ) - except: # noqa - await ctx.send(f"{ctx.author} wants to inspire {person}") + await interaction.response.send_message( + f"{interaction.user.mention} wants to inspire {person.mention}\n" + f"{image.text}" + ) else: - await ctx.send(image.text) + await interaction.response.send_message(image.text) - @cog_ext.cog_slash( - name="hypeme", - description="Husk's wise words of wisdom in meme format", - guild_ids=guild_id_list(), + @app_commands.command( + name="slow-king", + description="Crown someone as a Slowking", ) - async def _hypeme(self, ctx: SlashContext): - await ctx.defer() - with open("resources/husk_messages.txt", encoding="UTF-8") as f: - source_data = f.read() + @app_commands.describe(person="Person you want to inspire") + @app_commands.guilds(GUILD_PROD) + async def slowking( + self, interaction: discord.Interaction, person: discord.Member + ) -> None: + await interaction.response.defer() - text_model = markovify.NewlineText(source_data) + try: + avatar_thumbnail = Image.open( + requests.get(person.avatar.url, stream=True).raw + ).convert("RGBA") + except IOError: + logger.exception( + "Unable to create a Slow King avatar for user!", exc_info=True + ) + raise ImageException("Unable to create a Slow King avatar for user!") - output = ( - str(text_model.make_short_sentence(min_chars=20, max_chars=50)) - .lower() - .capitalize() + base_mask = Image.open("resources/images/mask.png").convert("L") + avatar_thumbnail = ImageOps.fit( + avatar_thumbnail, base_mask.size, centering=(0.5, 0.5) ) + avatar_thumbnail.putalpha(base_mask) + + paste_pos = (265, 250) + slowking_filename = "make_slowking.png" + + base_img = Image.open("resources/images/slowking.png").convert("RGBA") + base_img.paste(avatar_thumbnail, paste_pos, avatar_thumbnail) - if not output == "None": - await ctx.send(f'_"{output}"_ - Husk') + base_img.save(f"resources/images/{slowking_filename}", "PNG") + + if "Windows" in platform.platform(): + slowking_path = f"{pathlib.Path(__file__).parent.parent.resolve()}\\resources\\images\\{slowking_filename}" else: - await ctx.send("Unable to generate a Markov chain") + slowking_path = f"{pathlib.Path(__file__).parent.parent.resolve()}/resources/images/{slowking_filename}" - @cog_ext.cog_slash( - name="twos", - description="Send a randomly selected TWOS image", - guild_ids=guild_id_list(), + with open(slowking_path, "rb") as f: + file = discord.File(f) + + await interaction.followup.send(content=person.mention, file=file) + + file.close() + + @group_img.command(name="show", description="Show a server image") + @app_commands.describe(image_name="A keyword for the new image") + async def img_show(self, interaction: discord.Interaction, image_name: str) -> None: + await interaction.response.defer() + + image = retrieve_img(image_name) + + assert image, ImageException( + f"Unable to locate an image command named [{image_name}]." + ) + + author = interaction.guild.get_member(int(image["author"])) + if author is None: + author = "Unknown" + + await interaction.followup.send( + content=f"{interaction.user.mention} used [{image_name}]: \n{image['img_url']}" + ) + + @group_img.command(name="create", description="Create a server image") + @app_commands.describe( + image_name="A keyword for the new image", image_url="A valid URL for the iamge" ) - async def _twos(self, ctx: SlashContext): + async def img_create( + self, interaction: discord.Interaction, image_name: str, image_url: str + ) -> None: + logger.info(f"Attempting to create new server image '{image_name}'") + await interaction.response.defer(ephemeral=True) + + assert not retrieve_img(image_name), ImageException( + "An image with that name already exists. Try again!" + ) + + image_name = image_name.replace(" ", "_") # Replace spaces with undescores + + if not create_img(interaction.user.id, image_name, image_url): + raise ImageException("Unable to create the image in the MySQL database!") + + embed = buildEmbed( + title="Create an Image Command", + image=image_url, + fields=[ + dict( + name="Image Created!", + value=f"Congratulations, [{interaction.user.mention}] created the [{image_name}] command!", + ) + ], + ) + await interaction.followup.send(embed=embed) + + @group_img.command(name="list", description="List all the server images") + async def img_list(self, interaction: discord.Interaction) -> None: + await interaction.response.defer(ephemeral=True) + + global all_imgs + all_imgs = retrieve_all_img() + img_list = [img["img_name"] for img in all_imgs] + img_list.sort() + + await interaction.followup.send( + f"There are {len(img_list)} images listed below:\n{', '.join(img_list)}" + ) + + @group_img.command(name="delete", description="Delete a server image") + @app_commands.describe(image_name="A keyword for the new image") + async def img_delete( + self, interaction: discord.Interaction, image_name: str + ) -> None: + logger.info(f"Attempting to delete '{image_name}'!") + await interaction.response.defer(ephemeral=True) + + try: + img_author = int(retrieve_img(image_name)["author"]) + except TypeError: + raise ImageException(f"Unable to locate image [{image_name}]") + + assert img_author, ImageException(f"Unable to locate image [{image_name}]") + + admin = interaction.guild.get_role(ROLE_ADMIN_PROD) + admin_delete = False + + if admin in interaction.user.roles: + admin_delete = True + elif not interaction.user.id == img_author: + raise ImageException( # TODO Verify img_author works here... + f"This command was created by [{interaction.guild.get_member(int(img_author)).mention}] and can only be deleted by them" + ) + + try: + if admin_delete: + processMySQL( + query=sqlDeleteImageCommand, values=[image_name, str(img_author)] + ) + else: + processMySQL( + query=sqlDeleteImageCommand, + values=[image_name, str(interaction.user.id)], + ) + except: # noqa + raise ImageException("Unable to delete this image command!") + + embed = buildEmbed( + title="Delete Image Command", + fields=[ + dict( + name="Deleted", + value=f"You have deleted the image command [{image_name}].", + ) + ], + ) + await interaction.followup.send(embed=embed) + + @group_img.command(name="random", description="Send a random a server image") + async def img_random(self, interaction: discord.Interaction) -> None: + await interaction.response.defer() + + global all_imgs + all_imgs = retrieve_all_img() + image = random.choice(all_imgs) + + author = interaction.guild.get_member(int(image["author"])) + if author is None: + author = "Unknown" + + await interaction.followup.send(content=f"{image['img_url']}") + + del image + + @app_commands.command(name="twos", description="Random Tunnel Walk of Shame image") + @app_commands.guilds(GUILD_PROD) + async def tunnel_walk(self, interaction: discord.Interaction): + logger.info("Grabbing a random TWOS image") + await interaction.response.defer() + if "Windows" in platform.platform(): twos_path = f"{pathlib.Path(__file__).parent.parent.resolve()}\\resources\\images\\TWOS\\" else: @@ -419,8 +373,9 @@ async def _twos(self, ctx: SlashContext): with open(f"{twos_path}{random.choice(twos_files)}", "rb") as f: file = discord.File(f) - await ctx.send(file=file) + + await interaction.followup.send(file=file) -def setup(bot): - bot.add_cog(ImageCommands(bot)) +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(ImageCog(bot), guilds=[discord.Object(id=GUILD_PROD)]) diff --git a/commands/recruiting.py b/commands/recruiting.py new file mode 100644 index 00000000..db100406 --- /dev/null +++ b/commands/recruiting.py @@ -0,0 +1,829 @@ +import json +import logging +import re +from datetime import datetime +from typing import Union, Any + +import discord.ext.commands +import requests +from bs4 import BeautifulSoup +from discord import app_commands +from discord.ext import commands + +from helpers.constants import ( + CROOT_SEARCH_LIMIT, + DT_FAP_RECRUIT, + GUILD_PROD, + HEADERS, + RECRUIT_STATES, +) +from helpers.embed import buildEmbed, buildRecruitEmbed +from helpers.mysql import ( + processMySQL, + sqlGetIndividualPrediction, + sqlGetPrediction, + sqlInsertPrediction, + sqlTeamIDs, +) +from objects.Exceptions import RecruitException +from objects.Recruits import RecruitInterest, Recruit + +logger = logging.getLogger(__name__) + +croot_search = [] +prediction_search = [] +search_reactions = {"1๏ธโƒฃ": 0, "2๏ธโƒฃ": 1, "3๏ธโƒฃ": 2, "4๏ธโƒฃ": 3, "5๏ธโƒฃ": 4} + +CURRENT_CLASS = datetime.now().year +NO_MORE_PREDS = datetime.now().year + +__all__ = [""] + + +class RecruitListView(discord.ui.View): + """ + The View for each search button 1-5 + """ + + def __init__(self, recruit_search: list[Recruit]) -> None: + super().__init__() + self.croot_search: list[Recruit] = recruit_search + + logger.info("Creating recruit search buttons") + for index, reaction in enumerate(search_reactions): + self.add_item( + discord.ui.Button( + label=reaction, + custom_id=f"search_reaction_{index}", + style=discord.ButtonStyle.gray, + ) + ) + pass + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + await interaction.response.defer() + + reaction_pressed = int(interaction.data["custom_id"][-1]) + + logger.info(f"Croot-bot search button #{reaction_pressed + 1} pressed") + + embed = buildRecruitEmbed(croot_search[reaction_pressed]) + view = createPredictionView(croot_search[reaction_pressed]) + + logger.info("Sending a new recruit embed") + await interaction.edit_original_message(embed=embed, view=view) + + return True + + +class PredictionTeamModal(discord.ui.Modal, title="What school and confidence?"[:45]): + """ + The Modal for receiving text input to search a school + """ + + def __init__(self, recruit: Recruit) -> None: + super().__init__() + self.recruit: Recruit = recruit + + max_len = 44 + prediction_school = discord.ui.TextInput( + label="What is your school prediction?"[:max_len] + ) + prediction_confidence = discord.ui.TextInput( + label="Prediction confidence 1 (low) to 10 (high)"[:max_len] + ) + + async def on_submit(self, interaction: discord.Interaction) -> None: + logger.info("Prediction modal initiated") + await interaction.response.defer(ephemeral=True) + + global user_prediction + user_prediction.school = self.prediction_school + user_prediction.confidence = self.prediction_confidence + self.stop() + + logger.info("Confirming if recruit is already committed to a school") + formatted_team_list = [team.lower() for team in get_teams()] + if user_prediction.school.value.lower() in formatted_team_list: + teamm_index = formatted_team_list.index( + user_prediction.school.value.lower() + ) + team_prediction = get_teams()[teamm_index] + + if self.recruit.committed_school == team_prediction: + raise RecruitException( + f"{self.recruit.name} is already committed to {self.recruit.committed_school}!" + ) + + else: + raise RecruitException( + "Your team choice was not a valid team. Please try again!" + ) + + logger.info("Inserting prediction") + processMySQL( + query=sqlInsertPrediction, + values=( + interaction.user.name, + interaction.user.id, + self.recruit.name, + self.recruit.twofourseven_profile, + self.recruit.year, + user_prediction.school.value, + int(user_prediction.confidence.value), + ), + ) + + await interaction.followup.send( + f"Your prediction of [{self.recruit.name}] to [{str(user_prediction.school.value).capitalize()}] with a [{user_prediction.confidence}] level has been logged!", + ephemeral=True, + ) + logger.info("Prediction was recorded!") + + async def on_error( + self, interaction: discord.Interaction, error: Exception + ) -> None: + raise RecruitException(str(error)) + + +class PredictionView(discord.ui.View): + """ + The View to manage submitting and retrieving predictions + """ + + def __init__(self, recruit: Recruit) -> None: + super().__init__() + self.recruit = recruit + + @discord.ui.button(label="๐Ÿ”ฎ", disabled=True) + async def crystal_ball( + self, interaction: discord.Interaction, button: discord.Button + ) -> None: + logger.info("Starting a crystal ball prediction") + + if ( + self.recruit.committed.lower() + if self.recruit.committed is not None + else None + ) in [ + "signed", + "enrolled", + ]: + raise RecruitException( + "You cannot make predictions on recruits that have been signed or have enrolled in their school.", + ) + + if self.recruit.year < NO_MORE_PREDS: + raise RecruitException( + f"You cannot make predictions on recruits from before the [{NO_MORE_PREDS}] class. [{self.recruit.name}] was in the [{self.recruit.year}] recruiting class.", + ) + + logger.info("Collecting team and confidence predictions") + modal = PredictionTeamModal(self.recruit) + await interaction.response.send_modal(modal) + await modal.wait() + + @discord.ui.button(label="๐Ÿ“œ", disabled=True) + async def scroll(self, interaction: discord.Interaction, button: discord.Button): + logger.info(f"Retrieving predictions for {self.recruit.name}...") + await interaction.response.defer() + + individual_preds = processMySQL( + query=sqlGetIndividualPrediction, + fetch="all", + values=(self.recruit.twofourseven_profile,), + ) + if individual_preds is None: + raise RecruitException("This recruit has no predictions.") + + logger.info(f"Compilining {len(individual_preds)} predictions") + predictions = [] + for index, prediction in enumerate(individual_preds): + try: + prediction_user = interaction.guild.get_member(prediction["user_id"]) + prediction_user = prediction_user.display_name + except: # noqa + prediction_user = prediction["user"] + + if prediction_user is None: + prediction_user = prediction["user"] + + prediction_datetime = prediction["prediction_date"] + if isinstance(prediction_datetime, str): + prediction_datetime = datetime.strptime( + prediction["prediction_date"], DT_FAP_RECRUIT + ) + pred_field = [ + f"{prediction_user}", + f"{prediction['team']} ({prediction['confidence']}) - {prediction_datetime.month}/{prediction_datetime.day}/{prediction_datetime.year}", + ] + + if prediction["correct"] == 1: + pred_field[0] = "โœ… " + pred_field[0] + elif prediction["correct"] == 0: + pred_field[0] = "โŒ " + pred_field[0] + elif prediction["correct"] is None: + pred_field[0] = "โŒ› " + pred_field[0] + + predictions.append(dict(name=pred_field[0], value=pred_field[1])) + + embed = buildEmbed( + title=f"Predictions for {self.recruit.name}", fields=predictions + ) + embed.set_footer(text="โœ… = Correct, โŒ = Wrong, โŒ› = TBD") + + await interaction.followup.send(embed=embed) + + +class UserPrediction: + def __init__( + self, + school: Any, + confidence: Any, + ) -> None: + self.school: discord.ui.TextInput = school + self.confidence: discord.ui.Select = confidence + + +user_prediction: UserPrediction = UserPrediction(None, None) + + +def is_walk_on(soup: BeautifulSoup) -> bool: + icon = soup.find_all(attrs={"class": "icon-walkon"}) + return True if icon else False + + +def is_early_enrolee(soup: BeautifulSoup) -> bool: + icon = soup.find_all(attrs={"class": "icon-time"}) + return True if icon else False + + +def is_early_signee(soup: BeautifulSoup) -> bool: + icon = soup.find_all(attrs={"class": "signee-icon"}) + return True if icon else False + + +def reformat_weight(weight: str) -> str: + try: + int(weight) + except TypeError: + return "N/A" + + return f"{int(weight)} lbs." + + +def reformat_commitment_string(search_player: dict) -> Union[str, None]: + if search_player["HighestRecruitInterestEventType"] == "HardCommit": + return "Hard Commit" + elif ( + search_player["HighestRecruitInterestEventType"] == "OfficialVisit" + or search_player["HighestRecruitInterestEventType"] == "0" + ): + return None + else: + return search_player["HighestRecruitInterestEventType"].strip() + + +def reformat_composite_rating(cur_player: dict) -> str: + if cur_player.get("CompositeRating", None) is None: + return "0" + else: + return f"{cur_player['CompositeRating']:0.4f}" + + +def reformat_height(height: str) -> str: + if height is None: + return "N/A" + + double_apo = '" ' + height = f"{height.replace('-', double_apo)}{double_apo}" + return height + + +def get_team_id(search_player: dict) -> int: + if search_player["CommitedInstitutionTeamImage"] is None: + return 0 + + return int( + search_player["CommitedInstitutionTeamImage"] + .split("/")[-1] + .split("_")[-1] + .split(".")[0] + ) + + +def get_committed_school(all_team_ids: list[dict], team_id: int) -> Union[str, None]: + try: + if team_id > 0: + for entry in all_team_ids: + if team_id == entry[team_id]: + return all_team_ids[team_id]["school"] + else: + return None + except KeyError: + return None + + +def get_cb_experts(soup: BeautifulSoup, team_ids) -> list: + logger.info("Collecting expert crystal ball predictions") + experts = [] + + try: + cbs_long_expert = soup.find_all(attrs={"class": "prediction-list long expert"}) + except: # noqa + return experts + + if len(cbs_long_expert) == 0: + logger.info("Returning single expert") + return experts + + for index, expert in enumerate(cbs_long_expert[0].contents): + logger.info( + f"Searching expert #{index} out of {len(cbs_long_expert[0].contents)}" + ) + try: + expert_name = expert.contents[1].string + predicted_team = None + + if expert.find_all("img", src=True): + predicted_team_id = int( + expert.find_all("img", src=True)[0]["src"] + .split("/")[-1] + .split(".")[0] + ) + try: + predicted_team = ( + team_ids[str(predicted_team_id)] + if predicted_team_id > 0 + else None + ) + except KeyError: + predicted_team = "Unknown Team" + else: + if len(expert.find_all("b", attrs={"class": "question-icon"})) == 1: + predicted_team = "Undecided" + + # If the pick is undecided, it doesn't have a confidence + if predicted_team != "Undecided": + expert_confidence = f"{expert.contents[5].contents[1].text.strip()}, {expert.contents[5].contents[3].text.strip()}" + expert_string = ( + f"{expert_name} picks {predicted_team} ({expert_confidence})" + ) + else: + expert_string = f"{expert_name} is {predicted_team}" + + # I think 247 has some goofiness where there are some instances of "None" making a prediction, so I"m just not going to let those be added on + if expert_name is not None: + experts.append(expert_string) + except: # noqa + continue + + logger.info("Returning list of crystal ball predictions") + return experts + + +def get_cb_predictions(soup: BeautifulSoup) -> list: + logger.info("Getting crystal ball predictions") + crystal_balls = [] + + predictions_header = soup.find_all(attrs={"class": "list-header-item"}) + + if len(predictions_header) == 0: + logger.info("No crystal ball predictions found") + return crystal_balls + + cbs_long = cbs_one = None + + # When there are more than one predicted schools + try: + cbs_long = soup.find_all(attrs={"class": "prediction-list long"}) + except: # noqa + pass + # When there is only one predicted school + try: + cbs_one = soup.find_all(attrs={"class": "prediction-list one"}) + except: # noqa + pass + + if len(cbs_long) > 0: + for index, cb in enumerate(cbs_long[0].contents): + logger.info( + f"Searching long list of predictions #{index} out of {len(cbs_long[0].contents)}" + ) + try: + school_name = cb.contents[3].text.strip() + school_weight = cb.contents[5].text.strip() + school_string = f"{school_name}: {school_weight}" + # If there is an "Undecided" in the list, it won't have a confidence with it + if school_name != "Undecided": + school_confidence = f"{cb.contents[7].contents[1].text.strip()}, {cb.contents[7].contents[3].text.strip()}" + school_string += f"({school_confidence})" + crystal_balls.append(school_string) + except: # noqa + continue + + return crystal_balls + elif len(cbs_one) > 0: + logger.info(f"Searching short list of crystal ball predictions") + single_school = cbs_one[0].contents[1] + single_school_name = single_school.contents[3].text.strip() + single_school_weight = single_school.contents[5].text.strip() + try: + single_school_confidence = f"{single_school.contents[7].contents[1].text.strip()}, {single_school.contents[7].contents[3].text.strip()}" + except: # noqa + single_school_confidence = "" + single_school_string = ( + f"{single_school_name}: {single_school_weight} ({single_school_confidence})" + ) + + crystal_balls.append(single_school_string) + else: + return ["N/A"] + + logger.info("Returning crystal ball results") + return crystal_balls + + +def get_all_time_ranking(soup: BeautifulSoup) -> int: + recruit_rank = soup.find_all( + attrs={"href": "https://247sports.com/Sport/Football/AllTimeRecruitRankings/"} + ) + + try: + ranking = recruit_rank[1].contents[3].text + + if len(recruit_rank) > 1: + return ranking + else: + return 0 + except IndexError: + return 0 + + +def get_national_ranking(cur_player: dict) -> int: + if cur_player["NationalRank"] is None: + return 0 + + return cur_player["NationalRank"] + + +def get_position_ranking(cur_player: dict) -> int: + if cur_player["PositionRank"] is None: + return 0 + + return cur_player["PositionRank"] + + +def get_state_ranking(cur_player: dict) -> str: + if cur_player["StateRank"] is None: + return "0" + + return cur_player["StateRank"] + + +def get_recruit_interests(search_player: dict) -> list[RecruitInterest]: + logger.info("Getting recruit interests") + + reqs = requests.get(url=search_player["RecruitInterestsUrl"], headers=HEADERS) + interests_soup = BeautifulSoup(reqs.content, "html.parser") + interests = interests_soup.find( + "ul", attrs={"class": "recruit-interest-index_lst"} + ).find_all("li", recursive=False) + all_interests = [] + + # Goes through the list of interests and only adds in the ones that are offers + for index, interest in enumerate(interests): + offered = ( + interest.find("div", attrs={"class": "secondary_blk"}) + .find("span", attrs={"class": "offer"}) + .text.split(":")[1] + .strip() + ) + if offered == "Yes": + all_interests.append( + RecruitInterest( + school=interest.find("div", attrs={"class": "first_blk"}) + .find("a") + .text.strip(), + offered=offered, + status=interest.find("div", attrs={"class": "first_blk"}) + .find("span", attrs={"class": "status"}) + .find("span") + .text, + ) + ) + + del reqs, interests, interests_soup + + logger.info("Returning recruit interests") + return all_interests + + +def get_school_type(soup: BeautifulSoup) -> str: + institution_type = soup.find_all(attrs={"data-js": "institution-selector"}) + + if len(institution_type) == 0: + return "High School" + + institution_type = str(institution_type[0].text).strip() + return institution_type + + +def get_state_abbr(cur_player: dict) -> str: + try: + return RECRUIT_STATES[cur_player["Hometown"]["State"]] + except KeyError: + return cur_player["Hometown"]["State"] + + +def get_thumbnail(cur_player: dict) -> Union[None, str]: + if cur_player["DefaultAssetUrl"] == "/.": + return None + else: + return cur_player["DefaultAssetUrl"] + + +def get_twitter_handle(soup: BeautifulSoup) -> str: + twitter = soup.find_all(attrs={"class": "tweets-comp"}) + try: + twitter = twitter[0].attrs["data-username"] + twitter = re.sub(r"[^\w*]+", "", twitter) + return twitter + except: # noqa + return "N/A" + + +def get_teams() -> list[str]: + sql_teams = processMySQL(query=sqlTeamIDs, fetch="all") + teams_list = [t["school"] for t in sql_teams] + return teams_list + + +def get_individual_predictions(user_id: int, recruit): # TODO Figure out the type hint + sql_response = processMySQL( + query=sqlGetPrediction, + values=(user_id, recruit.twofourseven_profile), + fetch="one", # TODO Check on renaming variables + ) + return sql_response + + +def search_result_info(new_search: list[Recruit]) -> str: + logger.info("Building search result string") + result_info = "" + for index, recruit in enumerate(new_search): + if index < CROOT_SEARCH_LIMIT: + result_info += ( + f"{list(search_reactions.keys())[index]}: " + f"{recruit.year} - " + f"{'โญ' * recruit.rating_stars if recruit.rating_stars else 'N/R'} - " + f"{recruit.position} - " + f"{recruit.name}\n" + ) + return result_info + + +def createPredictionView(target_recruit: Recruit) -> PredictionView: + view = PredictionView(target_recruit) + if not target_recruit.committed == "Enrolled": + view.crystal_ball.disabled = False + view.scroll.disabled = False + + return view + + +def buildFootballRecruit(year: int, name: str) -> list[Recruit]: + logger.info("Building Football Recruit object") + + logger.info("Collecting team IDs") + all_team_ids = processMySQL(fetch="all", query=sqlTeamIDs) + name = name.split(" ") + + if len(name) == 1: + logger.info("Searching the single name for first and last name") + _247_search = f"https://247sports.com/Season/{year}-Football/Recruits.json?&Items=15&Page=1&Player.FirstName={name[0]}" + first_name = requests.get(url=_247_search, headers=HEADERS) + first_name = json.loads(first_name.text) + + _247_search = f"https://247sports.com/Season/{year}-Football/Recruits.json?&Items=15&Page=1&Player.LastName={name[0]}" + last_name = requests.get(url=_247_search, headers=HEADERS) + last_name = json.loads(last_name.text) + + search_results = first_name + last_name + elif len(name) == 2: + logger.info("Searching the combined name for first and last name") + _247_search = f"https://247sports.com/Season/{year}-Football/Recruits.json?&Items=15&Page=1&Player.FirstName={name[0]}&Player.LastName={name[1]}" + + search_results = requests.get(url=_247_search, headers=HEADERS) + search_results = json.loads(search_results.text) + else: + raise RecruitException( + f"Error occurred attempting to create 247sports search URL." + ) + + if not search_results: + raise RecruitException( + f"Unable to find [{name[0] if len(name) <= 1 else name[0] + ' ' + name[1]}] in the [{year}] class. Please try again!" + ) + + search_result_players = [] + + for index, search_player in enumerate(search_results): + if index + 1 > CROOT_SEARCH_LIMIT: + logger.info("Stoppping search because search limit reached") + break + + logger.info(f"Compiling search result #{index} of {len(search_results)}") + cur_player = search_player["Player"] + + reqs = requests.get(url=search_player["Player"]["Url"], headers=HEADERS) + soup = BeautifulSoup(reqs.content, "html.parser") + + # Put into separate variables for debugging purposes + # red_shirt + _247_highlights = cur_player.get("Url") + "Videos/" + _247_profile = cur_player.get("Url", None) + bio = cur_player.get("Bio", None) + cb_experts = get_cb_experts(soup, all_team_ids) + cb_predictions = get_cb_predictions(soup) + city = cur_player["Hometown"].get("City", None) + commitment_date = search_player.get("AnnouncementDate", None) + committed = reformat_commitment_string(search_player) + committed_school = get_committed_school( + all_team_ids, get_team_id(search_player) + ) + early_enrollee = is_early_enrolee(soup) + early_signee = is_early_signee(soup) + height = reformat_height(cur_player.get("Height", None)) + key = cur_player.get("Key", None) + name = cur_player.get("FullName", None) + position = cur_player["PrimaryPlayerPosition"].get("Abbreviation", None) + ranking_all_time = get_all_time_ranking(soup) + ranking_national = get_national_ranking(cur_player) + ranking_position = get_position_ranking(cur_player) + ranking_state = get_state_ranking(cur_player) + rating_numerical = reformat_composite_rating(cur_player) + rating_stars = cur_player.get("CompositeStarRating", None) + recruit_interests = get_recruit_interests(search_player) + recruit_interests_url = cur_player.get("RecruitInterestsUrl", None) + school = cur_player["PlayerHighSchool"].get("Name", None) + school_type = get_school_type(soup) + scout_evaluation = cur_player.get("ScoutEvaluation", None) + state = cur_player["Hometown"].get("State", None) + state_abbr = get_state_abbr(cur_player) + team_id = get_team_id(search_player) + thumbnail = get_thumbnail(cur_player) + twitter = get_twitter_handle(soup) + walk_on = is_walk_on(soup) + weight = reformat_weight(cur_player.get("Weight", None)) + year = search_player.get("Year", None) + + search_result_players.append( + Recruit( + twofourseven_highlights=_247_highlights, + twofourseven_profile=_247_profile, + bio=bio, + cb_experts=cb_experts, + cb_predictions=cb_predictions, + city=city, + commitment_date=commitment_date, + committed=committed, + committed_school=committed_school, + early_enrollee=early_enrollee, + early_signee=early_signee, + height=height, + key=key, + name=name, + position=position, + ranking_all_time=ranking_all_time, + ranking_national=ranking_national, + ranking_position=ranking_position, + ranking_state=ranking_state, + rating_numerical=rating_numerical, + rating_stars=rating_stars, + recruit_interests=recruit_interests, + recruit_interests_url=recruit_interests_url, + # red_shirt=None, + school=school, + school_type=school_type, + scout_evaluation=scout_evaluation, + state=state, + state_abbr=state_abbr, + team_id=team_id, + thumbnail=thumbnail, + twitter=twitter, + walk_on=walk_on, + weight=weight, + year=year, + ) + ) + + if index == CROOT_SEARCH_LIMIT - 1: + logger.info("Stopping loop because too many results found") + break + + return search_result_players + + +class RecruitingCog(commands.Cog, name="Recruiting Commands"): + group_recruit = app_commands.Group( + name="predict", + description="Recruiting prediction commands", + guild_ids=[GUILD_PROD], + ) + + @app_commands.command(name="croot-bot", description="Look up a recruit") + @app_commands.describe( + year="The recruit's class year", + search_name="Name of the recruit", + ) + async def croot_bot( + self, interaction: discord.Interaction, year: int, search_name: str + ) -> None: + logger.info(f"Searching for {year} {search_name.capitalize()}") + await interaction.response.defer() + + if len(search_name) == 0: + raise RecruitException( + "A player's first and/or last search_name is required." + ) + + if len(str(year)) == 2: + year += 2000 + # elif len(str(year)) == 1 or len(str(year)) == 3: + elif 1 < len(str(year)) < 4: + raise RecruitException("The search year must be two or four digits long.") + + if year > datetime.now().year + 5: + raise RecruitException( + "The search year must be within five years of the current class." + ) + + if year < 1869: + raise RecruitException( + "The search year must be after the first season of college football--1869." + ) + + logger.info(f"Searching for [{year} {search_name.capitalize()}]") + + global croot_search + croot_search = buildFootballRecruit(year, search_name) + + logger.info(f"Found [{len(croot_search)}] results") + + if len(croot_search) == 1: + embed = buildRecruitEmbed(croot_search[0]) + view = createPredictionView(croot_search[0]) + + await interaction.followup.send(embed=embed, view=view) + else: + result_info = search_result_info(croot_search) + + view = RecruitListView(croot_search) + embed = buildEmbed( + title=f"Search Results for [{year} {search_name.capitalize()}]", + fields=[dict(name="Search Results", value=result_info)], + ) + + await interaction.followup.send(embed=embed, view=view) + + logger.info(f"Sent search results for [{year} {search_name.capitalize()}]") + + @group_recruit.command( + name="submit", description="Submit a prediction for a recruit" + ) + @app_commands.describe( + year="The year of the recruit's recruitting class", + search_name="Name of the recruit", + ) + async def predict_submit( # predict, stats, leaderboard, user + self, interaction: discord.Interaction, year: int, search_name: str + ) -> None: + logger.info(f"Starting a prediction for [{year}] [{search_name}]") + + if not len(search_name.split(" ")) == 2: + raise RecruitException("You can only search by full name.") + + if len(str(year)) == 2: + year += 2000 + + if year > datetime.now().year + 5: + raise RecruitException( + "The search year must be within five years of the current class." + ) + + if year < 1869: + raise RecruitException( + "The search year must be after the first season of college football--1869." + ) + + global prediction_search + prediction_search = buildFootballRecruit(year, search_name) + + modal = PredictionTeamModal(prediction_search[0]) + await interaction.response.send_modal(modal) + await modal.wait() + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(RecruitingCog(bot), guilds=[discord.Object(id=GUILD_PROD)]) diff --git a/commands/reminder.py b/commands/reminder.py index ed23bba3..cc83531e 100644 --- a/commands/reminder.py +++ b/commands/reminder.py @@ -1,164 +1,14 @@ -import asyncio -import re -from datetime import datetime, timedelta - -import discord -import nest_asyncio +import discord.ext.commands from discord.ext import commands -from discord_slash import cog_ext -from discord_slash.context import SlashContext -from discord_slash.utils.manage_commands import create_option - -from objects.Thread import send_reminder -from utilities.constants import ( - CHAN_BANNED, - CommandError, - UserError, - guild_id_list, - pretty_time_delta, -) -from utilities.embed import build_embed -from utilities.mysql import Process_MySQL, sqlRecordTasks - - -class DateTimeStrings: - day = "d" - hour = "h" - minute = "m" - seconds = "s" - - -class ReminderCommands(commands.Cog): - @cog_ext.cog_slash( - name="remindme", - description="Set a reminder", - guild_ids=guild_id_list(), - options=[ - create_option( - name="remind_when", - description="When to send the message", - option_type=3, - required=True, - ), - create_option( - name="message", - description="The message to be sent", - option_type=3, - required=True, - ), - create_option( - name="who", - description="Who to send the reminder to", - option_type=6, - required=False, - ), - create_option( - name="channel", - description="Which channel to send the reminder to", - option_type=7, - required=False, - ), - ], - ) - async def _remindme( - self, - ctx: SlashContext, - remind_when: str, - message: str, - who: discord.member = None, - channel: discord.TextChannel = None, - ): - if who and channel: - raise UserError("You cannot input both a member and channel to remind.") - elif who and not ctx.author == who: - raise UserError("You cannot set reminders for anyone other than yourself!") - elif channel in CHAN_BANNED: - raise ValueError(f"Setting reminders in {channel.mention} is banned!") - - await ctx.defer() - - today = datetime.today() - - def convert_dt_value(dt_item: str, from_when: str): - - if dt_item in from_when: - raw = from_when.split(dt_item)[0] - if raw.isnumeric(): - return int(raw) - else: - try: - findall = re.findall(r"\D", raw)[-1] - return int(raw[raw.find(findall) + 1 :]) - except: # noqa - return 0 - else: - return 0 - - days = convert_dt_value(DateTimeStrings.day, remind_when) - hours = convert_dt_value(DateTimeStrings.hour, remind_when) - minutes = convert_dt_value(DateTimeStrings.minute, remind_when) - seconds = convert_dt_value(DateTimeStrings.seconds, remind_when) - - time_diff = timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds) - - min_timer_allowed = 5 # 60 * 5 - - if time_diff.total_seconds() < min_timer_allowed: - raise UserError( - f"The num_seconds entered is too short! The minimum allowed timer is {min_timer_allowed} seconds." - ) - - try: - raw_when = today + time_diff - except ValueError: - raise UserError("The num_seconds entered is too large!") - - duration = raw_when - today - send_when = today + duration - mysql_author = f"{ctx.author.name}#{ctx.author.discriminator}" - is_open = 1 - - if who: - destination = who - elif channel: - destination = channel - else: - destination = ctx.channel - try: - Process_MySQL( - sqlRecordTasks, - values=( - str(destination.id), - message, - str(send_when), - is_open, - mysql_author, - ), - ) - destination = ctx.channel - except: # noqa - raise CommandError("Error submitting MySQL") +from helpers.constants import GUILD_PROD - nest_asyncio.apply() - asyncio.create_task( - send_reminder( - num_seconds=duration.total_seconds(), - destination=destination, - message=message, - source=ctx.author, - alert_when=str(send_when), - ) - ) - embed = build_embed( - title="Bot Frost Reminders", - description=f"Setting a timer for [{destination.mention}] in [{pretty_time_delta(duration.total_seconds())}]. The timer will go off at [{send_when.strftime('%x %X')}].", - inline=False, - fields=[["Author", ctx.author.mention], ["Message", message]], - ) - await ctx.send(embed=embed) +class ReminderCog(commands.Cog, name="Reminder Commands"): + @commands.command() + async def remindme(self, interaction: discord.Interaction): + await interaction.response.send_message("Not implemented yet!", ephemeral=True) -def setup(bot): - bot.add_cog(ReminderCommands(bot)) +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(ReminderCog(bot), guilds=[discord.Object(id=GUILD_PROD)]) diff --git a/commands/testing.py b/commands/testing.py index a5373a3d..8cdb11d4 100644 --- a/commands/testing.py +++ b/commands/testing.py @@ -1,59 +1,543 @@ -import discord -from discord.ext import commands -from discord_slash import cog_ext -from discord_slash.context import SlashContext -from utilities.constants import guild_id_list -from datetime import datetime +tweet = { + "data": { + "attachments": {}, + "author_id": "15899943", + "conversation_id": "1533075589841702912", + "created_at": "2022-06-04T13:17:58.000Z", + "entities": {}, + "geo": {}, + "id": "1533075589841702912", + "lang": "et", + "possibly_sensitive": False, + "public_metrics": { + "retweet_count": 0, + "reply_count": 0, + "like_count": 0, + "quote_count": 0, + }, + "reply_settings": "everyone", + "source": "Twitter Web App", + "text": "test test", + }, + "includes": { + "users": [ + { + "created_at": "2008-08-19T03:09:46.000Z", + "description": "GBR", + "id": "15899943", + "name": "Aaron", + "profile_image_url": "https://pbs.twimg.com/profile_images/1206047447451086848/GEMbd3wB_normal.jpg", + "protected": False, + "public_metrics": { + "followers_count": 39, + "following_count": 563, + "tweet_count": 1157, + "listed_count": 0, + }, + "url": "", + "username": "ayy_gbr", + "verified": False, + } + ] + }, + "matching_rules": [{"id": "1532102238562402312", "tag": ""}], +} +test_2 = { + "attachments": {}, + "author_id": "15899943", + "context_annotations": [ + { + "domain": { + "id": "46", + "name": "Brand Category", + "description": "Categories within Brand Verticals that narrow down the scope of Brands", + }, + "entity": {"id": "781974596752842752", "name": "Services"}, + }, + { + "domain": { + "id": "47", + "name": "Brand", + "description": "Brands and Companies", + }, + "entity": {"id": "10045225402", "name": "Twitter"}, + }, + ], + "conversation_id": "1533095338705289216", + "created_at": "2022-06-04T14:36:26.000Z", + "entities": { + "urls": [ + { + "start": 53, + "end": 76, + "url": "https://t.co/5csNvIlRFK", + "expanded_url": "https://google.com", + "display_url": "google.com", + "status": 200, + "title": "Google", + "description": "Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for.", + "unwound_url": "https://www.google.com/", + }, + { + "start": 77, + "end": 100, + "url": "https://t.co/MVVdTA7Bvf", + "expanded_url": "https://yahoo.com", + "display_url": "yahoo.com", + "images": [ + { + "url": "https://pbs.twimg.com/news_img/1531799350120448000/jh2TO-QT?format=png&name=orig", + "width": 500, + "height": 500, + }, + { + "url": "https://pbs.twimg.com/news_img/1531799350120448000/jh2TO-QT?format=png&name=150x150", + "width": 150, + "height": 150, + }, + ], + "status": 200, + "title": "Yahoo | Mail, Weather, Search, Politics, News, Finance, Sports & Videos", + "description": "Latest news coverage, email, free stock quotes, live scores and video are just the beginning. Discover more every day at Yahoo!", + "unwound_url": "https://www.yahoo.com/", + }, + { + "start": 101, + "end": 124, + "url": "https://t.co/70oQ1m6IsN", + "expanded_url": "https://bing.com", + "display_url": "bing.com", + "status": 200, + "title": "The beauty that lies below", + "description": "Marovo Lagoon in the Solomon Islands is the larges", + "unwound_url": "https://www.bing.com/?toWww=1&redig=17CEBFF75CE44970A26DED98D0DDA3D3", + }, + ] + }, + "geo": {}, + "id": "1533095338705289216", + "lang": "tr", + "possibly_sensitive": False, + "public_metrics": { + "retweet_count": 0, + "reply_count": 0, + "like_count": 0, + "quote_count": 0, + }, + "reply_settings": "everyone", + "source": "Twitter Web App", + "text": "asdfasf sfsadfsafjsd;lkf Test of a tweet with links. https://t.co/5csNvIlRFK https://t.co/MVVdTA7Bvf https://t.co/70oQ1m6IsN", +} -def log(message: str, level: int): - import datetime +test_3 = { + "data": { + "attachments": { + "media_keys": [ + "3_1533105263263244296", + "3_1533105263825326080", + "3_1533105269856735232", + "3_1533105270150385664", + ] + }, + "author_id": "15899943", + "conversation_id": "1533105294837964800", + "created_at": "2022-06-04T15:16:00.000Z", + "entities": { + "urls": [ + { + "start": 16, + "end": 39, + "url": "https://t.co/kbAC9vW2UE", + "expanded_url": "https://twitter.com/ayy_gbr/status/1533105294837964800/photo/1", + "display_url": "pic.twitter.com/kbAC9vW2UE", + "media_key": "3_1533105263263244296", + }, + { + "start": 16, + "end": 39, + "url": "https://t.co/kbAC9vW2UE", + "expanded_url": "https://twitter.com/ayy_gbr/status/1533105294837964800/photo/1", + "display_url": "pic.twitter.com/kbAC9vW2UE", + "media_key": "3_1533105263825326080", + }, + { + "start": 16, + "end": 39, + "url": "https://t.co/kbAC9vW2UE", + "expanded_url": "https://twitter.com/ayy_gbr/status/1533105294837964800/photo/1", + "display_url": "pic.twitter.com/kbAC9vW2UE", + "media_key": "3_1533105269856735232", + }, + { + "start": 16, + "end": 39, + "url": "https://t.co/kbAC9vW2UE", + "expanded_url": "https://twitter.com/ayy_gbr/status/1533105294837964800/photo/1", + "display_url": "pic.twitter.com/kbAC9vW2UE", + "media_key": "3_1533105270150385664", + }, + ] + }, + "geo": {}, + "id": "1533105294837964800", + "lang": "en", + "possibly_sensitive": False, + "public_metrics": { + "retweet_count": 0, + "reply_count": 0, + "like_count": 0, + "quote_count": 0, + }, + "reply_settings": "everyone", + "source": "Twitter Web App", + "text": "Testing images. https://t.co/kbAC9vW2UE", + }, + "includes": { + "media": [ + { + "height": 1331, + "media_key": "3_1533105263263244296", + "public_metrics": {}, + "type": "photo", + "url": "https://pbs.twimg.com/media/FUavHTwWYAgXHqw.jpg", + "width": 2000, + }, + { + "height": 1331, + "media_key": "3_1533105263825326080", + "public_metrics": {}, + "type": "photo", + "url": "https://pbs.twimg.com/media/FUavHV2XEAAbXRi.jpg", + "width": 2000, + }, + { + "height": 1331, + "media_key": "3_1533105269856735232", + "public_metrics": {}, + "type": "photo", + "url": "https://pbs.twimg.com/media/FUavHsUXEAArMae.jpg", + "width": 2000, + }, + { + "height": 1331, + "media_key": "3_1533105270150385664", + "public_metrics": {}, + "type": "photo", + "url": "https://pbs.twimg.com/media/FUavHtaX0AAoFj6.jpg", + "width": 2000, + }, + ], + "users": [ + { + "created_at": "2008-08-19T03:09:46.000Z", + "description": "GBR", + "id": "15899943", + "name": "Aaron", + "profile_image_url": "https://pbs.twimg.com/profile_images/1206047447451086848/GEMbd3wB_normal.jpg", + "protected": False, + "public_metrics": { + "followers_count": 39, + "following_count": 563, + "tweet_count": 1155, + "listed_count": 0, + }, + "url": "", + "username": "ayy_gbr", + "verified": False, + } + ], + }, + "matching_rules": [{"id": "1532102238562402312", "tag": ""}], +} - if level == 0: - print(f"[{datetime.datetime.now()}] ### Testing: {message}") - elif level == 1: - print(f"[{datetime.datetime.now()}] ### ~~~ Testing: {message}") +test_4 = { + "data": { + "attachments": {}, + "author_id": "538207435", + "conversation_id": "1533117750285049860", + "created_at": "2022-06-04T16:06:36.000Z", + "entities": { + "annotations": [ + { + "start": 118, + "end": 127, + "probability": 0.992, + "type": "Person", + "normalized_text": "Jack Stoll", + }, + { + "start": 158, + "end": 162, + "probability": 0.9615, + "type": "Person", + "normalized_text": "Stoll", + }, + ] + }, + "geo": {}, + "id": "1533118028954624000", + "in_reply_to_user_id": "538207435", + "lang": "en", + "possibly_sensitive": False, + "public_metrics": { + "retweet_count": 0, + "reply_count": 0, + "like_count": 0, + "quote_count": 0, + }, + "referenced_tweets": [{"type": "replied_to", "id": "1533117750285049860"}], + "reply_settings": "everyone", + "source": "TweetDeck", + "text": "Every year, our staff breaks down each position group and highlights players to watch. " + "\\n\\nThe 2018 edition highlighted Jack Stoll as the \xe2\x80\x9cPlayer on the Rise.\\ Stoll was transitioning from a backup to starter while sharpening his ability to receive the ball down the field.", + }, + "includes": { + "users": [ + { + "created_at": "2012-03-27T14:33:45.000Z", + "description": "The official Twitter account of Hail Varsity, the voice of Husker Nation. #GBR | Magazine | Web | Radio | Podcasts", + "entities": { + "url": { + "urls": [ + { + "start": 0, + "end": 23, + "url": "https://t.co/OzX6Gv01oK", + "expanded_url": "http://hailvarsity.com", + "display_url": "hailvarsity.com", + } + ] + }, + "description": { + "hashtags": [{"start": 74, "end": 78, "tag": "GBR"}] + }, + }, + "id": "538207435", + "location": "Lincoln, NE", + "name": "Hail Varsity", + "pinned_tweet_id": "1532014324536967169", + "profile_image_url": "https://pbs.twimg.com/profile_images/994270366330753025/f_Dfdk4M_normal.jpg", + "protected": False, + "public_metrics": { + "followers_count": 40931, + "following_count": 902, + "tweet_count": 54675, + "listed_count": 409, + }, + "url": "https://t.co/OzX6Gv01oK", + "username": "HailVarsity", + "verified": False, + } + ], + "tweets": [ + { + "attachments": {"media_keys": ["3_1533117649718325249"]}, + "author_id": "538207435", + "context_annotations": [ + { + "domain": { + "id": "3", + "name": "TV Shows", + "description": "Television shows from around the world", + }, + "entity": { + "id": "10000611558", + "name": "College Football", + "description": "All the action from NCAA college football.", + }, + }, + { + "domain": {"id": "6", "name": "Sports Event"}, + "entity": { + "id": "1526526336893714432", + "name": "Sint Maarten vs Virgin Islands, U.S.", + }, + }, + { + "domain": { + "id": "11", + "name": "Sport", + "description": "Types of sports, like soccer and basketball", + }, + "entity": { + "id": "689566306014617600", + "name": "American football", + }, + }, + { + "domain": { + "id": "11", + "name": "Sport", + "description": "Types of sports, like soccer and basketball", + }, + "entity": {"id": "733756536430809088", "name": "Soccer"}, + }, + { + "domain": { + "id": "12", + "name": "Sports Team", + "description": "A sports team organization, like Arsenal and the Boston Celtics", + }, + "entity": { + "id": "898300546498445312", + "name": "Nebraska Cornhuskers", + }, + }, + { + "domain": { + "id": "26", + "name": "Sports League", + "description": "", + }, + "entity": { + "id": "731226237394243584", + "name": "International - Soccer", + }, + }, + { + "domain": { + "id": "26", + "name": "Sports League", + "description": "", + }, + "entity": { + "id": "892080380425125890", + "name": "NCAA Football", + "description": "NCAA Men's Football", + }, + }, + { + "domain": { + "id": "26", + "name": "Sports League", + "description": "", + }, + "entity": { + "id": "898300606468702211", + "name": "Big 10 football", + }, + }, + { + "domain": { + "id": "26", + "name": "Sports League", + "description": "", + }, + "entity": { + "id": "1526520711380054016", + "name": "CONCACAF Nations League", + }, + }, + { + "domain": { + "id": "29", + "name": "Events [Entity Service]", + "description": "Entity Service related Events domain", + }, + "entity": { + "id": "1526526336893714432", + "name": "Sint Maarten vs Virgin Islands, U.S.", + }, + }, + { + "domain": { + "id": "43", + "name": "Soccer Match", + "description": "Sports Event specific to Soccer matches", + }, + "entity": { + "id": "1526526336893714432", + "name": "Sint Maarten vs Virgin Islands, U.S.", + }, + }, + { + "domain": { + "id": "157", + "name": "Colleges & Universities", + "description": "Colleges & universities around the world", + }, + "entity": { + "id": "1286377759992766469", + "name": "University of Nebraska\xe2\x80\x93Lincoln", + }, + }, + ], + "conversation_id": "1533117750285049860", + "created_at": "2022-06-04T16:05:30.000Z", + "entities": { + "annotations": [ + { + "start": 151, + "end": 162, + "probability": 0.5645, + "type": "Organization", + "normalized_text": "Hail Varsity", + } + ], + "hashtags": [{"start": 9, "end": 17, "tag": "Huskers"}], + "urls": [ + { + "start": 168, + "end": 191, + "url": "https://t.co/1BGAHOfW4Q", + "expanded_url": "https://twitter.com/HailVarsity/status/1533117750285049860/photo/1", + "display_url": "pic.twitter.com/1BGAHOfW4Q", + "media_key": "3_1533117649718325249", + } + ], + }, + "geo": {}, + "id": "1533117750285049860", + "lang": "en", + "possibly_sensitive": False, + "public_metrics": { + "retweet_count": 0, + "reply_count": 1, + "like_count": 0, + "quote_count": 0, + }, + "reply_settings": "everyone", + "source": "TweetDeck", + "text": "The 2022 #Huskers Football Yearbook will be off to print soon, which has us flipping through the everything-you-need-to-know 2018 preseason edition of Hail Varsity. \xe2\xac\x87\xef\xb8\x8f https://t.co/1BGAHOfW4Q", + } + ], + }, + "matching_rules": [ + {"id": "1532102238562402312", "tag": ""}, + {"id": "1531812826058215424", "tag": ""}, + {"id": "1532096607239430145", "tag": ""}, + ], +} -class TestCommand(commands.Cog): - @cog_ext.cog_slash( - name="test", - description="Test command", - guild_ids=guild_id_list(), - ) - async def _test(self, ctx: SlashContext): - husks_messages = "" - all_history = [] +class TweetUserData(object): + def __init__(self, data): + self.profile_image_url = None + self.name = None + self.username = None + for key in data: + setattr(self, key, data[key]) - await ctx.defer(hidden=True) - print(datetime.now()) +class MyTweet(object): + def __init__(self, tweet_data): + self.data = None + self.includes = None + self.matching_rules = None - for index, channel in enumerate(ctx.guild.channels): - if channel.type == discord.ChannelType.text: - print(f"[{index} / {len(ctx.guild.channels)}] Searching {channel}") - all_history.append(await channel.history(limit=2000).flatten()) + for key in tweet_data: + setattr(self, key, tweet_data[key]) - # if index == 10: - # break - print(datetime.now()) +if __name__ == "__main__": + test = MyTweet(tweet) - history = [value for sublist in all_history for value in sublist] + author = None + print(type(test.includes["users"])) + for user in test.includes["users"]: + if test.data["author_id"] == user["id"]: + author: TweetUserData = TweetUserData(user) + break - for message in history: - if message.author.id == 598039388148203520: - if not str(message.clean_content).startswith("http"): - husks_messages += f"{message.clean_content}\n" - - husks_messages = str(husks_messages).encode("UTF-8", "ignore") - - f = open("husk_messages.txt", "wb") - f.write(husks_messages) - f.close() - - await ctx.send("Done!", hidden=True) - - -def setup(bot): - bot.add_cog(TestCommand(bot)) + print(author) diff --git a/commands/text.py b/commands/text.py index 671749df..d69fee0c 100644 --- a/commands/text.py +++ b/commands/text.py @@ -1,122 +1,312 @@ import json +import logging import random import re from datetime import timedelta -from urllib import parse +from typing import List -import discord +import discord.ext.commands import markovify import requests from bs4 import BeautifulSoup -from dinteractions_Paginator import Paginator +from discord import app_commands, Forbidden, HTTPException from discord.ext import commands -from discord_slash import ComponentContext, cog_ext -from discord_slash.context import SlashContext -from discord_slash.model import ButtonStyle -from discord_slash.utils.manage_commands import create_option -from discord_slash.utils.manage_components import create_actionrow, create_button - -from discord_surveys.survey import Survey -from objects.Weather import WeatherHour, WeatherResponse -from utilities.constants import ( + +from helpers.constants import ( CHAN_BANNED, CHAN_POSSUMS, - CommandError, DT_OPENWEATHER_UTC, + GLOBAL_TIMEOUT, + GUILD_PROD, HEADERS, TZ, US_STATES, - UserError, WEATHER_API_KEY, - guild_id_list, - set_component_key, ) -from utilities.embed import build_embed - -buttons_ud = [ - create_button( - style=ButtonStyle.gray, label="Previous", custom_id="ud_previous", disabled=True - ), - create_button(style=ButtonStyle.gray, label="Next", custom_id="ud_next"), -] - - -def ud_embed(embed_word, embed_meaning, embed_example, embed_contributor): - return build_embed( - title="Urban Dictionary Result", - inline=False, - footer=embed_contributor, - fields=[ - [embed_word, embed_meaning], - ["Example", embed_example], - [ - "Link", - f"https://www.urbandictionary.com/define.php?term={parse.quote(string=embed_word)}", - ], - ], - ) - - -def check_channel_or_message( - check_member: discord.Member, check_message: discord.Message = None -): - if check_message.content == "": - return "" - - if check_message.channel.id in CHAN_BANNED: - return "" +from helpers.embed import buildEmbed +from objects.Exceptions import CommandException, WeatherException +from objects.Paginator import EmbedPaginatorView +from objects.Survey import Survey +from objects.Weather import WeatherResponse, WeatherHour - if check_member.bot: - return "" +logger = logging.getLogger(__name__) - return "\n" + str(check_message.content.capitalize()) +class TextCog(commands.Cog, name="Text Commands"): + @app_commands.command( + name="eightball", description="Ask the Magic 8-Ball a question" + ) + @app_commands.describe(question="The question you want to ask the Magic 8-Ball") + @app_commands.guilds(GUILD_PROD) + async def eightball(self, interaction: discord.Interaction, question: str) -> None: + responses = [ + "As I see it, yes.", + "Ask again later.", + "Better not tell you now.", + "Cannot predict now.", + "Coach V's cigar would like this!", + "Concentrate and ask again.", + "Definitely yes!", + "Donโ€™t count on it...", + "Frosty!", + "Fuck Iowa!", + "It is certain.", + "It is decidedly so.", + "Most likely...", + "My reply is no.", + "My sources say no.", + "Outlook not so good and reply hazy", + "Scott Frost approves!", + "These are the affirmative answers.", + "Try again...", + "Without a doubt.", + "Yes โ€“ definitely!", + "You may rely on it.", + ] -def cleanup_source_data(source_data: str): - regex_strings = [ - r"(<@\d{18}>|<@!\d{18}>|<:\w{1,}:\d{18}>|<#\d{18}>)", # All Discord mentions - r"((Http|Https|http|ftp|https)://|)([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?", # All URLs - ] + reply = random.choice(responses) + embed = buildEmbed( + title="Eight Ball Response", + description="These are all 100% accurate. No exceptions! Unless an answer says anyone other than Nebraska is good.", + fields=[ + dict( + name="Question Asked", + value=question.capitalize(), + ), + dict( + name="Response", + value=reply, + ), + ], + ) + await interaction.response.send_message(embed=embed) - new_source_data = source_data + @app_commands.command( + name="markov", + description="Generate an AI-created message from the server's messages!", + ) + @app_commands.describe( + source_channel="A Discord text channel you want to use as a source", + source_member="A Discord server member you want to use as a source", + ) + @app_commands.guilds(GUILD_PROD) + async def markov( + self, + interaction: discord.Interaction, + source_channel: discord.TextChannel = None, + source_member: discord.Member = None, + ) -> None: + logger.info("Attempting to create a markov chain") + await interaction.response.defer() + + channel_history_limit: int = 1000 + combined_sources: list = [] + message_history: list = [] + source_conent: str = "" + message_channel_history = None + message_member_history = None + + if source_channel is not None: + logger.info("Adding channel to sources") + combined_sources.append(source_channel) + + if source_member is not None: + logger.info("Adding member to sources") + combined_sources.append(source_member) + + def check_message(message: discord.Message) -> str: + if ( + message.channel.id in CHAN_BANNED + or message.author.bot + or message.content == "" + ): + return "" + + return f"\n{message.content.capitalize()}" + + def cleanup_source_conent(check_source_conent: str) -> str: + logger.info("Cleaning source conent") + output = check_source_conent + + regex_discord_http = [ + r"(<@\d{18}>|<@!\d{18}>|<:\w{1,}:\d{18}>|<#\d{18}>)", # All Discord mentions + r"((Http|Https|http|ftp|https)://|)([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?", # All URLs + ] + + for regex in regex_discord_http: + output = re.sub(regex, "", output, flags=re.IGNORECASE) + + regex_new_lines = r"(\r\n|\r|\n){1,}" # All line breaks + output = re.sub(regex_new_lines, "\n", output, flags=re.IGNORECASE) + + regex_multiple_whitespace = r"\s{2,}" + output = re.sub(regex_multiple_whitespace, "", output, flags=re.IGNORECASE) + + logger.info("Source content cleaned") + return output + + if not combined_sources: # Nothing was provided + logger.info("No sources provided") + try: + message_history = [ + message + async for message in interaction.channel.history( + limit=channel_history_limit + ) + ] + except (Forbidden, HTTPException) as e: + logger.exception(f"Unable to collect message history!\n{e}") + raise + + for message in message_history: + source_conent += check_message(message) + logger.info("Compiled message content from current channel") + else: + logger.info("A source was provided") + for source in combined_sources: + if type(source) == discord.Member: + logger.info("Discord member source provided") + message_member_history = [ + message + async for message in interaction.channel.history( + limit=channel_history_limit + ) + ] + for message in message_member_history: + if message.author == source: + source_conent += check_message(message) + logger.info("Discord member source compiled") + elif type(source) == discord.TextChannel: + logger.info("Discord text channel source provided") + message_channel_history = [ + message + async for message in source.history(limit=channel_history_limit) + ] + for message in message_channel_history: + source_conent += check_message(message) + logger.info("Discord text channel source compiled") + else: + logger.exception("Unexpected source type!", exc_info=True) + continue + + if not source_conent == "": + source_conent = cleanup_source_conent(source_conent) + else: + logger.exception( + f"There was not enough information available to make a Markov chain.", + ) + raise + + logger.info("Cleaning up variables") + del ( + combined_sources, + message_channel_history, + message_history, + message_member_history, + source_channel, + source_member, + ) - for regex in regex_strings: - new_source_data = re.sub(regex, "", new_source_data, flags=re.IGNORECASE) + logger.info("Creating a markov chain") + markvov_response = markovify.NewlineText(source_conent, well_formed=True) + logger.info("Creating a markov original_message") + markov_output = markvov_response.make_sentence( + max_overlap_ratio=0.9, max_overlap_total=27, min_words=7, tries=100 + ) - regex_new_line = r"(\r\n|\r|\n){1,}" # All new lines - new_source_data = re.sub(regex_new_line, "\n", new_source_data, flags=re.IGNORECASE) + if markov_output is None: + logger.exception("Markovify failed to create an output!", exc_info=True) + raise + else: + await interaction.edit_original_message(content=markov_output) + logger.info("Markov out sent") - regex_front_new_line = r"^\n" - new_source_data = re.sub( - regex_front_new_line, "", new_source_data, flags=re.IGNORECASE + @app_commands.command( + name="police", + description="Arrest a server member!", ) - - regex_multiple_whitespace = r"\s{2,}" - new_source_data = re.sub( - regex_multiple_whitespace, " ", new_source_data, flags=re.IGNORECASE + @app_commands.describe( + arestee="A Discord member you want to arrest", ) + @app_commands.guilds(GUILD_PROD) + async def police( + self, interaction: discord.Interaction, arestee: discord.Member + ) -> None: + embed = buildEmbed( + title="Wee woo, wee woo!", + fields=[ + dict( + name="Halt!", + value=f"**" + f"๐Ÿšจ NANI ๐Ÿšจ\n" + f"..๐Ÿšจ THE ๐Ÿšจ\n" + f"...๐Ÿšจ FUCK ๐Ÿšจ\n" + f"....๐Ÿšจ DID ๐Ÿšจ\n" + f".....๐Ÿšจ YOU ๐Ÿšจ\n" + f"....๐Ÿšจ JUST ๐Ÿšจ\n" + f"...๐Ÿšจ SAY ๐Ÿšจ\n" + f"..๐Ÿšจ {arestee.mention} ๐Ÿšจ\n" + f"๐Ÿƒโ€โ™€๏ธ๐Ÿ’จ ๐Ÿ”ซ๐Ÿš“๐Ÿ”ซ๐Ÿš“๐Ÿ”ซ๐Ÿš“\n" + f"\n" + f"๐Ÿ‘ฎโ€๐Ÿ“ข Information โ„น provided in the VIP ๐Ÿ‘‘ Room ๐Ÿ† is intended for Husker247 ๐ŸŒฝ๐ŸŽˆ members only โ€ผ๐Ÿ”ซ. Please do not copy โœ and paste ๐Ÿ–จ or summarize this content elsewhereโ€ผ Please try to keep all replies in this thread ๐Ÿงต for Husker247 members only! ๐Ÿšซ โ›” ๐Ÿ‘Ž " + f"๐Ÿ™…โ€โ™€๏ธThanks for your cooperation. ๐Ÿ˜๐Ÿคฉ๐Ÿ˜˜" + f"**", + ) + ], + ) + await interaction.response.send_message(embed=embed) - return new_source_data - + @app_commands.command( + name="possum", + description="The message you want to pass along for the possum", + ) + @app_commands.describe( + message="Share possum droppings for to the server", + ) + @app_commands.guilds(GUILD_PROD) + async def possum(self, interaction: discord.Interaction, message: str) -> None: + assert interaction.channel.id == CHAN_POSSUMS, CommandException( + "You can only use this in the possum droppings channel!" + ) + assert message, CommandException("You cannot have an empty message!") -# noinspection PyUnresolvedReferences -class TextCommands(commands.Cog): - def __init__(self, bot): - self.bot = bot + await interaction.response.defer(ephemeral=True) - class Definition: - def __init__(self, lookup_word, meaning, example, contributor): - self.lookup_word = lookup_word - self.meaning = meaning - self.example = example - self.contributor = contributor + embed = buildEmbed( + title="Possum Droppings", + thumbnail="https://cdn.discordapp.com/attachments/593984711706279937/875162041818693632/unknown.jpeg", + footer="Created by a sneaky possum", + fields=[ + dict( + name="Dropping", + value=message, + ) + ], + ) + chan = await interaction.client.fetch_channel(CHAN_POSSUMS) + await chan.send(embed=embed) + await interaction.followup.send("Possum dropping sent!") - @cog_ext.cog_slash( - name="urbandictionary", + @app_commands.command( + name="urban-dictionary", description="Look up a word on Urban Dictionary", - guild_ids=guild_id_list(), ) - async def _urbandictionary(self, ctx: SlashContext, *, word: str): + @app_commands.describe( + word="The word to look up", + ) + @app_commands.guilds(GUILD_PROD) + async def urban_dictionary( + self, interaction: discord.Interaction, word: str + ) -> None: + await interaction.response.defer() + + class UrbanDictDefinition: + def __init__(self, lookup_word, meaning, example, contributor): + self.lookup_word = lookup_word + self.meaning = meaning + self.example = example + self.contributor = contributor + r = requests.get(f"https://www.urbandictionary.com/define.php?term={word}") soup = BeautifulSoup(r.content, features="html.parser") @@ -125,23 +315,18 @@ async def _urbandictionary(self, ctx: SlashContext, *, word: str): name="div", attrs={"class": re.compile("definition.*")} ) except AttributeError: - raise UserError(f"Unable to find [{word}] in the Urban Dictionary.") + raise CommandException(f"Unable to find [{word}] in the Urban Dictionary.") - del r, soup - - # if len(definitions) == 0: - # raise UserError(f"Unable to find [{word}] in the Urban Dictionary.") + assert definitions, CommandException( + f"Unable to find [{word}] in the Urban Dictionary." + ) - # try: - # del definitions[1] # Word of the day - # except IndexError: - # # pass - # raise UserError(f"Unable to find [{word}] in the Urban Dictionary.") + del r, soup results = [] for definition in definitions: results.append( - self.Definition( + UrbanDictDefinition( lookup_word=definition.contents[0].contents[0].text, meaning=definition.contents[0].contents[1].text, example=definition.contents[0].contents[2].text, @@ -149,338 +334,106 @@ async def _urbandictionary(self, ctx: SlashContext, *, word: str): ) ) - if not results: - raise CommandError(f"No results found for {word}. Try again!") - pages = [] for index, result in enumerate(results): pages.append( - build_embed( + buildEmbed( title=f"Searched for: {result.lookup_word}", description=f"Definition #{index + 1} from Urban Dictionary", fields=[ - ["Meaning", result.meaning], - ["Example", result.example], - ["Contributor", result.contributor], + dict( + name="Meaning", + value=result.meaning, + ), + dict( + name="Example", + value=result.example, + ), + dict( + name="Contributor", + value=result.contributor, + ), ], ) ) - await Paginator( - bot=ctx.bot, - ctx=ctx, - pages=pages, - useIndexButton=True, - useSelect=False, - firstStyle=ButtonStyle.gray, - nextStyle=ButtonStyle.gray, - prevStyle=ButtonStyle.gray, - lastStyle=ButtonStyle.gray, - indexStyle=ButtonStyle.gray, - ).run() - - @cog_ext.cog_slash( - name="vote", - description="Ask the community for their opinion in votes", - guild_ids=guild_id_list(), - options=[ - create_option( - name="query", - description="What to vote on", - option_type=3, - required=True, - ), - create_option( - name="option_a", - description="Option A to vote on", - option_type=3, - required=False, - ), - create_option( - name="option_b", - description="Option b to vote on", - option_type=3, - required=False, - ), - ], - ) - async def _vote( - self, - ctx: SlashContext, - query: str, - option_a: str = "UP VOTE", - option_b: str = "DOWN VOTE", - ): - if (option_a is not None and option_b is None) or ( - option_b is not None and option_a is None - ): - raise UserError("You must provide both options!") - - option_a = str(option_a).upper() - option_b = str(option_b).upper() - - but_a = ButtonStyle.green - but_b = ButtonStyle.red - - key = set_component_key() - buttons_voting = [] - - query = query.capitalize() - if not query.endswith("?"): - query += "?" - - if option_a != "UP VOTE" and option_b != "DOWN VOTE": # Non-standard vote - but_a = but_b = ButtonStyle.gray - - buttons_voting.append( - create_button(custom_id=f"{key}_a", label=option_a, style=but_a) - ) - buttons_voting.append( - create_button(custom_id=f"{key}_b", label=option_b, style=but_b) + view = EmbedPaginatorView( + embeds=pages, original_message=await interaction.original_message() ) + await interaction.edit_original_message(embed=view.initial, view=view) - embed = build_embed( - title=f"Q: {query}", - inline=False, - fields=[ - [buttons_voting[-2]["label"], "0"], - [buttons_voting[-1]["label"], "0"], - ["Voters", "_"], - ], - footer=key, - ) - - await ctx.send( - content="", embed=embed, components=[create_actionrow(*buttons_voting)] - ) - - @cog_ext.cog_slash( - name="markov", - description="Attempts to create a meaningful sentence from old messages", - guild_ids=guild_id_list(), - options=[ - create_option( - name="channel", - description="Discord text channel", - option_type=7, - required=False, - ), - create_option( - name="member", - description="Discord member", - option_type=6, - required=False, - ), - ], + @app_commands.command( + name="survey", + description="Create a survey for the server", ) - async def _markov(self, ctx: SlashContext, channel=None, member=None): - await ctx.defer() - - sources = [] - if channel is not None: - sources.append(channel) - - if member is not None: - sources.append(member) - - source_data = "" - - CHAN_HIST_LIMIT = 1000 - - if not sources: # Uses current channel for source data - compiled_message_history = await ctx.channel.history( - limit=CHAN_HIST_LIMIT - ).flatten() # potential discord vs discord_slash issue - for message in compiled_message_history: - source_data += check_channel_or_message(ctx.author, message) - else: - for source in sources: - if ( - type(source) == discord.Member - ): # Use current channel and source Discord Member - compiled_message_history = await ctx.channel.history( - limit=CHAN_HIST_LIMIT - ).flatten() - for message in compiled_message_history: - if message.author == source: - source_data += check_channel_or_message( - message.author, message - ) - elif type(source) == discord.TextChannel: - compiled_message_history = await source.history( - limit=CHAN_HIST_LIMIT - ).flatten() - for message in compiled_message_history: - source_data += check_channel_or_message(message.author, message) - - if not source_data == "": - source_data = cleanup_source_data(source_data) - else: - await ctx.send( - f"There was not enough information available to make a Markov chain.", - hidden=True, - ) - - chain = markovify.NewlineText(source_data, well_formed=True) - markov_output = chain.make_sentence( - max_overlap_ratio=0.9, max_overlap_total=27, min_words=7, tries=100 - ) - - if markov_output is None: - await ctx.send(f"Creating a Markov chain failed.", hidden=True) - else: - punctuation = ("!", ".", "?", "...") - markov_output += random.choice(punctuation) - await ctx.send(markov_output) - - @cog_ext.cog_slash( - name="possumdroppings", - description="Only the most secret and trustworthy drops", - guild_ids=guild_id_list(), + @app_commands.describe( + question="The question you want to ask", + options="A maximum of three space deliminated set of options; e.g., 'one two three'", + timeout="Number of seconds to run the survey.", ) - async def _possumdroppings(self, ctx: SlashContext, message: str): - await ctx.defer() - - if not ctx.channel_id == CHAN_POSSUMS: - raise UserError( - f"You can only use this command in [{ctx.guild.get_channel(CHAN_POSSUMS).mention}]" - ) - - await ctx.send("Thinking...", delete_after=0) - - embed = build_embed( - title="Possum Droppings", - inline=False, - thumbnail="https://cdn.discordapp.com/attachments/593984711706279937/875162041818693632/unknown.jpeg", - footer="Created by a possum", - fields=[["Droppings", message]], + @app_commands.guilds(GUILD_PROD) + async def survey( + self, + interaction: discord.Interaction, + question: str, + options: str, + timeout: int = GLOBAL_TIMEOUT, + ) -> None: + survey = Survey( + client=interaction.client, + interaction=interaction, + question=question, + options=options, + timeout=timeout, ) - await ctx.send(embed=embed) + await survey.send() - @cog_ext.cog_slash( - name="eightball", - description="Ask the magic 8-ball a question", - guild_ids=guild_id_list(), + @app_commands.command( + name="weather", + description="Show the weather for a given location", ) - async def _eightball(self, ctx: SlashContext, question: str): - eight_ball = [ - "As I see it, yes.", - "Ask again later.", - "Better not tell you now.", - "Cannot predict now.", - "Coach V's cigar would like this!", - "Concentrate and ask again.", - "Definitely yes!", - "Donโ€™t count on it...", - "Frosty!", - "Fuck Iowa!", - "It is certain.", - "It is decidedly so.", - "Most likely...", - "My reply is no.", - "My sources say no.", - "Outlook not so good and reply hazy", - "Scott Frost approves!", - "These are the affirmative answers.", - "Try again...", - "Without a doubt.", - "Yes โ€“ definitely!", - "You may rely on it.", - ] - - random.shuffle(eight_ball) - - embed = build_embed( - title="BotFrost Magic 8-Ball :8ball: says...", - description="These are all 100% accurate. No exceptions! Unless an answer says anyone other than Nebraska is good.", - inline=False, - fields=[ - ["Question", question.capitalize()], - ["Response", random.choice(eight_ball)], - ], - thumbnail="https://i.imgur.com/L5Gpu0z.png", - ) - - await ctx.send(embed=embed) - - @cog_ext.cog_slash( - name="police", description="You are under arrest!", guild_ids=guild_id_list() + @app_commands.describe( + city="The name of the city you are searching", + state="The name of the states the city is in", + country="The two digit abbreviation of the country the state is in", ) - async def _police(self, ctx: SlashContext, arestee: discord.Member): - message = ( - f"**" - f"๐Ÿšจ NANI ๐Ÿšจ\n" - f"..๐Ÿšจ THE ๐Ÿšจ\n" - f"...๐Ÿšจ FUCK ๐Ÿšจ\n" - f"....๐Ÿšจ DID ๐Ÿšจ\n" - f".....๐Ÿšจ YOU ๐Ÿšจ\n" - f"....๐Ÿšจ JUST ๐Ÿšจ\n" - f"...๐Ÿšจ SAY ๐Ÿšจ\n" - f"..๐Ÿšจ {arestee.mention} ๐Ÿšจ\n" - f"๐Ÿƒโ€โ™€๏ธ๐Ÿ’จ ๐Ÿ”ซ๐Ÿš“๐Ÿ”ซ๐Ÿš“๐Ÿ”ซ๐Ÿš“\n" - f"\n" - f"๐Ÿ‘ฎโ€๐Ÿ“ข Information โ„น provided in the VIP ๐Ÿ‘‘ Room ๐Ÿ† is intended for Husker247 ๐ŸŒฝ๐ŸŽˆ members only โ€ผ๐Ÿ”ซ. Please do not copy โœ and paste ๐Ÿ–จ or summarize this content elsewhereโ€ผ Please try to keep all replies in this thread ๐Ÿงต for Husker247 members only! ๐Ÿšซ โ›” ๐Ÿ‘Ž " - f"๐Ÿ™…โ€โ™€๏ธThanks for your cooperation. ๐Ÿ˜๐Ÿคฉ๐Ÿ˜˜" - f"**" - ) + @app_commands.guilds(GUILD_PROD) + async def weather( + self, + interaction: discord.Interaction, + city: str, + state: str, + country: str = "US", + ) -> None: + await interaction.response.defer() - embed = build_embed( - title="Wee woo wee woo!", inline=False, fields=[["Halt!", message]] - ) - await ctx.send(embed=embed) + try: + formatted_state = next( + ( + search_state + for search_state in US_STATES + if ( + search_state["State"].lower() == state.lower() + or search_state["Abbrev"][:-1].lower() == state.lower() + or search_state["Code"].lower() == state.lower() + ) + ), + None, + ) + except StopIteration: + raise WeatherException("Unable to find state. Please try again!") - @cog_ext.cog_slash( - name="weather", - description="Shows the weather for Husker games", - guild_ids=guild_id_list(), - options=[ - create_option( - name="city", - description="City to search for", - option_type=3, - required=True, - ), - create_option( - name="state", - description="State to search. Format is two letter state code. AL, AK, AS, etc.", - option_type=3, - required=True, - ), - create_option( - name="country", - description="Country code", - option_type=3, - required=False, - ), - ], - ) - async def _weather( - self, ctx: SlashContext, city: str, state: str, country: str = "US" - ): def shift_utc_tz(dt, shift): return dt + timedelta(seconds=shift) - if not len(state) == 2: - raise UserError("State input must be the two-digit state code.") - - found = False - for item in US_STATES: - if item.get("Code") == state.upper(): - found = True - break - if not found: - raise UserError( - f"Unable to find the state {state.upper()}. Please try again!" - ) - - weather_url = f"https://api.openweathermap.org/data/2.5/weather?appid={WEATHER_API_KEY}&units=imperial&lang=en&q={city},{state},{country}" + weather_url = f"https://api.openweathermap.org/data/2.5/weather?appid={WEATHER_API_KEY}&units=imperial&lang=en&q={city},{formatted_state['Code']},{country}" response = requests.get(weather_url, headers=HEADERS) j = json.loads(response.content) weather = WeatherResponse(j) if weather.cod == "404": - raise UserError( - f"Unable to find {city.title()}, {state.upper()}. Try again!" + raise WeatherException( + f"Unable to find {city.title()}, {state}. Try again!" ) temp_str = ( @@ -507,7 +460,7 @@ def shift_utc_tz(dt, shift): hourly_url = f"https://api.openweathermap.org/data/2.5/onecall?lat={weather.coord.lat}&lon={weather.coord.lon}&appid={WEATHER_API_KEY}&units=imperial" response = requests.get(hourly_url, headers=HEADERS) j = json.loads(response.content) - hours = [] + hours: List[WeatherHour] = [] for index, item in enumerate(j["hourly"]): hours.append(WeatherHour(item)) if index == 3: @@ -531,143 +484,88 @@ def shift_utc_tz(dt, shift): f"Sunset: {sunset.astimezone(tz=TZ).strftime(DT_OPENWEATHER_UTC)}" ) - embed = build_embed( + embed = buildEmbed( title=f"Weather conditions for {city.title()}, {state.upper()}", - description=f"It is currently {weather.weather[0].main} with {weather.weather[0].description}. {city.title()}, {state.upper()} is located at {weather.coord.lat}, {weather.coord.lon}.", + description=f"It is currently {weather.weather[0].main} with {weather.weather[0].description}. {city.title()}, {state} is located at {weather.coord.lat}, {weather.coord.lon}.", fields=[ - ["Temperature", temp_str], - ["Clouds", f"Coverage: {weather.clouds.all}%"], - ["Wind", wind_str], - ["Temp Next 4 Hours", hour_temp_str], - ["Wind Next 4 Hours", hour_wind_str], - ["Sun", sun_str], + dict( + name="Temperature", + value=temp_str, + ), + dict( + name="Clouds", + value=f"Coverage: {weather.clouds.all}%", + ), + dict( + name="Wind", + value=wind_str, + ), + dict( + name="Temp Next 4 Hours", + value=hour_temp_str, + ), + dict( + name="Wind Next 4 Hours", + value=hour_wind_str, + ), + dict( + name="Sun", + value=sun_str, + ), ], - inline=False, thumbnail=f"https://openweathermap.org/img/wn/{weather.weather[0].icon}@4x.png", ) - await ctx.send(embed=embed) + await interaction.followup.send(embed=embed) - @cog_ext.cog_slash( - name="survey", - description="Create a survey", - guild_ids=guild_id_list(), - options=[ - create_option( - name="question", - description="Question for the survey", - option_type=3, - required=True, - ), - create_option( - name="options", - description="Space delimited option(s) for the survey", - option_type=3, - required=True, - ), - create_option( - name="timeout", - description="How long to keep the survey open. 0 is forever.", - option_type=4, - required=False, - ), - ], - ) - async def _survey( - self, ctx: SlashContext, question: str, options: str, timeout: int = 120 - ): - await Survey( - bot=ctx.bot, ctx=ctx, question=question, options=options, timeout=timeout - ).send() - - @cog_ext.cog_slash( - name="recipes", - description="Gives you recipes ideas", - guild_ids=guild_id_list(), + @app_commands.command( + name="hype-me", + description="Get hype from Husk", ) - async def _recipes(self, ctx: ComponentContext): - r = requests.get(url="https://breakfastapi.fun/") - data = r.json() - import ast - - ingredients = ast.literal_eval(data["Ingredients"]) - - embed = build_embed( - title="Recipe ideas for the lazy chef", - description="Who knows what you'll get? Probably not vegan.", - fields=[ - ["Recipe Name", data["Recipe Name"]], - ["Cooking Time", f"{data['Cook Time (Minutes)']} mins"], - [ - "Ingredients", - f"\n".join(ingredients), - ], - ["Directions", data["Directions"]], - ], - inline=False, + @app_commands.guilds(GUILD_PROD) + async def hypeme(self, interaction: discord.Interaction) -> None: + class Scroll: + def __init__(self, message: str): + self.headder: str = " _______________________\n=(__ ___ __ _)=\n | |\n" + self.message_layer: str = " | |\n" + self.signature: str = "\n | ~*~ Husk |\n" + self.footer: str = " | |\n |__ ___ __ ___|\n=(_______________________)=\n" + self.max_line_len: int = 19 + self.message: str = message + + def compile(self): + new_line: str = "\n" + lines = [ + f" | {str(self.message[i : i + self.max_line_len]).ljust(self.max_line_len, ' ')} |" + for i in range(0, len(self.message), self.max_line_len) + ] + + return f"{self.headder}{new_line.join([line for line in lines])}{self.signature}{self.footer}" + + logger.info("Creating a Husk markov chain") + await interaction.response.defer() + + with open("resources/husk_messages.txt", encoding="UTF-8") as f: + source_data = f.read() + + text_model = markovify.NewlineText(source_data) + + output = ( + str(text_model.make_short_sentence(min_chars=20, max_chars=50)) + .lower() + .capitalize() ) - await ctx.send(embed=embed) - - @commands.Cog.listener() - async def on_component(self, ctx: ComponentContext): - try: # Avoid listening to events that don't apply to the vote command - if "Q:" not in ctx.origin_message.embeds[0].title: - return - except: # noqa - return - - embed = ctx.origin_message.embeds[0] - voters = embed.fields[2].value - voter_name = ctx.author.mention - - key = embed.footer.text - if key not in ctx.component_id: # Avoid over writing other votes - return - - if voter_name in voters: - await ctx.send("You cannot vote more than once!", hidden=True) - return + processed_output = Scroll(output).compile() - query = embed.title.split(": ")[1] - up_vote_count = int(embed.fields[0].value) - up_vote_label = ctx.origin_message.components[0]["components"][0]["label"] - down_vote_count = int(embed.fields[1].value) - down_vote_label = ctx.origin_message.components[0]["components"][1]["label"] - - if voters == "_": - voters = voter_name + if not output == "None": + await interaction.followup.send(f"```\n{processed_output}```") + logger.info("Husk markov chain created!") else: - voters += f", {voter_name}" - - if ctx.component_id == f"{key}_a": - try: - up_vote_count += 1 - except KeyError: - raise CommandError(f"Error modifying [{up_vote_label}]") - elif ctx.component_id == f"{key}_b": - try: - down_vote_count += 1 - except KeyError: - raise CommandError(f"Error modifying [{down_vote_label}]") - - embed = build_embed( - title=f"Q: {query.capitalize()}", - description="Times out after 60 seconds.", - inline=False, - fields=[ - [up_vote_label, str(up_vote_count)], - [down_vote_label, str(down_vote_count)], - ["Voters", voters], - ], - footer=key, - ) - new_buttons = ctx.origin_message.components[0]["components"] - - await ctx.edit_origin( - content="", embed=embed, components=[create_actionrow(*new_buttons)] - ) + await interaction.followup.send( + "Unable to make a Husk Chain!", ephemeral=True + ) -def setup(bot): - bot.add_cog(TextCommands(bot)) +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(TextCog(bot), guilds=[discord.Object(id=GUILD_PROD)]) diff --git a/utilities/constants.py b/helpers/constants.py similarity index 50% rename from utilities/constants.py rename to helpers/constants.py index 5e221a35..f5f42efc 100644 --- a/utilities/constants.py +++ b/helpers/constants.py @@ -1,157 +1,76 @@ -import pathlib +# TODO +# * update and modernize +# TODO +import logging import platform -import random -import string +from typing import Union import discord import pytz -import requests -from PIL import Image -from discord.ext.commands import BucketType -from discord_slash.utils.manage_commands import ( - SlashCommandPermissionType, - create_permission, +from discord import ( + CategoryChannel, + ForumChannel, + PartialMessageable, + StageChannel, + TextChannel, + Thread, + VoiceChannel, ) from dotenv import load_dotenv -from utilities.encryption import decrypt, decrypt_return_data, encrypt, load_key +from helpers.encryption import decrypt, decrypt_return_data, encrypt, load_key +from helpers.misc import loadVarPath +logger = logging.getLogger(__name__) -def log(message: str, level: int): - import datetime - - if level == 0: - print(f"[{datetime.datetime.now()}] ### Constants: {message}") - elif level == 1: - print(f"[{datetime.datetime.now()}] ### ~~~ Constants: {message}") - +logger.info(f"Platform == {platform.platform()}") # Consistent timezone -TZ = pytz.timezone("US/Central") - - -# Global Errors -class CommandError(Exception): - def __init__(self, message): - self.message = message - - -class UserError(Exception): - def __init__(self, message): - self.message = message - - -def pretty_time_delta(seconds): - seconds = int(seconds) - days, seconds = divmod(seconds, 86400) - hours, seconds = divmod(seconds, 3600) - minutes, seconds = divmod(seconds, 60) - if days > 0: - return f"{days:,}d, {hours}h, {minutes}m, and {seconds}s" - elif hours > 0: - return f"{hours}h, {minutes}m, and {seconds}s" - elif minutes > 0: - return f"{minutes}m and {seconds}s" - else: - return f"{seconds}s" - - -log(f"Platform == {platform.platform()}", 0) +TZ = pytz.timezone("CST6CDT") +logger.info(f"Timezone set as {TZ}") # Setting variables location -variables_path = None - -if "Windows" in platform.platform(): - log(f"Windows environment set", 0) - variables_path = pathlib.PurePath( - f"{pathlib.Path(__file__).parent.parent.resolve()}/resources/variables.json" - ) -elif "Linux" in platform.platform(): - log(f"Linux environment set", 0) - variables_path = pathlib.PurePosixPath( - f"{pathlib.Path(__file__).parent.parent.resolve()}/resources/variables.json" - ) - - -def guild_id_list() -> list: - return [GUILD_PROD, 587470195132596224] - - -def set_component_key() -> str: - return "".join( - random.SystemRandom().choice(string.ascii_uppercase + string.digits) - for _ in range(10) - ) - - -def make_slowking(user: discord.Member) -> discord.File: - resize = (225, 225) - - try: - avatar_thumbnail = Image.open( - requests.get(user.avatar_url, stream=True).raw - ).convert("RGBA") - avatar_thumbnail.thumbnail(resize, Image.ANTIALIAS) - # avatar_thumbnail.save("resources/images/avatar_thumbnail.png", "PNG") - except IOError: - raise CommandError("Unable to create a Slow King avatar for user!") - - paste_pos = (250, 250) - make_slowking_filename = "make_slowking.png" - - base_img = Image.open("resources/images/slowking.png").convert("RGBA") - base_img.paste(avatar_thumbnail, paste_pos, avatar_thumbnail) - base_img.save(f"resources/images/{make_slowking_filename}", "PNG") - - if "Windows" in platform.platform(): - slowking_path = f"{pathlib.Path(__file__).parent.parent.resolve()}\\resources\\images\\{make_slowking_filename}" - else: - slowking_path = f"{pathlib.Path(__file__).parent.parent.resolve()}/resources/images/{make_slowking_filename}" - - with open(slowking_path, "rb") as f: - file = discord.File(f) - - return file - - +variables_path = loadVarPath() load_dotenv(dotenv_path=variables_path) +logger.info("Enviroment path loaded") # Decrypt Env file env_file = variables_path key = load_key() +logger.info("Encryption key loaded") -# Save decrypted file +# DEBUGGING Save decrypted file run = False if run: decrypt(env_file, key) encrypt(env_file, key) env_vars = decrypt_return_data(env_file, key) +logger.info("Environment variables loaded") # SSH SSH_HOST = env_vars["ssh_host"] SSH_USERNAME = env_vars["ssh_username"] SSH_PASSWORD = env_vars["ssh_password"] +logger.info("SSH variables loaded") # Imgur IMGUR_CLIENT = env_vars["imgur_client"] IMGUR_SECRET = env_vars["imgur_secret"] - -# Cooldown rates for commands -CD_GLOBAL_RATE = env_vars["global_rate"] -CD_GLOBAL_PER = env_vars["global_per"] -CD_GLOBAL_TYPE = BucketType.user +logger.info("Imgur variables loaded") # Discord Bot Tokens TEST_TOKEN = env_vars["TEST_TOKEN"] PROD_TOKEN = env_vars["DISCORD_TOKEN"] BACKUP_TOKEN = env_vars["BACKUP_TOKEN"] +logger.info("Discord tokens loaded") # SQL information SQL_HOST = env_vars["sqlHost"] SQL_USER = env_vars["sqlUser"] SQL_PASSWD = env_vars["sqlPass"] SQL_DB = env_vars["sqlDb"] +logger.info("MySQL variables loaded") # Reddit Bot Info REDDIT_CLIENT_ID = env_vars["reddit_client_id"] @@ -160,34 +79,42 @@ def make_slowking(user: discord.Member) -> discord.File: # CFBD API Key CFBD_KEY = env_vars["cfbd_api"] +logger.info("CFBD key loaded") -# SSH Information -# SSH_HOST = env_vars["ssh_host"] -# SSH_USER = env_vars["ssh_user"] -# SSH_PW = env_vars["ssh_pw"] +# DEBUG +DEBUGGING_CODE = "Windows" in platform.platform() # Twitter variables -TWITTER_KEY = env_vars["twitter_key"] -TWITTER_SECRET_KEY = env_vars["twitter_secret_key"] -TWITTER_BEARER = env_vars["twitter_bearer"] -TWITTER_TOKEN = env_vars["twitter_token"] -TWITTER_TOKEN_SECRET = env_vars["twitter_token_secret"] TWITTER_HUSKER_MEDIA_LIST_ID = 1307680291285278720 -TWITTER_BLOCK16_SCREENANME = "Block16Omaha" TWITTER_BLOCK16_ID_STR = "457066083" +TWITTER_BLOCK16_SCREENANME = "Block16Omaha" +TWITTER_QUERY_MAX = 512 + +TWITTER_BEARER = env_vars["twitter_bearer"] + +TWITTER_KEY = env_vars["twitter_api_key"] +TWITTER_SECRET_KEY = env_vars["twitter_api_key_secret"] + +TWITTER_TOKEN = env_vars["twitter_access_token"] +TWITTER_TOKEN_SECRET = env_vars["twitter_access_token_secret"] + +TWITTER_V2_CLIENT_ID = env_vars["twitter_v2_client_id"] +TWITTER_V2_CLIENT_SECRET = env_vars["twitter_v2_client_secret"] + +logger.info("Twitter variables loaded") # Weather API WEATHER_API_KEY = env_vars["openweather_key"] +logger.info("Weather API key loaded") del env_vars, env_file, key +logger.info("Deleted environment variables, files, and key") # Headers for `requests` HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0" } - -# Embed titles -EMBED_TITLE_HYPE = "Nebraska Football Hype Squad ๐Ÿ“ˆ โš  โ›”" +logger.info("User-Agengt Header loaded") # Discord Roles ROLE_ADMIN_PROD = 440639061191950336 @@ -212,75 +139,74 @@ def make_slowking(user: discord.Member) -> discord.File: ROLE_RUNZA = 485086088017215500 ROLE_TARMAC = 881546056687583242 ROLE_TIME_OUT = 663881203983843338 +logger.info("Role variables loaded") # Discord Channels -CHAN_BETS = 622581511488667699 +CHAN_ADMIN = 525519594417291284 +CHAN_ADMIN_DOUBLE = 538419127535271946 +CHAN_ANNOUNCEMENT = 651523695214329887 CHAN_BOTLOGS = 458474143403212801 -CHAN_DBL_WAR_ROOM = 538419127535271946 +CHAN_BOT_SPAM = 593984711706279937 CHAN_DISCUSSION_LIVE = 768828614773833768 CHAN_DISCUSSION_STREAMING = 768828705102888980 +CHAN_FOOD = 453994941857923082 CHAN_GENERAL = 440868279150444544 -CHAN_HOF_PROD = 487431877792104470 -CHAN_HOF_TEST = 606655884340232192 +CHAN_HOF = 487431877792104470 +CHAN_HOS = 860686057850798090 CHAN_HYPE_MAX = 682386060264865953 CHAN_HYPE_NO = 682386220072042537 CHAN_HYPE_SOME = 682386133950136333 CHAN_IOWA = 749339421077274664 -CHAN_MINECRAFT_ADMIN = 662110504843739148 -CHAN_NORTH_BOTTTOMS = 620043869504929832 +CHAN_NOBOS = 620043869504929832 CHAN_POLITICS = 504777800100741120 CHAN_POSSUMS = 873645025878233099 -CHAN_RADIO_PROD = 660610967733796902 -CHAN_RADIO_TEST = 595705205069185050 CHAN_RECRUITING = 507520543096832001 -CHAN_RULES = 651523695214329887 -CHAN_SCOTTS_BOTS = 593984711706279937 -CHAN_SHAME = 860686057850798090 -CHAN_TEST_SPAM = 595705205069185047 CHAN_TWITTERVERSE = 636220560010903584 -CHAN_WAR_ROOM = 525519594417291284 -CHAN_FOOD = 453994941857923082 +logger.info("Channel variables loaded") # Game Day Category +CAT_ADMIN = 600530901407105055 CAT_GAMEDAY = 768828439636606996 CAT_GENERAL = 440632687087058944 +CAT_INTRO = 442062321695719434 +logger.info("Channel category variables loaded") CHAN_BANNED = ( + CHAN_ANNOUNCEMENT, + CHAN_ANNOUNCEMENT, CHAN_BOTLOGS, - CHAN_RULES, + CHAN_HOF, + CHAN_HOS, CHAN_POLITICS, - CHAN_MINECRAFT_ADMIN, - CHAN_HOF_PROD, - CHAN_RULES, ) +logger.info("Banned channels loaded") + CHAN_STATS_BANNED = ( - CHAN_DBL_WAR_ROOM, - CHAN_WAR_ROOM, + CHAN_ADMIN, + CHAN_ADMIN_DOUBLE, + CHAN_ANNOUNCEMENT, CHAN_BOTLOGS, - CHAN_HOF_PROD, - CHAN_SHAME, + CHAN_HOF, + CHAN_HOS, ) -CHAN_HYPE_GROUP = (CHAN_HYPE_MAX, CHAN_HYPE_SOME, CHAN_HYPE_NO) - -# Reactions -REACTION_HYPE_MAX = "๐Ÿ“ˆ" -REACTION_HYPE_SOME = "โš " -REACTION_HYPE_NO = "โ›”" +logger.info("Banned channel stats loaded") -REACITON_HYPE_SQUAD = (REACTION_HYPE_MAX, REACTION_HYPE_SOME, REACTION_HYPE_NO) +CHAN_HYPE_GROUP = (CHAN_HYPE_MAX, CHAN_HYPE_SOME, CHAN_HYPE_NO) +logger.info("Hype channels loaded") # Servers/guilds GUILD_PROD = 440632686185414677 GUILD_TEST = 595705205069185045 +logger.info("Guild variable loaded") # Member ID -TEST_BOT_MEMBER = 595705663997476887 -PROD_BOT_MEMBER = 593949013443608596 -TWITTER_BOT_MEMBER = 755193317997674607 -GEE_USER = 189554873778307073 +MEMBER_BOT = 593949013443608596 +MEMBER_GEE = 189554873778307073 +logger.info("Member variables loaded") # Currency CURRENCY_NAME = "Husker Coins" +logger.info("Currency variable loaded") # Bot Info BOT_DISPLAY_NAME = "Bot Frost" @@ -291,21 +217,27 @@ def make_slowking(user: discord.Member) -> discord.File: "These messages are anonymous and there is no way to verify messages are accurate." ) BOT_FOOTER_BOT = "Created by Bot Frost" - -# -CROOT_SEARCH_LIMIT = 5 +logger.info("Bot info variables loaded") # DateTime format +DT_FAP_RECRUIT = "%Y-%m-%d %H:%M:%S" DT_OBJ_FORMAT = "%d %b %Y %I:%M %p %Z" DT_OBJ_FORMAT_TBA = "%d %b %Y" +DT_OPENWEATHER_UTC = "%H:%M:%S %Z" DT_STR_FORMAT = "%b %d %Y %I:%M %p" DT_STR_RECRUIT = "%m/%d/%Y %I:%M:%S %p" -DT_TBA_TIME = "10:58 PM" +DT_TASK_FORMAT = "%Y-%m-%d %H:%M:%S.%f" DT_TBA_HR = 10 DT_TBA_MIN = 58 -DT_TASK_FORMAT = "%Y-%m-%d %H:%M:%S.%f" -DT_TWEET_FORMAT = "%Y-%m-%d %H:%M:%S" -DT_OPENWEATHER_UTC = "%H:%M:%S %Z" +DT_TBA_TIME = "10:58 PM" +DT_TWEET_FORMAT = "%H:%M:%S %d %b %Y" +DT_TWEET_FORMAT_OLD = "%Y-%m-%d %H:%M:%S" +logger.info("Datetime formatting variables loaded") + +GLOBAL_TIMEOUT = 3600 + +CROOT_SEARCH_LIMIT = 5 + # States US_STATES = [ @@ -360,75 +292,112 @@ def make_slowking(user: discord.Member) -> discord.File: {"State": "West Virginia", "Abbrev": "W.Va.", "Code": "WV"}, {"State": "Wisconsin", "Abbrev": "Wis.", "Code": "WI"}, {"State": "Wyoming", "Abbrev": "Wyo.", "Code": "WY"}, - {"State": "Puerto Rico", "Code": "PR"}, + {"State": "Puerto Rico", "Abbrev": "PR", "Code": "PR"}, ] -# Slash command permissions -admin_mod_perms = { - GUILD_PROD: [ - create_permission(ROLE_ADMIN_PROD, SlashCommandPermissionType.ROLE, True), - create_permission(ROLE_MOD_PROD, SlashCommandPermissionType.ROLE, True), - create_permission(ROLE_EVERYONE_PROD, SlashCommandPermissionType.ROLE, False), - ] +RECRUIT_STATES = { + "Alabama": "AL", + "Alaska": "AK", + "American Samoa": "AS", + "Arizona": "AZ", + "Arkansas": "AR", + "British Columbia": "BC", + "California": "CA", + "Colorado": "CO", + "Connecticut": "CT", + "Delaware": "DE", + "District Of Columbia": "DC", + "Florida": "FL", + "Georgia": "GA", + "Hawaii": "HI", + "Idaho": "ID", + "Illinois": "IL", + "Indiana": "IN", + "Iowa": "IA", + "Kansas": "KS", + "Kentucky": "KY", + "Louisiana": "LA", + "Maine": "ME", + "Maryland": "MD", + "Massachusetts": "MA", + "Michigan": "MI", + "Minnesota": "MN", + "Mississippi": "MS", + "Missouri": "MO", + "Montana": "MT", + "Nebraska": "NE", + "Nevada": "NV", + "New Hampshire": "NH", + "New Jersey": "NJ", + "New Mexico": "NM", + "New York": "NY", + "North Carolina": "NC", + "North Dakota": "ND", + "Ohio": "OH", + "Oklahoma": "OK", + "Oregon": "OR", + "Pennsylvania": "PA", + "Rhode Island": "RI", + "South Carolina": "SC", + "South Dakota": "SD", + "Tennessee": "TN", + "Texas": "TX", + "Utah": "UT", + "Vermont": "VT", + "Virginia": "VA", + "Washington": "WA", + "West Virginia": "WV", + "Wisconsin": "WI", + "Wyoming": "WY", } -admin_perms = { - GUILD_PROD: [ - create_permission(ROLE_ADMIN_PROD, SlashCommandPermissionType.ROLE, True), - create_permission(ROLE_EVERYONE_PROD, SlashCommandPermissionType.ROLE, False), - ] +RECRUIT_POSITIONS = { + "APB": "All-Purpose Back", + "ATH": "Athlete", + "CB": "Cornerback", + "DL": "Defensive Lineman", + "DT": "Defensive Tackle", + "DUAL": "Dual-Threat Quarterback", + "Edge": "Edge", + "FB": "Fullback", + "ILB": "Inside Linebacker", + "IOL": "Interior Offensive Lineman", + "K": "Kicker", + "LB": "Linebacker", + "LS": "Long Snapper", + "OC": "Center", + "OG": "Offensive Guard", + "OLB": "Outside Linebacker", + "OT": "Offensive Tackle", + "P": "Punter", + "PRO": "Pro-Style Quarterback", + "QB": "Quarterback", + "RB": "Running Back", + "RET": "Returner", + "S": "Safety", + "SDE": "Strong-Side Defensive End", + "TE": "Tight End", + "WDE": "Weak-Side Defensive End", + "WR": "Wide Receiver", } -# Retired code -# async def change_my_nickname(): -# nicks = ( -# "Bot Frost", -# "Mario Verbotzco", -# "Adrian Botinez", -# "Bot Devaney", -# "Mike Rilbot", -# "Robo Pelini", -# "Devine Ozigbot", -# "Mo Botty", -# "Bot Moos", -# "Bot Diaco", -# "Rahmir Botson", -# "I.M. Bott", -# "Linux Phillips", -# "Dicaprio Bottle", -# "Bryce Botheart", -# "Jobot Chamberlain", -# "Bot Bando", -# "Shawn Botson", -# "Zavier Botts", -# "Jimari Botler", -# "Bot Gunnerson", -# "Nash Botmacher", -# "Botger Craig", -# "Dave RAMington", -# "MarLAN Lucky", -# "Rex Bothead", -# "Nbotukong Suh", -# "Grant Bostrom", -# "Ameer Botdullah", -# "Botinic Raiola", -# "Vince Ferraboto", -# "economybot", -# "NotaBot_Human", -# "psybot", -# "2020: the year of the bot", -# "bottech129", -# "deerebot129" -# ) -# -# try: -# log(f"Attempting to change nickname...") -# await client.user.edit( -# username=random.choice(nicks) -# ) -# log(f"Successfully changed display name") -# except discord.HTTPException as err: -# err_msg = "### Unable to change display name: " + str(err).replace("\n", " ") -# print(err_msg) -# except: # noqa -# log(f"Unknown error!", sys.exc_info()[0]) +DISCORD_CHANNEL_TYPES = Union[ + CategoryChannel, + ForumChannel, + PartialMessageable, + StageChannel, + TextChannel, + Thread, + VoiceChannel, +] +DISCORD_USER_TYPES = Union[discord.Member, discord.User] + +logger.info("Discord Union group variables loaded") + +__all__ = [ + item + for item in globals() + if not item[1].islower() and (not item.startswith("__") or not item.startswith("_")) +] # Get rid of all local variables and imports. + +logger.info(f"{str(__name__).title()} module loaded!") diff --git a/helpers/embed.py b/helpers/embed.py new file mode 100644 index 00000000..78c13026 --- /dev/null +++ b/helpers/embed.py @@ -0,0 +1,356 @@ +import logging +from datetime import datetime +from typing import Union + +import discord +import validators + +# from commands.recruiting import get_faps, get_croot_predictions +from helpers.constants import ( + BOT_DISPLAY_NAME, + BOT_FOOTER_BOT, + BOT_GITHUB_URL, + BOT_ICON_URL, + BOT_THUMBNAIL_URL, + DT_OBJ_FORMAT, + DT_OBJ_FORMAT_TBA, + DT_TWEET_FORMAT, + TZ, + DT_STR_RECRUIT, +) +from helpers.misc import discordURLFormatter +from helpers.mysql import processMySQL, sqlGetCrootPredictions +from objects.Exceptions import CommandException +from objects.Schedule import HuskerSchedule + +logger = logging.getLogger(__name__) +__all__ = [ + "buildEmbed", + "buildRecruitEmbed", + "buildTweetEmbed", + "collectScheduleEmbeds", +] + +# https://discord.com/developers/docs/resources/channel#embed-object-embed-limits +desc_limit = 4096 +embed_max = 6000 +field_value_limit = 1024 +fields_limt = 25 +footer_limit = 2048 +title_limit = name_limit = field_name_limit = 256 + + +def buildEmbed(title: str, **kwargs) -> Union[discord.Embed, None]: + logger.info("Creating a normal embed") + + assert title is not None, CommandException("Title must not be blank!") + + dtNow = datetime.now().astimezone(tz=TZ) # .isoformat() + + if "color" in kwargs.keys(): + if "description" in kwargs.keys(): + e = discord.Embed( + title=title[:title_limit], + description=kwargs["description"][:desc_limit], + color=kwargs["color"], + timestamp=dtNow, + ) + else: + e = discord.Embed( + title=title[:title_limit], color=kwargs["color"], timestamp=dtNow + ) + else: + if "description" in kwargs.keys(): + e = discord.Embed( + title=title[:title_limit], + description=kwargs["description"][:desc_limit], + color=0xD00000, + ) + else: + e = discord.Embed(title=title[:title_limit], color=0xD00000) + + if "footer" in kwargs.keys(): + e.set_footer(text=kwargs.get("footer")[:footer_limit], icon_url=BOT_ICON_URL) + else: + e.set_footer( + text=BOT_FOOTER_BOT[:footer_limit], + icon_url=BOT_ICON_URL, + ) + + if "image" in kwargs.keys() and validators.url(kwargs.get("image")): + e.set_image(url=kwargs.get("image")) + + if "author" in kwargs.keys(): + e.set_author(name=kwargs.get("author")[:name_limit], url="", icon_url="") + else: + e.set_author( + name=BOT_DISPLAY_NAME[:name_limit], url=BOT_GITHUB_URL, icon_url="" + ) + + if "thumbnail" in kwargs.keys() and validators.url(kwargs.get("thumbnail")): + e.set_thumbnail(url=kwargs.get("thumbnail")) + else: + e.set_thumbnail(url=BOT_THUMBNAIL_URL) + + if "fields" in kwargs.keys(): + for index, field in enumerate(kwargs.get("fields")): + if index == fields_limt: + break + + e.add_field( + name=field["name"][:field_name_limit], + value=field["value"][:field_value_limit], + inline=field.get("inline", False), + ) + if len(e) > embed_max: + logger.exception("Embed is too big!", exc_info=True) + raise + else: + logger.info("Returning a normal embed") + return e + + +def buildTweetEmbed( + author_metrics: dict, + name: str, + profile_image_url: str, + source: str, + text: str, + tweet_created_at: datetime, + tweet_id: str, + tweet_metrics: dict, + username: str, + verified: bool, + medias: list = None, + quotes: list = None, + urls: dict = None, +) -> discord.Embed: + embed = buildEmbed( + title="", + fields=[ + dict( + name="Message", + value=text, + ), + dict( + name="Tweet URL", + value=f"https://twitter.com/{username}/status/{tweet_id}", + ), + ], + ) + # b'{"data":{"attachments":{},"author_id":"15899943","context_annotations":[{"domain":{"id":"46","name":"Brand Category","description":"Categories within Brand Verticals that narrow down the scope of Brands"},"entity":{"id":"781974596752842752","name":"Services"}},{"domain":{"id":"47","name":"Brand","description":"Brands and Companies"},"entity":{"id":"10045225402","name":"Twitter"}}],"conversation_id":"1534359106945003520","created_at":"2022-06-08T02:18:12.000Z","entities":{"urls":[{"start":27,"end":50,"url":"https://t.co/FcWRxe2bsw","expanded_url":"https://www.google.com","display_url":"google.com","status":200,"title":"Google","description":"Search the world\'s information, including webpages, images, videos and more. Google has many special features to help you find exactly what you\'re looking for.","unwound_url":"https://www.google.com"},{"start":51,"end":74,"url":"https://t.co/0H5vssRWaK","expanded_url":"http://www.yahoo.com","display_url":"yahoo.com","status":400,"unwound_url":"http://www.yahoo.com"},{"start":75,"end":98,"url":"https://t.co/aSHcc1XbiN","expanded_url":"http://bing.com","display_url":"bing.com","status":200,"title":"The beauty that lies below","description":"Marovo Lagoon in the Solomon Islands is the larges","unwound_url":"http://www.bing.com/"}]},"geo":{},"id":"1534359106945003520","lang":"en","possibly_sensitive":false,"public_metrics":{"retweet_count":0,"reply_count":0,"like_count":0,"quote_count":0},"reply_settings":"everyone","source":"Twitter Web App","text":"Testing tweets with links. https://t.co/FcWRxe2bsw https://t.co/0H5vssRWaK https://t.co/aSHcc1XbiN"},"includes":{"users":[{"created_at":"2008-08-19T03:09:46.000Z","description":"GBR","id":"15899943","name":"Aaron","profile_image_url":"https://pbs.twimg.com/profile_images/1206047447451086848/GEMbd3wB_normal.jpg","protected":false,"public_metrics":{"followers_count":39,"following_count":563,"tweet_count":1154,"listed_count":0},"url":"","username":"ayy_gbr","verified":false}]},"matching_rules":[{"id":"1532102238562402312","tag":""}]}' + if urls.get("urls"): + for url in urls["urls"]: # TODO KeyError is raising + if ( + medias + ): # Avoid duplicating media embeds. Also, there's no "title" field when the URL is from a media embed + if not url.get("media_key"): + logger.info("Skipping url because media_key does not exist") + break + if url["media_key"] in [media.media_key for media in medias]: + logger.info("Skipping duplicate media_key from URLs") + break + + # Quote tweets don't have title in the url item + if not url.get("title"): + break + + embed.add_field( + name="Embeded URL", + value=discordURLFormatter( + display_text=url["title"], url=url["expanded_url"] + ), + ) + + if medias: + for index, item in enumerate(medias): + embed.add_field( + name="Embeded Image", + value=discordURLFormatter(f"Image #{index+1}", item.url), + ) + + if quotes: + for item in quotes: + embed.add_field( + name="Quoted Tweet", + value=item.text, + ) + + embed.set_author( + name=f"{name}{' โ˜‘๏ธ' if verified else ' '}(@{username}) โ€ข Followers: {author_metrics['followers_count']} โ€ข Tweets: {author_metrics['tweet_count']}", + url=f"https://twitter.com/{username}", + icon_url=profile_image_url, + ) + embed.set_footer( + text=f"Sent via {source} at {tweet_created_at.strftime(DT_TWEET_FORMAT)} โ€ข Retweets: {tweet_metrics['retweet_count']} โ€ข Replies: {tweet_metrics['reply_count']} โ€ข Likes: {tweet_metrics['like_count']} โ€ข Quotes: {tweet_metrics['quote_count']}"[ + :footer_limit + ], + ) + embed.set_thumbnail(url=profile_image_url) + return embed + + +def collectScheduleEmbeds(year) -> list[discord.Embed]: + scheduled_games, season_stats = HuskerSchedule(year=year) + + new_line_char = "\n" + embeds = [] + + for game in scheduled_games: + embeds.append( + buildEmbed( + title=f"{game.opponent.title()}", + description=f"Nebraska's {year}'s Record: {season_stats.wins} - {season_stats.losses}", + thumbnail=game.icon, + fields=[ + dict( + name="Opponent", + value=f"{game.ranking + ' ' if game.ranking else ''}{game.opponent}", + ), + dict( + name="Conference Game", value="Yes" if game.conference else "No" + ), + dict( + name="Date/Time", + value=f"{game.game_date_time.strftime(DT_OBJ_FORMAT) if not game.game_date_time.hour == 21 else game.game_date_time.strftime(DT_OBJ_FORMAT_TBA)}{new_line_char}", + ), + dict(name="Location", value=game.location), + dict(name="Outcome", value=game.outcome if game.outcome else "TBD"), + ], + ) + ) + + return embeds + + +def buildRecruitEmbed(recruit) -> discord.Embed: + def get_all_predictions() -> str: + get_croot_preds_response = processMySQL( + query=sqlGetCrootPredictions, + values=recruit.twofourseven_profile, + fetch="all", + ) + + if get_croot_preds_response is None: + return "There are no predictions for this recruit." + else: + prediction_str = f"Team: Percent (Avg Confidence)" + for predictions in get_croot_preds_response: + prediction_str += f"\n{predictions['team']}: {predictions['percent']:.0f}% ({predictions['confidence']:.1f})" + prediction_str += ( + f"\nTotal Predictions: {get_croot_preds_response[0]['total']}" + ) + return prediction_str + + def prettify_predictions(): + pretty = "" + for item in recruit.cb_predictions: + pretty += f"{item}\n" + return pretty + + def prettify_experts(): + pretty = "" + for item in recruit.cb_experts: + pretty += f"{item}\n" + return pretty + + def prettify_offers(): + pretty = "" + for index, item in enumerate(recruit.recruit_interests): + if index > 9: + return pretty + discordURLFormatter( + "View remaining offers...", recruit.recruit_interests_url + ) + + pretty += f"{item.school}{' - ' + item.status if not item.status == 'None' else ''}\n" + + return pretty + + nl = "\n" + embed = buildEmbed( + title=f"{str(recruit.rating_stars) + 'โญ ' if recruit.rating_stars else ''}{recruit.year} {recruit.position}, {recruit.name}", + description=f"{recruit.committed if recruit.committed is not None else ''}" + f"{': ' + recruit.committed_school if recruit.committed_school is not None else ''} " + f"{': ' + str(datetime.strptime(recruit.commitment_date, DT_STR_RECRUIT)) if recruit.commitment_date is not None else ''}", + fields=[ + dict( + name="**Biography**", + value=f"{recruit.city}, {recruit.state}\n" + f"School: {recruit.school}\n" + f"School Type: {recruit.school_type}\n" + f"Height: {recruit.height}\n" + f"Weight: {recruit.weight}\n", + ), + dict( + name="**Social Media**", + value=f"{'[@' + recruit.twitter + '](' + 'https://twitter.com/' + recruit.twitter + ')' if not recruit.twitter == 'N/A' else 'N/A'}", + ), + dict( + name="**Highlights**", + value=f"{'[247Sports](' + recruit.twofourseven_highlights + ')' if recruit.twofourseven_highlights else '247Sports N/A'}", + ), + dict( + name="**Recruit Info**", + value=f"[247Sports Profile]({recruit.twofourseven_profile})\n" + f"Comp. Rating: {recruit.rating_numerical if recruit.rating_numerical else 'N/A'} \n" + f"Nat. Ranking: [{recruit.ranking_national:,}](https://247sports.com/Season/{recruit.year}-Football/CompositeRecruitRankings/?InstitutionGroup" + f"={recruit.school_type.replace(' ', '')})\n" + f"State Ranking: [{recruit.ranking_state}](https://247sports.com/Season/{recruit.year}-Football/CompositeRecruitRankings/?InstitutionGroup={recruit.school_type.replace(' ', '')}&State" + f"={recruit.state_abbr})\n" + f"Pos. Ranking: [{recruit.ranking_position}](https://247sports.com/Season/{recruit.year}-Football/CompositeRecruitRankings/?InstitutionGroup=" + f"{recruit.school_type.replace(' ', '')}&Position={recruit.position})\n" + f"{'All Time Ranking: [' + recruit.ranking_all_time + '](https://247sports.com/Sport/Football/AllTimeRecruitRankings/)' + nl if recruit.ranking_all_time else ''}" + f"{'Early Enrollee' + nl if recruit.early_enrollee else ''}" + f"{'Early Signee' + nl if recruit.early_signee else ''}" + f"{'Walk-On' + nl if recruit.walk_on else ''}", + ), + dict( + name="**Expert Averages**", + value=f"{prettify_predictions() if recruit.cb_predictions else 'N/A'}", + ), + dict( + name="**Lead Expert Picks**", + value=f"{prettify_experts() if recruit.cb_experts else 'N/A'}", + ), + dict( + name="**Offers**", + value=f"{prettify_offers() if recruit.recruit_interests else 'N/A'}", + ), + dict(name="**FAP Predictions**", value=get_all_predictions()), + ], + ) + + # TODO Work on the 'get_croot_predictions' and figure out where it went + # if (recruit.committed.lower() if recruit.committed is not None else None) not in [ + # "signed", + # "enrolled", + # ]: + # if (get_croot_predictions(recruit)) is not None: + # embed.set_footer( + # text=BOT_FOOTER_BOT + # + "\nClick the ๐Ÿ”ฎ to predict what school you think this recruit will commit to." + # "\nClick the ๐Ÿ“œ to get the individual predictions for this recruit." + # ) + # else: + # embed.set_footer( + # text=BOT_FOOTER_BOT + # + "\nClick the ๐Ÿ”ฎ to predict what school you think this recruit will commit to." + # ) + # else: + # if (get_croot_predictions(recruit)) is not None: + # embed.set_footer( + # text=BOT_FOOTER_BOT + # + "\nClick the ๐Ÿ“œ to get the individual predictions for this recruit." + # ) + # else: + # embed.set_footer(text=BOT_FOOTER_BOT) + embed.set_footer(text=BOT_FOOTER_BOT) + + if recruit.thumbnail and not recruit.thumbnail == "/.": + embed.set_thumbnail(url=recruit.thumbnail) + else: + embed.set_thumbnail(url=BOT_THUMBNAIL_URL) + return embed + + +logger.info(f"{str(__name__).title()} module loaded!") diff --git a/utilities/encryption.py b/helpers/encryption.py similarity index 69% rename from utilities/encryption.py rename to helpers/encryption.py index b8f32a78..bd97441b 100644 --- a/utilities/encryption.py +++ b/helpers/encryption.py @@ -1,34 +1,19 @@ import json +import logging import pathlib -import platform from cryptography.fernet import Fernet +logger = logging.getLogger(__name__) -def log(message: str, level: int): - import datetime - - if level == 0: - print(f"[{datetime.datetime.now()}] ### Encryption: {message}") - elif level == 1: - print(f"[{datetime.datetime.now()}] ### ~~~ Encryption: {message}") - - -if "Windows" in platform.platform(): - log(f"Windows encryption key set", 0) - key_path = pathlib.PurePath( - f"{pathlib.Path(__file__).parent.parent.resolve()}/resources/key.key" - ) -elif "Linux" in platform.platform(): - log(f"Windows encryption key set", 0) - key_path = pathlib.PurePosixPath( - f"{pathlib.Path(__file__).parent.parent.resolve()}/resources/key.key" - ) +key_path = pathlib.PurePath( + f"{pathlib.Path(__file__).parent.parent.resolve()}/resources/key.key" +) def write_key(): """ - Generates a image_name and save it into a file + Generates an image_name and save it into a file """ key = Fernet.generate_key() with open(key_path, "wb") as key_file: @@ -85,3 +70,6 @@ def decrypt(filename, key): # write the original file with open(filename, "wb") as file: file.write(decrypted_data) + + +logger.info(f"{str(__name__).title()} module loaded!") diff --git a/utilities/fryer.py b/helpers/fryer.py similarity index 82% rename from utilities/fryer.py rename to helpers/fryer.py index e7e175ec..58b6a016 100644 --- a/utilities/fryer.py +++ b/helpers/fryer.py @@ -1,3 +1,4 @@ +import logging import math import os @@ -6,6 +7,12 @@ from PIL import Image from numpy import random +logger = logging.getLogger(__name__) + +__all__ = ["fry_image"] + +logger.info(f"{str(__name__).title()} module loaded!") + face_cascade = cv2.CascadeClassifier( os.path.join(cv2.data.haarcascades, "haarcascade_frontalface_default.xml") ) @@ -16,8 +23,8 @@ deepfry_path = "resources/deepfry/" -# Pass an image to fry, pretty self explanatory -def fry(image, emote_amount, noise, contrast): +# Pass an image to fry, pretty self-explanatory +def fry_image(image, emote_amount, noise, contrast): gray = numpy.array(image.convert("L")) eye_coods = find_eyes(gray) @@ -99,8 +106,8 @@ def add_chars(image, coords): return image -def add_emotes(image, max): - for i in range(max): +def add_emotes(image, max_emotes): + for i in range(max_emotes): emote = Image.open(random_file(f"{deepfry_path}/emotes/")).convert("RGBA") coord = numpy.random.random(2) * numpy.array([image.width, image.height]) @@ -170,44 +177,6 @@ def bulge(img, f, r, a, h, ior): if isinstance(y_max, type(numpy.array([]))): y_max = y_max[0] - # # array for holding bulged image - # bulged = numpy.copy(img_data) - # time_start = time.time() - # for y in range(y_min, y_max): - # for x in range(x_min, x_max): - # ray = numpy.array([x, y]) - - # # find the magnitude of displacement in the xy plane between the ray and focus - # s = length(ray - f) - - # # if the ray is in the centre of the bulge or beyond the radius it doesn't need to be modified - # if 0 < s < r: - # # slope of the bulge relative to xy plane at (x, y) of the ray - # m = -s / (a * math.sqrt(r ** 2 - s ** 2)) - - # # find the angle between the ray and the normal of the bulge - # theta = numpy.pi / 2 + numpy.arctan(1 / m) - - # # find the magnitude of the angle between xy plane and refracted ray using snell's law - # # s >= 0 -> m <= 0 -> arctan(-1/m) > 0, but ray is below xy plane so we want a negative angle - # # arctan(-1/m) is therefore negated - # phi = numpy.abs(numpy.arctan(1 / m) - numpy.arcsin(numpy.sin(theta) / ior)) - - # # find length the ray travels in xy plane before hitting z=0 - # k = (h + (math.sqrt(r ** 2 - s ** 2) / a)) / numpy.sin(phi) - - # # find intersection point - # intersect = ray + (normalise(f - ray)) * k - - # # assign pixel with ray's coordinates the colour of pixel at intersection - # if 0 < intersect[0] < width - 1 and 0 < intersect[1] < height - 1: - # bulged[y][x] = img_data[int(intersect[1])][int(intersect[0])] - # else: - # bulged[y][x] = [0, 0, 0, 0] - # else: - # bulged[y][x] = img_data[y][x] - # time_end = time.time() - f_new = f bulge_square = numpy.copy(img_data) # img_data[x_min:x_max, y_min:y_max] @@ -221,7 +190,7 @@ def bulge(img, f, r, a, h, ior): # following is equivalent to m = -s / (a * math.sqrt(r ** 2 - s ** 2)) bulge_m = numpy.copy(bulge_s) bulge_m[circle] = (-1 * bulge_m[circle]) / ( - a * numpy.sqrt(r ** 2 - bulge_m[circle] ** 2) + a * numpy.sqrt(r**2 - bulge_m[circle] ** 2) ) # following is equivalent to theta = numpy.pi / 2 + numpy.arctan(1 / m) @@ -237,7 +206,7 @@ def bulge(img, f, r, a, h, ior): # following is equivalent to k = (h + (math.sqrt(r ** 2 - s ** 2) / a)) / numpy.sin(phi) bulge_k = numpy.copy(bulge_s) - bulge_k[circle] = (h + (numpy.sqrt(r ** 2 - bulge_s[circle] ** 2) / a)) / numpy.sin( + bulge_k[circle] = (h + (numpy.sqrt(r**2 - bulge_s[circle] ** 2) / a)) / numpy.sin( bulge_phi[circle] ) diff --git a/helpers/misc.py b/helpers/misc.py new file mode 100644 index 00000000..c7a431a9 --- /dev/null +++ b/helpers/misc.py @@ -0,0 +1,61 @@ +import logging +import pathlib +import platform +import random +import string + +from objects.Exceptions import CommandException + +logger = logging.getLogger(__name__) + +__all__ = [ + "createComponentKey", + "discordURLFormatter", + "formatPrettyTimeDelta", + "loadVarPath", +] + + +def formatPrettyTimeDelta(seconds) -> str: + seconds = int(seconds) + days, seconds = divmod(seconds, 86400) + hours, seconds = divmod(seconds, 3600) + minutes, seconds = divmod(seconds, 60) + if days > 0: + return f"{days:,}D {hours}H, {minutes}M, and {seconds}S" + elif hours > 0: + return f"{hours}H, {minutes}M, and {seconds}S" + elif minutes > 0: + return f"{minutes}M and {seconds}S" + else: + return f"{seconds}S" + + +def loadVarPath() -> [str, CommandException]: + myPlatform = platform.platform() + if "Windows" in myPlatform: + logger.info(f"Windows environment set") + return pathlib.PurePath( + f"{pathlib.Path(__file__).parent.parent.resolve()}/resources/variables.json" + ) + elif "Linux" in myPlatform: + logger.info(f"Linux environment set") + return pathlib.PurePosixPath( + f"{pathlib.Path(__file__).parent.parent.resolve()}/resources/variables.json" + ) + else: + return CommandException(f"Unable to support {platform.platform()}!") + + +def createComponentKey() -> str: + return "".join( + random.SystemRandom().choice(string.ascii_uppercase + string.digits) + for _ in range(10) + ) + + +def discordURLFormatter(display_text: str, url: str) -> str: + return f"[{display_text}]({url})" + + +logger.info(f"{str(__name__).title()} module loaded!") diff --git a/helpers/mysql.py b/helpers/mysql.py new file mode 100644 index 00000000..f1f0b95b --- /dev/null +++ b/helpers/mysql.py @@ -0,0 +1,342 @@ +# TODO +# * Review +# TODO +import logging +from typing import Union, Any + +import pymysql +from pymysql import OperationalError + +from helpers.constants import SQL_HOST, SQL_USER, SQL_PASSWD, SQL_DB + +logger = logging.getLogger(__name__) + + +# import pymysql.cursors +# +# from helpers.constants import SQL_HOST, SQL_PASSWD, SQL_DB, SQL_USER +# + +# Image Command +sqlCreateImageCommand = """ +INSERT INTO img_cmd_db (author, img_name, img_url) VALUES (%s, %s, %s) +""" + +sqlSelectImageCommand = """ +SELECT author, img_name, img_url FROM img_cmd_db WHERE img_name = %s +""" + +sqlSelectAllImageCommand = """ +SELECT author, img_name, img_url, created_at FROM img_cmd_db +""" + +sqlDeleteImageCommand = """ +DELETE FROM img_cmd_db WHERE img_name = %s AND author = %s +""" + +# Croot Bot +sqlTeamIDs = """ +SELECT id, school from botfrost.team_ids +""" + +sqlGetPrediction = """ +SELECT * FROM fap_predictions WHERE user_id = %s AND recruit_profile = %s; +""" + +sqlInsertPrediction = """ +INSERT INTO fap_predictions (user, user_id, recruit_name, recruit_profile, recruit_class, team, confidence, prediction_date) VALUES (%s, %s, %s, %s, %s, %s, %s, NOW()); +""" + +sqlUpdatePrediction = """ +UPDATE fap_predictions SET team = %s, confidence = %s, prediction_date = NOW() WHERE user_id = %s and recruit_profile = %s; +""" + +sqlGetCrootPredictions = """ +SELECT f.recruit_name, f.team, avg(f.confidence) as 'confidence', (count(f.team) / t.sumr) * 100 as 'percent', t.sumr as 'total' FROM fap_predictions as f JOIN (SELECT recruit_profile, COUNT(recruit_profile) as sumr FROM fap_predictions GROUP BY recruit_profile) as t on t.recruit_profile = f.recruit_profile WHERE f.recruit_profile = %s GROUP BY f.recruit_profile, f.recruit_name, f.team ORDER BY percent DESC; +""" + +sqlGetIndividualPrediction = "SELECT * FROM fap_predictions WHERE recruit_profile = %s ORDER BY prediction_date ASC" + +# # Iowa Command +sqlInsertIowa = """ +INSERT INTO iowa (user_id, reason, previous_roles) VALUES (%s, %s, %s) +""" + +sqlRetrieveIowa = """ +SELECT previous_roles FROM iowa WHERE user_id = %s +""" + +sqlRemoveIowa = """ +DELETE FROM iowa WHERE user_id = %s +""" +# +# # Tasks +# sqlRetrieveTasks = """ +# SELECT * FROM tasks_repo WHERE is_open = 1 +# """ +# +# sqlRecordTasks = """ +# INSERT INTO tasks_repo (send_to, message, send_when, is_open, author) VALUES (%s, %s, %s, %s, %s) +# """ +# +# sqlUpdateTasks = """ +# UPDATE tasks_repo SET is_open = %s WHERE send_to = %s AND message = %s AND send_when = %s AND author = %s +# """ +# +# # Karma +# sqlUpdateKarma = """ +# INSERT INTO karma (positive, negative, total) +# VALUES (%s, %s, %s) +# ON DUPLICATE KEY UPDATE (positive=%s, negative=%s, total=%s) +# """ +# +# # sqlRecordStats = """ +# # INSERT INTO stats (source, channel) VALUES (%s, %s) +# # """ +# +# # sqlRecordStatsManual = """ +# # INSERT INTO stats (source, channel, created_at) VALUES (%s, %s, %s) +# # """ +# +# # sqlRetrieveStats = """ +# # SELECT * FROM stats +# # """ +# +# # sqlRetrieveCurrencyLeaderboard = """ +# # SELECT * FROM currency ORDER BY balance DESC +# # """ +# +# # sqlRetrieveCurrencyUser = """ +# # SELECT balance FROM currency WHERE user_id = %s +# # """ +# +# # sqlCheckCurrencyInit = """ +# # SELECT init FROM currency WHERE user_id = %s +# # """ +# +# # sqlSetCurrency = """ +# # INSERT INTO currency (username, init, balance, user_id) VALUES (%s, 1, %s, %s) +# # """ +# +# # sqlUpdateCurrency = """ +# # UPDATE currency SET balance = balance + %s WHERE username = %s +# # """ +# +# # sqlRetrieveAllCustomLinesKeywords = """ +# # SELECT clb.source, cl.keyword, cl.description, clb.value, clb._for, clb.against, cl.source as orig_author, cl.result FROM custom_lines_bets clb INNER JOIN custom_lines cl ON (cl.keyword = clb.keyword ) WHERE clb.keyword = %s +# # """ +# +# # sqlRetrieveOneCustomLinesKeywords = """ +# # SELECT clb.source, cl.keyword, cl.description, clb.value, clb._for, clb.against, cl.source as orig_author, cl.result FROM custom_lines_bets clb INNER JOIN custom_lines cl ON (cl.keyword = clb.keyword ) WHERE clb.keyword = %s AND clb.source = %s +# # """ +# +# # sqlInsertCustomLines = """ +# # INSERT INTO custom_lines (source, keyword, description, result) VALUES (%s, %s, %s, 'tbd') +# # """ +# +# # sqlRetrieveAllOpenCustomLines = """ +# # SELECT * FROM custom_lines WHERE result = 'tbd' +# # """ +# +# # sqlRetreiveCustomLinesForAgainst = """ +# # SELECT source, _for, against, value FROM custom_lines_bets WHERE keyword = %s +# # """ +# +# # sqlRetrieveOneOpenCustomLine = """ +# # SELECT * FROM custom_lines WHERE keyword = %s and result = 'tbd' +# # """ +# +# # sqlInsertCustomLinesBets = """ +# # INSERT INTO custom_lines_bets (source, keyword, _for, against, value) VALUES (%s, %s, %s, %s, %s) +# # """ +# +# # sqlUpdateCustomLinesBets = """ +# # UPDATE custom_lines_bets SET `_for`=%s, against=%s, value=%s WHERE source=%s AND keyword=%s +# # """ +# +# # sqlUpdateCustomLinesResult = """ +# # UPDATE custom_lines SET result = %s WHERE keyword = %s +# # """ +# # sqlDatabaseTimestamp = """ +# # INSERT INTO bot_connections (user, connected, timestamp) VALUES (%s, %s, %s) +# # """ +# +# # sqlLogError = """ +# # INSERT INTO bot_error_log (user, error) VALUES (%s, %s) +# # """ +# +# # sqlLogUser = """ +# # INSERT INTO bot_user_log (user, event, comment) VALUES (%s, %s, %s) +# # """ +# +# # sqlLeaderboard = """ +# # SELECT user, COUNT(*) as num_games_bet_on, SUM(CASE b.win WHEN g.win THEN 1 ELSE 0 END) as correct_wins, SUM(CASE b.spread WHEN g.spread THEN 1 ELSE 0 END) as correct_spreads, SUM(CASE b.moneyline WHEN g.moneyline THEN 1 ELSE 0 END) as correct_moneylines, SUM(CASE b.win WHEN g.win THEN 1 ELSE 0 END + CASE b.spread WHEN g.spread THEN 2 ELSE 0 END + CASE b.moneyline WHEN g.moneyline THEN 2 ELSE 0 END) as total_points FROM bets b INNER JOIN games g ON (b.game_number = g.game_number) WHERE g.finished = true GROUP BY user ORDER BY total_points DESC; +# # """ +# +# # sqlAdjustedLeaderboard = """ +# # SELECT user, COUNT(*) as num_games_bet_on, SUM(CASE b.win WHEN g.win THEN 1 ELSE 0 END) as correct_wins, SUM(CASE b.spread WHEN g.spread THEN 1 ELSE 0 END) as correct_spreads, SUM(CASE b.moneyline WHEN g.moneyline THEN 1 ELSE 0 END) as correct_moneylines, SUM(CASE b.win WHEN g.win THEN 1 ELSE 0 END + CASE b.spread WHEN g.spread THEN 2 ELSE 0 END + CASE b.moneyline WHEN g.moneyline THEN 2 ELSE 0 END) / COUNT(*) as avg_pts_per_game FROM bets b INNER JOIN games g ON (b.game_number = g.game_number) WHERE g.finished = true GROUP BY user ORDER BY avg_pts_per_game DESC; +# # """ +# +# # sqlGetWinWinners = """ +# # SELECT b.user FROM bets b INNER JOIN games g ON (b.game_number = g.game_number) WHERE b.win = g.win AND g.opponent = %s; +# # """ +# +# # sqlGetSpreadWinners = """ +# # SELECT b.user FROM bets b INNER JOIN games g ON (b.game_number = g.game_number) WHERE b.spread = g.spread AND g.opponent = %s; +# # """ +# +# # sqlGetMoneylineWinners = """ +# # SELECT b.user FROM bets b INNER JOIN games g ON (b.game_number = g.game_number) WHERE b.moneyline = g.moneyline AND g.opponent = %s; +# # """ +# +# # sqlInsertWinorlose = """ +# # INSERT INTO bets (game_number, user, win, date_updated) VALUES (%s, %s, %s, NOW()) ON DUPLICATE KEY UPDATE win=%s; +# # """ +# +# # sqlInsertSpread = """ +# # INSERT INTO bets (game_number, user, spread, date_updated) VALUES (%s, %s, %s, NOW()) ON DUPLICATE KEY UPDATE spread=%s; +# # """ +# +# # sqlInsertMoneyline = """ +# # INSERT INTO bets (game_number, user, moneyline, date_updated) VALUES (%s, %s, %s, NOW()) ON DUPLICATE KEY UPDATE moneyline=%s; +# # """ +# +# # sqlRetrieveBet = """ +# # SELECT * FROM bets b WHERE b.user = %s; +# # """ +# +# # sqlRetrieveSpecificBet = """ +# # SELECT * FROM bets b WHERE b.game_number = %s AND b.user = %s; +# # """ +# +# # sqlRetrieveGameNumber = """ +# # SELECT g.game_number FROM games g WHERE g.opponent = %s; +# # """ +# +# # sqlRetrieveAllBet = """ +# # SELECT * FROM bets b WHERE b.game_number = %s; +# # """ +# +# # sqlUpdateScores = """ +# # INSERT INTO games (game_number, score, opponent_score) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE score=%s, opponent_score=%s +# # """ +# +# # sqlUpdateAllBetCategories = """ +# # INSERT INTO games (game_number, finished, win, spread, moneyline) VALUES (%s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE finished=%s, win=%s, spread=%s, moneyline=%s +# # """ +# +# # sqlRetrieveGameInfo = """ +# # SELECT * FROM games g WHERE g.game_number = %s; +# # """ +# +# # sqlRetrieveCrystalBallLastRun = """ +# # SELECT last_run FROM cb_lastrun +# # """ +# +# # sqlUpdateCrystalLastRun = """ +# # INSERT INTO cb_lastrun (last_run) VALUES (%s) +# # """ +# +# # sqlUpdateCrystalBall = """ +# # INSERT INTO crystal_balls (first_name, last_name, full_name, photo, prediction, result) VALUES (%s, %s, %s, %s, %s, %s) +# # """ +# +# # sqlRetrieveRedditInfo = """ +# # SELECT * FROM subreddit_info; +# # """ +# +# # sqlUpdateLineInfo = """ +# # INSERT INTO games (game_number, spread_value, moneyline_value) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE spread_value=%s, moneyline_value=%s +# # """ +# +# # sqlRetrieveTriviaQuestions = """ +# # SELECT * FROM trivia +# # """ +# +# # sqlInsertTriviaScore = """ +# # INSERT INTO trivia_scores (user, score) VALUES (%s, %s) ON DUPLICATE KEY UPDATE score=score+%s +# # """ +# +# # sqlZeroTriviaScore = """ +# # INSERT INTO trivia_scores (user, score) VALUES (%s, %s) ON DUPLICATE KEY UPDATE score=score+%s +# # """ +# +# # sqlRetrieveTriviaScores = """ +# # SELECT * FROM trivia_scores ORDER BY score DESC +# # """ +# +# # sqlClearTriviaScore = """ +# # TRUNCATE TABLE trivia_scores +# # """ + + +def processMySQL(query: str, **kwargs) -> Union[list[dict], None]: + logger.info(f"Starting a MySQL query") + sqlConnection = None + try: + sqlConnection = pymysql.connect( + host=SQL_HOST, + user=SQL_USER, + password=SQL_PASSWD, + db=SQL_DB, + charset="utf8mb4", + cursorclass=pymysql.cursors.DictCursor, + ) + logger.info(f"Connected to the MySQL Database!") + except OperationalError: # Unsure if this is the correct exception + logger.exception(f"Unable to connect to the `{SQL_DB}` database.") + + result = None + + try: + with sqlConnection.cursor() as cursor: + if ( + "fetch" not in kwargs.keys() + ): # Try using this instead: tries = kwargs.get('tries', DEFAULT_TRIES) + if "values" not in kwargs.keys(): + cursor.execute(query=query) + else: + cursor.execute(query=query, args=kwargs["values"]) + else: + if "values" not in kwargs.keys(): + if kwargs["fetch"] == "one": + cursor.execute(query=query) + result = cursor.fetchone() + elif kwargs["fetch"] == "many": + if "size" not in kwargs.keys(): + logger.exception( + "Fetching many requires a `size` kwargs.", exc_info=True + ) + cursor.execute(query=query) + result = cursor.fetchmany(many=kwargs["size"]) + elif kwargs["fetch"] == "all": + cursor.execute(query=query) + result = cursor.fetchall() + else: + if kwargs["fetch"] == "one": + cursor.execute(query=query, args=kwargs["values"]) + result = cursor.fetchone() + elif kwargs["fetch"] == "many": + if "size" not in kwargs.keys(): + logger.exception( + "Fetching many requires a `size` kwargs.", exc_info=True + ) + cursor.execute(query=query, args=kwargs["values"]) + result = cursor.fetchmany(many=kwargs["size"]) + elif kwargs["fetch"] == "all": + cursor.execute(query=query, args=kwargs["values"]) + result = cursor.fetchall() + + sqlConnection.commit() + except Exception as e: # noqa + logger.exception("Error occurred opening the MySQL database.", exc_info=True) + finally: + logger.info(f"Closing connection to the MySQL Database") + sqlConnection.close() + + if result: + logger.info(f"MySQL query finished") + return result + + +logger.info(f"{str(__name__).title()} module loaded!") diff --git a/helpers/slowking.py b/helpers/slowking.py new file mode 100644 index 00000000..2bee7207 --- /dev/null +++ b/helpers/slowking.py @@ -0,0 +1,38 @@ +# TODO +# * Everything +# TODO + +import logging + +logger = logging.getLogger(__name__) + +logger.info(f"{str(__name__).title()} module loaded!") + +# def make_slowking(user: discord.Member) -> discord.File: +# resize = (225, 225) +# +# try: +# avatar_thumbnail = Image.open( +# requests.get(user.avatar_url, stream=True).raw +# ).convert("RGBA") +# avatar_thumbnail.thumbnail(resize, Image.ANTIALIAS) +# # avatar_thumbnail.save("resources/images/avatar_thumbnail.png", "PNG") +# except IOError: +# logger.exception("Unable to create a Slow King avatar for user!", exc_info=True) +# +# paste_pos = (250, 250) +# make_slowking_filename = "make_slowking.png" +# +# base_img = Image.open("resources/images/slowking.png").convert("RGBA") +# base_img.paste(avatar_thumbnail, paste_pos, avatar_thumbnail) +# base_img.save(f"resources/images/{make_slowking_filename}", "PNG") +# +# if "Windows" in platform.platform(): +# slowking_path = f"{pathlib.Path(__file__).parent.parent.resolve()}\\resources\\images\\{make_slowking_filename}" +# else: +# slowking_path = f"{pathlib.Path(__file__).parent.parent.resolve()}/resources/images/{make_slowking_filename}" +# +# with open(slowking_path, "rb") as f: +# file = discord.File(f) +# +# return file diff --git a/main.py b/main.py deleted file mode 100644 index 61b8430e..00000000 --- a/main.py +++ /dev/null @@ -1,610 +0,0 @@ -import asyncio -import pathlib -import platform -import random -import sys -import traceback -import typing -from datetime import datetime, timedelta - -import discord -import tweepy -from discord.ext.commands import Bot -from discord_slash import ButtonStyle, ComponentContext, SlashCommand -from discord_slash.context import SlashContext -from discord_slash.utils.manage_components import create_actionrow, create_button -from imgurpython import ImgurClient - -from objects.Thread import send_reminder, TwitterStreamListener -from utilities.constants import ( - CHAN_GENERAL, - CHAN_HOF_PROD, - CHAN_RECRUITING, - CHAN_RULES, - CHAN_SCOTTS_BOTS, - CHAN_SHAME, - CHAN_TWITTERVERSE, - CommandError, - DT_TASK_FORMAT, - DT_TWEET_FORMAT, - GEE_USER, - GUILD_PROD, - IMGUR_CLIENT, - IMGUR_SECRET, - PROD_TOKEN, - ROLE_TIME_OUT, - TWITTER_BLOCK16_ID_STR, - TWITTER_BLOCK16_SCREENANME, - TWITTER_HUSKER_MEDIA_LIST_ID, - TWITTER_KEY, - TWITTER_SECRET_KEY, - TWITTER_TOKEN, - TWITTER_TOKEN_SECRET, - UserError, - make_slowking, - set_component_key, - CHAN_FOOD, -) -from utilities.embed import build_embed -from utilities.mysql import Process_MySQL, sqlRetrieveTasks, sqlRetrieveIowa - -client = Bot( - command_prefix="$", - case_insensitive=True, - description="Bot Frost version 3.0! Now with Slash Commands", - intents=discord.Intents.all(), -) - -slash = SlashCommand(client, sync_commands=True) # Sync required -client_percent = 0.0035 -list_members = [] - - -def log(message: str, level: int): - import datetime - - if level == 0: - print(f"[{datetime.datetime.now()}] ### Main: {message}") - elif level == 1: - print(f"[{datetime.datetime.now()}] ### ~~~ Main: {message}") - - -def current_guild() -> typing.Union[discord.Guild, None]: - if len(client.guilds) == 0: - log(f"Unable to find any guilds!", 1) - return None - else: - log(f"Active Guilds: {[guild.name for guild in client.guilds]}", 1) - return client.guilds[0] - - -async def change_my_status(): - statuses = ( - "Husker football 24/7", - "Nebraska beat Florida 62-24", - "the Huskers give up 400 yards rushing to one guy", - "a swing pass for -1 yards", - "a missed field goal", - "the Huskers lose another one-score game", - ) - try: - log(f"Attempting to change status...", 0) - await client.change_presence( - activity=discord.Activity( - type=discord.ActivityType.watching, name=random.choice(statuses) - ) - ) - log(f"Successfully changed status", 1) - except (AttributeError, discord.HTTPException) as err: - log(f"Unable to change status: " + str(err).replace("\n", " "), 1) - except: # noqa - log(f"Unknown error!" + str(sys.exc_info()[0]), 0) - - -async def load_tasks(): - def convert_duration(value: str): - imported_datetime = datetime.strptime(value, DT_TASK_FORMAT) - now = datetime.now() - - if imported_datetime > now: - duration = imported_datetime - now - return duration - return timedelta(seconds=0) - - async def convert_destination(cur_guild, destination_id: int): - destination_id = int(destination_id) - try: - member = cur_guild.get_member(destination_id) - if member is not None: - return member - except: # noqa - pass - - try: - channel = cur_guild.get_channel(destination_id) - if channel is not None: - return channel - except: # noqa - pass - - return None - - tasks = Process_MySQL(sqlRetrieveTasks, fetch="all") - - guild = current_guild() - if guild is None: - loop = asyncio.get_event_loop() - loop.stop() - await client.close() - log(f"Unable to find any guilds. Exiting...", 1) - else: - log(f"Guild == {guild}", 1) - if tasks is None: - return log(f"No tasks were loaded", 1) - - log(f"There are {len(tasks)} to be loaded", 0) - - task_repo = [] - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - for task in tasks: - send_when = convert_duration(task["send_when"]) - destination = await convert_destination(guild, task["send_to"]) - - if destination is None: - log(f"Skipping task because destination is None.", 1) - continue - - if task["author"] is None: - task["author"] = "N/A" - - if send_when == timedelta(seconds=0): - log(f"Alert time already passed! {task['send_when']}", 1) - await send_reminder( - num_seconds=0, - destination=destination, - message=task["message"], - source=task["author"], - alert_when=task["send_when"], - missed=True, - ) - continue - - task_repo.append( - asyncio.create_task( - send_reminder( - num_seconds=send_when.total_seconds(), - destination=destination, - message=task["message"], - source=task["author"], - alert_when=task["send_when"], - ) - ) - ) - - for index, task in enumerate(task_repo): - await task - - -async def send_tweet_alert(message: str): - log(f"Twitter Alert: {message}", 1) - - chan_twitter: discord.TextChannel = client.get_channel(id=CHAN_TWITTERVERSE) - embed = build_embed(fields=[["Twitter Stream Listener Alert", message]]) - await chan_twitter.send(embed=embed) - - -async def send_tweet(tweet): - if tweet.author.id_str not in [member["id_str"] for member in list_members]: - return - - direct_url = f"https://twitter.com/{tweet.author.screen_name}/status/{tweet.id_str}" - - if hasattr(tweet, "extended_tweet"): - fields = [["Message", tweet.extended_tweet["full_text"]]] - else: - fields = [["Message", tweet.text]] - - fields.append(["URL", direct_url]) - - embed = build_embed( - url="https://twitter.com/i/lists/1307680291285278720", - fields=fields, - footer=f"Tweet sent {tweet.created_at.strftime(DT_TWEET_FORMAT)}", - ) - - embed.set_author( - name=f"{tweet.author.name} (@{tweet.author.screen_name}) via {tweet.source}", - icon_url=tweet.author.profile_image_url_https, - ) - - if hasattr(tweet, "extended_entities"): - try: - for index, media in enumerate(tweet.extended_entities["media"]): - if index == 0: - embed.set_image( - url=tweet.extended_entities["media"][index]["media_url"] - ) - embed.add_field( - name=f"Media #{index + 1}", - value=f"[Link #{index + 1}]({media['media_url']})", - inline=False, - ) - except: # noqa - pass - - log(f"Sending tweet from @{tweet.author.screen_name}", 1) - - if tweet.author.screen_name.lower() == TWITTER_BLOCK16_SCREENANME.lower(): - chan: discord.TextChannel = client.get_channel(id=CHAN_FOOD) - await chan.send(embed=embed) - else: - buttons = [ - create_button( - style=ButtonStyle.gray, - custom_id=f"{set_component_key()}_send_to_general", - label="Send to General", - ), - create_button( - style=ButtonStyle.gray, - custom_id=f"{set_component_key()}_send_to_recruiting", - label="Send to Recruiting", - ), - create_button(style=ButtonStyle.URL, label="Open Tweet", url=direct_url), - ] - - chan: discord.TextChannel = client.get_channel(id=CHAN_TWITTERVERSE) - actionrow = create_actionrow(*buttons) - - # noinspection PyArgumentList - await chan.send(embed=embed, components=[actionrow]) - - -def start_twitter_stream(): - log("Bot is starting the Twitter stream", 0) - - auth = tweepy.OAuthHandler( - consumer_key=TWITTER_KEY, consumer_secret=TWITTER_SECRET_KEY - ) - auth.set_access_token(key=TWITTER_TOKEN, secret=TWITTER_TOKEN_SECRET) - api = tweepy.API(auth=auth, wait_on_rate_limit=True) - stream = TwitterStreamListener( - consumer_key=TWITTER_KEY, - consumer_secret=TWITTER_SECRET_KEY, - access_token=TWITTER_TOKEN, - access_token_secret=TWITTER_TOKEN_SECRET, - message_func=send_tweet, - alert_func=send_tweet_alert, - loop=client.loop, - ) - - for member in tweepy.Cursor( - api.get_list_members, list_id=TWITTER_HUSKER_MEDIA_LIST_ID - ).items(): - list_members.append( - { - "screen_name": member.screen_name, - "id_str": member.id_str, - } - ) - - # Block16 - list_members.append( - {"screen_name": TWITTER_BLOCK16_SCREENANME, "id_str": TWITTER_BLOCK16_ID_STR} - ) - - if "Windows" in platform.platform(): - list_members.append( - { - "screen_name": "ayy_gbr", - "id_str": "15899943", - } - ) - stream.filter(follow=[member["id_str"] for member in list_members], threaded=True) - - -def upload_picture(path: str) -> str: - imgur_client = ImgurClient(IMGUR_CLIENT, IMGUR_SECRET) - url = imgur_client.upload_from_path(path=path, config=None, anon=True) - - return url.get("link", None) - - -async def hall_of_fame_messages(reactions: list): - multiple_threshold = False - for reaction in reactions: - if not reaction.message.guild.id == GUILD_PROD: - break - if ( - multiple_threshold - ): # Rare instance where a message has multiple reactions that break the threshold - break - - if reaction.message.channel.id in ( - CHAN_HOF_PROD, - CHAN_SHAME, - ): # Stay out of HOF and HOS - continue - - slowpoke_emoji = "<:slowpoke:758361250048245770>" - server_member_count = len(client.users) - reaction_threshold = int(client_percent * server_member_count) - hos_channel = hof_channel = None - - if reaction.count >= reaction_threshold: - multiple_threshold = True - log( - f"Reaction threshold broken with [{reaction.count}] [{reaction.emoji}] in [{reaction.message.channel}] reactions", - 0, - ) - if str(reaction.emoji) == slowpoke_emoji: - hof = False - hos_channel = client.get_channel(id=CHAN_SHAME) - raw_message_history = await hos_channel.history(limit=5000).flatten() - else: - hof = True - hof_channel = client.get_channel(id=CHAN_HOF_PROD) - raw_message_history = await hof_channel.history(limit=5000).flatten() - - duplicate = False - for raw_message in raw_message_history: - if len(raw_message.embeds) > 0: - if str(reaction.message.id) in raw_message.embeds[0].footer.text: - duplicate = True - break - del raw_message_history - - if not duplicate: - thumb_url = "" - - if not hof: - embed_title = f"{slowpoke_emoji * 3} Hall of Shame Message {slowpoke_emoji * 3}" - embed_description = ( - "Messages so shameful they had to be memorialized forever!" - ) - channel = hos_channel - - thumb = make_slowking(reaction.message.author) - thumb_url = upload_picture( - str(pathlib.Path(thumb.filename).resolve()) - ) - else: - embed_title = f"{'๐Ÿ†' * 3} Hall of Fame Message {'๐Ÿ†' * 3}" - embed_description = ( - "Messages so amazing they had to be memorialized forever!" - ) - channel = hof_channel - - embed = build_embed( - title=embed_title, - description=embed_description, - thumbnail=thumb_url if thumb_url != "" else None, - fields=[ - ["Author", reaction.message.author.mention], - [ - "Message", - reaction.message.content - if reaction.message.content != "" - else reaction.message.attachments[0].url, - ], - [ - "Message Link", - f"[Click to view message]({reaction.message.jump_url})", - ], - ], - footer=f"Message ID: {reaction.message.id} | Hall of Shame message created at {reaction.message.created_at.strftime('%B %d, %Y at %H:%M%p')}", - ) - await channel.send(embed=embed) - log(f"Processed HOF/HOS message.", 0) - - -def check_if_iowa(member: discord.Member) -> bool: - previous_roles_raw = Process_MySQL( - query=sqlRetrieveIowa, values=member.id, fetch="all" - ) - - return True if previous_roles_raw is not None else False - - -async def send_welcome_message(who: discord.Member): - chan_rules = client.get_channel(CHAN_RULES) - - embed = build_embed( - title="Welcome to the server!", - description="The official Husker football discord server", - thumbnail="https://cdn.discordapp.com/icons/440632686185414677/a_061e9e57e43a5803e1d399c55f1ad1a4.gif", - fields=[ - [ - "Rules", - f"Please be sure to check out {chan_rules.mention if chan_rules is not None else 'the rules channel'} to catch up on server rules.", - ], - [ - "Commands", - f"View the list of commands with the `/commands` command. Note: commands only work while in the server.", - ], - [ - "Roles", - "You can assign yourself come flair by using the `/roles` command.", - ], - ], - ) - - await who.send(embed=embed) - - -@client.event -async def on_connect(): - start_twitter_stream() # Keeping for posterity to remind myself to remove debugging code before deep diving for how ever long to fix code that isn't broken - # if "Windows" not in platform.platform(): - # start_twitter_stream() - - -@client.event -async def on_ready(): - threshold = int(len(client.users) * client_percent) - - log(f"Bot Frost version 3.0", 0) - log(f"Name: {client.user}", 1) - log(f"ID: {client.user.id}", 1) - log(f"Guild: {current_guild()}", 1) - log(f"HOF/HOS Reaction Threshold: {threshold}", 1) - log(f"The bot is ready!", 0) - - if "Windows" not in platform.platform(): - try: - changelog_path = None - - if "Windows" in platform.platform(): - log(f"Windows changelog", 1) - changelog_path = pathlib.PurePath( - f"{pathlib.Path(__file__).parent.resolve()}/changelog.md" - ) - elif "Linux" in platform.platform(): - log(f"Linux changelog", 0) - changelog_path = pathlib.PurePosixPath( - f"{pathlib.Path(__file__).parent.resolve()}/changelog.md" - ) - - changelog = open(changelog_path, "r") - lines = changelog.readlines() - lines_str = "" - - for line in lines: - lines_str += f"* {str(line)}" - except OSError: - lines_str = "Error loading changelog." - - bot_spam = client.get_channel(CHAN_SCOTTS_BOTS) - embed = build_embed( - title="Husker Discord Bot", - fields=[ - [ - "Info", - f"I was restarted, but now I'm back! I'm now online as {client.user.mention}! Check out /commands.", - ], - ["HOF/HOS Reaction Threshold", f"{threshold}"], - ["Changelog", lines_str], - [ - "More Changelog", - f"[View rest of commits](https://github.com/refekt/Bot-Frost/commits/master)", - ], - ], - ) - await bot_spam.send(embed=embed, delete_after=600) - - await change_my_status() - await load_tasks() - - -@client.event -async def on_raw_reaction_add(payload: discord.RawReactionActionEvent): - channel = client.get_channel(payload.channel_id) - message = await channel.fetch_message(payload.message_id) - - await hall_of_fame_messages(message.reactions) - - -@client.event -async def on_member_join(member: discord.Member): - log(f"New Member: {member.display_name}", 0) - if not check_if_iowa(member): - await send_welcome_message(member) - else: - role_timeout = member.guild.get_role(ROLE_TIME_OUT) - await member.add_roles(role_timeout, reason="Returning back to Iowa.") - log(f"Added [{role_timeout}] role", 1) - - -@client.event -async def on_component(ctx: ComponentContext): - if ("send_to_general" in ctx.custom_id) or ("send_to_recruiting" in ctx.custom_id): - await ctx.defer(hidden=True) - chan: typing.Union[discord.TextChannel, None] = None - if "send_to_general" in ctx.custom_id: - chan = ctx.bot.get_channel(id=CHAN_GENERAL) - elif "send_to_recruiting" in ctx.custom_id: - chan = ctx.bot.get_channel(id=CHAN_RECRUITING) - if chan is not None: - twitter_url = str( - ctx.origin_message.components[0]["components"][2]["url"] - ).rstrip("/") - - await chan.send(f"{ctx.author.mention} forwarded: {twitter_url}") - await ctx.send(f"Sent to {chan.mention}!", hidden=True) - - -if "Windows" not in platform.platform(): - - @client.event - async def on_slash_command_error(ctx: SlashContext, ex: Exception): - def format_traceback(tback: list): - return "".join(tback).replace("Aaron", "Secret") - - if isinstance(ex, discord.errors.NotFound): - return log(f"Skipping a NotFound error", 1) - elif isinstance(ex, (UserError, AssertionError)): - embed = build_embed( - title="Husker Bot User Error", - description="An error occurred with user input", - fields=[["Error Message", ex.message]], - ) - elif isinstance(ex, CommandError): - embed = build_embed( - title="Husker Bot Command Error", - description="An error occurred with command processing", - fields=[["Error Message", ex.message]], - ) - else: - embed = build_embed( - title="Husker Bot Command Error", - description="An unknown error occurred", - fields=[["Error Message", f"{ex.__class__}: {ex}"]], - ) - - await ctx.send(embed=embed, hidden=True) - - traceback_raw = traceback.format_exception( - etype=type(ex), value=ex, tb=ex.__traceback__ - ) - - tback = format_traceback(traceback_raw) - cmd = ctx.command - sub_cmd = "" - if ctx.subcommand_name is not None: - sub_cmd = ctx.subcommand_name - - inputs = [] - - for key, value in ctx.data.items(): - inputs.append(f"{key} = {value}") - - message = ( - f"{ctx.author.mention} ({ctx.author.display_name}, {ctx.author_id}) received an unknown error!\n" - f"\n" - f"`/{cmd}{' ' + sub_cmd if sub_cmd is not None else ''} {inputs}`\n" - f"\n" - f"```\n{tback}\n```" - ) - - try: - gee = client.get_user(id=GEE_USER) - await gee.send(content=message) - except: # noqa - await ctx.send(content=f"<@{GEE_USER}>\n{message}") - - -extensions = [ - "commands.croot_bot", - "commands.admin", - "commands.text", - "commands.image", - "commands.football_stats", - "commands.reminder", - # "commands.testing", -] -for extension in extensions: - log(f"Loading extension: {extension}", 1) - client.load_extension(extension) - -client.run(PROD_TOKEN) diff --git a/objects/Bets.py b/objects/Bets.py index 8ff2096f..5c687f0c 100644 --- a/objects/Bets.py +++ b/objects/Bets.py @@ -1,76 +1,13 @@ -import requests +# TODO +# * Bet +# * Bets +# * Line +# * Lines +# TODO +import logging -from utilities.constants import HEADERS +logger = logging.getLogger(__name__) +# __all__ = [""] -class GameBets: - game_number = None - over_under = None - spread = None - win = None - - def __init__(self, game_number, over_under, spread, win): - self.game_number = game_number - self.over_under = over_under - self.spread = spread - self.win = win - - -class GameBetLine: - provider = None - spread = None - formatted_spread = None - over_under = None - - def __init__( - self, provider=None, spread=None, formatted_spread=None, over_under=None - ): - self.provider = provider - self.spread = spread - self.formatted_spread = formatted_spread - self.over_under = over_under - - -class GameBetInfo: - home_team = None - home_score = None - away_team = None - away_score = None - userbets = [] - lines = [] - - def __init__( - self, year, week, team, season="regular" - ): # , home_team=None, home_score=None, away_team=None, away_score=None, lines=None): - self.home_team = None - self.home_score = None - self.away_team = None - self.away_score = None - self.userbets = [] - self.lines = [] - - self.establish(year, week, team, season) - - def establish(self, year, week, team, season="regular"): - url = f"https://api.collegefootballdata.com/lines?year={year}&week={week}&seasonType={season}&team={team}" - re = requests.get(url=url, headers=HEADERS) - data = re.json() - - try: - self.home_team = data[0]["homeTeam"] - self.home_score = data[0]["homeScore"] - self.away_team = data[0]["awayTeam"] - self.away_score = data[0]["awayScore"] - linedata = data[0]["lines"] - - for line in linedata: - self.lines.append( - GameBetLine( - provider=line["provider"], - spread=line["spread"], - formatted_spread=line["formattedSpread"], - over_under=line["overUnder"], - ) - ) - except: # noqa - pass +logger.info(f"{str(__name__).title()} module loaded!") diff --git a/objects/Client.py b/objects/Client.py new file mode 100644 index 00000000..d3002e90 --- /dev/null +++ b/objects/Client.py @@ -0,0 +1,294 @@ +import logging +import pathlib +import platform +from typing import Union + +import discord +import tweepy +from discord.ext.commands import ( + Bot, +) + +from __version__ import _version +from helpers.constants import ( + CHAN_BOT_SPAM, + CHAN_GENERAL, + DEBUGGING_CODE, + DISCORD_CHANNEL_TYPES, + GUILD_PROD, + TWITTER_BEARER, + TWITTER_HUSKER_MEDIA_LIST_ID, + TWITTER_QUERY_MAX, +) +from helpers.embed import buildEmbed +from objects.Exceptions import ChangelogException +from objects.TweepyStreamListener import StreamClientV2 + +logger = logging.getLogger(__name__) + +__all__ = ["HuskerClient"] + +logger.info(f"{str(__name__).title()} module loaded!") + + +def start_twitter_stream(client: discord.Client) -> None: + logger.info("Bot is starting the Twitter stream") + + logger.info("Collecting Husker Media Twitter list") + tweeter_client = tweepy.Client(TWITTER_BEARER) + list_members = tweeter_client.get_list_members(TWITTER_HUSKER_MEDIA_LIST_ID) + + logger.info("Creating stream rule") + rule_query = "" + + if DEBUGGING_CODE: + rule_query = "from:ayy_gbr OR " + + for member in list_members[0]: + append_str = f"from:{member['username']} OR " + + if len(rule_query) + len(append_str) < TWITTER_QUERY_MAX: + rule_query += append_str + else: + break + + rule_query = rule_query[:-4] # Get rid of ' OR ' + + logger.info("Creating a stream client") + tweeter_stream = StreamClientV2( + bearer_token=TWITTER_BEARER, + client=client, + wait_on_rate_limit=True, + max_retries=3, + ) + + logger.debug(f"Created stream rule:\n\t{rule_query}") + tweeter_stream.add_rules(tweepy.StreamRule(value=rule_query)) + logger.debug(f"Stream filter rules:\n\t{tweeter_stream.get_rules()}") + + tweeter_stream.filter( + expansions=[ + "attachments.media_keys", + "attachments.poll_ids", + "author_id", + "entities.mentions.username", + "geo.place_id", + "in_reply_to_user_id", + "referenced_tweets.id", + "referenced_tweets.id.author_id", + ], + media_fields=[ + "alt_text", + "duration_ms", + "height", + "media_key", + "preview_image_url", + "public_metrics", + "type", + "url", + "width", + ], + place_fields=[ + "contained_within", + "country", + "country_code", + "full_name", + "geo", + "id", + "name", + "place_type", + ], + poll_fields=[ + "duration_minutes", + "end_datetime", + "id", + "options", + "voting_status", + ], + tweet_fields=[ + "attachments", + "author_id", + "context_annotations", + "conversation_id", + "created_at", + "entities", + "geo", + "id", + "in_reply_to_user_id", + "lang", + "possibly_sensitive", + "public_metrics", + "referenced_tweets", + "reply_settings", + "source", + "text", + "withheld", + ], + user_fields=[ + "created_at", + "description", + "entities", + "id", + "location", + "name", + "pinned_tweet_id", + "profile_image_url", + "protected", + "public_metrics", + "url", + "username", + "verified", + "withheld", + ], + threaded=True, + ) + logger.info(f"Twitter stream is running: {tweeter_stream.running}") + + +class HuskerClient(Bot): + add_extensions = [ + "commands.admin", + "commands.football_stats", + "commands.image", + "commands.recruiting", + "commands.reminder", + "commands.text", + ] + + # noinspection PyMethodMayBeStatic + def get_change_log(self) -> [str, ChangelogException]: + try: + changelog_path = None + changelog_file = "changelog.md" + + if DEBUGGING_CODE: + changelog_path = pathlib.PurePath( + f"{pathlib.Path(__file__).parent.parent.resolve()}/{changelog_file}" + ) + elif "Linux" in platform.platform(): + changelog_path = pathlib.PurePosixPath( + f"{pathlib.Path(__file__).parent.parent.resolve()}/{changelog_file}" + ) + else: + logger.exception("Unknown platform. Exiting", exc_info=True) + + changelog = open(changelog_path, "r") + lines = changelog.readlines() + lines_str = "" + + for line in lines: + lines_str += f"* {str(line)}" + + return lines_str + except OSError: + logger.exception("Error loading the changelog!", exc_info=True) + + # noinspection PyMethodMayBeStatic + async def send_welcome_message( + self, guild_member: Union[discord.Member, discord.User] + ) -> None: + # TODO Figure out how to handle this, if neeeded. May need to add something to bot logs. + channel_general: DISCORD_CHANNEL_TYPES = await self.fetch_channel(CHAN_GENERAL) + embed = buildEmbed( + title="New Husker fan!", + description="Welcome the new member to the server!", + fields=[ + dict( + name="New Member", + value=guild_member.mention, + ), + dict( + name="Info", + value=f"Be sure to check out `/commands` for how to use the bot!", + ), + ], + ) + await channel_general.send(embed=embed) + + # noinspection PyMethodMayBeStatic + async def create_online_message(self) -> None: + return buildEmbed( + title="Welcome to the Huskers server!", + description="The official Husker football discord server", + thumbnail="https://cdn.discordapp.com/icons/440632686185414677/a_061e9e57e43a5803e1d399c55f1ad1a4.gif", + fields=[ + dict( + name="Rules", + value=f"Please be sure to check out the rules channel to catch up on server rules.", + ), + dict( + name="Commands", + value=f"View the list of commands with the `/commands` command. Note: Commands do not work in Direct Messages.", + ), + dict( + name="Hall of Fame & Shame Reaaction Threshold", + value="TBD", + ), + dict( + name="Version", + value=_version, + ), + dict( + name="Changelong", + value=self.get_change_log(), + ), + dict( + name="Support Bot Frost", + value="Check out `/donate` to see how you can support the project!", + ), + ], + ) + + # async def check_reaction(self, reaction: discord.Reaction): + # ... + + # noinspection PyMethodMayBeStatic + async def on_connect(self) -> None: + logger.info("The bot has connected!") + + # noinspection PyMethodMayBeStatic + async def on_ready(self) -> None: + logger.info("Loading extensions") + + for extension in self.add_extensions: + try: + # NOTE Extensions will fail to load when runtime errors exist in the code. + # It will also NOT currently output a traceback. You MUST investigate + # mannually by stepping through code until I find a way to capture these + # exceptions. + await self.load_extension(extension) + logger.info(f"Loaded the {extension} extension") + except Exception as e: # noqa + logger.exception( + f"ERROR: Unable to laod the {extension} extension\n{e}" + ) + + logger.info("All extensions loaded") + + if not DEBUGGING_CODE: + chan_botspam: discord.TextChannel = await self.fetch_channel(CHAN_BOT_SPAM) + await chan_botspam.send(embed=await self.create_online_message()) # noqa + + logger.info("The bot is ready!") + + try: + await self.tree.sync(guild=discord.Object(id=GUILD_PROD)) + except Exception as e: # noqa + logger.exception("Error syncing the tree!\n\n{e}", exc_info=True) + + logger.info("The bot tree has synced!") + + logger.info("Starting Twitter stream") + start_twitter_stream(self) + logger.info("Twitter stream started") + + async def on_member_join( + self, guild_member: Union[discord.Member, discord.User] + ) -> None: + await self.send_welcome_message(guild_member) + + async def on_message_reaction_add( + self, + reaction: discord.Reaction, + ) -> None: # TODO + ... # await self.check_reaction(reaction) diff --git a/objects/Exceptions.py b/objects/Exceptions.py new file mode 100644 index 00000000..e87c1cb8 --- /dev/null +++ b/objects/Exceptions.py @@ -0,0 +1,81 @@ +# Global Errors +import logging +from dataclasses import dataclass # https://www.youtube.com/watch?v=vBH6GRJ1REM + +logger = logging.getLogger(__name__) + +__all__ = [ + "ChangelogException", + "CommandException", + "ExtensionException", + "ImageException", + "MySQLException", + "RecruitException", + "ScheduleException", + "StatsException", + "SurveyException", + "TwitterStreamException", + "UserInputException", + "WeatherException", +] + +logger.info(f"{str(__name__).title()} module loaded!") + + +@dataclass() +class CommandException(Exception): + message: str + + +@dataclass() +class UserInputException(Exception): + message: str + + +@dataclass() +class MySQLException(Exception): + message: str + + +@dataclass() +class ExtensionException(Exception): + message: str + + +@dataclass() +class TwitterStreamException(Exception): + message: str + + +@dataclass() +class ChangelogException(Exception): + message: str + + +@dataclass() +class SurveyException(Exception): + message: str + + +@dataclass() +class WeatherException(Exception): + message: str + + +@dataclass() +class ImageException(Exception): + message: str + + +@dataclass() +class ScheduleException(Exception): + message: str + + +@dataclass() +class StatsException(Exception): + message: str + + +class RecruitException(Exception): + message: str diff --git a/objects/FAPing.py b/objects/FAPing.py deleted file mode 100644 index cd139dbd..00000000 --- a/objects/FAPing.py +++ /dev/null @@ -1,680 +0,0 @@ -import asyncio -import datetime -from typing import Union - -import discord -import numpy as np -import pandas as pd -from discord import client as discord_client -from discord.ext import commands -from discord_slash.context import ComponentContext, SlashContext - -from objects.Recruits import FootballRecruit -from utilities.mysql import Process_MySQL, sqlTeamIDs - -CURRENT_CLASS = datetime.datetime.now().year -NO_MORE_PREDS = datetime.datetime.now().year -DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" - - -def log(message: str, level: int): - import datetime - - if level == 0: - print(f"[{datetime.datetime.now()}] ### FAP: {message}") - elif level == 1: - print(f"[{datetime.datetime.now()}] ### ~~~ FAP: {message}") - - -async def get_teams(): - sql_teams = Process_MySQL(query=sqlTeamIDs, fetch="all") - teams_list = [t["school"] for t in sql_teams] - return teams_list - - -async def get_prediction(user, recruit): - get_prediction_query = """ - SELECT * FROM fap_predictions WHERE user_id = %s AND recruit_profile = %s; - """ - sql_response = Process_MySQL( - query=get_prediction_query, values=(user.id, recruit._247_profile), fetch="one" - ) - return sql_response - - -async def insert_prediction( - user, recruit, team_prediction, prediction_confidence, previous_prediction -): - if previous_prediction is None: - insert_prediction_query = """ - INSERT INTO fap_predictions (user, user_id, recruit_name, recruit_profile, recruit_class, team, confidence, prediction_date) VALUES (%s, %s, %s, %s, %s, %s, %s, NOW()); - """ - Process_MySQL( - query=insert_prediction_query, - values=( - user.name, - user.id, - recruit.name, - recruit._247_profile, - recruit.year, - team_prediction, - prediction_confidence, - ), - ) - else: - update_prediction_query = """ - UPDATE fap_predictions SET team = %s, confidence = %s, prediction_date = NOW() WHERE user_id = %s and recruit_profile = %s; - """ - if team_prediction == previous_prediction["team"]: - update_prediction_query = """ - UPDATE fap_predictions SET team = %s, confidence = %s WHERE user_id = %s and recruit_profile = %s; - """ - Process_MySQL( - query=update_prediction_query, - values=( - team_prediction, - prediction_confidence, - user.id, - recruit._247_profile, - ), - ) - - -def get_croot_predictions(recruit): - get_croot_preds_query = """ - SELECT f.recruit_name, f.team, avg(f.confidence) as 'confidence', (count(f.team) / t.sumr) * 100 as 'percent', t.sumr as 'total' FROM fap_predictions as f JOIN (SELECT recruit_profile, COUNT(recruit_profile) as sumr FROM fap_predictions GROUP BY recruit_profile) as t on t.recruit_profile = f.recruit_profile WHERE f.recruit_profile = %s GROUP BY f.recruit_profile, f.recruit_name, f.team ORDER BY percent DESC; - """ - get_croot_preds_response = Process_MySQL( - query=get_croot_preds_query, values=recruit._247_profile, fetch="all" - ) - return get_croot_preds_response - - -async def initiate_fap( - ctx: Union[SlashContext, ComponentContext], user, recruit, client: discord_client -): - await ctx.send( - "Initiating FAP! Your inputs will be delete, but may appear to remain on the screen. They will disappear on reloading of Discord (Ctrl + R).", - hidden=True, - ) - - if (recruit.committed.lower() if recruit.committed is not None else None) in [ - "signed", - "enrolled", - ]: - return await ctx.send( - "You cannot make predictions on recruits that have been signed or have enrolled in their school.", - hidden=True, - ) - - if recruit.year < NO_MORE_PREDS: - return await ctx.send( - f"You cannot make predictions on recruits from before the [{NO_MORE_PREDS}] class. [{recruit.name}] was in the [{recruit.year}] recruiting class.", - hidden=True, - ) - - previous_prediction = await get_prediction(user, recruit) - team_prediction = None - prediction_confidence = None - - first_msg = "" - if previous_prediction is not None: - first_msg = f"It appears that you've previously predicted [{recruit.name}] to [{previous_prediction['team']}] with confidence [{previous_prediction['confidence']}]. You can answer the prompts to update your prediction.\n" - first_msg += f"Please predict which team you think [{recruit.name}] will commit to. [247Sports Profile]({recruit._247_profile}])" - await ctx.send(first_msg, hidden=True) - - while team_prediction is None: - try: - prediction_response_msg: discord.Message = await client.wait_for( - "message", - check=lambda message: message.author == user - and message.channel == ctx.channel, - timeout=30, - ) - prediction_response = prediction_response_msg.content.lower() - except asyncio.TimeoutError: - await ctx.send( - "Sorry, you ran out of time. You'll have to initiate the FAP process again by clicking the crystal ball emoji on the crootbot message or using the $predict command.", - hidden=True, - ) - - return - else: - await prediction_response_msg.delete() - valid_teams = await get_teams() - - if prediction_response in [t.lower() for t in valid_teams]: - team_index = [t.lower() for t in valid_teams].index(prediction_response) - team_prediction = valid_teams[team_index] - if recruit.committed_school == team_prediction: - await ctx.send( - f"{recruit.name}] is already committed to [{recruit.committed_school}]. Nice try.", - hidden=True, - ) - - return - await ctx.send( - f"You've selected [{team_prediction}] as your prediction, what is your confidence in that pick from 1 to 10?", - hidden=True, - ) - - else: - await ctx.send( - "That isn't a valid team. Please try again or ask my creators to add that as a valid team.", - hidden=True, - ) - - while prediction_confidence is None: - try: - confidence_response_msg: discord.Message = await client.wait_for( - "message", - check=lambda message: message.author == user - and message.channel == ctx.channel, - timeout=30, - ) - confidence_response = confidence_response_msg.content - except asyncio.TimeoutError: - await ctx.send( - "Sorry, you ran out of time. You'll have to initiate the FAP process again by clicking the crystal ball emoji on the crootbot message or using the $predict command.", - hidden=True, - ) - - return - else: - await confidence_response_msg.delete() - - try: - confidence = int(confidence_response) - except: # noqa - await ctx.send( - "That input was not accepted, please enter a number between 1 and 10.", - hidden=True, - ) - - else: - if 1 <= confidence <= 10: - await ctx.send( - f"You've selected [{confidence}] as your confidence level.", - hidden=True, - ) - - prediction_confidence = int(confidence_response) - else: - await ctx.send( - f"{confidence}] is not between 1 and 10. Try again.", - hidden=True, - ) - - await insert_prediction( - user, recruit, team_prediction, prediction_confidence, previous_prediction - ) - await ctx.send( - f"Your prediction of [{recruit.name}] to {team_prediction}] has been logged!", - hidden=True, - ) - - -def get_faps(recruit): - croot_preds = get_croot_predictions(recruit) - return croot_preds - - -async def individual_predictions(ctx, recruit): - get_individual_preds_query = "SELECT * FROM fap_predictions WHERE recruit_profile = %s ORDER BY prediction_date ASC" - individual_preds = Process_MySQL( - query=get_individual_preds_query, fetch="all", values=(recruit._247_profile,) - ) - if individual_preds is None: - await ctx.send("This recruit has no predictions.") - return - - embed_title = f"Predictions for {recruit.name}" - embed = discord.Embed(title=embed_title) - for i, p in enumerate(individual_preds): - try: - pred_user = ctx.guild.get_member(p["user_id"]) - pred_user = pred_user.display_name - except: # noqa - pred_user = p["user"] - if pred_user is None: - pred_user = p["user"] - # if i > 0: - # embed_description += '\n' - pred_dt = p["prediction_date"] - if isinstance(pred_dt, str): - pred_dt = datetime.datetime.strptime(p["prediction_date"], DATETIME_FORMAT) - pred_field = [ - f"{pred_user}", - f"{p['team']} ({p['confidence']}) - {pred_dt.month}/{pred_dt.day}/{pred_dt.year}", - ] - if p["correct"] == 1: - pred_field[0] = "โœ… " + pred_field[0] - elif p["correct"] == 0: - pred_field[0] = "โŒ " + pred_field[0] - elif p["correct"] is None: - pred_field[0] = "โŒ› " + pred_field[0] - embed.add_field(name=pred_field[0], value=pred_field[1], inline=False) - - embed.set_footer(text="โœ… = Correct, โŒ = Wrong, โŒ› = TBD") - await ctx.send(embed=embed) - - -class FAP_Commands(commands.Cog): - def __init__(self, bot): - self.bot = bot - - @commands.group() - async def fap(self, ctx): - """Frost Approved Predictions commands""" - if ctx.subcommand_passed: - return - else: - raise AttributeError(f"A subcommand must be used. Review $help.") - - @fap.command() - async def predict(self, ctx, year: int, *name): - """Put in a FAP prediction for a recruit.""" - from utilities.embed import build_embed - - if len(name) == 0: - raise discord.ext.commands.UserInputError( - "A player's first and/or last name is required." - ) - - if len(str(year)) == 2: - year += 2000 - - if year > datetime.datetime.now().year + 5: - raise discord.ext.commands.UserInputError( - "The search year must be within five years of the current class." - ) - - if year < 1869: - raise discord.ext.commands.UserInputError( - "The search year must be after the first season of college football--1869." - ) - - edit_msg = await ctx.send("Loading...") - search = FootballRecruit(year, name) - - if type(search) == commands.UserInputError: - await edit_msg.edit(content=search) - return - - async def send_fap_convo(target_recruit): - await initiate_fap(ctx.message.author, target_recruit, self.bot) - - if len(search) == 1: - await edit_msg.delete() - await send_fap_convo(search[0]) - return - - result_info = "" - search_reactions = { - "1๏ธโƒฃ": 0, - "2๏ธโƒฃ": 1, - "3๏ธโƒฃ": 2, - "4๏ธโƒฃ": 3, - "5๏ธโƒฃ": 4, - "6๏ธโƒฃ": 5, - "7๏ธโƒฃ": 6, - "8๏ธโƒฃ": 7, - "9๏ธโƒฃ": 8, - "๐Ÿ”Ÿ": 9, - } - - index = 0 - - for index, result in enumerate(search): - if index < 10: - result_info += f"{list(search_reactions.keys())[index]}: {result.year} - {'โญ' * result.rating_stars}{' - ' + result.position if result.rating_stars > 0 else result.position} - {result.name}\n" - - embed = build_embed( - title=f"Search Results for [{year} {[n for n in name]}]", - fields=[["Search Results", result_info]], - ) - - await edit_msg.edit(content="", embed=embed) - - for reaction in list(search_reactions.keys())[0 : index + 1]: - await edit_msg.add_reaction(reaction) - - def checking_reaction(reaction, user): - if not user.bot: - return (reaction.emoji in search_reactions) and ( - user == ctx.message.author - ) - - search_result_player = None - - try: - reaction, user = await self.bot.wait_for( - "reaction_add", check=checking_reaction - ) - except asyncio.TimeoutError: - pass - else: - search_result_player = search[search_reactions[reaction.emoji]] - - try: - await edit_msg.delete() - except discord.HTTPException: - log(f"Deleting the message failed.", 1) - except discord.ClientException: - log(f"Unable to delete message due to lack of permissions.", 1) - - await send_fap_convo(search_result_player) - - @fap.command() - async def leaderboard(self, ctx, year=None): - """Get the FAP leaderboard. If no year is given, it will provide the leaderboard for the current recruiting class.""" - if year is None: - year = str(CURRENT_CLASS) - embed_title = f"{year} FAP Leaderboard" - get_all_preds_query = ( - f"SELECT * FROM fap_predictions WHERE recruit_class = {year}" - ) - if year.lower() == "overall": - get_all_preds_query = """SELECT * FROM fap_predictions""" - embed_title = "All-Time FAP Leaderboard" - - faps = Process_MySQL(query=get_all_preds_query, fetch="all") - faps_df = pd.DataFrame(faps) - faps_nn = faps_df[(faps_df["correct"].notnull())].copy() - leaderboard = pd.DataFrame(faps_nn["user_id"].unique(), columns=["user_id"]) - leaderboard["points"] = 0 - leaderboard["correct_pct"] = 0 - for u in leaderboard["user_id"].unique(): - faps_user = faps_nn[faps_nn["user_id"] == u].copy() - leaderboard.loc[leaderboard["user_id"] == u, "correct_pct"] = ( - faps_user["correct"].mean() * 100 - ) - faps_user["correct"] = faps_user["correct"].replace(0.0, -1.0) - # Does -1*confidence for incorrect, confidence*(days_correct/10) for correct less than 10 days correct, and confidence+(days_correct-10)*0.1 for correct over 10 days correct - faps_user.loc[faps_user["correct"] == 1, "time_delta"] = ( - faps_user.loc[faps_user["correct"] == 1, "decision_date"] - - faps_user.loc[faps_user["correct"] == 1, "prediction_date"] - ) - faps_user.loc[ - faps_user["time_delta"].dt.total_seconds() <= 864000, "points" - ] = ( - faps_user.loc[ - faps_user["time_delta"].dt.total_seconds() <= 864000, "correct" - ] - * faps_user.loc[ - faps_user["time_delta"].dt.total_seconds() <= 864000, "confidence" - ] - * ( - faps_user.loc[ - faps_user["time_delta"].dt.total_seconds() <= 864000, - "time_delta", - ].dt.total_seconds() - / 864000 - ) - ) - faps_user.loc[ - faps_user["time_delta"].dt.total_seconds() > 864000, "points" - ] = faps_user.loc[ - faps_user["time_delta"].dt.total_seconds() > 864000, "correct" - ] * faps_user.loc[ - faps_user["time_delta"].dt.total_seconds() > 864000, "confidence" - ] + ( - ( - ( - faps_user.loc[ - faps_user["time_delta"].dt.total_seconds() > 864000, - "time_delta", - ].dt.total_seconds() - / 86400 - ) - - 10 - ) - * 0.1 - ) - faps_user.loc[faps_user["correct"] == -1.0, "points"] = ( - faps_user.loc[faps_user["correct"] == -1.0, "confidence"] * -1 - ) - - leaderboard.loc[leaderboard["user_id"] == u, "points"] = faps_user[ - "points" - ].sum() - leaderboard = leaderboard.sort_values("points", ascending=False) - - embed_string = "User: Points (Pct Correct)" - place = 1 - for u in leaderboard["user_id"]: - try: - user = ctx.guild.get_member(u) - username = user.display_name - except: # noqa - user = faps_df.loc[faps_df["user_id"] == u, "user"].values[-1] - username = user - if user is None: - continue - points = leaderboard.loc[leaderboard["user_id"] == u, "points"].values[0] - correct_pct = leaderboard.loc[ - leaderboard["user_id"] == u, "correct_pct" - ].values[0] - embed_string += f"\n#{place} {username}: {points:.1f} ({correct_pct:.0f}%)" - place += 1 - - embed_msg = discord.Embed(title=embed_title, description=embed_string) - await ctx.send(embed=embed_msg) - - @fap.command() - async def stats(self, ctx, target_member: discord.Member = None, year: int = None): - """Get the number of predictions and percent correct for a user for all-time and the current recruiting class. If no user is given, it will return the results - for the user that calls it.""" - if target_member is None: - target_member = ctx.author - if year is None: - year = CURRENT_CLASS - embed_title = f"FAP Stats for {target_member.display_name}" - get_stats_query = ( - f"""SELECT * FROM fap_predictions WHERE user_id = {target_member.id}""" - ) - faps = Process_MySQL(query=get_stats_query, fetch="all") - if faps is None: - await ctx.send( - "You have made no predictions previously. You can do so by calling `$predict `" - ) - return - - faps_df = pd.DataFrame(faps) - overall_pct = faps_df["correct"].mean() * 100 - current_pct = ( - faps_df.loc[faps_df["recruit_class"] == year, "correct"].mean() * 100 - ) - overall_count = len(faps_df.index) - current_count = len(faps_df[faps_df["recruit_class"] == year].index) - overall_correct_count = len(faps_df[faps_df["correct"] == 1].index) - current_correct_count = len( - faps_df[(faps_df["correct"] == 1) & (faps_df["recruit_class"] == year)] - ) - overall_wrong_count = len(faps_df[faps_df["correct"] == 0].index) - current_wrong_count = len( - faps_df[(faps_df["correct"] == 0) & (faps_df["recruit_class"] == year)] - ) - - avg_days_overall_str = "" - avg_days_current_str = "" - if ( - faps_df.loc[faps_df["correct"] == 1, "decision_date"].notna() - * faps_df.loc[faps_df["correct"] == 1, "prediction_date"].notna() - ).sum() > 0: - timedeltas_correct_overall = ( - faps_df.loc[faps_df["correct"] == 1, "decision_date"] - - faps_df.loc[faps_df["correct"] == 1, "prediction_date"] - ).values - avg_days_overall = ( - float(sum(timedeltas_correct_overall) / len(timedeltas_correct_overall)) - / 86400000000000 - ) - if avg_days_overall > 0: - avg_days_overall_str = f"\nAvg Days Correct: {avg_days_overall:.1f}" - if ( - faps_df.loc[ - (faps_df["correct"] == 1) & (faps_df["recruit_class"] == year), - "decision_date", - ].notna() - * faps_df.loc[ - (faps_df["correct"] == 1) & (faps_df["recruit_class"] == year), - "prediction_date", - ].notna() - ).sum() > 0: - timedeltas_correct_current = ( - faps_df.loc[ - (faps_df["correct"] == 1) & (faps_df["recruit_class"] == year), - "decision_date", - ] - - faps_df.loc[ - (faps_df["correct"] == 1) & (faps_df["recruit_class"] == year), - "prediction_date", - ] - ).values - avg_days_current = ( - float(sum(timedeltas_correct_current) / len(timedeltas_correct_current)) - / 86400000000000 - ) - if avg_days_current > 0: - avg_days_current_str = f"\nAvg Days Correct: {avg_days_current:.1f}" - - overall_ratio_str = "N/A" - current_ratio_str = "N/A" - if overall_correct_count + overall_wrong_count > 0: - overall_ratio_str = ( - f"{overall_correct_count}/{overall_correct_count + overall_wrong_count}" - ) - if current_correct_count + current_wrong_count > 0: - current_ratio_str = ( - f"{current_correct_count}/{current_correct_count + current_wrong_count}" - ) - - embed_msg = discord.Embed(title=embed_title) - if np.isnan(overall_pct): - overall_pct = "N/A" - embed_msg.add_field( - name="**Overall**", - value=f"Predictions: {overall_count}\nPercent Correct: {overall_pct}% ({overall_ratio_str})" - + avg_days_overall_str, - inline=False, - ) - else: - embed_msg.add_field( - name="**Overall**", - value=f"Predictions: {overall_count}\nPercent Correct: {overall_pct:.0f}% ({overall_ratio_str})" - + avg_days_overall_str, - inline=False, - ) - if np.isnan(current_pct): - current_pct = "N/A" - embed_msg.add_field( - name=f"**{year} Class**", - value=f"Predictions: {current_count}\nPercent Correct: {current_pct}% ({current_ratio_str})" - + avg_days_current_str, - inline=False, - ) - else: - embed_msg.add_field( - name=f"**{year} Class**", - value=f"Predictions: {current_count}\nPercent Correct: {current_pct:.0f}% ({current_ratio_str})" - + avg_days_current_str, - inline=False, - ) - - await ctx.send(embed=embed_msg) - - @fap.command() - async def user(self, ctx, target_member: discord.Member = None, year: int = None): - """Get the predictions for a user for a given year.""" - if target_member is None: - target_member = ctx.author - if year is None: - year = CURRENT_CLASS - get_user_preds_query = """SELECT * FROM fap_predictions - WHERE user_id = %s AND recruit_class = %s - ORDER BY prediction_date DESC""" - user_preds = Process_MySQL( - query=get_user_preds_query, - values=( - target_member.id, - year, - ), - fetch="all", - ) - if user_preds is None: - if target_member.id == ctx.author.id: - await ctx.send( - f"You do not have any predictions for the {year} class. Get on it already." - ) - else: - await ctx.send( - f"{target_member.display_name} doesn't have any predictions for the {year} class." - ) - return - - embed_title = f"{target_member.display_name}'s {year} Predictions" - embed = discord.Embed(title=embed_title, color=0xD00000) - embed_list = [] - - correct_amount = 0 - incorrect_amount = 0 - # make new embed after current embed has 25 fields, then iterate over each embed and send it to the channel - for i, p in enumerate(user_preds): - field_title = "" - field_value = "" - - if p["correct"] == 1: - field_title += "โœ… " - correct_amount += 1 - elif p["correct"] == 0: - field_title = "โŒ " - incorrect_amount += 1 - elif p["correct"] is None: - field_title += "โŒ› " - field_title += f"{p['recruit_name']}" - - pred_date = p["prediction_date"] - if isinstance(pred_date, str): - pred_date = datetime.datetime.strptime( - p["prediction_date"], DATETIME_FORMAT - ) - field_value = f"{p['team']} ({p['confidence']}) - {pred_date.month}/{pred_date.day}/{pred_date.year} \[[247 Profile]({p['recruit_profile']})\]" - - commit_date = p["decision_date"] - if p["correct"] == 1: - days_correct = (commit_date - pred_date).total_seconds() / 86400 - # spaces_added = int((len(f"{p['team']} ({p['confidence']}) ")/2)) * '\u2800' - field_value = f"--- {days_correct:.1f} Days Correct ---\n" + field_value - - embed.add_field(name=field_title, value=field_value, inline=False) - - if len(embed.fields) == 25: - embed_list.append(embed) - embed = discord.Embed(title=embed_title, color=0xD00000) - - embed_list.append(embed) - del embed - - # send multiple dms if multiple embeds if in DM, otherwise just send one and inform the user in the footer that they can DM to get all the results - embed_footer_text = "โœ… = Correct, โŒ = Wrong, โŒ› = TBD" - if not isinstance(ctx.channel, discord.DMChannel) and len(embed_list) > 1: - embed_list = [embed_list[0]] - embed_footer_text += f"\nOnly showing first 25 results. To view all {len(user_preds)} predictions, DM the bot instead." - for i, embed in enumerate(embed_list): - if len(embed_list) > 1: - embed.title += f" ({i + 1}/{len(embed_list)})" - embed_description = "" - if correct_amount + incorrect_amount > 0: - embed_description = f"""Total {year} Predictions: {len(user_preds)}\nPercent Correct: {(correct_amount / (correct_amount + incorrect_amount)) * 100:.0f}% ({correct_amount}/{correct_amount + incorrect_amount})\n\n""" - else: - embed_description = ( - f"""Total {year} Predictions: {len(user_preds)}\n\n""" - ) - embed.description = embed_description - embed.set_footer(text=embed_footer_text) - await ctx.send(embed=embed) - - -def setup(bot): - bot.add_cog(FAP_Commands(bot)) diff --git a/objects/Karma.py b/objects/Karma.py index acdf38d8..1262725a 100644 --- a/objects/Karma.py +++ b/objects/Karma.py @@ -1,29 +1,40 @@ -from typing import AnyStr +# TODO +# This is for server karma +# TODO +import logging -from utilities.constants import CommandError -from utilities.mysql import sqlUpdateKarma, Process_MySQL +logger = logging.getLogger(__name__) +# __all__ = [""] -class KarmaUser: - weight_msg = 0.25 - weight_react = 1 +logger.info(f"{str(__name__).title()} module loaded!") - def __init__( - self, user_id: int, user_name: AnyStr, positive: float, negative: float - ): - self.user_id = user_id - self.user_name = user_name - self.positive = positive - self.negative = negative - self.total = positive * negative - - def update(self, msg: bool = False, react: bool = False): - - if msg and not react: - value = self.weight_msg - elif react and not msg: - value = self.weight_react - else: - raise CommandError("Unable to update karma.") - - Process_MySQL(query=sqlUpdateKarma, values=value) +# from typing import AnyStr +# +# from objects.Exceptions import CommandException, UserInputException +# # from helpers.mysql import sqlUpdateKarma, Process_MySQL +# from helpers.mysql import processMySQL +# +# class KarmaUser: +# weight_msg = 0.25 +# weight_react = 1 +# +# def __init__( +# self, user_id: int, user_name: AnyStr, positive: float, negative: float +# ): +# self.user_id = user_id +# self.user_name = user_name +# self.positive = positive +# self.negative = negative +# self.total = positive * negative +# +# def update(self, msg: bool = False, react: bool = False): +# +# if msg and not react: +# value = self.weight_msg +# elif react and not msg: +# value = self.weight_react +# else: +# logger.exception("Unable to update karma.", exc_info=True) +# +# Process_MySQL(query=sqlUpdateKarma, values=value) diff --git a/objects/Paginator.py b/objects/Paginator.py new file mode 100644 index 00000000..9ac89a4d --- /dev/null +++ b/objects/Paginator.py @@ -0,0 +1,91 @@ +import logging +from collections import deque +from typing import List + +import discord + +from helpers.constants import GLOBAL_TIMEOUT + +logger = logging.getLogger(__name__) + + +__all__ = ["EmbedPaginatorView"] + + +class EmbedPaginatorView(discord.ui.View): + def __init__( + self, + embeds: List[discord.Embed], + original_message: discord.InteractionMessage, + timeout: int = GLOBAL_TIMEOUT, + ) -> None: + """ + + :param embeds: + :param original_message: + :param timeout: + """ + super().__init__(timeout=timeout) + self._embeds: List[discord.Embed] = embeds + self._queue = deque(embeds) # collections.deque + self._initial: discord.Embed = embeds[0] + self._len: int = len(embeds) + self.current_index: int = 1 + self.response: discord.InteractionMessage = original_message + + try: # TODO Make this show up in the middle. + self.add_item( + discord.ui.Button( + label=f"Page {self.current_index}/{self._len}", + custom_id="ud_current_page", + disabled=True, + style=discord.ButtonStyle.grey, + ) + ) + except (TypeError, ValueError) as e: + logger.exception( + f"Error creating ud_current_page button: {e}", exc_info=True + ) + + async def update_current_page(self): + logger.info(f"Current index is: {self.current_index}") + + for button in self.children: + if button.custom_id == "ud_current_page": + button.label = f"Page {self.current_index}/{self._len}" + break + await self.response.edit(view=self) + + @discord.ui.button(emoji="\N{LEFTWARDS BLACK ARROW}", custom_id="ud_left") + async def previous_embed( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + logger.info("Going to previous embed") + self._queue.rotate(1) + if self.current_index == 1: + self.current_index = len(self._queue) + else: + self.current_index -= 1 + await self.update_current_page() + + embed = self._queue[0] + await interaction.response.edit_message(embed=embed) + + @discord.ui.button(emoji="\N{BLACK RIGHTWARDS ARROW}", custom_id="ud_right") + async def next_embed( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + logger.info("Going to next embed") + self._queue.rotate(-1) + if self.current_index == len(self._queue): + self.current_index = 1 + else: + self.current_index += 1 + await self.update_current_page() + + embed = self._queue[0] + await interaction.response.edit_message(embed=embed) + + @property + def initial(self) -> discord.Embed: + return self._initial diff --git a/objects/Prediction.py b/objects/Prediction.py new file mode 100644 index 00000000..4c4dc1c4 --- /dev/null +++ b/objects/Prediction.py @@ -0,0 +1,10 @@ +# TODO +# * Everything... lol +# TODO +import logging + +logger = logging.getLogger(__name__) + +# __all__ = [""] + +logger.info(f"{str(__name__).title()} module loaded!") diff --git a/objects/Recruits.py b/objects/Recruits.py index 3feecdff..f9a9a6c1 100644 --- a/objects/Recruits.py +++ b/objects/Recruits.py @@ -1,18 +1,13 @@ -import json -import re +import logging -import requests -from bs4 import BeautifulSoup +logger = logging.getLogger(__name__) -from utilities.constants import CROOT_SEARCH_LIMIT, HEADERS, UserError -from utilities.mysql import Process_MySQL, sqlTeamIDs +__all__ = ["Recruit", "RecruitInterest"] +logger.info(f"{str(__name__).title()} module loaded!") -class RecruitInterest: - offered = None - school = None - status = None +class RecruitInterest: def __init__(self, school: str, offered: str, status: str = None): self.offered = offered self.school = school @@ -22,43 +17,41 @@ def __init__(self, school: str, offered: str, status: str = None): class Recruit: def __init__( self, - _247_highlights, - _247_profile, - ranking_all_time, - bio, - city, - commitment_date, + bio: str, + cb_experts, + cb_predictions, + city: str, + commitment_date: str, committed, - committed_school, + committed_school: str, early_enrollee, early_signee, - cb_experts, - height, - key, - name, - ranking_national, - position, - ranking_position, - cb_predictions, - rating_numerical, - rating_stars, - recruit_interests, - recruit_interests_url, - school, - school_type, - scout_evaluation, - state, - state_abbr, - ranking_state, - thumbnail, - twitter, - walk_on, - weight, - year, - team_id, + height: str, + key: int, + name: str, + position: str, + ranking_all_time: int, + ranking_national: int, + ranking_position: int, + ranking_state: str, + rating_numerical: str, + rating_stars: str, + recruit_interests: list[RecruitInterest], + recruit_interests_url: str, + school: str, + school_type: str, + scout_evaluation: str, + state: str, + state_abbr: str, + team_id: int, + thumbnail: str, + twitter: str, + twofourseven_highlights: str, + twofourseven_profile: str, + walk_on: bool, + weight: str, + year: int, ): - self._247_highlights = _247_highlights - self._247_profile = _247_profile self.bio = bio self.cb_experts = cb_experts self.cb_predictions = cb_predictions @@ -88,499 +81,8 @@ def __init__( self.team_id = team_id self.thumbnail = thumbnail self.twitter = twitter + self.twofourseven_highlights = twofourseven_highlights + self.twofourseven_profile = twofourseven_profile self.walk_on = walk_on self.weight = weight self.year = year - - -states = { - "Alabama": "AL", - "Alaska": "AK", - "American Samoa": "AS", - "Arizona": "AZ", - "Arkansas": "AR", - "British Columbia": "BC", - "California": "CA", - "Colorado": "CO", - "Connecticut": "CT", - "Delaware": "DE", - "District Of Columbia": "DC", - "Florida": "FL", - "Georgia": "GA", - "Hawaii": "HI", - "Idaho": "ID", - "Illinois": "IL", - "Indiana": "IN", - "Iowa": "IA", - "Kansas": "KS", - "Kentucky": "KY", - "Louisiana": "LA", - "Maine": "ME", - "Maryland": "MD", - "Massachusetts": "MA", - "Michigan": "MI", - "Minnesota": "MN", - "Mississippi": "MS", - "Missouri": "MO", - "Montana": "MT", - "Nebraska": "NE", - "Nevada": "NV", - "New Hampshire": "NH", - "New Jersey": "NJ", - "New Mexico": "NM", - "New York": "NY", - "North Carolina": "NC", - "North Dakota": "ND", - "Ohio": "OH", - "Oklahoma": "OK", - "Oregon": "OR", - "Pennsylvania": "PA", - "Rhode Island": "RI", - "South Carolina": "SC", - "South Dakota": "SD", - "Tennessee": "TN", - "Texas": "TX", - "Utah": "UT", - "Vermont": "VT", - "Virginia": "VA", - "Washington": "WA", - "West Virginia": "WV", - "Wisconsin": "WI", - "Wyoming": "WY", -} -long_positions = { - "APB": "All-Purpose Back", - "ATH": "Athlete", - "CB": "Cornerback", - "DL": "Defensive Lineman", - "DT": "Defensive Tackle", - "DUAL": "Dual-Threat Quarterback", - "Edge": "Edge", - "FB": "Fullback", - "ILB": "Inside Linebacker", - "IOL": "Interior Offensive Lineman", - "K": "Kicker", - "LB": "Linebacker", - "LS": "Long Snapper", - "OC": "Center", - "OG": "Offensive Guard", - "OLB": "Outside Linebacker", - "OT": "Offensive Tackle", - "P": "Punter", - "PRO": "Pro-Style Quarterback", - "QB": "Quarterback", - "RB": "Running Back", - "RET": "Returner", - "S": "Safety", - "SDE": "Strong-Side Defensive End", - "TE": "Tight End", - "WDE": "Weak-Side Defensive End", - "WR": "Wide Receiver", -} - - -def get_team_id(search_player): - if search_player["CommitedInstitutionTeamImage"] is not None: - return int( - search_player["CommitedInstitutionTeamImage"] - .split("/")[-1] - .split("_")[-1] - .split(".")[0] - ) - else: - return 0 - - -def reformat_commitment_string(search_player): - if search_player["HighestRecruitInterestEventType"] == "HardCommit": - return "Hard Commit" - elif search_player["HighestRecruitInterestEventType"] == "OfficialVisit": - return None - elif search_player["HighestRecruitInterestEventType"] == "0": - return None - else: - return search_player["HighestRecruitInterestEventType"].strip() - - -def get_committed_school(all_team_ids, team_id): - try: - if team_id > 0: - for entry in all_team_ids: - if team_id == entry[team_id]: - return all_team_ids[team_id] - else: - return None - except KeyError: - return None - - -def is_early_enrolee(soup): - icon = soup.find_all(attrs={"class": "icon-time"}) - return True if icon else False - - -def reformat_height(height: str): - if height is None: - return "N/A" - else: - double_apo = '" ' - height = f"{height.replace('-', double_apo)}{double_apo}" - return height - - -def is_early_signee(soup): - icon = soup.find_all(attrs={"class": "signee-icon"}) - return True if icon else False - - -def get_cb_experts(soup, team_ids) -> list: - experts = [] - - try: - cbs_long_expert = soup.find_all(attrs={"class": "prediction-list long expert"}) - except: # noqa - return experts - - if len(cbs_long_expert) == 0: - return experts - - for expert in cbs_long_expert[0].contents: - try: - expert_name = expert.contents[1].string - predicted_team = None - - if expert.find_all("img", src=True): - predicted_team_id = int( - expert.find_all("img", src=True)[0]["src"] - .split("/")[-1] - .split(".")[0] - ) - try: - predicted_team = ( - team_ids[str(predicted_team_id)] - if predicted_team_id > 0 - else None - ) - except KeyError: - predicted_team = "Unknown Team" - else: - if len(expert.find_all("b", attrs={"class": "question-icon"})) == 1: - predicted_team = "Undecided" - - # If the pick is undecided, it doesn"t have a confidence - if predicted_team != "Undecided": - expert_confidence = f"{expert.contents[5].contents[1].text.strip()}, {expert.contents[5].contents[3].text.strip()}" - expert_string = ( - f"{expert_name} picks {predicted_team} ({expert_confidence})" - ) - else: - expert_string = f"{expert_name} is {predicted_team}" - - # I think 247 has some goofiness where there are some instances of "None" making a prediction, so I"m just not going to let those be added on - if expert_name is not None: - experts.append(expert_string) - except: # noqa - continue - - return experts - - -def get_cb_predictions(soup): - crystal_balls = [] - - predictions_header = soup.find_all(attrs={"class": "list-header-item"}) - - if len(predictions_header) == 0: - return crystal_balls - - cbs_long = cbs_one = None - - # When there are more than one predicted schools - try: - cbs_long = soup.find_all(attrs={"class": "prediction-list long"}) - except: # noqa - pass - # When there is only one predicted school - try: - cbs_one = soup.find_all(attrs={"class": "prediction-list one"}) - except: # noqa - pass - - if len(cbs_long) > 0: - for cb in cbs_long[0].contents: - try: - school_name = cb.contents[3].text.strip() - school_weight = cb.contents[5].text.strip() - school_string = f"{school_name}: {school_weight}" - # If there is an "Undecided" in the list, it won't have a confidence with it - if school_name != "Undecided": - school_confidence = f"{cb.contents[7].contents[1].text.strip()}, {cb.contents[7].contents[3].text.strip()}" - school_string += f"({school_confidence})" - crystal_balls.append(school_string) - except: # noqa - continue - - return crystal_balls - elif len(cbs_one) > 0: - single_school = cbs_one[0].contents[1] - single_school_name = single_school.contents[3].text.strip() - single_school_weight = single_school.contents[5].text.strip() - try: - single_school_confidence = f"{single_school.contents[7].contents[1].text.strip()}, {single_school.contents[7].contents[3].text.strip()}" - except: # noqa - single_school_confidence = "" - single_school_string = ( - f"{single_school_name}: {single_school_weight} ({single_school_confidence})" - ) - - crystal_balls.append(single_school_string) - else: - return ["N/A"] - - return crystal_balls - - -def get_all_time_ranking(soup): - recruit_rank = soup.find_all( - attrs={"href": "https://247sports.com/Sport/Football/AllTimeRecruitRankings/"} - ) - - try: - ranking = recruit_rank[1].contents[3].text - - if len(recruit_rank) > 1: - return ranking - else: - return 0 - except IndexError: - return 0 - - -def get_national_ranking(cur_player): - if cur_player["NationalRank"] is not None: - return cur_player["NationalRank"] - else: - return 0 - - -def get_position_ranking(cur_player): - if cur_player["PositionRank"] is not None: - return cur_player["PositionRank"] - else: - return 0 - - -def get_state_ranking(cur_player): - if cur_player["StateRank"] is not None: - return cur_player["StateRank"] - else: - return 0 - - -def reformat_composite_rating(cur_player): - if cur_player.get("CompositeRating", None) is None: - return "0" - else: - return f"{cur_player['CompositeRating']:0.4f}" - - -def get_recruit_interests(search_player): - reqs = requests.get(url=search_player["RecruitInterestsUrl"], headers=HEADERS) - interests_soup = BeautifulSoup(reqs.content, "html.parser") - interests = interests_soup.find( - "ul", attrs={"class": "recruit-interest-index_lst"} - ).find_all("li", recursive=False) - all_interests = [] - - # Goes through the list of interests and only adds in the ones that are offers - for index, interest in enumerate(interests): - offered = ( - interest.find("div", attrs={"class": "secondary_blk"}) - .find("span", attrs={"class": "offer"}) - .text.split(":")[1] - .strip() - ) - if offered == "Yes": - all_interests.append( - RecruitInterest( - school=interest.find("div", attrs={"class": "first_blk"}) - .find("a") - .text.strip(), - offered=offered, - status=interest.find("div", attrs={"class": "first_blk"}) - .find("span", attrs={"class": "status"}) - .find("span") - .text, - ) - ) - - del reqs, interests, interests_soup - - return all_interests - - -def get_school_type(soup): - institution_type = soup.find_all(attrs={"data-js": "institution-selector"}) - - if not len(institution_type) == 0: - institution_type = str(institution_type[0].text).strip() - return institution_type - else: - return "High School" - - -def get_state_abbr(cur_player): - try: - return states[cur_player["Hometown"]["State"]] - except KeyError: - return cur_player["Hometown"]["State"] - - -def get_thumbnail(cur_player): - if cur_player["DefaultAssetUrl"] == "/.": - return None - else: - return cur_player["DefaultAssetUrl"] - - -def get_twitter_handle(soup): - twitter = soup.find_all(attrs={"class": "tweets-comp"}) - try: - twitter = twitter[0].attrs["data-username"] - twitter = re.sub(r"[^\w*]+", "", twitter) - return twitter - except: # noqa - return "N/A" - - -def get_walk_on(soup): - icon = soup.find_all(attrs={"class": "icon-walkon"}) - - if icon: - return True - else: - return False - - -def reformat_weight(weight: str): - return f"{int(weight)} lbs." - - -def FootballRecruit(year, name): - all_team_ids = Process_MySQL(fetch="all", query=sqlTeamIDs) - name = name.split(" ") - - if len(name) == 1: - _247_search = f"https://247sports.com/Season/{year}-Football/Recruits.json?&Items=15&Page=1&Player.FirstName={name[0]}" - first_name = requests.get(url=_247_search, headers=HEADERS) - first_name = json.loads(first_name.text) - - _247_search = f"https://247sports.com/Season/{year}-Football/Recruits.json?&Items=15&Page=1&Player.LastName={name[0]}" - last_name = requests.get(url=_247_search, headers=HEADERS) - last_name = json.loads(last_name.text) - - search_results = first_name + last_name - elif len(name) == 2: - _247_search = f"https://247sports.com/Season/{year}-Football/Recruits.json?&Items=15&Page=1&Player.FirstName={name[0]}&Player.LastName={name[1]}" - - search_results = requests.get(url=_247_search, headers=HEADERS) - search_results = json.loads(search_results.text) - else: - raise UserError(f"Error occurred attempting to create 247sports search URL.") - - if not search_results: - raise UserError( - f"Unable to find [{name[0] if len(name) <= 1 else name[0] + ' ' + name[1]}] in the [{year}] class. Please try again!" - ) - - search_result_players = [] - - for index, search_player in enumerate(search_results): - cur_player = search_player["Player"] - - reqs = requests.get(url=search_player["Player"]["Url"], headers=HEADERS) - soup = BeautifulSoup(reqs.content, "html.parser") - - # Put into separate variables for debugging purposes - # red_shirt - _247_highlights = cur_player.get("Url") + "Videos/" - _247_profile = cur_player.get("Url", None) - bio = cur_player.get("Bio", None) - cb_experts = get_cb_experts(soup, all_team_ids) - cb_predictions = get_cb_predictions(soup) - city = cur_player["Hometown"].get("City", None) - commitment_date = search_player.get("AnnouncementDate", None) - committed = reformat_commitment_string(search_player) - committed_school = get_committed_school( - all_team_ids, get_team_id(search_player) - ) - early_enrollee = is_early_enrolee(soup) - early_signee = is_early_signee(soup) - height = reformat_height(cur_player.get("Height", None)) - key = cur_player.get("Key", None) - name = cur_player.get("FullName", None) - position = cur_player["PrimaryPlayerPosition"].get("Abbreviation", None) - ranking_all_time = get_all_time_ranking(soup) - ranking_national = get_national_ranking(cur_player) - ranking_position = get_position_ranking(cur_player) - ranking_state = get_state_ranking(cur_player) - rating_numerical = reformat_composite_rating(cur_player) - rating_stars = cur_player.get("CompositeStarRating", None) - recruit_interests = get_recruit_interests(search_player) - recruit_interests_url = cur_player.get("RecruitInterestsUrl", None) - school = cur_player["PlayerHighSchool"].get("Name", None) - school_type = get_school_type(soup) - scout_evaluation = cur_player.get("ScoutEvaluation", None) - state = cur_player["Hometown"].get("State", None) - state_abbr = get_state_abbr(cur_player) - team_id = get_team_id(search_player) - thumbnail = get_thumbnail(cur_player) - twitter = get_twitter_handle(soup) - walk_on = get_walk_on(soup) - weight = reformat_weight(cur_player.get("Weight", None)) - year = search_player.get("Year", None) - - search_result_players.append( - Recruit( - _247_highlights=_247_highlights, - _247_profile=_247_profile, - bio=bio, - cb_experts=cb_experts, - cb_predictions=cb_predictions, - city=city, - commitment_date=commitment_date, - committed=committed, - committed_school=committed_school, - early_enrollee=early_enrollee, - early_signee=early_signee, - height=height, - key=key, - name=name, - position=position, - ranking_all_time=ranking_all_time, - ranking_national=ranking_national, - ranking_position=ranking_position, - ranking_state=ranking_state, - rating_numerical=rating_numerical, - rating_stars=rating_stars, - recruit_interests=recruit_interests, - recruit_interests_url=recruit_interests_url, - # red_shirt=None, - school=school, - school_type=school_type, - scout_evaluation=scout_evaluation, - state=state, - state_abbr=state_abbr, - team_id=team_id, - thumbnail=thumbnail, - twitter=twitter, - walk_on=walk_on, - weight=weight, - year=year, - ) - ) - - if index == CROOT_SEARCH_LIMIT - 1: - break - - return search_result_players diff --git a/objects/Schedule.py b/objects/Schedule.py index 96d0fff6..fd7161eb 100644 --- a/objects/Schedule.py +++ b/objects/Schedule.py @@ -1,59 +1,69 @@ -import datetime +import logging import urllib.parse +from datetime import datetime, timedelta +from typing import Union import requests from bs4 import BeautifulSoup -from utilities.constants import DT_STR_FORMAT, DT_TBA_TIME, HEADERS, TZ +from helpers.constants import HEADERS, DT_TBA_TIME, DT_STR_FORMAT, TZ +from objects.Exceptions import ScheduleException + +logger = logging.getLogger(__name__) + +__all__ = ["HuskerSchedule"] + +logger.info(f"{str(__name__).title()} module loaded!") class SeasonStats: - wins = None losses = None + wins = None def __init__(self, wins=0, losses=0): - self.wins = wins self.losses = losses + self.wins = wins class HuskerOpponent: def __init__(self, name, ranking, icon, date_time, week, location, outcome=None): - self.name = name - self.ranking = ranking - self.icon = icon self.date_time = date_time - self.week = week + self.icon = icon self.location = location + self.name = name self.outcome = outcome + self.ranking = ranking + self.week = week class HuskerDotComSchedule: def __init__( self, + conference, + game_date_time, + home, + icon, location, opponent, - icon, outcome, ranking, week, - game_date_time, - home, - conference, ): + self.conference = conference + self.game_date_time = game_date_time + self.home = home + self.icon = icon self.location = location self.opponent = opponent - self.icon = icon self.outcome = outcome self.ranking = ranking self.week = week - self.game_date_time = game_date_time - self.home = home - self.conference = conference -def collect_opponent(game, year, week): +def collect_opponent(game, year, week) -> Union[HuskerOpponent, str]: # This is the culmination of going line by line through Huskers.com HTML and CSS. # If the website changes, this will more than likely need to change. + logger.info(f"Collecting opponent informaitn for Week {week} {year}") game = game.contents[1] try: name = ( @@ -64,30 +74,38 @@ def collect_opponent(game, year, week): .text.strip() .replace("\n", " ") ) + ranking = None + if "#" in name: try: [ranking, name] = str(name).split(" ", maxsplit=1) except ValueError: pass + location = game.contents[3].contents[1].text.strip().replace("\n", " ") + if "Buy Tickets" in location: location = location.split("Buy Tickets ")[1].replace( " Memorial Stadium", "" ) + temp = game.contents[1].contents[1].contents[1].attrs["data-src"] + icon = None + if "://" in temp: # game.contents[1].contents[1].contents[1].attrs["data-src"]: # icon = temp_icon try: url_parser = urllib.parse.urlparse(temp) icon = f"{url_parser.scheme}://{url_parser.netloc}{urllib.parse.quote(url_parser.path)}" - except: + except: # noqa pass else: icon = ( "https://huskers.com" + game.contents[1].contents[1].contents[1].attrs["data-src"] ) + _date = ( game.contents[1] .contents[3] @@ -96,6 +114,7 @@ def collect_opponent(game, year, week): .contents[1] .text.strip() ) + _time = ( game.contents[1] .contents[3] @@ -120,9 +139,6 @@ def collect_opponent(game, year, week): except IndexError: outcome = "" - # if "(" in _time: - # _time = "TBA" - if _time == "Noon": _time = _time.replace("Noon", "12:00 PM") @@ -132,11 +148,6 @@ def collect_opponent(game, year, week): date_time = f"{_date[0:6]} {year} {_time}" del _date, _time - # if len(conference.contents) > 1: - # conference = conference.contents[1].text.strip() - # else: - # conference = "Non-Con" - return HuskerOpponent( name=name, ranking=ranking, @@ -147,16 +158,21 @@ def collect_opponent(game, year, week): outcome=outcome, ) except IndexError: - return "Unknown Opponent" + return "Unknown Opponent" # TODO Should this be a HuskerOpponent object? + +def HuskerSchedule( + year=datetime.now().year, +) -> tuple[list[HuskerDotComSchedule], SeasonStats]: + logger.info(f"Creating Husker schedule for '{year}'") -def HuskerSchedule(sport: str, year=datetime.datetime.now().year): reqs = requests.get( - url=f"https://huskers.com/sports/{sport}/schedule/{year}", headers=HEADERS + url=f"https://huskers.com/sports/football/schedule/{year}", headers=HEADERS ) - if not reqs.status_code == 200: - raise ConnectionError("Unable to retrieve schedule from Huskers.com.") + assert reqs.status_code == 200, ScheduleException( + "Unable to retrieve schedule from Huskers.com." + ) soup = BeautifulSoup(reqs.content, "html.parser") games_raw = soup.find_all(attrs={"class": "sidearm-schedule-game"}) @@ -176,48 +192,48 @@ def HuskerSchedule(sport: str, year=datetime.datetime.now().year): week += 1 opponent = collect_opponent(game, year, week) - if opponent == "Unknown Opponent": + if opponent == "Unknown Opponent": # TODO What am I doing here? continue if "TBA" in opponent.date_time: # Specific time to reference later for TBA games gdt_string = f"{opponent.date_time[0:6]} {year} {DT_TBA_TIME}" - opponent.date_time = datetime.datetime.strptime( + opponent.date_time = datetime.strptime( gdt_string, DT_STR_FORMAT ).astimezone(tz=TZ) else: - if "or" in opponent.date_time.lower(): - temp = opponent.date_time.split(" or ") + if "or" in str(opponent.date_time).lower(): + temp = str(opponent.date_time).split(" or ") if ":" in temp[0]: opponent.date_time = f"{temp[0]} {temp[1][-3:]}" else: opponent.date_time = f"{temp[0]}:00 {temp[1][-3:]}" - opponent.date_time = datetime.datetime.strptime( + opponent.date_time = datetime.strptime( opponent.date_time.replace("A.M.", "AM") .replace("a.m.", "AM") .replace("P.M.", "PM") .replace("p.m.", "PM"), DT_STR_FORMAT, ).astimezone(tz=TZ) - opponent.date_time += datetime.timedelta(hours=1) + opponent.date_time += timedelta(hours=1) - teams = ( + conferance_teams = ( "Illinois", + "Iowa", + "Maryland", "Michigan State", - "Northwestern", + "Michigan", "Minnesota", - "Purdue", + "Northwestern", "Ohio State", - "Wisconsin", - "Iowa", - "Michigan", - "Maryland", - "Rutgers", "Penn State", + "Purdue", + "Rutgers", + "Wisconsin", ) - conf = opponent.name in teams - home = "Lincoln, Neb." in opponent.location + conference: bool = opponent.name in conferance_teams + home: bool = "Lincoln, Neb." in opponent.location games.append( HuskerDotComSchedule( @@ -228,7 +244,7 @@ def HuskerSchedule(sport: str, year=datetime.datetime.now().year): ranking=opponent.ranking, week=opponent.week, game_date_time=opponent.date_time, - conference=conf, + conference=conference, home=home, ) ) diff --git a/objects/Survey.py b/objects/Survey.py new file mode 100644 index 00000000..bc049d4e --- /dev/null +++ b/objects/Survey.py @@ -0,0 +1,199 @@ +import hashlib +import logging +import math +import random +import string +from typing import Optional, AnyStr, Union, List, Any + +import discord +from discord import ( + ButtonStyle, + Client, + Embed, + Forbidden, + HTTPException, + Interaction, +) +from discord.ext.commands import Bot +from discord.ui import View, Button + +from helpers.constants import GLOBAL_TIMEOUT +from helpers.misc import formatPrettyTimeDelta +from objects.Exceptions import SurveyException + +logger = logging.getLogger(__name__) + +__all__ = ["Survey"] + + +def generate_random_key() -> str: + return "".join( + random.SystemRandom().choice(string.ascii_uppercase + string.digits) + for _ in range(10) + ) + + +class SurveyOption: + def __init__(self, label: AnyStr): + assert isinstance(label, str), SurveyException("Label must be a string!") + + self.custom_id: AnyStr = f"{generate_random_key()}_survey_option" + self.label: AnyStr = label + self.value: int = 0 + + +class Survey: + def __init__( + self, + client: Union[Client, Bot], + interaction: Interaction, + options: Union[AnyStr, List[Any], List[SurveyOption]], + question: AnyStr, + timeout: Optional[int] = GLOBAL_TIMEOUT, + ) -> None: + # Space deliminated options + options = options.strip() + if " " in options: + options = options.split() + else: + options = [options] + + assert len(options) == len(set(options)), SurveyException( + f"You cannot use the same option more than once!" + ) + + for index, opt in enumerate(options): + options[index] = SurveyOption(opt) # noqa + + max_options = 3 + + assert isinstance(question, str), SurveyException("Question must be a string!") + assert [isinstance(opt, SurveyOption) for opt in options], SurveyException( + "Options must be a SurveyOption object!" + ) + assert 2 <= len(options) <= max_options, SurveyException( + f"You must have between 2 and {max_options} options!" + ) + assert timeout is not None, SurveyException("You must provide a timeout value!") + + self.client: Union[Client, client] = client + self.interaction: Interaction = interaction + self.embed: Optional[Embed] = None + self.options: List[SurveyOption] = options + self.question: Optional[AnyStr] = question + self.timeout: int = math.ceil(timeout) + + class SurviewButtons(View): + def __init__(self, options: List[SurveyOption], timeout: int = None) -> None: + super().__init__(timeout=timeout) + self.options: List[SurveyOption] = options + self.tally_str: str = "Tally for: " + self.footer_str: str = "Users: " + + async def process_button(self, interaction: Interaction, button: Button): + logger.info( + f"{interaction.user.display_name} selected option '{button.label.upper()}'" + ) + + await interaction.response.send_message( + "Processing original_message!", ephemeral=True + ) + + embed: discord.Embed = interaction.message.embeds[0] + if interaction.user.display_name in embed.footer.text: + await interaction.edit_original_message( + content="You cannot vote more than once!" + ) + return + embed.set_footer( + text=f"{embed.footer.text} {interaction.user.display_name} " + ) + + for index, field in enumerate(embed.fields): + if field.name == f"{self.tally_str}{button.label.upper()}": + field.value = str(int(field.value) + 1) + embed.set_field_at( + index=index, + name=field.name, + value=field.value, + inline=field.inline, + ) + break + + # try: + # await interaction.followup.send_message(content="Response recorded!") + # except (HTTPException, NotFound, Forbidden, TypeError, ValueError) as e: + # logger.info(e) + # return + + try: + await interaction.message.edit(embed=embed) + except (HTTPException, Forbidden, TypeError) as e: + logger.info(e) + return + + @discord.ui.button(label="one", custom_id="opt_one", style=ButtonStyle.gray) + async def option_one(self, interaction: discord.Interaction, button: Button): + await self.process_button(interaction=interaction, button=button) + + @discord.ui.button(label="two", custom_id="opt_two", style=ButtonStyle.gray) + async def option_two(self, interaction: discord.Interaction, button: Button): + await self.process_button(interaction=interaction, button=button) + + @discord.ui.button(label="three", custom_id="opt_four", style=ButtonStyle.gray) + async def option_three(self, interaction: discord.Interaction, button: Button): + await self.process_button(interaction=interaction, button=button) + + async def on_timeout(self) -> None: + self.stop() + + def create_embed(self) -> None: + embed = Embed( + title="Survey", + description=f"Please provide your feedback. This survey timesout in {formatPrettyTimeDelta(self.timeout)} seconds.", + color=0xD00000, + ) + embed.set_footer(text="Users: ") + embed.add_field( + name="Survey Question", + value=self.question, + ) + for index, opt in enumerate(self.options): + embed.add_field( + name=f"Tally for: {opt.label.upper()}", + value=str(opt.value), + inline=True, + ) + + self.embed = embed + + def update_embed(self, user_id: str, opt: str) -> None: + hashed_user = hashlib.sha1(str(user_id).encode("UTF-8")).hexdigest()[:10] + if hashed_user in self.embed.footer.text: + return + + for option in self.options: + if option.label == opt: + option.value += 1 + break + + footer_text = self.embed.footer.text + self.create_embed() + self.embed.set_footer(text=f"{footer_text} {hashed_user} ") + + async def send(self) -> None: + await self.interaction.response.defer() + + self.create_embed() + + view = self.SurviewButtons(self.options) + for index, child in enumerate(view.children): + if index > len(self.options): + del child + break + child.label = self.options[index].label + + await self.interaction.followup.send(embed=self.embed, view=view) + + async def close_survey(self) -> None: + await self.interaction.edit_original_message(view=None) diff --git a/objects/Thread.py b/objects/Thread.py index 741a567d..d5738a20 100644 --- a/objects/Thread.py +++ b/objects/Thread.py @@ -1,198 +1,76 @@ -import asyncio -import time -import typing - -import discord -import tweepy -from discord_slash.context import SlashContext - -from utilities.constants import pretty_time_delta -from utilities.embed import build_embed -from utilities.mysql import Process_MySQL, sqlUpdateTasks - -exitFlag = 0 - - -def log(message: str, level: int): - import datetime - - if level == 0: - print(f"[{datetime.datetime.now()}] ### Twitter Stream: {message}") - elif level == 1: - print(f"[{datetime.datetime.now()}] ### ~~~ Twitter Stream: {message}") - - -class TwitterStreamListener(tweepy.Stream): - def __init__( - self, - consumer_key, - consumer_secret, - access_token, - access_token_secret, - message_func, - alert_func, - loop, - ): - super().__init__( - consumer_key, consumer_secret, access_token, access_token_secret - ) - self.consumer_key = consumer_key - self.consumer_secret = consumer_secret - self.access_token = access_token - self.access_token_secret = access_token_secret - self.message_func = message_func - self.alert_func = alert_func - self.loop = loop - self.cooldown = 1 - - def reset_cooldown(self): - self.cooldown = 0 - - def process_cooldown(self): - log(f"Pausing for {self.cooldown} seconds", 1) - # asyncio.sleep(self.cooldown) - time.sleep(self.cooldown) - self.cooldown = min(self.cooldown * 2, 600) # 10 minute max - log(f"Pause complete. New timer is {self.cooldown} seconds", 1) - - def send_message(self, tweet): - log("Sending a new tweet", 1) - future = asyncio.run_coroutine_threadsafe(self.message_func(tweet), self.loop) - future.result() - - def send_alert(self, message): - log("Sending an alert", 1) - future = asyncio.run_coroutine_threadsafe(self.alert_func(message), self.loop) - future.result() - - def on_connect(self): - log(f"Stream connected", 0) - self.send_alert(f"Stream connected!") - self.reset_cooldown() - - def on_status(self, status): - # if not status.retweeted and status.in_reply_to_status_id is None and not hasattr(status, "retweeted_status"): - log(f"Status received", 1) - self.send_message(status) - - def on_warning(self, notice): - log(f"Warning: {notice}", 1) - - def on_connection_error(self): - log(f"Connection error", 1) - self.send_alert(f"Stream failed to connect.") - self.process_cooldown() - # return True # Reconnect - - def on_request_error(self, status_code): - log(f"Request Error: {status_code}", 1) - if status_code == 420: - rl_msg = ( - f"Stream is being rate limited. Retrying in {self.cooldown} seconds..." - ) - log( - rl_msg, - 1, - ) - self.send_alert(rl_msg) - self.process_cooldown() - else: - log(f"Request Error: {status_code}", 1) - return True # Reconnect - - async def on_disconnect_message(self, message): - log(f"Stream disconnected. Notice: {message}", 1) - self.send_alert( - f"Stream was disconnected. Retrying in {self.cooldown} seconds..." - ) - self.process_cooldown() - self.send_alert("Stream attempting to reconnect!") - - return True # Reconnect - - def on_keep_alive(self): - log("Stream has received a keep alive signal.", 1) - # self.process_cooldown() - - def on_exception(self, exception): - log(f"Exception: {exception}", 1) - self.process_cooldown() - return True - - def on_limit(self, track): - log( - f"Stream is being rate limited. Unable to deliver {track} tweets", - 1, - ) - self.send_alert( - f"Stream is being rate limited. Unable to deliver {track} tweets" - ) - self.process_cooldown() - - # self.send_alert("Twitter stream attempting to reconnect!") - # return True # Reconnect - - -async def send_reminder( - num_seconds, - destination: typing.Union[discord.Member, discord.TextChannel], - message: str, - source: typing.Union[discord.Member, discord.TextChannel], - alert_when, - missed=False, -): - log( - f"Starting thread for [{pretty_time_delta(num_seconds)}] seconds. Send_When == [{alert_when}].", - 1, - ) - - if not missed: - log(f"Destination: [{destination}]", 1) - log(f"Message: [{message[:15] + '...'}]", 1) - - await asyncio.sleep(num_seconds) - - embed = build_embed( - title="Bot Frost Reminder", - inline=False, - fields=[["Author", source.mention], [f"Reminder!", message]], - ) - else: - embed = build_embed( - title="Missed Bot Frost Reminder", - inline=False, - fields=[ - ["Original Reminder Date Time", alert_when], - ["Author", source], - ["Message", message], - ], - ) - await destination.send(embed=embed) - - Process_MySQL( - sqlUpdateTasks, - values=(0, str(destination.id), message, alert_when, str(source)), - ) - - log(f"Thread completed successfully!", 0) - - -async def end_timeout(duration: int, ctx: SlashContext, who: discord.Member): - await asyncio.sleep(duration) - - from commands.admin import process_nebraska - - if await process_nebraska(ctx=ctx, who=who): - embed = build_embed( - title="Return to Nebraska", - inline=False, - fields=[ - ["Welcome back!", f"[{who.mention}] is welcomed back to Nebraska!"], - ["Welcomed by", ctx.author.mention], - ], - ) - await ctx.send(embed=embed) - log("Nebraska command complete", 0) - else: - await ctx.send("Unable to complete the Nebraska command!", hidden=True) - log("Unable to complete the Nebraska command", 0) +# TODO +# * Modernize and revamp +# TODO +import logging + +logger = logging.getLogger(__name__) + +# __all__ = [""] + +logger.info(f"{str(__name__).title()} module loaded!") + +# import asyncio +# import typing +# +# import discord +# import tweepy +# import time +# +# from helpers.constants import pretty_time_delta +# from helpers.embed import build_embed +# from helpers.mysql import Process_MySQL, sqlUpdateTasks +# +# exitFlag = 0 +# +# +# def log(level: int, message: str): +# import datetime +# +# if level == 0: +# print(f"[{datetime.datetime.now()}] ### Twitter Stream: {message}") +# elif level == 1: +# print(f"[{datetime.datetime.now()}] ### ~~~ Twitter Stream: {message}") +# +# +# async def send_reminder( +# num_seconds, +# destination: typing.Union[discord.Member, discord.TextChannel], +# message: str, +# source: typing.Union[discord.Member, discord.TextChannel], +# alert_when, +# missed=False, +# ): +# log( +# f"Starting thread for [{pretty_time_delta(num_seconds)}] seconds. Send_When == [{alert_when}].", +# 1, +# ) +# +# if not missed: +# log(1, f"Destination: [{destination}]") +# log(1, f"Message: [{message[:15] + '...'}]") +# +# await asyncio.sleep(num_seconds) +# +# embed = build_embed( +# title="Bot Frost Reminder", +# , +# fields=[["Author", source.mention], [f"Reminder!", message]], +# ) +# else: +# embed = build_embed( +# title="Missed Bot Frost Reminder", +# , +# fields=[ +# ["Original Reminder Date Time", alert_when], +# ["Author", source], +# ["Message", message], +# ], +# ) +# await destination.send(embed=embed) +# +# Process_MySQL( +# sqlUpdateTasks, +# values=(0, str(destination.id), message, alert_when, str(source)), +# ) +# +# log(0, f"Thread completed successfully!") diff --git a/objects/TweepyStreamListener.py b/objects/TweepyStreamListener.py new file mode 100644 index 00000000..c35e7c8a --- /dev/null +++ b/objects/TweepyStreamListener.py @@ -0,0 +1,222 @@ +import asyncio +import json +import logging + +import dateutil.parser +import discord +import tweepy + +from helpers.constants import ( + CHAN_TWITTERVERSE, + DEBUGGING_CODE, + CHAN_GENERAL, + CHAN_RECRUITING, +) +from helpers.embed import buildEmbed, buildTweetEmbed + +logger = logging.getLogger(__name__) + +# Example +# task = asyncio.run_coroutine_threadsafe( +# send_tweet_alert(self.client, "Connected!"), self.client.loop +# ) +# task.result() + + +class MyTweet(object): + def __init__(self, tweet_data) -> None: + self.data = None + self.includes = None + self.matching_rules = None + + for key in tweet_data: + setattr(self, key, tweet_data[key]) + + +class TweetMediaData(object): + def __init__(self, tweet_data) -> None: + self.url = None + + for key in tweet_data: + setattr(self, key, tweet_data[key]) + + +class TweetQuoteData(object): + def __init__(self, tweet_data) -> None: + self.id = None + self.text = None + self.author_id = None + for key in tweet_data: + setattr(self, key, tweet_data[key]) + + +class TweetUserData(object): + def __init__(self, data) -> None: + self.public_metrics = None + self.verified = None + self.profile_image_url = None + self.name = None + self.username = None + self.followers = None + self.following = None + self.tweet_count = None + self.listed_count = None + for key in data: + setattr(self, key, data[key]) + + +async def send_tweet_alert(client: discord.Client, message) -> None: + if DEBUGGING_CODE: + logger.info("Skipping alert because debugging") + return + + logger.info(f"Tweet alert received: {message}") + embed = buildEmbed( + title="Husker Twitter", + fields=[ + dict( + name="Twitter Stream Alert", + value=str(message), + ) + ], + ) + + twitter_channel: discord.TextChannel = await client.fetch_channel(CHAN_TWITTERVERSE) + await twitter_channel.send(embed=embed) + + logger.info(f"Twitter alert sent!") + + +async def send_tweet(client: discord.Client, tweet: MyTweet) -> None: + class TwitterButtons(discord.ui.View): + def __init__(self): + super().__init__() + + @discord.ui.button( + label="Send to General", + custom_id="send_to_general", + style=discord.ButtonStyle.gray, + ) + async def send_to_general( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + chan = await client.fetch_channel(CHAN_GENERAL) + await chan.send( + f"Tweet forwarded by {interaction.user.mention}", + embed=interaction.message.embeds[0], + ) + await interaction.response.send_message("Tweet forwarded!", ephemeral=True) + + @discord.ui.button( + label="Send to Recruiting", + custom_id="send_to_recruiting", + style=discord.ButtonStyle.gray, + ) + async def send_to_recruiting( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + chan = await client.fetch_channel(CHAN_RECRUITING) + await chan.send( + f"Tweet forwarded by {interaction.user.mention}", + embed=interaction.message.embeds[0], + ) + await interaction.response.send_message("Tweet forwarded!", ephemeral=True) + + # NOTE Discord API preventing creating a URL button at this scope + + logger.info(f"Sending tweet") + + author = None # noqa + for user in tweet.includes["users"]: + if tweet.data["author_id"] == user["id"]: + author: TweetUserData = TweetUserData(user) + break + + medias = [] + if "media" in tweet.includes: + for item in tweet.includes["media"]: + medias.append(TweetMediaData(item)) + + quotes = [] + if "tweets" in tweet.includes: + for item in tweet.includes["tweets"]: + quotes.append(TweetQuoteData(item)) + + embed = buildTweetEmbed( + name=author.name, + username=author.username, + author_metrics=author.public_metrics, + verified=author.verified, + source=tweet.data["source"], + text=tweet.data["text"], + tweet_metrics=tweet.data["public_metrics"], + tweet_id=tweet.data["id"], + tweet_created_at=dateutil.parser.parse(tweet.data["created_at"]), + profile_image_url=author.profile_image_url, + urls=tweet.data["entities"], + medias=medias, + quotes=quotes, + ) + + view = TwitterButtons() + twitter_channel: discord.TextChannel = await client.fetch_channel(CHAN_TWITTERVERSE) + await twitter_channel.send(embed=embed, view=view) + + logger.info(f"Tweet sent!") + + +class StreamClientV2(tweepy.StreamingClient): + def __init__( + self, + bearer_token, + client: discord.Client, + **kwargs, + ) -> None: + super().__init__(bearer_token, **kwargs) + self.client = client + logger.info("StreamClientV2 Initialized") + + def on_connect(self) -> None: + logger.info("Connected") + task = asyncio.run_coroutine_threadsafe( + send_tweet_alert(self.client, "Connected!"), self.client.loop + ) + task.result() + + def on_request_error(self, status_code) -> None: + logger.exception(f"Request Error: {status_code}") + + def on_connection_error(self) -> None: + logger.exception(f"Connection Error") + task = asyncio.run_coroutine_threadsafe( + send_tweet_alert( + self.client, "The Twitter Stream had an error connecting!" + ), + self.client.loop, + ) + task.result() + + def on_disconnect(self) -> None: + logger.warning("Disconnected") + task = asyncio.run_coroutine_threadsafe( + send_tweet_alert(self.client, "The Twitter Stream has been disconnected!"), + self.client.loop, + ) + task.result() + + def on_errors(self, errors) -> None: + logger.exception(f"Error received\n{errors}") + + def on_closed(self, response) -> None: + logger.warning(f"Closed: {response}") + + def on_exception(self, exception) -> None: + logger.exception(f"Exception: {type(exception)} -- {exception}") + + def on_data(self, raw_data) -> None: + logger.info(f"Raw Data\n{raw_data}") + processed_data = json.loads(raw_data) + task = asyncio.run_coroutine_threadsafe( + send_tweet(self.client, MyTweet(processed_data)), self.client.loop + ) + task.result() diff --git a/objects/Weather.py b/objects/Weather.py index 3040cd90..2f47c186 100644 --- a/objects/Weather.py +++ b/objects/Weather.py @@ -1,15 +1,24 @@ +import logging from datetime import datetime, timezone +logger = logging.getLogger(__name__) + +__all__ = ["WeatherResponse", "WeatherHour"] +# +logger.info(f"{str(__name__).title()} module loaded!") + class WeatherHour: - def __init__(self, dictionary): + def __init__(self, dictionary) -> None: + self.wind_speed = None + self.temp = None for key, value in dictionary.items(): setattr(self, key, value) self._data_len = len(dictionary) class WeatherMain: - def __init__(self, dictionary): + def __init__(self, dictionary) -> None: for key, value in dictionary.items(): setattr(self, key, value) self._data_len = len(dictionary) @@ -19,7 +28,7 @@ def __len__(self): class WeatherCoord: - def __init__(self, dictionary): + def __init__(self, dictionary) -> None: for key, value in dictionary.items(): setattr(self, key, value) self._data_len = len(dictionary) @@ -29,7 +38,7 @@ def __len__(self): class WeatherSys: - def __init__(self, dictionary): + def __init__(self, dictionary) -> None: for key, value in dictionary.items(): if key == "sunrise": self.sunrise = datetime.utcfromtimestamp(value).astimezone( @@ -48,7 +57,7 @@ def __len__(self): class WeatherWeather: - def __init__(self, dictionary): + def __init__(self, dictionary) -> None: for key, value in dictionary.items(): setattr(self, key, value) self._data_len = len(dictionary) @@ -58,7 +67,7 @@ def __len__(self): class WeatherWind: - def __init__(self, dictionary): + def __init__(self, dictionary) -> None: for key, value in dictionary.items(): setattr(self, key, value) self._data_len = len(dictionary) @@ -68,7 +77,7 @@ def __len__(self): class WeatherClouds: - def __init__(self, dictionary): + def __init__(self, dictionary) -> None: for key, value in dictionary.items(): setattr(self, key, value) self._data_len = len(dictionary) @@ -78,7 +87,7 @@ def __len__(self): class WeatherRain: - def __init__(self, dictionary): + def __init__(self, dictionary) -> None: for key, value in dictionary.items(): setattr(self, key, value) self._data_len = len(dictionary) @@ -88,7 +97,7 @@ def __len__(self): class WeatherSnow: - def __init__(self, dictionary): + def __init__(self, dictionary) -> None: for key, value in dictionary.items(): setattr(self, key, value) self._data_len = len(dictionary) @@ -98,7 +107,9 @@ def __len__(self): class WeatherResponse: - def __init__(self, dictionary): + def __init__(self, dictionary) -> None: + self.timezone = None + self.cod = None for key, value in dictionary.items(): if key == "main": self.main = WeatherMain(value) @@ -123,7 +134,5 @@ def __init__(self, dictionary): self.dt = datetime.utcfromtimestamp(value).astimezone( tz=timezone.utc ) - # elif key == "timezone": - # self.timezone = datetime.utcfromtimestamp(value) else: setattr(self, key, value) diff --git a/objects/Winsipedia.py b/objects/Winsipedia.py index a186f1dc..46600671 100644 --- a/objects/Winsipedia.py +++ b/objects/Winsipedia.py @@ -1,7 +1,18 @@ +# TODO +# * Modernize and revamp +# TODO +import logging + import requests from bs4 import BeautifulSoup -from utilities.constants import HEADERS +from helpers.constants import HEADERS + +logger = logging.getLogger(__name__) + +__all__ = ["TeamStatsWinsipediaTeam", "CompareWinsipediaTeam", "CompareWinsipedia"] + +logger.info(f"{str(__name__).title()} module loaded!") class TeamStatsWinsipediaTeam: diff --git a/requirements.txt b/requirements.txt index 9109a303..6634bdee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,22 +1,16 @@ -Pillow>=8.3.1 -beautifulsoup4>=4.9.3 -cfbd>=4.1.4 -cryptography>=3.4.7 -discord-surveys -discord>=1.7.3 -imgurpython>=1.1.7 -markovify>=0.9.3 -numpy>=1.21.2 -pandas>=1.3.2 -paramiko -pymysql>=1.0.2 -python-dotenv>=0.19.0 -pytz>=2021.1 -requests>=2.26.0 -tweepy -validators>=0.18.2 -discord-py-interactions==3.0.2 -dinteractions-Paginator -opencv-python -nest_asyncio -# Verified 28 August 2021 \ No newline at end of file +Pillow==9.1.1 +beautifulsoup4==4.11.1 +cfbd>=4.3.2 +cryptography==37.0.2 +discord>=2.0.0 +markovify==0.9.4 +numpy==1.22.4 +opencv-python==4.5.5.64 +paramiko==2.11.0 +pymysql==1.0.2 +python-dateutil==2.8.2 +python-dotenv==0.20.0 +pytz==2022.1 +requests>=2.27.1 +tweepy==4.10.0 +validators==0.19.0 \ No newline at end of file diff --git a/resources/husk_messages.txt b/resources/husk_messages.txt index 289fb037..4b953751 100644 --- a/resources/husk_messages.txt +++ b/resources/husk_messages.txt @@ -9,18 +9,14 @@ The rule of two was pretty dope lore. I read a Star Wars book about darth bane that was fire. Bill burr is in mandolorian?. Dumbledore dies!. -||<:MaxHype:686664366535475204> ||. Its a great test as to whether you can do your own research. If you think you can, youre too stupid. just in time for the bye week. -thank god. great man. how long till actual nba games start?. -big baby!. I was like, why would the pelicans trade zion?. last week i thought the suns were landing zion. i get taken by all the nba trade bullshit rumors. -ugh. a nets sixers series would be nuts if that happened. why would they do that?. are they considering trading kyrie for ben?. @@ -30,19 +26,16 @@ two ass buckets being ass buckets. I fucking hate that. millenials in shambles. eric strickland or gtfo. -gross. simmons?. who is marty going to watch play?. bring in ricky. send his ass to minnesota. -i love it. Ben sounds like a brat. bill burr is great. You got roots in philly?. I cant remember. second round exit tho. Or did you make it 3?. -Lol. that and learning to d up. These AAU kids are so fragile. The ben simmons drama give me life. @@ -63,9 +56,6 @@ went form like 40 to 80 percent. thats true. ny tough i mean unstoppable. hes tough when hes hitting shots.. -giannis. -<:bo:508088396162531328>. -<:sadnhat:612423571351928993>. yall got giannis?. Imagine stanning for ben simmons. but that moment was real fun.. @@ -88,7 +78,6 @@ out of the nba chat history nerds. what are they doing with ben?. people put all this effort into chasing a dream and quit over some delusion. Fuck curry. -lol. Curry?. I'd that petrovich. Larry bird ray Allen Reggie Miller. @@ -109,7 +98,6 @@ he got there last season?. okafor is still in the league?. Embrace the hate. You play in philly buddy.. -Zoomer. They don't like him now he wants out. Typical sooner. get that anthony guy from minnesota. @@ -125,10 +113,8 @@ I'd rather make the defense work. I'd rather have some people touch the ball.. Lost in 5 or did they take you six?. Their best playmaker. -Lol fine. Murray. He's awesome. -Their. First round exit against a team without there best scorer. How's embiid gonna feel when he gets a block runs the floor for dame to chuck up a long three. He'll get hot and blow a team away no doubt. @@ -202,17 +188,14 @@ thought about buying one of the toys and seeing if a could get it autographed.. the white mamba correct?. still remember them haven't looked at them in like twenty years. just opened up my old shaq card collection. -excuse me. I believe they say hoop now. do actuaries ball?. she has a road named after her so i hope. my wife used to sell louie vuitton to her and griner as well. definitey her. looks like her at least cant read the jersey. -yeah. seen her live a couple times. Shes really fucking good. -lol. hova!. they resigned frank. yeah im siked about mcgee. @@ -228,7 +211,6 @@ baynes was solid\. remember when he had IT running the show?. horrible run organization. suns in four. -whatever. its always like why did we just sign allen houston to a thousand years. its never like wow, good pick up. they never make any good moves.. @@ -240,15 +222,12 @@ are we calling kemba a star?. will the knicks ever land a star?. knicks shoulda went after derozen. bulls making some moves. -dope. did Melo go to the lakeshow?. -max hype. Bringing back Abdul Nader. thought thryd get a big name. knicks fizzling out. any news on simmons?. they signed him for 4 years? interesting. -hazaah. ahh i see. ah i missed it. little worried he might join kawhi somewhere. @@ -259,7 +238,6 @@ NO had some attitude issues. theyre on tv more than leave it to beaver reruns. duke is duke. Trust the process. -Uh oh. east is going to be tough at the top again. Gotta imagine the nets come back stronger,. you cant give up four straight possessions of beal getting worked on the switch. @@ -272,7 +250,6 @@ sounds horrible. crowded expensive and full of politicians. somewhere there is a talented player on a shitty team in a shitty state who wants out. why would houston care. -really. harden wanted to justice league with durant and kyrie. scrambling to get a bradely beal. now look at you. @@ -285,7 +262,6 @@ ben is going to hate the sixers when they trade him. everyone hates the jazz. that dude can ball. youre in the finals with mitchel. -frauds. they arent competing in the west ill tell you that much. utah probably loves picks. go out and get mitchel. @@ -295,7 +271,6 @@ he was fucking ice cold last year tho. He destroyed us at least once.. why not dame?. hes a st louis guy so i like beal but hes coming of a career year. -ehhh. this is the last time. im right there with scott frost. i feel that. @@ -319,7 +294,6 @@ youre trading at his lowest. they'd be stupid to trade simmons. run that pick and roll to the rim. Who remembers the one true Perkins.. -Sam??. Hang in there perk. He woulda been god damn perfect. Fuck I wanted Adams. @@ -328,7 +302,6 @@ Booker just got there 2 days ago. Everyone stay calm. Help is on the way. I saw that live. -Jesus Ron. Die slow. Maybe like Oklahoma's okst. We all root for the huskers so we all understand. @@ -359,7 +332,6 @@ I'll end the nba season by saying the lakers and the spurs are the suns true riv But the nuggets are our Iowa. It's been a long time since I followed a winning team. Great season.. -Talk away. Suns fans got a little taste of a Wisconsin road trip. Duck those people.. Monty is the best.. @@ -368,22 +340,18 @@ Giannis to the knicks. I can't remember. Before this one?. He put up forty thevladt two games?. -Giannis. When he's hitting that fade and his free throws he's ungaurdable. Suns in 4 forever. Big up giannis. That was something. -Lol. hope we see some ball movement in the 4th. not just devin trying to draw a foul. man im dying here. -wasnt. nice guy when he was being a nuggets fan.. met a nuggets fan in sedona last weekend. we dont want to start the night this way. No way I disappear in the football off season. Chris is going off. -What!. Get a big who can actually hold his own down low. Frank can't be here next year. Gotta fucking rebound. @@ -396,7 +364,6 @@ He was nailing that fade. Deandre played him pretty well on a few and he still knocked it down.. Giannis is a monster. Wonder how much we'll have to pay cam Payne. -Cold take. Nba players are banking. Is that a normal contract. That guy was making 17 million?. @@ -412,10 +379,8 @@ Trade him for dame. I hate them so much. Lol fans are shouting bucks in six. How can you be an 7 foot nba player and offer nothing. -Point god. Ayton with that quick 4 killed us. Good for the nba I guess. -Yeah lol. Naw bucks are good. I wanted that 4. I'm not worried long term. @@ -439,7 +404,6 @@ You are all filth compared to dbook. Gtfo of suns central with this bullish. I'm taking van gundy all day. TNT is miller? Or van Grundy... -Possible. They made all of this.... Don't come one here disrespecting espn. But I didn't the last two. @@ -448,7 +412,6 @@ I'm more about the love of the game. I mean I'm watching but I can't say I remember who does what.. They stayed with Nichols then?. Uncomfortable. -I see. Who is Maria Taylor. I get the p bev part. What's the context. @@ -460,7 +423,6 @@ Warrior all year and bubble monster. Out in the first two minutes on the floor that has to suck. Get the brooms out. Cp3 might not let them win. -Wooooo. I'll spare you. Shit was supposed to be the new Zane Zor suns rap.. Lopez is pretty good. @@ -491,50 +453,38 @@ That's salt if you didn't catch it. The forty percent who aren't baked off their ass. Live look at the city of Denver. I'm on twelve. -Lolololol. It's now confirmed. -This tbh. Y'all suck and are not as good as us suns fans. This is karma. give him a couple moments. naw masker is working hard. lets gooooo!!!!. fixed that too. -nice. I think we see the problem. it happens. bryce is transfering. -ugh brice. thats good. -piper too. benhart getting some work. we won on the third touchdown. we are getting good pressure. get morrison and scott in. it makes the game less stressful. it is weird. -news. how good is q nes. -yeah. henrich looks like hes gonna be an all conference guy. -yeah. hes the difference between a 4 yard play in the flat and a 12. -easily. is reimer our best player on defense?. out linerbackers close out soo fast. hes had like 3 in the last 2 weeks. just mentally shaky. we are good. -yeah. get these 2s in!. THIS IS SOOOO MUUUUCCCCHHHH FFFFFFUUUUUUNNNNN!!!!!!!!!!!!!!!!!!!. -woooooo. woooooooooooooooo. woooooooooo. wooooooooooooooooooooooooooooooooooooooooooooo. woooooooooooooooo. woooooooooooo. -woooooooo. Ctb wants a pick badly. Best frost game?. I don't think even we could blow this one. @@ -546,9 +496,7 @@ Frost has already earned an extension. They are running behind teddy a lot. Oline is killing it. Let's go yant. -All year. He's pressing. -. Maybe he just lost him. Maybe he was supposed to have help. These games are always close cause Fitzgerald is a damn. @@ -558,9 +506,7 @@ Wooooooooo. They had their chance. No train hoppers!!. Let's fucking go friends let's fucking go. -So shut. That's why you live with the over throws. -Wow. Geezus he's so fast. What was that?. He's better than I thought. @@ -571,7 +517,6 @@ As a walk on. I like this. That was big missing him. Martin thank god he's back. -Please. He shoulda got a chance. Crouch was the fastest dude on earth. And quit on the rams. @@ -584,7 +529,6 @@ He brings size and power other backs don't. Yant is Ron Dane. He didn't run through contact like this. He's big and hard to tackle like every other carry he's ever taken. -Shocker. We max hyping!!!. It's happening. Apologize to yant!!!!. @@ -596,41 +540,32 @@ I don't think he finished his drop back.. Stars showing up early 2am toure jojo ctb. What formation was that?. It's coming. -Yant. Good run by rahmir walk in for Adrian. -Two. Ran it in in too. -Perfect. Changes on the line. We are about to put it together. End this game. Get a fucking pick six. I love football. Lovely game. -Lovely. Here we go. They moved Corcoran. Yeah we are beating them and Purdue and Michigan and northwestern and osu and Iola.. This was the year to not suck ass. Wisc not good Minnesota not good. -Got him. Interesting. great movie. -$bones. Nice. -Enjoy. Why are you wildin out on a Monday?. Not doing orders anymore. Ever since I lost my play streak and 4 stars I'm just attacking Iowa. Did we win?. -Ah. Chaos doesn't really defend tho right? We should be taking ever chaos territory we can get to. Sometimes the auto correct knows best. Yet I saw tomato and left it. We need to move in a circle. Like the rotation of a tomato and we can rip through the plains. Make 300 more of those and we are in business. -Step back. You the real mvp. Yet you continue to input your moves. That's my thought as well. @@ -643,13 +578,11 @@ The Texas thing never made sense.. We need to stop. It was cathartic. I do miss really truly hating something once a year. -Correct. If Colorado and Missouri came back too. I would be down for a return to the twelve. Texas is gone. And when you compare them with Michigan pennstate and Ohio state they weren't that bad. All the ties from the past need to be cut loose. -Let it go. Let's come out of this quarantine with a hard reset. If we are the most hated team in cfb risk than Texas has to be a close second. Ally with Texas. @@ -666,7 +599,6 @@ That explains why our previous strategy failed. What is our strategy going forward?. Let's play it low key. Let them and Ohio state kill Michigan while we work on wisc. -. I have no idea how to play this game.. Looks like we could still kill Stanford before we get snuffed. It's easy on the computer. @@ -678,7 +610,6 @@ Great delivery.. @Jey mike judge is quietly on of the best to ever do this.. > No let nebraska go it's pointless. @NotAFanof_SpeacialTeamsTeams the fuck did you just say?. -. Helms deep strategy. Then let's take back the holy land give everything else up.. Yeah ohiostate looks to have it pretty well in hand. @@ -701,14 +632,12 @@ Plan from the start should have been to bum rush the west coast wipe out the tre Which is okay cause I get to attack. But is kinda weak cause I don't hate any Alaska football teams. I got orders for Alaska. -They are playing us for fools!! <:bo:508088396162531328> <:bo:508088396162531328> <:bo:508088396162531328> <:bo:508088396162531328> <:bo:508088396162531328>. +They are playing us for fools!! Clear the trees Florida and asu and sweep the left coast. We got them surrounded. Why don't we just kill the trees in the next two turns?. -6 wins. Nwestern w, Purdue w. Then you need two more.. -We had. I don't know what they can do but We has everything we needed but a line. They have to change something up tho. Banks in for piper. @@ -722,14 +651,12 @@ Omar looked good. We left a lot of opportunities on the field. Our kicker is twisted up inside. Our oline is weak. -But... You can't lose if you don't play. I think we've had this convo? Do we have some kinda bet in place?. alledgedly. in the first six. ou is my 1 loss. 8 is a good number. -8. actually maybe not 9. so thats 7 with michigan wisconsin iowa. osu is a loss. @@ -737,10 +664,8 @@ osu is a loss. We still get purdue and minne.. ill names them. this is some hype. -x. Lets see what happens after our 9 win season. keeping players will be important going forward. -true. something they got to figure out but i dont think its as atypical as people think. I am concerned that the offense loses so many guys and the defense not as much. we play in the west. @@ -755,7 +680,6 @@ i cant think of another. JD in a coaching transition and Wandale... next man up. we are stacked with talent. -youre. your in the wrong channel. we could run a bottled up offense but we'd be giving away our talent advantage. we have talent. @@ -768,7 +692,6 @@ abbreviated offseason. ill take toure coming in with one shot to make the pros. and why its taken so long. thats a sign of frost gettting a shitty hand. -not at 19. he shouldnt have been a captain. wandale was 2 years in the program. For a team that needs to "learn to win" this schedule sets up perfectly.. @@ -780,7 +703,6 @@ I dont know who the key leaders we lost were. Luke? Wandale? Id rather have upperclassmen leading. The defense is stacked with leadership.. Lets see what happens with a full roster, and full offseason to rev up.. -. Frost can ride this D through the first half of the season if he needs to. I dont think he will need to. But thats all speculative.. @@ -800,10 +722,8 @@ i can respect that. stick to your guns. fair enough. i can only appeal to your self respect. -Hype up. Or Hype out. No fence sitting.. -hype up. no half stepping. I thought we shamed you people to extinction. an actual somehyper?. @@ -815,7 +735,6 @@ That's some hype as fuck. Assertions were made but I did not see facts. Put it in an article and let frost respond you ard. Sean Callahan popping off on his vip message board is not news. -Well.... Frost agrees. Sunshine over snowfall. It's cold as fuck there. @@ -835,10 +754,7 @@ Se la- 27 -17 this gets scary we are on the ropes. Wisconsin - 35-6 fuck Lisconsin we are taking this shit. Mertz isn't it.. Iola - 28-27 Nebraska.. -. 9 wins but we don't win the division. -. -<:sadnhat:612423571351928993>. Yeah that appears to be a rendering of a penis. Oh the painting?. Some hype to brighten your day. @@ -854,11 +770,9 @@ through gifs and predictions of greatness. how is hype. 21 for sure. when is hype. -myself. who is hype. to combat the growing sadness. why is hype. -truth?. energy?. excitement?. What is hype?. @@ -872,17 +786,12 @@ conceal the channel until the oklahoma game. clear any of the remaining riff raff. i think thats step number one. I should have ban powers. -some hype. the place to come to get some hype.. we need to pivot. a blank canvas. -hmmm. Some hypers on their koolaid cheat day. -<:MaxHype:686664366535475204>. Ah those are good ones. -To hype. Max hype:. -$mkv. oh has it always been a gif. I just deleted the mobile app. going straight comp. @@ -890,7 +799,6 @@ how did you get your profile to gif?. we makes it sound like theres multiple somehyping. Collecting rings while never playing a snap. Some hype is for casuals!. -Side bar. I am that gif. It's a done deal. Naw frost has never lost a bowl game. @@ -910,46 +818,35 @@ It's been a tough season. Wtf is this wack ass bullish. Put it right beside coors for best domestic. At the bar I used to go to that was their cheap beer. -I do. cold. yeah, wonder what those states are like. and rhode island?. delaware is by maryland?. not delaware. -vermont. the two on top between maine and newyork would be new hampshire and.. -delaware?. its the north east i might have trouble with. Chili's is the best of those types of restaurants. is that hugh freeze?. Sounds like your setting people up for disappointment. that has to be like yelling fire in a theater. I really don't hate cyclists I just love the memes. -Lol. -. Guess it depended on what music videos you were in to. If you weren't sagging in the 9os early 00s what were you doing. It was the style. -lol. are the 70 years olds into podcasts now?. Except for Minecraft. No room for lames. Lotta people wanted to jump on that suns bandwagon. I honestly hate when people root for the team I like. Unless you're 100. -I love it. That's how I rock my summers. Live everyday like it's Friday. Can phones read minds? It's probably not the case but it seems like it. We don't do those in AZ. -. In ten years we'll all be begging the Chinese for jobs via tik tok resume. -. -. Meme point stands. These smash capitalism children are woefully unaware.. Fuuuccckk that's a ton of slow pokes. -. Remove that vomit. Marty with the rack of strawberries?. Step outa your comfort zone. @@ -961,7 +858,6 @@ Let's go!!!!!!!!!. Ahh haha haha. I expected more than this. Why?. -Why?. cinema sucks. sounds like protesting. protesters protest. @@ -975,7 +871,6 @@ Might be progressive there. I'm also for getting legal status for people who are living here illegally for years.. Yeah I'm for free community college, universal pre k, although impact after third grade is debateable, big on single payer health care. Some might consider those progressive.. -Ah I see.. Why are uncomfortable saying that? Doesn't seem like a controversial thing to say.. Taxuaries might be the only thing worse than actuaries. John Stewart daily show was iconic. @@ -1002,7 +897,6 @@ Just to be clear. But could they justify it that way?. How soon do you think covid is going away?. Spreading our prisoners to Alleviate over crowding might help fight covid?. -Could be. rip. people need to realize how dumb they are. Once I figured that out everything else fell into place.. @@ -1035,7 +929,6 @@ the community college thing i really like.. most people get pre k, they do in our district if they want it.. I like the stuff in it, but its not something that will impact a ton of voters. whats the process on that is a a vote in congress?. -yeah. i disagree. no one knows what that is or cares. I think the abortion fight will be big, maybe the dems will try to pack the courts if it doesnt get thrown out.. @@ -1062,14 +955,12 @@ i dont think hes bad mentally. i just think hes a shitty public speaker. why wouldnt you? you have a crazy schedule and a job that requires a lot of focus. all the big time movers and shakers are drugged up. -has to be. people get up for that. hell get hit with it but is a botched withdrawl going to fire up the base? I think the abortion fight will shape this election more than any geo politics. he might be cause hes old. but they can pump you full of prescriptions and shit. ive heard republicans who want to say hes senile say that. i havent seen him say that. -no. yeah but thats not changing your vote. most people wanted to leave. we sure they didnt tell him that and he said. @@ -1083,7 +974,6 @@ I woulda voted Mcsally. Sinema had the olds on her side. They came hard on some vote they said got rid of medicare or something wild. I cant remember now but it was a lot of sad and angry old people commercials.. -der. in the world. maybe your personal finance situation is different than that of the largest economy in the nation. my favorite is the I have to balance my budget or i wont be able to pay my bills argument you see people make. @@ -1091,7 +981,6 @@ through bonds or to the state governments. i thought i heard that somewhere. Isn't most of the debt to the states?. They might and we say no u. -eye roll. Great the debt ceiling thing is back. The dems dont use it cause they spent years telling their constituents how irresponsible it is to use it. can't ddouble back now.. @@ -1099,11 +988,9 @@ My friends I'm St. Louis lived above an angel store in south city. With a lift and shit. Buy like a professional garage that has an apartment over it. -Buy. But a garage with an apartment over it. I mean if your talking about apartments and large unit developments not renting out to keep rent high I could see that. You think so? I'm not sure.. -Yeah. Build more homes move to virtual work so people leave the cities and demand relaxes. We are trying to solve the housing crisis here. Owning is better than renting for sure. @@ -1115,7 +1002,6 @@ That's kinda like rent. But if your house doesn't appreciate you are basically paying rent. Your not building your wealth you just paying a mortgage for a place to stay. Who wants to live in Fremont tho.. -or Omaha.. Maybe virtual work will send more people to Fremont instead of Omaha. Lessen up the demand there and increase the Fremont prices. Both good.. @@ -1124,9 +1010,6 @@ And it's not like cars where they stop working. I don't think it would be possible cause its not just the house it's the land. Sounds like that would just push people out of homeownership and take away the best way for a working man to grow his money.. Why do you want that?. -No. -. -. Jobs are for suckers. Like me unfortunately.. most of it is the same person. @@ -1134,7 +1017,6 @@ Unless you are owning developments. people hate work but want everyone to be stuck working a job. but doing all the things required to rent and keep up a rental house is.. You dont think there should be a rental option?. -why?. You think large landlords are the issue?. to i guess discourage people from making their money off being a landlord. how to change property tax. @@ -1142,14 +1024,11 @@ this is just an idea ben had. more than 3 rentals was the suggestion i believe. but most people live with large scale landlords id assume. feel like this will make rent higher. -no good.. closest i could get. They think theres votes to get.. bird flu was really killing 50 percent of people?. -i see. what is that thing. jesus 80%?. -i see. who the fuck is janet. Its a good book, but it takes some commitment to read it through. Yeah years ago. @@ -1177,7 +1056,6 @@ They had a big Kurdish population in the school I student taught at. Lol called ted beautiful ted. That's pretty go. Called bill wild bill Clinton. -True. But Crooked Hilary hit. Naw people looked past the nicknames that second go. dude did not like comey much. @@ -1205,7 +1083,6 @@ you need something to call your own. duplexes are a good option but people buy those to rent out. everybody laughed at the rent is too damn high guy but he hit it on the nose. cost of medical care and cost of rent are killing us. -yeah. we need more houses i guess. they sold a manufactured house across the street for me for like 250k and as soon as the sign went up buyer after buyer after buyer were coming through. a lot of rich people. @@ -1231,13 +1108,10 @@ californians suck and are ruining probably the best state in the union if it was Newsome is an assmunch is my stand. The lock downs have been pretty intense there. Probably good to take a vote and make sure you have a consensus. -Too bad. misfire. -shit. deontres mom thinks we can win it. not admitting a heart attack is different than not meeting standards of care. that seems wild. -43 icus. Why are they turning away heart attacks. Fire up those tent hospitals for civics. cause bin laden was in pakistan and al queda was training in afghanistan. @@ -1248,12 +1122,10 @@ That's the real story. Guy on internet claims to have made 120k. Easier to get people to comply if their job is on the line. alright gotta bounce. -gbr. i feel like we have. im thinking we are going to be managing this pandemic for the forseeable future. eh im not too hopefull. thats my understanding as well. -yeah. significantly. if you spread it it is, but apparently vaccinated people have specifically less spread. or thats what the district i work for thinks. @@ -1262,14 +1134,12 @@ is my line of thinking. become more dominant. the strains that affect young kids will spread faster. cause they arent vaccinated. -my guess. none. next mutation is gonna hit young kids, then we'll see where these people stand. india got rocked. but if thats 75% youd think the overall wouldnt be so high, or maybe it is way lower when you compare it to before. shoudl still slow it down youd think. thought it was like 50%. -fuuck. and this shit is still spreading?. arent we only like 50 percent vaxxed. if they have a police presence. @@ -1285,7 +1155,6 @@ like disaster relief that has to come quick. tbh i dont even know what would be appropriate for an executive order or not. they cant even get their own party to back it. they cant even pass an infrastructure bill. -plandemic. thanks obummer. the two previous presidents were doing the same thing, its just par for the course now. its a very shortterm way of governing when the first order of business for a new president is undueing all the old presidents execuative orders. @@ -1296,7 +1165,6 @@ I dont know, just seems like itll be year 107 of covid if we dont get people vax if its some bullshit political point scoring. If they think it will get vaccination rates up im fine with it.. you just got to learn how to do it, like anything else. -nice. I enjoy it.. special education. not personally but i helped her set it up online and suggested she go get one. @@ -1317,10 +1185,8 @@ i see so reconciliation has like a tax reform angle. youd think itd just coast. surely every senator congress person could go back to their constituents with some fat contracts to give out. theyre going to blow it. -idk. seemed like a great chance to upgrade somethings. the progressives want that?. -ciliation. what is reconcilation. I like pools i can stand in. i wouldn't recommend it. @@ -1333,18 +1199,14 @@ maybe come up with a childcare bill or something. Lets just let our bridges crumble then. current or something? I dont know. They probably dont handle that well. -yahoo. thats a name i havent thought of in a minute. -Jesus. Was it a Fox News persona maybe?. Rick perry?. It was someone crazy... I do remember that. Was that Santorum?. Minority report. -The Dems. fucking airlines and government money. -/ud bot. ill try. sounds unlikely but sure.. then i should have some oil money coming. @@ -1364,13 +1226,11 @@ theyre trained armed and working as a team usually. youd expect more people to get shot by cops than shoot and kill cops. but it reminded me of it and then boom there it was on the gif search. wasnt sure if you were making a reference to the pork grind? pork grind. -line. the best.. Shes trying to be good at her job. Which is good campaigning.. is that something youd be interested in?. dont you dare. -i see. so a labor union can sue on behalf of people not in a union?. i hate the bill but i also hate the sideways approach. typical husker fan, living in the past. @@ -1386,7 +1246,6 @@ he didnt do so hot at ad. america will never be great again until the huskers are great again. or cause of the nebraska game. cause of texas?. -i see. or it coulda been npr just trying to wind me up for the ratings. I think it has to get a ruling is the hold up, instead of just getting shut down?? i dont know a lot about the law, but i got a lot of things to say about it i guess. the law changed that in this instance?. @@ -1399,7 +1258,6 @@ He should get fired for another five w season. Just so he knows he sucks.. Using it to calibrate my hype. Just assessing the state of the Nebraska fan base. -Deere. Oh god. Lol let the big dog lead. Who is g lam?. @@ -1422,11 +1280,8 @@ Absolutely. If we score one more big touchdown against Iowa or Wisconsin that we follow up by allowing a st touchdown I am burning mizzou to the ground. Or it seems like it. Our special teams play has been the worst in recorded history under frost. -Tu. Ty. -No sir. Baseball = no hype. -yeah. we'll be the ones to bring attention to it. iowa fan made that list. wisconsin didnt get on either. @@ -1436,7 +1291,6 @@ If you can find me. I might go into the winds.. I will lose my shit if we start 4-0 and don't make a bowl game. If we start 1-2 I'll join you. -1996... when did winstrom go?. Lol Indiana. cap that cap. @@ -1448,16 +1302,13 @@ ugh long night. Making all the right moves. prove me wrong. nebraska vs nebraska is the purest form of football. -. Didn't realize where I was. no hype on meetings. Football and ..... -Careful. Sounds kinda hypefull. They don't like that around here. Would you take max 6 win seasons if once every 5 you go undefeated. But that one season tho. -The worst. Ugh. What color is ard. Honestly as long as I have a color no one else has I'm fine. @@ -1478,15 +1329,12 @@ Year three i thought they'd be clicking a bit.. We are a year out from wrecking teams but frost will continue to hamstring us with horriblly inconsistent play on offense.. you can just watch the games and see us beat guys up front.. You cant put too much stock in those stats when the defense plays so many snaps and the offense stalls out for entire halfs and and puts them in shitty field position.. -hard pass. Seen him coach iowa lite. anyone who thinks campbell would be better than frost is delusional. Iowa lite does not equal nebraska. -As??. campbell is garbage. im here to no hype campbell. just truth. -no hype. we have the best d in the division next year. youre not excited about these linebackers?. when have we ever been good doing anything else. @@ -1537,7 +1385,6 @@ he couldnt do those for jack year two and first half of year one. 2am looks way better at the timing passes. minus the second half of last game which im pretty sure he got hurt. adrian has improved the second half of this year. -milton?. we need to dump this bullshit offense. no one had tape. the first half of minne he was straight dealing. @@ -1566,7 +1413,6 @@ Oh that's right. What a meat head. What is he talking about. What did peter do?. -Kelly*. Did he call plays at Oregon or did jelly do that?. Not when they happen. He just addresses mistakes in the film room. @@ -1585,10 +1431,8 @@ Not max hype. He's going to be an all american before his career is over. He just needs more. Snapping comes with reps. -He's fine. Cam Jurgens is fine. That's the no hype. -Yeah. He'd start on osu. Absolutely he would. Robinson?!. @@ -1596,13 +1440,11 @@ Mosaic newsone next year will be a stud. Casey Rogers not quite there yet but he's coming. He's start on any team in the conf.. Difference maker. -Star. He's a gamer. Rogers made like three plays right after the hands to the face. He's a star flat out. These are real blackshirts we're talking about. They just want to crack heads. -Yeah. They play in any system. Not for Robinson and Rogers. They came for a chance to be apart of greatness. @@ -1618,7 +1460,6 @@ They wait to see what the rest of the crowd is doing before they cheer. Self loathing and yet arrogant. They are a shameful people. It's a moving target for sure. -Some hype. No hype is Wiley coyote after he runs off the ledge but before he realizes it. I'll have to see what my emotions tell me on Friday. I'm thinking it might be max hype. @@ -1711,9 +1552,7 @@ Worst loss I've seen in awhile.. Stuff the run, get pressure on third. Only way you get off the field consistently. We need that guy at olber. -Yeah. They at least flash the potential. -Hope not. If you want to get off the field on third down you need cornerbacks and a pass rusher. Luke reimer will be a star. Ty Robinson will be a star. @@ -1748,7 +1587,6 @@ Him dickerson. Those are the athletes we need. Apparently his recruitment was non traditional. No he just qualified. -Play him. We needed Dickerson. YANT YANT YANT YANT YANT YANT YANT. did he abandon us right before the michigan game?. @@ -1986,7 +1824,6 @@ WE ARE WINNING OUT!!!!!!!!!!!. WE ARE WINNING OUT!!!!!!!!!!!. Or did he bounce bounce. Is Nota still iowa'd?. -<:bezolol:798958334060789770>. SATURDAY AGAINST MICHIGAN!. WHEN DO WE WANT IT!. WHAT DO WE WANT!. @@ -1994,10 +1831,7 @@ im one long yant touchdown run away from combusting. big game folks. big ducking game.. im feeling this hype. -!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!. -GO. BIG. -RED. FUCK SHARED TITLES. FUCK DESMOND HOWARD. FUCK BLUE AND GOLD. @@ -2013,14 +1847,9 @@ maybe 18 - 19. I think we slide into the top 20. What are we ranked after saturday?. MIchigan week. -9!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!. -not 8. not 7. -not 6. not 5. -Not 4. Not 3. -Not 2. Not 1. No hype clowns getting shut down. Husker victories in the regular season 9. @@ -2067,9 +1896,7 @@ We are winning out!!!!!!!!!!!!!!!!!!!!!!!!!!!. We are winning out!!!!!!!!!!!!!!!!!!!!!. We are winning out!!!!!!!!!!!!!!!!!!!!!!. We are winning out!!!!!!!!!!!!!!!!!!!!. -Michigan!. It was a good game for sure. -Still?. Where is nota?. If you're in youre in. There will be no tolerance of any faking the funk. @@ -2113,7 +1940,6 @@ the sad sacks when we beat nwestern. WE ARE WINNING OUT!!!!!. There we go!. Those SOBs are always up in our business. -mine too. Think we get a big turnover that leads to an easy score as well. Maybe multiple.. we need to score like two or three times and put this game out of reach right at the start. @@ -2147,7 +1973,6 @@ WE ARE WINNING OUT!!!!!!!!!!!. WE ARE WINNING OUT!!!!!!!!!!!. WE ARE WINNING OUT!!!!!!!!!!!. WE ARE WINNING OUT!!!!!!!!!!!. -Clements. We got kpai ho ouli Gybor coming up behind them. This might be one of the best lbing cores we've had in a long time.... Garret Nelson Caleb tannor a bringing it this year. @@ -2165,7 +1990,6 @@ But these dudes are fucking our shit up. Or banks I'm not sure. Hixon is not the answer. Nouli at lg?. -Rt. Maybe we see teddy at rlt. This is the oline game to get it right. Nw doesn't give you anything cheap but they don't have the players this year. @@ -2326,29 +2150,20 @@ WE ARE WINNING OUT!!!!!!!!!!!. WE ARE WINNING OUT!!!!!!!!!!!. WE ARE WINNING OUT!!!!!!!!!!!. We are winning out. -Duck it. We are winning out. Max hype team. -Coaching. Special teams. -The oline. The RBs. The tight ends. The wr Corp. -2am. The secondary. -The lbers. The Dline?. -Iowa!. Lisconsin!. -Michigan!. MSU!. That'll free up some time for special teams. the talent is there except in a couple unfortunately key position groups. adrian looked good real good. -real good. the d looked good. -yes. admit it. you came away feeling a better about the team. you can when the sad sacks are predicting a blowout. @@ -2380,7 +2195,6 @@ We run the ball all over Purdue and win.. And Iowa is good.. We are ranked.. This is the game that changes the program.. -Wins. Two more games for the line. Two more games for the RBs.. Two more ws for Adrian, two more games for Omar two more games for Betts. @@ -2388,12 +2202,9 @@ A guy like toure came to Nebraska for these types of games.. You won't be taking Michigan going into this I tell you that. Big time game gets a lot of eyeballs. Harbaugh v frost.... -Michigan. Played ou to the last possession, asserted dominance over a ranked msu, got the job done at nwestern.. -At home. Then we come to Michigan. We beat them but maybe don't cover cause of high spread and nwestern.. -Doubt.. What then? You think the d is letting die against nwestern?. This is a 28-9 type game.. Is a msu quarterback who has to throw going to rise to the occasion? Or is he going to start turning the ball over. @@ -2410,7 +2221,6 @@ We limit time of possession by attacking the run one the early downs. Adrian gets a lot of possessions early. Oklahoma ran on us cause we played the pass. When we played run we got to the ball.. -We are.. This will be the best team msu has played. We would roll Miami. These ooc schools aren't building their team to stop a big ten running attack.. @@ -2422,7 +2232,6 @@ Michigan state is not going to put up a score in the 20s. But they are similarly built. My thinking is msu is better than people thought but is not the dantonio Michigan state you might not score a touchdown msu.. I'm more and more convinced we are winning big games.. -Ahh shit. Why are we here if not to get out of hand?. We got down field quite a bit. Think we can do the same against Michigan state.. @@ -2459,12 +2268,9 @@ That sounds like a w.. 4-2 getting Michigan at home.... 4-2 is still a thing. Final form Adrian . -+. Surging defense a couple turnovers away from elite. -+. Nfl talents getting the ball down field. Allen Omar Betts toure are all nfl prospects imo.. -+. Confident reasonable appropriately risk adverse play calling that values the act of running the damn ball.. It was the Adrian we saw at the end of last season too. It's not crazy to think we are seeing final form Adrian.. @@ -2514,12 +2320,9 @@ The offense was smooth. I can see us going into Iowa at 8-3. We got a little carried away. That's where this is leading. -Us. It's ya and Iowa. -6-2. If we beat Michigan state and the defense locks it down again. I'm ready to say we beat nwestern, we beat Michigan and we beat purdont. -L. I don't know why people think that's an auto l. We win there, beat nwestern we are 4-2 at home against Michigan. I'm really looking forward to MSU. @@ -2559,7 +2362,6 @@ I think we need to early. We might see some yant or Scott next week. I haven't heard. Looked like it. -Ervin. Didn't see any Hickman. I like teddy lead blocking at te. I'd rather see the diamond back. @@ -2580,7 +2382,6 @@ He shoulda stayed.. I don't hate him. That one hurt. I don't blame him but I hate him. -I know. Ku. He's tearing it up at ju. Fuck wandale. @@ -2595,7 +2396,6 @@ Domann Heinrich reimer gnelly is the best 3-4 lb core we've hand. We looked fucking decent. MSU nw Purdue Michigan Minnesota Wisconsin. Nebraska is winning 8. -Dends. I think that's what we're seeing. Betts might be the best of the bunch. I could see us really exploding in the pass game with them Allen and Martin. @@ -2619,9 +2419,7 @@ Regardless. It's at least a six point swing. And make you pat. But you gotta hit from 35. -Long. I don't really count the fg we missed thatbwas omg. -Yeah. If Adrian plays like he played today we are gonna beat some good teams. This defense can get us to 4-2. MSU looking like a must win. @@ -2687,7 +2485,6 @@ at least in the old black and whites. theres a tradeoff i guess. honestly they look hot and miserable but also fancy. looking at the old black and white photos they got did up in the 30s. -maybe. Yeah and their parents dressed nicer than they did. Roll those dice frost. Go down being frost.. @@ -2712,7 +2509,6 @@ Let your run game get worked cause you're oline kinda sucks. But you know once you get vok back you can can unleash a new run attack out of double tights.. Think Scott will drop a depth chart. Banks is ready. -??. i want to 100x it. should i parlay it with an iowa outright?. i can dig it. @@ -2735,7 +2531,6 @@ We have a lock down defense. I'm putting money down on the outright if the odds are nice. FFFROOOSSSTTT!!!!!!!!!!!!. Frrrrrooooosssstttttttt!!!!!!!!!. -Frost!!. We are coming!!!!!!. Frost has won big games. It's our time. @@ -2745,13 +2540,11 @@ But life isn't perfect. We can win this. I think we got a chance. Got to where we needed to be with no muss.. -etc.. Some new looks on offense. I want to see some wrinkles. We can hold our own. Make rattler make a throw. Let our secondary square up with them. -Chase. Chance rattler around. Send pressure. Give them a fucking game plan that takes some chances. @@ -2778,7 +2571,6 @@ Run some screens cause I'm going to bet they'll try and come after us.. And protection. It will come down to turnovers. Our tes will make catches. -Our. Or secondary will tackle well. We'll be tough to run on. Adrian wil make big playscon the ground.. @@ -2812,7 +2604,6 @@ And more brown.. Toure is good. He can get open and has good hands.. Last game he was open a lot too. -3 for 90. Hickman is really intriguing. We've got more playmakers this year than last year on offense.. We got a bit more pressure. @@ -2847,7 +2638,6 @@ that might be vodka tho. martinis maybe. that might be the only things i drink it in. gin and tonic. -greyhound. i like it in certain drinks. you dont like gin?. pretty hipster but pretty cool. @@ -2934,21 +2724,12 @@ dog people taking over. You on a trip psy?. They hit 8 weeks on the 5th. Got a home lined up.. -$cd. Kittens psyched to see frost turn this thing around. -. I was surprised they all came out the same color. -. -No. Gonna give them away when they get bigger. They are a lot of fun tho. -. -No. Yeah getting big fast. -. -Saw that. Quality post. -intense. when i was younger used to want to go to one. yall ever seen those white and black raves in europe?. Youre a jump right in the mosh pit kinda guy?. @@ -2956,35 +2737,29 @@ but the kid kudi concert i had to play the back cause i dont dig crowds. When I saw sango i got right up up front. I like the smaller venues too.. yall get everyone coming through. -nice. Did they start doing live shows in nyc yet?. Need some 5th ward boys in that mix. actually real solid playlist. diamonds in the wood is fire. -. theres a song where the hook is riding a wave i cant remember the title. i like mr rager. parents let them watch a ton of stuff i might not. yes and they love horror movies. none of them can tell me anything about it other than the name and its scary. the kids at school will not shut up about this movie. -Got him. nonsense. Did anyone listen to the mountain goats in college?. Or just overall wackness of vanilla ice. Cause of the German nazi line?. -haters. Give me a penguin riddler remake. is this why your boss doesnt care if you call in?. Better than off the computer. I do like music on a drive. -I'm not. You an audio snob?. my brother was a big buffy fan. i think they line up. It was a smash. dont think i ever got into dawsons creek. -i see. why was it controversial?. I thought so. my wife has a set list and drum stick from a black keys concert. @@ -3004,14 +2779,12 @@ Frost is fucking lost. Give up the clipboard you sob. Dawson's creek was dope.. That riding a wave song blew the house down live. -I see. I heard he gave him his cd when he was working at some clothes store and Kanye put him on. Y'all listen to sango?. I went to a cudi concert a few years back. It was fire.. Kanye discovered him correct?. Colab is what it was billed as. -Eh. You count that as a Kanye tho?. That kudi Kanye is fire. It's been a minute since I've heard it. @@ -3019,7 +2792,6 @@ Past 25 maybe. We wasn't sposed to make it to 25 jokes on you we still alive. Drug dealing just get byyy stack your money till it gets sky higghhh. College drop out was his peak. -rich tho. Is jk a billionaire?. Anyone see the bourdain movie?. He has sleeping in beds with young kids. @@ -3031,11 +2803,9 @@ It probably gets good ratings. Harry Potter?. what did he say?\. i used to have a kung fu compilation cd with street fight and some other sonny chiba movie in it. -gruesome. ah shit. i didn't know that. season three is in maricopa!. -crazy. thats the one. Anybody remember that Steven segal show where he was riding around with the cops?. Are you at Garth?. @@ -3046,17 +2816,14 @@ pretty sure he catches rabbies and they shoot him or something. he was a great dog while he was. Did you fall asleep before the end?. yeah elijah killed it. -agreed. he handled himself pretty well. going to the milk. there you go. jesus i can hardly watch. -yes. why are they doing this to themselves>. the host is going to put a hole in his stomach. i dont think id fair well in this game. uh oh its getting up there. -spoilers!. bake it in a cake pan? is that what makes it detroit style?. ive never had one so maybe. detroit style pizza gross. @@ -3064,14 +2831,11 @@ the short bottle?. last one id assume?. do they switch the hot sauces around?. stay in the moment. -me?. dont know if you dig outkast but he was in their extended click. cool breeze from the dungeon family. im gonna watch tanks for a bit but ill drop one real quick.. pretty chill. -nice. that old infinity shot. -fair. get you one. welcome back chonks. thats a better question. @@ -3082,10 +2846,8 @@ then it hit me thats tyreke from off the street around my grams i havent seen hi shook my hips to the bassline this joker grabbed my waistline. i got kidnapped they took me to dc had me working underground building missles for ww3. soupin me up saying your a good worker, how would you like a quarter raise move up to the register.. -opeth. good drinking music here. nice nishi. -pause. good shit. alright nishi youre up. im in danger. @@ -3099,9 +2861,7 @@ cant beat that with a bat. i know night moves. im way behind. pause! pause!. -yes muzak. ill get to it but its gonna be a sec. -post it. just made it to the beatles. i got more green than grass in the yard. you been to with girl six month paul wall the reason she wont have sex wit ya. @@ -3117,18 +2877,14 @@ this is a run it back banger. 18 year old rap is always wild. they did this outa highschool and promptly split up. thats not easy to find actually. -really?. isms would like this shit. all these years i thought he was a wife beater. not a huge deal. -yeah. but the music is. thats not great. he did play for someone committing genocide. -im off. weird nothing. i could be wrong. -really?. one of those things not like the other. like ill listen to chris brown or mike jack. like tied to a chair kinda shit. @@ -3139,7 +2895,6 @@ he was in on some bad domestic violence?. give each moment its moment. seal is killing this. pause!. -pause!. god no wonder you dont hype. you know whats better than a shower beer. a roll out of bed into the shower moment. @@ -3153,7 +2908,6 @@ these fucking zoomers and their internet accesss. the who.... im on seal. alright go. -pause. you put up with the shower mist landing in the beer if your about to vomit. im telling you your doing it wrong. its not meant to be an end of the night thing. @@ -3163,23 +2917,16 @@ drink it out the shower. he does a briar rabbit song thats fire. might be a little old timey.. hope the dude doesnt offend.. -seal!?. ther eyou go. chonks you got one?. -un pause. makes youforgetthe stench of urine. its pretty good. bout new orleans gots lots of jazz. -or hbo. did yall watch treme on netflix?. -i dig it. skeleton has some preboomer vibes. -pause. okay im going to hip you to your rappers favorite rapper. chonks you got one? or can i come with the dude. -pause. fuck you your too young for that. -sam cooke. i might hit that for my next. do yall listen to devin the dude?. go for it skeleton. @@ -3189,7 +2936,6 @@ god damn this is 5th grade. alright un pause. theres so much rap. no ghetto boys?. -no ugk?. no fugees?. no talib kweli?. no gangstarr?. @@ -3220,9 +2966,7 @@ caught the vibe perfect. honestly didnt expect this. chonks excelente. its going to be a minute. -pause. i just like to stay within 1. -bring it. fuck. this is in a thousand rap songs. love this beat!!!. @@ -3230,26 +2974,20 @@ okay im on kool. whod have thought. damn were headed somewhere. i know the name more than the music. -maybe. not sure. -okay. nooo. pause songs. -what?!. kool and the gang!!!. are we about to go randb?. excellent skeleton. good fucking pick. -okay nice. chonks get in here. alright onto george. only darkness every day/ aint no sunshine when shes gone, and this house just aint no home, anytime she goes away. hmm i dont know that one. -yes. how cool can one man be. watch that drummer. get hip to it if you arent. -use me. mothers hands. bill withers is fire. aint no sunshine is so fire. @@ -3257,22 +2995,16 @@ gotta hit some bruce at some point. shit now i got multiples i want to play. not enough harmonica in millenial music. except for all the cds i used to own. -me too. good call skeleton. they know what ever google leads them to. cause hes youth. -right??. im still working through the dylan. -pause tho. watch this drummer. -nvm. oh wait same song. -post it. on the road??. anyone jack kerouac fans?. okay im on bob dylan. shit man down. -be chill. im not ready. damnit skeleton. which is working well. @@ -3282,11 +3014,9 @@ started teachers back last week students tomorrow. ugh down to one beer. dont hate on talent. weed musak for sure. -true. damnit you gotta pause. this is some weed musak. the skin flute sounds are... -different. no a metal flute id assume. they have a flute. well give it a listen. @@ -3297,21 +3027,16 @@ good call chonks. are we going to get some flute?. jethro tull???. gotta hit chonks then marley. -pause. v. -good shit. feel bad for you covid kids. yeah that sucks man. you hit that live music scene there?. thats okay. -okay. -?. is he from kearney>. never heard of mat one t kearney. my parents always played jt and bob marley on road trips. lets get to know each other through muzak. a little underwhelming. -sorry. your posting vids. skel gets it. alright everyone pause while i catch up. @@ -3325,13 +3050,10 @@ leaning towards sweet baby james. deciding which taylor. still got a full 230 left tho. im about to bust some james taylor step back. -max hype. and its not 12 minutes by pink floyd. here we go. lol nvm thats the commecial. -jazzy. alright no one post anymore songs till i catch up. -will do. yall seen green street hooligans?. its next up but no. havent got there yet. @@ -3340,7 +3062,6 @@ there you go skeleton. word to the father and mother earth, seek an everlasting life through what its worth. someone else post their music. this is ajam. -pause brb. the truth. the truth will set you free. how long will we stay under the heel of goose stepping charlitains. @@ -3350,24 +3071,19 @@ and their devisive rhetoric. i blame the red names. arguing over hype when we should be kicking jams. why do we always end up like this. -1. flip it plus `. its the best picture ive ever taken. i told deere if i could get you the full shot you could appreciate how small that bowl is. you know not what you speak. stop child. -9-3 it is. every win. i want it all. -not ever. and i wont settle. where there is a fucking right and a fucking wrong. i walk that middle path except on here. to kids who have behaviors. -calming. it might surprise you to know that might stregnth as a teacher is i am super even keel. im just playing characters. -no. if your willing to. its pretty easy to evaluate your path tho. im happier tho. @@ -3384,7 +3100,6 @@ getting on top of your shit and achieving goals is better than just not caring a kinda like herion feels good. but adults can serperate that from being good. it feels good. -its not. you cant see me but im shaking my head. i was wildin the fuck out in st louis. through my st louis years for sure. @@ -3395,9 +3110,7 @@ no more excuses marty. shit worked out fine. but its not really a regret. Plus i wasn't confident like i shoulda been. -ance. ince?. -ance. and word was you couldnt get clearence. that i was thinking of going. it was right outa college. @@ -3406,7 +3119,6 @@ I think it came down to questioning the legality of it cause i got a dui in coll shoulda taught in korea. I was closest to doing that.. i used to voyeur those forums but didnt take the plunge. -nice. NIGHT CREW!!!!!!!!!!!!!!!!!!!!!!! 4 EVA!. @NotaSadSack_Hype re get in here with a jam!. are you a solo travel guy?. @@ -3423,9 +3135,7 @@ above song is flames im running it back. latina if you are from other central and south american countries. hispanic if you are from mexico. hispanic i believe is the politcally correct term. -moms side. half. -yes. or at least to a neutural position. this shit could turn the saddest sacks frown upside down. im curious. @@ -3464,30 +3174,24 @@ Nice nice nice nice nice nice nice. I'm just eyeballing it. It may come as a surprise but I'm not a details kinda guy. What's this shit?. -Psy. At pay. Don't sully this moment. Only then can you move on with your life. Make amends. Let him know you were sad sacking. You need to apologize to him on twitter.. -Wooooo. Y'all going extinct. Duck the sad sacks!!!. We are winning out Deere. This is insane. -Wow. He'll get better. Is it wild to think the changes fixed some things?. -Right?. Line looks good. Levi Faulk quietly playing the best ball of his life.. Woulda been a good time to practice field goals. What's the streaming site? 6 something. -Yant. Because the sad sacks doubted. Apologize to yant you sad sacks he's better than I could have even imagined. -Good feet. We good. He's bout his business today. where you guys at?. @@ -3495,15 +3199,12 @@ And he never fucking will. Damn no home for the Hulu live stream. Right where we want them. Where y'all at in the game?. -My bad. Oh wait I'm in raging. Luke is operating the offense. Calm yourselves. Scott is running over people. -Tied up. they know husk. tell them i am busy and i wont be coming in. -fuck them. w. see, when god closes a door he opens a windo. i didnt make it out by noon either. @@ -3513,7 +3214,6 @@ may luck be on our side. thats what im shooting for. apologize to yant marty. thats makes sense. -ah I see. keeping the deep fry at bay so i guess its okay for now. i dont know its like a combo markov / image finder. ten minutes till pizza. @@ -3524,11 +3224,8 @@ they should be bringing pizza soon. 4 more hours and im free. what are these. /inspire,e. -look up. @NotaSadSack_Hype. -I do!. nvm. -ugh. how cold is it there. careful I might fuck around and make a pilgrimage back to the holy land. 2 more days till fall break!. @@ -3545,21 +3242,16 @@ calm down man. therell be other games. with kliff kingsbury somehow. cards might be good this year. -or you. he was. mortgage the future to make the past kick ass. I hear its best practice. -/deepfy. huge crossover in membership. If your calling something toxic for that, then you might as well call the whole internet toxic. yeah people who like pcms are probably all assholes. -Buffalo. As proved in the tweet from mustache Adam. That's a boomer move for sure. Millennials could never commit like that. what kinda broke dicks get upset about getting called a yuppie?. -?. -Ugh. Portland usually a little warmer. I drive through there once and it looked dope.. Deere you could go to mobile. @@ -3567,23 +3259,18 @@ Hit the beach. I don't want to go if there's a chance I see snow. Wife and I are considering visiting Portland in a couple weeks. Is it cold up in Seattle yet?. -Go to St. Louis. Or really just moved bedtime back. When I stopped adhering to a bedtime my Sunday anxiety didn't ruin my day so much. Ugh I'm not thinking about it. -What up?. Night gang?. Rat and spy drone?. That's kinda funny. But Deere is a hater.... is it friday yet. -sigh. Gotta adventure into the link. Suh kicking facts. -Yes. @UM More Like Useless Mostly. -Eh. I'm pretty much Twitter and here. I think I can speak for everyone here. We don't have tik tok. @@ -3606,7 +3293,6 @@ Lots of young white women get killed not all of them go on tv. are they going to want to protect a murderer?. more than they have right now. you have your health. -cap. must be with relatives.. how much money could he have?. thats what her friend said at least. @@ -3615,11 +3301,9 @@ he was abusive and controlling. probably not too far from florida. they think hes in puerto rico now?. in puerto rico puerto rico maybe not. -yes. at an all inclusive yet. didnt think of it like that. what about the dog. -mm. they are coming after someone. you aid and abed a fugitive cover up a crime and now they cant charge the son cause he ded. who else they going after?. @@ -3633,7 +3317,6 @@ through eye witnesses or registry. the dog is on this shit. dog verified that his parents dropped him off at a different state park. came back another time.. -yeah. how insane would it be if the dog tracks him down. you talking the dog?. sounds like me. @@ -3642,7 +3325,6 @@ She bought that shit online. Who Is Ana?. The fuck is that one about. I don't think they are. -Evil. The Who the band is mad overrated. The Who has to be political with China and withheld things early at least. They withhold things to manage public behavior. @@ -3653,7 +3335,6 @@ That's just what I heard on the radio. The Who has really been on top of it. If you mix the two dose shot you got stronger immunity. Is where I heard it. -NPR. Behind getting covid then getting the vaccine. They say mixing shots is the best protection. theres a lot of drugs that get used off purpose when they find out it can fix other things.. @@ -3682,7 +3363,6 @@ I dont think they would have listened to him either. These anti vaxx people have been around for awhile. They just want anything else but what the dems want. Its weird cause trumps whole covid play was get the vaccine out fast and keep the economy rolling. -jesus. yeah people are taking the vet stuff im sure. I guess there is a human one too. Thats what rogan was taking.. @@ -3702,7 +3382,6 @@ I like them both. Celery is great if youre juicing or eating wings. G with the excuses: Nishi with the answers. not an option. -fair. what did you order?. you really see where someones heart is at. i just avoid it now mostly. @@ -3718,7 +3397,6 @@ Still dont like tight spaces or crowds.. but i moved when i was four so not a lot of memories. when I lived in swedeburg caseys was the closest shopping. never had caseys pizza. -i see. tbh. looks like vals?. Where's that from?. @@ -3737,7 +3415,6 @@ Chicken taco meat. Chimichanga. is that a vegan burger?. I need to pick up a Dutch oven. -It'll eat. No tooth picks?. working closely with people is mad overrated. What to you imagine office work is like?. @@ -3749,11 +3426,9 @@ Oktoberfest is the best. Damn they got the same recipe from 200 years ago?. Make a good split p with like 3 ingredients. I love soup. -Dank. I'll half an avocado salt it and eat it in or tortilla. Avocado is the best. 10 bucks ain't bad. -Max hype. That havarti looks perfect. we've known that about deere. Bacon on pizza is one of the few ways I'll eat bacon. @@ -3778,13 +3453,10 @@ Do all cali burritos have fries?. I used to put fries on my hamburger at mcdonalds when i was a child. salmon wrap sounds pretty tasty. including chicken gyro. -meat. they serve them with all type of meet. -gyro. guess thats what makes it a dragon not a gyre. are the fries inside?. just call it a gyro. -Fair. You ruined it. Don't at @NotaSadSack_Hype. Should we at him. @@ -3798,7 +3470,6 @@ Not like compared to yuppies. All the servers I know are just tossing around cash at bars and nice restaurants. You can make big money in the service industry here.. It's better in the moment to have that sweet tax free money. -or a loan. Because you're sane. If it's bud light it's pretty typical. For shiner is real good. @@ -3807,7 +3478,6 @@ You didn't tip. Chili without beans is just spaghetti sauce. Miss wataburger and go to braums. Interesting autocorrect. -Qdoba. Adonai. Uncooked chicken is raw. Rare not raw. @@ -3816,19 +3486,15 @@ vacation in the lou?. i dont have anything else. max hype the huskers. eat rare chicken. -live life. im trying to shame them from being afraid of getting sick and dying. but yeah the illness stuff is a little iffy. If I was in a good restaurant id try it. you are fucking up the texture of the chicken without realizing it. -cowards. yall wouldnt try rare chicken?. I'd like to try it. What's the deal with food born illness and chicken. Is some chicken good other chicken poison. -The duck?. Some might say the greatest. -St. Louis had a great Oktoberfest. letssss GGGGOOOOOOOOO!!!!!!!!!!!!!!!!. I saw that too and it's what I immediately think of when I hear about cold water boiling faster. @@ -3840,15 +3506,12 @@ I did a kettle bell routine for awhile. It was good. You can work a lot of different muscles.. Next your going to be doing the required reading in class. -Wrong. You're wasting your youth. You don't have to exercise till you hit twenty five. Go have fun.. -?. You training for a marathon. The fuck is a mars bar. best thing there. -amigos. looks like that would be better sans alcohol. Ahh now I see it. is the white stuff shredded cabbage?. @@ -3858,10 +3521,8 @@ I just heard an npr bit on ted lasso. suck it up. not a great chance but ill take my chances with the huskers over the other options. a upset in norman is the only chance you have. -it wont. take notes skeleton. you could buy a house with those winnings. -yeah. i need it the next season. im not waiting three. imagine one more. @@ -3872,7 +3533,6 @@ everything is negotiable as god intended. you could slam it in iowegian faces on twitter forever. football or gtfo. cause a basketball championship wouldnt hit the same. -very true. gotta be careful with this stuff. good question. god can wait a little longer for us to finish. @@ -3884,7 +3544,6 @@ Have you no shame. You gotta ease back in after a vacation for sure. I hate bk just as much. sounds like you can end your day. -fri-yay!. then it was pretty forgetable. their green sauce wasnt bad until i had other green sauce. i like their spicy bean burrito but everything they serve is luke warm. @@ -3896,14 +3555,12 @@ disgusting. had to give up shakes but it wasn't easy. But a roast beef and a shake used to be my jam when i could eat whatever i wanted. I like arbys, i hate their fries tho. -at. other than bk and churches i cant think of one i wouldnt eat. true for any of them. yeah badly run ones are bad. hard to find bad ones. im not sure, fast food is so good. then probably... -churches?. bk is the worst fast food. id disagree. cant say i ever ordered off the a&w menu tho. @@ -3915,10 +3572,8 @@ more places should serve hotdogs. chili cheese dog runza. sonic has good ones actually. love a chili cheese dog. -Wrong. Life is short Deere. No time to waste. -amigos. i stand by what i said. stir it up. I tried to tell you to break the ice but now you've let this thing build past the point of no return. @@ -3929,18 +3584,15 @@ Might have a legit reason for having the lights off.. Might put your name on his list. He's not gonna say shit. Tell him, hey sorry man I gotta have some light in here and turn the lights on. -Whoa. Wrap it in foil. I see people doing water melon and pine apple. Asparagus is good grilled. -Kabob it. They are good and easy. Get some bulb green onion. I love potato foil packets. Don't need lighter fluid. Fire starter. Charcoal chimneys are for yuppies and slicksters. -U maybe. Bacon. dont encourage him. I would be ecstatic if 2am got his name called. @@ -3948,15 +3600,12 @@ maybe he gets a look. I forgot about DW. He can really tackle in the open field. Allen maybe?. -Hope so.. im not sure who else we get in. -CTB maybe. DD maybe... Yeah thinking like 6-7th ?. if hes big enough i guess. definitely a draft guy. hes lights out right now.. -ah. hes really playing well. 1st team all conference?. How good is jojo?. @@ -3993,7 +3642,6 @@ but hes got them going now. the starts of the season are fucking dreadful. i think by frost 10th year we can return punts again. the staff is getting it together. -true. but saban is not letting that slip. think their bama cause they rank in the top 5 recruiting every year. bama is bama. @@ -4003,7 +3651,6 @@ run the ball stop the run. frost is just hoping we catch it. we just gave up any kind of return. we did good last week. -true. you handle that you win the game. most of the plays are on offense or defense. is my argument. @@ -4037,10 +3684,8 @@ They'll come out fired up and early we'll break a big play. These dudes suck hard. We are going up and down the field on them.. perfect symmetry. -31-13. Michigan is not scoring in the twenties.. nebrasketball . -count it. buford looks a lot bigger than i thought hed be. watching some chaz clips. Might be a good option for a lot of players stuck in the portal with no where to go.. @@ -4058,12 +3703,10 @@ He's going to be a star. He got called out as being the best young guy. Nash is the polar bear who is from south orange Dakota and was the big time wrestler. He is playing nt. -Minnesota. Our d is fishing lines before they were dolphin safe. Nothing getting out.. Never said the line was good. Adrian held up. -Idk. Adrian held up at Oklahoma and that was a mean d line. Memorial is going to be bumping. They are going to have to throw. @@ -4076,10 +3719,7 @@ Probably working a lot of play action drop backs. When we put the in third and long we'll get to them. I sad sacked the offense last week. This is Adrian's game. -32-13. No fuck that.. -Nebraska. -17-10. Frost better not fuck around with these yant carries. This is gonna be a grind or be ground game.. This Michigan game came at the perfect time. @@ -4087,11 +3727,9 @@ I think we show up in a big way.. Maybe the line will put together another game. We get sacked like potatoes. desmond howard is the new heel anyways. -Good. He can be there when we bully Michigan. but not this year. usually that would have me nervous. -nice!. Yeah we are built for this type of game. Michigan must have a good back. Apparently they are running 70 percent of the time.. @@ -4103,9 +3741,7 @@ guys like nance arent holding up in this league. they can still use small guys just not on the outside if they expact them to block big ten dbs. I think TE will be fine. with hickman fidone and that dude from nau. -transfer. maybe vok as well. -allen too. dd leaving for the nfl id bet. itd be great to have him back. looks a lot like khalil out there. @@ -4121,7 +3757,6 @@ maybe a qb. maybe take another safety in the portal,. he lucked into this one. we smoked him and he lucked out with that punt. -hes not. it works for us pretty good. theres a ton of talent there. i think they are just going to try to land a bunch of transfers.. @@ -4138,18 +3773,15 @@ They hit me with a bizarre slow the other day.. I think they liked ervin and stepp and rahmir just kept working.. think he deserves a shot. Howd you think the new starters on the line look? Was this just nwestern being down or are we better with teddy and nouli. -Gracias. Is there a 40 minute version out?. How's nouli look?. Teddy looked decent. They ran behind him a number of times. -Yeah. What happened with the oline yesterday? Did the changes really make the difference or did nwestern just play like shit?. We are winning out. We finally put it together. Wisconsin blows balls this year. Iowa never be shit. -Jrs. We don't need that many guys. Especially if we get some hrs to stay. I don't hate that. @@ -4163,7 +3795,6 @@ Or ante up. Ruff riders anthem. the tunnel walk song we deserve. I bet if he got an entire half of three and outs he would score more than one touchdown. -right?. how far would monken take this team with this defense. we should be 5-0. full backs pounding linebackers. @@ -4175,7 +3806,6 @@ Theres so many coaches to pick from.. If we cant pull monken. wait was brady michigan or nd?. he did well in he big ten. -Take him. take their coaches too. san diego?. maybe we should poach some of the dakota coaches. @@ -4191,24 +3821,18 @@ Held will figure it out.. Not sure about Johnny. And Johnny Rogers. As did Luke Reimer. -On. Came as a walk-in. No on scholly. -Garbage. Point being this is the weakest rb room we've had. And we have yant sitting on the bench. He sucked for us. -3?. How many games before zigbo took the 1?. -Gtfo. That's how fucking dumb held is that he though bell was better than I zigbo. Better that Greg bell. Not as good as mills or zigbo or Tre or newby or ameer or Rex or helu etc.... He looks okay. Everyone but yant. -Rb. Yant is our only playmaker at th. -Only 2 am. Martinez can make a guy miss no one else on team does that. I like the slant thrown high to Allen or Omar. Not as much as running 2am tho.. @@ -4230,14 +3854,11 @@ chaz calling out austin. WE ARE WINNING OUT!!!. We need Iowa to pick up an l or we won't be able to catch them. Chaz in SoCal syncs the radio call to the video if you want to hear Davidson day oh no a thousand times as we fumble our way to disappointment. -Wtf. Matt Davidson is getting paid 200 k?!. Don't care! We winning out!. -lets GO!. Its not like were the only team to play lower level opponents.. That about sums it up. Beckton is good. -2am?. for a game I think hes okay. For a long run thats trouble.. thats what i was thinking. @@ -4247,7 +3868,6 @@ maybe he'll show out.. can the line protect him. he can run it, and his throwing looked better. if smothers has to play this is the game to do it. -oh god. rightly so for this game. is adrian confirmed hurt?. i mean he was out there letting it bounce. @@ -4256,7 +3876,6 @@ smothers looked okay. Theres always a bunch of things you could point to say if we had done that right.. but your up by 7 on the road basically a stop away. Thats a good position to be in.. -flat out. The bad punt lost the game. i like dawson. lol did he say that?. @@ -4264,7 +3883,6 @@ fucking frost. lol i guess that would be a little insulting. did he say why they cant field a competent st unit?. interesting. -really?. we sucked hard?. what do you say. what did he say?. @@ -4274,7 +3892,6 @@ its up for debate. this might be the worst special teams play ever. wonder if you go back historically if we are still at the bottom. that tracks with what we are seeing. -well done. did you just make that?. that almost killed the spark tbh. god that was a dark day. @@ -4307,7 +3924,6 @@ Not the line. cause i feel pretty strong about the 2 am part. on which part. 2 am is the best qb in the league behind the worst oline in the league. -but good. no 2 am. They got a good qb. then they suck. @@ -4332,7 +3948,6 @@ Get in early on finding a new coach. Cause USC is taking one. Heard LSU might end up moving on.. mid season let chins take over.. -yes. I dont see it happening. Their offense is garbage. But still if it does thats the ball game.. @@ -4354,7 +3969,6 @@ Mills would be the starter in this unit and it wouldnt be close. So they are worse than last year and mills was less productive than ozigbo.. Because he sucks at evaluating talent?. Held has produced the worst running backs I've seen at Nebraska.. -No where. We'd upgrade both of them immediately. Austin can bounce. How many lineman did we take the first two years here and you have nothing to show for it.. @@ -4380,16 +3994,13 @@ That's what I thought at first glance. Why is he so weak? and Slow. He always looks off balance.. Benhart has not taken flight yet thats for sure. -Better. They are not. The offense sucks. I watch the same games you do.. Those blackshirts tho. That's Nebraska football. St sucks too. -It does. Better than 2020 but are you trying to prove the offense sucks?. -Oh 20. Did they have timeouts?. You incomplete on second there's 28 seconds?. You take a timeout after the sack there's what? 36 seconds?. @@ -4402,14 +4013,11 @@ Bit of a pussy move I guess. We probably coulda taken a shot and been fine. But the risk of giving up a field goal by fucking up is very real with this team.. Magic numbers.. -What odds. I'm not sure other than convincing me we had more than a prayers chance of scoring. -Fair. Oh yeah got me. We had just given up 7 on a bad punt. What hindsight. Or just not being able to cover a good kick and giving up a return to field goal range?. -Would it?. Adrian three a pick theee plays later. Why would he trust them. The threat of fucking it up and giving them the ball in field goal range was more real than us getting downfield with our line protecting. @@ -4436,7 +4044,6 @@ Not when it's undeserved. I provided the reasoning accept it or don't. Irrelevant to the call at the time when the game was undecided. Cause we lost?. -Why?. Not blaming anything. Just explaining to the frost haters you can make a good call and get a bad outcome nothing is promised in this life. Or a fumble. @@ -4456,9 +4063,7 @@ I think In the moment the call had merit. When ever we have to drive and score i know we are getting stuffed. last one i remember is tommy throwing that pick in miami. how many ots. -jesus. yeah, i can still see why he didnt. -mm. i can see why he didn't go. obviously hindsight is twenty twenty and it lost us the game. worried about a 40 yard punt we cant cover. @@ -4478,7 +4083,6 @@ Throwing on third and three instead of running with adrian and getting a field g theres something wrong with the numbers if this is a middle of the road statistical team.. the d is as good as we could have hoped for. The d shut them down. -Cover. If I'm frost I'm leaning hard on the run. Give the line a heads up and be like this is your game. We want to run the ball get it done. @@ -4499,7 +4103,6 @@ MSU beat them pretty good and their offense wasn't that good. I think the offense will look better. We should win. The line is horrible. -On and on. But kicking to the wrong side, slamming the qb after a sack. Turnover is gonna happen. Smothers was driving that ball... @@ -4513,7 +4116,6 @@ It's ridiculous. But the defense is fun to watch. In Oklahoma yes. This last game... -fuck. We had this last game. We had it locked. He kicked that shit to the wrong side of the field how the fuck does that happen? We gave that shit away.. @@ -4521,7 +4123,6 @@ He's still good. Idk if he woulda started with Thomas back strong. D line has been good but having Rogers makes us better. He's a good player I've been excited to see play. -max hype. Casey Rogers should be back this week. gotta give the offense time to rest. Probably that Maryland game. @@ -4563,10 +4164,8 @@ In what sense?. If the expose MSU not upset. That's the no hypest I can get. MSU 3 nwestern 4 Purdue 5 Minnesota 6, Wisconsin 7. -7 wins. Good thing all those games are behind us. I think we sneak over 20 and grab. -W. You get as many sauces as you want if you need the sauce. The chicken is juicy. Dry as I'm they got no sauce. @@ -4585,7 +4184,6 @@ thats a ridiculous statement. on the service, i prefer in and out over cfa. in and out is pretty close tho. best service too really. -cardano?. I know coworkers who door dash. You'll get some fat tips once and awhile. Good way to make extra cash. @@ -4602,8 +4200,6 @@ I guess specifically has anyone made one?. Anyone done anything with nfts?. Idk drunk sports fans whos team just lost a game in the finals are pretty fucking bad. My Twitter is full of go puff husker nil ads. -Basically. -. Not great tho. Not a bad day at the plate. Just giving us some runway for this trip to the moon. @@ -4644,14 +4240,11 @@ lol i dig it. nvm ugghhhhhh. i guess i know who he is.. lol who is this?. -i dig it. Once we get to the moon do we have a plan to get back?. thanks Obama. -Right. I thought this was America.. Theta token is the full name. Some kinda video currency if that matters. -Crypto??. Anyone know anything about theta?. Let it go down so I can reup. It's still up a half a dollar. @@ -4660,7 +4253,6 @@ I almost did before. Everyone was planning to sell. In it for a quick come up. He's doing us a favor talking it up.. -Yeah. I'm not sweating it too hard. I need to get a wallet where I can own my stuff anyways. my wife put in a 100 and i panicked and told her to take it out right away. @@ -4679,35 +4271,30 @@ Thinking of getting some silver. Just to give it a whirl. Did you buy it as an investment. Anyone hold any precious metals?. -Misfire. ive been seeing this a ton on all the platforms and it always makes me laugh. like wins and losses. Am I gonna be able to get my cash outa rh?. its doge day on twitter. doge in full burn. -.50 by the end of the day?. +50 by the end of the day?. i was at 25 like 2 days ago now its 150. the doge is outa control. Fucking doge is outa control. Almost at 30 cents. $ud alt szn. so its based on the price of a bitcoin or the value of the percentage your peeling off to pay your bar tab?. -?. transaction to what? by a candy bar. I'd assume transaction fees will come down as it starts to become more prevalent. I don't know if it will ever hit to where we are using straight bitcoin but ive predicted huge win totals for the huskers going back 15 years now and I wont be right until this season.. Make better solar panels.. -em what?. doubt. when the aliens land you think their gonna want your green paper. id say bitcoin is more like gold than a currency.. -Gold. The good economy was fine for awhile. Economy outgrew it.. so i figure give it a shot. not playing with the rent or booze money or anything.. I thought all the crypto stuff was bullshit till it wasn't. -but yea. its life. yeah, i remember when the wsb thing was first grabbing my attention i saw a few people twitching it. you watch that kaiser guy?. @@ -4722,7 +4309,6 @@ mac for the win. Run that ball. You got your slow king. Gave him that tmagic stat line. -creatons. They splitting it into parts? or just doing it all in one. thought it was coming in novemeber. Gonna have to catch this one in theaters. @@ -4733,7 +4319,6 @@ Castle win?. Is he just far back. He looks tiny. How tall did you make him?. -Not close. so are cat girls pets or people with cat ears.... i used to play magic. What show is that off of?. @@ -4747,12 +4332,10 @@ nice to see a back who can break a tackle. Yeah he just had to lose the weight.. i do think the other guys have potential. Sevion could be good if he knew where the hole was. -agreed. he hits the right hole. i think its gotta be rahmir and yant. yeah i always hated his dad when i was in st louis. kade warner. -Yant. 2020 was a disaster, husker football was not immune. who was a captian because of consistency. as a captian. @@ -4763,7 +4346,6 @@ but we've been playing well. you never know. Yeah thats what im feeling. then i adjusted and now im just another nebraska transplant in the crowd. -Enjoy it. When I first came to best time i felt like superman. Could wake up at 5... could walk around in shorts and tshirt all year round. @@ -4785,15 +4367,12 @@ now i remember. that one we won. I cant remember that year for some reason. we won that right?. -shes. sorry. shes asking for it. -yes. huffing and puffing trying to keep the hype up the last 24 hours. feel like i peaked my hype too early. classic husker fan. Deere living in the past. -sure. helmet game for use. they seem to be fired up. taylors jr year we had a squad. @@ -4821,7 +4400,6 @@ idk they might just come out and run away with it but i really doubt it. At home, coming in with some mo, the defense is flying around. Michigan is going to try to run it, i think our defense is ready for that. wisconsin sucks tho. -i see. oh year. its like 3 hours away. its exactly the same as nebraska. @@ -4853,12 +4431,10 @@ weather and the beaches. Theres a lot of good shows that go one there. I liked LA if it didnt have all the californians in it. thing is people dont pump up LA. -imo. and LA has somethings going for it no other city does. you heard it too!. and i was pulling into work so i couldnt call in. he didnt realize the danger. -dick bag. some iowegian was calling into dan patrick this morning talking about sending in some quality iowa corn. maybe throw denver on that list. or is that austin?. @@ -4884,12 +4460,9 @@ Feel like jojo might just go off. deere should be furious. michigan has the yuppiest fan base. they suck so much. -Deere. thats what im wondering. -lol. a day before the michigan game?!. Are you back to sadsacking?. -Deere?. marty?. whose doubting yant?. kpai hohouli gybor all these guys are going to come up behind reimer henrich who are going to set a really high standard. @@ -4904,7 +4477,6 @@ im not great. but all these years and only one accident and i determined not to be at fault. and it rides kinda low. my problem is i nail potholes and curbs all the time. -hmm. changing what?. might get a hybrid. took it from arizona to florida. @@ -4926,7 +4498,6 @@ sounds like a pain in the ass. lol why didnt it embed that. If we can play without dumb ass mistakes we win by 30. Definitely a risk of devastation hyping this game.. -no. half stepping!. did you parlay that?. domann making some bank. @@ -4945,17 +4516,14 @@ yeah something like that could sully an otherwise good W. but id love to see it. I dont know if michigan is worth that. Think we'll rush the field if we win?. -big game. Crowd should be nuts saturday. oh yea, im way outa school. this might be a good one. -but. man.. every iowa game ive been to we lost. Are we at home against iowa this year?. yeah sounds about perfect. yeah woulda been a good one. -too.. kinda kicking myself. Was worried it might be cold.. which goes against everything i stand for. @@ -4966,7 +4534,6 @@ apologize to yant!. discords been fucking up that shit. network is networth. Diplo was solid for like 4 years. -84. Fran Drescher was born in 57. they put one together for sure. We are trending up.. @@ -4985,7 +4552,6 @@ No one ever over looks us. A game against nebraska in Lincoln is still a big game. closer, but not quite there yet. Strip sacks are my fear. -U. Worried about turnovers. If they can find time and Adrian can slip away from pressure I think our WRs can make plays. We gotta get down field. @@ -5030,13 +4596,11 @@ yeah he was garbage in 19 but we couldnt snap, we couldnt run the plays correctl The play calling sucked too. its so much better this year.. Im surprised the passing game is as good as it is, despite some line issues. -never was. like 175. little under 200 maybe. all the running backs combined.... 75 on the ground 225 through the air. think hes close to 300. -way over. buncha uncle ricos i guess. LIke i love sports (nebraska football) but i dont watch and think, god i wish i was out there, so badly that i push my kid into that mess. maybe if youre a former nba or nfl player or something. @@ -5044,7 +4608,6 @@ who is the parent thinking about their kids career in sports.... worth it i guess. technically i think 21 is the max but just for hypotheticals. what if you were 22 by the time you graduated?. -exactly. How many nebraska players did you guys produce?. its a tough call. in the end if youre right around the cutoff it probably doesnt make too much of a difference. @@ -5058,7 +4621,6 @@ thats pretty crazy. and with a disability its definitely not best practice unless something crazy has happened. eh the numbers on people held back are not great. basically the philosophy im working under. -j/k. but i know exactly who to fail. we are progress focused in my classroom. Yeah we've got half days and grading left so I'm pretty low key here. @@ -5066,7 +4628,6 @@ fair, i like the ball park nachos with the nacho cheese and jalapeno. not a big fan of the super nacho type stuff with the extra goop. If goign that route ill go fries over nachos.. hi Jey, how is your day going. -agree. yeah.. cheese fries over nachos?. what a goober. @@ -5075,9 +4636,7 @@ gives us more space than the screens ever did. think they added the option to replace the screens. just think how good hed be on the wr option. kenny bell talking shit on the triple option?. -good read. triple option >>>>>>>> wr screens. -i see. whats the connection between russ and cooking?. Im missing the joke.... except russ. @@ -5092,7 +4651,6 @@ huge opportunity to change the narrative. in the end they'll realize their mistake. but just like an antivaxxer getting put on a ventilator.. they are basically the plandemic crowd of husker football. -let them. He has 2 years left? wow. Line should be way better. We could make a legit run.. @@ -5101,7 +4659,6 @@ undersized. hes sized maybe but hes explosive. i figured they move him to guard. eh im not sure. -Maybe. Well if thats the case 22 is going to be a ride. hes going to show out with the pro day stuff. i think he might go pro. @@ -5151,18 +4708,14 @@ almost flawless. I can see why people on the outside dont understand but if youve been watching nebraska football you see the difference. If the line can put another game together we are going to make waves. this thing came together at nwestern.. -never. once this big red machine is rolling we are going to win the division 4/5 years. If youre iowa you know the clock is ticking.. they hate us, and they are afraid we will return. People got ptsd from our decade of dominance. It made me angrier than it should have. you dont know yet, but your going to find out. -like gtfo. was listening to a husker stream yesterday that had some bitchigan fans complaining how nebraska thinks they are going to win. -love it.. Last chance for frost to make this a great season imo. -karma. see what he says. Ask him Do you smell that?. id like to think so. @@ -5171,7 +4724,6 @@ We're only going to beat michigan by 28 sorry. I'm a Mets fan. I like the cardinals. Enjoyed following them when I lived in St. -Louis. But I am born and raised mets fan. Maxhype Albert Pujols tho. Fuck the cubs 4 lyfe. diff --git a/utilities/embed.py b/utilities/embed.py deleted file mode 100644 index 83273878..00000000 --- a/utilities/embed.py +++ /dev/null @@ -1,330 +0,0 @@ -from datetime import datetime - -import discord -import validators - -import objects.FAPing as FAP -from objects.Schedule import HuskerSchedule -from utilities.constants import ( - BOT_DISPLAY_NAME, - BOT_FOOTER_BOT, - BOT_GITHUB_URL, - BOT_ICON_URL, - BOT_THUMBNAIL_URL, - DT_OBJ_FORMAT, - DT_OBJ_FORMAT_TBA, - DT_STR_RECRUIT, - DT_TBA_HR, - DT_TBA_MIN, - TZ, -) - - -class EmbedType: - rich = "rich" - image = "image" - video = "video" - gifv = "gifv" - article = "article" - link = "link" - - -def build_embed(**kwargs): - title_limit = name_limit = 256 - desc_limit = 4096 - fields_limit = 25 - value_limit = 1024 - footer_limit = 2048 - # embed_limit = 6000 - - if kwargs.get("color", False): - embed = discord.Embed( - timestamp=datetime.now().astimezone(tz=TZ), color=kwargs.get("color") - ) - else: - embed = discord.Embed( - timestamp=datetime.now().astimezone(tz=TZ), color=0xD00000 - ) - - if kwargs.get("author", False) is not None: - embed.set_author( - name=BOT_DISPLAY_NAME, url=BOT_GITHUB_URL, icon_url=BOT_ICON_URL - ) - - if kwargs.get("description", False): - embed.description = kwargs.get("description")[:desc_limit] - - if kwargs.get("title", False): - embed.title = kwargs.get("title")[:title_limit] - - if kwargs.get("url", False) and validators.url(kwargs.get("url")): - embed.url = kwargs.get("url") - - if kwargs.get("type", False): - embed.type = kwargs.get("type") - else: - embed.type = EmbedType.rich - - if kwargs.get("footer", False): - embed.set_footer( - text=kwargs.get("footer")[:footer_limit], icon_url=BOT_ICON_URL - ) - - if kwargs.get("image", False) and validators.url(kwargs.get("image")): - if kwargs.get("image") is not None: - embed.set_image(url=kwargs["image"]) - - if kwargs.get("thumbnail", False): - embed.set_thumbnail(url=kwargs.get("thumbnail")) - else: - if kwargs.get("thumbnail") is not None: - embed.set_thumbnail(url=BOT_THUMBNAIL_URL) - - if kwargs.get("fields", False): - for index, field in enumerate(kwargs["fields"]): - if index >= fields_limit: - break - - if kwargs.get("inline", False): - embed.add_field( - name=str(field[0])[:name_limit], - value=str(field[1])[:value_limit], - inline=kwargs["inline"], - ) - else: - embed.add_field( - name=str(field[0])[:name_limit], - value=str(field[1])[:value_limit], - inline=False, - ) - - return embed - - -def build_countdown_embed( - days: int, - hours: int, - minutes: int, - opponent, - thumbnail, - date_time: datetime, - consensus, - location, -): - if ( - date_time.hour == DT_TBA_HR and date_time.minute == DT_TBA_MIN - ): # Place holder hour and minute to signify TBA games - return build_embed( - title="Countdown until...", - inline=False, - thumbnail=thumbnail, - fields=[ - ["Opponent", opponent], - ["Scheduled Time", date_time.strftime(DT_OBJ_FORMAT_TBA)], - ["Location", location], - ["Time Remaining", f"{days}"], - ["Betting Info", consensus if consensus else "Line TBD"], - ], - ) - else: - return build_embed( - title="Countdown until...", - inline=False, - thumbnail=thumbnail, - fields=[ - ["Opponent", opponent], - ["Scheduled Time", date_time.strftime(DT_OBJ_FORMAT)], - ["Location", location], - [ - "Time Remaining", - f"{days} days, {hours} hours, and {minutes} minutes", - ], - ["Betting Info", consensus if consensus else "Line TBD"], - ], - ) - - -def build_recruit_embed(recruit): - def prettify_predictions(): - pretty = "" - for item in recruit.cb_predictions: - pretty += f"{item}\n" - return pretty - - def prettify_experts(): - pretty = "" - for item in recruit.cb_experts: - pretty += f"{item}\n" - return pretty - - def prettify_offers(): - pretty = "" - for index, item in enumerate(recruit.recruit_interests): - if index > 9: - return ( - pretty - + f"[View remaining offers...]({recruit.recruit_interests_url})" - ) - - pretty += f"{item.school}{' - ' + item.status if not item.status == 'None' else ''}\n" - - return pretty - - def fap_predictions(): - fap_preds = FAP.get_faps(recruit) - if fap_preds is None: - return "There are no predictions for this recruit." - else: - init_string = f"Team: Percent (Avg Confidence)" - for p in fap_preds: - init_string += ( - f"\n{p['team']}: {p['percent']:.0f}% ({p['confidence']:.1f})" - ) - init_string += f"\nTotal Predictions: {fap_preds[0]['total']}" - return init_string - - nl = "\n" - embed = build_embed( - title=f"{str(recruit.rating_stars) + 'โญ ' if recruit.rating_stars else ''}{recruit.year} {recruit.position}, {recruit.name}", - description=f"{recruit.committed if recruit.committed is not None else ''}" - f"{': ' + recruit.committed_school if recruit.committed_school is not None else ''} " - f"{': ' + str(datetime.strptime(recruit.commitment_date, DT_STR_RECRUIT)) if recruit.commitment_date is not None else ''}", - fields=[ - [ - "**Biography**", - f"{recruit.city}, {recruit.state}\n" - f"School: {recruit.school}\n" - f"School Type: {recruit.school_type}\n" - f"Height: {recruit.height}\n" - f"Weight: {recruit.weight}\n", - ], - [ - "**Social Media**", - f"{'[@' + recruit.twitter + '](' + 'https://twitter.com/' + recruit.twitter + ')' if not recruit.twitter == 'N/A' else 'N/A'}", - ], - [ - "**Highlights**", - f"{'[247Sports](' + recruit._247_highlights + ')' if recruit._247_highlights else '247Sports N/A'}", - ], - [ - "**Recruit Info**", - f"[247Sports Profile]({recruit._247_profile})\n" - f"Comp. Rating: {recruit.rating_numerical if recruit.rating_numerical else 'N/A'} \n" - f"Nat. Ranking: [{recruit.ranking_national:,}](https://247sports.com/Season/{recruit.year}-Football/CompositeRecruitRankings/?InstitutionGroup" - f"={recruit.school_type.replace(' ', '')})\n" - f"State Ranking: [{recruit.ranking_state}](https://247sports.com/Season/{recruit.year}-Football/CompositeRecruitRankings/?InstitutionGroup={recruit.school_type.replace(' ', '')}&State" - f"={recruit.state_abbr})\n" - f"Pos. Ranking: [{recruit.ranking_position}](https://247sports.com/Season/{recruit.year}-Football/CompositeRecruitRankings/?InstitutionGroup=" - f"{recruit.school_type.replace(' ', '')}&Position={recruit.position})\n" - f"{'All Time Ranking: [' + recruit.ranking_all_time + '](https://247sports.com/Sport/Football/AllTimeRecruitRankings/)' + nl if recruit.ranking_all_time else ''}" - f"{'Early Enrollee' + nl if recruit.early_enrollee else ''}" - f"{'Early Signee' + nl if recruit.early_signee else ''}" - f"{'Walk-On' + nl if recruit.walk_on else ''}", - ], - [ - "**Expert Averages**", - f"{prettify_predictions() if recruit.cb_predictions else 'N/A'}", - ], - [ - "**Lead Expert Picks**", - f"{prettify_experts() if recruit.cb_experts else 'N/A'}", - ], - [ - "**Offers**", - f"{prettify_offers() if recruit.recruit_interests else 'N/A'}", - ], - ["**FAP Predictions**", f"{fap_predictions()}"], - ], - ) - - if (recruit.committed.lower() if recruit.committed is not None else None) not in [ - "signed", - "enrolled", - ]: - if (FAP.get_croot_predictions(recruit)) is not None: - embed.set_footer( - text=BOT_FOOTER_BOT - + "\nClick the ๐Ÿ”ฎ to predict what school you think this recruit will commit to." - "\nClick the ๐Ÿ“œ to get the individual predictions for this recruit." - ) - else: - embed.set_footer( - text=BOT_FOOTER_BOT - + "\nClick the ๐Ÿ”ฎ to predict what school you think this recruit will commit to." - ) - else: - if (FAP.get_croot_predictions(recruit)) is not None: - embed.set_footer( - text=BOT_FOOTER_BOT - + "\nClick the ๐Ÿ“œ to get the individual predictions for this recruit." - ) - else: - embed.set_footer(text=BOT_FOOTER_BOT) - - if recruit.thumbnail and not recruit.thumbnail == "/.": - embed.set_thumbnail(url=recruit.thumbnail) - else: - embed.set_thumbnail(url=BOT_THUMBNAIL_URL) - return embed - - -def build_schedule_embed(year, **kwargs): - scheduled_games, season_stats = HuskerSchedule(year=year, sport=kwargs["sport"]) - - arrow = "ยป " - new_line_char = "\n" - fields = [] - - for game in scheduled_games: - field_value = ( - f"{arrow + game.outcome + new_line_char if not game.outcome == '' else ''}" - f"{arrow}{game.game_date_time.strftime(DT_OBJ_FORMAT) if not game.game_date_time.hour == 21 else game.game_date_time.strftime(DT_OBJ_FORMAT_TBA)}{new_line_char}" - f"{arrow}{game.location}" - ) - - fields.append( - [ - f"**Week {game.week}: {game.opponent} ({'Home' if game.home else 'Away'}) **", - field_value, - ] - ) - - embed = build_embed( - title=f"Nebraska's {year} Schedule ({season_stats.wins} - {season_stats.losses})", - inline=False, - fields=fields, - ) - - return embed - - -def return_schedule_embeds(year, **kwargs): - scheduled_games, season_stats = HuskerSchedule(year=year, sport=kwargs["sport"]) - - new_line_char = "\n" - embeds = [] - - for game in scheduled_games: - embeds.append( - build_embed( - title=f"{game.opponent.title()}", - description=f"Nebraska's {year}'s Record: {season_stats.wins} - {season_stats.losses}", - inline=False, - thumbnail=game.icon, - fields=[ - [ - "Opponent", - f"{game.ranking + ' ' if game.ranking else ''}{game.opponent}", - ], - ["Conference Game", "Yes" if game.conference else "No"], - [ - "Date/Time", - f"{game.game_date_time.strftime(DT_OBJ_FORMAT) if not game.game_date_time.hour == 21 else game.game_date_time.strftime(DT_OBJ_FORMAT_TBA)}{new_line_char}", - ], - ["Location", game.location], - ["Outcome", game.outcome if game.outcome else "TBD"], - ], - ) - ) - - return embeds diff --git a/utilities/mysql.py b/utilities/mysql.py deleted file mode 100644 index 7ea7ded1..00000000 --- a/utilities/mysql.py +++ /dev/null @@ -1,337 +0,0 @@ -import pymysql.cursors - -from utilities.constants import SQL_HOST, SQL_PASSWD, SQL_DB, SQL_USER - - -def log(message: str, level: int): - import datetime - - if level == 0: - print(f"[{datetime.datetime.now()}] ### MySQL: {message}") - elif level == 1: - print(f"[{datetime.datetime.now()}] ### ~~~ MySQL: {message}") - - -# Image Command -sqlCreateImageCommand = """ -INSERT INTO img_cmd_db (author, img_name, img_url) VALUES (%s, %s, %s) -""" - -sqlSelectImageCommand = """ -SELECT author, img_name, img_url FROM img_cmd_db WHERE img_name = %s -""" - -sqlSelectAllImageCommand = """ -SELECT author, img_name, img_url, created_at FROM img_cmd_db -""" - -sqlDeleteImageCommand = """ -DELETE FROM img_cmd_db WHERE img_name = %s AND author = %s -""" - -# Croot Bot -sqlTeamIDs = """ -SELECT id, school from botfrost.team_ids -""" - -# Iowa Command -sqlInsertIowa = """ -INSERT INTO iowa (user_id, reason, previous_roles) VALUES (%s, %s, %s) -""" - -sqlRetrieveIowa = """ -SELECT previous_roles FROM iowa WHERE user_id = %s -""" - -sqlRemoveIowa = """ -DELETE FROM iowa WHERE user_id = %s -""" - -# Tasks -sqlRetrieveTasks = """ -SELECT * FROM tasks_repo WHERE is_open = 1 -""" - -sqlRecordTasks = """ -INSERT INTO tasks_repo (send_to, message, send_when, is_open, author) VALUES (%s, %s, %s, %s, %s) -""" - -sqlUpdateTasks = """ -UPDATE tasks_repo SET is_open = %s WHERE send_to = %s AND message = %s AND send_when = %s AND author = %s -""" - -# Karma -sqlUpdateKarma = """ -INSERT INTO karma (positive, negative, total) -VALUES (%s, %s, %s) -ON DUPLICATE KEY UPDATE (positive=%s, negative=%s, total=%s) -""" - -# sqlRecordStats = """ -# INSERT INTO stats (source, channel) VALUES (%s, %s) -# """ - -# sqlRecordStatsManual = """ -# INSERT INTO stats (source, channel, created_at) VALUES (%s, %s, %s) -# """ - -# sqlRetrieveStats = """ -# SELECT * FROM stats -# """ - -# sqlRetrieveCurrencyLeaderboard = """ -# SELECT * FROM currency ORDER BY balance DESC -# """ - -# sqlRetrieveCurrencyUser = """ -# SELECT balance FROM currency WHERE user_id = %s -# """ - -# sqlCheckCurrencyInit = """ -# SELECT init FROM currency WHERE user_id = %s -# """ - -# sqlSetCurrency = """ -# INSERT INTO currency (username, init, balance, user_id) VALUES (%s, 1, %s, %s) -# """ - -# sqlUpdateCurrency = """ -# UPDATE currency SET balance = balance + %s WHERE username = %s -# """ - -# sqlRetrieveAllCustomLinesKeywords = """ -# SELECT clb.source, cl.keyword, cl.description, clb.value, clb._for, clb.against, cl.source as orig_author, cl.result FROM custom_lines_bets clb INNER JOIN custom_lines cl ON (cl.keyword = clb.keyword ) WHERE clb.keyword = %s -# """ - -# sqlRetrieveOneCustomLinesKeywords = """ -# SELECT clb.source, cl.keyword, cl.description, clb.value, clb._for, clb.against, cl.source as orig_author, cl.result FROM custom_lines_bets clb INNER JOIN custom_lines cl ON (cl.keyword = clb.keyword ) WHERE clb.keyword = %s AND clb.source = %s -# """ - -# sqlInsertCustomLines = """ -# INSERT INTO custom_lines (source, keyword, description, result) VALUES (%s, %s, %s, 'tbd') -# """ - -# sqlRetrieveAllOpenCustomLines = """ -# SELECT * FROM custom_lines WHERE result = 'tbd' -# """ - -# sqlRetreiveCustomLinesForAgainst = """ -# SELECT source, _for, against, value FROM custom_lines_bets WHERE keyword = %s -# """ - -# sqlRetrieveOneOpenCustomLine = """ -# SELECT * FROM custom_lines WHERE keyword = %s and result = 'tbd' -# """ - -# sqlInsertCustomLinesBets = """ -# INSERT INTO custom_lines_bets (source, keyword, _for, against, value) VALUES (%s, %s, %s, %s, %s) -# """ - -# sqlUpdateCustomLinesBets = """ -# UPDATE custom_lines_bets SET `_for`=%s, against=%s, value=%s WHERE source=%s AND keyword=%s -# """ - -# sqlUpdateCustomLinesResult = """ -# UPDATE custom_lines SET result = %s WHERE keyword = %s -# """ -# sqlDatabaseTimestamp = """ -# INSERT INTO bot_connections (user, connected, timestamp) VALUES (%s, %s, %s) -# """ - -# sqlLogError = """ -# INSERT INTO bot_error_log (user, error) VALUES (%s, %s) -# """ - -# sqlLogUser = """ -# INSERT INTO bot_user_log (user, event, comment) VALUES (%s, %s, %s) -# """ - -# sqlLeaderboard = """ -# SELECT user, COUNT(*) as num_games_bet_on, SUM(CASE b.win WHEN g.win THEN 1 ELSE 0 END) as correct_wins, SUM(CASE b.spread WHEN g.spread THEN 1 ELSE 0 END) as correct_spreads, SUM(CASE b.moneyline WHEN g.moneyline THEN 1 ELSE 0 END) as correct_moneylines, SUM(CASE b.win WHEN g.win THEN 1 ELSE 0 END + CASE b.spread WHEN g.spread THEN 2 ELSE 0 END + CASE b.moneyline WHEN g.moneyline THEN 2 ELSE 0 END) as total_points FROM bets b INNER JOIN games g ON (b.game_number = g.game_number) WHERE g.finished = true GROUP BY user ORDER BY total_points DESC; -# """ - -# sqlAdjustedLeaderboard = """ -# SELECT user, COUNT(*) as num_games_bet_on, SUM(CASE b.win WHEN g.win THEN 1 ELSE 0 END) as correct_wins, SUM(CASE b.spread WHEN g.spread THEN 1 ELSE 0 END) as correct_spreads, SUM(CASE b.moneyline WHEN g.moneyline THEN 1 ELSE 0 END) as correct_moneylines, SUM(CASE b.win WHEN g.win THEN 1 ELSE 0 END + CASE b.spread WHEN g.spread THEN 2 ELSE 0 END + CASE b.moneyline WHEN g.moneyline THEN 2 ELSE 0 END) / COUNT(*) as avg_pts_per_game FROM bets b INNER JOIN games g ON (b.game_number = g.game_number) WHERE g.finished = true GROUP BY user ORDER BY avg_pts_per_game DESC; -# """ - -# sqlGetWinWinners = """ -# SELECT b.user FROM bets b INNER JOIN games g ON (b.game_number = g.game_number) WHERE b.win = g.win AND g.opponent = %s; -# """ - -# sqlGetSpreadWinners = """ -# SELECT b.user FROM bets b INNER JOIN games g ON (b.game_number = g.game_number) WHERE b.spread = g.spread AND g.opponent = %s; -# """ - -# sqlGetMoneylineWinners = """ -# SELECT b.user FROM bets b INNER JOIN games g ON (b.game_number = g.game_number) WHERE b.moneyline = g.moneyline AND g.opponent = %s; -# """ - -# sqlInsertWinorlose = """ -# INSERT INTO bets (game_number, user, win, date_updated) VALUES (%s, %s, %s, NOW()) ON DUPLICATE KEY UPDATE win=%s; -# """ - -# sqlInsertSpread = """ -# INSERT INTO bets (game_number, user, spread, date_updated) VALUES (%s, %s, %s, NOW()) ON DUPLICATE KEY UPDATE spread=%s; -# """ - -# sqlInsertMoneyline = """ -# INSERT INTO bets (game_number, user, moneyline, date_updated) VALUES (%s, %s, %s, NOW()) ON DUPLICATE KEY UPDATE moneyline=%s; -# """ - -# sqlRetrieveBet = """ -# SELECT * FROM bets b WHERE b.user = %s; -# """ - -# sqlRetrieveSpecificBet = """ -# SELECT * FROM bets b WHERE b.game_number = %s AND b.user = %s; -# """ - -# sqlRetrieveGameNumber = """ -# SELECT g.game_number FROM games g WHERE g.opponent = %s; -# """ - -# sqlRetrieveAllBet = """ -# SELECT * FROM bets b WHERE b.game_number = %s; -# """ - -# sqlUpdateScores = """ -# INSERT INTO games (game_number, score, opponent_score) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE score=%s, opponent_score=%s -# """ - -# sqlUpdateAllBetCategories = """ -# INSERT INTO games (game_number, finished, win, spread, moneyline) VALUES (%s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE finished=%s, win=%s, spread=%s, moneyline=%s -# """ - -# sqlRetrieveGameInfo = """ -# SELECT * FROM games g WHERE g.game_number = %s; -# """ - -# sqlRetrieveCrystalBallLastRun = """ -# SELECT last_run FROM cb_lastrun -# """ - -# sqlUpdateCrystalLastRun = """ -# INSERT INTO cb_lastrun (last_run) VALUES (%s) -# """ - -# sqlUpdateCrystalBall = """ -# INSERT INTO crystal_balls (first_name, last_name, full_name, photo, prediction, result) VALUES (%s, %s, %s, %s, %s, %s) -# """ - -# sqlRetrieveRedditInfo = """ -# SELECT * FROM subreddit_info; -# """ - -# sqlUpdateLineInfo = """ -# INSERT INTO games (game_number, spread_value, moneyline_value) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE spread_value=%s, moneyline_value=%s -# """ - -# sqlRetrieveTriviaQuestions = """ -# SELECT * FROM trivia -# """ - -# sqlInsertTriviaScore = """ -# INSERT INTO trivia_scores (user, score) VALUES (%s, %s) ON DUPLICATE KEY UPDATE score=score+%s -# """ - -# sqlZeroTriviaScore = """ -# INSERT INTO trivia_scores (user, score) VALUES (%s, %s) ON DUPLICATE KEY UPDATE score=score+%s -# """ - -# sqlRetrieveTriviaScores = """ -# SELECT * FROM trivia_scores ORDER BY score DESC -# """ - -# sqlClearTriviaScore = """ -# TRUNCATE TABLE trivia_scores -# """ - - -def Process_MySQL(query: str, **kwargs): - log(f"Starting a MySQL query", 0) - try: - sqlConnection = pymysql.connect( - host=SQL_HOST, - user=SQL_USER, - password=SQL_PASSWD, - db=SQL_DB, - charset="utf8mb4", - cursorclass=pymysql.cursors.DictCursor, - ) - log(f"Connected to the MySQL Database!", 1) - except: # noqa - log(f"Unable to connect to the `{SQL_DB}` database.", 1) - return - - result = None - - try: - with sqlConnection.cursor() as cursor: - fetch_witch = kwargs.get("fetch", None) - vals = kwargs.get("values", None) - - if fetch_witch is None: - log("No fetch found", 1) - - cursor.execute(query=query, args=vals) - else: - log("Fetch found", 1) - - if fetch_witch == "one": - cursor.execute(query=query, args=vals) - result = cursor.fetchone() - elif fetch_witch == "many": - assert kwargs.get("size"), ValueError( - "Fetching many requires a size in kwargs" - ) - - cursor.execute(query=query, args=vals) - result = cursor.fetchmany(kwargs.get("size", 5)) - elif fetch_witch == "all": - cursor.execute(query=query, args=vals) - result = cursor.fetchall() - - # if not kwargs.get("values"): - # if fetch_witch == "one": - # cursor.execute(query=query) - # result = cursor.fetchone() - # elif fetch_witch == "many": - # assert kwargs.get("size"), ValueError( - # "Fetching many requires a size in kwargs" - # ) - # - # cursor.execute(query=query) - # result = cursor.fetchmany(kwargs["size"]) - # elif fetch_witch == "all": - # cursor.execute(query=query) - # result = cursor.fetchall() - # else: - # vals = kwargs.get("vals", None) - # - # if fetch_witch == "one": - # cursor.execute(query=query, args=vals) - # result = cursor.fetchone() - # elif fetch_witch == "many": - # assert kwargs.get("size"), ValueError( - # "Fetching many requires a size in kwargs" - # ) - # - # cursor.execute(query=query, args=vals) - # result = cursor.fetchmany(kwargs["size"]) - # elif fetch_witch == "all": - # cursor.execute(query=query, args=vals) - # result = cursor.fetchall() - - sqlConnection.commit() - except pymysql.err.ProgrammingError as e: - log(e, 1) - raise e - except: # noqa - raise "Unknown error" # ConnectionError("Error occurred opening the MySQL database.") - finally: - log(f"Closing connection to the MySQL Database", 1) - sqlConnection.close() - - if result: - log(f"MySQL query finished", 0) - return result