robo.js icon indicating copy to clipboard operation
robo.js copied to clipboard

@robojs/social — Social Alerts Plugin (Twitch, YouTube, RSS/Atom)

Open Pkmmte opened this issue 2 months ago • 2 comments

Summary

A Robo.js plugin that alerts Discord servers when creators go live or publish new content.

@robojs/social lets server admins subscribe to creators and feeds on Twitch, YouTube, and RSS/Atom and automatically post rich announcements into Discord. It supports slash commands for setup, cron-based polling (MVP), and optional webhooks (Twitch EventSub) when @robojs/server is available. It stores per‑guild settings durably and exposes both a slash and an imperative API so teams can build dashboards later.

Goals

  • Simple “add a creator → get alerts” flow for Twitch live, YouTube uploads/lives, and RSS/Atom updates.
  • Per‑guild configuration (target channel, optional role mention, templates), durable storage, and safe defaults.
  • MVP uses polling via @robojs/cron. Optional: Twitch EventSub webhooks via @robojs/server.
  • Clean, readable embeds that respect Discord limits and optionally publish in Announcement channels when permitted.
  • Encouraged but not required: tests, i18n, analytics hooks.

Non‑Goals

  • Music streaming or content scraping.
  • A full web dashboard in this PR (we expose APIs so others can build one later).

User Stories

  • As a server admin, I can run /social add twitch <channel> and get a message when the streamer goes live.
  • As a content team, I can subscribe to a YouTube channel and post a templated embed on publish (title, thumbnail, link).
  • As a community manager, I can follow RSS/Atom feeds and post trimmed summaries with links.
  • As a server owner, I can set a default post channel and an optional role mention.
  • As a developer, I can programmatically add/remove subscriptions and update templates via an exported API (for a custom dashboard).

High‑Level Design

Providers (MVP)

  • Twitch

    • Webhook (optional): EventSub for stream.online/stream.offline when @robojs/server is present.
    • Polling (fallback): Helix endpoints using an app token to detect live status.
  • YouTube

    • Polling: YouTube Data / Live Streaming API to detect new uploads or ongoing live broadcasts. Be mindful of quota (e.g., search.list cost) and cache aggressively.
  • RSS/Atom

    • Polling: Fetch and parse feeds; dedupe by GUID or link+timestamp using a Node parser (e.g., rss-parser).

Announcement channels: if posting into an Announcement channel, try to Publish (crosspost) when permissions allow; otherwise, post normally.

Slash Command API (root: social)

