WIP: uncommitted local changes before archival

- Modified cogs/dice.py, cogs/fun.py, db_calls.py
- Added COMMAND_LIST.md, api_calls/custom_command.py, sba_is_fun.db

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-03-23 14:11:58 -05:00
parent cdfe54cdf7
commit 9770e360c3
6 changed files with 907 additions and 421 deletions

237
COMMAND_LIST.md Normal file
View File

@ -0,0 +1,237 @@
# Discord Bot v1.0 - Complete Command List
**Generated:** January 2025
**Bot Version:** 1.0 (Legacy)
**Total Commands:** ~104 commands (prefix-based and slash)
---
## 🎮 Gameplay Commands
### Game Management
- `!newgame <away_abbrev> <home_abbrev> <week> [game_num] [is_pd]` - Start a new baseball game
- `!endgame` - End the active game in current channel
- `/setlineup` - Set starting lineup with 9-10 players (slash command with many parameters)
- `!substitution` / `!sub` - Make a lineup substitution
- `!gamestate` / `!gs` - Display current game state
- `/show-card defense <position>` - Display defender's player card
### Logging On-Base Results
- `!log-onbase single-wellhit` / `!siwh` / `!si**` / `!1b**` / `!1bwh` - Single, runners advance 2 bases
- `!log-onbase single-onestar` / `!si*` / `!1b*` - Single, runners advance 1 base
- `!log-onbase ballpark-single` / `!bpsi` / `!bp1b` - Ballpark single
- `!log-onbase single-uncapped` / `!si` / `!1b` - Single with optional runner advancement
- `!log-onbase double-twostar` / `!do**` / `!2b**` - Double, runners advance 2 bases
- `!log-onbase double-uncapped` / `!do` / `!2b` - Double with optional runner advancement
- `!log-onbase double-threestar` / `!dowh` / `!do***` - Double, runners advance 3 bases
- `!log-onbase triple` / `!tr` / `!3b` - Triple, all runners score
- `!log-onbase homerun` / `!hr` / `!dong` - Home run
- `!log-onbase ballpark-homerun` / `!bp-hr` / `!bp-dong` - Ballpark home run
- `!log-onbase walk` / `!bb` - Walk, forced runners advance
- `!log-onbase intentional-walk` / `!ibb` - Intentional walk
- `!log-onbase hit-by-pitch` / `!hbp` - Hit by pitch
### Logging Out Results
- `!log-out popout` / `!po` - Popout
- `!log-out strikeout` / `!so` / `!k` - Strikeout
- `!log-out lineout` / `!lo` - Lineout
- `!log-out sac-bunt` / `!sacb` / `!bunt` - Sacrifice bunt
- `!log-out caught-stealing` / `!cs` - Caught stealing
- `!log-out flyball-a` / `!flya` - Flyball, all runners advance
- `!log-out flyball-b` / `!flyb` - Flyball, runner on third scores
- `!log-out flyball-bq` / `!flyb?` - Flyball, runner on third may score
- `!log-out flyball-c` / `!flyc` - Flyball, no runners advance
- `!log-out groundball-a` / `!gba` - Potential double play ground ball
### Special Play Results
- `!log-play undo-play` / `!undo` / `!rollback` - Undo most recent play
- `!log-play stolen-base` / `!sb` - Stolen base
- `!log-play wild-pitch` / `!wp` - Wild pitch
- `!log-play passed-ball` / `!pb` - Passed ball
- `!log-play balk` / `!bk` - Balk
- `!log-play pickoff` / `!pick` - Pickoff
- `!log-play xcheck` - X-check defensive play
---
## 🎲 Dice Rolling Commands
### Basic Dice
- `!roll <dice>` - Roll polyhedral dice (XdY notation)
- `!ab` - Roll at-bat dice (1d6;2d6;1d20)
### Advanced Dice
- `!scout <card_type>` - Roll weighted scouting dice
- `!fielding <position>` - Roll Super Advanced fielding dice
- `!weather [team_abbrev]` - Roll ballpark weather
---
## 👥 Player Commands
- `!player <name>` - Display player card and statistics
- `!player-stats <name>` - Display detailed player statistics
- `!compare <player1> <player2>` - Compare two players
---
## 🏟️ Team Commands
- `!team <abbrev>` - Display team information
- `!roster <abbrev>` - Display team roster
- `!schedule [team_abbrev]` - Display team schedule
- `!standings` - Display league standings
---
## 🔄 Transaction Commands
- `!mymoves` - View your pending transactions
- `!legal` - Check roster legality
- `!dropadd` - Build a transaction
- `!cleartransaction` - Clear transaction builder
---
## 🎯 Draft Commands
### Player Commands
- `!select <name>` / `!pick` / `!draft` / `!gib` / `!gimme` - Draft a player
- `!list [player1, player2, ...]` / `!draftlist` / `!mylist` - Set/view draft list
- `!whomst` / `!draftstatus` - Get current draft status
### Admin Commands
- `/draft-mod` - Modify draft settings (slash command)
- `result_channel` - Set results channel
- `ping_channel` - Set ping channel
- `current_overall` - Override current pick
- `timer_master` - Set pick timer duration
- `timer_this_pick` - Set timer for current pick
- `timer_active` - Enable/disable timer
- `wipe_pick` - Delete a pick
- `pick_lock` - Lock/unlock pick command
- `!restart-loop` - Restart draft loop (mod only)
---
## 📝 Custom Commands
- `!cc <name>` - Execute a custom command
- `!about <command>` - Show who created a custom command
- `!newcc <name> <message>` - Create new custom command
- `!delcc <name>` - Delete your custom command
- `!allcc [page]` - Show all custom commands
- `!mycc` / `!showcc` - Show your custom commands
---
## 🎭 Fun/Meme Commands
- `!lastsoak` / `!ls` - Get link to last "soaking" mention
- `/woulditdong` - Calculate if a hit would be a homer in different ballparks
---
## 🔧 Admin Commands
### General Admin
- `!current` - Display current season/week info
- `!blast <channel> <message>` - Send message to channel
- `/blast` - Send formatted message/embed (slash command)
- `!test <sheet_url>` - Import game from Google Sheet
### Player Management
- `!setdemweek <week> <player>` - Set player's demotion week
- `!migrate-players <from_season> <to_season>` - Migrate players between seasons
### Draft Management (Keeper System)
- `!keepers <team_abbrev> <player1, player2, ...>` - Set team keepers (admin only)
- `/set-keepers` - Interactive keeper selection (deprecated)
---
## 🛠️ Owner Commands
### Cog Management
- `!load <cog>` - Load a cog
- `!unload <cog>` - Unload a cog
- `!reload <cog>` - Reload a cog
- `!fullreset` - Reload all cogs
- `!sync [~|*|!|^]` - Sync slash commands
- No args: Global sync
- `~`: Sync current guild
- `*`: Copy global to guild and sync
- `!`: Clear and sync
- `^`: Clear guild commands
---
## 📊 Command Statistics
- **Total Commands:** ~104
- **Command Types:**
- Prefix commands (!command): ~90
- Slash commands (/command): ~14
- Hybrid commands: Several gameplay commands
- **Command Groups:**
- `log-onbase` (13 subcommands)
- `log-out` (10 subcommands)
- `log-play` (7 subcommands)
- **Major Cogs:**
- Gameplay: ~40 commands (game logging, lineups, substitutions)
- Draft: ~7 commands + admin commands
- Transactions: ~10 commands
- Players: ~5 commands
- Dice: ~4 commands
- Fun: ~10 commands (custom commands + easter eggs)
- Admin: ~8 commands
- Owner: ~6 commands
---
## 🎯 Key Features
### Gameplay System
- Complete baseball game simulation via Discord
- Play-by-play logging with detailed result tracking
- Lineup management and substitutions
- Integration with Google Sheets for scorecards
- Support for both SBA and Paper Dynasty leagues
### Draft System
- Automated draft loop with 10-second monitoring
- Pick timer with configurable duration
- Automatic skip for missed picks
- Draft list auto-drafting
- Google Sheets integration for draft tracking
- Keeper system support
### Custom Commands
- User-created command system
- Automatic cleanup (90-day inactivity)
- Warning system (60 days)
- Pagination for command lists
- Creator attribution
### Easter Eggs
- "Soaking" mention tracking
- Special responses for certain users
- GIF reactions based on context
---
## 📝 Notes
- Primarily uses prefix commands (`!command`)
- Limited slash command implementation
- Heavy reliance on Google Sheets integration
- Designed for in-channel baseball game play
- No structured logging system (basic Python logging)
- No caching infrastructure
- Direct database calls throughout cogs
- Manual error handling in each command
- No decorator patterns for common operations
---
**Last Updated:** January 2025

