Skip to content

Commit

Permalink
Suggest voice channel
Browse files Browse the repository at this point in the history
  • Loading branch information
lexicalunit committed Jan 1, 2025
1 parent b1e0a56 commit 0e17811
Show file tree
Hide file tree
Showing 12 changed files with 261 additions and 38 deletions.
4 changes: 2 additions & 2 deletions src/spellbot/actions/admin_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ async def info(self, game_id: str) -> None:
if not found:
return await self._report_failure()

embed = await games.to_embed(dm=True)
embed = await games.to_embed(self.guild, dm=True)
await safe_send_channel(self.interaction, embed=embed, ephemeral=True)
return None

Expand Down Expand Up @@ -504,7 +504,7 @@ async def set_points(self, game_id: int, player_xid: int, points: int) -> None:
message_xid = post["message_xid"]
message = safe_get_partial_message(channel, guild_xid, message_xid)
if message:
embed = await self.services.games.to_embed()
embed = await self.services.games.to_embed(self.guild)

Check warning on line 507 in src/spellbot/actions/admin_action.py

View check run for this annotation

Codecov / codecov/patch

src/spellbot/actions/admin_action.py#L507

Added line #L507 was not covered by tests
await safe_update_embed(message, embed=embed)

await safe_send_channel(
Expand Down
8 changes: 4 additions & 4 deletions src/spellbot/actions/leave_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async def _handle_click(self) -> None:
assert self.interaction.message is not None
await safe_delete_message(self.interaction.message)
else:
embed = await self.services.games.to_embed()
embed = await self.services.games.to_embed(self.guild)
await safe_update_embed_origin(self.interaction, embed=embed)
continue

Expand All @@ -65,7 +65,7 @@ async def _handle_click(self) -> None:
if do_delete_game:
await safe_delete_message(message)
else:
embed = await self.services.games.to_embed()
embed = await self.services.games.to_embed(self.guild)

Check warning on line 68 in src/spellbot/actions/leave_action.py

View check run for this annotation

Codecov / codecov/patch

src/spellbot/actions/leave_action.py#L68

Added line #L68 was not covered by tests
await safe_update_embed(message, embed=embed)

if do_delete_game:
Expand Down Expand Up @@ -104,7 +104,7 @@ async def _handle_command(self) -> None:
if do_delete_game:
await safe_delete_message(message)
else:
embed = await self.services.games.to_embed()
embed = await self.services.games.to_embed(self.guild)
await safe_update_embed(message, embed=embed)

if do_delete_game:
Expand Down Expand Up @@ -145,7 +145,7 @@ async def execute_all(self) -> None:
if do_delete_game:
await safe_delete_message(message)
else:
embed = await self.services.games.to_embed()
embed = await self.services.games.to_embed(self.guild)
await safe_update_embed(
message,
embed=embed,
Expand Down
10 changes: 5 additions & 5 deletions src/spellbot/actions/lfg_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ async def upsert_game(
self.guild.id,
message_xid,
):
embed = await self.services.games.to_embed()
embed = await self.services.games.to_embed(self.guild)
view: BaseView | None = None
if self.channel_data.get("show_points", False):
view = StartedGameView(bot=self.bot)
Expand Down Expand Up @@ -256,7 +256,7 @@ async def _update_other_game_posts(self, other_game_ids: list[int]) -> None:
message_xid,
)
):
embed = await self.services.games.to_embed()
embed = await self.services.games.to_embed(self.guild)

Check warning on line 259 in src/spellbot/actions/lfg_action.py

View check run for this annotation

Codecov / codecov/patch

src/spellbot/actions/lfg_action.py#L259