All commands live under /social …:

  • /social add twitch <channelNameOrId> [#channel]
  • /social add youtube <channelUrlOrId> [#channel]
  • /social add feed <url> [#channel]
  • /social remove <subscriptionId>
  • /social list [provider] (paginated)
  • /social set-channel <subscriptionId|default> <#channel>
  • /social set-role <subscriptionId|default> <@role|none>
  • /social set-template <subscriptionId|provider|default> <preset|json|reset>
  • /social test <subscriptionId|provider> (posts a sample embed)
  • /social settings (ephemeral summary for the guild)

Behavior notes

  • Mentions: store an optional role ID to ping on posts.
  • Channel overrides: per‑subscription override or guild default.
  • Templates: see “Templating” below (presets + validated JSON).
  • Anti‑spam: per‑guild throttling/cooldown to avoid bursts.

Imperative API (for dashboards & advanced usage)

// ESM export from @robojs/social
import { Social } from '@robojs/social'

export type Provider = 'twitch' | 'youtube' | 'feed'

export interface AddOptions {
  guildId: string
  provider: Provider
  target: string            // twitch channel id/name, youtube channel id/url, feed URL
  channelId?: string        // where to post; falls back to guild default
  roleId?: string | null    // role to mention (optional)
  templateKey?: string      // 'default' | 'compact' | 'minimal' | custom key
}

export interface TemplateUpsert {
  scope: { guildId: string, provider?: Provider, subscriptionId?: string }
  template: Record<string, unknown> // validated JSON-ish embed model
}

await Social.addSubscription(options: AddOptions): Promise<string /* subscriptionId */>
await Social.removeSubscription(subscriptionId: string): Promise<void>

await Social.setDefaultChannel(guildId: string, channelId: string): Promise<void>
await Social.setDefaultRole(guildId: string, roleId: string | null): Promise<void>

await Social.upsertTemplate(input: TemplateUpsert): Promise<void>
await Social.getSettings(guildId: string): Promise<GuildSettings>
await Social.listSubscriptions(
  guildId: string,
  filter?: { provider?: Provider }
): Promise<Subscription[]>
  • All setters persist via Flashcore (see “Persistence & Data Model”).
  • If @robojs/server is installed, the plugin can also expose REST routes (see “Optional server integration”).

Templating & Embeds

Presets (built‑in): default, compact, minimal — per provider (Twitch/YouTube/feed).

Custom templates (JSON example)

{
  "title": "${broadcasterName} is LIVE!",
  "description": "${title}",
  "url": "${url}",
  "thumbnail": "${thumbnail}",
  "footer": "Started at ${startedAt}"
}
{
  "title": "New video: ${title}",
  "description": "Watch now → ${url}",
  "thumbnail": "${thumbnail}",
  "footer": "Channel: ${channelTitle} • ${publishedAt}"
}
{
  "title": "${title}",
  "description": "${summary}",
  "url": "${url}",
  "footer": "Published ${publishedAt} • ${sourceTitle}"
}

Validation & guardrails

  • Validate custom JSON and truncate fields to Discord embed limits (title, description, total length, etc.).
  • Replace unknown template variables with empty strings and log a warning.
  • Always include a plain link fallback if the embed fails validation.

Features

  • Subscriptions: Add/remove/list Twitch, YouTube, and RSS/Atom sources.
  • Posting: Send embeds to a configured or default channel; optional role mention.
  • Deduplication: Track lastSeenId per subscription to avoid reposts.
  • Throttling: Per‑guild cooldown windows (e.g., max 1 alert per X seconds per provider).
  • Templating: Switch presets or provide raw JSON (validated & sanitized) with fallbacks.
  • Testing: /social test to preview message appearance.
  • Persistence: All settings stored in Flashcore, namespaced by guild.
  • Scheduling: Register periodic jobs via @robojs/cron.
  • (Optional) Webhooks: Twitch EventSub when @robojs/server is present for near‑real‑time delivery and fewer API calls.
  • (Optional) Portal & Analytics: Add Portal toggles; fire custom analytics events if an analytics plugin is installed.
  • (Optional) i18n: Wire copy through @robojs/i18n.

Persistence & Data Model

Storage: Flashcore (durable KV). Consider Keyv adapters later. Namespace = guildId.

// KV shapes
export interface GuildSettings {
  defaultChannelId?: string
  defaultRoleId?: string | null
  throttleMs?: number
  templates: {
    default?: Template
    twitch?: Template
    youtube?: Template
    feed?: Template
    bySubscriptionId: Record<string, Template>
  }
}

export interface Subscription {
  id: string
  guildId: string
  provider: 'twitch' | 'youtube' | 'feed'
  target: string                 // e.g., twitch broadcaster_id, youtube channelId, feed URL
  channelId?: string
  roleId?: string | null
  isActive: boolean
  lastSeen: { id?: string; at?: number }
  meta?: Record<string, unknown> // provider‑specific
}

Architecture

File Layout (plugin)

@robojs/social/
  package.json
  src/
    commands/social/
      add.ts
      remove.ts
      list.ts
      set-channel.ts
      set-role.ts
      set-template.ts
      test.ts
      settings.ts
    events/
      ready.ts                 // boot polling / webhooks
    providers/
      twitch.ts
      youtube.ts
      feed.ts
      types.ts
    lib/
      storage.ts               // Flashcore wrappers (ns = guildId)
      templates.ts             // presets, sanitization, embed builders
      posting.ts               // send + optional crosspost
      dedupe.ts                // lastSeen helpers
      throttle.ts              // per‑guild provider throttle
      cron.ts                  // register jobs via @robojs/cron
      server.ts                // (optional) webhook routes via @robojs/server
      validate.ts              // zod (or similar) schemas for inputs
    index.ts                   // imperative API exports

Provider Adapter Interface

export interface ProviderAdapter {
  id: 'twitch' | 'youtube' | 'feed'
  displayName: string

  init(ctx: InitContext): Promise<void>
  poll(sub: Subscription): Promise<Alert[]>

  webhook?: {
    subscribe(sub: Subscription): Promise<void>
    unsubscribe(sub: Subscription): Promise<void>
    handler(req: Request, body: unknown): Promise<Alert[]>
  }

  dedupeKey(a: Alert): string
}

Ready Event & Scheduling

  • On ready, detect installed plugins and register cron jobs (e.g., Twitch/YouTube every 2–5 min; feeds every 10–15 min; all configurable).
  • If @robojs/server exists, mount webhook routes (e.g., /api/social/twitch/eventsub) and validate signatures before enqueuing alerts.

Posting & Crossposting

  • lib/posting.ts builds embeds from templates (with truncation), posts to the target channel, and attempts crosspost when the channel is an Announcement channel and the bot has permission.

Provider Details

Twitch

  • Webhook: EventSub for stream.online / stream.offline; requires public callback.
  • Polling: Use Helix endpoints with app access token; batch requests sensibly.
  • Normalized fields: { type: 'live', title, url, thumbnail, startedAt, broadcasterName, game }.

YouTube

  • Polling: Use YouTube Data / Live Streaming API to detect uploads/live; cache results and be mindful of quota.
  • Normalized fields: { type: 'upload' | 'live', title, url, thumbnail, publishedAt, channelTitle }.

RSS/Atom

  • Polling: Fetch and parse feeds (RSS 2.0/Atom); dedupe using GUID or link+timestamp.
  • Normalized fields: { type: 'post', title, url, summary, publishedAt, sourceTitle }.

Security & Permissions

  • Discord: The bot needs permissions to send messages, embed links, and optionally Publish messages in Announcement channels.
  • Embeds: Enforce size limits before sending (title, description, fields, total length). Fallback to plain link if necessary.
  • Tokens: Provider API keys (YouTube, Twitch) come from environment variables.
  • Webhooks (Twitch): Verify signatures per EventSub guidance.

Robo.js Integration Notes (for contributors)

  • Scaffold a plugin and add it to a dev Robo project with CLI commands; follow Robo’s file‑based structure for commands/events.
  • Use Flashcore for durable KV with guild‑scoped namespaces.
  • Schedule polling via @robojs/cron if available; add small, per‑provider intervals with jitter.
  • Consider Portal toggles to enable/disable the module at runtime, and analytics hooks if present.

Error Handling & Resilience

  • Provider errors: backoff & retry (exponential, capped).
  • Quota: YouTube’s searches are costly; cache by channel, use publishedAfter/ETags where applicable.
  • Webhook lifecycle: auto re‑subscribe Twitch EventSub before expiration; keep an “active subs” map.
  • Discord rate limits: obey backoff; coalesce bursts (multiple alerts → consolidated embed when appropriate).
  • Validation: reject malformed templates with a helpful error; always keep a safe default template.

Testing (Encouraged, not required)

  • Unit tests for template truncation, dedupe, throttle, and provider normalizers.
  • Optional integration tests using mocked provider responses.
  • CI is welcome but not required for first release.

Developer Setup

  • Requirements: Node 18+, Discord bot token & client ID, provider API keys (Twitch, YouTube).
  • Scaffold: create a plugin workspace and link it locally to a test Robo project.
  • Run: npx robo dev in the test project, then try /social add … commands.

References

  • Discord: Message/Embed limits; Announcement channel publishing rights.
  • Twitch: EventSub (webhooks), Helix reference.
  • YouTube: Data API v3 & Live Streaming API; quotas & best practices.
  • RSS/Atom: RSS 2.0 and Atom specs; common Node parsers (e.g., rss-parser).

If anything above feels unclear or too large for a single PR, feel free to propose a slice (e.g., “core + RSS only”) and we’ll iterate together. Happy Hacktoberfest! 🎉

Pkmmte avatar Oct 06 '25 06:10 Pkmmte

Hi. Can you assign this to me?

bug-author avatar Oct 08 '25 18:10 bug-author

@bug-author Sure, task has been assigned! Feel free to reach out either here or on our Discord if you have any questions.

Pkmmte avatar Oct 10 '25 08:10 Pkmmte