char-slop/city-role-bot

discord auto role applicator bot for charlotte polycule city

git clone https://git.t4t.associates/char-slop/city-role-bot

Charlotte Somadd tomato to starboardf13db32

main
6.3 KiB121 linesraw

City role bot

Starboard

The starboard is configured in src/starboard.ts for guild 1244870846556803074, posting to channel 1559594091497660507.

Messages qualify when five unique people react with any combination of ⭐, 🔥, 😭, and 🍅. Each person counts once across all four emoji and normal/super reactions. Bot votes and the original author's votes do not count. Reposts show separate counts, such as ⭐ 3 🔥 5 😭 4, using unique eligible people per emoji. Zero-count emoji are omitted. The reaction summary is followed on the next line by the raw, unmasked original-message URL, which Discord can render as a message link. There is no separate channel mention. These counts can overlap; qualification still uses five unique people across all eligible emoji, not the sum of the displayed counts.

All readable message-bearing channels and accessible threads are eligible, including archived threads. Current and previously used starboard destinations and their threads are excluded to avoid reposting old highlights.

Set STARBOARD_EXCLUDED_CHANNEL_IDS in .env to a comma-separated list of source channel IDs, then restart the bot. It defaults to 1308583779157151804; an explicitly empty value clears the configurable exclusions. Exclusions also apply to child channels/threads, live reactions, and candidates already in the saved backfill queue. Existing Discord reposts are not deleted automatically. /starboard status shows the configured list.

Restricted-channel messages can be copied into the destination, so give the bot access only to channels whose messages may be shared there.

Reposts include author attribution, text, the first non-spoiler image attachment, attachment links, individual emoji counts, and a link to the original. Replies also include the replied-to author's name, a short text preview, and a link to that message in an embed field. Deleted or inaccessible reply targets display an unavailable notice without preventing the reply from being reposted.

Rich embeds, stickers, polls, and forwarded-message snapshots are not reconstructed; use the original link for those. Text, reply previews, and attachment lists are bounded by Discord's embed limits. Spoiler images are not displayed automatically.

Reposts remain if their score falls below five, and update when reactions or the original text/attachments change. Deleting the original removes its repost. Missing access does not count as deletion. Manually deleting a repost can result in its recreation when the original is next reconciled.

Discord setup

Enable Message Content Intent in the Developer Portal. The bot requests it along with guild message and reaction events.

In source channels it needs View Channel and Read Message History. In the destination it also needs Send Messages and Embed Links. It can enumerate joined archived private threads without Manage Threads; enumerating other archived private threads requires Manage Threads. Discord still determines which history the bot may read. Deleted messages and inaccessible history cannot be recovered.

Backfill

On first startup, the bot starts a full-history scan with no age cutoff. It paginates messages and public/private archived thread lists, respects Discord's rate limits, and pauses briefly between pages to leave room for live events.

Backfill has two resumable phases:

  1. Collect: scan every accessible source, saving potentially qualifying message IDs and source channel IDs without creating reposts.
  2. Post: sort candidates by creation order across the whole server, oldest first, then fetch each original again and check its current unique-person eligibility before posting.

Full message contents are not retained in the queue. New live candidates wait while backfill runs, including while it is paused. Candidates arriving during collection join the history queue; those arriving during posting wait until the history queue has been processed. Existing reposts can still update or be deleted.

Mappings and scan checkpoints are saved atomically in data/starboard.json. Restarts reconcile existing reposts and resume unfinished scans. Connection failures, timeouts, and server errors are retried with exponential backoff (1, 2, 4, and 8 seconds). If retries are exhausted, backfill pauses without advancing its current page or queued candidate. Permission errors are not retried; inaccessible history or thread-list endpoints are skipped and reported in logs and status. Recent send retries also use Discord nonce deduplication. Run only one bot process against this data directory, and retain it across deployments.

Changing the configured destination archives the previous destination's mappings locally. The new destination reuses the collected history: already-posted originals, remaining history candidates, and waiting live candidates are merged and posted oldest-first. If collection was still running, its cursors are preserved. Old Discord reposts are not edited or deleted. Originals are fetched again and must still qualify before being posted to the new destination.

Legacy state without a destination ID belongs to the original channel 1559288341717844088. State from before the two-phase backfill requires a fresh history scan because it did not retain a candidate queue.

Server admins with Manage Server can use:

  • /starboard status — settings, repost count, scan progress, and failures.
  • /starboard backfill — resume a paused scan; after a completed scan, start a fresh full-history scan without duplicating tracked reposts.

A completed scan does not automatically restart on every boot. Reactions added while the bot was offline to previously untracked messages require another full scan, or a later live reaction event on those messages. Run backfill again after adding access to previously skipped channels.

Development

pnpm-workspace.yaml applies the checked-in Oceanic 1.14.0 patch in patches/. It fixes transport failures leaving REST promises unresolved and clears their request timers. Client errors are logged; starboard operations can then retry instead of crashing or hanging. Install dependencies with pnpm so this patch is applied.

  • pnpm check — type-check source and tests.
  • pnpm test — offline starboard tests (does not connect to Discord).
  • pnpm start — start the bot using .env.
1# City role bot
2
3## Starboard
4
5The starboard is configured in `src/starboard.ts` for guild
6`1244870846556803074`, posting to channel `1559594091497660507`.
7
8Messages qualify when **five unique people** react with any combination of
9⭐, 🔥, 😭, and 🍅. Each person counts once across all four emoji and normal/super
10reactions. Bot votes and the original author's votes do not count. Reposts show
11separate counts, such as `⭐ 3  🔥 5  😭 4`, using unique eligible people per
12emoji. Zero-count emoji are omitted. The reaction summary is followed on the
13next line by the raw, unmasked original-message URL, which Discord can render
14as a message link. There is no separate channel mention. These counts can overlap;
15qualification still uses five unique people across all eligible emoji, not the
16sum of the displayed counts.
17
18All readable message-bearing channels and accessible threads are eligible,
19including archived threads. Current and previously used starboard destinations
20and their threads are excluded to avoid reposting old highlights.
21
22Set `STARBOARD_EXCLUDED_CHANNEL_IDS` in `.env` to a comma-separated list of
23source channel IDs, then restart the bot. It defaults to `1308583779157151804`;
24an explicitly empty value clears the configurable exclusions. Exclusions also
25apply to child channels/threads, live reactions, and candidates already in the
26saved backfill queue. Existing Discord reposts are not deleted automatically.
27`/starboard status` shows the configured list.
28
29**Restricted-channel messages can be copied into the destination**, so give the
30bot access only to channels whose messages may be shared there.
31
32Reposts include author attribution, text, the first non-spoiler image attachment,
33attachment links, individual emoji counts, and a link to the original. Replies
34also include the replied-to author's name, a short text preview, and a link to
35that message in an embed field. Deleted or inaccessible reply targets display an
36unavailable notice without preventing the reply from being reposted.
37
38Rich embeds, stickers, polls, and forwarded-message snapshots are not
39reconstructed; use the original link for those. Text, reply previews, and
40attachment lists are bounded by Discord's embed limits. Spoiler images are not
41displayed automatically.
42
43Reposts remain if their score falls below five, and update when reactions or the
44original text/attachments change. Deleting the original removes its repost.
45Missing access does not count as deletion. Manually deleting a repost can result
46in its recreation when the original is next reconciled.
47
48### Discord setup
49
50Enable **Message Content Intent** in the Developer Portal. The bot requests it
51along with guild message and reaction events.
52
53In source channels it needs **View Channel** and **Read Message History**. In the
54destination it also needs **Send Messages** and **Embed Links**. It can enumerate
55joined archived private threads without Manage Threads; enumerating other
56archived private threads requires **Manage Threads**. Discord still determines
57which history the bot may read. Deleted messages and inaccessible history cannot
58be recovered.
59
60### Backfill
61
62On first startup, the bot starts a full-history scan with no age cutoff. It
63paginates messages and public/private archived thread lists, respects Discord's
64rate limits, and pauses briefly between pages to leave room for live events.
65
66Backfill has two resumable phases:
67
681. **Collect:** scan every accessible source, saving potentially qualifying
69   message IDs and source channel IDs without creating reposts.
702. **Post:** sort candidates by creation order across the whole server, oldest
71   first, then fetch each original again and check its current unique-person
72   eligibility before posting.
73
74Full message contents are not retained in the queue. New live candidates wait
75while backfill runs, including while it is paused. Candidates arriving during
76collection join the history queue; those arriving during posting wait until the
77history queue has been processed. Existing reposts can still update or be deleted.
78
79Mappings and scan checkpoints are saved atomically in `data/starboard.json`.
80Restarts reconcile existing reposts and resume unfinished scans. Connection
81failures, timeouts, and server errors are retried with exponential backoff
82(1, 2, 4, and 8 seconds). If retries are exhausted, backfill pauses without
83advancing its current page or queued candidate. Permission errors are not
84retried; inaccessible history or thread-list endpoints are skipped and reported
85in logs and status. Recent send retries also use Discord nonce deduplication. Run only
86one bot process
87against this data directory, and retain it across deployments.
88
89Changing the configured destination archives the previous destination's mappings
90locally. The new destination reuses the collected history: already-posted
91originals, remaining history candidates, and waiting live candidates are merged
92and posted oldest-first. If collection was still running, its cursors are
93preserved. Old Discord reposts are not edited or deleted. Originals are fetched
94again and must still qualify before being posted to the new destination.
95
96Legacy state without a destination ID belongs to the original channel
97`1559288341717844088`. State from before the two-phase backfill requires a fresh
98history scan because it did not retain a candidate queue.
99
100Server admins with **Manage Server** can use:
101
102- `/starboard status` — settings, repost count, scan progress, and failures.
103- `/starboard backfill` — resume a paused scan; after a completed scan, start a
104  fresh full-history scan without duplicating tracked reposts.
105
106A completed scan does not automatically restart on every boot. Reactions added
107while the bot was offline to previously untracked messages require another full
108scan, or a later live reaction event on those messages. Run backfill again after
109adding access to previously skipped channels.
110
111### Development
112
113`pnpm-workspace.yaml` applies the checked-in Oceanic 1.14.0 patch in `patches/`.
114It fixes transport failures leaving REST promises unresolved and clears their
115request timers. Client errors are logged; starboard operations can then retry
116instead of crashing or hanging. Install dependencies with pnpm so this patch
117is applied.
118
119- `pnpm check` — type-check source and tests.
120- `pnpm test` — offline starboard tests (does not connect to Discord).
121- `pnpm start` — start the bot using `.env`.