Skip to content

Integrate Twitch API for ad scheduling and enhance widget support#668

Open
saebyn wants to merge 4 commits into
mainfrom
twitch-ads-backend
Open

Integrate Twitch API for ad scheduling and enhance widget support#668
saebyn wants to merge 4 commits into
mainfrom
twitch-ads-backend

Conversation

@saebyn
Copy link
Copy Markdown
Owner

@saebyn saebyn commented Mar 29, 2026

  • Understand review comments and plan changes
  • Fix AppContext::new to panic on missing/invalid Twitch secret (fail fast)
  • Fix deserialize_widget to deserialize user_id from DynamoDB
  • Fix compute_new_state to return None when state unchanged (avoid unnecessary writes)
  • Add Twitch /validate call to get real broadcaster_id (not Cognito sub)
  • Move TwitchValidateResponse to module level for reusability
  • Add 7 unit tests for compute_new_state (nextAdAt, snooze, backFromAdsUntil, no-op)
  • Replace Scan with paginated Query on user_id-index GSI in chat_processor
  • Remove redundant !start_key.is_empty() check in pagination loop
  • Fix UpdateItem to update only specific nested state keys (preserve snooze fields)
  • Reduce update expression duplication using base_update pattern
  • Fix create_eventsub_subscription to return None for 409 (not "already_exists")
  • Fix subscribe handler to pass Option<String> subscription_id correctly

⚡ Quickly spin up Copilot coding agent tasks from anywhere on your macOS or Windows machine with Raycast.

Copilot AI review requested due to automatic review settings March 29, 2026 21:51
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an ad_timer widget type and wires Twitch ad-break events + periodic polling into the system so widget state can reflect ad breaks in near real time.

Changes:

  • Extend StreamWidget schema/types to include ad_timer.
  • Add a new widget updater path that polls Twitch’s ad schedule API and writes widget state updates.
  • Subscribe to channel.ad_break.begin via EventSub and update matching widgets immediately; update CDK to pass secrets and permissions.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
widget_updater_lambda/src/updaters/mod.rs Exposes the new ad_timer updater module.
widget_updater_lambda/src/updaters/ad_timer.rs Implements Twitch ad schedule polling + state computation for ad_timer widgets.
widget_updater_lambda/src/main.rs Adds Secrets Manager + HTTP client wiring and routes ad_timer updates through async logic.
widget_updater_lambda/Cargo.toml Adds dependencies needed for Twitch polling (Secrets Manager, reqwest, gt_secrets).
chat_processor_lambda/src/main.rs Handles channel.ad_break.begin events and writes immediate widget state changes to DynamoDB.
twitch_lambda/src/handlers.rs Refactors EventSub subscription creation and adds ad-break EventSub subscription.
cdk/lib/widgetUpdater.ts Passes Twitch secret info to widget-updater lambda, grants permissions, and schedules ad_timer polling.
cdk/lib/twitchChatProcessing.ts Injects STREAM_WIDGETS_TABLE and grants DynamoDB permissions for widget updates.
cdk/lib/appStack.ts Wires streamWidgetsTable into chat processing and passes Twitch secret props to widget updater.
cdk/lib/api.ts Exposes twitchAppSecret for downstream constructs.
docs/v2/schemas/StreamWidget.schema Adds ad_timer to the widget type enum.
types/src/types.ts Adds ad_timer to the StreamWidgetType union.
types/src/types.rs Adds StreamWidgetType::AdTimer and minor formatting adjustments.
Cargo.lock Locks new Rust dependencies for widget_updater_lambda.

Comment thread widget_updater_lambda/src/main.rs Outdated
Comment thread widget_updater_lambda/src/updaters/ad_timer.rs
Comment thread widget_updater_lambda/src/updaters/ad_timer.rs Outdated
Comment thread chat_processor_lambda/src/main.rs Outdated
Comment thread chat_processor_lambda/src/main.rs Outdated
Comment thread twitch_lambda/src/handlers.rs
Comment thread widget_updater_lambda/src/main.rs
Comment thread chat_processor_lambda/src/main.rs Outdated
Comment thread twitch_lambda/src/handlers.rs
Comment thread widget_updater_lambda/src/updaters/ad_timer.rs
@saebyn
Copy link
Copy Markdown
Owner Author

saebyn commented Mar 30, 2026

@copilot apply changes based on the comments in this thread

…updates, token validation

Agent-Logs-Url: https://github.com/saebyn/glowing-telegram/sessions/2fe3d96f-3907-4984-bb6c-888603a5b4fe

Co-authored-by: saebyn <185030+saebyn@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 30, 2026

@copilot apply changes based on the comments in this thread

