Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

type more modules #134

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,21 @@ build-backend = "poetry.core.masonry.api"
[tool.pyright]
strict = ["**"]
exclude = [
# large and/or annoying multiclass files
"**/advent.py",
"**/bot.py",
"**/error_handler.py",
"**/events.py",
"**/gaming.py",
"**/haiku.py",
"**/member_counter.py",
"**/remindme.py",
"**/snailrace.py",
"**/starboard.py",
"**/uptime.py",
"**/whatsdue.py",
"**/working_on.py",

# waiting for external stubs
"**/bot.py",

# only used by another blocked file
"**/error_handler.py",
"**/utils/command_utils.py",
"**/utils/snailrace_utils.py",
"**/utils/uq_course_utils.py"
"**/utils/uq_course_utils.py",

# isaac's job
"**/haiku.py"
]
10 changes: 4 additions & 6 deletions uqcsbot/advent.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,6 @@ def sort_key(sort: SortMode) -> Callable[["Member"], Any]:
class Advent(commands.Cog):
CHANNEL_NAME = "contests"

# Session cookie (will expire in approx 30 days).
# See: https://github.com/UQComputingSociety/uqcsbot-discord/wiki/Tokens-and-Environment-Variables#aoc_session_id
SESSION_ID: str = ""

def __init__(self, bot: UQCSBot):
self.bot = bot
self.bot.schedule_task(
Expand All @@ -183,8 +179,10 @@ def __init__(self, bot: UQCSBot):
month=12,
)

