Skip to content

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

Merged
full-bars merged 8 commits into
mainfrom
fix/tropical-channel-hardcoded-ids
Jul 25, 2026
Merged

fix: remove hardcoded channel IDs and add permission-failure visibility#619
full-bars merged 8 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 auto-posts now create discussion threads containing full raw text, while the channel message shows a short summary.
    • Added background-safe Discord send/thread helpers with clearer permission-failure logging and graceful thread fallback.
  • Bug Fixes
    • Failed message sends no longer mark items as posted; state updates now happen only after confirmed delivery.
    • Fixed tropical parsing/routing issues (including a rating-parsing issue) and improved handling of missing/partial summaries.
  • Documentation
    • Updated Unreleased changelog and .env.example notes for tropical/weather channel routing.
  • Tests
    • Updated tropical posting test expectations; adjusted warnings test setup.

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 centralizes Discord send and thread error handling, adds environment-backed channel routing, gates posting state on successful delivery, and updates tropical and SPC outlook products to use discussion threads with compact summaries and chunked raw content.

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, exports configurable channel IDs, and documents 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 follow-up state changes only after successful sends.
Threaded tropical and outlook products
cogs/tropical.py, cogs/outlooks.py, tests/test_tropical.py
Builds compact tropical summaries, sends summaries and chunked raw content through threads or fallback channels, and gates deduplication and product state on successful delivery.
Behavior documentation and fixture updates
CHANGELOG.md, tests/test_warnings.py
Documents routing, parsing, threading, and posting changes 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 summary and raw product text
  DiscordHelpers->>Discord: send thread content
  TropicalCog->>StateStore: record deduplication key after success
Loading

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

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 clearly captures the two main changes: removing hardcoded channel IDs and improving visibility into permission failures.
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

@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

🤖 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/tropical.py`:
- Around line 390-409: Update the tropical product posting flow around
summary_embed, raw_text_embed, and safe_send so text exceeding Discord’s
4096-character embed description limit is split into ordered continuation embeds
rather than truncated. Preserve all parsed summary and raw_text content,
including the existing titles and colors where appropriate, and ensure each
resulting embed remains within the limit.
🪄 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: 183114b8-d65a-4b2c-af2b-29a63d63f60f

📥 Commits

Reviewing files that changed from the base of the PR and between 6fc4637 and 3559ba3.

📒 Files selected for processing (2)
  • cogs/tropical.py
  • tests/test_tropical.py

Comment thread cogs/tropical.py Outdated
- _post_warning: uses safe_send instead of raw channel.send, returns
  (None, area) on failure so callers can abort cleanly
- _discover_and_post_warning: guard against None msg, remove obsolete
  HTTPException catch
- CON update loop: remove redundant try/except (safe_send handles it)
- Watch cancellation: uses safe_send with watch rollback on failure
- LSR reports: defer add_posted_report until all reports succeed

@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/warnings.py (1)

771-776: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Propagate failed-send results before advancing warning state.

safe_send now returns None on failure, but these paths still suppress retries:

  • cogs/warnings.py#L771-L776: make _handle_cancellation return success and only let its callers remove active_warnings / mark the product handled after a cancellation was posted. Otherwise a failed cancellation is never retried.
  • cogs/warnings.py#L978-L980: capture _post_warning’s message and skip the later add_posted_warning(... area=curr_area ...) update when it is None; otherwise the changed area is persisted despite no update post and future polls will not retry 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 771 - 776, Update cogs/warnings.py at lines
771-776 and 978-980: make _handle_cancellation return whether safe_send posted
successfully, and have its callers remove active_warnings or mark the product
handled only on success so failed cancellations retry. In the warning flow,
capture _post_warning’s returned message and call add_posted_warning(...
area=curr_area ...) only when that result is not None.
🤖 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/warnings.py`:
- Around line 771-776: Update cogs/warnings.py at lines 771-776 and 978-980:
make _handle_cancellation return whether safe_send posted successfully, and have
its callers remove active_warnings or mark the product handled only on success
so failed cancellations retry. In the warning flow, capture _post_warning’s
returned message and call add_posted_warning(... area=curr_area ...) only when
that result is not None.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5b60db00-8400-40eb-ac45-9666c0e3e38d

📥 Commits

Reviewing files that changed from the base of the PR and between 3559ba3 and e8285d2.

📒 Files selected for processing (5)
  • cogs/reports.py
  • cogs/tropical.py
  • cogs/warnings.py
  • cogs/watches.py
  • utils/discord_send.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • utils/discord_send.py
  • cogs/reports.py
  • cogs/watches.py
  • cogs/tropical.py

@full-bars
full-bars merged commit dc7372b into main Jul 25, 2026
10 checks passed
@full-bars
full-bars deleted the fix/tropical-channel-hardcoded-ids branch July 25, 2026 21:36
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