Applied all changes from the review thread in commit 7d3c10e:

  1. Fail fast on missing Twitch secretAppContext::new now panics with a descriptive message if the secret cannot be read, parsed, or has an empty id field.
  2. Deserialize user_id from DynamoDBdeserialize_widget now reads user_id from the item instead of hardcoding an empty string.
  3. Correct broadcaster_id via token validationprocess_widget calls the Twitch OAuth /validate endpoint to derive the real Twitch user_id / broadcaster ID, rather than using widget.user_id (a Cognito sub).
  4. compute_new_state returns None when unchanged — added field-by-field comparison; only returns Some when at least one field changed.
  5. Unit tests for compute_new_state — added 7 deterministic tests covering nextAdAt set/cleared, snooze detection, backFromAdsUntil behavior, and the no-op path.
  6. Query on user_id-index GSI with pagination — replaced the full-table Scan with a paginated Query using the existing GSI.
  7. Targeted nested state updatesUpdateItem now uses SET #state.#nextAdAt / #state.#backFromAdsUntil instead of replacing the entire state map, preserving snooze fields.
  8. create_eventsub_subscription returns Option<String> — HTTP 409 now returns Ok(None) instead of Ok("already_exists"), preventing the placeholder string from leaking as a subscription ID.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated 4 comments.

Comment on lines +221 to +240
// Query the user_id-index GSI to efficiently find active ad_timer widgets
// for this broadcaster, and paginate through all results.
let mut items: Vec<HashMap<String, AttributeValue>> = Vec::new();
let mut exclusive_start_key: Option<HashMap<String, AttributeValue>> = None;

loop {
let mut query_builder = context
.dynamodb
.query()
.table_name(&context.config.stream_widgets_table)
.index_name("user_id-index")
.key_condition_expression("#user_id = :broadcaster_id")
.filter_expression("#type = :widget_type AND #active = :active")
.expression_attribute_names("#user_id", "user_id")
.expression_attribute_names("#type", "type")
.expression_attribute_names("#active", "active")
.expression_attribute_values(
":broadcaster_id",
AttributeValue::S(event.broadcaster_user_id.clone()),
)
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user_id-index is described as the primary access pattern for “get all widgets for a user” (i.e., the widget owner). Here the query uses event.broadcaster_user_id (a Twitch broadcaster ID) as user_id, but elsewhere in this PR the widget user_id is treated as a Cognito user id (used for per-user Secrets Manager paths). As written, this query will not find any widgets, so the ad-break push update will never run. Consider storing the Twitch broadcaster id on the widget item (e.g., twitch_broadcaster_id) and querying on a dedicated GSI, or otherwise mapping Twitch broadcaster_id -> Cognito user_id before querying/updating widgets.

Copilot uses AI. Check for mistakes.
Comment on lines +212 to +219
// Compute when the ad break ends so the frontend can derive status correctly.
let back_from_ads_until =
chrono::DateTime::parse_from_rfc3339(&event.started_at)
.ok()
.map(|t| {
(t + chrono::Duration::seconds(event.duration_seconds))
.to_rfc3339()
});
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

backFromAdsUntil is computed here as “ad break ends at” (started_at + duration_seconds), but in the widget updater the same field is documented/used as “until when to show the 'Back from Ads' message after the break ends” (i.e., a post-break window). These conflicting semantics will make the state hard to reason about and can cause the frontend to show the wrong status depending on which writer last updated the field. Align on one meaning (or introduce separate fields such as adBreakEndsAt vs backFromAdsUntil).

Copilot uses AI. Check for mistakes.
Comment on lines +246 to +248
.cloned()
.unwrap_or_default();

Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user_id is required for ad_timer updates (Secrets Manager lookup uses it to build the secret path), but missing user_id is silently converted to an empty string here. That can hide data issues and leads to confusing downstream errors (secret path becomes .../). Consider returning an error when user_id is missing so the widget is surfaced as malformed during deserialization.

Suggested change
.cloned()
.unwrap_or_default();
.cloned();
let user_id = match (&stream_widget_type, user_id) {
(StreamWidgetType::AdTimer, Some(id)) if !id.is_empty() => id,
(StreamWidgetType::AdTimer, _) => {
return Err("Missing user_id for ad_timer widget".into());
}
(_, Some(id)) => id,
(_, None) => String::new(),
};

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +92
actions: [
'dynamodb:Scan',
'dynamodb:UpdateItem',
'dynamodb:GetItem',
],
resources: [
props.streamWidgetsTable.tableArn,
`${props.streamWidgetsTable.tableArn}/index/*`,
],
}),
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The chat processor now calls DynamoDB Query (including on the user_id-index GSI) to find ad_timer widgets, but this IAM policy does not grant dynamodb:Query. This will fail at runtime with AccessDenied. Add dynamodb:Query (and you can likely drop dynamodb:Scan if it’s no longer used).

Copilot uses AI. Check for mistakes.
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.

3 participants