Skip to content
4 changes: 0 additions & 4 deletions src/app_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@
run_player_search_deferred,
run_subscribe_deferred,
run_subscription_deferred,
run_unsubscribe_clan_deferred,
run_unsubscribe_deferred,
run_unsubscribe_player_deferred,
)
from .config import Settings, get_settings
from .discord_auth import verify_discord_signature_with_reason
Expand Down Expand Up @@ -48,8 +46,6 @@ def create_app() -> FastAPI:
"subscribe": run_subscribe_deferred,
"subscription": run_subscription_deferred,
"unsubscribe": run_unsubscribe_deferred,
"unsubscribe-player": run_unsubscribe_player_deferred,
"unsubscribe-clan": run_unsubscribe_clan_deferred,
}

def _msg(content: str) -> dict[str, Any]:
Expand Down
67 changes: 46 additions & 21 deletions src/commands/subscribe.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,16 @@
bungie_emblem_url,
format_player_display_name,
parse_clan_group_id,
resolve_player_membership_id,
resolve_player_subscription_row,
)
from .subscription_messages import (
CLAN_ID_NOT_RECOGNIZED_TITLE,
PLAYER_NOT_FOUND_TITLE,
SUBSCRIBE_COMMAND_TITLE,
SUBSCRIBE_FAILED_TITLE,
SUBSCRIPTION_SAVED_TITLE,
subscribe_success_description,
)
from .subscription_helpers import (
fetch_subscription_status_envelope,
format_clan_display_name,
Expand Down Expand Up @@ -52,7 +59,7 @@ async def run_subscribe_deferred(
app_id,
token,
warn_embed(
"Subscribe Command",
SUBSCRIBE_COMMAND_TITLE,
"Use `/subscribe player` or `/subscribe clan` with a target.",
),
)
Expand All @@ -63,18 +70,24 @@ async def run_subscribe_deferred(
await patch_discord_followup_best_effort(
app_id,
token,
warn_embed("Subscribe Command", "Unknown `/subscribe` subcommand."),
warn_embed(SUBSCRIBE_COMMAND_TITLE, "Unknown `/subscribe` subcommand."),
)
return

leaf = flatten_options(top_opts[0].get("options"))
target_raw = str(leaf.get("player") or leaf.get("clan") or "").strip()
filters: dict[str, Any] = {}
if "require_fresh" in leaf:
filters["requireFresh"] = bool(leaf["require_fresh"])
if "require_completed" in leaf:
filters["requireCompleted"] = bool(leaf["require_completed"])
# TODO: Add raid filter input once we have a solid multi-select UX.
if not target_raw:
await patch_discord_followup_best_effort(
app_id,
token,
warn_embed(
"Subscribe Command",
SUBSCRIBE_COMMAND_TITLE,
"Provide a target (membership id / player name, or clan id / URL).",
),
)
Expand All @@ -85,7 +98,7 @@ async def run_subscribe_deferred(
app_id,
token,
warn_embed(
"Subscribe Command",
SUBSCRIBE_COMMAND_TITLE,
"Run `/subscribe` in a server text channel, not a DM.",
),
)
Expand All @@ -97,7 +110,7 @@ async def run_subscribe_deferred(
app_id,
token,
error_embed(
"Subscribe Failed",
SUBSCRIBE_FAILED_TITLE,
subscription_envelope_error_message(status_env),
),
)
Expand All @@ -112,19 +125,25 @@ async def run_subscribe_deferred(
app_id,
token,
error_embed(
"Player Not Found",
PLAYER_NOT_FOUND_TITLE,
"Could not resolve that player. Try a Destiny membership id or a clearer"
" name (first RaidHub search hit is used).",
),
)
return
raw_mid = prow.get("membershipId")
resolved_id = str(int(str(raw_mid).strip())) if raw_mid is not None else ""
try:
resolved_id = str(int(str(raw_mid).strip())) if raw_mid is not None else ""
except (TypeError, ValueError):
resolved_id = ""
if not resolved_id or not resolved_id.isdigit():
await patch_discord_followup_best_effort(
app_id,
token,
error_embed("Player Not Found", "Missing membership id for that player."),
error_embed(
PLAYER_NOT_FOUND_TITLE,
"Missing membership id for that player.",
),
Comment on lines 140 to +146

Copilot AI Apr 24, 2026

Copy link

Choose a reason for hiding this comment

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

The "Missing membership id" branch here assumes resolved_id normalization always succeeds, but resolved_id = str(int(str(raw_mid).strip())) just above can raise ValueError before this check runs (e.g., if membershipId is non-numeric/empty). Consider switching to resolve_player_membership_id(...) (or the existing _norm_membership_id) so invalid IDs are handled as a normal control-flow case instead of an exception.

Copilot uses AI. Check for mistakes.
)
return
if registered:
Expand All @@ -135,6 +154,8 @@ async def run_subscribe_deferred(
body: dict[str, Any] = {"targets": {"playerMembershipIds": merged_players}}
else:
body = {"targets": {"playerMembershipIds": [resolved_id]}}
if filters:
body["filters"] = filters
display_name = format_player_display_name(prow)
icon_raw = prow.get("iconPath")
thumb_url = (
Expand All @@ -149,7 +170,7 @@ async def run_subscribe_deferred(
app_id,
token,
error_embed(
"Clan ID Not Recognized",
CLAN_ID_NOT_RECOGNIZED_TITLE,
"Could not parse a clan group id from that value. Use digits only, or a"
" raidhub.io/clan/... / Bungie clan URL containing the group id.",
),
Expand All @@ -164,6 +185,8 @@ async def run_subscribe_deferred(
body = {"targets": {"clanGroupIds": merged_clans}}
else:
body = {"targets": {"clanGroupIds": [resolved_id]}}
if filters:
body["filters"] = filters

ctx = discord_invocation_context(interaction, route_id=SUB_ROUTE_PUT)
env = await raidhub.request_envelope(
Expand All @@ -178,19 +201,20 @@ async def run_subscribe_deferred(
app_id,
token,
error_embed(
"Subscribe Failed",
SUBSCRIBE_FAILED_TITLE,
subscription_envelope_error_message(env),
),
)
return

if sub == "player":
desc = (
f"Subscribed to **{display_name}** (`{resolved_id}`) for this channel. "
"Use `/subscription status` to see all rules for this channel."
desc = subscribe_success_description(
display_name,
resolved_id,
str(interaction.get("channel_id") or ""),
)
msg = success_embed(
"Subscription Saved",
SUBSCRIPTION_SAVED_TITLE,
desc,
thumbnail_url=thumb_url,
)
Expand All @@ -208,12 +232,13 @@ async def run_subscribe_deferred(
if clan_row and isinstance(apath, str)
else None
)
desc = (
f"Subscribed to **{c_disp}** (`{resolved_id}`) for this channel. "
"Use `/subscription status` to see all rules for this channel."
desc = subscribe_success_description(
c_disp,
resolved_id,
str(interaction.get("channel_id") or ""),
)
msg = success_embed(
"Subscription Saved",
SUBSCRIPTION_SAVED_TITLE,
desc,
thumbnail_url=c_thumb,
)
Expand All @@ -226,10 +251,10 @@ async def run_subscribe_deferred(
err=err,
discord_application_id=app_id,
interaction_token=token,
user_message_payload=error_embed("Subscribe Failed", USER_FACING_GENERIC),
user_message_payload=error_embed(SUBSCRIBE_FAILED_TITLE, USER_FACING_GENERIC),
)
finally:
observe_deferred_completion(command="subscribe", outcome=outcome)


__all__ = ["run_subscribe_deferred", "resolve_player_membership_id", "parse_clan_group_id"]
__all__ = ["run_subscribe_deferred", "parse_clan_group_id"]
60 changes: 19 additions & 41 deletions src/commands/subscription.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,21 @@
from ..config import Settings
from ..prom_metrics import observe_deferred_completion
from ..raidhub_client import RaidHubClient, discord_invocation_context
from .subscription_messages import (
SUBSCRIPTION_COMMAND_TITLE,
SUBSCRIPTION_REQUEST_FAILED_TITLE,
)
from .subscription_helpers import (
format_subscription_status_embed,
subscription_envelope_error_message,
)
from .subscription_routes import SUB_ROUTE_DELETE, SUB_ROUTE_STATUS
from .subscription_routes import SUB_ROUTE_STATUS
from .shared import (
USER_FACING_GENERIC,
application_id,
error_embed,
patch_discord_followup_best_effort,
report_deferred_exception,
success_embed,
warn_embed,
)

Expand All @@ -32,23 +35,14 @@ async def run_subscription_deferred(
try:
data = interaction.get("data") or {}
top_opts = data.get("options") or []
if not top_opts or not isinstance(top_opts[0], dict):
sub = "status"
if top_opts and isinstance(top_opts[0], dict):
sub = str(top_opts[0].get("name") or "").strip().lower() or "status"
if sub != "status":
await patch_discord_followup_best_effort(
app_id,
token,
warn_embed(
"Subscription Command",
"Pick `status` or `delete` under `/subscription`.",
),
)
return

sub = str(top_opts[0].get("name") or "").strip().lower()
if sub not in ("delete", "status"):
await patch_discord_followup_best_effort(
app_id,
token,
warn_embed("Subscription Command", "Unknown `/subscription` subcommand."),
warn_embed(SUBSCRIPTION_COMMAND_TITLE, "Use `/subscription` to view status."),
)
return

Expand All @@ -57,48 +51,32 @@ async def run_subscription_deferred(
app_id,
token,
warn_embed(
"Subscription Command",
SUBSCRIPTION_COMMAND_TITLE,
"Run this command in a server channel, not a DM.",
),
)
return

route_id = {"delete": SUB_ROUTE_DELETE, "status": SUB_ROUTE_STATUS}[sub]
ctx = discord_invocation_context(interaction, route_id=route_id)

if sub == "status":
env = await raidhub.request_envelope(
"GET",
"/subscriptions/discord/webhooks",
discord_context=ctx,
)
else:
env = await raidhub.request_envelope(
"DELETE",
"/subscriptions/discord/webhooks",
discord_context=ctx,
)
ctx = discord_invocation_context(interaction, route_id=SUB_ROUTE_STATUS)
env = await raidhub.request_envelope(
"GET",
"/subscriptions/discord/webhooks",
discord_context=ctx,
)

if not env.get("success"):
await patch_discord_followup_best_effort(
app_id,
token,
error_embed(
"Subscription Request Failed",
SUBSCRIPTION_REQUEST_FAILED_TITLE,
subscription_envelope_error_message(env),
),
)
return

inner = env.get("response") or {}
if sub == "status":
msg = await format_subscription_status_embed(raidhub, inner)
else:
msg = success_embed(
"Subscription Removed",
"RaidHub will no longer use a webhook in this channel.",
)

msg = await format_subscription_status_embed(raidhub, inner)
await patch_discord_followup_best_effort(app_id, token, msg)
except Exception as err:
outcome = "error"
Expand Down
Loading
Loading