if os.environ.get("AOC_SESSION_ID") is not None:
SESSION_ID = os.environ.get("AOC_SESSION_ID")
# Session cookie (will expire in approx 30 days).
# See: https://github.com/UQComputingSociety/uqcsbot-discord/wiki/Tokens-and-Environment-Variables#aoc_session_id
if (session := os.environ.get("AOC_SESSION_ID")) is not None:
self.SESSION_ID = session
else:
raise FatalErrorWithLog(
bot, "Unable to find AoC session ID. Not loading advent cog."
Expand Down
4 changes: 3 additions & 1 deletion uqcsbot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

"""
TODO: TYPE ISSUES IN THIS FILE:
- apscheduler has no stubs. They're planned for the 4.0 release... in the future.
- apscheduler has no stubs. They're planned for the 4.0 release... sometime.
- aiohttp handler witchery
"""

Expand All @@ -29,6 +29,8 @@ def __init__(self, *args: Any, **kwargs: Any):
# Important channel names & constants go here
self.ADMIN_ALERTS_CNAME = "admin-alerts"
self.GENERAL_CNAME = "general"
self.BOT_CNAME = "bot-testing"
self.STARBOARD_CNAME = "starboard"
self.BOT_TIMEZONE = timezone("Australia/Brisbane")

self.uqcs_server: discord.Guild
Expand Down
87 changes: 60 additions & 27 deletions uqcsbot/gaming.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from difflib import SequenceMatcher
from json import loads
from typing import Optional, Dict, Any
from typing import Optional, Dict, Any, Set, TypedDict, Union
from urllib.error import HTTPError
from urllib.request import urlopen
from xml.etree.ElementTree import fromstring
Expand All @@ -13,6 +13,23 @@
from uqcsbot.bot import UQCSBot


class Parameters(TypedDict):
categories: Set[str]
mechanics: Set[str]
subranks: Dict[Any, Any]
identity: str
min_players: int
max_players: int
name: str
score: Union[str, None]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would Optional[str] be more appropriate? Same for the other parameters?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes but i'm braindead. will fix soon:tm:

users: Union[str, None]
rank: str
description: Union[str, None]
image: Union[str, None]
min_time: str
max_time: str


class Gaming(commands.Cog):
"""
Various gaming related commands
Expand All @@ -36,18 +53,21 @@ def get_bgg_id(self, search_name: str) -> Optional[str]:
return None

# filters for the closest name match
match = {}
match: Dict[str, float] = {}
for item in results:
if item.get("id") is None:
item_id = item.get("id")
if item_id is None:
continue
for element in item:
if element.tag == "name":
match[item.get("id")] = SequenceMatcher(
None, search_name, element.get("value")
match[item_id] = SequenceMatcher(
None,
search_name,
(x if (x := element.get("value")) is not None else ""),
).ratio()
return max(match, key=match.get)
return max(match, key=lambda x: match.get(x, 0))

def get_board_game_parameters(self, identity: str) -> Optional[Dict[str, str]]:
def get_board_game_parameters(self, identity: str) -> Optional[Parameters]:
"""
returns the various parameters of a board game from bgg
"""
Expand All @@ -57,28 +77,39 @@ def get_board_game_parameters(self, identity: str) -> Optional[Dict[str, str]]:
if query.status_code != 200:
return None
result = fromstring(query.text)[0]
parameters: Dict[str, Any] = {}
parameters["categories"] = set()
parameters["mechanics"] = set()
parameters["subranks"] = {}
parameters["identity"] = identity
parameters: Parameters = {
49Indium marked this conversation as resolved.
Show resolved Hide resolved
"categories": set(),
"mechanics": set(),
"subranks": {},
"identity": identity,
"min_players": -1,
"max_players": -1,
"name": "",
"score": None,
"users": None,
"rank": "",
"description": None,
"image": None,
"min_time": "",
"max_time": "",
}

for element in result:
tag = element.tag
tag_name = element.attrib.get("name")
tag_value = element.attrib.get("value")
tag_value = element.attrib.get("value", "")
tag_type = element.attrib.get("type")
tag_text = element.text

# sets the range of players
if tag == "poll" and tag_name == "suggested_numplayers":
players = set()
players: Set[int] = set()
for option in element:
numplayers = option.attrib.get("numplayers")
numplayers = option.attrib.get("numplayers", "0")
votes = 0

for result in option:
numvotes = int(result.attrib.get("numvotes"))
numvotes = int(result.attrib.get("numvotes", "0"))
direction = (
-1 if result.attrib.get("value") == "Not Recommended" else 1
)
Expand All @@ -95,12 +126,12 @@ def get_board_game_parameters(self, identity: str) -> Optional[Dict[str, str]]:
parameters["max_players"] = max(players)

# sets the backup min players
if tag == "minplayers":
parameters.setdefault("min_players", int(tag_value))
if tag == "minplayers" and parameters["min_players"] == -1:
parameters["min_players"] = int(tag_value)

# sets the backup max players
if tag == "maxplayers":
parameters.setdefault("max_players", int(tag_value))
if tag == "maxplayers" and parameters["max_players"] == -1:
parameters["max_players"] = int(tag_value)

# sets the name of the board game
elif tag == "name" and tag_type == "primary":
Expand All @@ -118,7 +149,7 @@ def get_board_game_parameters(self, identity: str) -> Optional[Dict[str, str]]:
elif tag == "statistics":
for statistic in element[0]:
stat_tag = statistic.tag
stat_value = statistic.attrib.get("value")
stat_value = statistic.attrib.get("value", "")
if stat_tag == "average":
try:
parameters["score"] = str(round(float(stat_value), 2))
Expand All @@ -129,7 +160,7 @@ def get_board_game_parameters(self, identity: str) -> Optional[Dict[str, str]]:
if stat_tag == "ranks":
for genre in statistic:
genre_name = genre.attrib.get("name")
genre_value = genre.attrib.get("value")
genre_value = genre.attrib.get("value", "")
if genre_name == "boardgame" and genre_value.isnumeric():
position = int(genre_value)
# gets the ordinal suffix
Expand All @@ -141,7 +172,7 @@ def get_board_game_parameters(self, identity: str) -> Optional[Dict[str, str]]:
]
parameters["rank"] = f"{position:d}{suffix:s}"
elif genre_value.isnumeric():
friendlyname = genre.attrib.get("friendlyname")
friendlyname = genre.attrib.get("friendlyname", "")
# removes "game" as last word
friendlyname = " ".join(friendlyname.split(" ")[:-1])
position = int(genre_value)
Expand Down Expand Up @@ -170,7 +201,7 @@ def get_board_game_parameters(self, identity: str) -> Optional[Dict[str, str]]:

return parameters

def format_board_game_parameters(self, parameters: Dict[str, str]) -> discord.Embed:
def format_board_game_parameters(self, parameters: Parameters) -> discord.Embed:
embed = discord.Embed(title=parameters.get("name", ":question:"))
embed.add_field(
name="Summary",
Expand Down Expand Up @@ -198,12 +229,14 @@ def format_board_game_parameters(self, parameters: Dict[str, str]) -> discord.Em
f"• Ranked {value:s} in the {key:s} genre.\n"
for key, value in parameters.get("subranks", {}).items()
)
+ f"Categories: {', '.join(parameters.get('categories', set())):s}\n"
f"Mechanics: {', '.join(parameters.get('mechanics', set())):s}\n"
+ f"Categories: {', '.join(parameters['categories']):s}\n"
+ f"Mechanics: {', '.join(parameters['mechanics']):s}\n"
),
)
max_message_length = 1000
description = parameters.get("description", ":question:")
description: str = (
x if (x := parameters["description"]) is not None else ":question:"
)
if len(description) > max_message_length:
description = description[:max_message_length] + "\u2026"
embed.add_field(name="Description", inline=False, value=description)
Expand Down
27 changes: 22 additions & 5 deletions uqcsbot/member_counter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,27 @@
from discord import app_commands
from discord.ext import commands

from uqcsbot.bot import UQCSBot
from typing import List


class MemberCounter(commands.Cog):
MEMBER_COUNT_PREFIX = "Member Count: "
RATE_LIMIT = timedelta(minutes=5)
NEW_MEMBER_TIME = timedelta(days=7)

def __init__(self, bot: commands.Bot):
def __init__(self, bot: UQCSBot):
self.bot = bot
self.last_rename_time = datetime.now()
self.waiting_for_rename = False

@commands.Cog.listener()
async def on_ready(self):
member_count_channels = [
member_count_channels: List[discord.VoiceChannel] = [
channel
for channel in self.bot.uqcs_server.channels
if channel.name.startswith(self.MEMBER_COUNT_PREFIX)
and isinstance(channel, discord.VoiceChannel)
]
if len(member_count_channels) == 0:
logging.warning(
Expand All @@ -43,7 +47,14 @@ async def on_ready(self):
)
return

bot_member = self.bot.uqcs_server.get_member(self.bot.user.id)
if (
bot_member := self.bot.uqcs_server.get_member(self.bot.safe_user.id)
) is None:
logging.warning(
f"Unable to determine bot permissions for managing #Member Count channel."
)
return

permissions = self.member_count_channel.permissions_for(bot_member)
if not permissions.manage_channels:
logging.warning(
Expand All @@ -55,10 +66,16 @@ async def on_ready(self):
@app_commands.command(name="membercount")
async def member_count(self, interaction: discord.Interaction, force: bool = False):
"""Display the number of members"""
if interaction.guild is None or not isinstance(
interaction.user, discord.Member
):
return

new_members = [
member
for member in interaction.guild.members
if member.joined_at
if member.joined_at is not None
and member.joined_at
> datetime.now(tz=ZoneInfo("Australia/Brisbane")) - self.NEW_MEMBER_TIME
]
await interaction.response.send_message(
Expand Down Expand Up @@ -107,5 +124,5 @@ async def _update_member_count_channel_name(self):
self.waiting_for_rename = False


async def setup(bot: commands.Bot):
async def setup(bot: UQCSBot):
await bot.add_cog(MemberCounter(bot))
16 changes: 13 additions & 3 deletions uqcsbot/snailrace.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import discord, asyncio
import discord
from discord import app_commands, ui

from discord.ext import commands
from uqcsbot.bot import UQCSBot

import uqcsbot.utils.snailrace_utils as snail
from typing_extensions import TypeVar

V = TypeVar("V", bound="SnailRaceView", covariant=True)


# Trying out Discord buttons for Snail Race Interactions
Expand All @@ -18,17 +21,24 @@ async def on_timeout(self):
Called when the view times out. This will deactivate the buttons and
begine the race.
"""
if self.raceState.open_interaction is None:
return

for child in self.children:
child.disabled = True
if isinstance(child, discord.ui.Button):
child.disabled = True
await self.raceState.open_interaction.edit_original_response(
content=snail.SNAILRACE_ENTRY_CLOSE, view=self
)
await self.raceState.race_start()

@ui.button(label="Enter Race", style=discord.ButtonStyle.primary)
async def button_callback(
self, interaction: discord.Interaction, button: discord.ui.Button
self, interaction: discord.Interaction, button: discord.ui.Button[V]
):
if not isinstance(interaction.user, discord.Member):
return

action = self.raceState.add_racer(interaction.user)

if action == snail.SnailRaceJoinAdded:
Expand Down
Loading