Added line #L259 was not covered by tests
await safe_update_embed(
message,
embed=embed,
Expand All @@ -277,7 +277,7 @@ async def add_points(self, message: Message, points: int) -> None:
return

await self.services.games.add_points(self.interaction.user.id, points)
embed = await self.services.games.to_embed()
embed = await self.services.games.to_embed(self.guild)
await safe_update_embed(message, embed=embed)

@tracer.wrap()
Expand Down Expand Up @@ -415,7 +415,7 @@ async def _handle_embed_creation( # noqa: C901,PLR0912
assert self.channel

# build the game post's embed and view:
embed: discord.Embed = await self.services.games.to_embed()
embed: discord.Embed = await self.services.games.to_embed(self.guild)
content = self.channel_data.get("extra", None)
game_data = await self.services.games.to_dict()

Expand Down Expand Up @@ -494,7 +494,7 @@ async def _reply_found_embed(self) -> None:
@tracer.wrap()
async def _handle_direct_messages(self) -> None:
player_xids = await self.services.games.player_xids()
embed = await self.services.games.to_embed(dm=True)
embed = await self.services.games.to_embed(self.guild, dm=True)
failed_xids: list[int] = []

# notify players
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""
Adds voice channel suggestions.
Revision ID: 6f5fb731f4c1
Revises: 0a7382dbcae6
Create Date: 2025-01-01 11:43:54.940328
"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "6f5fb731f4c1"
down_revision = "0a7382dbcae6"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.add_column(
"guilds",
sa.Column(
"suggest_voice_channel",
sa.Boolean(),
server_default=sa.text("false"),
nullable=False,
),
)


def downgrade() -> None:
op.drop_column("guilds", "suggest_voice_channel")
44 changes: 37 additions & 7 deletions src/spellbot/models/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
if TYPE_CHECKING:
from . import Channel, Guild, Post, PostDict, User # noqa: F401

HR = "**˙ॱ⋅.˳.⋅ॱ˙ॱ⋅.˳.⋅ॱ˙ॱ⋅.˳.⋅ॱ˙ॱ⋅.˳.⋅ॱ˙ॱ⋅.˳.⋅ॱ˙ॱ⋅.˳.⋅ॱ˙ॱ⋅.˳.⋅ॱ˙ॱ⋅.˳.⋅ॱ˙ॱ⋅.˳.⋅ॱ˙**"


class GameStatus(Enum):
PENDING = auto()
Expand Down Expand Up @@ -227,7 +229,11 @@ def embed_title(self) -> str:
plural = "s" if remaining > 1 else ""
return f"**Waiting for {remaining} more player{plural} to join...**"

def embed_description(self, dm: bool = False) -> str: # noqa: C901,PLR0912
def embed_description( # noqa: C901,PLR0912
self,
guild: discord.Guild | None,
dm: bool = False,
) -> str:
description = ""
if self.guild.notice:
description += f"{self.guild.notice}\n\n"
Expand All @@ -244,10 +250,12 @@ def embed_description(self, dm: bool = False) -> str: # noqa: C901,PLR0912
if self.show_links(dm):
if self.spelltable_link:
description += (
f"[Join your {GameService(self.service)} game now!]({self.spelltable_link})"
f"# [Join your {GameService(self.service)} game now!]"
f"({self.spelltable_link})"
)
if self.service == GameService.SPELLTABLE.value:
description += f" (or [spectate this game]({self.spectate_link}))"
# Spectate links do not seem to work anymore on SpellTable.
# if self.service == GameService.SPELLTABLE.value:
# description += f" (or [spectate this game]({self.spectate_link}))"
elif self.service == GameService.SPELLTABLE.value:
description += (
"Sorry but SpellBot was unable to create a SpellTable link"
Expand All @@ -267,9 +275,15 @@ def embed_description(self, dm: bool = False) -> str: # noqa: C901,PLR0912
if self.password:
description += f"\n\nPassword: `{self.password}`"
if self.voice_xid:
description += f"\n\nJoin your voice chat now: <#{self.voice_xid}>"
description += f"\n\n## Join your voice chat now: <#{self.voice_xid}>"
if self.voice_invite_link:
description += f"\nOr use this voice channel invite: {self.voice_invite_link}"
elif (
self.guild.suggest_voice_channel
and guild is not None
and (suggestion := self.voice_channel_suggestion(guild))
):
description += f"\n{suggestion}\n{HR}"
else:
description += "Please check your Direct Messages for your game details."
if dm:
Expand All @@ -287,6 +301,22 @@ def embed_description(self, dm: bool = False) -> str: # noqa: C901,PLR0912
description += f"\n\n{self.apply_placeholders(placeholders, self.channel.motd)}"
return description

def voice_channel_suggestion(self, guild: discord.Guild) -> str | None:
from spellbot.operations import safe_suggest_voice_channel

resp = safe_suggest_voice_channel(guild, [p.xid for p in self.players])
if resp.already_picked:
return (

Check warning on line 309 in src/spellbot/models/game.py

View check run for this annotation

Codecov / codecov/patch

src/spellbot/models/game.py#L309

Added line #L309 was not covered by tests
"\n## Your pod is already using a voice channel, "
f"join them now: <#{resp.already_picked}>!"
)
if resp.random_empty:
return (
"\n## Please consider using this available voice channel: "
f"<#{resp.random_empty}>."
)
return None

Check warning on line 318 in src/spellbot/models/game.py

View check run for this annotation

Codecov / codecov/patch

src/spellbot/models/game.py#L318

Added line #L318 was not covered by tests

@property
def placeholders(self) -> dict[str, str]:
game_start = f"<t:{self.started_at_timestamp}>" if self.started_at else "pending"
Expand Down Expand Up @@ -375,12 +405,12 @@ def confirmed(self) -> bool:
)
return player_count == confirmed_count

def to_embed(self, dm: bool = False) -> discord.Embed:
def to_embed(self, guild: discord.Guild | None = None, dm: bool = False) -> discord.Embed:
embed = discord.Embed(title=self.embed_title)
embed.set_thumbnail(
url=settings.QUEER_THUMB_URL if settings.queer(self.guild_xid) else settings.THUMB_URL
)
embed.description = self.embed_description(dm)
embed.description = self.embed_description(guild, dm)
if self.players:
embed.add_field(name="Players", value=self.embed_players, inline=False)
embed.add_field(name="Format", value=self.format_name)
Expand Down
9 changes: 9 additions & 0 deletions src/spellbot/models/guild.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class GuildDict(TypedDict):
awards: list[GuildAwardDict]
banned: bool
notice: str
suggest_voice_channel: bool


class Guild(Base):
Expand Down Expand Up @@ -93,6 +94,13 @@ class Guild(Base):
default=None,
server_default=null(),
)
suggest_voice_channel = Column(
Boolean,
nullable=False,
default=False,
server_default=false(),
doc="If true, suggest a voice channel to use",
)

games = relationship(
"Game",
Expand Down Expand Up @@ -134,4 +142,5 @@ def to_dict(self) -> GuildDict:
),
"banned": self.banned,
"notice": self.notice,
"suggest_voice_channel": self.suggest_voice_channel,
}
31 changes: 31 additions & 0 deletions src/spellbot/operations.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from __future__ import annotations

import logging
import random
from asyncio import sleep
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, cast

import discord
Expand Down Expand Up @@ -672,3 +674,32 @@ async def safe_add_role(
guild.id,
str(user_or_member),
)


@dataclass
class VoiceChannelSuggestion:
already_picked: int | None = None
random_empty: int | None = None


def safe_suggest_voice_channel(
guild: discord.Guild,
player_xids: list[int],
) -> VoiceChannelSuggestion:
empty_channels = []
random_empty = None
already_picked = None
for vc in guild.voice_channels:
if not vc.category or not vc.category.name.lower().startswith("lfg voice"):
# TCC uses voice channels with the category name "LFG VOICE A-B", this
# feature is only for TCC, so let's just skip non-LFG voice channels.
# This could be made configurable in the future if other guilds want it.
continue

Check warning on line 697 in src/spellbot/operations.py

View check run for this annotation

Codecov / codecov/patch

src/spellbot/operations.py#L697

Added line #L697 was not covered by tests
member_xids = {m.id for m in vc.members}
if any(player_xid in member_xids for player_xid in player_xids):
already_picked = vc.id
break
if not member_xids:
empty_channels.append(vc.id)
random_empty = random.choice(empty_channels) if empty_channels else None # noqa: S311
return VoiceChannelSuggestion(already_picked, random_empty)
4 changes: 2 additions & 2 deletions src/spellbot/services/games.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,9 +257,9 @@ def _find_existing(

@sync_to_async()
@tracer.wrap()
def to_embed(self, dm: bool = False) -> discord.Embed:
def to_embed(self, guild: discord.Guild | None, dm: bool = False) -> discord.Embed:
assert self.game
return self.game.to_embed(dm)
return self.game.to_embed(guild, dm)

@sync_to_async()
@tracer.wrap()
Expand Down
Loading

0 comments on commit 0e17811

Please sign in to comment.