Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added

- Added discordPlayers to Convoke game creation.

## [v17.8.4](https://github.com/lexicalunit/spellbot/releases/tag/v17.8.4) - 2026-01-19

### Fixed
Expand Down
11 changes: 5 additions & 6 deletions src/spellbot/integrations/convoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
from spellbot import __version__
from spellbot.enums import GameBracket, GameFormat
from spellbot.metrics import add_span_error
from spellbot.services import ServicesRegistry
from spellbot.settings import settings

if TYPE_CHECKING:
from spellbot.models import GameDict

logger = logging.getLogger(__name__)
services = ServicesRegistry()

USE_PASSWORD = False # no password is now supported!
RETRY_ATTEMPTS = 2
Expand Down Expand Up @@ -113,6 +115,7 @@ async def fetch_convoke_link( # pragma: no cover
name = f"SB{game['id']}"
sb_game_format = GameFormat(game["format"])
format = convoke_game_format(sb_game_format).value
players = await services.games.player_data(game["id"])
payload = {
"apiKey": settings.CONVOKE_API_KEY,
"isPublic": False,
Expand All @@ -121,6 +124,7 @@ async def fetch_convoke_link( # pragma: no cover
"format": format,
"discordGuild": str(game["guild_xid"]),
"discordChannel": str(game["channel_xid"]),
"discordPlayers": [{"id": str(p["xid"]), "name": p["name"]} for p in players],
}
if game["bracket"] != GameBracket.NONE.value:
payload["bracketLevel"] = f"B{game['bracket'] - 1}"
Expand All @@ -140,12 +144,7 @@ async def generate_link(
return None, None

key = passphrase()
timeout = httpx.Timeout(
TIMEOUT_S,
connect=TIMEOUT_S,
read=TIMEOUT_S,
write=TIMEOUT_S,
)
timeout = httpx.Timeout(TIMEOUT_S, connect=TIMEOUT_S, read=TIMEOUT_S, write=TIMEOUT_S)
data: dict[str, Any] | None = None
async with httpx.AsyncClient(timeout=timeout) as client:
for attempt in range(RETRY_ATTEMPTS):
Expand Down
5 changes: 3 additions & 2 deletions src/spellbot/services/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ def verify_token(self, key: str, path: str) -> bool:
return False
if token.scopes == "*":
return True
required_scope = path.lstrip("/").split("/")[1]
if not required_scope:
try:
required_scope = path.lstrip("/").split("/")[1]
except IndexError:
return False
scopes_list = token.scopes.split(",")
return required_scope in scopes_list
12 changes: 12 additions & 0 deletions src/spellbot/services/games.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
Post,
Queue,
QueueDict,
User,
UserAward,
UserDict,
Watch,
)
from spellbot.settings import settings
Expand Down Expand Up @@ -566,3 +568,13 @@ def select_last_game(self, user_xid: int, guild_xid: int) -> GameDict | None:
.first()
)
return self.game.to_dict() if self.game else None

@sync_to_async()
@tracer.wrap()
def player_data(self, game_id: int) -> list[UserDict]:
game = DatabaseSession.query(Game).filter(Game.id == game_id).first()
if not game:
return []
player_xids = game.player_xids
players = DatabaseSession.query(User).filter(User.xid.in_(player_xids)).all()
return [p.to_dict() for p in players]
7 changes: 7 additions & 0 deletions tests/actions/test_lfg_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -1099,3 +1099,10 @@ async def test_handle_direct_messages_award_success(
add_role_stub.assert_called_once()
# Should send award message
assert send_stub.call_count >= 2 # One for game DM, one for award message

async def test_get_bracket_when_no_format_no_bracket_no_default(
self,
action: LookingForGameAction,
) -> None:
action.channel_data = {"default_bracket": None} # type: ignore
assert await action.get_bracket(None, None) == GameBracket.NONE.value
6 changes: 6 additions & 0 deletions tests/services/test_apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,9 @@ async def test_verify_token_empty_required_scope(self, factories: Factories) ->
# Let's test with a path that produces an empty string at index 1
# "//game" -> ["", "", "game"] -> [1] is "" which triggers the check
assert await apps.verify_token(token.key, "//game/1") is False

async def test_verify_token_when_path_is_bad(self, factories: Factories) -> None:
"""Test that a bad path doesn't crash and returns False."""
apps = AppsService()
token = factories.token.create(key="key", scopes="game")
assert await apps.verify_token(token.key, "/bogus") is False
20 changes: 11 additions & 9 deletions tests/services/test_games.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,7 @@

from spellbot.database import DatabaseSession
from spellbot.enums import GameBracket, GameFormat, GameService
from spellbot.models import (
Channel,
Game,
GameStatus,
Guild,
Post,
Queue,
User,
)
from spellbot.models import Channel, Game, GameStatus, Guild, Post, Queue, User
from spellbot.services import GamesService
from tests.factories import (
BlockFactory,
Expand Down Expand Up @@ -176,6 +168,16 @@ async def test_dequeue_players(self, game: Game) -> None:
assert user1.game(game.channel_xid) is None
assert user2.game(game.channel_xid) is None

async def test_player_data(self, game: Game) -> None:
user1 = UserFactory.create(game=game)
user2 = UserFactory.create(game=game)
games = GamesService()
assert await games.player_data(game.id) == [user1.to_dict(), user2.to_dict()]

async def test_player_data_when_game_not_found(self) -> None:
games = GamesService()
assert await games.player_data(404) == []


@pytest.mark.asyncio
class TestServiceGamesPlays:
Expand Down
Loading