Skip to content

fix: remove hardcoded channel IDs and add permission-failure visibility#619

Open
full-bars wants to merge 3 commits into
mainfrom
fix/tropical-channel-hardcoded-ids
Open

fix: remove hardcoded channel IDs and add permission-failure visibility#619
full-bars wants to merge 3 commits into
mainfrom
fix/tropical-channel-hardcoded-ids

Conversation

@full-bars

@full-bars full-bars commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Summary

  • Redirects the still-in-testing tropical auto-poster to the dev channel (warning_channel:tropical override) instead of the public production channel it was defaulting to — that channel was in fact silently failing every post with discord.errors.Forbidden: 403 Missing Permissions.
  • Removes hardcoded fallback channel IDs: config.py's tropical_channel_id (now uses the same _optional_int fallback chain as every other channel), a dead/unused DEV_CHANNEL_ID constant in cogs/tropical.py, and a hardcoded WEATHER_CHAT_CHANNEL_ID in cogs/fronts.py (now a proper .env-backed config value).
  • Adds utils/discord_send.py (safe_send/safe_create_thread) so a missing-permissions failure in any background auto-poster logs a clear ERROR with the channel name/ID, instead of blending into generic exception noise or (in a couple of spots) being swallowed entirely. Applied to tropical, mesoscale/MD, outlooks, watches, warnings, scp, ncar, reports, and recorder — scoped to background task-loop posts only; interactive slash-command replies already surface errors to the invoking user and were left untouched.
  • Fixes tropical's dedup-before-send bug: a product was marked "posted" before the send was attempted, so any failed post (like the permissions issue above) permanently skipped that product instead of retrying once fixed.

Test plan

  • pytest tests/ — 569 passed
  • ruff check / ruff format clean
  • Verified locally that config.TROPICAL_CHANNEL_ID correctly falls through to DEV_CHANNEL_ID when TROPICAL_CHANNEL_ID is unset
  • Live state on the primary server already redirected (warning_channel:tropical → dev channel) ahead of this merge

Summary by CodeRabbit

  • New Features
    • Tropical product posts now create discussion threads that include the full product text, while the channel post remains a short summary.
    • Added dedicated discussion-thread support for sounding and mesoscale/MD summaries, with graceful inline fallback.
    • Channel routing is now configurable for tropical and weather-chat messages.
  • Bug Fixes
    • Improved handling of missing Discord permissions and failed deliveries; failed posts no longer update “posted” state.
    • Fixed tropical-product parsing/posting edge cases and improved deduplication to mark items posted only after confirmed delivery.
    • Resolved additional tropical-product and multi-tornado damage survey rating parsing issues.
  • Documentation
    • Updated unreleased changelog and environment configuration examples.

Redirects the still-in-testing tropical auto-poster to the dev channel
via the warning_channel:tropical override instead of the public
production channel it had been silently failing against (403 Missing
Permissions on every attempt). Removes the hardcoded fallback channel
IDs in config.py, cogs/tropical.py, and cogs/fronts.py in favor of the
same env-backed fallback-chain convention used everywhere else.

Adds utils/discord_send.py (safe_send/safe_create_thread) so a missing-
permissions failure in any background auto-poster is logged clearly
instead of blending into generic exception noise, applied across
tropical, mesoscale/MD, outlooks, watches, warnings, scp, ncar,
reports, and recorder. Also fixes tropical's dedup-before-send, which
permanently skipped a product on any failed post instead of retrying
once the underlying issue was fixed.
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The PR adds failure-aware Discord send and thread helpers, moves channel IDs to environment-backed configuration, migrates multiple cogs to success-gated posting, and updates tropical and SPC outlook posts to use discussion threads.

Discord Posting and Routing

Layer / File(s) Summary
Posting helpers and channel configuration
utils/discord_send.py, config.py, .env.example, cogs/fronts.py
Adds safe_send and safe_create_thread, adds configurable channel exports, and documents their fallback behavior.
Cog posting migration
cogs/mesoscale.py, cogs/ncar.py, cogs/recorder.py, cogs/reports.py, cogs/scp.py, cogs/warnings.py, cogs/watches.py
Routes outbound messages through safe_send and performs state updates only after successful sends.
Threaded tropical and outlook products
cogs/tropical.py, cogs/outlooks.py
Creates discussion threads for product content and defers deduplication or posting state updates until successful delivery.
Behavior documentation and fixture updates
CHANGELOG.md, tests/test_warnings.py
Documents the new posting behavior and initializes warning cancellation state in the test fixture.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TropicalCog
  participant DiscordHelpers
  participant Discord
  participant StateStore
  TropicalCog->>DiscordHelpers: safe_send product summary
  DiscordHelpers->>Discord: send message
  Discord-->>DiscordHelpers: message or failure
  DiscordHelpers-->>TropicalCog: result
  TropicalCog->>DiscordHelpers: safe_create_thread(message)
  DiscordHelpers->>Discord: create discussion thread
  Discord-->>DiscordHelpers: thread or failure
  TropicalCog->>DiscordHelpers: safe_send full product text
  DiscordHelpers->>Discord: send thread content
  TropicalCog->>StateStore: record deduplication key after success
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: removing hardcoded channel IDs and improving permission-failure visibility.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/tropical-channel-hardcoded-ids

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cogs/warnings.py (1)

