Skip to content

feat(kick): fix campaign discoverability — open-campaign directory fallback + shell rows (#60) - #62

Open
steethebutcher wants to merge 4 commits into
aalejandrofer:masterfrom
steethebutcher:feat/kick-campaign-discoverability
Open

steethebutcher wants to merge 4 commits into
aalejandrofer:masterfrom
steethebutcher:feat/kick-campaign-discoverability

Conversation

@steethebutcher

Copy link
Copy Markdown
Contributor

Fixes the Kick half of #60 ("even if I add kick campaigns like WoW Forever it won't mine it, and I must add whitelist channel like rewardstation to mine the wow drop").

Root causes (two independent defects)

1. Open campaigns had no channel candidates. Most Kick campaigns are open (channels: [] — accrue on any live channel in the category). ListEligibleChannels only considered the campaign's own channels, a category pool built exclusively from sibling campaigns that embed channels, and manual operator channels. WoW Forever is the only WoW campaign, so the pool was empty → "no eligible streams live" forever → unmineable without a manual channel registration.

2. Non-whitelisted campaigns were invisible (deadlock). The backend dropped them (GameFilter continue) and the scraper re-dropped them, so they never reached the campaigns table or the /drops Discoverable tab. A user couldn't see "WoW Forever" to whitelist it — the only way in was adding the game blind on /priority. Twitch deliberately avoids this deadlock with shell rows; Kick now does too.

Commits

1. feat(kick): directory fallback for open campaigns with no channel pool

When an open campaign's pooled candidates yield nothing live, consult the public category directory, then merge + probe:

  • Parses the campaign payload's authoritative game_slug (previously discarded) so the directory is addressed correctly instead of re-deriving the slug from the display name (cf. the Twitch slug bugs in "watcher no eligible streams live" logged despite active Twitch streams with drops for campaigns #61).
  • Directory results are viewer-sorted and merged into categoryChannels so later picks work even if the feed goes away.
  • Every candidate still passes probeLive's live + on-category verification — a wrong slug can only produce zero results, never the junk picks that got the old generic feed removed.
  • Restricted campaigns (own channels[]) never consult the directory.

2. feat(kick): emit shell rows for non-whitelisted campaigns

GameFilter becomes a should-map-benefits gate (Twitch parity, campaigns.go:236): non-whitelisted campaigns keep ID/Game/Name/Status/window/AllowedChannels but carry empty Benefits and skip channel-pool caching. The scraper passes shells through to the persister so they surface in Discoverable + the game picker. Safety is the same mechanism that already makes Twitch shells safe: the watcher's own AllowGame + never-mine-empty-Benefits.

3. fix(kick): directory feed is global + paginated — filter client-side

I live-checked the public /stream/livestreams/{slug} endpoint before wiring it in (2026-10-03) and found the dead-code assumptions wrong:

  • The feed ignores the category slug — rust vs world-of-warcraft return the identical global list (same failure family that got the old feed removed).
  • It paginates (per_page=5, ?page=N), and each entry carries its own categories[] (name+slug) + channel.slug.

So DiscoverChannelsForCategory now walks ≤4 pages and filters client-side by slug or display name (case/space-insensitive — the real feed mixes case, e.g. "Project-Zomboid"). Live-verified by simulation against the production feed: a 4-page scan extracted categories correctly and the filter matched channels for a category present in the window. Combined with probeLive's independent verification, junk picks remain impossible whether or not Kick restores per-category filtering.

Known limitations (deliberate)

  • Directory discovery is progressive, not exhaustive. The window is 4 pages × 5 entries of a global feed; a category absent from this cycle's window gets picked up on a later scan as the feed shifts. Fine for mining cadence (retried every pick cycle), documented here so nobody expects instant results for niche categories.
  • /api/v1/drops/campaigns has no pagination handling — if Kick ever pages that endpoint, campaigns would silently truncate. Unverifiable without a live authed response; flagging for awareness.
  • No new config knobs — the fallback is automatic and self-verifying.

Verification

  • go build ./..., go vet ./... clean; full test suite green (24 packages).
  • 12 new/updated tests: directory happy path (with off-category entry filtered pre-probe), payload-slug precedence, wrong-category rejection at both layers, restricted-campaign exclusion, pool persistence across picks, shell-row contract ×2, scraper pass-through.
  • Directory endpoint shape live-verified via curl + parsing simulation (details in commit 3).
  • Still needs a live mining verification on a deployment with a Kick account: WoW Forever should appear in Discoverable, whitelist it, and mine without manual channel registration — happy to run that before merge if you want (same gate the Spade PR used).

Out of scope

Twitch slug derivation (#61 — JourneyOver's fixes in flight), device-code login (#48), pipeline redesign (#50).

Most Kick campaigns are open (channels: []) — they accrue on any live
channel in the category. ListEligibleChannels only considered the
campaign's own channels, a category pool built exclusively from sibling
campaigns that embed channels, and manual operator channels. When no
sibling campaign carries channels (e.g. "WoW Forever", the only WoW
campaign), the pool was empty, the watcher logged "no eligible streams
live" forever, and the campaign was unmineable without registering a
channel manually (aalejandrofer#60).

Wire the public /stream/livestreams/{category} directory back in as the
open-campaign fallback — behind probeLive's live + on-category
verification, which makes the old failure mode (the generic feed's
wrong-game junk picks) impossible to regress: a wrong slug now yields
zero results, never junk. Restricted campaigns never consult the
directory.

- api: parse the campaign payload's authoritative category slug
  (game_slug/category_slug sibling or nested game.slug) into the new
  kickCampaign.GameSlug — previously discarded, so the directory was
  addressed with a name-derived slug.
- backend: categorySlugs cache (game -> payload slug); the fallback
  resolves payload slug first, then gameslug.Slug(name). Directory
  results are viewer-sorted (probeLive's cap lands on the biggest,
  most stable streams) and merged into categoryChannels so later picks
  work even if the feed goes away.

Tests: happy path, payload-slug precedence, wrong-category rejection,
restricted-campaign exclusion, and pool persistence across picks.
Non-whitelisted Kick campaigns were dropped twice — inside the backend's
ListActiveCampaigns (GameFilter continue) and again by the discovery
scraper's re-filter — so they never reached the campaigns table or the
/drops Discoverable tab. A user couldn't see a campaign like "WoW
Forever" to whitelist it; the only way in was adding the game blind on
/priority. Twitch deliberately avoids this deadlock by emitting shell
rows; now Kick does too.

- backend: GameFilter is now a should-map-benefits gate instead of a
  drop-the-campaign gate. Non-whitelisted campaigns keep ID/Game/Name/
  Status/window/link-state/AllowedChannels but carry EMPTY Benefits and
  skip the channel-pool caching. The watcher's own AllowGame (applied
  after ListActiveCampaigns) plus its never-mine-empty-Benefits rule
  keep shells out of mining — the same mechanism that already makes
  Twitch shells safe.
- discovery/kick: stop re-applying the whitelist; pass everything the
  backend returned (shells included) to the persister so they surface
  in Discoverable and the game picker. Also refresh the stale
  sidecar/Cloudflare header comments — Kick data has been pure-HTTP
  utls since the kick-breakthrough rework.

Tests: MapsAndFilters updated to the shell contract (both campaigns
emitted; non-whitelisted has empty Benefits + intact status), a new
ShellRowsSkipChannelPools guard, and a scraper pass-through test.
Live-checking the public /stream/livestreams/{slug} endpoint (2026-10-03)
showed two things the dead-code implementation assumed wrong:

1. The feed IGNORES the {category} path segment — querying rust vs
   world-of-warcraft returns the identical global list (and next_page_url
   even leaks an unrelated default category). This is the same
   wrong-game-junk failure mode that got the old generic feed removed
   from channel selection.
2. It is paginated: per_page=5, ?page=N works, entries carry their own
   categories[] (name+slug) alongside channel.slug.

So DiscoverChannelsForCategory now walks up to 4 pages and filters
client-side — an entry qualifies when any embedded category matches the
wanted slug or the campaign's display name (case/space-insensitive;
real feed even mixes case, e.g. "Project-Zomboid"). Correct whether or
not Kick restores per-category filtering, and probeLive still hard-
verifies every candidate afterwards, so junk picks remain impossible.

Live-verified by simulation against the production feed: 4-page scan
extracted categories correctly and the filter matched channels for a
category present in the window. The window is a slice of a global feed,
so discovery is best-effort progressive — successive watcher cycles
keep scanning — not exhaustive; noted in the PR.

api_live_test updated to the new signature with a note that zero
results may just mean the category wasn't live in the scanned window.

This branch has not been deployed

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

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

2 participants