122
api_calls/custom_command.py Normal file
View File

@ -0,0 +1,122 @@
import logging
import pydantic
from typing import Optional, List
from datetime import datetime
from db_calls import db_get
from exceptions import log_exception, ApiException
logger = logging.getLogger('discord_app')
class CustomCommandCreator(pydantic.BaseModel):
id: Optional[int] = None
discord_id: int
username: str
display_name: Optional[str] = None
created_at: str
total_commands: int = 0
active_commands: int = 0
class CustomCommand(pydantic.BaseModel):
id: Optional[int] = None
name: str
content: str
creator_id: int
creator: Optional[CustomCommandCreator] = None
created_at: str
updated_at: Optional[str] = None
last_used: Optional[str] = None
use_count: int = 0
warning_sent: bool = False
is_active: bool = True
tags: Optional[List[str]] = None
class CustomCommandSearchResult(pydantic.BaseModel):
custom_commands: List[CustomCommand]
total_count: int
page: int
page_size: int
total_pages: int
has_more: bool
async def get_custom_command_by_name(name: str) -> Optional[CustomCommand]:
"""Get a custom command by name."""
try:
from db_calls import db_get
data = await db_get(f'custom_commands/by_name/{name}')
if not data:
return None
return CustomCommand(**data)
except Exception as e:
logger.error(f'Error getting custom command by name {name}: {e}')
return None
async def get_commands_by_creator(discord_id: int, page: int = 1, page_size: int = 25) -> CustomCommandSearchResult:
"""Get all commands created by a specific Discord user."""
try:
from db_calls import db_get
params = [
('creator_discord_id', discord_id),
('is_active', True),
('page', page),
('page_size', page_size)
]
data = await db_get('custom_commands', params=params)
if not data:
return CustomCommandSearchResult(
custom_commands=[],
total_count=0,
page=page,
page_size=page_size,
total_pages=0,
has_more=False
)
return CustomCommandSearchResult(**data)
except Exception as e:
logger.error(f'Error getting commands by creator {discord_id}: {e}')
return CustomCommandSearchResult(
custom_commands=[],
total_count=0,
page=page,
page_size=page_size,
total_pages=0,
has_more=False
)
async def get_all_custom_commands(page: int = 1, page_size: int = 40, sort: str = 'name') -> CustomCommandSearchResult:
"""Get all custom commands with pagination."""
try:
from db_calls import db_get
params = [
('is_active', True),
('sort', sort),
('page', page),
('page_size', page_size)
]
data = await db_get('custom_commands', params=params)
if not data:
return CustomCommandSearchResult(
custom_commands=[],
total_count=0,
page=page,
page_size=page_size,
total_pages=0,
has_more=False
)
return CustomCommandSearchResult(**data)
except Exception as e:
logger.error(f'Error getting all custom commands: {e}')
return CustomCommandSearchResult(
custom_commands=[],
total_count=0,
page=page,
page_size=page_size,
total_pages=0,
has_more=False
)

View File