979-983: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Safe-send migration is incomplete for the most safety-critical posting paths. Both files migrated a sibling send in the same function/file to safe_send, but left another send path on raw channel.send() with only generic exception logging (no channel name/ID) — missing exactly the permission-failure visibility this PR is meant to add, for the highest-stakes cases (new severe-warning issuance/updates, watch cancellations).

  • cogs/warnings.py#L979-L983,1053-L1058,1150-L1156: route _post_warning's channel.send(embed=embed) (L1155) through safe_send, and update the _discover_and_post_warning/CON-update callers to check a truthy return instead of catching discord.HTTPException (needed to preserve the claim-rollback-on-failure semantics).
  • cogs/watches.py#L480-L491: replace the cancellation channel.send(embed=embed) + try/except discord.HTTPException with safe_send(...), gating the existing active_watches[watch_num] = info rollback on a falsy return, mirroring the new-watch loop directly below it.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cogs/warnings.py` around lines 979 - 983, Complete the safe-send migration
for the specified warning and watch posting paths: in cogs/warnings.py at lines
979-983, 1053-1058, and 1150-1156, route _post_warning’s
channel.send(embed=embed) through safe_send and update the
_discover_and_post_warning and CON-update callers to use its truthy result while
preserving claim rollback on failure; in cogs/watches.py at lines 480-491,
replace the cancellation send and HTTPException handler with safe_send and
restore active_watches[watch_num] only when the send returns falsy, matching the
new-watch loop behavior.
🧹 Nitpick comments (1)
utils/discord_send.py (1)

47-49: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Inconsistent failure-logging level vs. safe_send.

safe_create_thread's generic-exception branch uses logger.warning without a traceback, while safe_send's equivalent branch uses logger.exception. This module's stated purpose is to avoid failures being "buried in a traceback" — but this branch does the opposite for non-Forbidden thread-creation errors, making them harder to diagnose than send failures.

🛠️ Proposed fix
     except Exception as e:
-        logger.warning(f"Failed to create thread for {context}: {e}")
+        logger.exception(f"Failed to create thread for {context}: {e}")
         return None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/discord_send.py` around lines 47 - 49, Update the generic exception
handler in safe_create_thread to use logger.exception instead of logger.warning,
preserving the existing failure message and return None behavior so
thread-creation errors include their traceback consistently with safe_send.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cogs/outlooks.py`:
- Around line 169-174: Update both full-text thread-posting sites in
cogs/outlooks.py (lines 169-186 and 316-333) to send through safe_send instead
of calling thread.send directly, preserving the existing post content and state
flow while using the helper’s permission-aware logging.

---

Outside diff comments:
In `@cogs/warnings.py`:
- Around line 979-983: Complete the safe-send migration for the specified
warning and watch posting paths: in cogs/warnings.py at lines 979-983,
1053-1058, and 1150-1156, route _post_warning’s channel.send(embed=embed)
through safe_send and update the _discover_and_post_warning and CON-update
callers to use its truthy result while preserving claim rollback on failure; in
cogs/watches.py at lines 480-491, replace the cancellation send and
HTTPException handler with safe_send and restore active_watches[watch_num] only
when the send returns falsy, matching the new-watch loop behavior.

---

Nitpick comments:
In `@utils/discord_send.py`:
- Around line 47-49: Update the generic exception handler in safe_create_thread
to use logger.exception instead of logger.warning, preserving the existing
failure message and return None behavior so thread-creation errors include their
traceback consistently with safe_send.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0a9c23f8-773c-45c7-94be-1633efb86d6d

📥 Commits

Reviewing files that changed from the base of the PR and between bda5316 and 9a68c33.

📒 Files selected for processing (15)
  • .env.example
  • CHANGELOG.md
  • cogs/fronts.py
  • cogs/mesoscale.py
  • cogs/ncar.py
  • cogs/outlooks.py
  • cogs/recorder.py
  • cogs/reports.py
  • cogs/scp.py
  • cogs/tropical.py
  • cogs/warnings.py
  • cogs/watches.py
  • config.py
  • tests/test_warnings.py
  • utils/discord_send.py

