Skip to content

Red-DiscordBot Stream Alerts — Querying, Auditing & Pruning

Red's built-in Streams cog posts a go-live announcement when a configured Twitch/YouTube streamer starts broadcasting. Over time the streamer→channel map drifts: channels get deleted, servers get left, and the config fills with dead entries that silently fail. This runbook covers how to inspect that map, resolve the numeric IDs to human names, add a read-only lookup command, and safely prune dead channels.

What the Streams cog does and does not store

  • Stores: a flat list of stream alerts — each is {name, id, channels[], type}. channels[] holds the Discord channel IDs the go-live message is posted to. That's it.
  • Does NOT store: message history. Red is not a searchable archive of what people said. You can find whether a streamer is set to auto-announce, but not whether someone mentioned them in chat.
  • Scope: only guilds the bot has actually joined. "Is X promoted on any server?" really means "any server this bot instance is in."

Where the data lives

Instance layout (JSON storage backend):

~<botuser>/.config/Red-DiscordBot/config.json        # instance -> DATA_PATH, STORAGE_TYPE
<DATA_PATH>/core/settings.json                        # bot token, prefix, autoloaded `packages`
<DATA_PATH>/cogs/Streams/settings.json                # the stream alert map
<DATA_PATH>/cogs/CogManager/cogs/                     # local (custom) cogs live here

The stream list is under a per-cog Config identifier (a fixed integer, e.g. 26262626):

settings.json -> {"<cog_id>": {"GLOBAL": {"streams": [ {name,id,channels,type}, ... ],
                                           "refresh_timer": 60},
                               "GUILD": { "<guild_id>": {...per-guild toggles...} } }}

Note the GUILD scope only lists guilds that have per-guild settings — the bot can announce into a channel whose guild has no GUILD entry, so counting GUILD keys undercounts the servers actually in use. Derive the real server list from the channels instead (below).

Querying from inside Discord

Assume prefix ~.

Command Effect
~streamalert list Lists all active alerts in the current server (verbose; whole roster).
~twitch <name> One-off: is that streamer live right now? Does not touch alerts.
~streamalert twitch <name> Toggles an alert on/off in the current channel — mutating, avoid for lookups.
~streamalert stop Removes all alerts in the server.

There is no stock read-only "is this one streamer announced?" command — list dumps everything and the single-name command toggles. Add the cog below to get one.

Read-only single-streamer lookup — streamcheck cog

A tiny custom cog that reads the live Streams list and reports where one streamer is announced, changing nothing.

cogs/CogManager/cogs/streamcheck/__init__.py:

from .streamcheck import StreamCheck


async def setup(bot):
    await bot.add_cog(StreamCheck(bot))

cogs/CogManager/cogs/streamcheck/streamcheck.py:

from redbot.core import commands


class StreamCheck(commands.Cog):
    """Read-only lookups against the Streams cog's alert list."""

    def __init__(self, bot):
        self.bot = bot

    @commands.command()
    async def streamcheck(self, ctx, name: str):
        """Show where a streamer is set to auto-announce. Read-only — changes nothing."""
        streams_cog = self.bot.get_cog("Streams")
        if streams_cog is None:
            return await ctx.send("The Streams cog is not loaded.")

        name_l = name.lower()
        found = [s for s in streams_cog.streams if (s.name or "").lower() == name_l]
        if not found:
            return await ctx.send(f"\N{CROSS MARK} `{name}` is not in any stream alert.")

        lines = []
        for s in found:
            for cid in s.channels:
                ch = self.bot.get_channel(cid)
                if ch is None:
                    lines.append(f"\N{SMALL ORANGE DIAMOND} unknown/inaccessible channel `{cid}`")
                else:
                    lines.append(f"\N{BULLET} **{ch.guild.name}** \N{RIGHTWARDS ARROW} {ch.mention}")

        header = f"\N{WHITE HEAVY CHECK MARK} `{found[0].name}` is announced in {len(lines)} channel(s):"
        await ctx.send(header + "\n" + "\n".join(lines))

It relies on the Streams cog exposing self.streams (a List[Stream], each with .name and .channels) — stable in current Red 3.5.

Deploy it

# 1. Copy the folder into the local-cog path (owned by the bot user)
scp -r streamcheck root@<host>:/tmp/streamcheck
ssh root@<host> 'D=<DATA_PATH>/cogs/CogManager/cogs; mv /tmp/streamcheck $D/ && chown -R <botuser>:<botuser> $D/streamcheck'

Then load it. Zero-downtime way — as bot owner in Discord: ~load streamcheck.

Hands-free (systemd) way — add it to autoload and restart. Because Red rewrites core/settings.json on a clean shutdown, edit the file while the bot is stopped, or the save-on-exit clobbers your change:

systemctl stop red@<instance>.service
python3 - <<'PY'
import json,glob,shutil
f=glob.glob("<DATA_PATH>/core/settings.json")[0]; shutil.copy(f,f+".bak")
d=json.load(open(f))
def walk(o):
    if isinstance(o,dict):
        for k,v in o.items():
            if k=="packages" and isinstance(v,list) and "streamcheck" not in v: v.append("streamcheck")
            else: walk(v)
    elif isinstance(o,list):
        [walk(x) for x in o]
walk(d); json.dump(d,open(f,"w"),indent=4)
PY
systemctl start red@<instance>.service

Usage: ~streamcheck <streamer> → lists every server/channel that streamer announces in, or ❌ … not in any stream alert.

A channel that shows as 🔸 unknown/inaccessible means the bot's own cache can't resolve it — almost always a deleted channel. A channel that resolves (server name shown) but renders as "unknown" in your client just means you lack permission to view it; the bot posts there fine.

Resolving IDs → human names (Discord REST API)

settings.json only has numeric IDs. To label them, ask the Discord API with the bot token — do it on the host so the token never touches your terminal or logs:

import json, glob, urllib.request
base = "<DATA_PATH>"
tok = None
d = json.load(open(glob.glob(base + "/core/settings.json")[0]))
def walk(o):
    global tok
    if isinstance(o, dict):
        for k, v in o.items():
            if k == "token" and isinstance(v, str) and v: tok = v
            else: walk(v)
    elif isinstance(o, list):
        [walk(x) for x in o]
walk(d)
H = {"Authorization": "Bot " + tok, "User-Agent": "DiscordBot (admin,1.0)"}
def api(p):
    r = urllib.request.Request("https://discord.com/api/v10" + p, headers=H)
    return json.load(urllib.request.urlopen(r, timeout=15))

ch = api("/channels/994734863829696532")     # -> {"name": "live", "guild_id": "..."}
g  = api("/guilds/" + ch["guild_id"])         # -> {"name": "Stream Raleigh"}
  • HTTP 200 = live; 404 Unknown Channel = deleted (or the bot left that guild); 403 = access denied.
  • The Streams cog references few unique channels (dedupe channel IDs first) — a handful of API calls, well under rate limits. Handle 429 by sleeping retry_after.

Fleet-wide dead-channel audit

Collect every unique channel across all streams, classify each via the API, then flag streams pointing at dead channels. A stream whose channels are all dead announces nowhere and is safe to delete entirely; a partially-dead one just needs the dead refs stripped. Write the report to a file rather than dumping to the terminal.

Editing the Streams config safely

Any write to cogs/Streams/settings.json on a running bot risks being overwritten by Red's clean-shutdown save. Always:

systemctl stop red@<instance>.service
# ...back up (cp settings.json settings.json.bak-<what>) then edit with python...
systemctl start red@<instance>.service

Prune pattern (remove a set of dead channel IDs everywhere, drop now-empty entries):

DEAD = {403344034778578949, 514149407281905676, ...}
streams = root["streams"]
kept = []
for s in streams:
    s["channels"] = [c for c in s.get("channels", []) if int(c) not in DEAD]
    if s["channels"]:                 # drop entries left with no channels
        kept.append(s)
root["streams"] = kept

Verify after restart: re-read the file and assert zero dead refs remain and zero channel-less entries exist. Confirm the bot reconnected (journalctl -u red@<instance>.service | grep "Connected to Discord").

Rollback

Every mutating step writes a timestamped backup beside the file (.bak-<what>). To revert: stop the bot, copy the backup over the live file, start the bot. To remove the cog: ~unload streamcheck and delete streamcheck from packages.

Gotchas

  • Edit while stopped, or lose the change to the shutdown save.
  • GUILD-scope keys undercount servers — derive the real server list from live channels.
  • get_channel is cache-only — None ≈ deleted/left, not necessarily an error.
  • Streams config is not chat history — you can audit announcements, never message mentions.