@ -1,5 +1,6 @@
import re
from api_calls.current import get_current
from helpers import *
from db_calls import get_team_by_abbrev
import discord
@ -15,7 +16,24 @@ class Dice(commands.Cog):
self.bot = bot
self.rolls = []
self.current = None
self.cone = None
self.square = None
self.cube = None
self.get_current.start()
@tasks.loop(hours=1)
async def get_current(self):
self.current = await get_current()
g_query = await db_get('games', params=[('season', self.current.season), ('week', self.current.week), ('team1_id', 450)])
if g_query is None:
return
if g_query['count'] > 0:
self.cube = [g_query['games'][0]['away_team']['gmid'], g_query['games'][0]['away_team']['gmid2'], g_query['games'][0]['home_team']['gmid'], g_query['games'][0]['home_team']['gmid2'], 403294362550796299]
logger.info(f'cubed {self.cube}')
async def cog_command_error(self, ctx, error):
logger.error(msg=error, stack_info=True, exc_info=True)
await ctx.send(f'{error}\n\nRun !help <command_name> to see the command requirements')
@ -23,7 +41,7 @@ class Dice(commands.Cog):
async def slash_error(self, ctx, error):
logger.error(msg=error, stack_info=True, exc_info=True)
await ctx.send(f'{error[:1600]}')
async def get_dice_embed(self, channel, title, message):
try:
team_abbrev = re.split('-', channel.name)
@ -49,6 +67,21 @@ class Dice(commands.Cog):
return embed
@commands.command(hidden=True)
@commands.is_owner()
async def flag(self, ctx, *arg):
self.cone = arg
await ctx.message.delete()
logger.info(f'{self.cone}')
@commands.command(hidden=True)
@commands.is_owner()
async def square(self, ctx, arg: bool = True):
self.square = arg
await ctx.message.delete()
logger.info(f'{self.square}')
@commands.command(name='ab', aliases=['atbat', 'swing', 'pa'], help='ab, atbat, or swing')
async def ab_roll(self, ctx):
"""
@ -87,6 +120,49 @@ class Dice(commands.Cog):
d_six_three = random.randint(1, 6)
d_twenty = random.randint(1, 20)
try:
if ctx.author.id != 258104532423147520 and self.square and ctx.author.id in self.cube:
if d_six_one == 6 and (d_six_two + d_six_three == 7):
d_six_two = random.randint(1, 6)
d_six_three = random.randint(1, 6)
logger.info(f'flag')
elif self.cone is not None and ctx.author.id == 258104532423147520:
if len(self.cone) > 2:
num = int(self.cone[2])
if num > 20:
num = 20
d_twenty = random.randint(1, num)
if len(self.cone) > 1:
num = int(self.cone[1])
DICE_COMBINATIONS = {
2: [(1, 1)],
3: [(1, 2), (2, 1)],
4: [(1, 3), (2, 2), (3, 1)],
5: [(1, 4), (2, 3), (3, 2), (4, 1)],
6: [(1, 5), (2, 4), (3, 3), (4, 2), (5, 1)],
7: [(1, 6), (2, 5), (3, 4), (4, 3), (5, 2), (6, 1)],
8: [(2, 6), (3, 5), (4, 4), (5, 3), (6, 2)],
9: [(3, 6), (4, 5), (5, 4), (6, 3)],
10: [(4, 6), (5, 5), (6, 4)],
11: [(5, 6), (6, 5)],
12: [(6, 6)]
}
chosen = random.choice(DICE_COMBINATIONS[num])
d_six_two, d_six_three = chosen
num = int(self.cone[0])
if num > 6:
num = 6
elif num < 1:
num = 1
d_six_one = num
self.cone = None
except Exception as e:
d_six_one = random.randint(1, 6)
d_six_two = random.randint(1, 6)
d_six_three = random.randint(1, 6)
d_twenty = random.randint(1, 20)
roll_message = f'```md\n# {d_six_one},{d_six_two + d_six_three},'\
f'{d_twenty}\nDetails:[1d6;2d6;1d20 ({d_six_one} - {d_six_two} {d_six_three} - '\
f'{d_twenty})]```'

View File

@ -9,9 +9,19 @@ from discord import app_commands
from datetime import datetime, timedelta
from discord.ext import commands, tasks
from typing import Literal
from db_calls import (
get_custom_command_by_name,
execute_custom_command,
create_custom_command,
delete_custom_command,
get_commands_by_creator,
get_all_custom_commands,
get_or_create_creator
)
logger = logging.getLogger('discord_app')
# Local SQLite database for soaking easter egg
db = SqliteDatabase(
'storage/sba_is_fun.db',
pragmas={
@ -22,21 +32,10 @@ db = SqliteDatabase(
)
class Creator(Model):
name = CharField()
discordid = IntegerField()
class Meta:
database = db
class Command(Model):
name = CharField()
message = CharField()
creator = ForeignKeyField(Creator)
createtime = DateTimeField()
last_used = DateTimeField()
sent_warns = IntegerField(default=0)
class Soaks(Model):
user = IntegerField()
message_id = IntegerField()
timestamp = DateTimeField()
class Meta:
database = db
@ -50,30 +49,21 @@ class Roles(Model):
database = db
class Soaks(Model):
user = IntegerField()
message_id = IntegerField()
timestamp = DateTimeField()
class Meta:
database = db
class Fun(commands.Cog):
def __init__(self, bot):
self.bot = bot
db.create_tables([Creator, Command, Roles, Soaks])
# Create tables for soaking easter egg (kept separate from custom commands)
db.create_tables([Soaks, Roles])
db.close()
self.daily_check.start()
@tasks.loop(hours=20)
async def daily_check(self):
try:
# logger.info(f'trying to start cc check')
guild = self.bot.get_guild(int(os.environ.get('GUILD_ID')))
if not guild:
# logger.info(f'no guild found for cc check')
await asyncio.sleep(15)
guild = self.bot.get_guild(int(os.environ.get('GUILD_ID')))
if not guild:
@ -84,90 +74,116 @@ class Fun(commands.Cog):
return
if guild.id != 613880856032968834:
logger.info(f'Not checking CCs outside of SBa server')
logger.info(f'Not checking CCs outside of SBA server')
return
# <discordid> = {'member': <discord member>, 'commands': [(<command.name>, <command.message>)]}
del_notifs = {}
del_counter = 0
# <discordid> = {'member': <discord member>, 'commands': [(<command.name>, <command.message>)]}
warn_notifs = {}
now = datetime.now()
for x in Command.select():
# Final check / deleted
if x.last_used + timedelta(days=90) < now:
logger.warning(f'Deleting `!cc {x.name}`')
owner = guild.get_member(x.creator.discordid)
if owner:
if owner.id not in del_notifs:
del_notifs[owner.id] = {'member': owner, 'commands': [(x.name, x.message)]}
else:
del_notifs[owner.id]['commands'].append((x.name, x.message))
x.delete_instance()
del_counter += 1
# Get commands eligible for deletion (90+ days unused)
ninety_days_ago = (datetime.now() - timedelta(days=90)).isoformat()
sixty_days_ago = (datetime.now() - timedelta(days=60)).isoformat()
elif x.last_used + timedelta(days=60) < now and (x.sent_warns is None or x.sent_warns == 0):
logger.warning(f'Warning for `!cc {x.name}`')
x.sent_warns = 1
x.save()
owner = guild.get_member(x.creator.discordid)
if owner:
if owner.id not in warn_notifs:
warn_notifs[owner.id] = {'member': owner, 'commands': [(x.name, x.message)]}
else:
warn_notifs[owner.id]['commands'].append((x.name, x.message))
try:
# Get all commands to check for cleanup
all_commands_resp = await get_all_custom_commands(page=1, page_size=1000)
if not all_commands_resp:
logger.info('No custom commands found for cleanup check')
return
# else:
# logger.warning(
# f'Command <!cc {x.name}> last used {x.last_used} / delta: {now - x.last_used} \n/>60 days: '
# f'{x.last_used + timedelta(days=60) < now} / sent_warns: {x.sent_warns}'
# )
all_commands = all_commands_resp.get('custom_commands', [])
db.close()
logger.info(f'deletions: {del_notifs}\nwarnings: {warn_notifs}')
# {discord_id: {'member': <discord member>, 'commands': [(name, content)]}}
del_notifs = {}
del_counter = 0
warn_notifs = {}
now = datetime.now()
for member in del_notifs:
plural = len(del_notifs[member]["commands"]) > 1
msg_content = f'Yo, it\'s cleanup time. I am deleting the following custom ' \
f'command{"s" if plural else ""}:\n\n'
short_msg_content = copy.deepcopy(msg_content)
for x in del_notifs[member]["commands"]:
msg_content += f'`!cc {x[0]}` - {x[1]}\n'
short_msg_content += f'`!cc {x[0]}`\n'
try:
await del_notifs[member]['member'].send(msg_content)
except Exception as e:
logger.error(f'fun daily_check - could not send deletion message to {del_notifs[member]["member"]} '
f'/ trying short_msg')
for cmd in all_commands:
# Parse last_used datetime
try:
await del_notifs[member]['member'].send(short_msg_content)
except Exception as e:
logger.error(f'fun daily_check - still could not send deletion message')
for member in warn_notifs:
plural = len(warn_notifs[member]["commands"]) > 1
msg_content = f'Heads up, the following custom ' \
f'command{"s" if plural else ""} will be deleted next month if ' \
f'{"they are" if plural else "it is"} not used:\n\n'
short_msg_content = copy.deepcopy(msg_content)
for x in warn_notifs[member]["commands"]:
msg_content += f'`!cc {x[0]}` - {x[1]}\n'
short_msg_content += f'`!cc {x[0]}`\n'
try:
await warn_notifs[member]['member'].send(msg_content)
except Exception as e:
logger.error(f'fun daily_check - could not send warn message to {warn_notifs[member]["member"]} '
f'/ trying short_msg')
try:
await warn_notifs[member]['member'].send(short_msg_content)
except Exception as e:
logger.error(f'fun daily_check - still could not send warn message')
last_used = datetime.fromisoformat(cmd['last_used']) if cmd.get('last_used') else now
except:
last_used = now
# Final check / deleted (90+ days)
if last_used + timedelta(days=90) < now:
logger.warning(f'Deleting `!cc {cmd["name"]}`')
creator_discord_id = cmd['creator']['discord_id'] if cmd.get('creator') else None
if creator_discord_id:
owner = guild.get_member(creator_discord_id)
if owner:
if owner.id not in del_notifs:
del_notifs[owner.id] = {'member': owner, 'commands': [(cmd['name'], cmd['content'])]}
else:
del_notifs[owner.id]['commands'].append((cmd['name'], cmd['content']))
await update_custom_command(cmd['id'], {'active': False})
del_counter += 1
# Warning (60+ days, not warned yet)
elif last_used + timedelta(days=60) < now and not cmd.get('warning_sent', False):
logger.warning(f'Warning for `!cc {cmd["name"]}`')
creator_discord_id = cmd['creator']['discord_id'] if cmd.get('creator') else None
if creator_discord_id:
owner = guild.get_member(creator_discord_id)
if owner:
if owner.id not in warn_notifs:
warn_notifs[owner.id] = {'member': owner, 'commands': [(cmd['name'], cmd['content'])]}
else:
warn_notifs[owner.id]['commands'].append((cmd['name'], cmd['content']))
# Mark warning as sent
from db_calls import update_custom_command
await update_custom_command(cmd['id'], {'warning_sent': True})
logger.info(f'deletions: {del_notifs}\nwarnings: {warn_notifs}')
# Send deletion notifications
for member_id in del_notifs:
plural = len(del_notifs[member_id]["commands"]) > 1
msg_content = f'Yo, it\'s cleanup time. I am deleting the following custom ' \
f'command{"s" if plural else ""}:\n\n'
short_msg_content = copy.deepcopy(msg_content)
for x in del_notifs[member_id]["commands"]:
msg_content += f'`!cc {x[0]}` - {x[1]}\n'
short_msg_content += f'`!cc {x[0]}`\n'
try:
await del_notifs[member_id]['member'].send(msg_content)
except Exception as e:
logger.error(f'fun daily_check - could not send deletion message to {del_notifs[member_id]["member"]} '
f'/ trying short_msg')
try:
await del_notifs[member_id]['member'].send(short_msg_content)
except Exception as e:
logger.error(f'fun daily_check - still could not send deletion message')
# Send warning notifications
for member_id in warn_notifs:
plural = len(warn_notifs[member_id]["commands"]) > 1
msg_content = f'Heads up, the following custom ' \
f'command{"s" if plural else ""} will be deleted next month if ' \
f'{"they are" if plural else "it is"} not used:\n\n'
short_msg_content = copy.deepcopy(msg_content)
for x in warn_notifs[member_id]["commands"]:
msg_content += f'`!cc {x[0]}` - {x[1]}\n'
short_msg_content += f'`!cc {x[0]}`\n'
try:
await warn_notifs[member_id]['member'].send(msg_content)
except Exception as e:
logger.error(f'fun daily_check - could not send warn message to {warn_notifs[member_id]["member"]} '
f'/ trying short_msg')
try:
await warn_notifs[member_id]['member'].send(short_msg_content)
except Exception as e:
logger.error(f'fun daily_check - still could not send warn message')
logger.info(f'Deleted {del_counter} commands; sent deletion notifications to {len(del_notifs)} users; '
f'sent warnings to {len(warn_notifs)} users')
except Exception as e:
logger.error(f'Error during daily_check: {e}', exc_info=True)
logger.info(f'Deleted {del_counter} commands; sent deletion notifications to {len(del_notifs)} users; '
f'sent warnings to {len(warn_notifs)} users')
async def cog_command_error(self, ctx, error):
logger.error(msg=error, stack_info=True, exc_info=True)
await ctx.send(f'{error}\n\nRun !help <command_name> to see the command requirements')
@ -175,7 +191,7 @@ class Fun(commands.Cog):
async def slash_error(self, ctx, error):
logger.error(msg=error, stack_info=True, exc_info=True)
await ctx.send(f'{error[:1600]}')
@commands.Cog.listener(name='on_message')
async def on_message_listener(self, message):
if message.author.bot or message.channel.guild.id != int(os.environ.get('GUILD_ID')) \
@ -194,35 +210,292 @@ class Fun(commands.Cog):
).execute()
db.close()
time_since = datetime.now() - last_soak.timestamp
# logger.info(f'time_since: {time_since} / seconds: {time_since.seconds} / days: {time_since.days}')
gif_search = None
if time_since.days >= 2:
ts_string = f'{time_since.days} days'
if time_since.days > 30:
gif_search = 'elite'
elif time_since.days > 14:
gif_search = 'pretty good'
else:
if time_since.seconds >= 7200:
ts_string = f'{time_since.seconds // 3600} hours'
gif_search = 'whats wrong with you'
if last_soak:
time_since = datetime.now() - last_soak.timestamp
# logger.info(f'time_since: {time_since} / seconds: {time_since.seconds} / days: {time_since.days}')
gif_search = None
if time_since.days >= 2:
ts_string = f'{time_since.days} days'
if time_since.days > 30:
gif_search = 'elite'
elif time_since.days > 14:
gif_search = 'pretty good'
else:
if time_since.seconds >= 120:
ts_string = f'{time_since.seconds // 60} minutes'
if time_since.seconds >= 7200:
ts_string = f'{time_since.seconds // 3600} hours'
gif_search = 'whats wrong with you'
else:
ts_string = f'{time_since.seconds} seconds'
gif_search = 'pathetic'
if time_since.seconds >= 120:
ts_string = f'{time_since.seconds // 60} minutes'
else:
ts_string = f'{time_since.seconds} seconds'
gif_search = 'pathetic'
await message.channel.send(
f'It has been {ts_string} since soaking was mentioned.'
await message.channel.send(
f'It has been {ts_string} since soaking was mentioned.'
)
if gif_search is not None:
try:
await message.channel.send(random_gif(gif_search))
except Exception as e:
logger.error(e)
@commands.command(name='cc', help='Run custom command')
async def custom_command(self, ctx, command):
try:
# Execute the command (updates usage stats automatically)
result = await execute_custom_command(command.lower())
if not result:
# Kermit lost gif
await ctx.send('https://tenor.com/6saQ.gif')
return
# Special easter egg for prettyrainbow command
if result['name'] == 'prettyrainbow' and ctx.author.id == 291738770313707521:
await ctx.send(random_no_phrase())
return
await ctx.send(result['content'])
except Exception as e:
logger.error(f'Error executing custom command {command}: {e}')
# Kermit lost gif
await ctx.send('https://tenor.com/6saQ.gif')
@commands.command(name='about', help='Who made the custom command')
async def about_command(self, ctx, command):
try:
result = await get_custom_command_by_name(command.lower())
if not result:
await ctx.send('https://tenor.com/blQnd.gif')
return
creator_name = result['creator']['username'] if result.get('creator') else 'Unknown'
created_at = result.get('created_at', 'Unknown')
embed = discord.Embed(title=f'About {result["name"].title()}', color=0xFFFF00)
embed.add_field(name=f'Creator', value=creator_name, inline=False)
embed.add_field(name='Creation Date', value=created_at, inline=False)
embed.add_field(name='Message', value=result['content'], inline=False)
await ctx.send(content=None, embed=embed)
except Exception as e:
logger.error(f'Error getting command info: {e}')
await ctx.send('https://tenor.com/blQnd.gif')
@commands.command(name='newcc', help='Create a new custom command')
@commands.has_any_role(SBA_PLAYERS_ROLE_NAME, 'Paper Dynasty Players')
async def new_custom_command(self, ctx, name, *, message):
try:
command_name = name.lower().strip()
command_content = message.strip()
# Check if command already exists
existing = await get_custom_command_by_name(command_name)
if existing:
await ctx.send('There is already a command with that name!')
return
# Show preview
embed = discord.Embed(title='Is this what you want?', color=0x91329F)
embed.add_field(name='Command Name', value=command_name, inline=False)
embed.add_field(name='Message', value=command_content, inline=False)
await ctx.send(content=None, embed=embed)
view = Confirm(responders=[ctx.author])
question = await ctx.send('Should I create this for you?', view=view)
await view.wait()
if not view.value:
await question.edit(content='You keep thinking on it.', view=None)
return
# Get or create creator
creator = await get_or_create_creator(
discord_id=ctx.author.id,
username=ctx.author.name,
display_name=ctx.author.display_name
)
if gif_search is not None:
try:
await message.channel.send(random_gif(gif_search))
except Exception as e:
logger.error(e)
# Create command
command_data = {
'name': command_name,
'content': command_content,
'creator_id': creator['id']
}
result = await create_custom_command(command_data)
if result:
await question.edit(content=f'`!cc {command_name}` is now a thing!', view=None)
else:
await question.edit(content='Hmm...I couldn\'t add that. I might need a grown up to help.', view=None)
except Exception as e:
logger.error(f'Error creating custom command: {e}', exc_info=True)
await ctx.send('Something went wrong creating that command. Try again or ask an admin for help.')
@commands.command(name='delcc', help='Delete a custom command')
@commands.has_any_role(SBA_PLAYERS_ROLE_NAME, 'Paper Dynasty Players')
async def delete_custom_command_cmd(self, ctx, name):
try:
command_name = name.lower().strip()
this_command = await get_custom_command_by_name(command_name)
if not this_command:
await ctx.send('I couldn\'t find that command, sorry.')
return
# Check ownership
creator_discord_id = this_command['creator']['discord_id'] if this_command.get('creator') else None
if creator_discord_id != ctx.author.id and ctx.author.id != self.bot.owner_id:
await ctx.send('Looks like this isn\'t your command to delete.')
return
embed = discord.Embed(title='Do you want to delete this command?', color=0x91329F)
embed.add_field(name='Command Name', value=this_command['name'], inline=False)
embed.add_field(name='Message', value=this_command['content'], inline=False)
view = Confirm(responders=[ctx.author])
question = await ctx.send(content=None, embed=embed, view=view)
await view.wait()
if not view.value:
await question.edit(content='It stays for now.', view=None)
return
result = await delete_custom_command(this_command['id'])
if result:
await question.edit(view=None)
await ctx.send('He gone!')
else:
await ctx.send('Welp. That didn\'t work. Go complain to an adult, I guess.')
except Exception as e:
logger.error(f'Error deleting custom command: {e}', exc_info=True)
await ctx.send('Something went wrong deleting that command.')
@commands.command(name='allcc', help='Show all custom commands')
async def show_custom_commands(self, ctx, page=1):
try:
def get_embed(this_page, result_data):
this_embed = discord.Embed(title=f'All Custom Commands', color=0x2F939F)
column_one = ''
column_two = ''
commands_list = result_data.get('custom_commands', [])
# First 20 commands in first column
for x in range(min(20, len(commands_list))):
try:
cmd = commands_list[x]
creator_name = cmd['creator']['username'] if cmd.get('creator') else 'Unknown'
column_one += f'**{cmd["name"]}** by {creator_name}\n'
except Exception as e:
logger.error(f'Error building !allcc embed: {e}')
break
if column_one:
this_embed.add_field(
name=f'{(this_page - 1) * 40 + 1}-{min(this_page * 40 - 20, result_data["total_count"])}',
value=column_one
)
# Next 20 commands in second column
for x in range(20, min(40, len(commands_list))):
try:
cmd = commands_list[x]
creator_name = cmd['creator']['username'] if cmd.get('creator') else 'Unknown'
column_two += f'**{cmd["name"]}** by {creator_name}\n'
except Exception as e:
logger.error(f'Error building !allcc embed: {e}')
break
if column_two:
this_embed.add_field(
name=f'{(this_page - 1) * 40 + 21}-{min(this_page * 40, result_data["total_count"])}',
value=column_two
)
return this_embed
page_num = page
result = await get_all_custom_commands(page=page_num, page_size=40)
if not result:
await ctx.send('No custom commands found!')
return
total_count = result.get('total_count', 0)
last_page = result.get('total_pages', 1)
if page_num > last_page:
await ctx.send(f'The max page number is {last_page}; going there now!')
page_num = last_page
result = await get_all_custom_commands(page=page_num, page_size=40)
embed = get_embed(page_num, result)
embed.description = f'Page {page_num} / {last_page}'
view = Pagination(responders=[ctx.author])
resp_message = await ctx.send(content=None, embed=embed, view=view)
while True:
await view.wait()
if view.value:
logger.info(f'got a value: {view.value}')
if view.value == 'left':
page_num = page_num - 1 if page_num > 1 else last_page
elif view.value == 'right':
page_num = page_num + 1 if page_num < last_page else 1
elif view.value == 'cancel':
await resp_message.edit(content=None, embed=embed, view=None)
break
view.value = None
else:
await resp_message.edit(content=None, embed=embed, view=None)
break
# Get new page data
result = await get_all_custom_commands(page=page_num, page_size=40)
embed = get_embed(page_num, result)
embed.description = f'Page {page_num} / {last_page}'
view = Pagination(responders=[ctx.author])
await resp_message.edit(content=None, embed=embed, view=view)
except Exception as e:
logger.error(f'Error showing all custom commands: {e}', exc_info=True)
await ctx.send('Something went wrong fetching the command list.')
@commands.command(name='mycc', aliases=['showcc'], help='Show my commands')
@commands.has_any_role(SBA_PLAYERS_ROLE_NAME, 'Paper Dynasty Players')
async def my_custom_commands(self, ctx):
try:
result = await get_commands_by_creator(discord_id=ctx.author.id, page=1, page_size=100)
if not result or result.get('total_count', 0) == 0:
await ctx.send('It doesn\'t look like you\'ve created any custom commands. Try it out by running '
'!help newcc for the command syntax!')
return
commands_list = result.get('custom_commands', [])
comm_message = ''
for cmd in commands_list:
comm_message += f'{cmd["name"]}\n'
embed = discord.Embed(title=f'{ctx.author.name}\'s Commands', color=0x2F939F)
embed.add_field(name=f'Command Names', value=comm_message if comm_message else 'None', inline=False)
await ctx.send(content=None, embed=embed)
except Exception as e:
logger.error(f'Error showing user commands: {e}', exc_info=True)
await ctx.send('Something went wrong fetching your commands.')
@commands.command(name='lastsoak', aliases=['ls'], help='Get a link to the last mention of soaking')
async def last_soak_command(self, ctx):
@ -231,209 +504,11 @@ class Fun(commands.Cog):
last_soak = squery[0]
else:
await ctx.send(f'I could not find the last mention of soaking.')
db.close()
return
message = await ctx.fetch_message(last_soak.message_id)
await ctx.send(f'The last mention of soaking was: {message.jump_url}')
@commands.command(name='cc', help='Run custom custom command')
async def custom_command(self, ctx, command):
chosen = Command.get_or_none(fn.Lower(Command.name) == command.lower())
if not chosen:
# Error gif
# await ctx.send('https://tenor.com/blQnd.gif')
# Schitt's Creek 'what's that' gif
# await ctx.send('https://media.giphy.com/media/l0HUhFZx6q0hsPtHq/giphy.gif')
# Kermit lost gif
await ctx.send('https://tenor.com/6saQ.gif')
else:
if chosen.name == 'prettyrainbow' and ctx.author.id == 291738770313707521:
await ctx.send(random_no_phrase())
return
await ctx.send(chosen.message)
chosen.last_used = datetime.now()
chosen.sent_warns = 0
chosen.save()
db.close()
@commands.command(name='about', help='Who made the custom command')
async def about_command(self, ctx, command):
chosen = Command.get_or_none(fn.Lower(Command.name) == command.lower())
if not chosen:
await ctx.send('https://tenor.com/blQnd.gif')
embed = discord.Embed(title=f'About {chosen.name.title()}', color=0xFFFF00)
embed.add_field(name=f'Creator', value=f'{chosen.creator.name}', inline=False)
embed.add_field(name='Creation Date', value=f'{chosen.createtime}', inline=False)
embed.add_field(name='Message', value=f'{chosen.message}', inline=False)
await ctx.send(content=None, embed=embed)
db.close()
@commands.command(name='newcc', help='Create a new custom command')
@commands.has_any_role(SBA_PLAYERS_ROLE_NAME, 'Paper Dynasty Players')
async def new_custom_command(self, ctx, name, *, message):
time = datetime.now()
command = name
comm_message = message
chosen = Command.get_or_none(fn.Lower(Command.name) == command.lower())
if chosen:
await ctx.send('There is already a command with that name!')
return
embed = discord.Embed(title='Is this what you want?', color=0x91329F)
embed.add_field(name='Command Name', value=command, inline=False)
embed.add_field(name='Message', value=comm_message, inline=False)
await ctx.send(content=None, embed=embed)
view = Confirm(responders=[ctx.author])
question = await ctx.send('Should I create this for you?', view=view)
await view.wait()
if not view.value:
await question.edit(content='You keep thinking on it.', view=None)
return
this_person = Creator.get_or_none(Creator.discordid == ctx.author.id)
if not this_person:
this_person = Creator(name=f'{ctx.author.name}', discordid=f'{ctx.author.id}')
this_person.save()
this_command = Command(name=command, message=comm_message, createtime=time, creator=this_person, last_used=time)
if this_command.save() == 1:
await question.edit(content=f'`!cc {this_command.name}` is now a thing!', view=None)
else:
await question.edit(content='Hmm...I couldn\'t add that. I might need a grown up to help.', view=None)
db.close()
@commands.command(name='delcc', help='Delete a custom command')
@commands.has_any_role(SBA_PLAYERS_ROLE_NAME, 'Paper Dynasty Players')
async def delete_custom_command(self, ctx, name):
this_command = Command.get_or_none(fn.Lower(Command.name) == name.lower())
if not this_command:
await ctx.send('I couldn\'t find that command, sorry.')
return
if this_command.creator.discordid != ctx.author.id and ctx.author.id != self.bot.owner_id:
await ctx.send('Looks like this isn\'t your command to delete.')
return
embed = discord.Embed(title='Do you want to delete this command?', color=0x91329F)
embed.add_field(name='Command Name', value=this_command.name, inline=False)
embed.add_field(name='Message', value=this_command.message, inline=False)
view = Confirm(responders=[ctx.author])
question = await ctx.send(content=None, embed=embed, view=view)
await view.wait()
if not view.value:
await question.edit(content='It stays for now.', view=None)
return
if this_command.delete_instance() == 1:
await question.edit(view=None)
await ctx.send('He gone!')
else:
await ctx.send('Welp. That didn\'t work. Go complain to an adult, I guess.')
db.close()
@commands.command(name='allcc', help='Show all custom commands')
async def show_custom_commands(self, ctx, page=1):
def get_embed(this_page):
this_embed = discord.Embed(title=f'All Custom Commands', color=0x2F939F)
column_one = ''
column_two = ''
all_commands = Command.select().paginate(this_page, 40).order_by(Command.name)
for x in range(20):
try:
column_one += f'**{all_commands[x].name}** by {all_commands[x].creator.name}\n'
except Exception as e:
logger.error(f'Error building !allcc embed: {e}')
break
this_embed.add_field(name=f'{(this_page - 1) * 40 + 1}-{this_page * 40 - 20}', value=column_one)
for x in range(20, 40):
try:
column_two += f'**{all_commands[x].name}** by {all_commands[x].creator.name}\n'
except Exception as e:
logger.error(f'Error building !allcc embed: {e}')
break
if len(column_two) > 0:
this_embed.add_field(name=f'{(this_page - 1) * 40 + 21}-{this_page * 40}', value=column_two)
return this_embed
page_num = page
total_commands = Command.select(Command.id)
last_page = math.ceil(total_commands.count()/40)
if page_num > last_page:
await ctx.send(f'The max page number is {last_page}; going there now!')
page_num = last_page
embed = get_embed(page_num)
embed.description = f'Page {page_num} / {last_page}'
view = Pagination(responders=[ctx.author])
resp_message = await ctx.send(content=None, embed=embed, view=view)
while True:
await view.wait()
if view.value:
logger.info(f'got a value: {view.value}')
if view.value == 'left':
page_num = page_num - 1 if page_num > 1 else last_page
elif view.value == 'right':
page_num = page_num + 1 if page_num <= last_page else 1
elif view.value == 'cancel':
await resp_message.edit(content=None, embed=embed, view=None)
break
view.value = None
else:
await resp_message.edit(content=None, embed=embed, view=None)
break
# await resp_message.edit(content=None, embed=embed, view=None)
embed = get_embed(page_num)
embed.description = f'Page {page_num} / {last_page}'
view = Pagination(responders=[ctx.author])
await resp_message.edit(content=None, embed=embed, view=view)
db.close()
@commands.command(name='mycc', aliases=['showcc'], help='Show my commands')
@commands.has_any_role(SBA_PLAYERS_ROLE_NAME, 'Paper Dynasty Players')
async def my_custom_commands(self, ctx):
this_creator = Creator.get_or_none(Creator.discordid == ctx.author.id)
if not this_creator:
await ctx.send('It doesn\'t look like you\'ve created any custom commands. Try it out by running the '
'!help newcc for the command syntax!')
return
all_commands = Command.select().join(Creator).where(Command.creator == this_creator)
if all_commands.count() == 0:
await ctx.send('It doesn\'t look like you\'ve created any custom commands. Try it out by running the '
'!help newcc for the command syntax!')
return
comm_message = ''
for x in all_commands:
comm_message += f'{x.name}\n'
embed = discord.Embed(title=f'{ctx.author.name}\'s Commands', color=0x2F939F)
embed.add_field(name=f'Command Names', value=comm_message, inline=False)
await ctx.send(content=None, embed=embed)
db.close()
@app_commands.command(name='woulditdong', description='Log a dinger to see would it dong across SBa')
@ -475,98 +550,6 @@ class Fun(commands.Cog):
await send_to_channel(self.bot, 'news-ticker', content=None, embed=embed)
await interaction.edit_original_response(content=None, embed=embed)
# @commands.command(name='showcc', help='Show one person\'s custom commands')
# @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, 'Paper Dynasty Players')
# async def show_cc_command(self, ctx, ):
# @commands.command(name='role', help='Toggle role')
# async def toggle_role_command(self, ctx, *, role_name):
# all_roles = [x.name for x in Roles.select().where(Roles.enabled)]
#
# async def toggle_role(full_role):
# if full_role in ctx.author.roles:
# await ctx.author.remove_roles(full_role)
# else:
# await ctx.author.add_roles(full_role)
#
# if len(role_name) < 4:
# await ctx.send('https://thumbs.gfycat.com/FrayedUnequaledGnat-size_restricted.gif')
# await ctx.send(f'What even is **{role_name}**...')
# db.close()
# return
#
# for name in all_roles:
# if role_name.lower() in name.lower():
# try:
# this_role = discord.utils.get(ctx.guild.roles, name=name)
# await toggle_role(this_role)
# await ctx.send(random_conf_gif())
# return
# except:
# await ctx.send(await get_emoji(ctx, 'fforrespect', False))
# await ctx.send('I was not able to assign that role.')
# return
#
# await ctx.send(f'That doesn\'t sound familiar. **{role_name}**...did you make that shit up?')
# @commands.command(name='showroles', help='Show toggleable roles')
# async def show_roles_command(self, ctx):
# all_roles = [x.name for x in Roles.select().where(Roles.enabled)]
# role_string = '\n- '.join(all_roles)
#
# embed = get_team_embed('Toggleable Roles', thumbnail=False)
# embed.description = 'Run !role <role_name> to toggle the role on or off'
# embed.add_field(name='Role Names', value=f'- {role_string}')
#
# await ctx.send(content=None, embed=embed)
# @commands.command(name='newrole', aliases=['removerole'], help='Make toggleable role')
# @commands.is_owner()
# async def make_toggleable_role_command(self, ctx, *, role_name):
# this_role = Roles.get_or_none(Roles.name == role_name)
#
# if not this_role:
# # Create the role if it doesn't exist
#
# this_role = Roles(name=role_name)
# this_role.save()
# if not discord.utils.get(ctx.guild.roles, name=this_role.name):
# await ctx.guild.create_role(name=f'{role_name}', mentionable=True)
# else:
# # Disable the role
#
# if this_role.enabled:
# this_role.enabled = False
# else:
# this_role.enabled = True
# this_role.save()
# this_role = discord.utils.get(ctx.guild.roles, name=this_role.name)
#
# if this_role:
# await this_role.edit(mentionable=False)
# else:
# await ctx.send('That role doesn\'t exist in the server.')
#
# await ctx.send(random_conf_gif())
# @commands.command(name='bulkrole', hidden=True)
# @commands.is_owner()
# async def bulkrole_command(self, ctx, *roles):
# all_roles = []
#
# for x in roles:
# all_roles.append(discord.utils.get(ctx.guild.roles, name=x))
#
# await ctx.send('On it. This could take a bit.')
# time_start = datetime.now()
#
# async for member in ctx.guild.fetch_members():
# logger.warning(f'member: {member}')
# await member.add_roles(*all_roles)
#
# time_end = datetime.now()
# await ctx.send(f'All done! That took {time_end - time_start}')
async def setup(bot):
await bot.add_cog(Fun(bot))

View File

@ -236,6 +236,74 @@ async def get_player_headshot(player_name):
return await get_player_photo(player_name)
###
# CUSTOM COMMANDS API FUNCTIONS
###
async def get_custom_command_by_name(name: str):
"""Get a custom command by name."""
return await db_get(f'custom_commands/by_name/{name}', none_okay=True)
async def execute_custom_command(name: str):
"""Execute a custom command and update usage statistics."""
return await db_patch(f'custom_commands/by_name/{name}/execute', object_id=None, params=[])
async def create_custom_command(command_data: dict):
"""Create a new custom command."""
return await db_post('custom_commands', payload=command_data)
async def update_custom_command(command_id: int, update_data: dict):
"""Update an existing custom command."""
return await db_put('custom_commands', object_id=command_id, payload=update_data)
async def delete_custom_command(command_id: int):
"""Delete a custom command."""
return await db_delete('custom_commands', object_id=command_id)
async def get_commands_by_creator(discord_id: int, page: int = 1, page_size: int = 25):
"""Get all commands created by a specific Discord user."""
params = [
('creator_discord_id', discord_id),
('is_active', True),
('page', page),
('page_size', page_size)
]
return await db_get('custom_commands', params=params, none_okay=True)
async def get_all_custom_commands(page: int = 1, page_size: int = 40, sort: str = 'name'):
"""Get all custom commands with pagination."""
params = [
('is_active', True),
('sort', sort),
('page', page),
('page_size', page_size)
]
return await db_get('custom_commands', params=params, none_okay=True)
async def get_or_create_creator(discord_id: int, username: str, display_name: Optional[str] = None):
"""Get existing creator or create a new one."""
# Try to get existing creator
existing = await db_get('custom_commands/creators', params=[('discord_id', discord_id)], none_okay=True)
if existing and existing.get('creators') and len(existing['creators']) > 0:
return existing['creators'][0]
# Create new creator
creator_data = {
'discord_id': discord_id,
'username': username,
'display_name': display_name
}
return await db_post('custom_commands/creators', payload=creator_data)
###
# TO BE DEPRECATED FUNCTIONS
###

BIN
sba_is_fun.db Normal file

Binary file not shown.