Comment thread cogs/outlooks.py
Comment on lines +169 to +174
thread = await safe_create_thread(
msg,
context=f"SPC Day {day} outlook",
name=f"SPC Day {day} Outlook",
auto_archive_duration=1440,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
path = Path("cogs/outlooks.py")
text = path.read_text()
for needle in [
    "safe_create_thread(",
    "thread.send(",
    "safe_send(",
]:
    print(f"\n== {needle} ==")
    for i, line in enumerate(text.splitlines(), 1):
        if needle in line:
            print(f"{i}: {line}")
PY

Repository: full-bars/spc-bot

Length of output: 562


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path

path = Path("cogs/outlooks.py")
lines = path.read_text().splitlines()

for start, end in [(140, 200), (285, 345)]:
    print(f"\n--- lines {start}-{end} ---")
    for i in range(start, end + 1):
        if i <= len(lines):
            print(f"{i}: {lines[i-1]}")
PY

Repository: full-bars/spc-bot

Length of output: 6378


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path

# Find safe_send / safe_create_thread definitions and usages.
for path in Path(".").rglob("*.py"):
    text = path.read_text(errors="ignore")
    if "def safe_send" in text or "async def safe_send" in text or "safe_send(" in text:
        if path.as_posix() != "cogs/outlooks.py":
            print(f"\n== {path} ==")
            for i, line in enumerate(text.splitlines(), 1):
                if "def safe_send" in line or "async def safe_send" in line or "safe_send(" in line:
                    print(f"{i}: {line}")
PY

Repository: full-bars/spc-bot

Length of output: 1233


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path

# Show the helper and any thread-related wrappers/usages.
for file in ["utils/discord_send.py", "utils/discord_thread.py", "utils/discord.py"]:
    p = Path(file)
    if p.exists():
        print(f"\n=== {file} ===")
        lines = p.read_text().splitlines()
        for i, line in enumerate(lines, 1):
            if "safe_send" in line or "safe_create_thread" in line or "thread" in line.lower():
                print(f"{i}: {line}")

print("\n=== thread.send / safe_send on thread-like objects ===")
for path in Path(".").rglob("*.py"):
    text = path.read_text(errors="ignore")
    if "thread.send(" in text or "safe_send(" in text and "thread" in text:
        # print only files that look relevant
        if path.as_posix() in {
            "cogs/outlooks.py",
            "cogs/tropical.py",
            "cogs/mesoscale.py",
            "cogs/warnings.py",
            "cogs/scp.py",
            "cogs/reports.py",
            "cogs/watches.py",
            "cogs/ncar.py",
            "cogs/recorder.py",
        }:
            print(f"\n== {path} ==")
            for i, line in enumerate(text.splitlines(), 1):
                if "thread.send(" in line or "safe_send(" in line:
                    print(f"{i}: {line}")
PY

Repository: full-bars/spc-bot

Length of output: 1492


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path

p = Path("utils/discord_send.py")
lines = p.read_text().splitlines()
for i, line in enumerate(lines, 1):
    print(f"{i}: {line}")
PY

Repository: full-bars/spc-bot

Length of output: 2182


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path

# Inspect the analogous thread-target send in cogs/tropical.py and any surrounding flow.
p = Path("cogs/tropical.py")
lines = p.read_text().splitlines()
for start, end in [(240, 290)]:
    print(f"\n--- lines {start}-{end} ---")
    for i in range(start, end + 1):
        if i <= len(lines):
            print(f"{i}: {lines[i-1]}")
PY

Repository: full-bars/spc-bot

Length of output: 2228


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path

p = Path("cogs/outlooks.py")
lines = p.read_text().splitlines()

for start, end in [(150, 240), (300, 380)]:
    print(f"\n--- lines {start}-{end} ---")
    for i in range(start, end + 1):
        if i <= len(lines):
            print(f"{i}: {lines[i-1]}")
PY

Repository: full-bars/spc-bot

Length of output: 8113


Route the thread full-text post through safe_send. The direct thread.send(...) calls bypass the helper’s permission-aware logging, so failures here are only surfaced by the outer catch after the post state has already been recorded.

  • cogs/outlooks.py#L169-L186
  • cogs/outlooks.py#L316-L333
📍 Affects 1 file
  • cogs/outlooks.py#L169-L174 (this comment)
  • cogs/outlooks.py#L316-L321
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cogs/outlooks.py` around lines 169 - 174, Update both full-text
thread-posting sites in cogs/outlooks.py (lines 169-186 and 316-333) to send
through safe_send instead of calling thread.send directly, preserving the
existing post content and state flow while using the helper’s permission-aware
logging.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cogs/reports.py (1)

282-285: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not mark a multi-report LSR product as posted after the first successful send.

This handler splits one product into multiple reports, but product_id is persisted immediately after any single successful message. If a later report fails, the next invocation skips the entire product and that report can never retry. Track posted state per report, or defer the product-level marker until every report succeeds while preventing duplicate sends.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cogs/reports.py` around lines 282 - 285, Update the handler around safe_send
and add_posted_report so a multi-report LSR product is not marked posted after
only the first successful report. Track completion per report, or defer the
product-level marker until all reports send successfully, while preserving
duplicate-send prevention and allowing failed reports to retry on later
invocations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@cogs/reports.py`:
- Around line 282-285: Update the handler around safe_send and add_posted_report
so a multi-report LSR product is not marked posted after only the first
successful report. Track completion per report, or defer the product-level
marker until all reports send successfully, while preserving duplicate-send
prevention and allowing failed reports to retry on later invocations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 275e771c-4b4c-4b27-ad0d-840aa5951a8b

📥 Commits

Reviewing files that changed from the base of the PR and between 9a68c33 and 6fc4637.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • cogs/reports.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant