From dbe2dff91dccd7d4c151c3c550f7b15f7c1fa1dd Mon Sep 17 00:00:00 2001 From: lleadbet Date: Tue, 13 Jul 2021 16:45:14 -0700 Subject: [PATCH 01/36] Minor changes per PR merge. --- .../extension_transaction/transaction_event.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/internal/events/types/extension_transaction/transaction_event.go b/internal/events/types/extension_transaction/transaction_event.go index cebb8bc1..0538be39 100644 --- a/internal/events/types/extension_transaction/transaction_event.go +++ b/internal/events/types/extension_transaction/transaction_event.go @@ -44,6 +44,15 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven if params.Cost == 0 { params.Cost = 100 } + + if params.ItemID == "" { + params.ItemID = "testItemSku" + } + + if params.ItemName == "" { + params.ItemName = "Test Trigger Item from CLI" + } + switch params.Transport { case models.TransportEventSub: body := &models.TransactionEventSubResponse{ @@ -72,8 +81,8 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven UserLogin: "testUser", UserID: params.FromUserID, Product: models.TransactionEventSubProduct{ - Name: "Test Trigger Item from CLI", - Sku: "testItemSku", + Name: params.ItemName, + Sku: params.ItemID, Bits: params.Cost, InDevelopment: true, }, From 1c9add59a161c6bab77c5e5e614d0404ef1df5f1 Mon Sep 17 00:00:00 2001 From: zneix Date: Wed, 14 Jul 2021 22:05:38 +0200 Subject: [PATCH 02/36] Fixed a crash upon trying to use query param with no value --- internal/api/api.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/internal/api/api.go b/internal/api/api.go index 18de4ab5..6fae0d2b 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -56,9 +56,13 @@ func NewRequest(method string, path string, queryParameters []string, body []byt } q := u.Query() - for _, param := range queryParameters { - value := strings.Split(param, "=") - q.Add(value[0], value[1]) + for _, paramStr := range queryParameters { + var value string + param := strings.Split(paramStr, "=") + if len(param) == 2 { + value = param[1] + } + q.Add(param[0], value) } if cursor != "" { From fa52492dbbb3b930b0d072dc8122ed52423c75d6 Mon Sep 17 00:00:00 2001 From: lleadbet Date: Wed, 14 Jul 2021 15:25:51 -0700 Subject: [PATCH 03/36] adding schedules api support --- README.md | 1 + internal/api/api.go | 2 +- internal/database/_schema.sql | 16 +++++++- internal/database/drops.go | 1 + internal/database/init.go | 6 ++- .../mock_api/authentication/authentication.go | 1 + internal/mock_api/endpoints/endpoints.go | 3 ++ internal/mock_api/generate/generate.go | 37 ++++++++++++++++++- internal/mock_api/generate/generate_tools.go | 2 + internal/mock_api/mock_server/server.go | 6 ++- 10 files changed, 69 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e9fe8a17..ef65c73e 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ The CLI currently supports the following products: - [api](./docs/api.md) - [configure](./docs/configure.md) - [event](docs/event.md) +- [mock-api](docs/mock-api.md) - [token](docs/token.md) - [version](docs/version.md) diff --git a/internal/api/api.go b/internal/api/api.go index 18de4ab5..969e2559 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -103,7 +103,7 @@ func NewRequest(method string, path string, queryParameters []string, body []byt } d := data.Data.([]interface{}) - data.Data = append(d, apiResponse.Data) + data.Data = append(d, apiResponse.Data.([]interface{})...) if apiResponse.Pagination == nil || *&apiResponse.Pagination.Cursor == "" { break diff --git a/internal/database/_schema.sql b/internal/database/_schema.sql index b10b2e0c..f77bfa22 100644 --- a/internal/database/_schema.sql +++ b/internal/database/_schema.sql @@ -204,7 +204,8 @@ create table drops_entitlements( benefit_id text not null, timestamp text not null, user_id text not null, - game_id text not null, + game_id text not null, + status text not null default 'CLAIMED', foreign key (user_id) references users(id), foreign key (game_id) references categories(id) ); @@ -289,3 +290,16 @@ create table clips ( foreign key (broadcaster_id) references users(id), foreign key (creator_id) references users(id) ); +create table stream_schedule( + id text not null primary key, + broadcaster_id text not null, + starttime text not null, + endtime text not null, + timezone text not null, + is_vacation boolean not null default false, + is_recurring boolean not null default false, + title text, + category_id text, + foreign key (broadcaster_id) references users(id), + foreign key (category_id) references categories(id) +); \ No newline at end of file diff --git a/internal/database/drops.go b/internal/database/drops.go index dffbd5ef..dcf53759 100644 --- a/internal/database/drops.go +++ b/internal/database/drops.go @@ -10,6 +10,7 @@ type DropsEntitlement struct { BenefitID string `db:"benefit_id" json:"benefit_id"` GameID string `db:"game_id" json:"game_id"` Timestamp string `db:"timestamp" json:"timestamp"` + Status string `db:"status" json:"fulfillment_status"` } func (q *Query) GetDropsEntitlements(de DropsEntitlement) (*DBResponse, error) { diff --git a/internal/database/init.go b/internal/database/init.go index 04967c2d..20828f7e 100644 --- a/internal/database/init.go +++ b/internal/database/init.go @@ -30,6 +30,10 @@ var migrateSQL = map[int]migrateMap{ SQL: `create table categories( id text not null primary key, category_name text not null ); create table users( id text not null primary key, user_login text not null, display_name text not null, email text not null, user_type text, broadcaster_type text, user_description text, created_at text not null, category_id text, modified_at text, stream_language text not null default 'en', title text not null default '', delay int not null default 0, foreign key (category_id) references categories(id) ); create table follows ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table blocks ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table bans ( broadcaster_id text not null, user_id text not null, created_at text not null, expires_at text, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table ban_events ( id text not null primary key, event_timestamp text not null, event_type text not null, event_version text not null default '1.0', broadcaster_id text not null, user_id text not null, expires_at text, foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table moderators ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table moderator_actions ( id text not null primary key, event_timestamp text not null, event_type text not null, event_version text not null default '1.0', broadcaster_id text not null, user_id text not null, foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table editors ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table channel_points_rewards( id text not null primary key, broadcaster_id text not null, reward_image text, background_color text, is_enabled boolean not null default false, cost int not null default 0, title text not null, reward_prompt text, is_user_input_required boolean default false, stream_max_enabled boolean default false, stream_max_count int default 0, stream_user_max_enabled boolean default false, stream_user_max_count int default 0, global_cooldown_enabled boolean default false, global_cooldown_seconds int default 0, is_paused boolean default false, is_in_stock boolean default true, should_redemptions_skip_queue boolean default false, redemptions_redeemed_current_stream int, cooldown_expires_at text, foreign key (broadcaster_id) references users(id) ); create table channel_points_redemptions( id text not null primary key, reward_id text not null, broadcaster_id text not null, user_id text not null, user_input text, redemption_status text not null, redeemed_at text, foreign key (reward_id) references channel_points_rewards(id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table streams( id text not null primary key, broadcaster_id id text not null, stream_type text not null default 'live', viewer_count int not null, started_at text not null, is_mature boolean not null default false, foreign key (broadcaster_id) references users(id) ); create table tags( id text not null primary key, is_auto boolean not null default false, tag_name text not null ); create table stream_tags( user_id text not null, tag_id text not null, primary key(user_id, tag_id), foreign key(user_id) references users(id), foreign key(tag_id) references tags(id) ); create table teams( id text not null primary key, background_image_url text, banner text, created_at text not null, updated_at text, info text, thumbnail_url text, team_name text, team_display_name text ); create table team_members( team_id text not null, user_id text not null, primary key (team_id, user_id) foreign key (team_id) references teams(id), foreign key (user_id) references users(id) ); create table videos( id text not null primary key, stream_id text, broadcaster_id text not null, title text not null, video_description text not null, created_at text not null, published_at text, viewable text not null, view_count int not null default 0, duration text not null, video_language text not null default 'en', category_id text, type text default 'archive', foreign key (stream_id) references streams(id), foreign key (broadcaster_id) references users(id), foreign key (category_id) references categories(id) ); create table stream_markers( id text not null primary key, video_id text not null, position_seconds int not null, created_at text not null, description text not null, broadcaster_id text not null, foreign key (broadcaster_id) references users(id), foreign key (video_id) references videos(id) ); create table video_muted_segments ( video_id text not null, video_offset int not null, duration int not null, primary key (video_id, video_offset), foreign key (video_id) references videos(id) ); create table subscriptions ( broadcaster_id text not null, user_id text not null, is_gift boolean not null default false, gifter_id text, tier text not null default '1000', created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id), foreign key (gifter_id) references users(id) ); create table drops_entitlements( id text not null primary key, benefit_id text not null, timestamp text not null, user_id text not null, game_id text not null, foreign key (user_id) references users(id), foreign key (game_id) references categories(id) ); create table clients ( id text not null primary key, secret text not null, is_extension boolean default false, name text not null ); create table authorizations ( id integer not null primary key AUTOINCREMENT, client_id text not null, user_id text, token text not null unique, expires_at text not null, scopes text, foreign key (client_id) references clients(id) ); create table polls ( id text not null primary key, broadcaster_id text not null, title text not null, bits_voting_enabled boolean default false, bits_per_vote int default 10, channel_points_voting_enabled boolean default false, channel_points_per_vote int default 10, status text not null, duration int not null, started_at text not null, ended_at text, foreign key (broadcaster_id) references users(id) ); create table poll_choices ( id text not null primary key, title text not null, votes int not null default 0, channel_points_votes int not null default 0, bits_votes int not null default 0, poll_id text not null, foreign key (poll_id) references polls(id) ); create table predictions ( id text not null primary key, broadcaster_id text not null, title text not null, winning_outcome_id text, prediction_window int, status text not null, created_at text not null, ended_at text, locked_at text, foreign key (broadcaster_id) references users(id) ); create table prediction_outcomes ( id text not null primary key, title text not null, users int not null default 0, channel_points int not null default 0, color text not null, prediction_id text not null, foreign key (prediction_id) references predictions(id) ); create table prediction_predictions ( prediction_id text not null, user_id text not null, amount int not null, outcome_id text not null, primary key(prediction_id, user_id), foreign key(user_id) references users(id), foreign key(prediction_id) references predictions(id), foreign key(outcome_id) references prediction_outcomes(id) ); create table clips ( id text not null primary key, broadcaster_id text not null, creator_id text not null, video_id text not null, game_id text not null, title text not null, view_count int default 0, created_at text not null, duration real not null, foreign key (broadcaster_id) references users(id), foreign key (creator_id) references users(id) ); `, Message: "Adding mock API tables.", }, + 3: { + SQL: `alter table drops_entitlements add column status text not null default 'CLAIMED'; create table stream_schedule( id text not null primary key, broadcaster_id text not null, starttime text not null, endtime text not null, timezone text not null, is_vacation boolean not null default false, is_recurring boolean not null default false, category_id text, title text, foreign key(broadcaster_id) references users(id), foreign key (category_id) references categories(id));`, + Message: ``, + }, } func checkAndUpdate(db sqlx.DB) error { @@ -65,7 +69,7 @@ func checkAndUpdate(db sqlx.DB) error { } func initDatabase(db sqlx.DB) error { - createSQL := `create table events( id text not null primary key, event text not null, json text not null, from_user text not null, to_user text not null, transport text not null, timestamp text not null); create table categories( id text not null primary key, category_name text not null ); create table users( id text not null primary key, user_login text not null, display_name text not null, email text not null, user_type text, broadcaster_type text, user_description text, created_at text not null, category_id text, modified_at text, stream_language text not null default 'en', title text not null default '', delay int not null default 0, foreign key (category_id) references categories(id) ); create table follows ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table blocks ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table bans ( broadcaster_id text not null, user_id text not null, created_at text not null, expires_at text, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table ban_events ( id text not null primary key, event_timestamp text not null, event_type text not null, event_version text not null default '1.0', broadcaster_id text not null, user_id text not null, expires_at text, foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table moderators ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table moderator_actions ( id text not null primary key, event_timestamp text not null, event_type text not null, event_version text not null default '1.0', broadcaster_id text not null, user_id text not null, foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table editors ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table channel_points_rewards( id text not null primary key, broadcaster_id text not null, reward_image text, background_color text, is_enabled boolean not null default false, cost int not null default 0, title text not null, reward_prompt text, is_user_input_required boolean default false, stream_max_enabled boolean default false, stream_max_count int default 0, stream_user_max_enabled boolean default false, stream_user_max_count int default 0, global_cooldown_enabled boolean default false, global_cooldown_seconds int default 0, is_paused boolean default false, is_in_stock boolean default true, should_redemptions_skip_queue boolean default false, redemptions_redeemed_current_stream int, cooldown_expires_at text, foreign key (broadcaster_id) references users(id) ); create table channel_points_redemptions( id text not null primary key, reward_id text not null, broadcaster_id text not null, user_id text not null, user_input text, redemption_status text not null, redeemed_at text, foreign key (reward_id) references channel_points_rewards(id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table streams( id text not null primary key, broadcaster_id id text not null, stream_type text not null default 'live', viewer_count int not null, started_at text not null, is_mature boolean not null default false, foreign key (broadcaster_id) references users(id) ); create table tags( id text not null primary key, is_auto boolean not null default false, tag_name text not null ); create table stream_tags( user_id text not null, tag_id text not null, primary key(user_id, tag_id), foreign key(user_id) references users(id), foreign key(tag_id) references tags(id) ); create table teams( id text not null primary key, background_image_url text, banner text, created_at text not null, updated_at text, info text, thumbnail_url text, team_name text, team_display_name text ); create table team_members( team_id text not null, user_id text not null, primary key (team_id, user_id) foreign key (team_id) references teams(id), foreign key (user_id) references users(id) ); create table videos( id text not null primary key, stream_id text, broadcaster_id text not null, title text not null, video_description text not null, created_at text not null, published_at text, viewable text not null, view_count int not null default 0, duration text not null, video_language text not null default 'en', category_id text, type text default 'archive', foreign key (stream_id) references streams(id), foreign key (broadcaster_id) references users(id), foreign key (category_id) references categories(id) ); create table stream_markers( id text not null primary key, video_id text not null, position_seconds int not null, created_at text not null, description text not null, broadcaster_id text not null, foreign key (broadcaster_id) references users(id), foreign key (video_id) references videos(id) ); create table video_muted_segments ( video_id text not null, video_offset int not null, duration int not null, primary key (video_id, video_offset), foreign key (video_id) references videos(id) ); create table subscriptions ( broadcaster_id text not null, user_id text not null, is_gift boolean not null default false, gifter_id text, tier text not null default '1000', created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id), foreign key (gifter_id) references users(id) ); create table drops_entitlements( id text not null primary key, benefit_id text not null, timestamp text not null, user_id text not null, game_id text not null, foreign key (user_id) references users(id), foreign key (game_id) references categories(id) ); create table clients ( id text not null primary key, secret text not null, is_extension boolean default false, name text not null ); create table authorizations ( id integer not null primary key AUTOINCREMENT, client_id text not null, user_id text, token text not null unique, expires_at text not null, scopes text, foreign key (client_id) references clients(id) ); create table polls ( id text not null primary key, broadcaster_id text not null, title text not null, bits_voting_enabled boolean default false, bits_per_vote int default 10, channel_points_voting_enabled boolean default false, channel_points_per_vote int default 10, status text not null, duration int not null, started_at text not null, ended_at text, foreign key (broadcaster_id) references users(id) ); create table poll_choices ( id text not null primary key, title text not null, votes int not null default 0, channel_points_votes int not null default 0, bits_votes int not null default 0, poll_id text not null, foreign key (poll_id) references polls(id) ); create table predictions ( id text not null primary key, broadcaster_id text not null, title text not null, winning_outcome_id text, prediction_window int, status text not null, created_at text not null, ended_at text, locked_at text, foreign key (broadcaster_id) references users(id) ); create table prediction_outcomes ( id text not null primary key, title text not null, users int not null default 0, channel_points int not null default 0, color text not null, prediction_id text not null, foreign key (prediction_id) references predictions(id) ); create table prediction_predictions ( prediction_id text not null, user_id text not null, amount int not null, outcome_id text not null, primary key(prediction_id, user_id), foreign key(user_id) references users(id), foreign key(prediction_id) references predictions(id), foreign key(outcome_id) references prediction_outcomes(id) ); create table clips ( id text not null primary key, broadcaster_id text not null, creator_id text not null, video_id text not null, game_id text not null, title text not null, view_count int default 0, created_at text not null, duration real not null, foreign key (broadcaster_id) references users(id), foreign key (creator_id) references users(id) ); ` + createSQL := `create table events( id text not null primary key, event text not null, json text not null, from_user text not null, to_user text not null, transport text not null, timestamp text not null); create table categories( id text not null primary key, category_name text not null ); create table users( id text not null primary key, user_login text not null, display_name text not null, email text not null, user_type text, broadcaster_type text, user_description text, created_at text not null, category_id text, modified_at text, stream_language text not null default 'en', title text not null default '', delay int not null default 0, foreign key (category_id) references categories(id) ); create table follows ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table blocks ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table bans ( broadcaster_id text not null, user_id text not null, created_at text not null, expires_at text, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table ban_events ( id text not null primary key, event_timestamp text not null, event_type text not null, event_version text not null default '1.0', broadcaster_id text not null, user_id text not null, expires_at text, foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table moderators ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table moderator_actions ( id text not null primary key, event_timestamp text not null, event_type text not null, event_version text not null default '1.0', broadcaster_id text not null, user_id text not null, foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table editors ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table channel_points_rewards( id text not null primary key, broadcaster_id text not null, reward_image text, background_color text, is_enabled boolean not null default false, cost int not null default 0, title text not null, reward_prompt text, is_user_input_required boolean default false, stream_max_enabled boolean default false, stream_max_count int default 0, stream_user_max_enabled boolean default false, stream_user_max_count int default 0, global_cooldown_enabled boolean default false, global_cooldown_seconds int default 0, is_paused boolean default false, is_in_stock boolean default true, should_redemptions_skip_queue boolean default false, redemptions_redeemed_current_stream int, cooldown_expires_at text, foreign key (broadcaster_id) references users(id) ); create table channel_points_redemptions( id text not null primary key, reward_id text not null, broadcaster_id text not null, user_id text not null, user_input text, redemption_status text not null, redeemed_at text, foreign key (reward_id) references channel_points_rewards(id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table streams( id text not null primary key, broadcaster_id id text not null, stream_type text not null default 'live', viewer_count int not null, started_at text not null, is_mature boolean not null default false, foreign key (broadcaster_id) references users(id) ); create table tags( id text not null primary key, is_auto boolean not null default false, tag_name text not null ); create table stream_tags( user_id text not null, tag_id text not null, primary key(user_id, tag_id), foreign key(user_id) references users(id), foreign key(tag_id) references tags(id) ); create table teams( id text not null primary key, background_image_url text, banner text, created_at text not null, updated_at text, info text, thumbnail_url text, team_name text, team_display_name text ); create table team_members( team_id text not null, user_id text not null, primary key (team_id, user_id) foreign key (team_id) references teams(id), foreign key (user_id) references users(id) ); create table videos( id text not null primary key, stream_id text, broadcaster_id text not null, title text not null, video_description text not null, created_at text not null, published_at text, viewable text not null, view_count int not null default 0, duration text not null, video_language text not null default 'en', category_id text, type text default 'archive', foreign key (stream_id) references streams(id), foreign key (broadcaster_id) references users(id), foreign key (category_id) references categories(id) ); create table stream_markers( id text not null primary key, video_id text not null, position_seconds int not null, created_at text not null, description text not null, broadcaster_id text not null, foreign key (broadcaster_id) references users(id), foreign key (video_id) references videos(id) ); create table video_muted_segments ( video_id text not null, video_offset int not null, duration int not null, primary key (video_id, video_offset), foreign key (video_id) references videos(id) ); create table subscriptions ( broadcaster_id text not null, user_id text not null, is_gift boolean not null default false, gifter_id text, tier text not null default '1000', created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id), foreign key (gifter_id) references users(id) ); create table drops_entitlements( id text not null primary key, benefit_id text not null, timestamp text not null, user_id text not null, game_id text not null, status text not null default 'CLAIMED', foreign key (user_id) references users(id), foreign key (game_id) references categories(id) ); create table clients ( id text not null primary key, secret text not null, is_extension boolean default false, name text not null ); create table authorizations ( id integer not null primary key AUTOINCREMENT, client_id text not null, user_id text, token text not null unique, expires_at text not null, scopes text, foreign key (client_id) references clients(id) ); create table polls ( id text not null primary key, broadcaster_id text not null, title text not null, bits_voting_enabled boolean default false, bits_per_vote int default 10, channel_points_voting_enabled boolean default false, channel_points_per_vote int default 10, status text not null, duration int not null, started_at text not null, ended_at text, foreign key (broadcaster_id) references users(id) ); create table poll_choices ( id text not null primary key, title text not null, votes int not null default 0, channel_points_votes int not null default 0, bits_votes int not null default 0, poll_id text not null, foreign key (poll_id) references polls(id) ); create table predictions ( id text not null primary key, broadcaster_id text not null, title text not null, winning_outcome_id text, prediction_window int, status text not null, created_at text not null, ended_at text, locked_at text, foreign key (broadcaster_id) references users(id) ); create table prediction_outcomes ( id text not null primary key, title text not null, users int not null default 0, channel_points int not null default 0, color text not null, prediction_id text not null, foreign key (prediction_id) references predictions(id) ); create table prediction_predictions ( prediction_id text not null, user_id text not null, amount int not null, outcome_id text not null, primary key(prediction_id, user_id), foreign key(user_id) references users(id), foreign key(prediction_id) references predictions(id), foreign key(outcome_id) references prediction_outcomes(id) ); create table clips ( id text not null primary key, broadcaster_id text not null, creator_id text not null, video_id text not null, game_id text not null, title text not null, view_count int default 0, created_at text not null, duration real not null, foreign key (broadcaster_id) references users(id), foreign key (creator_id) references users(id) ); create table stream_schedule( id text not null primary key, broadcaster_id text not null, starttime text not null, endtime text not null, timezone text not null, is_vacation boolean not null default false, is_recurring boolean not null default false, category_id text, title text, foreign key(broadcaster_id) references users(id), foreign key (category_id) references categories(id));` for i := 1; i <= 5; i++ { tx := db.MustBegin() tx.Exec(createSQL) diff --git a/internal/mock_api/authentication/authentication.go b/internal/mock_api/authentication/authentication.go index 6fb7ae3c..0c183203 100644 --- a/internal/mock_api/authentication/authentication.go +++ b/internal/mock_api/authentication/authentication.go @@ -24,6 +24,7 @@ type UserAuthentication struct { func AuthenticationMiddleware(next mock_api.MockEndpoint) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") db := r.Context().Value("db").(database.CLIDatabase) // skip auth check for unsupported methods diff --git a/internal/mock_api/endpoints/endpoints.go b/internal/mock_api/endpoints/endpoints.go index 548ccd9b..502074ad 100644 --- a/internal/mock_api/endpoints/endpoints.go +++ b/internal/mock_api/endpoints/endpoints.go @@ -15,6 +15,7 @@ import ( "github.com/twitchdev/twitch-cli/internal/mock_api/endpoints/moderation" "github.com/twitchdev/twitch-cli/internal/mock_api/endpoints/polls" "github.com/twitchdev/twitch-cli/internal/mock_api/endpoints/predictions" + "github.com/twitchdev/twitch-cli/internal/mock_api/endpoints/schedule" "github.com/twitchdev/twitch-cli/internal/mock_api/endpoints/search" "github.com/twitchdev/twitch-cli/internal/mock_api/endpoints/streams" "github.com/twitchdev/twitch-cli/internal/mock_api/endpoints/subscriptions" @@ -47,6 +48,8 @@ func All() []mock_api.MockEndpoint { moderation.Moderators{}, polls.Polls{}, predictions.Predictions{}, + schedule.Schedule{}, + schedule.ScheduleICal{}, search.SearchCategories{}, search.SearchChannels{}, streams.AllTags{}, diff --git a/internal/mock_api/generate/generate.go b/internal/mock_api/generate/generate.go index cffa8dec..d9bc6488 100644 --- a/internal/mock_api/generate/generate.go +++ b/internal/mock_api/generate/generate.go @@ -5,6 +5,7 @@ package generate import ( "context" "database/sql" + "encoding/base64" "fmt" "log" "strings" @@ -232,7 +233,7 @@ func generateUsers(ctx context.Context, count int) error { }, { ID: util.RandomGUID(), - Title: "Choice1", + Title: "Choice2", Color: "PINK", Users: 0, ChannelPoints: 0, @@ -245,6 +246,34 @@ func generateUsers(ctx context.Context, count int) error { log.Print(err.Error()) } + // create fake schedule event + segmentID := util.RandomGUID() + + // just a years worth of recurring events; mock data + for i := 0; i < 52; i++ { + weekAdd := (i + 1) * 7 * 24 + startTime := time.Now().Add(time.Duration(weekAdd) * time.Hour).UTC() + endTime := time.Now().Add(time.Duration(weekAdd) * time.Hour).UTC() + eventID := base64.RawStdEncoding.EncodeToString([]byte(fmt.Sprintf("%v\\%v", segmentID, startTime))) + + segment := database.ScheduleSegment{ + ID: eventID, + StartTime: startTime.Format(time.RFC3339), + EndTime: endTime.Format(time.RFC3339), + IsRecurring: true, + IsVacation: false, + CategoryID: &dropsGameID, + Title: "Test Title", + UserID: broadcaster.ID, + Timezone: "America/Los_Angeles", + } + + err := db.NewQuery(nil, 100).InsertSchedule(segment) + if err != nil { + log.Print(err.Error()) + } + } + for j, user := range users { // create a seed used for the below determination on if a user should follow one another- this simply simulates a social mesh userSeed := util.RandomInt(100 * 100) @@ -509,6 +538,10 @@ func generateAuthorization(ctx context.Context, c database.AuthenticationClient, if err != nil { return err } - log.Printf("Created authorization for user %v with token %v", userID, auth.Token) + if userID != "" { + log.Printf("Created authorization for user %v with token %v", userID, auth.Token) + } else { + log.Printf("Created authorization with token %v", auth.Token) + } return nil } diff --git a/internal/mock_api/generate/generate_tools.go b/internal/mock_api/generate/generate_tools.go index 3e6fd62e..7eefd8a9 100644 --- a/internal/mock_api/generate/generate_tools.go +++ b/internal/mock_api/generate/generate_tools.go @@ -30,6 +30,8 @@ var usernamePossibilities = []string{ "Skateboard", "Egg", "Lion", + "Isaac", + "Jill", } func generateUsername() string { diff --git a/internal/mock_api/mock_server/server.go b/internal/mock_api/mock_server/server.go index 819cc745..c69ff781 100644 --- a/internal/mock_api/mock_server/server.go +++ b/internal/mock_api/mock_server/server.go @@ -81,6 +81,11 @@ func StartServer(port int) { func RegisterHandlers(m *http.ServeMux) { // all mock endpoints live in the /mock/ namespace for _, e := range endpoints.All() { + // no auth requirements on this endpoint, so just add it manually + if e.Path() == "/schedule/icalendar" { + m.Handle(MOCK_NAMESPACE+e.Path(), loggerMiddleware(e)) + continue + } m.Handle(MOCK_NAMESPACE+e.Path(), loggerMiddleware(authentication.AuthenticationMiddleware(e))) } for _, e := range mock_units.All() { @@ -95,7 +100,6 @@ func RegisterHandlers(m *http.ServeMux) { func loggerMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Printf("%v %v", r.Method, r.URL.Path) - w.Header().Set("Content-Type", "application/json") next.ServeHTTP(w, r) }) } From 0af248dab8bc66cf34dd73413610eb3040d6cdf9 Mon Sep 17 00:00:00 2001 From: lleadbet Date: Wed, 14 Jul 2021 15:25:57 -0700 Subject: [PATCH 04/36] adding schedules api support --- internal/database/schedule.go | 103 ++++++++++ internal/mock_api/endpoints/schedule/ical.go | 79 ++++++++ .../mock_api/endpoints/schedule/schedule.go | 130 +++++++++++++ .../mock_api/endpoints/schedule/segment.go | 184 ++++++++++++++++++ .../mock_api/endpoints/schedule/settings.go | 43 ++++ .../mock_api/endpoints/schedule/shared.go | 7 + 6 files changed, 546 insertions(+) create mode 100644 internal/database/schedule.go create mode 100644 internal/mock_api/endpoints/schedule/ical.go create mode 100644 internal/mock_api/endpoints/schedule/schedule.go create mode 100644 internal/mock_api/endpoints/schedule/segment.go create mode 100644 internal/mock_api/endpoints/schedule/settings.go create mode 100644 internal/mock_api/endpoints/schedule/shared.go diff --git a/internal/database/schedule.go b/internal/database/schedule.go new file mode 100644 index 00000000..c7037949 --- /dev/null +++ b/internal/database/schedule.go @@ -0,0 +1,103 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package database + +import ( + "time" +) + +type Schedule struct { + Segments []ScheduleSegment `json:"segments"` + UserID string `db:"broadcaster_id" json:"broadcaster_id"` + UserLogin string `db:"broadcaster_login" json:"broadcaster_login" dbi:"false"` + UserName string `db:"broadcaster_name" json:"broadcaster_name" dbi:"false"` + Vacation *ScheduleVacation `json:"vacation"` +} + +type ScheduleSegment struct { + ID string `db:"id" json:"id" dbs:"s.id"` + Title string `db:"title" json:"title"` + StartTime string `db:"starttime" json:"start_time"` + EndTime string `db:"endtime" json:"end_time"` + IsRecurring bool `db:"is_recurring" json:"is_recurring"` + IsVacation bool `db:"is_vacation" json:"-"` + Category *SegmentCategory `json:"category"` + UserID string `db:"broadcaster_id" json:"-"` + Timezone string `db:"timezone" json:"timezone"` + CategoryID *string `db:"category_id" json:"-"` + CategoryName *string `db:"category_name" json:"-"` +} +type ScheduleVacation struct { + StartTime string `json:"start_time"` + EndTime string `json:"end_time"` +} + +type SegmentCategory struct { + ID *string `db:"category_id" json:"id" dbs:"category_id"` + CategoryName *string `db:"category_name" json:"name" dbi:"false"` +} + +func (q *Query) GetSchedule(p ScheduleSegment, startTime time.Time) (*DBResponse, error) { + r := Schedule{} + + u, err := q.GetUser(User{ID: p.UserID}) + if err != nil { + return nil, err + } + r.UserID = u.ID + r.UserLogin = u.UserLogin + r.UserName = u.DisplayName + + sql := generateSQL("select s.*, c.category_name from stream_schedule s left join categories c on s.category_id = c.id", p, SEP_AND) + p.StartTime = startTime.Format(time.RFC3339) + sql += " and datetime(starttime) >= datetime(:starttime) " + q.SQL + rows, err := q.DB.NamedQuery(sql, p) + if err != nil { + return nil, err + } + + for rows.Next() { + var s ScheduleSegment + err := rows.StructScan(&s) + if err != nil { + return nil, err + } + if s.CategoryID != nil { + s.Category = &SegmentCategory{ + ID: s.CategoryID, + CategoryName: s.CategoryName, + } + } + if s.IsVacation { + r.Vacation = &ScheduleVacation{ + StartTime: s.StartTime, + EndTime: s.EndTime, + } + } else { + r.Segments = append(r.Segments, s) + } + } + + dbr := DBResponse{ + Data: r, + Limit: q.Limit, + Total: len(r.Segments), + } + + if len(r.Segments) != q.Limit { + q.PaginationCursor = "" + } + + dbr.Cursor = q.PaginationCursor + + return &dbr, err +} + +func (q *Query) InsertSchedule(p ScheduleSegment) error { + tx := q.DB.MustBegin() + _, err := tx.NamedExec(generateInsertSQL("stream_schedule", "id", p, false), p) + if err != nil { + return err + } + return tx.Commit() +} diff --git a/internal/mock_api/endpoints/schedule/ical.go b/internal/mock_api/endpoints/schedule/ical.go new file mode 100644 index 00000000..ff087441 --- /dev/null +++ b/internal/mock_api/endpoints/schedule/ical.go @@ -0,0 +1,79 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package schedule + +import ( + "net/http" + + "github.com/twitchdev/twitch-cli/internal/database" + "github.com/twitchdev/twitch-cli/internal/mock_api/mock_errors" +) + +var scheduleICalMethodsSupported = map[string]bool{ + http.MethodGet: true, + http.MethodPost: false, + http.MethodDelete: false, + http.MethodPatch: false, + http.MethodPut: false, +} + +var scheduleICalScopesByMethod = map[string][]string{ + http.MethodGet: {}, + http.MethodPost: {}, + http.MethodDelete: {}, + http.MethodPatch: {}, + http.MethodPut: {}, +} + +type ScheduleICal struct{} + +func (e ScheduleICal) Path() string { return "/schedule/icalendar" } + +func (e ScheduleICal) GetRequiredScopes(method string) []string { + return scheduleICalScopesByMethod[method] +} + +func (e ScheduleICal) ValidMethod(method string) bool { + return scheduleICalMethodsSupported[method] +} + +func (e ScheduleICal) ServeHTTP(w http.ResponseWriter, r *http.Request) { + db = r.Context().Value("db").(database.CLIDatabase) + + switch r.Method { + case http.MethodGet: + e.getIcal(w, r) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +// stubbed with fake data for now, since .ics generation libraries are far and few between for golang +// and it's just useful for mock data +func (e ScheduleICal) getIcal(w http.ResponseWriter, r *http.Request) { + broadcaster := r.URL.Query().Get("broadcaster_id") + if broadcaster == "" { + mock_errors.WriteBadRequest(w, "Missing required paramater broadaster_id") + return + } + + body := + `BEGIN:VCALENDAR +PRODID:-//twitch.tv//StreamSchedule//1.0 +VERSION:2.0 +CALSCALE:GREGORIAN +REFRESH-INTERVAL;VALUE=DURATION:PT1H +NAME:TwitchDev +BEGIN:VEVENT +UID:e4acc724-371f-402c-81ca-23ada79759d4 +DTSTAMP:20210323T040131Z +DTSTART;TZID=/America/New_York:20210701T140000 +DTEND;TZID=/America/New_York:20210701T150000 +SUMMARY:TwitchDev Monthly Update // July 1, 2021 +DESCRIPTION:Science & Technology. +CATEGORIES:Science & Technology +END:VEVENT +END:VCALENDAR%` + w.Header().Add("Content-Type", "text/calendar") + w.Write([]byte(body)) +} diff --git a/internal/mock_api/endpoints/schedule/schedule.go b/internal/mock_api/endpoints/schedule/schedule.go new file mode 100644 index 00000000..4e309a1b --- /dev/null +++ b/internal/mock_api/endpoints/schedule/schedule.go @@ -0,0 +1,130 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package schedule + +import ( + "encoding/json" + "net/http" + "strconv" + "time" + + "github.com/twitchdev/twitch-cli/internal/database" + "github.com/twitchdev/twitch-cli/internal/mock_api/mock_errors" + "github.com/twitchdev/twitch-cli/internal/models" +) + +var scheduleMethodsSupported = map[string]bool{ + http.MethodGet: true, + http.MethodPost: false, + http.MethodDelete: false, + http.MethodPatch: false, + http.MethodPut: false, +} + +var scheduleScopesByMethod = map[string][]string{ + http.MethodGet: {}, + http.MethodPost: {}, + http.MethodDelete: {}, + http.MethodPatch: {}, + http.MethodPut: {}, +} + +type Schedule struct{} + +func (e Schedule) Path() string { return "/schedule" } + +func (e Schedule) GetRequiredScopes(method string) []string { + return scheduleScopesByMethod[method] +} + +func (e Schedule) ValidMethod(method string) bool { + return scheduleMethodsSupported[method] +} + +func (e Schedule) ServeHTTP(w http.ResponseWriter, r *http.Request) { + db = r.Context().Value("db").(database.CLIDatabase) + + switch r.Method { + case http.MethodGet: + e.getSchedule(w, r) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +func (e Schedule) getSchedule(w http.ResponseWriter, r *http.Request) { + broadcasterID := r.URL.Query().Get("broadcaster_id") + queryTime := r.URL.Query().Get("start_time") + offset := r.URL.Query().Get("utc_offset") + ids := r.URL.Query()["id"] + schedule := database.Schedule{} + startTime := time.Now().UTC() + apiResponse := models.APIResponse{} + + if broadcasterID == "" { + mock_errors.WriteBadRequest(w, "Required parameter broadcaster_id is missing") + return + } + + if queryTime != "" { + st, err := time.Parse(time.RFC3339, queryTime) + if err != nil { + mock_errors.WriteBadRequest(w, "Parameter start_time is in an invalid format") + return + } + startTime = st.UTC() + } + + if offset != "" { + o, err := strconv.Atoi(offset) + if err != nil { + mock_errors.WriteBadRequest(w, "Error decoding parameter offset") + return + } + tz := time.FixedZone("", o*60) + startTime = startTime.In(tz) + } + + segments := []database.ScheduleSegment{} + if len(ids) > 0 { + if len(ids) > 100 { + mock_errors.WriteBadRequest(w, "Parameter id may only have a maximum of 100 values") + return + } + for _, id := range ids { + dbr, err := db.NewQuery(r, 25).GetSchedule(database.ScheduleSegment{ID: id, UserID: broadcasterID}, startTime) + if err != nil { + mock_errors.WriteServerError(w, err.Error()) + } + response := dbr.Data.(database.Schedule) + schedule = response + segments = append(segments, response.Segments...) + } + schedule.Segments = segments + apiResponse = models.APIResponse{ + Data: schedule, + } + } else { + dbr, err := db.NewQuery(r, 25).GetSchedule(database.ScheduleSegment{UserID: broadcasterID}, startTime) + if err != nil { + mock_errors.WriteServerError(w, err.Error()) + return + } + response := dbr.Data.(database.Schedule) + segments = append(segments, response.Segments...) + schedule = response + schedule.Segments = segments + apiResponse = models.APIResponse{ + Data: schedule, + } + + if len(schedule.Segments) == dbr.Limit { + apiResponse.Pagination = &models.APIPagination{ + Cursor: dbr.Cursor, + } + } + } + + bytes, _ := json.Marshal(apiResponse) + w.Write(bytes) +} diff --git a/internal/mock_api/endpoints/schedule/segment.go b/internal/mock_api/endpoints/schedule/segment.go new file mode 100644 index 00000000..f117a075 --- /dev/null +++ b/internal/mock_api/endpoints/schedule/segment.go @@ -0,0 +1,184 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package schedule + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/twitchdev/twitch-cli/internal/database" + "github.com/twitchdev/twitch-cli/internal/mock_api/authentication" + "github.com/twitchdev/twitch-cli/internal/mock_api/mock_errors" + "github.com/twitchdev/twitch-cli/internal/util" +) + +var scheduleSegmentMethodsSupported = map[string]bool{ + http.MethodGet: false, + http.MethodPost: true, + http.MethodDelete: true, + http.MethodPatch: true, + http.MethodPut: false, +} + +var scheduleSegmentScopesByMethod = map[string][]string{ + http.MethodGet: {}, + http.MethodPost: {"channel:manage:schedule"}, + http.MethodDelete: {"channel:manage:schedule"}, + http.MethodPatch: {"channel:manage:schedule"}, + http.MethodPut: {}, +} + +type ScheduleSegment struct{} + +type SegmentPatchAndPostBody struct { + StartTime string `json:"start_time"` + Timezone string `json:"timezone"` + IsRecurring *bool `json:"is_recurring"` + Duration string `json:"duration"` + CategoryID *string `json:"category_id"` + Title string `json:"title"` +} + +func (e ScheduleSegment) Path() string { return "/schedule/segment" } + +func (e ScheduleSegment) GetRequiredScopes(method string) []string { + return scheduleSegmentScopesByMethod[method] +} + +func (e ScheduleSegment) ValidMethod(method string) bool { + return scheduleSegmentMethodsSupported[method] +} + +func (e ScheduleSegment) ServeHTTP(w http.ResponseWriter, r *http.Request) { + db = r.Context().Value("db").(database.CLIDatabase) + + switch r.Method { + case http.MethodPost: + e.postSegment(w, r) + case http.MethodDelete: + e.deleteSegment(w, r) + case http.MethodPatch: + e.patchSegment(w, r) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +func (e ScheduleSegment) postSegment(w http.ResponseWriter, r *http.Request) { + userCtx := r.Context().Value("auth").(authentication.UserAuthentication) + duration := 240 + + if !userCtx.MatchesBroadcasterIDParam(r) { + mock_errors.WriteBadRequest(w, "User token does not match broadcaster_id parameter") + return + } + var body SegmentPatchAndPostBody + err := json.NewDecoder(r.Body).Decode(&body) + if err != nil { + mock_errors.WriteBadRequest(w, "Error parsing body") + return + } + + if body.StartTime == "" { + mock_errors.WriteBadRequest(w, "Missing start_time") + return + } + st, err := time.Parse(time.RFC3339, body.StartTime) + if err != nil { + mock_errors.WriteBadRequest(w, "Invalid timezone provided") + return + } + if body.Timezone == "" { + mock_errors.WriteBadRequest(w, "Missing timezone") + return + } + _, err = time.LoadLocation(body.Timezone) + if err != nil { + mock_errors.WriteBadRequest(w, "Invalid timezone provided") + return + } + if body.IsRecurring == nil { + mock_errors.WriteBadRequest(w, "Missing is_recurring") + return + } + + if len(body.Title) > 140 { + mock_errors.WriteBadRequest(w, "Title must be less than 140 characters") + return + } + + if body.Duration != "" { + duration, err = strconv.Atoi(body.Duration) + if err != nil { + mock_errors.WriteBadRequest(w, "Invalid duration provided") + return + } + } + et := st.Add(time.Duration(duration) * time.Minute) + + segmentID := util.RandomGUID() + eventID := base64.RawStdEncoding.EncodeToString([]byte(fmt.Sprintf("%v\\%v", segmentID, st))) + segment := database.ScheduleSegment{ + ID: eventID, + StartTime: st.UTC().Format(time.RFC3339), + EndTime: et.UTC().Format(time.RFC3339), + IsRecurring: true, + IsVacation: false, + CategoryID: body.CategoryID, + Title: body.Title, + UserID: userCtx.UserID, + Timezone: "America/Los_Angeles", + } + err = db.NewQuery(nil, 100).InsertSchedule(segment) + if err != nil { + mock_errors.WriteServerError(w, err.Error()) + return + } + if *body.IsRecurring { + // just a years worth of recurring events; mock data + for i := 0; i < 52; i++ { + weekAdd := (i + 1) * 7 * 24 + startTime := time.Now().Add(time.Duration(weekAdd) * time.Hour).UTC() + endTime := time.Now().Add(time.Duration(weekAdd) * time.Hour).UTC() + eventID := base64.RawStdEncoding.EncodeToString([]byte(fmt.Sprintf("%v\\%v", segmentID, startTime))) + + s := database.ScheduleSegment{ + ID: eventID, + StartTime: startTime.Format(time.RFC3339), + EndTime: endTime.Format(time.RFC3339), + IsRecurring: true, + IsVacation: false, + CategoryID: body.CategoryID, + Title: "Test Title", + UserID: userCtx.UserID, + Timezone: body.Timezone, + } + + err := db.NewQuery(nil, 100).InsertSchedule(s) + if err != nil { + mock_errors.WriteServerError(w, err.Error()) + return + } + } + } + dbr, err := db.NewQuery(nil, 100).GetSchedule(database.ScheduleSegment{ID: segment.ID}, time.Now()) + if err != nil { + mock_errors.WriteServerError(w, err.Error()) + return + } + b := dbr.Data.(database.Schedule) + bytes, _ := json.Marshal(b) + w.Write(bytes) +} + +func (e ScheduleSegment) deleteSegment(w http.ResponseWriter, r *http.Request) { + +} + +func (e ScheduleSegment) patchSegment(w http.ResponseWriter, r *http.Request) { + +} diff --git a/internal/mock_api/endpoints/schedule/settings.go b/internal/mock_api/endpoints/schedule/settings.go new file mode 100644 index 00000000..4192450d --- /dev/null +++ b/internal/mock_api/endpoints/schedule/settings.go @@ -0,0 +1,43 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package schedule + +import ( + "net/http" + + "github.com/twitchdev/twitch-cli/internal/database" +) + +var scheduleSettingsMethodsSupported = map[string]bool{ + http.MethodGet: false, + http.MethodPost: false, + http.MethodDelete: false, + http.MethodPatch: true, + http.MethodPut: false, +} + +var scheduleSettingsScopesByMethod = map[string][]string{ + http.MethodGet: {}, + http.MethodPost: {}, + http.MethodDelete: {}, + http.MethodPatch: {"channel:manage:schedule"}, + http.MethodPut: {}, +} + +type ScheduleSettings struct{} + +func (e ScheduleSettings) Path() string { return "/schedule/settings" } + +func (e ScheduleSettings) GetRequiredScopes(method string) []string { + return scheduleSettingsScopesByMethod[method] +} + +func (e ScheduleSettings) ValidMethod(method string) bool { + return scheduleSettingsMethodsSupported[method] +} + +func (e ScheduleSettings) ServeHTTP(w http.ResponseWriter, r *http.Request) { + db = r.Context().Value("db").(database.CLIDatabase) + + w.WriteHeader(200) +} diff --git a/internal/mock_api/endpoints/schedule/shared.go b/internal/mock_api/endpoints/schedule/shared.go new file mode 100644 index 00000000..4ba602dd --- /dev/null +++ b/internal/mock_api/endpoints/schedule/shared.go @@ -0,0 +1,7 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package schedule + +import "github.com/twitchdev/twitch-cli/internal/database" + +var db database.CLIDatabase From b26dd40657087f7ff78f8f4697da943070ca1bdb Mon Sep 17 00:00:00 2001 From: lleadbet Date: Wed, 14 Jul 2021 15:27:16 -0700 Subject: [PATCH 05/36] Fixing #74. --- internal/api/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/api/api.go b/internal/api/api.go index 6fae0d2b..acd7f52d 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -107,7 +107,7 @@ func NewRequest(method string, path string, queryParameters []string, body []byt } d := data.Data.([]interface{}) - data.Data = append(d, apiResponse.Data) + data.Data = append(d, apiResponse.Data.([]interface{})...) if apiResponse.Pagination == nil || *&apiResponse.Pagination.Cursor == "" { break From 81f7bcea9941c17b219460c6ec170ae5f04bc1b0 Mon Sep 17 00:00:00 2001 From: lleadbet Date: Wed, 14 Jul 2021 15:29:44 -0700 Subject: [PATCH 06/36] Adding exclusion for schedule APIs --- internal/api/api.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/api/api.go b/internal/api/api.go index acd7f52d..9d81662f 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -107,7 +107,11 @@ func NewRequest(method string, path string, queryParameters []string, body []byt } d := data.Data.([]interface{}) - data.Data = append(d, apiResponse.Data.([]interface{})...) + if strings.Contains(path, "schedule") { + data.Data = append(d, apiResponse.Data) + } else { + data.Data = append(d, apiResponse.Data.([]interface{})...) + } if apiResponse.Pagination == nil || *&apiResponse.Pagination.Cursor == "" { break From 05462d22daca55ea50d464c4e749d53dc88b6d7b Mon Sep 17 00:00:00 2001 From: lleadbet Date: Wed, 14 Jul 2021 15:31:29 -0700 Subject: [PATCH 07/36] fixing tests --- internal/api/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/api/api.go b/internal/api/api.go index 9d81662f..498f0e38 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -107,7 +107,7 @@ func NewRequest(method string, path string, queryParameters []string, body []byt } d := data.Data.([]interface{}) - if strings.Contains(path, "schedule") { + if strings.Contains(path, "schedule") || apiResponse.Data == nil { data.Data = append(d, apiResponse.Data) } else { data.Data = append(d, apiResponse.Data.([]interface{})...) From 36c08f963b493d3de28841258ee81b30882f02e1 Mon Sep 17 00:00:00 2001 From: lleadbet Date: Fri, 16 Jul 2021 13:05:53 -0700 Subject: [PATCH 08/36] adding basic schedule unit tests --- internal/database/_schema.sql | 1 + internal/database/init.go | 4 +- internal/database/schedule.go | 51 +++++-- internal/mock_api/endpoints/endpoints.go | 2 + .../mock_api/endpoints/schedule/segment.go | 139 +++++++++++++++++- .../mock_api/endpoints/schedule/settings.go | 102 ++++++++++++- internal/mock_api/generate/generate.go | 3 + internal/mock_auth/mock_auth.go | 1 + 8 files changed, 282 insertions(+), 21 deletions(-) diff --git a/internal/database/_schema.sql b/internal/database/_schema.sql index f77bfa22..b79aeabc 100644 --- a/internal/database/_schema.sql +++ b/internal/database/_schema.sql @@ -298,6 +298,7 @@ create table stream_schedule( timezone text not null, is_vacation boolean not null default false, is_recurring boolean not null default false, + is_canceled boolean not null default false, title text, category_id text, foreign key (broadcaster_id) references users(id), diff --git a/internal/database/init.go b/internal/database/init.go index 20828f7e..174f040b 100644 --- a/internal/database/init.go +++ b/internal/database/init.go @@ -31,7 +31,7 @@ var migrateSQL = map[int]migrateMap{ Message: "Adding mock API tables.", }, 3: { - SQL: `alter table drops_entitlements add column status text not null default 'CLAIMED'; create table stream_schedule( id text not null primary key, broadcaster_id text not null, starttime text not null, endtime text not null, timezone text not null, is_vacation boolean not null default false, is_recurring boolean not null default false, category_id text, title text, foreign key(broadcaster_id) references users(id), foreign key (category_id) references categories(id));`, + SQL: `alter table drops_entitlements add column status text not null default 'CLAIMED'; create table stream_schedule( id text not null primary key, broadcaster_id text not null, starttime text not null, endtime text not null, timezone text not null, is_vacation boolean not null default false, is_recurring boolean not null default false, is_canceled boolean not null default false, title text, category_id text, foreign key(broadcaster_id) references users(id), foreign key (category_id) references categories(id));`, Message: ``, }, } @@ -69,7 +69,7 @@ func checkAndUpdate(db sqlx.DB) error { } func initDatabase(db sqlx.DB) error { - createSQL := `create table events( id text not null primary key, event text not null, json text not null, from_user text not null, to_user text not null, transport text not null, timestamp text not null); create table categories( id text not null primary key, category_name text not null ); create table users( id text not null primary key, user_login text not null, display_name text not null, email text not null, user_type text, broadcaster_type text, user_description text, created_at text not null, category_id text, modified_at text, stream_language text not null default 'en', title text not null default '', delay int not null default 0, foreign key (category_id) references categories(id) ); create table follows ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table blocks ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table bans ( broadcaster_id text not null, user_id text not null, created_at text not null, expires_at text, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table ban_events ( id text not null primary key, event_timestamp text not null, event_type text not null, event_version text not null default '1.0', broadcaster_id text not null, user_id text not null, expires_at text, foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table moderators ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table moderator_actions ( id text not null primary key, event_timestamp text not null, event_type text not null, event_version text not null default '1.0', broadcaster_id text not null, user_id text not null, foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table editors ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table channel_points_rewards( id text not null primary key, broadcaster_id text not null, reward_image text, background_color text, is_enabled boolean not null default false, cost int not null default 0, title text not null, reward_prompt text, is_user_input_required boolean default false, stream_max_enabled boolean default false, stream_max_count int default 0, stream_user_max_enabled boolean default false, stream_user_max_count int default 0, global_cooldown_enabled boolean default false, global_cooldown_seconds int default 0, is_paused boolean default false, is_in_stock boolean default true, should_redemptions_skip_queue boolean default false, redemptions_redeemed_current_stream int, cooldown_expires_at text, foreign key (broadcaster_id) references users(id) ); create table channel_points_redemptions( id text not null primary key, reward_id text not null, broadcaster_id text not null, user_id text not null, user_input text, redemption_status text not null, redeemed_at text, foreign key (reward_id) references channel_points_rewards(id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table streams( id text not null primary key, broadcaster_id id text not null, stream_type text not null default 'live', viewer_count int not null, started_at text not null, is_mature boolean not null default false, foreign key (broadcaster_id) references users(id) ); create table tags( id text not null primary key, is_auto boolean not null default false, tag_name text not null ); create table stream_tags( user_id text not null, tag_id text not null, primary key(user_id, tag_id), foreign key(user_id) references users(id), foreign key(tag_id) references tags(id) ); create table teams( id text not null primary key, background_image_url text, banner text, created_at text not null, updated_at text, info text, thumbnail_url text, team_name text, team_display_name text ); create table team_members( team_id text not null, user_id text not null, primary key (team_id, user_id) foreign key (team_id) references teams(id), foreign key (user_id) references users(id) ); create table videos( id text not null primary key, stream_id text, broadcaster_id text not null, title text not null, video_description text not null, created_at text not null, published_at text, viewable text not null, view_count int not null default 0, duration text not null, video_language text not null default 'en', category_id text, type text default 'archive', foreign key (stream_id) references streams(id), foreign key (broadcaster_id) references users(id), foreign key (category_id) references categories(id) ); create table stream_markers( id text not null primary key, video_id text not null, position_seconds int not null, created_at text not null, description text not null, broadcaster_id text not null, foreign key (broadcaster_id) references users(id), foreign key (video_id) references videos(id) ); create table video_muted_segments ( video_id text not null, video_offset int not null, duration int not null, primary key (video_id, video_offset), foreign key (video_id) references videos(id) ); create table subscriptions ( broadcaster_id text not null, user_id text not null, is_gift boolean not null default false, gifter_id text, tier text not null default '1000', created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id), foreign key (gifter_id) references users(id) ); create table drops_entitlements( id text not null primary key, benefit_id text not null, timestamp text not null, user_id text not null, game_id text not null, status text not null default 'CLAIMED', foreign key (user_id) references users(id), foreign key (game_id) references categories(id) ); create table clients ( id text not null primary key, secret text not null, is_extension boolean default false, name text not null ); create table authorizations ( id integer not null primary key AUTOINCREMENT, client_id text not null, user_id text, token text not null unique, expires_at text not null, scopes text, foreign key (client_id) references clients(id) ); create table polls ( id text not null primary key, broadcaster_id text not null, title text not null, bits_voting_enabled boolean default false, bits_per_vote int default 10, channel_points_voting_enabled boolean default false, channel_points_per_vote int default 10, status text not null, duration int not null, started_at text not null, ended_at text, foreign key (broadcaster_id) references users(id) ); create table poll_choices ( id text not null primary key, title text not null, votes int not null default 0, channel_points_votes int not null default 0, bits_votes int not null default 0, poll_id text not null, foreign key (poll_id) references polls(id) ); create table predictions ( id text not null primary key, broadcaster_id text not null, title text not null, winning_outcome_id text, prediction_window int, status text not null, created_at text not null, ended_at text, locked_at text, foreign key (broadcaster_id) references users(id) ); create table prediction_outcomes ( id text not null primary key, title text not null, users int not null default 0, channel_points int not null default 0, color text not null, prediction_id text not null, foreign key (prediction_id) references predictions(id) ); create table prediction_predictions ( prediction_id text not null, user_id text not null, amount int not null, outcome_id text not null, primary key(prediction_id, user_id), foreign key(user_id) references users(id), foreign key(prediction_id) references predictions(id), foreign key(outcome_id) references prediction_outcomes(id) ); create table clips ( id text not null primary key, broadcaster_id text not null, creator_id text not null, video_id text not null, game_id text not null, title text not null, view_count int default 0, created_at text not null, duration real not null, foreign key (broadcaster_id) references users(id), foreign key (creator_id) references users(id) ); create table stream_schedule( id text not null primary key, broadcaster_id text not null, starttime text not null, endtime text not null, timezone text not null, is_vacation boolean not null default false, is_recurring boolean not null default false, category_id text, title text, foreign key(broadcaster_id) references users(id), foreign key (category_id) references categories(id));` + createSQL := `create table events( id text not null primary key, event text not null, json text not null, from_user text not null, to_user text not null, transport text not null, timestamp text not null); create table categories( id text not null primary key, category_name text not null ); create table users( id text not null primary key, user_login text not null, display_name text not null, email text not null, user_type text, broadcaster_type text, user_description text, created_at text not null, category_id text, modified_at text, stream_language text not null default 'en', title text not null default '', delay int not null default 0, foreign key (category_id) references categories(id) ); create table follows ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table blocks ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table bans ( broadcaster_id text not null, user_id text not null, created_at text not null, expires_at text, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table ban_events ( id text not null primary key, event_timestamp text not null, event_type text not null, event_version text not null default '1.0', broadcaster_id text not null, user_id text not null, expires_at text, foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table moderators ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table moderator_actions ( id text not null primary key, event_timestamp text not null, event_type text not null, event_version text not null default '1.0', broadcaster_id text not null, user_id text not null, foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table editors ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table channel_points_rewards( id text not null primary key, broadcaster_id text not null, reward_image text, background_color text, is_enabled boolean not null default false, cost int not null default 0, title text not null, reward_prompt text, is_user_input_required boolean default false, stream_max_enabled boolean default false, stream_max_count int default 0, stream_user_max_enabled boolean default false, stream_user_max_count int default 0, global_cooldown_enabled boolean default false, global_cooldown_seconds int default 0, is_paused boolean default false, is_in_stock boolean default true, should_redemptions_skip_queue boolean default false, redemptions_redeemed_current_stream int, cooldown_expires_at text, foreign key (broadcaster_id) references users(id) ); create table channel_points_redemptions( id text not null primary key, reward_id text not null, broadcaster_id text not null, user_id text not null, user_input text, redemption_status text not null, redeemed_at text, foreign key (reward_id) references channel_points_rewards(id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table streams( id text not null primary key, broadcaster_id id text not null, stream_type text not null default 'live', viewer_count int not null, started_at text not null, is_mature boolean not null default false, foreign key (broadcaster_id) references users(id) ); create table tags( id text not null primary key, is_auto boolean not null default false, tag_name text not null ); create table stream_tags( user_id text not null, tag_id text not null, primary key(user_id, tag_id), foreign key(user_id) references users(id), foreign key(tag_id) references tags(id) ); create table teams( id text not null primary key, background_image_url text, banner text, created_at text not null, updated_at text, info text, thumbnail_url text, team_name text, team_display_name text ); create table team_members( team_id text not null, user_id text not null, primary key (team_id, user_id) foreign key (team_id) references teams(id), foreign key (user_id) references users(id) ); create table videos( id text not null primary key, stream_id text, broadcaster_id text not null, title text not null, video_description text not null, created_at text not null, published_at text, viewable text not null, view_count int not null default 0, duration text not null, video_language text not null default 'en', category_id text, type text default 'archive', foreign key (stream_id) references streams(id), foreign key (broadcaster_id) references users(id), foreign key (category_id) references categories(id) ); create table stream_markers( id text not null primary key, video_id text not null, position_seconds int not null, created_at text not null, description text not null, broadcaster_id text not null, foreign key (broadcaster_id) references users(id), foreign key (video_id) references videos(id) ); create table video_muted_segments ( video_id text not null, video_offset int not null, duration int not null, primary key (video_id, video_offset), foreign key (video_id) references videos(id) ); create table subscriptions ( broadcaster_id text not null, user_id text not null, is_gift boolean not null default false, gifter_id text, tier text not null default '1000', created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id), foreign key (gifter_id) references users(id) ); create table drops_entitlements( id text not null primary key, benefit_id text not null, timestamp text not null, user_id text not null, game_id text not null, status text not null default 'CLAIMED', foreign key (user_id) references users(id), foreign key (game_id) references categories(id) ); create table clients ( id text not null primary key, secret text not null, is_extension boolean default false, name text not null ); create table authorizations ( id integer not null primary key AUTOINCREMENT, client_id text not null, user_id text, token text not null unique, expires_at text not null, scopes text, foreign key (client_id) references clients(id) ); create table polls ( id text not null primary key, broadcaster_id text not null, title text not null, bits_voting_enabled boolean default false, bits_per_vote int default 10, channel_points_voting_enabled boolean default false, channel_points_per_vote int default 10, status text not null, duration int not null, started_at text not null, ended_at text, foreign key (broadcaster_id) references users(id) ); create table poll_choices ( id text not null primary key, title text not null, votes int not null default 0, channel_points_votes int not null default 0, bits_votes int not null default 0, poll_id text not null, foreign key (poll_id) references polls(id) ); create table predictions ( id text not null primary key, broadcaster_id text not null, title text not null, winning_outcome_id text, prediction_window int, status text not null, created_at text not null, ended_at text, locked_at text, foreign key (broadcaster_id) references users(id) ); create table prediction_outcomes ( id text not null primary key, title text not null, users int not null default 0, channel_points int not null default 0, color text not null, prediction_id text not null, foreign key (prediction_id) references predictions(id) ); create table prediction_predictions ( prediction_id text not null, user_id text not null, amount int not null, outcome_id text not null, primary key(prediction_id, user_id), foreign key(user_id) references users(id), foreign key(prediction_id) references predictions(id), foreign key(outcome_id) references prediction_outcomes(id) ); create table clips ( id text not null primary key, broadcaster_id text not null, creator_id text not null, video_id text not null, game_id text not null, title text not null, view_count int default 0, created_at text not null, duration real not null, foreign key (broadcaster_id) references users(id), foreign key (creator_id) references users(id) ); create table stream_schedule( id text not null primary key, broadcaster_id text not null, starttime text not null, endtime text not null, timezone text not null, is_vacation boolean not null default false, is_recurring boolean not null default false, is_canceled boolean not null default false, title text, category_id text, foreign key(broadcaster_id) references users(id), foreign key (category_id) references categories(id));` for i := 1; i <= 5; i++ { tx := db.MustBegin() tx.Exec(createSQL) diff --git a/internal/database/schedule.go b/internal/database/schedule.go index c7037949..c1f26087 100644 --- a/internal/database/schedule.go +++ b/internal/database/schedule.go @@ -15,21 +15,24 @@ type Schedule struct { } type ScheduleSegment struct { - ID string `db:"id" json:"id" dbs:"s.id"` - Title string `db:"title" json:"title"` - StartTime string `db:"starttime" json:"start_time"` - EndTime string `db:"endtime" json:"end_time"` - IsRecurring bool `db:"is_recurring" json:"is_recurring"` - IsVacation bool `db:"is_vacation" json:"-"` - Category *SegmentCategory `json:"category"` - UserID string `db:"broadcaster_id" json:"-"` - Timezone string `db:"timezone" json:"timezone"` - CategoryID *string `db:"category_id" json:"-"` - CategoryName *string `db:"category_name" json:"-"` + ID string `db:"id" json:"id" dbs:"s.id"` + Title string `db:"title" json:"title"` + StartTime string `db:"starttime" json:"start_time"` + EndTime string `db:"endtime" json:"end_time"` + IsRecurring bool `db:"is_recurring" json:"is_recurring"` + IsVacation bool `db:"is_vacation" json:"-"` + Category *SegmentCategory `json:"category"` + UserID string `db:"broadcaster_id" json:"-"` + Timezone string `db:"timezone" json:"timezone"` + CategoryID *string `db:"category_id" json:"-"` + CategoryName *string `db:"category_name" dbi:"false" json:"-"` + IsCanceled *bool `db:"is_canceled" json:"-"` + CanceledUntil *string `json:"canceled_until"` } type ScheduleVacation struct { - StartTime string `json:"start_time"` - EndTime string `json:"end_time"` + ID string `db:"id" json:"-"` + StartTime string `db:"starttime" json:"start_time"` + EndTime string `db:"endtime" json:"end_time"` } type SegmentCategory struct { @@ -77,7 +80,11 @@ func (q *Query) GetSchedule(p ScheduleSegment, startTime time.Time) (*DBResponse r.Segments = append(r.Segments, s) } } - + v, err := q.GetVacations(ScheduleSegment{UserID: p.UserID}) + if err != nil { + return nil, err + } + r.Vacation = &v dbr := DBResponse{ Data: r, Limit: q.Limit, @@ -101,3 +108,19 @@ func (q *Query) InsertSchedule(p ScheduleSegment) error { } return tx.Commit() } + +func (q *Query) DeleteSegment(id string, broadcasterID string) error { + _, err := q.DB.Exec("delete from stream_schedule where id=$1 and broadcaster_id=$2", id, broadcasterID) + return err +} + +func (q *Query) UpdateSegment(p ScheduleSegment) error { + _, err := q.DB.NamedExec(generateUpdateSQL("stream_schedule", []string{"id"}, p), p) + return err +} + +func (q *Query) GetVacations(p ScheduleSegment) (ScheduleVacation, error) { + v := ScheduleVacation{} + err := q.DB.Get(&v, "select id,starttime,endtime from stream_schedule where is_vacation=true and datetime(endtime) > datetime('now') and broadcaster_id= $1 limit 1", p.UserID) + return v, err +} diff --git a/internal/mock_api/endpoints/endpoints.go b/internal/mock_api/endpoints/endpoints.go index 502074ad..2c52898b 100644 --- a/internal/mock_api/endpoints/endpoints.go +++ b/internal/mock_api/endpoints/endpoints.go @@ -50,6 +50,8 @@ func All() []mock_api.MockEndpoint { predictions.Predictions{}, schedule.Schedule{}, schedule.ScheduleICal{}, + schedule.ScheduleSegment{}, + schedule.ScheduleSettings{}, search.SearchCategories{}, search.SearchChannels{}, streams.AllTags{}, diff --git a/internal/mock_api/endpoints/schedule/segment.go b/internal/mock_api/endpoints/schedule/segment.go index f117a075..8ac9b3ca 100644 --- a/internal/mock_api/endpoints/schedule/segment.go +++ b/internal/mock_api/endpoints/schedule/segment.go @@ -32,6 +32,8 @@ var scheduleSegmentScopesByMethod = map[string][]string{ http.MethodPut: {}, } +var f = false + type ScheduleSegment struct{} type SegmentPatchAndPostBody struct { @@ -41,6 +43,7 @@ type SegmentPatchAndPostBody struct { Duration string `json:"duration"` CategoryID *string `json:"category_id"` Title string `json:"title"` + IsCanceled *bool `json:"is_canceled"` } func (e ScheduleSegment) Path() string { return "/schedule/segment" } @@ -126,12 +129,13 @@ func (e ScheduleSegment) postSegment(w http.ResponseWriter, r *http.Request) { ID: eventID, StartTime: st.UTC().Format(time.RFC3339), EndTime: et.UTC().Format(time.RFC3339), - IsRecurring: true, + IsRecurring: *body.IsRecurring, IsVacation: false, CategoryID: body.CategoryID, Title: body.Title, UserID: userCtx.UserID, Timezone: "America/Los_Angeles", + IsCanceled: &f, } err = db.NewQuery(nil, 100).InsertSchedule(segment) if err != nil { @@ -150,12 +154,13 @@ func (e ScheduleSegment) postSegment(w http.ResponseWriter, r *http.Request) { ID: eventID, StartTime: startTime.Format(time.RFC3339), EndTime: endTime.Format(time.RFC3339), - IsRecurring: true, + IsRecurring: *body.IsRecurring, IsVacation: false, CategoryID: body.CategoryID, - Title: "Test Title", + Title: body.Title, UserID: userCtx.UserID, Timezone: body.Timezone, + IsCanceled: &f, } err := db.NewQuery(nil, 100).InsertSchedule(s) @@ -165,7 +170,7 @@ func (e ScheduleSegment) postSegment(w http.ResponseWriter, r *http.Request) { } } } - dbr, err := db.NewQuery(nil, 100).GetSchedule(database.ScheduleSegment{ID: segment.ID}, time.Now()) + dbr, err := db.NewQuery(nil, 100).GetSchedule(database.ScheduleSegment{ID: eventID}, time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC)) if err != nil { mock_errors.WriteServerError(w, err.Error()) return @@ -176,9 +181,135 @@ func (e ScheduleSegment) postSegment(w http.ResponseWriter, r *http.Request) { } func (e ScheduleSegment) deleteSegment(w http.ResponseWriter, r *http.Request) { + userCtx := r.Context().Value("auth").(authentication.UserAuthentication) + id := r.URL.Query().Get("id") + if !userCtx.MatchesBroadcasterIDParam(r) { + mock_errors.WriteBadRequest(w, "User token does not match broadcaster_id parameter") + return + } + if id == "" { + mock_errors.WriteBadRequest(w, "Missing required parameter id") + return + } + + err := db.NewQuery(nil, 100).DeleteSegment(id, userCtx.UserID) + if err != nil { + mock_errors.WriteServerError(w, err.Error()) + return + } + w.WriteHeader(http.StatusNoContent) } func (e ScheduleSegment) patchSegment(w http.ResponseWriter, r *http.Request) { + userCtx := r.Context().Value("auth").(authentication.UserAuthentication) + id := r.URL.Query().Get("id") + if !userCtx.MatchesBroadcasterIDParam(r) { + mock_errors.WriteBadRequest(w, "User token does not match broadcaster_id parameter") + return + } + + dbr, err := db.NewQuery(nil, 100).GetSchedule(database.ScheduleSegment{ID: id, UserID: userCtx.UserID}, time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC)) + if err != nil { + mock_errors.WriteServerError(w, err.Error()) + return + } + b := dbr.Data.(database.Schedule) + + if len(b.Segments) == 0 { + mock_errors.WriteBadRequest(w, "Invalid ID requested") + return + } + segment := b.Segments[0] + + var body SegmentPatchAndPostBody + err = json.NewDecoder(r.Body).Decode(&body) + if err != nil { + mock_errors.WriteBadRequest(w, "Error parsing body") + return + } + + // start_time + st, err := time.Parse(time.RFC3339, segment.StartTime) + if err != nil { + mock_errors.WriteServerError(w, err.Error()) + return + } + if body.StartTime != "" { + st, err = time.Parse(time.RFC3339, body.StartTime) + if err != nil { + mock_errors.WriteBadRequest(w, "Error parsing start_time") + return + } + } + // timezone + tz, err := time.LoadLocation(segment.Timezone) + if err != nil { + mock_errors.WriteServerError(w, err.Error()) + return + } + if body.Timezone != "" { + tz, err = time.LoadLocation(body.Timezone) + if err != nil { + mock_errors.WriteBadRequest(w, "Error parsing timezone") + return + } + } + + // is_canceled + isCanceled := false + if body.IsCanceled != nil { + isCanceled = *body.IsCanceled + } + + // title + title := segment.Title + if body.Title != "" { + if len(body.Title) > 140 { + mock_errors.WriteBadRequest(w, "Title must be less than 140 characters") + return + } + title = body.Title + } + + // duration + et, err := time.Parse(time.RFC3339, segment.EndTime) + if err != nil { + mock_errors.WriteServerError(w, err.Error()) + return + } + if body.Duration != "" { + duration, err := strconv.Atoi(body.Duration) + if err != nil { + mock_errors.WriteBadRequest(w, "Invalid duration provided") + return + } + + et = st.Add(time.Duration(duration) * time.Minute) + } + + s := database.ScheduleSegment{ + ID: segment.ID, + StartTime: st.UTC().Format(time.RFC3339), + EndTime: et.UTC().Format(time.RFC3339), + IsCanceled: &isCanceled, + Timezone: tz.String(), + Title: title, + } + + err = db.NewQuery(r, 20).UpdateSegment(s) + if err != nil { + mock_errors.WriteServerError(w, err.Error()) + return + } + + dbr, err = db.NewQuery(nil, 100).GetSchedule(database.ScheduleSegment{ID: segment.ID}, time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC)) + if err != nil { + mock_errors.WriteServerError(w, err.Error()) + return + } + b = dbr.Data.(database.Schedule) + bytes, _ := json.Marshal(b) + w.Write(bytes) } diff --git a/internal/mock_api/endpoints/schedule/settings.go b/internal/mock_api/endpoints/schedule/settings.go index 4192450d..c67f1c91 100644 --- a/internal/mock_api/endpoints/schedule/settings.go +++ b/internal/mock_api/endpoints/schedule/settings.go @@ -3,9 +3,18 @@ package schedule import ( + "database/sql" + "encoding/base64" + "encoding/json" + "errors" + "fmt" "net/http" + "time" "github.com/twitchdev/twitch-cli/internal/database" + "github.com/twitchdev/twitch-cli/internal/mock_api/authentication" + "github.com/twitchdev/twitch-cli/internal/mock_api/mock_errors" + "github.com/twitchdev/twitch-cli/internal/util" ) var scheduleSettingsMethodsSupported = map[string]bool{ @@ -26,6 +35,13 @@ var scheduleSettingsScopesByMethod = map[string][]string{ type ScheduleSettings struct{} +type PatchSettingsBody struct { + IsVacationEnabled *bool `json:"is_vacation_enabled"` + VacationStartTime string `json:"vacation_start_time"` + VacationEndTime string `json:"vacation_end_time"` + Timezone string `json:"timezone"` +} + func (e ScheduleSettings) Path() string { return "/schedule/settings" } func (e ScheduleSettings) GetRequiredScopes(method string) []string { @@ -39,5 +55,89 @@ func (e ScheduleSettings) ValidMethod(method string) bool { func (e ScheduleSettings) ServeHTTP(w http.ResponseWriter, r *http.Request) { db = r.Context().Value("db").(database.CLIDatabase) - w.WriteHeader(200) + switch r.Method { + case http.MethodPatch: + e.patchSchedule(w, r) + } +} + +func (e ScheduleSettings) patchSchedule(w http.ResponseWriter, r *http.Request) { + userCtx := r.Context().Value("auth").(authentication.UserAuthentication) + if !userCtx.MatchesBroadcasterIDParam(r) { + mock_errors.WriteBadRequest(w, "User token does not match broadcaster_id parameter") + return + } + + vacation, err := db.NewQuery(r, 100).GetVacations(database.ScheduleSegment{UserID: userCtx.UserID}) + if err != nil { + if !errors.As(err, &sql.ErrNoRows) { + mock_errors.WriteServerError(w, err.Error()) + return + } + } + + var body PatchSettingsBody + err = json.NewDecoder(r.Body).Decode(&body) + + if body.IsVacationEnabled == nil { + w.WriteHeader(http.StatusNoContent) + return + } + + if *body.IsVacationEnabled == false { + if vacation.ID != "" { + err := db.NewQuery(r, 100).DeleteSegment(vacation.ID, userCtx.UserID) + if err != nil { + mock_errors.WriteServerError(w, err.Error()) + return + } + } + w.WriteHeader(http.StatusNoContent) + return + } + + if vacation.ID != "" && *body.IsVacationEnabled == true { + mock_errors.WriteBadRequest(w, "Existing vacation already exists") + return + } + + if body.Timezone == "" || body.VacationStartTime == "" || body.VacationEndTime == "" { + mock_errors.WriteBadRequest(w, "Missing required parameter") + return + } + + _, err = time.LoadLocation(body.Timezone) + if err != nil { + mock_errors.WriteBadRequest(w, "Invalid timezone requested") + return + } + + st, err := time.Parse(time.RFC3339, body.VacationStartTime) + if err != nil { + mock_errors.WriteBadRequest(w, "Invalid vacation_start_time requested") + return + } + + et, err := time.Parse(time.RFC3339, body.VacationEndTime) + if err != nil { + mock_errors.WriteBadRequest(w, "Invalid vacation_end_time requested") + return + } + f := false + err = db.NewQuery(r, 100).InsertSchedule(database.ScheduleSegment{ + ID: base64.RawStdEncoding.EncodeToString([]byte(fmt.Sprintf("%v\\%v", util.RandomGUID(), st))), + StartTime: st.UTC().Format(time.RFC3339), + EndTime: et.UTC().Format(time.RFC3339), + IsVacation: true, + IsRecurring: false, + IsCanceled: &f, + UserID: userCtx.UserID, + }) + if err != nil { + mock_errors.WriteServerError(w, err.Error()) + return + } + + w.WriteHeader(http.StatusNoContent) + } diff --git a/internal/mock_api/generate/generate.go b/internal/mock_api/generate/generate.go index d9bc6488..bd548a1b 100644 --- a/internal/mock_api/generate/generate.go +++ b/internal/mock_api/generate/generate.go @@ -25,6 +25,8 @@ type UserInfo struct { Type string } +var f = false + func Generate(userCount int) error { db, err := database.NewConnection() if err != nil { @@ -266,6 +268,7 @@ func generateUsers(ctx context.Context, count int) error { Title: "Test Title", UserID: broadcaster.ID, Timezone: "America/Los_Angeles", + IsCanceled: &f, } err := db.NewQuery(nil, 100).InsertSchedule(segment) diff --git a/internal/mock_auth/mock_auth.go b/internal/mock_auth/mock_auth.go index d63f0b62..f3c91289 100644 --- a/internal/mock_auth/mock_auth.go +++ b/internal/mock_auth/mock_auth.go @@ -39,6 +39,7 @@ var validScopesByTokenType = map[string]map[string]bool{ "channel:read:polls": true, "channel:read:predictions": true, "channel:read:redemptions": true, + "channel:manage:schedule": true, "channel:read:stream_key": true, "channel:read:subscriptions": true, "clips:edit": true, From cf19c40b4ec1f9f88804b6dda27e60283f675944 Mon Sep 17 00:00:00 2001 From: lleadbet Date: Fri, 16 Jul 2021 13:06:00 -0700 Subject: [PATCH 09/36] adding basic schedule unit tests --- .../endpoints/schedule/scehdule_test.go | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 internal/mock_api/endpoints/schedule/scehdule_test.go diff --git a/internal/mock_api/endpoints/schedule/scehdule_test.go b/internal/mock_api/endpoints/schedule/scehdule_test.go new file mode 100644 index 00000000..7d61ad5a --- /dev/null +++ b/internal/mock_api/endpoints/schedule/scehdule_test.go @@ -0,0 +1,182 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package schedule + +import ( + "bytes" + "encoding/json" + "log" + "net/http" + "os" + "testing" + "time" + + "github.com/twitchdev/twitch-cli/internal/database" + "github.com/twitchdev/twitch-cli/internal/util" + "github.com/twitchdev/twitch-cli/test_setup" + "github.com/twitchdev/twitch-cli/test_setup/test_server" +) + +type RewardResponse struct { + Data []database.ChannelPointsReward `json:"data"` +} + +var ( + segment database.ScheduleSegment +) + +func TestMain(m *testing.M) { + test_setup.SetupTestEnv(&testing.T{}) + + db, err := database.NewConnection() + if err != nil { + log.Fatal(err) + } + f := false + s := database.ScheduleSegment{ + ID: util.RandomGUID(), + UserID: "1", + Title: "from_unit_tests", + IsRecurring: true, + IsVacation: false, + StartTime: time.Now().UTC().Format(time.RFC3339), + EndTime: time.Now().UTC().Add(24 * time.Hour).Format(time.RFC3339), + IsCanceled: &f, + } + err = db.NewQuery(nil, 100).InsertSchedule(s) + if err != nil { + log.Fatal(err) + } + + dbr, err := db.NewQuery(nil, 100).GetSchedule(database.ScheduleSegment{UserID: "1", ID: s.ID}, time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC)) + log.Printf("%v %#v", err, dbr.Data) + + segment = s + db.DB.Close() + + os.Exit(m.Run()) +} +func TestSchedule(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + ts := test_server.SetupTestServer(Schedule{}) + + // get + req, _ := http.NewRequest(http.MethodGet, ts.URL+Schedule{}.Path(), nil) + q := req.URL.Query() + q.Set("broadcaster_id", "1") + req.URL.RawQuery = q.Encode() + resp, err := http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(200, resp.StatusCode) + + q.Set("broadcaster_id", "2") + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(401, resp.StatusCode) + + q.Set("broadcaster_id", "1") + q.Set("id", segment.ID) + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(200, resp.StatusCode) +} + +func TestICal(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + ts := test_server.SetupTestServer(ScheduleICal{}) + req, _ := http.NewRequest(http.MethodGet, ts.URL+Schedule{}.Path(), nil) + q := req.URL.Query() + req.URL.RawQuery = q.Encode() + resp, err := http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(200, resp.StatusCode) +} + +func TestSegment(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + ts := test_server.SetupTestServer(ScheduleSegment{}) + + // post tests + body := SegmentPatchAndPostBody{ + Title: "hello", + Timezone: "America/Los_Angeles", + StartTime: time.Now().Format(time.RFC3339), + } + + b, _ := json.Marshal(body) + req, _ := http.NewRequest(http.MethodPost, ts.URL+ScheduleSegment{}.Path(), bytes.NewBuffer(b)) + q := req.URL.Query() + q.Set("broadcaster_id", "1") + req.URL.RawQuery = q.Encode() + resp, err := http.DefaultClient.Do(req) + a.Nil(err) + a.NotNil(resp) + a.Equal(200, resp.StatusCode) + + body.Title = "" + b, _ = json.Marshal(body) + req, _ = http.NewRequest(http.MethodPost, ts.URL+ScheduleSegment{}.Path(), bytes.NewBuffer(b)) + q.Set("broadcaster_id", "1") + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(200, resp.StatusCode) + + b, _ = json.Marshal(body) + req, _ = http.NewRequest(http.MethodPost, ts.URL+ScheduleSegment{}.Path(), bytes.NewBuffer(b)) + q.Set("broadcaster_id", "2") + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(401, resp.StatusCode) + + body.Title = "testing" + body.Timezone = "" + b, _ = json.Marshal(body) + req, _ = http.NewRequest(http.MethodPost, ts.URL+ScheduleSegment{}.Path(), bytes.NewBuffer(b)) + q.Set("broadcaster_id", "1") + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(400, resp.StatusCode) + + // patch + // no id + b, _ = json.Marshal(body) + req, _ = http.NewRequest(http.MethodPatch, ts.URL+ScheduleSegment{}.Path(), bytes.NewBuffer(b)) + q.Set("broadcaster_id", "1") + q.Del("id") + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(400, resp.StatusCode) + + //mismatch bid and token + b, _ = json.Marshal(body) + req, _ = http.NewRequest(http.MethodPatch, ts.URL+ScheduleSegment{}.Path(), bytes.NewBuffer(b)) + q.Set("broadcaster_id", "2") + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(401, resp.StatusCode) + + // good request + body.Title = "patched_title" + b, _ = json.Marshal(body) + req, _ = http.NewRequest(http.MethodPatch, ts.URL+ScheduleSegment{}.Path(), bytes.NewBuffer(b)) + q.Set("broadcaster_id", "1") + q.Set("id", segment.ID) + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(200, resp.StatusCode) +} + +func TestSettings(t *testing.T) { + +} From f33a73d4d5c0dbc1ab6b7b511589b36909790913 Mon Sep 17 00:00:00 2001 From: Benoit Verreault Date: Thu, 8 Jul 2021 15:57:36 -0400 Subject: [PATCH 10/36] Adding support for transaction events with eventsub transport. --- internal/events/trigger/trigger_event_test.go | 3 +- .../transaction_event.go | 52 +++++++++++++++++-- .../transaction_event_test.go | 16 +++++- internal/models/eventsub.go | 1 + internal/models/transactions.go | 24 +++++++++ 5 files changed, 90 insertions(+), 6 deletions(-) diff --git a/internal/events/trigger/trigger_event_test.go b/internal/events/trigger/trigger_event_test.go index d47c3730..5471faf1 100644 --- a/internal/events/trigger/trigger_event_test.go +++ b/internal/events/trigger/trigger_event_test.go @@ -172,7 +172,8 @@ func TestFire(t *testing.T) { Count: 0, } res, err = Fire(params) - a.NotNil(err) + a.Nil(err) + a.NotEmpty(res) params = *&TriggerParameters{ Event: "add-reward", diff --git a/internal/events/types/extension_transaction/transaction_event.go b/internal/events/types/extension_transaction/transaction_event.go index 60a8e0a3..cebb8bc1 100644 --- a/internal/events/types/extension_transaction/transaction_event.go +++ b/internal/events/types/extension_transaction/transaction_event.go @@ -4,9 +4,9 @@ package extension_transaction import ( "encoding/json" - "errors" "time" + "github.com/spf13/viper" "github.com/twitchdev/twitch-cli/internal/events" "github.com/twitchdev/twitch-cli/internal/models" "github.com/twitchdev/twitch-cli/internal/util" @@ -14,7 +14,7 @@ import ( var transportsSupported = map[string]bool{ models.TransportWebSub: true, - models.TransportEventSub: false, + models.TransportEventSub: true, } var triggerSupported = []string{"transaction"} @@ -23,6 +23,9 @@ var triggerMapping = map[string]map[string]string{ models.TransportWebSub: { "transaction": "transaction", }, + models.TransportEventSub: { + "transaction": "extension.bits_transaction.create", + }, } type Event struct{} @@ -31,12 +34,55 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven var event []byte var err error + clientID := viper.GetString("clientId") + + // if not configured, generate a random one + if clientID == "" { + clientID = util.RandomClientID() + } + if params.Cost == 0 { params.Cost = 100 } switch params.Transport { case models.TransportEventSub: - return events.MockEventResponse{}, errors.New("Extension transactions are unsupported on Eventsub") + body := &models.TransactionEventSubResponse{ + Subscription: models.EventsubSubscription{ + ID: params.ID, + Status: "enabled", + Type: triggerMapping[params.Transport][params.Trigger], + Version: "1", + Condition: models.EventsubCondition{ + ExtensionClientID: clientID, + }, + Transport: models.EventsubTransport{ + Method: "webhook", + Callback: "null", + }, + Cost: 1, + CreatedAt: util.GetTimestamp().Format(time.RFC3339Nano), + }, + Event: models.TransactionEventSubEvent{ + ID: params.ID, + ExtensionClientID: clientID, + BroadcasterUserID: params.ToUserID, + BroadcasterUserLogin: "testBroadcaster", + BroadcasterUserName: "testBroadcaster", + UserName: "testUser", + UserLogin: "testUser", + UserID: params.FromUserID, + Product: models.TransactionEventSubProduct{ + Name: "Test Trigger Item from CLI", + Sku: "testItemSku", + Bits: params.Cost, + InDevelopment: true, + }, + }, + } + event, err = json.Marshal(body) + if err != nil { + return events.MockEventResponse{}, err + } case models.TransportWebSub: body := *&models.TransactionWebSubResponse{ Data: []models.TransactionWebsubEvent{ diff --git a/internal/events/types/extension_transaction/transaction_event_test.go b/internal/events/types/extension_transaction/transaction_event_test.go index 9b146f4c..53f911a6 100644 --- a/internal/events/types/extension_transaction/transaction_event_test.go +++ b/internal/events/types/extension_transaction/transaction_event_test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "testing" + "github.com/spf13/viper" "github.com/twitchdev/twitch-cli/internal/events" "github.com/twitchdev/twitch-cli/internal/models" "github.com/twitchdev/twitch-cli/test_setup" @@ -13,6 +14,7 @@ import ( var fromUser = "1234" var toUser = "4567" +var clientId = "7890" func TestWebusbTransaction(t *testing.T) { a := test_setup.SetupTestEnv(t) @@ -46,8 +48,18 @@ func TestEventsubTransaction(t *testing.T) { Trigger: "transaction", } - _, err := Event{}.GenerateEvent(params) - a.NotNil(err) + viper.Set("clientId", clientId) + + r, err := Event{}.GenerateEvent(params) + a.Nil(err) + + var body models.TransactionEventSubResponse + err = json.Unmarshal(r.JSON, &body) + a.Nil(err) + + a.Equal(toUser, body.Event.BroadcasterUserID, "Expected to user %v, got %v", toUser, body.Event.BroadcasterUserID) + a.Equal(fromUser, body.Event.UserID, "Expected from user %v, got %v", fromUser, body.Event.UserID) + a.Equal(clientId, body.Event.ExtensionClientID, "Expected client id %v, got %v", clientId, body.Event.ExtensionClientID) } func TestFakeTransport(t *testing.T) { diff --git a/internal/models/eventsub.go b/internal/models/eventsub.go index 3f17b77a..7c88d736 100644 --- a/internal/models/eventsub.go +++ b/internal/models/eventsub.go @@ -23,6 +23,7 @@ type EventsubCondition struct { ToBroadcasterUserID string `json:"to_broadcaster_user_id,omitempty"` FromBroadcasterUserID string `json:"from_broadcaster_user_id,omitempty"` ClientID string `json:"client_id,omitempty"` + ExtensionClientID string `json:"extension_client_id,omitempty"` } type EventsubResponse struct { diff --git a/internal/models/transactions.go b/internal/models/transactions.go index f231e842..c7bd47f6 100644 --- a/internal/models/transactions.go +++ b/internal/models/transactions.go @@ -2,6 +2,30 @@ // SPDX-License-Identifier: Apache-2.0 package models +type TransactionEventSubEvent struct { + ID string `json:"id"` + ExtensionClientID string `json:"extension_client_id"` + BroadcasterUserID string `json:"broadcaster_user_id"` + BroadcasterUserLogin string `json:"broadcaster_user_login"` + BroadcasterUserName string `json:"broadcaster_user_name"` + UserName string `json:"user_name"` + UserLogin string `json:"user_login"` + UserID string `json:"user_id"` + Product TransactionEventSubProduct `json:"product"` +} + +type TransactionEventSubProduct struct { + Name string `json:"name"` + Sku string `json:"sku"` + Bits int64 `json:"bits"` + InDevelopment bool `json:"in_development"` +} + +type TransactionEventSubResponse struct { + Subscription EventsubSubscription `json:"subscription"` + Event TransactionEventSubEvent `json:"event"` +} + type TransactionWebsubEvent struct { ID string `json:"id"` Timestamp string `json:"timestamp"` From cbefd55c89059957b06d719e41dd297b6e641427 Mon Sep 17 00:00:00 2001 From: zneix Date: Wed, 14 Jul 2021 22:05:38 +0200 Subject: [PATCH 11/36] Fixed a crash upon trying to use query param with no value --- internal/api/api.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/internal/api/api.go b/internal/api/api.go index 969e2559..acd7f52d 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -56,9 +56,13 @@ func NewRequest(method string, path string, queryParameters []string, body []byt } q := u.Query() - for _, param := range queryParameters { - value := strings.Split(param, "=") - q.Add(value[0], value[1]) + for _, paramStr := range queryParameters { + var value string + param := strings.Split(paramStr, "=") + if len(param) == 2 { + value = param[1] + } + q.Add(param[0], value) } if cursor != "" { From ba00f5fe2e44a0b3fd39c36d06e4fa4050cf280e Mon Sep 17 00:00:00 2001 From: lleadbet Date: Wed, 14 Jul 2021 15:29:44 -0700 Subject: [PATCH 12/36] Adding exclusion for schedule APIs --- internal/api/api.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/api/api.go b/internal/api/api.go index acd7f52d..9d81662f 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -107,7 +107,11 @@ func NewRequest(method string, path string, queryParameters []string, body []byt } d := data.Data.([]interface{}) - data.Data = append(d, apiResponse.Data.([]interface{})...) + if strings.Contains(path, "schedule") { + data.Data = append(d, apiResponse.Data) + } else { + data.Data = append(d, apiResponse.Data.([]interface{})...) + } if apiResponse.Pagination == nil || *&apiResponse.Pagination.Cursor == "" { break From cb0a99c351e49122e94f1b2af5de0d02abc81c60 Mon Sep 17 00:00:00 2001 From: lleadbet Date: Wed, 14 Jul 2021 15:31:29 -0700 Subject: [PATCH 13/36] fixing tests --- internal/api/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/api/api.go b/internal/api/api.go index 9d81662f..498f0e38 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -107,7 +107,7 @@ func NewRequest(method string, path string, queryParameters []string, body []byt } d := data.Data.([]interface{}) - if strings.Contains(path, "schedule") { + if strings.Contains(path, "schedule") || apiResponse.Data == nil { data.Data = append(d, apiResponse.Data) } else { data.Data = append(d, apiResponse.Data.([]interface{})...) From f267dc961341c097121bd7cc336bda4b58444973 Mon Sep 17 00:00:00 2001 From: lleadbet Date: Wed, 14 Jul 2021 15:25:51 -0700 Subject: [PATCH 14/36] adding schedules api support --- internal/database/init.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/database/init.go b/internal/database/init.go index 174f040b..8dbd0876 100644 --- a/internal/database/init.go +++ b/internal/database/init.go @@ -31,7 +31,11 @@ var migrateSQL = map[int]migrateMap{ Message: "Adding mock API tables.", }, 3: { +<<<<<<< HEAD SQL: `alter table drops_entitlements add column status text not null default 'CLAIMED'; create table stream_schedule( id text not null primary key, broadcaster_id text not null, starttime text not null, endtime text not null, timezone text not null, is_vacation boolean not null default false, is_recurring boolean not null default false, is_canceled boolean not null default false, title text, category_id text, foreign key(broadcaster_id) references users(id), foreign key (category_id) references categories(id));`, +======= + SQL: `alter table drops_entitlements add column status text not null default 'CLAIMED'; create table stream_schedule( id text not null primary key, broadcaster_id text not null, starttime text not null, endtime text not null, timezone text not null, is_vacation boolean not null default false, is_recurring boolean not null default false, category_id text, title text, foreign key(broadcaster_id) references users(id), foreign key (category_id) references categories(id));`, +>>>>>>> 9ae4d30 (adding schedules api support) Message: ``, }, } @@ -69,7 +73,11 @@ func checkAndUpdate(db sqlx.DB) error { } func initDatabase(db sqlx.DB) error { +<<<<<<< HEAD createSQL := `create table events( id text not null primary key, event text not null, json text not null, from_user text not null, to_user text not null, transport text not null, timestamp text not null); create table categories( id text not null primary key, category_name text not null ); create table users( id text not null primary key, user_login text not null, display_name text not null, email text not null, user_type text, broadcaster_type text, user_description text, created_at text not null, category_id text, modified_at text, stream_language text not null default 'en', title text not null default '', delay int not null default 0, foreign key (category_id) references categories(id) ); create table follows ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table blocks ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table bans ( broadcaster_id text not null, user_id text not null, created_at text not null, expires_at text, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table ban_events ( id text not null primary key, event_timestamp text not null, event_type text not null, event_version text not null default '1.0', broadcaster_id text not null, user_id text not null, expires_at text, foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table moderators ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table moderator_actions ( id text not null primary key, event_timestamp text not null, event_type text not null, event_version text not null default '1.0', broadcaster_id text not null, user_id text not null, foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table editors ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table channel_points_rewards( id text not null primary key, broadcaster_id text not null, reward_image text, background_color text, is_enabled boolean not null default false, cost int not null default 0, title text not null, reward_prompt text, is_user_input_required boolean default false, stream_max_enabled boolean default false, stream_max_count int default 0, stream_user_max_enabled boolean default false, stream_user_max_count int default 0, global_cooldown_enabled boolean default false, global_cooldown_seconds int default 0, is_paused boolean default false, is_in_stock boolean default true, should_redemptions_skip_queue boolean default false, redemptions_redeemed_current_stream int, cooldown_expires_at text, foreign key (broadcaster_id) references users(id) ); create table channel_points_redemptions( id text not null primary key, reward_id text not null, broadcaster_id text not null, user_id text not null, user_input text, redemption_status text not null, redeemed_at text, foreign key (reward_id) references channel_points_rewards(id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table streams( id text not null primary key, broadcaster_id id text not null, stream_type text not null default 'live', viewer_count int not null, started_at text not null, is_mature boolean not null default false, foreign key (broadcaster_id) references users(id) ); create table tags( id text not null primary key, is_auto boolean not null default false, tag_name text not null ); create table stream_tags( user_id text not null, tag_id text not null, primary key(user_id, tag_id), foreign key(user_id) references users(id), foreign key(tag_id) references tags(id) ); create table teams( id text not null primary key, background_image_url text, banner text, created_at text not null, updated_at text, info text, thumbnail_url text, team_name text, team_display_name text ); create table team_members( team_id text not null, user_id text not null, primary key (team_id, user_id) foreign key (team_id) references teams(id), foreign key (user_id) references users(id) ); create table videos( id text not null primary key, stream_id text, broadcaster_id text not null, title text not null, video_description text not null, created_at text not null, published_at text, viewable text not null, view_count int not null default 0, duration text not null, video_language text not null default 'en', category_id text, type text default 'archive', foreign key (stream_id) references streams(id), foreign key (broadcaster_id) references users(id), foreign key (category_id) references categories(id) ); create table stream_markers( id text not null primary key, video_id text not null, position_seconds int not null, created_at text not null, description text not null, broadcaster_id text not null, foreign key (broadcaster_id) references users(id), foreign key (video_id) references videos(id) ); create table video_muted_segments ( video_id text not null, video_offset int not null, duration int not null, primary key (video_id, video_offset), foreign key (video_id) references videos(id) ); create table subscriptions ( broadcaster_id text not null, user_id text not null, is_gift boolean not null default false, gifter_id text, tier text not null default '1000', created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id), foreign key (gifter_id) references users(id) ); create table drops_entitlements( id text not null primary key, benefit_id text not null, timestamp text not null, user_id text not null, game_id text not null, status text not null default 'CLAIMED', foreign key (user_id) references users(id), foreign key (game_id) references categories(id) ); create table clients ( id text not null primary key, secret text not null, is_extension boolean default false, name text not null ); create table authorizations ( id integer not null primary key AUTOINCREMENT, client_id text not null, user_id text, token text not null unique, expires_at text not null, scopes text, foreign key (client_id) references clients(id) ); create table polls ( id text not null primary key, broadcaster_id text not null, title text not null, bits_voting_enabled boolean default false, bits_per_vote int default 10, channel_points_voting_enabled boolean default false, channel_points_per_vote int default 10, status text not null, duration int not null, started_at text not null, ended_at text, foreign key (broadcaster_id) references users(id) ); create table poll_choices ( id text not null primary key, title text not null, votes int not null default 0, channel_points_votes int not null default 0, bits_votes int not null default 0, poll_id text not null, foreign key (poll_id) references polls(id) ); create table predictions ( id text not null primary key, broadcaster_id text not null, title text not null, winning_outcome_id text, prediction_window int, status text not null, created_at text not null, ended_at text, locked_at text, foreign key (broadcaster_id) references users(id) ); create table prediction_outcomes ( id text not null primary key, title text not null, users int not null default 0, channel_points int not null default 0, color text not null, prediction_id text not null, foreign key (prediction_id) references predictions(id) ); create table prediction_predictions ( prediction_id text not null, user_id text not null, amount int not null, outcome_id text not null, primary key(prediction_id, user_id), foreign key(user_id) references users(id), foreign key(prediction_id) references predictions(id), foreign key(outcome_id) references prediction_outcomes(id) ); create table clips ( id text not null primary key, broadcaster_id text not null, creator_id text not null, video_id text not null, game_id text not null, title text not null, view_count int default 0, created_at text not null, duration real not null, foreign key (broadcaster_id) references users(id), foreign key (creator_id) references users(id) ); create table stream_schedule( id text not null primary key, broadcaster_id text not null, starttime text not null, endtime text not null, timezone text not null, is_vacation boolean not null default false, is_recurring boolean not null default false, is_canceled boolean not null default false, title text, category_id text, foreign key(broadcaster_id) references users(id), foreign key (category_id) references categories(id));` +======= + createSQL := `create table events( id text not null primary key, event text not null, json text not null, from_user text not null, to_user text not null, transport text not null, timestamp text not null); create table categories( id text not null primary key, category_name text not null ); create table users( id text not null primary key, user_login text not null, display_name text not null, email text not null, user_type text, broadcaster_type text, user_description text, created_at text not null, category_id text, modified_at text, stream_language text not null default 'en', title text not null default '', delay int not null default 0, foreign key (category_id) references categories(id) ); create table follows ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table blocks ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table bans ( broadcaster_id text not null, user_id text not null, created_at text not null, expires_at text, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table ban_events ( id text not null primary key, event_timestamp text not null, event_type text not null, event_version text not null default '1.0', broadcaster_id text not null, user_id text not null, expires_at text, foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table moderators ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table moderator_actions ( id text not null primary key, event_timestamp text not null, event_type text not null, event_version text not null default '1.0', broadcaster_id text not null, user_id text not null, foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table editors ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table channel_points_rewards( id text not null primary key, broadcaster_id text not null, reward_image text, background_color text, is_enabled boolean not null default false, cost int not null default 0, title text not null, reward_prompt text, is_user_input_required boolean default false, stream_max_enabled boolean default false, stream_max_count int default 0, stream_user_max_enabled boolean default false, stream_user_max_count int default 0, global_cooldown_enabled boolean default false, global_cooldown_seconds int default 0, is_paused boolean default false, is_in_stock boolean default true, should_redemptions_skip_queue boolean default false, redemptions_redeemed_current_stream int, cooldown_expires_at text, foreign key (broadcaster_id) references users(id) ); create table channel_points_redemptions( id text not null primary key, reward_id text not null, broadcaster_id text not null, user_id text not null, user_input text, redemption_status text not null, redeemed_at text, foreign key (reward_id) references channel_points_rewards(id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table streams( id text not null primary key, broadcaster_id id text not null, stream_type text not null default 'live', viewer_count int not null, started_at text not null, is_mature boolean not null default false, foreign key (broadcaster_id) references users(id) ); create table tags( id text not null primary key, is_auto boolean not null default false, tag_name text not null ); create table stream_tags( user_id text not null, tag_id text not null, primary key(user_id, tag_id), foreign key(user_id) references users(id), foreign key(tag_id) references tags(id) ); create table teams( id text not null primary key, background_image_url text, banner text, created_at text not null, updated_at text, info text, thumbnail_url text, team_name text, team_display_name text ); create table team_members( team_id text not null, user_id text not null, primary key (team_id, user_id) foreign key (team_id) references teams(id), foreign key (user_id) references users(id) ); create table videos( id text not null primary key, stream_id text, broadcaster_id text not null, title text not null, video_description text not null, created_at text not null, published_at text, viewable text not null, view_count int not null default 0, duration text not null, video_language text not null default 'en', category_id text, type text default 'archive', foreign key (stream_id) references streams(id), foreign key (broadcaster_id) references users(id), foreign key (category_id) references categories(id) ); create table stream_markers( id text not null primary key, video_id text not null, position_seconds int not null, created_at text not null, description text not null, broadcaster_id text not null, foreign key (broadcaster_id) references users(id), foreign key (video_id) references videos(id) ); create table video_muted_segments ( video_id text not null, video_offset int not null, duration int not null, primary key (video_id, video_offset), foreign key (video_id) references videos(id) ); create table subscriptions ( broadcaster_id text not null, user_id text not null, is_gift boolean not null default false, gifter_id text, tier text not null default '1000', created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id), foreign key (gifter_id) references users(id) ); create table drops_entitlements( id text not null primary key, benefit_id text not null, timestamp text not null, user_id text not null, game_id text not null, status text not null default 'CLAIMED', foreign key (user_id) references users(id), foreign key (game_id) references categories(id) ); create table clients ( id text not null primary key, secret text not null, is_extension boolean default false, name text not null ); create table authorizations ( id integer not null primary key AUTOINCREMENT, client_id text not null, user_id text, token text not null unique, expires_at text not null, scopes text, foreign key (client_id) references clients(id) ); create table polls ( id text not null primary key, broadcaster_id text not null, title text not null, bits_voting_enabled boolean default false, bits_per_vote int default 10, channel_points_voting_enabled boolean default false, channel_points_per_vote int default 10, status text not null, duration int not null, started_at text not null, ended_at text, foreign key (broadcaster_id) references users(id) ); create table poll_choices ( id text not null primary key, title text not null, votes int not null default 0, channel_points_votes int not null default 0, bits_votes int not null default 0, poll_id text not null, foreign key (poll_id) references polls(id) ); create table predictions ( id text not null primary key, broadcaster_id text not null, title text not null, winning_outcome_id text, prediction_window int, status text not null, created_at text not null, ended_at text, locked_at text, foreign key (broadcaster_id) references users(id) ); create table prediction_outcomes ( id text not null primary key, title text not null, users int not null default 0, channel_points int not null default 0, color text not null, prediction_id text not null, foreign key (prediction_id) references predictions(id) ); create table prediction_predictions ( prediction_id text not null, user_id text not null, amount int not null, outcome_id text not null, primary key(prediction_id, user_id), foreign key(user_id) references users(id), foreign key(prediction_id) references predictions(id), foreign key(outcome_id) references prediction_outcomes(id) ); create table clips ( id text not null primary key, broadcaster_id text not null, creator_id text not null, video_id text not null, game_id text not null, title text not null, view_count int default 0, created_at text not null, duration real not null, foreign key (broadcaster_id) references users(id), foreign key (creator_id) references users(id) ); create table stream_schedule( id text not null primary key, broadcaster_id text not null, starttime text not null, endtime text not null, timezone text not null, is_vacation boolean not null default false, is_recurring boolean not null default false, category_id text, title text, foreign key(broadcaster_id) references users(id), foreign key (category_id) references categories(id));` +>>>>>>> 9ae4d30 (adding schedules api support) for i := 1; i <= 5; i++ { tx := db.MustBegin() tx.Exec(createSQL) From ff341bf24afff7a9946a13149f0e55d54e7fe52c Mon Sep 17 00:00:00 2001 From: lleadbet Date: Wed, 14 Jul 2021 15:25:57 -0700 Subject: [PATCH 15/36] adding schedules api support --- internal/mock_api/endpoints/schedule/segment.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/internal/mock_api/endpoints/schedule/segment.go b/internal/mock_api/endpoints/schedule/segment.go index 8ac9b3ca..06b1d30a 100644 --- a/internal/mock_api/endpoints/schedule/segment.go +++ b/internal/mock_api/endpoints/schedule/segment.go @@ -154,6 +154,7 @@ func (e ScheduleSegment) postSegment(w http.ResponseWriter, r *http.Request) { ID: eventID, StartTime: startTime.Format(time.RFC3339), EndTime: endTime.Format(time.RFC3339), +<<<<<<< HEAD IsRecurring: *body.IsRecurring, IsVacation: false, CategoryID: body.CategoryID, @@ -161,6 +162,14 @@ func (e ScheduleSegment) postSegment(w http.ResponseWriter, r *http.Request) { UserID: userCtx.UserID, Timezone: body.Timezone, IsCanceled: &f, +======= + IsRecurring: true, + IsVacation: false, + CategoryID: body.CategoryID, + Title: "Test Title", + UserID: userCtx.UserID, + Timezone: body.Timezone, +>>>>>>> 7985dac (adding schedules api support) } err := db.NewQuery(nil, 100).InsertSchedule(s) From f715ae5fb6f1ee32382ab29b8f36ff3c3aae66a3 Mon Sep 17 00:00:00 2001 From: lleadbet Date: Fri, 16 Jul 2021 13:05:53 -0700 Subject: [PATCH 16/36] adding basic schedule unit tests --- internal/mock_api/endpoints/schedule/segment.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/internal/mock_api/endpoints/schedule/segment.go b/internal/mock_api/endpoints/schedule/segment.go index 06b1d30a..8ac9b3ca 100644 --- a/internal/mock_api/endpoints/schedule/segment.go +++ b/internal/mock_api/endpoints/schedule/segment.go @@ -154,7 +154,6 @@ func (e ScheduleSegment) postSegment(w http.ResponseWriter, r *http.Request) { ID: eventID, StartTime: startTime.Format(time.RFC3339), EndTime: endTime.Format(time.RFC3339), -<<<<<<< HEAD IsRecurring: *body.IsRecurring, IsVacation: false, CategoryID: body.CategoryID, @@ -162,14 +161,6 @@ func (e ScheduleSegment) postSegment(w http.ResponseWriter, r *http.Request) { UserID: userCtx.UserID, Timezone: body.Timezone, IsCanceled: &f, -======= - IsRecurring: true, - IsVacation: false, - CategoryID: body.CategoryID, - Title: "Test Title", - UserID: userCtx.UserID, - Timezone: body.Timezone, ->>>>>>> 7985dac (adding schedules api support) } err := db.NewQuery(nil, 100).InsertSchedule(s) From f856b691459003111fbcc819cb3102b1054e6d6a Mon Sep 17 00:00:00 2001 From: lleadbet Date: Fri, 16 Jul 2021 13:17:54 -0700 Subject: [PATCH 17/36] merging main --- internal/mock_api/endpoints/schedule/scehdule_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/mock_api/endpoints/schedule/scehdule_test.go b/internal/mock_api/endpoints/schedule/scehdule_test.go index 7d61ad5a..3cb73387 100644 --- a/internal/mock_api/endpoints/schedule/scehdule_test.go +++ b/internal/mock_api/endpoints/schedule/scehdule_test.go @@ -48,7 +48,7 @@ func TestMain(m *testing.M) { log.Fatal(err) } - dbr, err := db.NewQuery(nil, 100).GetSchedule(database.ScheduleSegment{UserID: "1", ID: s.ID}, time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC)) + dbr, err := db.NewQuery(nil, 100).GetSchedule(database.ScheduleSegment{UserID: "1"}, time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC)) log.Printf("%v %#v", err, dbr.Data) segment = s From a3bea9ec0cbe10d846e00bb7256a8c61c957b955 Mon Sep 17 00:00:00 2001 From: lleadbet Date: Fri, 16 Jul 2021 13:22:33 -0700 Subject: [PATCH 18/36] merging main --- internal/database/init.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/internal/database/init.go b/internal/database/init.go index 8dbd0876..174f040b 100644 --- a/internal/database/init.go +++ b/internal/database/init.go @@ -31,11 +31,7 @@ var migrateSQL = map[int]migrateMap{ Message: "Adding mock API tables.", }, 3: { -<<<<<<< HEAD SQL: `alter table drops_entitlements add column status text not null default 'CLAIMED'; create table stream_schedule( id text not null primary key, broadcaster_id text not null, starttime text not null, endtime text not null, timezone text not null, is_vacation boolean not null default false, is_recurring boolean not null default false, is_canceled boolean not null default false, title text, category_id text, foreign key(broadcaster_id) references users(id), foreign key (category_id) references categories(id));`, -======= - SQL: `alter table drops_entitlements add column status text not null default 'CLAIMED'; create table stream_schedule( id text not null primary key, broadcaster_id text not null, starttime text not null, endtime text not null, timezone text not null, is_vacation boolean not null default false, is_recurring boolean not null default false, category_id text, title text, foreign key(broadcaster_id) references users(id), foreign key (category_id) references categories(id));`, ->>>>>>> 9ae4d30 (adding schedules api support) Message: ``, }, } @@ -73,11 +69,7 @@ func checkAndUpdate(db sqlx.DB) error { } func initDatabase(db sqlx.DB) error { -<<<<<<< HEAD createSQL := `create table events( id text not null primary key, event text not null, json text not null, from_user text not null, to_user text not null, transport text not null, timestamp text not null); create table categories( id text not null primary key, category_name text not null ); create table users( id text not null primary key, user_login text not null, display_name text not null, email text not null, user_type text, broadcaster_type text, user_description text, created_at text not null, category_id text, modified_at text, stream_language text not null default 'en', title text not null default '', delay int not null default 0, foreign key (category_id) references categories(id) ); create table follows ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table blocks ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table bans ( broadcaster_id text not null, user_id text not null, created_at text not null, expires_at text, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table ban_events ( id text not null primary key, event_timestamp text not null, event_type text not null, event_version text not null default '1.0', broadcaster_id text not null, user_id text not null, expires_at text, foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table moderators ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table moderator_actions ( id text not null primary key, event_timestamp text not null, event_type text not null, event_version text not null default '1.0', broadcaster_id text not null, user_id text not null, foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table editors ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table channel_points_rewards( id text not null primary key, broadcaster_id text not null, reward_image text, background_color text, is_enabled boolean not null default false, cost int not null default 0, title text not null, reward_prompt text, is_user_input_required boolean default false, stream_max_enabled boolean default false, stream_max_count int default 0, stream_user_max_enabled boolean default false, stream_user_max_count int default 0, global_cooldown_enabled boolean default false, global_cooldown_seconds int default 0, is_paused boolean default false, is_in_stock boolean default true, should_redemptions_skip_queue boolean default false, redemptions_redeemed_current_stream int, cooldown_expires_at text, foreign key (broadcaster_id) references users(id) ); create table channel_points_redemptions( id text not null primary key, reward_id text not null, broadcaster_id text not null, user_id text not null, user_input text, redemption_status text not null, redeemed_at text, foreign key (reward_id) references channel_points_rewards(id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table streams( id text not null primary key, broadcaster_id id text not null, stream_type text not null default 'live', viewer_count int not null, started_at text not null, is_mature boolean not null default false, foreign key (broadcaster_id) references users(id) ); create table tags( id text not null primary key, is_auto boolean not null default false, tag_name text not null ); create table stream_tags( user_id text not null, tag_id text not null, primary key(user_id, tag_id), foreign key(user_id) references users(id), foreign key(tag_id) references tags(id) ); create table teams( id text not null primary key, background_image_url text, banner text, created_at text not null, updated_at text, info text, thumbnail_url text, team_name text, team_display_name text ); create table team_members( team_id text not null, user_id text not null, primary key (team_id, user_id) foreign key (team_id) references teams(id), foreign key (user_id) references users(id) ); create table videos( id text not null primary key, stream_id text, broadcaster_id text not null, title text not null, video_description text not null, created_at text not null, published_at text, viewable text not null, view_count int not null default 0, duration text not null, video_language text not null default 'en', category_id text, type text default 'archive', foreign key (stream_id) references streams(id), foreign key (broadcaster_id) references users(id), foreign key (category_id) references categories(id) ); create table stream_markers( id text not null primary key, video_id text not null, position_seconds int not null, created_at text not null, description text not null, broadcaster_id text not null, foreign key (broadcaster_id) references users(id), foreign key (video_id) references videos(id) ); create table video_muted_segments ( video_id text not null, video_offset int not null, duration int not null, primary key (video_id, video_offset), foreign key (video_id) references videos(id) ); create table subscriptions ( broadcaster_id text not null, user_id text not null, is_gift boolean not null default false, gifter_id text, tier text not null default '1000', created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id), foreign key (gifter_id) references users(id) ); create table drops_entitlements( id text not null primary key, benefit_id text not null, timestamp text not null, user_id text not null, game_id text not null, status text not null default 'CLAIMED', foreign key (user_id) references users(id), foreign key (game_id) references categories(id) ); create table clients ( id text not null primary key, secret text not null, is_extension boolean default false, name text not null ); create table authorizations ( id integer not null primary key AUTOINCREMENT, client_id text not null, user_id text, token text not null unique, expires_at text not null, scopes text, foreign key (client_id) references clients(id) ); create table polls ( id text not null primary key, broadcaster_id text not null, title text not null, bits_voting_enabled boolean default false, bits_per_vote int default 10, channel_points_voting_enabled boolean default false, channel_points_per_vote int default 10, status text not null, duration int not null, started_at text not null, ended_at text, foreign key (broadcaster_id) references users(id) ); create table poll_choices ( id text not null primary key, title text not null, votes int not null default 0, channel_points_votes int not null default 0, bits_votes int not null default 0, poll_id text not null, foreign key (poll_id) references polls(id) ); create table predictions ( id text not null primary key, broadcaster_id text not null, title text not null, winning_outcome_id text, prediction_window int, status text not null, created_at text not null, ended_at text, locked_at text, foreign key (broadcaster_id) references users(id) ); create table prediction_outcomes ( id text not null primary key, title text not null, users int not null default 0, channel_points int not null default 0, color text not null, prediction_id text not null, foreign key (prediction_id) references predictions(id) ); create table prediction_predictions ( prediction_id text not null, user_id text not null, amount int not null, outcome_id text not null, primary key(prediction_id, user_id), foreign key(user_id) references users(id), foreign key(prediction_id) references predictions(id), foreign key(outcome_id) references prediction_outcomes(id) ); create table clips ( id text not null primary key, broadcaster_id text not null, creator_id text not null, video_id text not null, game_id text not null, title text not null, view_count int default 0, created_at text not null, duration real not null, foreign key (broadcaster_id) references users(id), foreign key (creator_id) references users(id) ); create table stream_schedule( id text not null primary key, broadcaster_id text not null, starttime text not null, endtime text not null, timezone text not null, is_vacation boolean not null default false, is_recurring boolean not null default false, is_canceled boolean not null default false, title text, category_id text, foreign key(broadcaster_id) references users(id), foreign key (category_id) references categories(id));` -======= - createSQL := `create table events( id text not null primary key, event text not null, json text not null, from_user text not null, to_user text not null, transport text not null, timestamp text not null); create table categories( id text not null primary key, category_name text not null ); create table users( id text not null primary key, user_login text not null, display_name text not null, email text not null, user_type text, broadcaster_type text, user_description text, created_at text not null, category_id text, modified_at text, stream_language text not null default 'en', title text not null default '', delay int not null default 0, foreign key (category_id) references categories(id) ); create table follows ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table blocks ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table bans ( broadcaster_id text not null, user_id text not null, created_at text not null, expires_at text, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table ban_events ( id text not null primary key, event_timestamp text not null, event_type text not null, event_version text not null default '1.0', broadcaster_id text not null, user_id text not null, expires_at text, foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table moderators ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table moderator_actions ( id text not null primary key, event_timestamp text not null, event_type text not null, event_version text not null default '1.0', broadcaster_id text not null, user_id text not null, foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table editors ( broadcaster_id text not null, user_id text not null, created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table channel_points_rewards( id text not null primary key, broadcaster_id text not null, reward_image text, background_color text, is_enabled boolean not null default false, cost int not null default 0, title text not null, reward_prompt text, is_user_input_required boolean default false, stream_max_enabled boolean default false, stream_max_count int default 0, stream_user_max_enabled boolean default false, stream_user_max_count int default 0, global_cooldown_enabled boolean default false, global_cooldown_seconds int default 0, is_paused boolean default false, is_in_stock boolean default true, should_redemptions_skip_queue boolean default false, redemptions_redeemed_current_stream int, cooldown_expires_at text, foreign key (broadcaster_id) references users(id) ); create table channel_points_redemptions( id text not null primary key, reward_id text not null, broadcaster_id text not null, user_id text not null, user_input text, redemption_status text not null, redeemed_at text, foreign key (reward_id) references channel_points_rewards(id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) ); create table streams( id text not null primary key, broadcaster_id id text not null, stream_type text not null default 'live', viewer_count int not null, started_at text not null, is_mature boolean not null default false, foreign key (broadcaster_id) references users(id) ); create table tags( id text not null primary key, is_auto boolean not null default false, tag_name text not null ); create table stream_tags( user_id text not null, tag_id text not null, primary key(user_id, tag_id), foreign key(user_id) references users(id), foreign key(tag_id) references tags(id) ); create table teams( id text not null primary key, background_image_url text, banner text, created_at text not null, updated_at text, info text, thumbnail_url text, team_name text, team_display_name text ); create table team_members( team_id text not null, user_id text not null, primary key (team_id, user_id) foreign key (team_id) references teams(id), foreign key (user_id) references users(id) ); create table videos( id text not null primary key, stream_id text, broadcaster_id text not null, title text not null, video_description text not null, created_at text not null, published_at text, viewable text not null, view_count int not null default 0, duration text not null, video_language text not null default 'en', category_id text, type text default 'archive', foreign key (stream_id) references streams(id), foreign key (broadcaster_id) references users(id), foreign key (category_id) references categories(id) ); create table stream_markers( id text not null primary key, video_id text not null, position_seconds int not null, created_at text not null, description text not null, broadcaster_id text not null, foreign key (broadcaster_id) references users(id), foreign key (video_id) references videos(id) ); create table video_muted_segments ( video_id text not null, video_offset int not null, duration int not null, primary key (video_id, video_offset), foreign key (video_id) references videos(id) ); create table subscriptions ( broadcaster_id text not null, user_id text not null, is_gift boolean not null default false, gifter_id text, tier text not null default '1000', created_at text not null, primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id), foreign key (gifter_id) references users(id) ); create table drops_entitlements( id text not null primary key, benefit_id text not null, timestamp text not null, user_id text not null, game_id text not null, status text not null default 'CLAIMED', foreign key (user_id) references users(id), foreign key (game_id) references categories(id) ); create table clients ( id text not null primary key, secret text not null, is_extension boolean default false, name text not null ); create table authorizations ( id integer not null primary key AUTOINCREMENT, client_id text not null, user_id text, token text not null unique, expires_at text not null, scopes text, foreign key (client_id) references clients(id) ); create table polls ( id text not null primary key, broadcaster_id text not null, title text not null, bits_voting_enabled boolean default false, bits_per_vote int default 10, channel_points_voting_enabled boolean default false, channel_points_per_vote int default 10, status text not null, duration int not null, started_at text not null, ended_at text, foreign key (broadcaster_id) references users(id) ); create table poll_choices ( id text not null primary key, title text not null, votes int not null default 0, channel_points_votes int not null default 0, bits_votes int not null default 0, poll_id text not null, foreign key (poll_id) references polls(id) ); create table predictions ( id text not null primary key, broadcaster_id text not null, title text not null, winning_outcome_id text, prediction_window int, status text not null, created_at text not null, ended_at text, locked_at text, foreign key (broadcaster_id) references users(id) ); create table prediction_outcomes ( id text not null primary key, title text not null, users int not null default 0, channel_points int not null default 0, color text not null, prediction_id text not null, foreign key (prediction_id) references predictions(id) ); create table prediction_predictions ( prediction_id text not null, user_id text not null, amount int not null, outcome_id text not null, primary key(prediction_id, user_id), foreign key(user_id) references users(id), foreign key(prediction_id) references predictions(id), foreign key(outcome_id) references prediction_outcomes(id) ); create table clips ( id text not null primary key, broadcaster_id text not null, creator_id text not null, video_id text not null, game_id text not null, title text not null, view_count int default 0, created_at text not null, duration real not null, foreign key (broadcaster_id) references users(id), foreign key (creator_id) references users(id) ); create table stream_schedule( id text not null primary key, broadcaster_id text not null, starttime text not null, endtime text not null, timezone text not null, is_vacation boolean not null default false, is_recurring boolean not null default false, category_id text, title text, foreign key(broadcaster_id) references users(id), foreign key (category_id) references categories(id));` ->>>>>>> 9ae4d30 (adding schedules api support) for i := 1; i <= 5; i++ { tx := db.MustBegin() tx.Exec(createSQL) From 828503a6ace8af6e3532e5910b8a36c9cdc7ab3f Mon Sep 17 00:00:00 2001 From: lleadbet Date: Fri, 16 Jul 2021 15:40:48 -0700 Subject: [PATCH 19/36] Finished API parity update. --- internal/database/drops.go | 6 + internal/database/schedule.go | 5 + .../mock_api/endpoints/chat/channel_emotes.go | 96 ++++++++++ internal/mock_api/endpoints/chat/chat_test.go | 51 ++++++ internal/mock_api/endpoints/chat/emote_set.go | 93 ++++++++++ .../mock_api/endpoints/chat/global_emotes.go | 74 ++++++++ internal/mock_api/endpoints/chat/shared.go | 17 ++ .../mock_api/endpoints/drops/drops_test.go | 64 +++++++ .../mock_api/endpoints/drops/entitlements.go | 81 ++++++++- internal/mock_api/endpoints/endpoints.go | 3 + .../endpoints/schedule/scehdule_test.go | 164 +++++++++++++++++- .../mock_api/endpoints/schedule/schedule.go | 3 + .../mock_api/endpoints/schedule/segment.go | 10 +- .../mock_api/endpoints/schedule/settings.go | 13 +- internal/mock_api/generate/generate.go | 1 + 15 files changed, 661 insertions(+), 20 deletions(-) create mode 100644 internal/mock_api/endpoints/chat/channel_emotes.go create mode 100644 internal/mock_api/endpoints/chat/emote_set.go create mode 100644 internal/mock_api/endpoints/chat/global_emotes.go diff --git a/internal/database/drops.go b/internal/database/drops.go index dcf53759..53ec5723 100644 --- a/internal/database/drops.go +++ b/internal/database/drops.go @@ -49,6 +49,12 @@ func (q *Query) GetDropsEntitlements(de DropsEntitlement) (*DBResponse, error) { } func (q *Query) InsertDropsEntitlement(d DropsEntitlement) error { stmt := generateInsertSQL("drops_entitlements", "id", d, false) + println(stmt) _, err := q.DB.NamedExec(stmt, d) return err } + +func (q *Query) UpdateDropsEntitlement(d DropsEntitlement) error { + _, err := q.DB.NamedExec(generateUpdateSQL("drops_entitlements", []string{"id"}, d), d) + return err +} diff --git a/internal/database/schedule.go b/internal/database/schedule.go index c1f26087..10667838 100644 --- a/internal/database/schedule.go +++ b/internal/database/schedule.go @@ -3,6 +3,8 @@ package database import ( + "database/sql" + "errors" "time" ) @@ -122,5 +124,8 @@ func (q *Query) UpdateSegment(p ScheduleSegment) error { func (q *Query) GetVacations(p ScheduleSegment) (ScheduleVacation, error) { v := ScheduleVacation{} err := q.DB.Get(&v, "select id,starttime,endtime from stream_schedule where is_vacation=true and datetime(endtime) > datetime('now') and broadcaster_id= $1 limit 1", p.UserID) + if errors.As(err, &sql.ErrNoRows) { + return v, nil + } return v, err } diff --git a/internal/mock_api/endpoints/chat/channel_emotes.go b/internal/mock_api/endpoints/chat/channel_emotes.go new file mode 100644 index 00000000..1decc21b --- /dev/null +++ b/internal/mock_api/endpoints/chat/channel_emotes.go @@ -0,0 +1,96 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package chat + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/twitchdev/twitch-cli/internal/database" + "github.com/twitchdev/twitch-cli/internal/mock_api/mock_errors" + "github.com/twitchdev/twitch-cli/internal/models" + "github.com/twitchdev/twitch-cli/internal/util" +) + +var channelEmotesMethodsSupported = map[string]bool{ + http.MethodGet: true, + http.MethodPost: false, + http.MethodDelete: false, + http.MethodPatch: false, + http.MethodPut: false, +} + +var channelEmotesScopesByMethod = map[string][]string{ + http.MethodGet: {}, + http.MethodPost: {}, + http.MethodDelete: {}, + http.MethodPatch: {}, + http.MethodPut: {}, +} + +type ChannelEmotes struct{} + +func (e ChannelEmotes) Path() string { return "/chat/emotes/channel" } + +func (e ChannelEmotes) GetRequiredScopes(method string) []string { + return channelEmotesScopesByMethod[method] +} + +func (e ChannelEmotes) ValidMethod(method string) bool { + return channelEmotesMethodsSupported[method] +} + +func (e ChannelEmotes) ServeHTTP(w http.ResponseWriter, r *http.Request) { + db = r.Context().Value("db").(database.CLIDatabase) + + switch r.Method { + case http.MethodGet: + getChannelEmotes(w, r) + break + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} +func getChannelEmotes(w http.ResponseWriter, r *http.Request) { + emotes := []EmotesResponse{} + broadcaster := r.URL.Query().Get("broadcaster_id") + if broadcaster == "" { + mock_errors.WriteBadRequest(w, "Missing required parameter broadcaster_id") + return + } + + setID := fmt.Sprint(util.RandomInt(10 * 1000)) + ownerID := util.RandomUserID() + for _, v := range defaultEmoteTypes { + emoteType := v + for i := 0; i < 5; i++ { + id := util.RandomInt(10 * 1000) + name := util.RandomGUID() + er := EmotesResponse{ + ID: fmt.Sprint(id), + Name: name, + Images: EmotesImages{ + ImageURL1X: fmt.Sprintf("https://static-cdn.jtvnw.net/emoticons/v1/%v/1.0", id), + ImageURL2X: fmt.Sprintf("https://static-cdn.jtvnw.net/emoticons/v1/%v/2.0", id), + ImageURL4X: fmt.Sprintf("https://static-cdn.jtvnw.net/emoticons/v1/%v/4.0", id), + }, + EmoteType: &emoteType, + EmoteSetID: &setID, + OwnerID: &ownerID, + } + if emoteType == "subscription" { + thousand := "1000" + er.Tier = &thousand + } else { + es := "" + er.Tier = &es + } + + emotes = append(emotes, er) + } + } + + bytes, _ := json.Marshal(models.APIResponse{Data: emotes}) + w.Write(bytes) +} diff --git a/internal/mock_api/endpoints/chat/chat_test.go b/internal/mock_api/endpoints/chat/chat_test.go index 5aa23728..d5284d50 100644 --- a/internal/mock_api/endpoints/chat/chat_test.go +++ b/internal/mock_api/endpoints/chat/chat_test.go @@ -41,3 +41,54 @@ func TestChannelBadges(t *testing.T) { a.Nil(err) a.Equal(200, resp.StatusCode) } + +func TestGlobalEmotes(t *testing.T) { + a := test_setup.SetupTestEnv(t) + ts := test_server.SetupTestServer(GlobalEmotes{}) + + // get + req, _ := http.NewRequest(http.MethodGet, ts.URL+GlobalEmotes{}.Path(), nil) + q := req.URL.Query() + req.URL.RawQuery = q.Encode() + resp, err := http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(200, resp.StatusCode) +} + +func TestChannelEmotes(t *testing.T) { + a := test_setup.SetupTestEnv(t) + ts := test_server.SetupTestServer(ChannelEmotes{}) + + // get + req, _ := http.NewRequest(http.MethodGet, ts.URL+ChannelEmotes{}.Path(), nil) + q := req.URL.Query() + req.URL.RawQuery = q.Encode() + resp, err := http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(400, resp.StatusCode) + + q.Set("broadcaster_id", "1") + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(200, resp.StatusCode) +} + +func TestEmoteSets(t *testing.T) { + a := test_setup.SetupTestEnv(t) + ts := test_server.SetupTestServer(EmoteSets{}) + + // get + req, _ := http.NewRequest(http.MethodGet, ts.URL+EmoteSets{}.Path(), nil) + q := req.URL.Query() + req.URL.RawQuery = q.Encode() + resp, err := http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(400, resp.StatusCode) + + q.Set("emote_set_id", "1") + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(200, resp.StatusCode) +} diff --git a/internal/mock_api/endpoints/chat/emote_set.go b/internal/mock_api/endpoints/chat/emote_set.go new file mode 100644 index 00000000..9d0a32dd --- /dev/null +++ b/internal/mock_api/endpoints/chat/emote_set.go @@ -0,0 +1,93 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package chat + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/twitchdev/twitch-cli/internal/database" + "github.com/twitchdev/twitch-cli/internal/mock_api/mock_errors" + "github.com/twitchdev/twitch-cli/internal/models" + "github.com/twitchdev/twitch-cli/internal/util" +) + +var emoteSetsMethodsSupported = map[string]bool{ + http.MethodGet: true, + http.MethodPost: false, + http.MethodDelete: false, + http.MethodPatch: false, + http.MethodPut: false, +} + +var emoteSetsScopesByMethod = map[string][]string{ + http.MethodGet: {}, + http.MethodPost: {}, + http.MethodDelete: {}, + http.MethodPatch: {}, + http.MethodPut: {}, +} + +type EmoteSets struct{} + +func (e EmoteSets) Path() string { return "/chat/emotes/set" } + +func (e EmoteSets) GetRequiredScopes(method string) []string { + return emoteSetsScopesByMethod[method] +} + +func (e EmoteSets) ValidMethod(method string) bool { + return emoteSetsMethodsSupported[method] +} + +func (e EmoteSets) ServeHTTP(w http.ResponseWriter, r *http.Request) { + db = r.Context().Value("db").(database.CLIDatabase) + + switch r.Method { + case http.MethodGet: + getEmoteSets(w, r) + break + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} +func getEmoteSets(w http.ResponseWriter, r *http.Request) { + emotes := []EmotesResponse{} + setID := r.URL.Query().Get("emote_set_id") + if setID == "" { + mock_errors.WriteBadRequest(w, "Missing required parameter emote_set_id") + return + } + + for _, v := range defaultEmoteTypes { + emoteType := v + for i := 0; i < 5; i++ { + id := util.RandomInt(10 * 1000) + name := util.RandomGUID() + er := EmotesResponse{ + ID: fmt.Sprint(id), + Name: name, + Images: EmotesImages{ + ImageURL1X: fmt.Sprintf("https://static-cdn.jtvnw.net/emoticons/v1/%v/1.0", id), + ImageURL2X: fmt.Sprintf("https://static-cdn.jtvnw.net/emoticons/v1/%v/2.0", id), + ImageURL4X: fmt.Sprintf("https://static-cdn.jtvnw.net/emoticons/v1/%v/4.0", id), + }, + EmoteType: &emoteType, + EmoteSetID: &setID, + } + if emoteType == "subscription" { + thousand := "1000" + er.Tier = &thousand + } else { + es := "" + er.Tier = &es + } + + emotes = append(emotes, er) + } + } + + bytes, _ := json.Marshal(models.APIResponse{Data: emotes}) + w.Write(bytes) +} diff --git a/internal/mock_api/endpoints/chat/global_emotes.go b/internal/mock_api/endpoints/chat/global_emotes.go new file mode 100644 index 00000000..2dceedad --- /dev/null +++ b/internal/mock_api/endpoints/chat/global_emotes.go @@ -0,0 +1,74 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package chat + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/twitchdev/twitch-cli/internal/database" + "github.com/twitchdev/twitch-cli/internal/models" + "github.com/twitchdev/twitch-cli/internal/util" +) + +var globalEmotesMethodsSupported = map[string]bool{ + http.MethodGet: true, + http.MethodPost: false, + http.MethodDelete: false, + http.MethodPatch: false, + http.MethodPut: false, +} + +var globalEmotesScopesByMethod = map[string][]string{ + http.MethodGet: {}, + http.MethodPost: {}, + http.MethodDelete: {}, + http.MethodPatch: {}, + http.MethodPut: {}, +} + +type GlobalEmotes struct{} + +func (e GlobalEmotes) Path() string { return "/chat/emotes/global" } + +func (e GlobalEmotes) GetRequiredScopes(method string) []string { + return globalEmotesScopesByMethod[method] +} + +func (e GlobalEmotes) ValidMethod(method string) bool { + return globalEmotesMethodsSupported[method] +} + +func (e GlobalEmotes) ServeHTTP(w http.ResponseWriter, r *http.Request) { + db = r.Context().Value("db").(database.CLIDatabase) + + switch r.Method { + case http.MethodGet: + getGlobalEmotes(w, r) + break + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +func getGlobalEmotes(w http.ResponseWriter, r *http.Request) { + emotes := []EmotesResponse{} + + for i := 0; i < 100; i++ { + id := util.RandomInt(10 * 1000) + name := util.RandomGUID() + emotes = append(emotes, EmotesResponse{ + ID: fmt.Sprintf("%v", id), + Name: name, + Images: EmotesImages{ + ImageURL1X: fmt.Sprintf("https://static-cdn.jtvnw.net/emoticons/v1/%v/1.0", id), + ImageURL2X: fmt.Sprintf("https://static-cdn.jtvnw.net/emoticons/v1/%v/2.0", id), + ImageURL4X: fmt.Sprintf("https://static-cdn.jtvnw.net/emoticons/v1/%v/4.0", id), + }, + }) + } + + bytes, _ := json.Marshal(models.APIResponse{Data: emotes}) + w.Write(bytes) +} diff --git a/internal/mock_api/endpoints/chat/shared.go b/internal/mock_api/endpoints/chat/shared.go index 0552b074..c81b6cbd 100644 --- a/internal/mock_api/endpoints/chat/shared.go +++ b/internal/mock_api/endpoints/chat/shared.go @@ -5,6 +5,7 @@ package chat import "github.com/twitchdev/twitch-cli/internal/database" var db database.CLIDatabase +var defaultEmoteTypes = []string{"subscription", "bitstier", "follower"} type BadgesResponse struct { SetID string `json:"set_id"` @@ -17,3 +18,19 @@ type BadgesVersion struct { ImageURL2X string `json:"image_url_2x"` ImageURL4X string `json:"image_url_4x"` } + +type EmotesResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Images EmotesImages `json:"images"` + Tier *string `json:"tier,omitempty"` + EmoteType *string `json:"emote_type,omitempty"` + EmoteSetID *string `json:"emote_set_id,omitempty"` + OwnerID *string `json:"owner_id,omitempty"` +} + +type EmotesImages struct { + ImageURL1X string `json:"url_1x"` + ImageURL2X string `json:"url_2x"` + ImageURL4X string `json:"url_4x"` +} diff --git a/internal/mock_api/endpoints/drops/drops_test.go b/internal/mock_api/endpoints/drops/drops_test.go index aff4805c..69d3edfe 100644 --- a/internal/mock_api/endpoints/drops/drops_test.go +++ b/internal/mock_api/endpoints/drops/drops_test.go @@ -3,13 +3,48 @@ package drops import ( + "bytes" + "encoding/json" + "log" "net/http" + "os" "testing" + "time" + "github.com/twitchdev/twitch-cli/internal/database" + "github.com/twitchdev/twitch-cli/internal/util" "github.com/twitchdev/twitch-cli/test_setup" "github.com/twitchdev/twitch-cli/test_setup/test_server" ) +var entitlement database.DropsEntitlement + +func TestMain(m *testing.M) { + test_setup.SetupTestEnv(&testing.T{}) + + db, err := database.NewConnection() + if err != nil { + log.Fatal(err) + } + e := database.DropsEntitlement{ + ID: util.RandomGUID(), + UserID: "1", + BenefitID: "1234", + GameID: "1", + Timestamp: util.GetTimestamp().Format(time.RFC3339), + Status: "CLAIMED", + } + + err = db.NewQuery(nil, 100).InsertDropsEntitlement(e) + if err != nil { + log.Fatal(err) + } + entitlement = e + + db.DB.Close() + + os.Exit(m.Run()) +} func TestDropsEntitlements(t *testing.T) { a := test_setup.SetupTestEnv(t) ts := test_server.SetupTestServer(DropsEntitlements{}) @@ -21,4 +56,33 @@ func TestDropsEntitlements(t *testing.T) { resp, err := http.DefaultClient.Do(req) a.Nil(err) a.Equal(200, resp.StatusCode) + + // patch + // patch tests + body := PatchEntitlementsBody{ + FulfillmentStatus: "FULFILLED", + EntitlementIDs: []string{ + entitlement.ID, + "potato", + }, + } + + b, _ := json.Marshal(body) + req, _ = http.NewRequest(http.MethodPatch, ts.URL+DropsEntitlements{}.Path(), bytes.NewBuffer(b)) + q = req.URL.Query() + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.NotNil(resp) + a.Equal(200, resp.StatusCode) + + body.FulfillmentStatus = "potato" + b, _ = json.Marshal(body) + req, _ = http.NewRequest(http.MethodPatch, ts.URL+DropsEntitlements{}.Path(), bytes.NewBuffer(b)) + q = req.URL.Query() + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.NotNil(resp) + a.Equal(400, resp.StatusCode) } diff --git a/internal/mock_api/endpoints/drops/entitlements.go b/internal/mock_api/endpoints/drops/entitlements.go index cdb9b619..6fdcf79a 100644 --- a/internal/mock_api/endpoints/drops/entitlements.go +++ b/internal/mock_api/endpoints/drops/entitlements.go @@ -16,7 +16,7 @@ var dropsEntitlementsMethodsSupported = map[string]bool{ http.MethodGet: true, http.MethodPost: false, http.MethodDelete: false, - http.MethodPatch: false, + http.MethodPatch: true, http.MethodPut: false, } @@ -30,6 +30,16 @@ var dropsEntitlementsScopesByMethod = map[string][]string{ type DropsEntitlements struct{} +type PatchEntitlementsBody struct { + FulfillmentStatus string `json:"fulfillment_status"` + EntitlementIDs []string `json:"entitlement_ids"` +} + +type PatchEntitlementsResponse struct { + Status string `json:"status"` + IDs []string `json:"ids"` +} + func (e DropsEntitlements) Path() string { return "/entitlements/drops" } func (e DropsEntitlements) GetRequiredScopes(method string) []string { @@ -46,7 +56,8 @@ func (e DropsEntitlements) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: getEntitlements(w, r) - break + case http.MethodPatch: + patchEntitlements(w, r) default: w.WriteHeader(http.StatusMethodNotAllowed) } @@ -86,3 +97,69 @@ func getEntitlements(w http.ResponseWriter, r *http.Request) { bytes, err := json.Marshal(apiResponse) w.Write(bytes) } + +func patchEntitlements(w http.ResponseWriter, r *http.Request) { + userCtx := r.Context().Value("auth").(authentication.UserAuthentication) + + var body PatchEntitlementsBody + err := json.NewDecoder(r.Body).Decode(&body) + if err != nil { + mock_errors.WriteBadRequest(w, "Invalid body") + return + } + + if body.FulfillmentStatus != "CLAIMED" && body.FulfillmentStatus != "FULFILLED" { + mock_errors.WriteBadRequest(w, "fulfillment_status must be one of CLAIMED or FULFILLED") + return + } + + if len(body.EntitlementIDs) == 0 || len(body.EntitlementIDs) > 100 { + mock_errors.WriteBadRequest(w, "entitlement_ids must be at least 1 and at most 100") + return + } + s := PatchEntitlementsResponse{ + Status: "SUCCESS", + } + ua := PatchEntitlementsResponse{Status: "UNAUTHORIZED"} + fail := PatchEntitlementsResponse{Status: "UPDATE_FAILED"} + notFound := PatchEntitlementsResponse{Status: "NOT_FOUND"} + for _, e := range body.EntitlementIDs { + dbr, err := db.NewQuery(nil, 100).GetDropsEntitlements(database.DropsEntitlement{ID: e}) + if err != nil { + fail.IDs = append(fail.IDs, e) + continue + } + entitlement := dbr.Data.([]database.DropsEntitlement) + if len(entitlement) == 0 { + notFound.IDs = append(notFound.IDs, e) + continue + } + + if userCtx.UserID != "" && userCtx.UserID != entitlement[0].UserID { + ua.IDs = append(ua.IDs, e) + continue + } + + err = db.NewQuery(nil, 100).UpdateDropsEntitlement(database.DropsEntitlement{ID: e, UserID: entitlement[0].UserID, Status: body.FulfillmentStatus}) + if err != nil { + fail.IDs = append(fail.IDs, e) + continue + } + s.IDs = append(s.IDs, e) + } + all := []PatchEntitlementsResponse{ + s, + ua, + fail, + notFound, + } + resp := []PatchEntitlementsResponse{} + for _, r := range all { + if len(r.IDs) != 0 { + resp = append(resp, r) + } + } + + bytes, _ := json.Marshal(resp) + w.Write(bytes) +} diff --git a/internal/mock_api/endpoints/endpoints.go b/internal/mock_api/endpoints/endpoints.go index 2c52898b..257f2536 100644 --- a/internal/mock_api/endpoints/endpoints.go +++ b/internal/mock_api/endpoints/endpoints.go @@ -36,7 +36,10 @@ func All() []mock_api.MockEndpoint { channels.Editors{}, channels.InformationEndpoint{}, chat.ChannelBadges{}, + chat.ChannelEmotes{}, + chat.EmoteSets{}, chat.GlobalBadges{}, + chat.GlobalEmotes{}, clips.Clips{}, drops.DropsEntitlements{}, hype_train.HypeTrainEvents{}, diff --git a/internal/mock_api/endpoints/schedule/scehdule_test.go b/internal/mock_api/endpoints/schedule/scehdule_test.go index 3cb73387..5f9b54cf 100644 --- a/internal/mock_api/endpoints/schedule/scehdule_test.go +++ b/internal/mock_api/endpoints/schedule/scehdule_test.go @@ -48,8 +48,7 @@ func TestMain(m *testing.M) { log.Fatal(err) } - dbr, err := db.NewQuery(nil, 100).GetSchedule(database.ScheduleSegment{UserID: "1"}, time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC)) - log.Printf("%v %#v", err, dbr.Data) + _, err = db.NewQuery(nil, 100).GetSchedule(database.ScheduleSegment{UserID: "1"}, time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC)) segment = s db.DB.Close() @@ -64,17 +63,22 @@ func TestSchedule(t *testing.T) { // get req, _ := http.NewRequest(http.MethodGet, ts.URL+Schedule{}.Path(), nil) q := req.URL.Query() - q.Set("broadcaster_id", "1") req.URL.RawQuery = q.Encode() resp, err := http.DefaultClient.Do(req) a.Nil(err) + a.Equal(400, resp.StatusCode) + + q.Set("broadcaster_id", "1") + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) a.Equal(200, resp.StatusCode) q.Set("broadcaster_id", "2") req.URL.RawQuery = q.Encode() resp, err = http.DefaultClient.Do(req) a.Nil(err) - a.Equal(401, resp.StatusCode) + a.Equal(200, resp.StatusCode) q.Set("broadcaster_id", "1") q.Set("id", segment.ID) @@ -82,6 +86,24 @@ func TestSchedule(t *testing.T) { resp, err = http.DefaultClient.Do(req) a.Nil(err) a.Equal(200, resp.StatusCode) + + q.Set("utc_offset", "60") + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(200, resp.StatusCode) + + q.Set("start_time", "test") + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(400, resp.StatusCode) + + q.Set("start_time", segment.StartTime) + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(200, resp.StatusCode) } func TestICal(t *testing.T) { @@ -93,6 +115,12 @@ func TestICal(t *testing.T) { req.URL.RawQuery = q.Encode() resp, err := http.DefaultClient.Do(req) a.Nil(err) + a.Equal(400, resp.StatusCode) + + q.Set("broadcaster_id", "1") + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) a.Equal(200, resp.StatusCode) } @@ -100,12 +128,15 @@ func TestSegment(t *testing.T) { a := test_setup.SetupTestEnv(t) ts := test_server.SetupTestServer(ScheduleSegment{}) + tr := true // post tests body := SegmentPatchAndPostBody{ - Title: "hello", - Timezone: "America/Los_Angeles", - StartTime: time.Now().Format(time.RFC3339), + Title: "hello", + Timezone: "America/Los_Angeles", + StartTime: time.Now().Format(time.RFC3339), + IsRecurring: &tr, + Duration: "60", } b, _ := json.Marshal(body) @@ -145,6 +176,25 @@ func TestSegment(t *testing.T) { a.Nil(err) a.Equal(400, resp.StatusCode) + body.Timezone = "test" + b, _ = json.Marshal(body) + req, _ = http.NewRequest(http.MethodPost, ts.URL+ScheduleSegment{}.Path(), bytes.NewBuffer(b)) + q.Set("broadcaster_id", "1") + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(400, resp.StatusCode) + + body.Timezone = segment.Timezone + body.IsRecurring = nil + b, _ = json.Marshal(body) + req, _ = http.NewRequest(http.MethodPost, ts.URL+ScheduleSegment{}.Path(), bytes.NewBuffer(b)) + q.Set("broadcaster_id", "1") + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(400, resp.StatusCode) + // patch // no id b, _ = json.Marshal(body) @@ -175,8 +225,108 @@ func TestSegment(t *testing.T) { resp, err = http.DefaultClient.Do(req) a.Nil(err) a.Equal(200, resp.StatusCode) + + // delete + req, _ = http.NewRequest(http.MethodDelete, ts.URL+ScheduleSegment{}.Path(), nil) + q.Set("broadcaster_id", "1") + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(204, resp.StatusCode) + + q.Set("id", segment.ID) + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(204, resp.StatusCode) + + q.Set("broadcaster_id", "2") + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(401, resp.StatusCode) } func TestSettings(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + ts := test_server.SetupTestServer(ScheduleSettings{}) + tr := true + f := false + // patch tests + body := PatchSettingsBody{ + Timezone: "America/Los_Angeles", + VacationStartTime: time.Now().Format(time.RFC3339), + VacationEndTime: segment.EndTime, + IsVacationEnabled: &f, + } + + b, _ := json.Marshal(body) + req, _ := http.NewRequest(http.MethodPatch, ts.URL+ScheduleSettings{}.Path(), bytes.NewBuffer(b)) + q := req.URL.Query() + req.URL.RawQuery = q.Encode() + resp, err := http.DefaultClient.Do(req) + a.Nil(err) + a.NotNil(resp) + a.Equal(401, resp.StatusCode) + + b, _ = json.Marshal(body) + req, _ = http.NewRequest(http.MethodPatch, ts.URL+ScheduleSettings{}.Path(), bytes.NewBuffer(b)) + q.Set("broadcaster_id", "1") + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(204, resp.StatusCode) + + body.IsVacationEnabled = &tr + b, _ = json.Marshal(body) + req, _ = http.NewRequest(http.MethodPatch, ts.URL+ScheduleSettings{}.Path(), bytes.NewBuffer(b)) + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(204, resp.StatusCode) + + b, _ = json.Marshal(body) + req, _ = http.NewRequest(http.MethodPatch, ts.URL+ScheduleSettings{}.Path(), bytes.NewBuffer(b)) + q.Set("broadcaster_id", "1") + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(400, resp.StatusCode) + + body.IsVacationEnabled = &f + b, _ = json.Marshal(body) + req, _ = http.NewRequest(http.MethodPatch, ts.URL+ScheduleSettings{}.Path(), bytes.NewBuffer(b)) + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(204, resp.StatusCode) + + body.IsVacationEnabled = &tr + body.VacationStartTime = "123" + b, _ = json.Marshal(body) + req, _ = http.NewRequest(http.MethodPatch, ts.URL+ScheduleSettings{}.Path(), bytes.NewBuffer(b)) + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(400, resp.StatusCode) + + body.VacationStartTime = segment.StartTime + body.VacationEndTime = "123" + b, _ = json.Marshal(body) + req, _ = http.NewRequest(http.MethodPatch, ts.URL+ScheduleSettings{}.Path(), bytes.NewBuffer(b)) + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(400, resp.StatusCode) + + body.VacationEndTime = segment.EndTime + body.Timezone = "1" + b, _ = json.Marshal(body) + req, _ = http.NewRequest(http.MethodPatch, ts.URL+ScheduleSettings{}.Path(), bytes.NewBuffer(b)) + req.URL.RawQuery = q.Encode() + resp, err = http.DefaultClient.Do(req) + a.Nil(err) + a.Equal(400, resp.StatusCode) } diff --git a/internal/mock_api/endpoints/schedule/schedule.go b/internal/mock_api/endpoints/schedule/schedule.go index 4e309a1b..63254015 100644 --- a/internal/mock_api/endpoints/schedule/schedule.go +++ b/internal/mock_api/endpoints/schedule/schedule.go @@ -4,6 +4,7 @@ package schedule import ( "encoding/json" + "log" "net/http" "strconv" "time" @@ -94,7 +95,9 @@ func (e Schedule) getSchedule(w http.ResponseWriter, r *http.Request) { for _, id := range ids { dbr, err := db.NewQuery(r, 25).GetSchedule(database.ScheduleSegment{ID: id, UserID: broadcasterID}, startTime) if err != nil { + log.Print(err) mock_errors.WriteServerError(w, err.Error()) + return } response := dbr.Data.(database.Schedule) schedule = response diff --git a/internal/mock_api/endpoints/schedule/segment.go b/internal/mock_api/endpoints/schedule/segment.go index 8ac9b3ca..0b423c00 100644 --- a/internal/mock_api/endpoints/schedule/segment.go +++ b/internal/mock_api/endpoints/schedule/segment.go @@ -76,7 +76,7 @@ func (e ScheduleSegment) postSegment(w http.ResponseWriter, r *http.Request) { duration := 240 if !userCtx.MatchesBroadcasterIDParam(r) { - mock_errors.WriteBadRequest(w, "User token does not match broadcaster_id parameter") + mock_errors.WriteUnauthorized(w, "User token does not match broadcaster_id parameter") return } var body SegmentPatchAndPostBody @@ -184,7 +184,7 @@ func (e ScheduleSegment) deleteSegment(w http.ResponseWriter, r *http.Request) { userCtx := r.Context().Value("auth").(authentication.UserAuthentication) id := r.URL.Query().Get("id") if !userCtx.MatchesBroadcasterIDParam(r) { - mock_errors.WriteBadRequest(w, "User token does not match broadcaster_id parameter") + mock_errors.WriteUnauthorized(w, "User token does not match broadcaster_id parameter") return } @@ -205,7 +205,11 @@ func (e ScheduleSegment) patchSegment(w http.ResponseWriter, r *http.Request) { userCtx := r.Context().Value("auth").(authentication.UserAuthentication) id := r.URL.Query().Get("id") if !userCtx.MatchesBroadcasterIDParam(r) { - mock_errors.WriteBadRequest(w, "User token does not match broadcaster_id parameter") + mock_errors.WriteUnauthorized(w, "User token does not match broadcaster_id parameter") + return + } + if id == "" { + mock_errors.WriteBadRequest(w, "Missing required parameter id") return } diff --git a/internal/mock_api/endpoints/schedule/settings.go b/internal/mock_api/endpoints/schedule/settings.go index c67f1c91..a41a82c2 100644 --- a/internal/mock_api/endpoints/schedule/settings.go +++ b/internal/mock_api/endpoints/schedule/settings.go @@ -3,10 +3,8 @@ package schedule import ( - "database/sql" "encoding/base64" "encoding/json" - "errors" "fmt" "net/http" "time" @@ -58,22 +56,22 @@ func (e ScheduleSettings) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPatch: e.patchSchedule(w, r) + default: + w.WriteHeader(http.StatusMethodNotAllowed) } } func (e ScheduleSettings) patchSchedule(w http.ResponseWriter, r *http.Request) { userCtx := r.Context().Value("auth").(authentication.UserAuthentication) if !userCtx.MatchesBroadcasterIDParam(r) { - mock_errors.WriteBadRequest(w, "User token does not match broadcaster_id parameter") + mock_errors.WriteUnauthorized(w, "User token does not match broadcaster_id parameter") return } vacation, err := db.NewQuery(r, 100).GetVacations(database.ScheduleSegment{UserID: userCtx.UserID}) if err != nil { - if !errors.As(err, &sql.ErrNoRows) { - mock_errors.WriteServerError(w, err.Error()) - return - } + mock_errors.WriteServerError(w, err.Error()) + return } var body PatchSettingsBody @@ -139,5 +137,4 @@ func (e ScheduleSettings) patchSchedule(w http.ResponseWriter, r *http.Request) } w.WriteHeader(http.StatusNoContent) - } diff --git a/internal/mock_api/generate/generate.go b/internal/mock_api/generate/generate.go index bd548a1b..79ed7e44 100644 --- a/internal/mock_api/generate/generate.go +++ b/internal/mock_api/generate/generate.go @@ -186,6 +186,7 @@ func generateUsers(ctx context.Context, count int) error { GameID: dropsGameID, UserID: broadcaster.ID, Timestamp: util.GetTimestamp().Format(time.RFC3339Nano), + Status: "CLAIMED", } err = db.NewQuery(nil, 1000).InsertDropsEntitlement(entitlement) if err != nil { From 41d71a1426c625accfd9d87bf0fdd8cfe6d101a5 Mon Sep 17 00:00:00 2001 From: lleadbet Date: Fri, 16 Jul 2021 15:54:26 -0700 Subject: [PATCH 20/36] pruning some debugging logging --- internal/database/drops.go | 1 - internal/events/trigger/trigger_event.go | 3 ++- internal/mock_api/endpoints/clips/clips.go | 6 ++---- internal/mock_api/endpoints/polls/polls.go | 3 +-- internal/mock_api/endpoints/search/channels.go | 1 - internal/mock_api/endpoints/streams/all_tags.go | 1 - internal/mock_api/endpoints/streams/markers.go | 6 ++---- internal/mock_api/endpoints/streams/stream_tags.go | 6 ++---- 8 files changed, 9 insertions(+), 18 deletions(-) diff --git a/internal/database/drops.go b/internal/database/drops.go index 53ec5723..fab3c70a 100644 --- a/internal/database/drops.go +++ b/internal/database/drops.go @@ -49,7 +49,6 @@ func (q *Query) GetDropsEntitlements(de DropsEntitlement) (*DBResponse, error) { } func (q *Query) InsertDropsEntitlement(d DropsEntitlement) error { stmt := generateInsertSQL("drops_entitlements", "id", d, false) - println(stmt) _, err := q.DB.NamedExec(stmt, d) return err } diff --git a/internal/events/trigger/trigger_event.go b/internal/events/trigger/trigger_event.go index 20271cf7..93ce195c 100644 --- a/internal/events/trigger/trigger_event.go +++ b/internal/events/trigger/trigger_event.go @@ -4,6 +4,7 @@ package trigger import ( "fmt" + "log" "time" "github.com/twitchdev/twitch-cli/internal/database" @@ -111,7 +112,7 @@ func Fire(p TriggerParameters) (string, error) { } defer resp.Body.Close() - println(fmt.Sprintf(`[%v] Request Sent`, resp.StatusCode)) + log.Println(fmt.Sprintf(`[%v] Request Sent`, resp.StatusCode)) } return string(resp.JSON), nil diff --git a/internal/mock_api/endpoints/clips/clips.go b/internal/mock_api/endpoints/clips/clips.go index e5c51071..9af8d60e 100644 --- a/internal/mock_api/endpoints/clips/clips.go +++ b/internal/mock_api/endpoints/clips/clips.go @@ -87,8 +87,7 @@ func getClips(w http.ResponseWriter, r *http.Request) { dbr, err := db.NewQuery(r, 100).GetClips(database.Clip{ID: id, BroadcasterID: broadcasterID, GameID: gameID}, startedAt, endedAt) if err != nil { - println(err.Error()) - mock_errors.WriteServerError(w, "Error fetching clips") + mock_errors.WriteServerError(w, err.Error()) return } @@ -150,8 +149,7 @@ func postClips(w http.ResponseWriter, r *http.Request) { err = db.NewQuery(r, 100).InsertClip(clip) if err != nil { - println(err.Error()) - mock_errors.WriteServerError(w, "Error creating clip for user") + mock_errors.WriteServerError(w, err.Error()) return } diff --git a/internal/mock_api/endpoints/polls/polls.go b/internal/mock_api/endpoints/polls/polls.go index 8b3fd41d..bacb8000 100644 --- a/internal/mock_api/endpoints/polls/polls.go +++ b/internal/mock_api/endpoints/polls/polls.go @@ -227,8 +227,7 @@ func patchPolls(w http.ResponseWriter, r *http.Request) { dbr, err := db.NewQuery(r, 100).GetPolls(database.Poll{BroadcasterID: userCtx.UserID, ID: body.ID}) if err != nil { - println(err.Error()) - mock_errors.WriteServerError(w, "error fetching polls") + mock_errors.WriteServerError(w, err.Error()) return } diff --git a/internal/mock_api/endpoints/search/channels.go b/internal/mock_api/endpoints/search/channels.go index 827606d8..809bf2d2 100644 --- a/internal/mock_api/endpoints/search/channels.go +++ b/internal/mock_api/endpoints/search/channels.go @@ -64,7 +64,6 @@ func searchChannels(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("live_only") != "" { live_only, _ = strconv.ParseBool(r.URL.Query().Get("live_only")) } - println(live_only) dbr, err := db.NewQuery(r, 100).SearchChannels(query, live_only) if err != nil { log.Print(err) diff --git a/internal/mock_api/endpoints/streams/all_tags.go b/internal/mock_api/endpoints/streams/all_tags.go index 635361ff..bbd962de 100644 --- a/internal/mock_api/endpoints/streams/all_tags.go +++ b/internal/mock_api/endpoints/streams/all_tags.go @@ -63,7 +63,6 @@ func getAllTags(w http.ResponseWriter, r *http.Request) { if len(tagIDs) > 0 { for _, id := range tagIDs { - println(id) t := database.Tag{ID: id} dbr, err := db.NewQuery(r, 100).GetTags(t) if err != nil { diff --git a/internal/mock_api/endpoints/streams/markers.go b/internal/mock_api/endpoints/streams/markers.go index 7fad7065..988b65be 100644 --- a/internal/mock_api/endpoints/streams/markers.go +++ b/internal/mock_api/endpoints/streams/markers.go @@ -89,8 +89,7 @@ func getMarkers(w http.ResponseWriter, r *http.Request) { dbr, err := db.NewQuery(r, 100).GetStreamMarkers(database.StreamMarker{BroadcasterID: userID, VideoID: videoID}) if err != nil { - println(err.Error()) - mock_errors.WriteServerError(w, "error fetching markers") + mock_errors.WriteServerError(w, err.Error()) return } markerResponse := dbr.Data.([]database.StreamMarkerUser) @@ -145,8 +144,7 @@ func postMarkers(w http.ResponseWriter, r *http.Request) { err = db.NewQuery(r, 100).InsertStreamMarker(sm) if err != nil { - println(err.Error()) - mock_errors.WriteServerError(w, "error inserting marker") + mock_errors.WriteServerError(w, err.Error()) return } diff --git a/internal/mock_api/endpoints/streams/stream_tags.go b/internal/mock_api/endpoints/streams/stream_tags.go index 7a24e15b..b282b9d4 100644 --- a/internal/mock_api/endpoints/streams/stream_tags.go +++ b/internal/mock_api/endpoints/streams/stream_tags.go @@ -106,15 +106,13 @@ func putStreamTags(w http.ResponseWriter, r *http.Request) { err = db.NewQuery(r, 100).DeleteAllStreamTags(userCtx.UserID) if err != nil { - println(err.Error()) - mock_errors.WriteBadRequest(w, "error removing stream tags") + mock_errors.WriteServerError(w, err.Error()) return } for _, tag := range body.TagIDs { err = db.NewQuery(r, 100).InsertStreamTag(database.StreamTag{UserID: userCtx.UserID, TagID: tag}) if err != nil { - println(err.Error()) - mock_errors.WriteBadRequest(w, "error adding stream tag") + mock_errors.WriteServerError(w, err.Error()) return } } From e82a6f5d2c1b0a40c4df81e7e3b14d693862a7ed Mon Sep 17 00:00:00 2001 From: lleadbet Date: Fri, 16 Jul 2021 16:06:00 -0700 Subject: [PATCH 21/36] fixing unit test --- internal/mock_api/endpoints/streams/stream_tags.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/mock_api/endpoints/streams/stream_tags.go b/internal/mock_api/endpoints/streams/stream_tags.go index b282b9d4..d677937e 100644 --- a/internal/mock_api/endpoints/streams/stream_tags.go +++ b/internal/mock_api/endpoints/streams/stream_tags.go @@ -7,6 +7,7 @@ import ( "log" "net/http" + "github.com/mattn/go-sqlite3" "github.com/twitchdev/twitch-cli/internal/database" "github.com/twitchdev/twitch-cli/internal/mock_api/authentication" "github.com/twitchdev/twitch-cli/internal/mock_api/mock_errors" @@ -106,12 +107,17 @@ func putStreamTags(w http.ResponseWriter, r *http.Request) { err = db.NewQuery(r, 100).DeleteAllStreamTags(userCtx.UserID) if err != nil { + log.Print(err) mock_errors.WriteServerError(w, err.Error()) return } for _, tag := range body.TagIDs { err = db.NewQuery(r, 100).InsertStreamTag(database.StreamTag{UserID: userCtx.UserID, TagID: tag}) if err != nil { + if database.DatabaseErrorIs(err, sqlite3.ErrConstraintForeignKey) { + mock_errors.WriteBadRequest(w, "invalid tag provided") + return + } mock_errors.WriteServerError(w, err.Error()) return } From ad5943ee3653b3cd5f305117a39c76de16e827fe Mon Sep 17 00:00:00 2001 From: Martin Purcell Date: Thu, 22 Jul 2021 18:13:24 +0100 Subject: [PATCH 22/36] fix: hype-train-progress and hype-train-end events were generating incorrect output --- .../events/types/hype_train/hype_train_event.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/internal/events/types/hype_train/hype_train_event.go b/internal/events/types/hype_train/hype_train_event.go index c98e639e..1f6b6a44 100644 --- a/internal/events/types/hype_train/hype_train_event.go +++ b/internal/events/types/hype_train/hype_train_event.go @@ -39,6 +39,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven lastType := util.RandomType() //Local variables which will be used for the trigger params below + localLevel := util.RandomInt(5) localTotal := util.RandomInt(10 * 100) localGoal := util.RandomInt(10*100*100) + localTotal localProgress := (localTotal / localGoal) @@ -92,11 +93,20 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven UserLoginWhoMadeContribution: "cli_user2", }, StartedAtTimestamp: util.GetTimestamp().Format(time.RFC3339Nano), - ExpiresAtTimestamp: util.GetTimestamp().Format(time.RFC3339Nano), + ExpiresAtTimestamp: util.GetTimestamp().Add(5 * time.Minute).Format(time.RFC3339Nano), }, } - if triggerMapping[params.Transport][params.Trigger] == "hype-train-end " { + if triggerMapping[params.Transport][params.Trigger] == "channel.hype_train.progress" { + body.Event.Level = localLevel + } + if triggerMapping[params.Transport][params.Trigger] == "channel.hype_train.end" { body.Event.CooldownEndsAtTimestamp = util.GetTimestamp().Add(1 * time.Hour).Format(time.RFC3339Nano) + body.Event.EndedAtTimestamp = util.GetTimestamp().Format(time.RFC3339Nano) + body.Event.ExpiresAtTimestamp = "" + body.Event.Goal = 0 + body.Event.Level = localLevel + body.Event.Progress = 0 + body.Event.StartedAtTimestamp = util.GetTimestamp().Add(5 * -time.Minute).Format(time.RFC3339Nano) } event, err = json.Marshal(body) if err != nil { From a7d9a0f3fa762b425176a4e9d833cbf61e5940fd Mon Sep 17 00:00:00 2001 From: Martin Purcell Date: Thu, 22 Jul 2021 18:55:03 +0100 Subject: [PATCH 23/36] fix: localLevel could sometimes be zero --- internal/events/types/hype_train/hype_train_event.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/events/types/hype_train/hype_train_event.go b/internal/events/types/hype_train/hype_train_event.go index 1f6b6a44..1462ba73 100644 --- a/internal/events/types/hype_train/hype_train_event.go +++ b/internal/events/types/hype_train/hype_train_event.go @@ -39,7 +39,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven lastType := util.RandomType() //Local variables which will be used for the trigger params below - localLevel := util.RandomInt(5) + localLevel := util.RandomInt(4) + 1 localTotal := util.RandomInt(10 * 100) localGoal := util.RandomInt(10*100*100) + localTotal localProgress := (localTotal / localGoal) From 4cb8e8a1e3fb21d29c293723995c73cd3a3466fc Mon Sep 17 00:00:00 2001 From: Martin Purcell Date: Thu, 22 Jul 2021 18:55:43 +0100 Subject: [PATCH 24/36] feat: simplified the statements for progress and end events --- internal/events/types/hype_train/hype_train_event.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/events/types/hype_train/hype_train_event.go b/internal/events/types/hype_train/hype_train_event.go index 1462ba73..ecc670f3 100644 --- a/internal/events/types/hype_train/hype_train_event.go +++ b/internal/events/types/hype_train/hype_train_event.go @@ -96,10 +96,10 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven ExpiresAtTimestamp: util.GetTimestamp().Add(5 * time.Minute).Format(time.RFC3339Nano), }, } - if triggerMapping[params.Transport][params.Trigger] == "channel.hype_train.progress" { + if params.Trigger == "hype-train-progress" { body.Event.Level = localLevel } - if triggerMapping[params.Transport][params.Trigger] == "channel.hype_train.end" { + if params.Trigger == "hype-train-end" { body.Event.CooldownEndsAtTimestamp = util.GetTimestamp().Add(1 * time.Hour).Format(time.RFC3339Nano) body.Event.EndedAtTimestamp = util.GetTimestamp().Format(time.RFC3339Nano) body.Event.ExpiresAtTimestamp = "" From 3f42430a1965c702b694a4b061ad2cc76d45becc Mon Sep 17 00:00:00 2001 From: lleadbet Date: Mon, 26 Jul 2021 12:53:00 -0400 Subject: [PATCH 25/36] updating eventsub to match production --- .gitignore | 2 +- .vscode/settings.json | 5 - docs/event.md | 126 +++++++++------ .../authorization.go} | 8 +- .../authorization_test.go} | 19 +-- internal/events/types/drop/drop.go | 107 ++++++++++++ internal/events/types/drop/drop_test.go | 75 +++++++++ internal/events/types/gift/channel_gift.go | 108 +++++++++++++ .../events/types/gift/channel_gift_test.go | 79 +++++++++ internal/events/types/poll/poll.go | 132 +++++++++++++++ internal/events/types/poll/poll_test.go | 107 ++++++++++++ .../events/types/prediction/prediction.go | 153 ++++++++++++++++++ .../types/prediction/prediction_test.go | 115 +++++++++++++ internal/events/types/subscribe/sub_event.go | 16 +- .../subscription_message.go | 113 +++++++++++++ .../subscription_message_test.go | 94 +++++++++++ internal/events/types/types.go | 22 ++- internal/models/drops.go | 29 ++-- internal/models/eventsub.go | 1 + internal/models/poll.go | 36 +++++ internal/models/prediction.go | 40 +++++ internal/models/subs.go | 48 ++++++ 22 files changed, 1332 insertions(+), 103 deletions(-) delete mode 100644 .vscode/settings.json rename internal/events/types/{authorization_revoke/authorization_revoke.go => authorization/authorization.go} (91%) rename internal/events/types/{authorization_revoke/authorization_revoke_test.go => authorization/authorization_test.go} (80%) create mode 100644 internal/events/types/drop/drop.go create mode 100644 internal/events/types/drop/drop_test.go create mode 100644 internal/events/types/gift/channel_gift.go create mode 100644 internal/events/types/gift/channel_gift_test.go create mode 100644 internal/events/types/poll/poll.go create mode 100644 internal/events/types/poll/poll_test.go create mode 100644 internal/events/types/prediction/prediction.go create mode 100644 internal/events/types/prediction/prediction_test.go create mode 100644 internal/events/types/subscription_message/subscription_message.go create mode 100644 internal/events/types/subscription_message/subscription_message_test.go create mode 100644 internal/models/poll.go create mode 100644 internal/models/prediction.go diff --git a/.gitignore b/.gitignore index 05722bde..6ca677ff 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,7 @@ twitch-cli *.html # Editor configs -.vsvode/ +.vscode/ .idea/ # junk files diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 1f29e122..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "cSpell.words": [ - "SPDX" - ] -} \ No newline at end of file diff --git a/docs/event.md b/docs/event.md index 07921cf6..7123382c 100644 --- a/docs/event.md +++ b/docs/event.md @@ -16,30 +16,42 @@ Used to either create or send mock events for use with local webhooks testing. **Args** -| Argument | Description | -|-----------------------|------------------------------------------------------------------------------------------------------------| -| `subscribe` | A standard subscription event. Triggers a basic tier 1 sub. | -| `unsubscribe` | A standard unsubscribe event. Triggers a basic tier 1 sub. | -| `gift` | A gifted subscription event. Triggers a basic tier 1 sub. | -| `cheer` | Only usable with the `eventsub` transport, shows Cheers from chat. | -| `transaction` | Bits in Extensions transactions events. | -| `add-reward` | Channel Points EventSub event for a Custom Reward being added. | -| `update-reward` | Channel Points EventSub event for a Custom Reward being updated. | -| `remove-reward` | Channel Points EventSub event for a Custom Reward being removed. | -| `add-redemption` | Channel Points EventSub event for a redemption being performed. | -| `update-redemption` | Channel Points EventSub event for a redemption being updated. | -| `raid` | Channel Raid event with a random viewer count. | -| `revoke` | User authorization revoke event. Uses local Client as set in `twitch configure` or generates one randomly. | -| `stream-change` | Stream Changed event. | -| `streamup` | Stream online event. | -| `streamdown` | Sstream offline event. | -| `add-moderator` | Channel moderator add event. | -| `remove-moderator` | Channel moderator removal event. | -| `ban` | Channel ban event. | -| `unban` | Channel unban event. | -| `hype-train-begin` | Channel hype train start event. | -| `hype-train-progress` | Channel hype train progress event. | -| `hype-train-end` | Channel hype train end event. | +| Argument | Description | +|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------| +| `add-moderator` | Channel moderator add event. | +| `add-redemption` | Channel Points EventSub event for a redemption being performed. | +| `add-reward` | Channel Points EventSub event for a Custom Reward being added. | +| `ban` | Channel ban event. | +| `channel-gift` | Channel gifting event; not to be confused with the `gift` event. This event is a description of the number of gifts given by a user. | +| `cheer` | Only usable with the `eventsub` transport | +| `drop` | Drops Entitlement event. | +| `gift` | A gifted subscription event. Triggers a basic tier 1 sub. | +| `grant` | Authorization grant event. | +| `hype-train-begin` | Channel hype train start event. | +| `hype-train-end` | Channel hype train end event. | +| `hype-train-progress` | Channel hype train progress event. | +| `poll-begin` | Channel poll begin event. | +| `poll-end` | Channel poll end event. | +| `poll-progress` | Channel poll progress event. | +| `prediction-begin` | Channel prediction begin event. | +| `prediction-end` | Channel prediction end event. | +| `prediction-lock` | Channel prediction lock event. | +| `prediction-progress` | Channel prediction progress event. | +| `raid` | Channel Raid event with a random viewer count. | +| `remove-moderator` | Channel moderator removal event. | +| `remove-reward` | Channel Points EventSub event for a Custom Reward being removed. | +| `revoke` | User authorization revoke event. Uses local Client as set in `twitch configure` or generates one randomly. | +| `stream-change` | Stream Changed event. | +| `streamdown` | Sstream offline event. | +| `streamup` | Stream online event. | +| `subscribe-message` | Subscription Message event. | +| `subscribe` | A standard subscription event. Triggers a basic tier 1 sub. | +| `transaction` | Bits in Extensions transactions events. | +| `unban` | Channel unban event. | +| `unsubscribe` | A standard unsubscribe event. Triggers a basic tier 1 sub. | +| `update-redemption` | Channel Points EventSub event for a redemption being updated. | +| `update-reward` | Channel Points EventSub event for a Custom Reward being updated. | + @@ -48,18 +60,18 @@ Used to either create or send mock events for use with local webhooks testing. | Flag | Shorthand | Description | Example | Required? (Y/N) | |---------------------|-----------|---------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------|-----------------| | `--forward-address` | `-F` | Web server address for where to send mock events. | `-F https://localhost:8080` | N | -| `--transport` | `-T` | The method used to send events. Default is eventsub, but can send using websub. | `-T websub` | N | +| `--transport` | `-T` | The method used to send events. Default is `eventsub`, but can send using `websub`. | `-T websub` | N | | `--to-user` | `-t` | Denotes the receiver's TUID of the event, usually the broadcaster. | `-t 44635596` | N | | `--from-user` | `-f` | Denotes the sender's TUID of the event, for example the user that follows another user or the subscriber to a broadcaster. | `-f 44635596` | N | | `--gift-user` | `-g` | Used only for subcription-based events, denotes the gifting user ID | `-g 44635596` | N | | `--secret` | `-s` | Webhook secret. If defined, signs all forwarded events with the SHA256 HMAC. | `-s testsecret` | N | -| `--count` | `-c` | Count of events to fire. This can be used to simulate an influx of subscriptions. | `-c 100` | N | +| `--count` | `-c` | Count of events to fire. This can be used to simulate an influx of events. | `-c 100` | N | | `--anonymous` | `-a` | If the event is anonymous. Only applies to `gift` and `cheer` events. | `-a` | N | | `--status` | `-S` | Status of the event object, currently applies to channel points redemptions. | `-S fulfilled` | N | | `--item-id` | `-i` | Manually set the ID of the event payload item (for example the reward ID in redemption events or game in stream events). | `-i 032e4a6c-4aef-11eb-a9f5-1f703d1f0b92` | N | | `--item-name` | `-n` | Manually set the name of the event payload item (for example the reward ID in redemption events or game name in stream events). | `-n "Science & Technology"` | N | | `--cost` | `-C` | Amount of bits or channel points redeemed/used in the event. | `-C 250` | N | -| `--description` | `-d` | Title the stream should be updated/started with. | `-d Awesome new title!` | N | +| `--description` | `-d` | Title the stream should be updated/started with. Additionally used as the category ID for Drops events. | `-d Awesome new title!` | N | **Examples** @@ -110,30 +122,42 @@ Allows you to test if your webserver responds to subscription requests properly. **Args** -| Argument | Description | -|-----------------------|------------------------------------------------------------------------------------------------------------| -| `subscribe` | A standard subscription event. Triggers a basic tier 1 sub. | -| `unsubscribe` | A standard unsubscribe event. Triggers a basic tier 1 sub. | -| `gift` | A gifted subscription event. Triggers a basic tier 1 sub. | -| `cheer` | Only usable with the `eventsub` transport, shows Cheers from chat. | -| `transaction` | Bits in Extensions transactions events. | -| `add-reward` | Channel Points EventSub event for a Custom Reward being added. | -| `update-reward` | Channel Points EventSub event for a Custom Reward being updated. | -| `remove-reward` | Channel Points EventSub event for a Custom Reward being removed. | -| `add-redemption` | Channel Points EventSub event for a redemption being performed. | -| `update-redemption` | Channel Points EventSub event for a redemption being updated. | -| `raid` | Channel Raid event with a random viewer count. | -| `revoke` | User authorization revoke event. Uses local Client as set in `twitch configure` or generates one randomly. | -| `stream-change` | Stream Changed event. | -| `streamup` | Stream online event. | -| `streamdown` | Sstream offline event. | -| `add-moderator` | Channel moderator add event. | -| `remove-moderator` | Channel moderator removal event. | -| `ban` | Channel ban event. | -| `unban` | Channel unban event. | -| `hype-train-begin` | Channel hype train start event. | -| `hype-train-progress` | Channel hype train progress event. | -| `hype-train-end` | Channel hype train end event. | +| Argument | Description | +|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------| +| `add-moderator` | Channel moderator add event. | +| `add-redemption` | Channel Points EventSub event for a redemption being performed. | +| `add-reward` | Channel Points EventSub event for a Custom Reward being added. | +| `ban` | Channel ban event. | +| `channel-gift` | Channel gifting event; not to be confused with the `gift` event. This event is a description of the number of gifts given by a user. | +| `cheer` | Only usable with the `eventsub` transport | +| `drop` | Drops Entitlement event. | +| `gift` | A gifted subscription event. Triggers a basic tier 1 sub. | +| `grant` | Authorization grant event. | +| `hype-train-begin` | Channel hype train start event. | +| `hype-train-end` | Channel hype train end event. | +| `hype-train-progress` | Channel hype train progress event. | +| `poll-begin` | Channel poll begin event. | +| `poll-end` | Channel poll end event. | +| `poll-progress` | Channel poll progress event. | +| `prediction-begin` | Channel prediction begin event. | +| `prediction-end` | Channel prediction end event. | +| `prediction-lock` | Channel prediction lock event. | +| `prediction-progress` | Channel prediction progress event. | +| `raid` | Channel Raid event with a random viewer count. | +| `remove-moderator` | Channel moderator removal event. | +| `remove-reward` | Channel Points EventSub event for a Custom Reward being removed. | +| `revoke` | User authorization revoke event. Uses local Client as set in `twitch configure` or generates one randomly. | +| `stream-change` | Stream Changed event. | +| `streamdown` | Sstream offline event. | +| `streamup` | Stream online event. | +| `subscribe-message` | Subscription Message event. | +| `subscribe` | A standard subscription event. Triggers a basic tier 1 sub. | +| `transaction` | Bits in Extensions transactions events. | +| `unban` | Channel unban event. | +| `unsubscribe` | A standard unsubscribe event. Triggers a basic tier 1 sub. | +| `update-redemption` | Channel Points EventSub event for a redemption being updated. | +| `update-reward` | Channel Points EventSub event for a Custom Reward being updated. | + **Flags** diff --git a/internal/events/types/authorization_revoke/authorization_revoke.go b/internal/events/types/authorization/authorization.go similarity index 91% rename from internal/events/types/authorization_revoke/authorization_revoke.go rename to internal/events/types/authorization/authorization.go index 09b32f39..8e5d7dbb 100644 --- a/internal/events/types/authorization_revoke/authorization_revoke.go +++ b/internal/events/types/authorization/authorization.go @@ -1,10 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package authorization_revoke +package authorization import ( "encoding/json" - "errors" "time" "github.com/spf13/viper" @@ -18,11 +17,12 @@ var transportsSupported = map[string]bool{ models.TransportEventSub: true, } -var triggerSupported = []string{"revoke"} +var triggerSupported = []string{"revoke", "grant"} var triggerMapping = map[string]map[string]string{ models.TransportEventSub: { "revoke": "user.authorization.revoke", + "grant": "user.authorization.grant", }, } @@ -66,8 +66,6 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven if err != nil { return events.MockEventResponse{}, err } - case models.TransportWebSub: - return events.MockEventResponse{}, errors.New("Websub is unsupported for authorization revoke events") default: return events.MockEventResponse{}, nil } diff --git a/internal/events/types/authorization_revoke/authorization_revoke_test.go b/internal/events/types/authorization/authorization_test.go similarity index 80% rename from internal/events/types/authorization_revoke/authorization_revoke_test.go rename to internal/events/types/authorization/authorization_test.go index a26a7f11..eeaa4627 100644 --- a/internal/events/types/authorization_revoke/authorization_revoke_test.go +++ b/internal/events/types/authorization/authorization_test.go @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package authorization_revoke +package authorization import ( "encoding/json" @@ -29,7 +29,7 @@ func TestEventSub(t *testing.T) { r, err := Event{}.GenerateEvent(params) a.Nil(err) - var body models.AuthorizationRevokeEventSubResponse // replace with actual value + var body models.AuthorizationRevokeEventSubResponse err = json.Unmarshal(r.JSON, &body) a.Nil(err) @@ -37,21 +37,6 @@ func TestEventSub(t *testing.T) { a.Equal(body.Event.ClientID, body.Subscription.Condition.ClientID) a.Equal("1234", body.Event.ClientID) } -func TestWebSub(t *testing.T) { - a := test_setup.SetupTestEnv(t) - - params := *&events.MockEventParameters{ - FromUserID: fromUser, - ToUserID: toUser, - Transport: models.TransportWebSub, - Trigger: "revoke", - } - - _, err := Event{}.GenerateEvent(params) - a.NotNil(err) - - // write tests here for websub -} func TestFakeTransport(t *testing.T) { a := test_setup.SetupTestEnv(t) diff --git a/internal/events/types/drop/drop.go b/internal/events/types/drop/drop.go new file mode 100644 index 00000000..4b5aa184 --- /dev/null +++ b/internal/events/types/drop/drop.go @@ -0,0 +1,107 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package drop + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/twitchdev/twitch-cli/internal/events" + "github.com/twitchdev/twitch-cli/internal/models" + "github.com/twitchdev/twitch-cli/internal/util" +) + +var transportsSupported = map[string]bool{ + models.TransportWebSub: false, + models.TransportEventSub: true, +} + +var triggerSupported = []string{"drop"} + +var triggerMapping = map[string]map[string]string{ + models.TransportEventSub: { + "drop": "drop.entitlement.grant", + }, +} + +type Event struct{} + +func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEventResponse, error) { + var event []byte + var err error + + if params.ItemID == "" { + params.ItemID = util.RandomGUID() + } + + if params.Description == "" { + params.Description = fmt.Sprintf("%v", util.RandomInt(1000)) + } + switch params.Transport { + case models.TransportEventSub: + body := &models.DropsEntitlementEventSubResponse{ + Subscription: models.EventsubSubscription{ + ID: params.ID, + Status: "enabled", + Type: triggerMapping[params.Transport][params.Trigger], + Version: "1", + Condition: models.EventsubCondition{ + OrganizationID: params.FromUserID, + }, + Transport: models.EventsubTransport{ + Method: "webhook", + Callback: "null", + }, + Cost: 0, + CreatedAt: util.GetTimestamp().Format(time.RFC3339Nano), + }, + Events: []models.DropsEntitlementEventSubEvent{ + { + ID: util.RandomGUID(), + Data: models.DropsEntitlementEventSubEventData{ + OrganizationID: params.FromUserID, + CategoryID: params.Description, + CategoryName: "", + CampaignID: util.RandomGUID(), + EntitlementID: util.RandomGUID(), + BenefitID: params.ItemID, + UserID: params.ToUserID, + UserName: params.ToUserName, + UserLogin: params.ToUserName, + CreatedAt: util.GetTimestamp().Format(time.RFC3339Nano), + }, + }, + }, + } + event, err = json.Marshal(body) + if err != nil { + return events.MockEventResponse{}, err + } + default: + return events.MockEventResponse{}, nil + } + + return events.MockEventResponse{ + ID: params.ID, + JSON: event, + FromUser: params.FromUserID, + ToUser: params.ToUserID, + }, nil +} + +func (e Event) ValidTransport(t string) bool { + return transportsSupported[t] +} + +func (e Event) ValidTrigger(t string) bool { + for _, ts := range triggerSupported { + if ts == t { + return true + } + } + return false +} +func (e Event) GetTopic(transport string, trigger string) string { + return triggerMapping[transport][trigger] +} diff --git a/internal/events/types/drop/drop_test.go b/internal/events/types/drop/drop_test.go new file mode 100644 index 00000000..f0bb768d --- /dev/null +++ b/internal/events/types/drop/drop_test.go @@ -0,0 +1,75 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package drop + +import ( + "encoding/json" + "testing" + + "github.com/twitchdev/twitch-cli/internal/events" + "github.com/twitchdev/twitch-cli/internal/models" + "github.com/twitchdev/twitch-cli/test_setup" +) + +var fromUser = "1234" +var toUser = "4567" + +func TestEventSub(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + params := *&events.MockEventParameters{ + FromUserID: fromUser, + ToUserID: toUser, + Transport: models.TransportEventSub, + Trigger: "drop", + } + + r, err := Event{}.GenerateEvent(params) + a.Nil(err) + + var body models.DropsEntitlementEventSubResponse // replace with actual value + err = json.Unmarshal(r.JSON, &body) + a.Nil(err) + + a.Len(body.Events, 1) +} + +func TestFakeTransport(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + params := *&events.MockEventParameters{ + FromUserID: fromUser, + ToUserID: toUser, + Transport: "fake_transport", + Trigger: "drop", + } + + r, err := Event{}.GenerateEvent(params) + a.Nil(err) + a.Empty(r) +} +func TestValidTrigger(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + r := Event{}.ValidTrigger("drop") + a.Equal(true, r) + + r = Event{}.ValidTrigger("notdrop") + a.Equal(false, r) +} + +func TestValidTransport(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + r := Event{}.ValidTransport(models.TransportEventSub) + a.Equal(true, r) + + r = Event{}.ValidTransport("noteventsub") + a.Equal(false, r) +} +func TestGetTopic(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + r := Event{}.GetTopic(models.TransportEventSub, "drop") + a.NotNil(r) +} diff --git a/internal/events/types/gift/channel_gift.go b/internal/events/types/gift/channel_gift.go new file mode 100644 index 00000000..b01d9ee7 --- /dev/null +++ b/internal/events/types/gift/channel_gift.go @@ -0,0 +1,108 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package gift + +import ( + "encoding/json" + "time" + + "github.com/twitchdev/twitch-cli/internal/events" + "github.com/twitchdev/twitch-cli/internal/models" + "github.com/twitchdev/twitch-cli/internal/util" +) + +var transportsSupported = map[string]bool{ + models.TransportWebSub: false, + models.TransportEventSub: true, +} + +var triggerSupported = []string{"channel-gift"} + +var triggerMapping = map[string]map[string]string{ + models.TransportEventSub: { + "channel-gift": "channel.subscription.gift", + }, +} + +type Event struct{} + +func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEventResponse, error) { + var event []byte + var err error + + if params.Cost <= 0 { + params.Cost = 5 + } + + total := int(util.RandomInt(200) + params.Cost) + + if params.IsAnonymous { + params.FromUserID = "274598607" + params.FromUserName = "ananonymousgifter" + } + + switch params.Transport { + case models.TransportEventSub: + body := &models.GiftEventSubResponse{ + Subscription: models.EventsubSubscription{ + ID: params.ID, + Status: "enabled", + Type: triggerMapping[params.Transport][params.Trigger], + Version: "1", + Condition: models.EventsubCondition{ + BroadcasterUserID: params.ToUserID, + }, + Transport: models.EventsubTransport{ + Method: "webhook", + Callback: "null", + }, + Cost: 0, + CreatedAt: util.GetTimestamp().Format(time.RFC3339Nano), + }, + Event: models.GiftEventSubEvent{ + UserID: params.FromUserID, + UserLogin: params.FromUserName, + UserName: params.FromUserName, + BroadcasterUserID: params.ToUserID, + BroadcasterUserLogin: params.ToUserName, + BroadcasterUserName: params.ToUserName, + Tier: "1000", + Total: int(params.Cost), + CumulativeTotal: &total, + IsAnonymous: params.IsAnonymous, + }, + } + if params.IsAnonymous { + body.Event.CumulativeTotal = nil + } + event, err = json.Marshal(body) + if err != nil { + return events.MockEventResponse{}, err + } + default: + return events.MockEventResponse{}, nil + } + + return events.MockEventResponse{ + ID: params.ID, + JSON: event, + FromUser: params.FromUserID, + ToUser: params.ToUserID, + }, nil +} + +func (e Event) ValidTransport(t string) bool { + return transportsSupported[t] +} + +func (e Event) ValidTrigger(t string) bool { + for _, ts := range triggerSupported { + if ts == t { + return true + } + } + return false +} +func (e Event) GetTopic(transport string, trigger string) string { + return triggerMapping[transport][trigger] +} diff --git a/internal/events/types/gift/channel_gift_test.go b/internal/events/types/gift/channel_gift_test.go new file mode 100644 index 00000000..98397bc2 --- /dev/null +++ b/internal/events/types/gift/channel_gift_test.go @@ -0,0 +1,79 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package gift + +import ( + "encoding/json" + "testing" + + "github.com/twitchdev/twitch-cli/internal/events" + "github.com/twitchdev/twitch-cli/internal/models" + "github.com/twitchdev/twitch-cli/test_setup" +) + +var fromUser = "1234" +var toUser = "4567" + +func TestEventSub(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + params := *&events.MockEventParameters{ + FromUserID: fromUser, + ToUserID: toUser, + Transport: models.TransportEventSub, + Trigger: "channel-gift", + Cost: 0, + IsAnonymous: true, + } + + r, err := Event{}.GenerateEvent(params) + a.Nil(err) + + var body models.GiftEventSubResponse // replace with actual value + err = json.Unmarshal(r.JSON, &body) + a.Nil(err) + + a.GreaterOrEqual(body.Event.Total, 1) + a.Nil(body.Event.CumulativeTotal) + a.NotEmpty(body.Event.UserID) +} + +func TestFakeTransport(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + params := *&events.MockEventParameters{ + FromUserID: fromUser, + ToUserID: toUser, + Transport: "fake_transport", + Trigger: "unsubscribe", + } + + r, err := Event{}.GenerateEvent(params) + a.Nil(err) + a.Empty(r) +} +func TestValidTrigger(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + r := Event{}.ValidTrigger("channel-gift") + a.Equal(true, r) + + r = Event{}.ValidTrigger("notgift") + a.Equal(false, r) +} + +func TestValidTransport(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + r := Event{}.ValidTransport(models.TransportEventSub) + a.Equal(true, r) + + r = Event{}.ValidTransport("noteventsub") + a.Equal(false, r) +} +func TestGetTopic(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + r := Event{}.GetTopic(models.TransportEventSub, "channel-gift") + a.NotNil(r) +} diff --git a/internal/events/types/poll/poll.go b/internal/events/types/poll/poll.go new file mode 100644 index 00000000..5fa6e84a --- /dev/null +++ b/internal/events/types/poll/poll.go @@ -0,0 +1,132 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package poll + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/twitchdev/twitch-cli/internal/events" + "github.com/twitchdev/twitch-cli/internal/models" + "github.com/twitchdev/twitch-cli/internal/util" +) + +var transportsSupported = map[string]bool{ + models.TransportWebSub: false, + models.TransportEventSub: true, +} + +var triggerSupported = []string{"poll-begin", "poll-progress", "poll-end"} + +var triggerMapping = map[string]map[string]string{ + models.TransportEventSub: { + "poll-begin": "channel.poll.begin", + "poll-progress": "channel.poll.progress", + "poll-end": "channel.poll.end", + }, +} + +type Event struct{} + +func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEventResponse, error) { + var event []byte + var err error + + if params.Description == "" { + params.Description = "Pineapple on pizza?" + } + + switch params.Transport { + case models.TransportEventSub: + choices := []models.PollEventSubEventChoice{} + for i := 1; i < 5; i++ { + c := models.PollEventSubEventChoice{ + ID: util.RandomGUID(), + Title: fmt.Sprintf("Yes but choice %v", i), + } + if params.Trigger != "poll-begin" { + c.BitsVotes = intPointer(int(util.RandomInt(10))) + c.ChannelPointsVotes = intPointer(int(util.RandomInt(10))) + c.Votes = intPointer(*c.BitsVotes + *c.ChannelPointsVotes + int(util.RandomInt(10))) + } + choices = append(choices, c) + } + + body := &models.PollEventSubResponse{ + Subscription: models.EventsubSubscription{ + ID: params.ID, + Status: "enabled", + Type: triggerMapping[params.Transport][params.Trigger], + Version: "1", + Condition: models.EventsubCondition{ + BroadcasterUserID: params.ToUserID, + }, + Transport: models.EventsubTransport{ + Method: "webhook", + Callback: "null", + }, + Cost: 0, + CreatedAt: util.GetTimestamp().Format(time.RFC3339Nano), + }, + Event: models.PollEventSubEvent{ + ID: util.RandomGUID(), + BroadcasterUserID: params.ToUserID, + BroadcasterUserLogin: params.ToUserName, + BroadcasterUserName: params.ToUserName, + Title: params.Description, + Choices: choices, + BitsVoting: models.PollEventSubEventGoodVoting{ + IsEnabled: true, + AmountPerVote: 10, + }, + ChannelPointsVoting: models.PollEventSubEventGoodVoting{ + IsEnabled: true, + AmountPerVote: 500, + }, + StartedAt: util.GetTimestamp().Format(time.RFC3339Nano), + }, + } + + if params.Trigger == "poll-end" { + body.Event.EndedAt = util.GetTimestamp().Add(time.Minute * 15).Format(time.RFC3339Nano) + body.Event.Status = "completed" + } else { + body.Event.EndsAt = util.GetTimestamp().Add(time.Minute * 15).Format(time.RFC3339Nano) + } + + event, err = json.Marshal(body) + if err != nil { + return events.MockEventResponse{}, err + } + default: + return events.MockEventResponse{}, nil + } + + return events.MockEventResponse{ + ID: params.ID, + JSON: event, + FromUser: params.FromUserID, + ToUser: params.ToUserID, + }, nil +} + +func (e Event) ValidTransport(t string) bool { + return transportsSupported[t] +} + +func (e Event) ValidTrigger(t string) bool { + for _, ts := range triggerSupported { + if ts == t { + return true + } + } + return false +} +func (e Event) GetTopic(transport string, trigger string) string { + return triggerMapping[transport][trigger] +} + +func intPointer(i int) *int { + return &i +} diff --git a/internal/events/types/poll/poll_test.go b/internal/events/types/poll/poll_test.go new file mode 100644 index 00000000..c74601e0 --- /dev/null +++ b/internal/events/types/poll/poll_test.go @@ -0,0 +1,107 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package poll + +import ( + "encoding/json" + "testing" + + "github.com/twitchdev/twitch-cli/internal/events" + "github.com/twitchdev/twitch-cli/internal/models" + "github.com/twitchdev/twitch-cli/test_setup" +) + +var fromUser = "1234" +var toUser = "4567" + +func TestEventSub(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + params := *&events.MockEventParameters{ + FromUserID: fromUser, + ToUserID: toUser, + Transport: models.TransportEventSub, + Trigger: "poll-begin", + } + + r, err := Event{}.GenerateEvent(params) + a.Nil(err) + + var body models.PollEventSubResponse + err = json.Unmarshal(r.JSON, &body) + a.Nil(err) + a.NotEmpty(body.Event.EndsAt) + a.Empty(body.Event.EndedAt) + + params = *&events.MockEventParameters{ + FromUserID: fromUser, + ToUserID: toUser, + Transport: models.TransportEventSub, + Trigger: "poll-progress", + } + + r, err = Event{}.GenerateEvent(params) + a.Nil(err) + + body = models.PollEventSubResponse{} + err = json.Unmarshal(r.JSON, &body) + a.Nil(err) + a.NotEmpty(body.Event.EndsAt) + a.Empty(body.Event.EndedAt) + + params = *&events.MockEventParameters{ + FromUserID: fromUser, + ToUserID: toUser, + Transport: models.TransportEventSub, + Trigger: "poll-end", + } + + r, err = Event{}.GenerateEvent(params) + a.Nil(err) + + body = models.PollEventSubResponse{} + err = json.Unmarshal(r.JSON, &body) + a.Nil(err) + a.Empty(body.Event.EndsAt) + a.NotEmpty(body.Event.EndedAt) +} + +func TestFakeTransport(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + params := *&events.MockEventParameters{ + FromUserID: fromUser, + ToUserID: toUser, + Transport: "fake_transport", + Trigger: "poll-begin", + } + + r, err := Event{}.GenerateEvent(params) + a.Nil(err) + a.Empty(r) +} +func TestValidTrigger(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + r := Event{}.ValidTrigger("poll-begin") + a.Equal(true, r) + + r = Event{}.ValidTrigger("notgift") + a.Equal(false, r) +} + +func TestValidTransport(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + r := Event{}.ValidTransport(models.TransportEventSub) + a.Equal(true, r) + + r = Event{}.ValidTransport("noteventsub") + a.Equal(false, r) +} +func TestGetTopic(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + r := Event{}.GetTopic(models.TransportEventSub, "poll-begin") + a.NotNil(r) +} diff --git a/internal/events/types/prediction/prediction.go b/internal/events/types/prediction/prediction.go new file mode 100644 index 00000000..5c024c7f --- /dev/null +++ b/internal/events/types/prediction/prediction.go @@ -0,0 +1,153 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package prediction + +import ( + "encoding/json" + "time" + + "github.com/twitchdev/twitch-cli/internal/events" + "github.com/twitchdev/twitch-cli/internal/models" + "github.com/twitchdev/twitch-cli/internal/util" +) + +var transportsSupported = map[string]bool{ + models.TransportWebSub: false, + models.TransportEventSub: true, +} + +var triggerSupported = []string{"prediction-begin", "prediction-progress", "prediction-end"} + +var triggerMapping = map[string]map[string]string{ + models.TransportEventSub: { + "prediction-begin": "channel.prediction.begin", + "prediction-progress": "channel.prediction.progress", + "prediction-lock": "channel.prediction.lock", + "prediction-end": "channel.prediction.end", + }, +} + +type Event struct{} + +func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEventResponse, error) { + var event []byte + var err error + if params.Description == "" { + params.Description = "Will the developer finish this program?" + } + + switch params.Transport { + case models.TransportEventSub: + var outcomes []models.PredictionEventSubEventOutcomes + for i := 0; i < 2; i++ { + color := "blue" + title := "yes" + + if i == 1 { + color = "pink" + title = "no" + } + + o := models.PredictionEventSubEventOutcomes{ + ID: util.RandomGUID(), + Title: title, + Color: color, + } + + if params.Trigger != "prediction-begin" { + tp := []models.PredictionEventSubEventTopPredictors{} + + for j := 0; j < int(util.RandomInt(10))+1; j++ { + t := models.PredictionEventSubEventTopPredictors{ + UserID: util.RandomUserID(), + UserLogin: "testLogin", + UserName: "testLogin", + ChannelPointsUsed: int(util.RandomInt(10*1000)) + 100, + } + if params.Trigger == "prediction-lock" || params.Trigger == "prediction-end" { + if i == 0 { + t.ChannelPointsWon = intPointer(t.ChannelPointsUsed * 2) + } else { + t.ChannelPointsWon = intPointer(0) + } + } + tp = append(tp, t) + o.TopPredictors = &tp + } + } + + outcomes = append(outcomes, o) + } + + body := &models.PredictionEventSubResponse{ + Subscription: models.EventsubSubscription{ + ID: params.ID, + Status: "enabled", + Type: triggerMapping[params.Transport][params.Trigger], + Version: "1", + Condition: models.EventsubCondition{ + BroadcasterUserID: params.ToUserID, + }, + Transport: models.EventsubTransport{ + Method: "webhook", + Callback: "null", + }, + Cost: 0, + CreatedAt: util.GetTimestamp().Format(time.RFC3339Nano), + }, + Event: models.PredictionEventSubEvent{ + ID: util.RandomGUID(), + BroadcasterUserID: params.ToUserID, + BroadcasterUserLogin: params.ToUserName, + BroadcasterUserName: params.ToUserName, + Title: params.Description, + Outcomes: outcomes, + StartedAt: util.GetTimestamp().Format(time.RFC3339Nano), + }, + } + + if params.Trigger == "prediction-begin" || params.Trigger == "prediction-progress" { + body.Event.LocksAt = util.GetTimestamp().Add(time.Minute * 10).Format(time.RFC3339Nano) + } else if params.Trigger == "prediction-lock" { + body.Event.LockedAt = util.GetTimestamp().Add(time.Minute * 10).Format(time.RFC3339Nano) + } else if params.Trigger == "prediction-end" { + body.Event.WinningOutcomeID = outcomes[0].ID + body.Event.EndedAt = util.GetTimestamp().Add(time.Minute * 10).Format(time.RFC3339Nano) + body.Event.Status = "resolved" + } + + event, err = json.Marshal(body) + if err != nil { + return events.MockEventResponse{}, err + } + default: + return events.MockEventResponse{}, nil + } + + return events.MockEventResponse{ + ID: params.ID, + JSON: event, + FromUser: params.FromUserID, + ToUser: params.ToUserID, + }, nil +} + +func (e Event) ValidTransport(t string) bool { + return transportsSupported[t] +} + +func (e Event) ValidTrigger(t string) bool { + for _, ts := range triggerSupported { + if ts == t { + return true + } + } + return false +} +func (e Event) GetTopic(transport string, trigger string) string { + return triggerMapping[transport][trigger] +} + +func intPointer(i int) *int { + return &i +} diff --git a/internal/events/types/prediction/prediction_test.go b/internal/events/types/prediction/prediction_test.go new file mode 100644 index 00000000..a66c0897 --- /dev/null +++ b/internal/events/types/prediction/prediction_test.go @@ -0,0 +1,115 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package prediction + +import ( + "encoding/json" + "testing" + + "github.com/twitchdev/twitch-cli/internal/events" + "github.com/twitchdev/twitch-cli/internal/models" + "github.com/twitchdev/twitch-cli/test_setup" +) + +var fromUser = "1234" +var toUser = "4567" + +func TestEventSub(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + params := *&events.MockEventParameters{ + FromUserID: fromUser, + ToUserID: toUser, + Transport: models.TransportEventSub, + Trigger: "prediction-begin", + } + + r, err := Event{}.GenerateEvent(params) + a.Nil(err) + + var body models.PredictionEventSubResponse + err = json.Unmarshal(r.JSON, &body) + a.Nil(err) + + params = *&events.MockEventParameters{ + FromUserID: fromUser, + ToUserID: toUser, + Transport: models.TransportEventSub, + Trigger: "prediction-progress", + } + + r, err = Event{}.GenerateEvent(params) + a.Nil(err) + + body = models.PredictionEventSubResponse{} + err = json.Unmarshal(r.JSON, &body) + a.Nil(err) + + params = *&events.MockEventParameters{ + FromUserID: fromUser, + ToUserID: toUser, + Transport: models.TransportEventSub, + Trigger: "prediction-lock", + } + + r, err = Event{}.GenerateEvent(params) + a.Nil(err) + + body = models.PredictionEventSubResponse{} + err = json.Unmarshal(r.JSON, &body) + a.Nil(err) + + params = *&events.MockEventParameters{ + FromUserID: fromUser, + ToUserID: toUser, + Transport: models.TransportEventSub, + Trigger: "prediction-end", + } + + r, err = Event{}.GenerateEvent(params) + a.Nil(err) + + body = models.PredictionEventSubResponse{} + err = json.Unmarshal(r.JSON, &body) + a.Nil(err) +} + +func TestFakeTransport(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + params := *&events.MockEventParameters{ + FromUserID: fromUser, + ToUserID: toUser, + Transport: "fake_transport", + Trigger: "unsubscribe", + } + + r, err := Event{}.GenerateEvent(params) + a.Nil(err) + a.Empty(r) +} +func TestValidTrigger(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + r := Event{}.ValidTrigger("prediction-begin") + a.Equal(true, r) + + r = Event{}.ValidTrigger("notgift") + a.Equal(false, r) +} + +func TestValidTransport(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + r := Event{}.ValidTransport(models.TransportEventSub) + a.Equal(true, r) + + r = Event{}.ValidTransport("noteventsub") + a.Equal(false, r) +} +func TestGetTopic(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + r := Event{}.GetTopic(models.TransportEventSub, "prediction-begin") + a.NotNil(r) +} diff --git a/internal/events/types/subscribe/sub_event.go b/internal/events/types/subscribe/sub_event.go index 8f783f93..3fd79b6f 100644 --- a/internal/events/types/subscribe/sub_event.go +++ b/internal/events/types/subscribe/sub_event.go @@ -16,18 +16,20 @@ var transportsSupported = map[string]bool{ models.TransportEventSub: true, } -var triggerSupported = []string{"subscribe", "gift", "unsubscribe"} +var triggerSupported = []string{"subscribe", "gift", "unsubscribe", "subscribe-end"} var triggerMapping = map[string]map[string]string{ models.TransportWebSub: { - "subscribe": "subscriptions.subscribe", - "unsubscribe": "subscriptions.unsubscribe", - "gift": "subscriptions.subscribe", + "subscribe": "subscriptions.subscribe", + "unsubscribe": "subscriptions.unsubscribe", + "gift": "subscriptions.subscribe", + "subscribe-end": "", }, models.TransportEventSub: { - "subscribe": "channel.subscribe", - "unsubscribe": "channel.unsubscribe", - "gift": "channel.subscribe", + "subscribe": "channel.subscribe", + "unsubscribe": "channel.unsubscribe", + "gift": "channel.subscribe", + "subscribe-end": "channel.subscription.end", }, } diff --git a/internal/events/types/subscription_message/subscription_message.go b/internal/events/types/subscription_message/subscription_message.go new file mode 100644 index 00000000..6bcac31d --- /dev/null +++ b/internal/events/types/subscription_message/subscription_message.go @@ -0,0 +1,113 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package subscription_message + +import ( + "encoding/json" + "time" + + "github.com/twitchdev/twitch-cli/internal/events" + "github.com/twitchdev/twitch-cli/internal/models" + "github.com/twitchdev/twitch-cli/internal/util" +) + +var transportsSupported = map[string]bool{ + models.TransportWebSub: false, + models.TransportEventSub: true, +} + +var triggerSupported = []string{"subscribe-message"} + +var triggerMapping = map[string]map[string]string{ + models.TransportEventSub: { + "subscribe-message": "channel.subscription.message", + }, +} + +type Event struct{} + +func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEventResponse, error) { + var event []byte + var err error + + if params.Cost == 0 { + params.Cost = util.RandomInt(120) + 1 + } + + switch params.Transport { + case models.TransportEventSub: + body := &models.SubscribeMessageEventSubResponse{ + Subscription: models.EventsubSubscription{ + ID: params.ID, + Status: "enabled", + Type: triggerMapping[params.Transport][params.Trigger], + Version: "1", + Condition: models.EventsubCondition{ + BroadcasterUserID: params.ToUserID, + }, + Transport: models.EventsubTransport{ + Method: "webhook", + Callback: "null", + }, + Cost: 0, + CreatedAt: util.GetTimestamp().Format(time.RFC3339Nano), + }, + Event: models.SubscribeMessageEventSubEvent{ + UserID: params.FromUserID, + UserLogin: params.FromUserName, + UserName: params.FromUserName, + BroadcasterUserID: params.ToUserID, + BroadcasterUserLogin: params.ToUserName, + BroadcasterUserName: params.ToUserName, + Tier: "1000", + Message: models.SubscribeMessageEventSubMessage{ + Text: "Hello from the Twitch CLI! twitchdevLeek", + Emotes: []models.SubscribeMessageEventSubMessageEmote{ + { + Begin: 26, + End: 39, + ID: "304456816", + }, + }, + }, + CumulativeMonths: int(params.Cost) + int(util.RandomInt(10)), + DurationMonths: 1, + }, + } + + if !params.IsAnonymous { + streak := int(params.Cost) + body.Event.StreakMonths = &streak + } + event, err = json.Marshal(body) + if err != nil { + return events.MockEventResponse{}, err + } + + default: + return events.MockEventResponse{}, nil + } + + return events.MockEventResponse{ + ID: params.ID, + JSON: event, + FromUser: params.FromUserID, + ToUser: params.ToUserID, + }, nil +} + +func (e Event) ValidTransport(t string) bool { + return transportsSupported[t] +} + +func (e Event) ValidTrigger(t string) bool { + for _, ts := range triggerSupported { + if ts == t { + return true + } + } + return false +} +func (e Event) GetTopic(transport string, trigger string) string { + return triggerMapping[transport][trigger] +} diff --git a/internal/events/types/subscription_message/subscription_message_test.go b/internal/events/types/subscription_message/subscription_message_test.go new file mode 100644 index 00000000..7f30b2c4 --- /dev/null +++ b/internal/events/types/subscription_message/subscription_message_test.go @@ -0,0 +1,94 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package subscription_message + +import ( + "encoding/json" + "testing" + + "github.com/twitchdev/twitch-cli/internal/events" + "github.com/twitchdev/twitch-cli/internal/models" + "github.com/twitchdev/twitch-cli/test_setup" +) + +var fromUser = "1234" +var toUser = "4567" + +func TestEventSub(t *testing.T) { + a := test_setup.SetupTestEnv(t) + ten := 10 + + params := *&events.MockEventParameters{ + FromUserID: fromUser, + ToUserID: toUser, + Transport: models.TransportEventSub, + Trigger: "subscribe-message", + Cost: int64(ten), + } + + r, err := Event{}.GenerateEvent(params) + a.Nil(err) + + var body models.SubscribeMessageEventSubResponse // replace with actual value + err = json.Unmarshal(r.JSON, &body) + a.Nil(err) + a.Equal(&ten, body.Event.StreakMonths) + a.GreaterOrEqual(body.Event.CumulativeMonths, 10) + + params = *&events.MockEventParameters{ + FromUserID: fromUser, + ToUserID: toUser, + Transport: models.TransportEventSub, + Trigger: "subscribe-message", + Cost: int64(ten), + IsAnonymous: true, + } + + r, err = Event{}.GenerateEvent(params) + a.Nil(err) + + err = json.Unmarshal(r.JSON, &body) + a.Nil(err) + a.Nil(body.Event.StreakMonths) + a.GreaterOrEqual(body.Event.CumulativeMonths, 10) +} + +func TestFakeTransport(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + params := *&events.MockEventParameters{ + FromUserID: fromUser, + ToUserID: toUser, + Transport: "fake_transport", + Trigger: "subscribe-message", + } + + r, err := Event{}.GenerateEvent(params) + a.Nil(err) + a.Empty(r) +} +func TestValidTrigger(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + r := Event{}.ValidTrigger("subscribe-message") + a.Equal(true, r) + + r = Event{}.ValidTrigger("notmessage") + a.Equal(false, r) +} + +func TestValidTransport(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + r := Event{}.ValidTransport(models.TransportEventSub) + a.Equal(true, r) + + r = Event{}.ValidTransport("noteventsub") + a.Equal(false, r) +} +func TestGetTopic(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + r := Event{}.GetTopic(models.TransportEventSub, "subscribe-message") + a.NotNil(r) +} diff --git a/internal/events/types/types.go b/internal/events/types/types.go index 49676ad4..ff61a213 100644 --- a/internal/events/types/types.go +++ b/internal/events/types/types.go @@ -6,38 +6,48 @@ import ( "errors" "github.com/twitchdev/twitch-cli/internal/events" - "github.com/twitchdev/twitch-cli/internal/events/types/authorization_revoke" + "github.com/twitchdev/twitch-cli/internal/events/types/authorization" + "github.com/twitchdev/twitch-cli/internal/events/types/ban" "github.com/twitchdev/twitch-cli/internal/events/types/channel_points_redemption" "github.com/twitchdev/twitch-cli/internal/events/types/channel_points_reward" "github.com/twitchdev/twitch-cli/internal/events/types/cheer" + "github.com/twitchdev/twitch-cli/internal/events/types/drop" "github.com/twitchdev/twitch-cli/internal/events/types/extension_transaction" "github.com/twitchdev/twitch-cli/internal/events/types/follow" + "github.com/twitchdev/twitch-cli/internal/events/types/gift" "github.com/twitchdev/twitch-cli/internal/events/types/hype_train" "github.com/twitchdev/twitch-cli/internal/events/types/moderator_change" + "github.com/twitchdev/twitch-cli/internal/events/types/poll" + "github.com/twitchdev/twitch-cli/internal/events/types/prediction" "github.com/twitchdev/twitch-cli/internal/events/types/raid" "github.com/twitchdev/twitch-cli/internal/events/types/stream_change" "github.com/twitchdev/twitch-cli/internal/events/types/streamdown" "github.com/twitchdev/twitch-cli/internal/events/types/streamup" "github.com/twitchdev/twitch-cli/internal/events/types/subscribe" - "github.com/twitchdev/twitch-cli/internal/events/types/ban" + "github.com/twitchdev/twitch-cli/internal/events/types/subscription_message" ) func All() []events.MockEvent { return []events.MockEvent{ - authorization_revoke.Event{}, + authorization.Event{}, + ban.Event{}, channel_points_redemption.Event{}, channel_points_reward.Event{}, cheer.Event{}, + drop.Event{}, extension_transaction.Event{}, follow.Event{}, + gift.Event{}, hype_train.Event{}, + moderator_change.Event{}, + poll.Event{}, + prediction.Event{}, raid.Event{}, - subscribe.Event{}, stream_change.Event{}, streamup.Event{}, streamdown.Event{}, - moderator_change.Event{}, - ban.Event{}, + subscribe.Event{}, + subscription_message.Event{}, } } diff --git a/internal/models/drops.go b/internal/models/drops.go index 79bd2a04..65d6d93a 100644 --- a/internal/models/drops.go +++ b/internal/models/drops.go @@ -2,17 +2,24 @@ // SPDX-License-Identifier: Apache-2.0 package models -type DropsEntitlementsData struct { - ID string `json:"id"` - BenefitID string `json:"benefit_id"` - Timestamp string `json:"timestamp"` - UserID string `json:"user_id"` - GameID string `json:"game_id"` +type DropsEntitlementEventSubResponse struct { + Subscription EventsubSubscription `json:"subscription"` + Events []DropsEntitlementEventSubEvent `json:"events"` } -type DropsEntitlementsResponse struct { - Pagination struct { - Cursor string `json:"cursor"` - } `json:"pagination"` - Data []DropsEntitlementsData `json:"data"` +type DropsEntitlementEventSubEvent struct { + ID string `json:"id"` + Data DropsEntitlementEventSubEventData `json:"data"` +} +type DropsEntitlementEventSubEventData struct { + EntitlementID string `json:"entitlement_id"` + BenefitID string `json:"benefit_id"` + CampaignID string `json:"campaign_id"` + OrganizationID string `json:"organization_id"` + CreatedAt string `json:"created_at"` + UserID string `json:"user_id"` + UserName string `json:"user_name"` + UserLogin string `json:"user_login"` + CategoryID string `json:"category_id"` + CategoryName string `json:"category_name"` } diff --git a/internal/models/eventsub.go b/internal/models/eventsub.go index 7c88d736..ad30fd60 100644 --- a/internal/models/eventsub.go +++ b/internal/models/eventsub.go @@ -24,6 +24,7 @@ type EventsubCondition struct { FromBroadcasterUserID string `json:"from_broadcaster_user_id,omitempty"` ClientID string `json:"client_id,omitempty"` ExtensionClientID string `json:"extension_client_id,omitempty"` + OrganizationID string `json:"organization_id,omitempty"` } type EventsubResponse struct { diff --git a/internal/models/poll.go b/internal/models/poll.go new file mode 100644 index 00000000..bef9b143 --- /dev/null +++ b/internal/models/poll.go @@ -0,0 +1,36 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package models + +type PollEventSubResponse struct { + Subscription EventsubSubscription `json:"subscription"` + Event PollEventSubEvent `json:"event"` +} + +type PollEventSubEvent struct { + ID string `json:"id"` + BroadcasterUserID string `json:"broadcaster_user_id"` + BroadcasterUserLogin string `json:"broadcaster_user_login"` + BroadcasterUserName string `json:"broadcaster_user_name"` + Title string `json:"title"` + Choices []PollEventSubEventChoice `json:"choices"` + BitsVoting PollEventSubEventGoodVoting `json:"bits_voting"` + ChannelPointsVoting PollEventSubEventGoodVoting `json:"channel_points_voting"` + Status string `json:"status,omitempty"` + StartedAt string `json:"started_at"` + EndsAt string `json:"ends_at,omitempty"` + EndedAt string `json:"ended_at,omitempty"` +} + +type PollEventSubEventChoice struct { + ID string `json:"id"` + Title string `json:"title"` + BitsVotes *int `json:"bits_votes,omitempty"` + ChannelPointsVotes *int `json:"channel_points_votes,omitempty"` + Votes *int `json:"votes,omitempty"` +} + +type PollEventSubEventGoodVoting struct { + IsEnabled bool `json:"is_enabled"` + AmountPerVote int `json:"amount_per_vote"` +} diff --git a/internal/models/prediction.go b/internal/models/prediction.go new file mode 100644 index 00000000..ca9c38e1 --- /dev/null +++ b/internal/models/prediction.go @@ -0,0 +1,40 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package models + +type PredictionEventSubResponse struct { + Subscription EventsubSubscription `json:"subscription"` + Event PredictionEventSubEvent `json:"event"` +} + +type PredictionEventSubEvent struct { + ID string `json:"id"` + BroadcasterUserID string `json:"broadcaster_user_id"` + BroadcasterUserLogin string `json:"broadcaster_user_login"` + BroadcasterUserName string `json:"broadcaster_user_name"` + Title string `json:"title"` + WinningOutcomeID string `json:"winning_outcome_id,omitempty"` + Outcomes []PredictionEventSubEventOutcomes `json:"outcomes"` + StartedAt string `json:"started_at"` + LocksAt string `json:"locks_at,omitempty"` + LockedAt string `json:"locked_at,omitempty"` + EndedAt string `json:"ended_at,omitempty"` + Status string `json:"status,omitempty"` +} + +type PredictionEventSubEventOutcomes struct { + ID string `json:"id"` + Title string `json:"title"` + Color string `json:"color"` + Users *int `json:"users,omitempty"` + ChannelPoints *int `json:"channel_points,omitempty"` + TopPredictors *[]PredictionEventSubEventTopPredictors `json:"top_predictors,omitempty"` +} + +type PredictionEventSubEventTopPredictors struct { + UserID string `json:"user_id"` + UserLogin string `json:"user_login"` + UserName string `json:"user_name"` + ChannelPointsWon *int `json:"channel_points_won"` + ChannelPointsUsed int `json:"channel_points_used"` +} diff --git a/internal/models/subs.go b/internal/models/subs.go index b5d48a01..c2303314 100644 --- a/internal/models/subs.go +++ b/internal/models/subs.go @@ -41,3 +41,51 @@ type SubEventSubEvent struct { Tier string `json:"tier"` IsGift bool `json:"is_gift"` } + +type GiftEventSubResponse struct { + Subscription EventsubSubscription `json:"subscription"` + Event GiftEventSubEvent `json:"event"` +} + +type GiftEventSubEvent struct { + UserID string `json:"user_id"` + UserLogin string `json:"user_login"` + UserName string `json:"user_name"` + BroadcasterUserID string `json:"broadcaster_user_id"` + BroadcasterUserLogin string `json:"broadcaster_user_login"` + BroadcasterUserName string `json:"broadcaster_user_name"` + Tier string `json:"tier"` + Total int `json:"total"` + IsAnonymous bool `json:"is_anonymous"` + CumulativeTotal *int `json:"cumulative_total"` +} + +type SubscribeMessageEventSubResponse struct { + Subscription EventsubSubscription `json:"subscription"` + Event SubscribeMessageEventSubEvent `json:"event"` +} + +type SubscribeMessageEventSubEvent struct { + UserID string `json:"user_id"` + UserLogin string `json:"user_login"` + UserName string `json:"user_name"` + BroadcasterUserID string `json:"broadcaster_user_id"` + BroadcasterUserLogin string `json:"broadcaster_user_login"` + BroadcasterUserName string `json:"broadcaster_user_name"` + Tier string `json:"tier"` + Message SubscribeMessageEventSubMessage `json:"message"` + CumulativeMonths int `json:"cumulative_months"` + StreakMonths *int `json:"streak_months"` + DurationMonths int `json:"duration_months"` +} + +type SubscribeMessageEventSubMessage struct { + Text string `json:"text"` + Emotes []SubscribeMessageEventSubMessageEmote `json:"emotes"` +} + +type SubscribeMessageEventSubMessageEmote struct { + Begin int `json:"begin"` + End int `json:"end"` + ID string `json:"id"` +} From 90cffb59cf75cbf65bf4bf4873b9cffb86ea16f6 Mon Sep 17 00:00:00 2001 From: lleadbet Date: Mon, 26 Jul 2021 13:38:41 -0400 Subject: [PATCH 26/36] updating ban events to match production --- docs/mock-api.md | 18 ++++------- internal/database/moderation.go | 32 ++++++++++++------- .../mock_api/endpoints/moderation/banned.go | 11 +++++++ .../endpoints/moderation/banned_events.go | 12 +++++++ 4 files changed, 50 insertions(+), 23 deletions(-) diff --git a/docs/mock-api.md b/docs/mock-api.md index 33edae58..d63be159 100644 --- a/docs/mock-api.md +++ b/docs/mock-api.md @@ -40,18 +40,12 @@ None. ## start -The `start` function starts a new mock server for use with testing functionality. Currently, this replicates a large majority of the current API endpoints on the new API, but are ommitting: - -* GET /analytics/extensions -* GET /analytics/games -* GET /extensions/transactions -* POST /eventsub/subscriptions -* DELETE /eventsub/subscriptions -* GET /eventsub/subscriptions -* GET /users/extensions/list -* GET /users/extensions -* PUT /users/extensions -* GET /webhooks/subscriptions +The `start` function starts a new mock server for use with testing functionality. Currently, this replicates a large majority of the current API endpoints on the new API, but are omitting: + +* Extensions endpoints +* Code entitlement endpoints +* Websub endpoints +* EventSub endpoints For many of these, we are exploring how to better integrate this with existing features (for example, allowing events to be triggered on unit creation or otherwise), and for others, the value is minimal compared to the docs. All other endpoints should be currently supported, however it is possible to be out of date- if so, [please raise an issue](https://github.com/twitchdev/twitch-cli/issues). diff --git a/internal/database/moderation.go b/internal/database/moderation.go index e6374ec0..a4dd3887 100644 --- a/internal/database/moderation.go +++ b/internal/database/moderation.go @@ -40,13 +40,17 @@ type ModeratorActionEvent struct { } type BanActionEvent struct { - BroadcasterID string `db:"broadcaster_id" json:"broadcaster_id"` - BroadcasterLogin string `db:"broadcaster_login" json:"broadcaster_login"` - BroadcasterName string `db:"broadcaster_name" json:"broadcaster_name"` - UserID string `db:"user_id" json:"user_id"` - UserLogin string `db:"user_login" json:"user_login"` - UserName string `db:"user_name" json:"user_name"` - ExpiresAt *string `db:"expires_at" json:"expires_at"` + BroadcasterID string `db:"broadcaster_id" json:"broadcaster_id"` + BroadcasterLogin string `db:"broadcaster_login" json:"broadcaster_login"` + BroadcasterName string `db:"broadcaster_name" json:"broadcaster_name"` + UserID string `db:"user_id" json:"user_id"` + UserLogin string `db:"user_login" json:"user_login"` + UserName string `db:"user_name" json:"user_name"` + ExpiresAt *string `db:"expires_at" json:"expires_at"` + Reason string `json:"reason"` + ModeratorID string `json:"moderator_id"` + ModeratorUserLogin string `json:"moderator_login"` + ModeratorUserName string `json:"moderator_user_name"` } type BanEvent struct { @@ -57,10 +61,14 @@ type BanEvent struct { BanActionEvent `json:"event_data"` } type Ban struct { - UserID string `db:"user_id" json:"user_id"` - UserLogin string `db:"user_login" json:"user_login"` - UserName string `db:"user_name" json:"user_name"` - ExpiresAt *string `db:"expires_at" json:"expires_at"` + UserID string `db:"user_id" json:"user_id"` + UserLogin string `db:"user_login" json:"user_login"` + UserName string `db:"user_name" json:"user_name"` + ExpiresAt *string `db:"expires_at" json:"expires_at"` + Reason string `json:"reason"` + ModeratorID string `json:"moderator_id"` + ModeratorUserLogin string `json:"moderator_login"` + ModeratorUserName string `json:"moderator_user_name"` } var es = "" @@ -192,6 +200,7 @@ func (q *Query) GetBans(p UserRequestParams) (*DBResponse, error) { if b.ExpiresAt == nil { b.ExpiresAt = &es } + b.Reason = "CLI ban" r = append(r, b) } dbr := DBResponse{ @@ -228,6 +237,7 @@ func (q *Query) GetBanEvents(p UserRequestParams) (*DBResponse, error) { if b.ExpiresAt == nil { b.ExpiresAt = &es } + b.Reason = "CLI ban" r = append(r, b) } dbr := DBResponse{ diff --git a/internal/mock_api/endpoints/moderation/banned.go b/internal/mock_api/endpoints/moderation/banned.go index 2afcf3e8..36cc76c5 100644 --- a/internal/mock_api/endpoints/moderation/banned.go +++ b/internal/mock_api/endpoints/moderation/banned.go @@ -61,6 +61,12 @@ func getBans(w http.ResponseWriter, r *http.Request) { mock_errors.WriteUnauthorized(w, "broadcaster_id does not match token") return } + broadcaster, err := db.NewQuery(r, 100).GetUser(database.User{ID: userCtx.UserID}) + if err != nil { + mock_errors.WriteServerError(w, "error fetching broadcaster") + return + } + bans := []database.Ban{} userIDs := r.URL.Query()["user_id"] if len(userIDs) > 0 { @@ -81,6 +87,11 @@ func getBans(w http.ResponseWriter, r *http.Request) { } bans = append(bans, dbr.Data.([]database.Ban)...) } + for i := range bans { + bans[i].ModeratorID = broadcaster.ID + bans[i].ModeratorUserLogin = broadcaster.UserLogin + bans[i].ModeratorUserName = broadcaster.DisplayName + } apiResponse := models.APIResponse{Data: bans} if dbr.Cursor != "" { apiResponse.Pagination = &models.APIPagination{Cursor: dbr.Cursor} diff --git a/internal/mock_api/endpoints/moderation/banned_events.go b/internal/mock_api/endpoints/moderation/banned_events.go index 3f16d658..16f14e77 100644 --- a/internal/mock_api/endpoints/moderation/banned_events.go +++ b/internal/mock_api/endpoints/moderation/banned_events.go @@ -60,6 +60,13 @@ func getBanEvents(w http.ResponseWriter, r *http.Request) { mock_errors.WriteUnauthorized(w, "broadcaster_id does not match token") return } + + broadcaster, err := db.NewQuery(r, 100).GetUser(database.User{ID: userCtx.UserID}) + if err != nil { + mock_errors.WriteServerError(w, "error fetching broadcaster") + return + } + bans := []database.BanEvent{} userIDs := r.URL.Query()["user_id"] if len(userIDs) > 0 { @@ -80,6 +87,11 @@ func getBanEvents(w http.ResponseWriter, r *http.Request) { } bans = append(bans, dbr.Data.([]database.BanEvent)...) } + for i := range bans { + bans[i].ModeratorID = broadcaster.ID + bans[i].ModeratorUserLogin = broadcaster.UserLogin + bans[i].ModeratorUserName = broadcaster.DisplayName + } apiResponse := models.APIResponse{Data: bans} if dbr.Cursor != "" { apiResponse.Pagination = &models.APIPagination{Cursor: dbr.Cursor} From cd61cb0352e5fd945e6636d0f7da579afb1145e5 Mon Sep 17 00:00:00 2001 From: lleadbet Date: Mon, 26 Jul 2021 13:45:03 -0400 Subject: [PATCH 27/36] updating docs --- docs/mock-api.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/mock-api.md b/docs/mock-api.md index d63be159..3b444d97 100644 --- a/docs/mock-api.md +++ b/docs/mock-api.md @@ -20,8 +20,9 @@ The `mock-api` product has two primary functions. The first is to generate so-ca * Teams * Users -The second is the actual server used to mock the endpoints. In the next iteration, you will be able to edit these and add further ones manually (for example, making a user with specific attributes), but for the beta we won't be providing this functionality as the current `generate` feature will make all of these (and more), +The second is the actual server used to mock the endpoints. In the next iteration, you will be able to edit these and add further ones manually (for example, making a user with specific attributes), but for the beta we won't be providing this functionality as the current `generate` feature will make all of these (and more). +As of the 1.1 release, this product is in an **open beta** and any bugs should be filed via GitHub Issues. Given the breadth of the tool, it is likely you may run across oddities; please fill out an issue if that is the case. ## generate From fa22b7f360e83822599c290da22629954fa2a723 Mon Sep 17 00:00:00 2001 From: lleadbet Date: Mon, 26 Jul 2021 17:33:44 -0400 Subject: [PATCH 28/36] updating per pr feedback --- cmd/events.go | 3 ++ docs/event.md | 32 ++++++++++--------- internal/events/event.go | 1 + internal/events/trigger/trigger_event.go | 5 +++ internal/events/types/drop/drop.go | 2 +- internal/events/types/drop/drop_test.go | 2 +- .../events/types/gift/channel_gift_test.go | 2 +- internal/events/types/raid/raid_test.go | 2 +- .../stream_change/stream_change_event.go | 12 ++++--- .../stream_change/stream_change_event_test.go | 4 +-- .../subscription_message_test.go | 2 +- 11 files changed, 40 insertions(+), 27 deletions(-) diff --git a/cmd/events.go b/cmd/events.go index 5d5a2e2c..5c127742 100644 --- a/cmd/events.go +++ b/cmd/events.go @@ -28,6 +28,7 @@ var ( cost int64 count int description string + gameID string ) var eventCmd = &cobra.Command{ @@ -90,6 +91,7 @@ func init() { triggerCmd.Flags().StringVarP(&itemName, "item-name", "n", "", "Manually set the name of the event payload item (for example the reward ID in redemption events). For stream events, this is the game title.") triggerCmd.Flags().Int64VarP(&cost, "cost", "C", 0, "Amount of bits or channel points redeemed/used in the event.") triggerCmd.Flags().StringVarP(&description, "description", "d", "", "Title the stream should be updated with.") + triggerCmd.Flags().StringVarP(&gameID, "game-id", "G", "", "Sets the game/category ID for applicable events.") // retrigger flags retriggerCmd.Flags().StringVarP(&forwardAddress, "forward-address", "F", "", "Forward address for mock event.") @@ -134,6 +136,7 @@ func triggerCmdRun(cmd *cobra.Command, args []string) { Cost: cost, Description: description, ItemName: itemName, + GameID: gameID, }) if err != nil { diff --git a/docs/event.md b/docs/event.md index 7123382c..64098141 100644 --- a/docs/event.md +++ b/docs/event.md @@ -57,21 +57,23 @@ Used to either create or send mock events for use with local webhooks testing. **Flags** -| Flag | Shorthand | Description | Example | Required? (Y/N) | -|---------------------|-----------|---------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------|-----------------| -| `--forward-address` | `-F` | Web server address for where to send mock events. | `-F https://localhost:8080` | N | -| `--transport` | `-T` | The method used to send events. Default is `eventsub`, but can send using `websub`. | `-T websub` | N | -| `--to-user` | `-t` | Denotes the receiver's TUID of the event, usually the broadcaster. | `-t 44635596` | N | -| `--from-user` | `-f` | Denotes the sender's TUID of the event, for example the user that follows another user or the subscriber to a broadcaster. | `-f 44635596` | N | -| `--gift-user` | `-g` | Used only for subcription-based events, denotes the gifting user ID | `-g 44635596` | N | -| `--secret` | `-s` | Webhook secret. If defined, signs all forwarded events with the SHA256 HMAC. | `-s testsecret` | N | -| `--count` | `-c` | Count of events to fire. This can be used to simulate an influx of events. | `-c 100` | N | -| `--anonymous` | `-a` | If the event is anonymous. Only applies to `gift` and `cheer` events. | `-a` | N | -| `--status` | `-S` | Status of the event object, currently applies to channel points redemptions. | `-S fulfilled` | N | -| `--item-id` | `-i` | Manually set the ID of the event payload item (for example the reward ID in redemption events or game in stream events). | `-i 032e4a6c-4aef-11eb-a9f5-1f703d1f0b92` | N | -| `--item-name` | `-n` | Manually set the name of the event payload item (for example the reward ID in redemption events or game name in stream events). | `-n "Science & Technology"` | N | -| `--cost` | `-C` | Amount of bits or channel points redeemed/used in the event. | `-C 250` | N | -| `--description` | `-d` | Title the stream should be updated/started with. Additionally used as the category ID for Drops events. | `-d Awesome new title!` | N | + Flag | Shorthand | Description | Example | Required? (Y/N) +---------------------|-----------|---------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------|----------------- + `--forward-address` | `-F` | Web server address for where to send mock events. | `-F https://localhost:8080` | N + `--transport` | `-T` | The method used to send events. Default is `eventsub`, but can send using `websub`. | `-T websub` | N + `--to-user` | `-t` | Denotes the receiver's TUID of the event, usually the broadcaster. | `-t 44635596` | N + `--from-user` | `-f` | Denotes the sender's TUID of the event, for example the user that follows another user or the subscriber to a broadcaster. | `-f 44635596` | N + `--gift-user` | `-g` | Used only for subcription-based events, denotes the gifting user ID | `-g 44635596` | N + `--secret` | `-s` | Webhook secret. If defined, signs all forwarded events with the SHA256 HMAC. | `-s testsecret` | N + `--count` | `-c` | Count of events to fire. This can be used to simulate an influx of events. | `-c 100` | N + `--anonymous` | `-a` | If the event is anonymous. Only applies to `gift` and `cheer` events. | `-a` | N + `--status` | `-S` | Status of the event object, currently applies to channel points redemptions. | `-S fulfilled` | N + `--item-id` | `-i` | Manually set the ID of the event payload item (for example the reward ID in redemption events or game in stream events). | `-i 032e4a6c-4aef-11eb-a9f5-1f703d1f0b92` | N + `--item-name` | `-n` | Manually set the name of the event payload item (for example the reward ID in redemption events or game name in stream events). | `-n "Science & Technology"` | N + `--cost` | `-C` | Amount of bits or channel points redeemed/used in the event. | `-C 250` | N + `--description` | `-d` | Title the stream should be updated/started with. | `-d Awesome new title!` | N + `--game-id` | `-G` | Game ID for Drop or other relevant events. | `-G 1234` | N + **Examples** diff --git a/internal/events/event.go b/internal/events/event.go index 33a64edb..901a0b40 100644 --- a/internal/events/event.go +++ b/internal/events/event.go @@ -20,6 +20,7 @@ type MockEventParameters struct { Cost int64 IsPermanent bool Description string + GameID string } type MockEventResponse struct { diff --git a/internal/events/trigger/trigger_event.go b/internal/events/trigger/trigger_event.go index 93ce195c..3735fab5 100644 --- a/internal/events/trigger/trigger_event.go +++ b/internal/events/trigger/trigger_event.go @@ -30,6 +30,7 @@ type TriggerParameters struct { Count int Description string ItemName string + GameID string } type TriggerResponse struct { @@ -53,6 +54,9 @@ func Fire(p TriggerParameters) (string, error) { p.FromUser = util.RandomUserID() } + if p.GameID == "" { + p.GameID = fmt.Sprint(util.RandomInt(10 * 1000)) + } eventParamaters := events.MockEventParameters{ ID: util.RandomGUID(), Trigger: p.Event, @@ -67,6 +71,7 @@ func Fire(p TriggerParameters) (string, error) { ItemID: p.ItemID, Description: p.Description, ItemName: p.ItemName, + GameID: p.GameID, } e, err := types.GetByTriggerAndTransport(p.Event, p.Transport) diff --git a/internal/events/types/drop/drop.go b/internal/events/types/drop/drop.go index 4b5aa184..b6dca9c6 100644 --- a/internal/events/types/drop/drop.go +++ b/internal/events/types/drop/drop.go @@ -61,7 +61,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven ID: util.RandomGUID(), Data: models.DropsEntitlementEventSubEventData{ OrganizationID: params.FromUserID, - CategoryID: params.Description, + CategoryID: params.GameID, CategoryName: "", CampaignID: util.RandomGUID(), EntitlementID: util.RandomGUID(), diff --git a/internal/events/types/drop/drop_test.go b/internal/events/types/drop/drop_test.go index f0bb768d..fb0431f2 100644 --- a/internal/events/types/drop/drop_test.go +++ b/internal/events/types/drop/drop_test.go @@ -27,7 +27,7 @@ func TestEventSub(t *testing.T) { r, err := Event{}.GenerateEvent(params) a.Nil(err) - var body models.DropsEntitlementEventSubResponse // replace with actual value + var body models.DropsEntitlementEventSubResponse err = json.Unmarshal(r.JSON, &body) a.Nil(err) diff --git a/internal/events/types/gift/channel_gift_test.go b/internal/events/types/gift/channel_gift_test.go index 98397bc2..3aa40b28 100644 --- a/internal/events/types/gift/channel_gift_test.go +++ b/internal/events/types/gift/channel_gift_test.go @@ -29,7 +29,7 @@ func TestEventSub(t *testing.T) { r, err := Event{}.GenerateEvent(params) a.Nil(err) - var body models.GiftEventSubResponse // replace with actual value + var body models.GiftEventSubResponse err = json.Unmarshal(r.JSON, &body) a.Nil(err) diff --git a/internal/events/types/raid/raid_test.go b/internal/events/types/raid/raid_test.go index ca67f0c7..dab555b5 100644 --- a/internal/events/types/raid/raid_test.go +++ b/internal/events/types/raid/raid_test.go @@ -27,7 +27,7 @@ func TestEventSub(t *testing.T) { r, err := Event{}.GenerateEvent(params) a.Nil(err) - var body models.SubEventSubResponse // replace with actual value + var body models.SubEventSubResponse err = json.Unmarshal(r.JSON, &body) a.Nil(err) diff --git a/internal/events/types/stream_change/stream_change_event.go b/internal/events/types/stream_change/stream_change_event.go index ef94d1fc..ffe645a8 100644 --- a/internal/events/types/stream_change/stream_change_event.go +++ b/internal/events/types/stream_change/stream_change_event.go @@ -37,8 +37,10 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven if params.Description == "" { params.Description = "Example title from the CLI!" } - if params.ItemID == "" { - params.ItemID = "509658" + if params.ItemID == "" && params.GameID == "" { + params.GameID = "509658" + } else if params.ItemID != "" && params.GameID == "" { + params.GameID = params.ItemID } if params.ItemName == "" { params.ItemName = "Just Chatting" @@ -69,7 +71,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven BroadcasterUserName: params.ToUserName, StreamTitle: params.Description, StreamLanguage: "en", - StreamCategoryID: params.ItemID, + StreamCategoryID: params.GameID, StreamCategoryName: params.ItemName, IsMature: false, }, @@ -86,8 +88,8 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven BroadcasterUserID: params.ToUserID, BroadcasterUserLogin: params.ToUserName, BroadcasterUserName: params.ToUserName, - StreamCategoryID: params.ItemID, - StreamCategoryName: "Just Chatting", + StreamCategoryID: params.GameID, + StreamCategoryName: params.ItemName, StreamType: "live", StreamTitle: params.Description, StreamViewerCount: 9848, diff --git a/internal/events/types/stream_change/stream_change_event_test.go b/internal/events/types/stream_change/stream_change_event_test.go index bda3cabf..11e1c0be 100644 --- a/internal/events/types/stream_change/stream_change_event_test.go +++ b/internal/events/types/stream_change/stream_change_event_test.go @@ -40,7 +40,7 @@ func TestEventSub(t *testing.T) { ToUserID: toUser, Transport: models.TransportEventSub, Trigger: "stream_change", - ItemID: "1234", + GameID: "1234", } r, err = Event{}.GenerateEvent(params) @@ -65,7 +65,7 @@ func TestWebSubStreamChange(t *testing.T) { Transport: models.TransportWebSub, Trigger: "stream-change", Description: newStreamTitle, - ItemID: "1234", + GameID: "1234", } r, err := Event{}.GenerateEvent(params) diff --git a/internal/events/types/subscription_message/subscription_message_test.go b/internal/events/types/subscription_message/subscription_message_test.go index 7f30b2c4..4a14fc1a 100644 --- a/internal/events/types/subscription_message/subscription_message_test.go +++ b/internal/events/types/subscription_message/subscription_message_test.go @@ -29,7 +29,7 @@ func TestEventSub(t *testing.T) { r, err := Event{}.GenerateEvent(params) a.Nil(err) - var body models.SubscribeMessageEventSubResponse // replace with actual value + var body models.SubscribeMessageEventSubResponse err = json.Unmarshal(r.JSON, &body) a.Nil(err) a.Equal(&ten, body.Event.StreakMonths) From 3d4fb361f0cd16dae6118ec384222ae47864b074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emil=20Gardstr=C3=B6m?= Date: Mon, 2 Aug 2021 23:50:43 +0200 Subject: [PATCH 29/36] fix differences with prod --- internal/database/categories.go | 2 +- internal/database/user.go | 3 +++ internal/mock_api/endpoints/channels/information.go | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/database/categories.go b/internal/database/categories.go index f2c0de4f..02b2cf29 100644 --- a/internal/database/categories.go +++ b/internal/database/categories.go @@ -9,7 +9,7 @@ import ( type Category struct { ID string `db:"id" json:"id"` Name string `db:"category_name" json:"name"` - BoxartURL string `json:"boxart_url"` + BoxartURL string `json:"box_art_url"` ViewerCount int `db:"vc" json:"-"` } diff --git a/internal/database/user.go b/internal/database/user.go index b372137d..1118d48e 100644 --- a/internal/database/user.go +++ b/internal/database/user.go @@ -72,6 +72,8 @@ type SearchChannel struct { TagIDs []string `json:"tag_ids" dbi:"false"` IsLive bool `json:"is_live" db:"is_live"` StartedAt *string `db:"started_at" json:"started_at"` + // calculated fields + ThumbNailURL string `json:"thumbnail_url"` } func (q *Query) GetUser(u User) (User, error) { @@ -327,6 +329,7 @@ func (q *Query) SearchChannels(query string, live_only bool) (*DBResponse, error r[i].StartedAt = &emptyString } r[i].TagIDs = st + r[i].ThumbNailURL = "https://static-cdn.jtvnw.net/jtv_user_pictures/3f13ab61-ec78-4fe6-8481-8682cb3b0ac2-channel_offline_image-300x300.png" } dbr := DBResponse{ diff --git a/internal/mock_api/endpoints/channels/information.go b/internal/mock_api/endpoints/channels/information.go index 1b5346d2..69e0b6c6 100644 --- a/internal/mock_api/endpoints/channels/information.go +++ b/internal/mock_api/endpoints/channels/information.go @@ -22,7 +22,7 @@ type Channel struct { CategoryID string `db:"category_id" json:"game_id"` CategoryName string `db:"category_name" json:"game_name" dbi:"false"` Title string `db:"title" json:"title"` - Language string `db:"stream_language" json:"stream_language"` + Language string `db:"stream_language" json:"broadcaster_language"` Delay int `dbi:"false" json:"delay"` } From 0378645df87c404d15943e3cae860c6b6ff05ce1 Mon Sep 17 00:00:00 2001 From: lleadbet Date: Mon, 9 Aug 2021 17:14:37 -0700 Subject: [PATCH 30/36] Fixing moderation endpoint per #83. --- internal/database/init.go | 2 +- internal/database/moderation.go | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/internal/database/init.go b/internal/database/init.go index 174f040b..2ad5a40e 100644 --- a/internal/database/init.go +++ b/internal/database/init.go @@ -10,7 +10,7 @@ import ( "github.com/jmoiron/sqlx" ) -const currentVersion = 2 +const currentVersion = 3 type migrateMap struct { SQL string diff --git a/internal/database/moderation.go b/internal/database/moderation.go index a4dd3887..d0f60a57 100644 --- a/internal/database/moderation.go +++ b/internal/database/moderation.go @@ -113,7 +113,7 @@ func (q *Query) AddModerator(p UserRequestParams) error { tx := q.DB.MustBegin() tx.NamedExec(stmt, p) - tx.NamedExec(`INSERT INTO moderator_actions VALUES(:id, :event_type, :event_timestamp, :event_version, :broadcaster_id, :user_id)`, ma) + tx.NamedExec(`INSERT INTO moderator_actions VALUES(:id, :event_timestamp, :event_type, :event_version, :broadcaster_id, :user_id)`, ma) return tx.Commit() } @@ -156,7 +156,7 @@ func (q *Query) RemoveModerator(broadcaster string, user string) error { tx := q.DB.MustBegin() tx.Exec(`delete from moderators where broadcaster_id=$1 and user_id=$2`, broadcaster, user) - tx.NamedExec(`INSERT INTO moderator_actions VALUES(:id, :event_type, :event_timestamp, :event_version, :broadcaster_id, :user_id)`, ma) + tx.NamedExec(`INSERT INTO moderator_actions VALUES(:id, :event_timestamp, :event_type, :event_version, :broadcaster_id, :user_id)`, ma) return tx.Commit() } @@ -178,7 +178,7 @@ func (q *Query) InsertBan(p UserRequestParams) error { tx := q.DB.MustBegin() tx.NamedExec(stmt, p) - tx.NamedExec(`INSERT INTO ban_events VALUES(:id, :event_type, :event_timestamp, :event_version, :broadcaster_id, :user_id, :expires_at)`, ma) + tx.NamedExec(`INSERT INTO ban_events VALUES(:id, :event_timestamp, :event_type, :event_version, :broadcaster_id, :user_id, :expires_at)`, ma) return tx.Commit() } @@ -270,6 +270,14 @@ func (q *Query) GetModeratorEvents(p UserRequestParams) (*DBResponse, error) { log.Print(err) return nil, err } + // shim for https://github.com/twitchdev/twitch-cli/issues/83 + _, err = time.Parse(time.RFC3339, ma.EventTimestamp) + if err != nil { + ts := ma.EventType + ma.EventType = ma.EventTimestamp + ma.EventTimestamp = ts + } + r = append(r, ma) } dbr := DBResponse{ From 4fe0b4d642f236612a6b1d9ed43bee6678d2e73b Mon Sep 17 00:00:00 2001 From: lleadbet Date: Mon, 9 Aug 2021 17:21:52 -0700 Subject: [PATCH 31/36] Fixes content-type per #81. --- internal/mock_api/authentication/authentication.go | 1 - internal/mock_api/endpoints/schedule/ical.go | 4 ++-- internal/mock_api/mock_server/server.go | 2 ++ 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/mock_api/authentication/authentication.go b/internal/mock_api/authentication/authentication.go index 0c183203..6fb7ae3c 100644 --- a/internal/mock_api/authentication/authentication.go +++ b/internal/mock_api/authentication/authentication.go @@ -24,7 +24,6 @@ type UserAuthentication struct { func AuthenticationMiddleware(next mock_api.MockEndpoint) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") db := r.Context().Value("db").(database.CLIDatabase) // skip auth check for unsupported methods diff --git a/internal/mock_api/endpoints/schedule/ical.go b/internal/mock_api/endpoints/schedule/ical.go index ff087441..59d0713e 100644 --- a/internal/mock_api/endpoints/schedule/ical.go +++ b/internal/mock_api/endpoints/schedule/ical.go @@ -73,7 +73,7 @@ SUMMARY:TwitchDev Monthly Update // July 1, 2021 DESCRIPTION:Science & Technology. CATEGORIES:Science & Technology END:VEVENT -END:VCALENDAR%` - w.Header().Add("Content-Type", "text/calendar") +END:VCALENDAR` + w.Header().Set("Content-Type", "text/calendar") w.Write([]byte(body)) } diff --git a/internal/mock_api/mock_server/server.go b/internal/mock_api/mock_server/server.go index c69ff781..7bf2c748 100644 --- a/internal/mock_api/mock_server/server.go +++ b/internal/mock_api/mock_server/server.go @@ -100,6 +100,8 @@ func RegisterHandlers(m *http.ServeMux) { func loggerMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Printf("%v %v", r.Method, r.URL.Path) + w.Header().Set("Content-Type", "application/json") + next.ServeHTTP(w, r) }) } From a9672104902568363c3c32244c405f5b9f843cfc Mon Sep 17 00:00:00 2001 From: lleadbet Date: Thu, 12 Aug 2021 13:53:29 -0400 Subject: [PATCH 32/36] adding missed shim --- internal/database/moderation.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/internal/database/moderation.go b/internal/database/moderation.go index d0f60a57..99ce5254 100644 --- a/internal/database/moderation.go +++ b/internal/database/moderation.go @@ -234,9 +234,18 @@ func (q *Query) GetBanEvents(p UserRequestParams) (*DBResponse, error) { return nil, err } es := "" + if b.ExpiresAt == nil { b.ExpiresAt = &es } + // shim for https://github.com/twitchdev/twitch-cli/issues/83 + _, err = time.Parse(time.RFC3339, b.EventTimestamp) + if err != nil { + ts := b.EventType + b.EventType = b.EventTimestamp + b.EventTimestamp = ts + } + b.Reason = "CLI ban" r = append(r, b) } From 07e8c0b755a8dde309039ba67ef6d5dca68915ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emil=20Gardstr=C3=B6m?= Date: Thu, 12 Aug 2021 20:35:18 +0200 Subject: [PATCH 33/36] add missing fields in helix endpoints --- internal/database/moderation.go | 8 ++++---- internal/database/streams.go | 7 +++++-- internal/database/subscriptions.go | 12 ++++++++++++ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/internal/database/moderation.go b/internal/database/moderation.go index d0f60a57..3f410094 100644 --- a/internal/database/moderation.go +++ b/internal/database/moderation.go @@ -26,7 +26,7 @@ type ModeratorAction struct { ID string `db:"id" json:"id"` EventType string `db:"event_type" json:"event_type"` EventTimestamp string `db:"event_timestamp" json:"event_timestamp"` - EventVersion string `db:"event_version" json:"event_version"` + EventVersion string `db:"event_version" json:"version"` ModeratorActionEvent `json:"event_data"` } @@ -50,14 +50,14 @@ type BanActionEvent struct { Reason string `json:"reason"` ModeratorID string `json:"moderator_id"` ModeratorUserLogin string `json:"moderator_login"` - ModeratorUserName string `json:"moderator_user_name"` + ModeratorUserName string `json:"moderator_name"` } type BanEvent struct { ID string `db:"id" json:"id"` EventType string `db:"event_type" json:"event_type"` EventTimestamp string `db:"event_timestamp" json:"event_timestamp"` - EventVersion string `db:"event_version" json:"event_version"` + EventVersion string `db:"event_version" json:"version"` BanActionEvent `json:"event_data"` } type Ban struct { @@ -68,7 +68,7 @@ type Ban struct { Reason string `json:"reason"` ModeratorID string `json:"moderator_id"` ModeratorUserLogin string `json:"moderator_login"` - ModeratorUserName string `json:"moderator_user_name"` + ModeratorUserName string `json:"moderator_name"` } var es = "" diff --git a/internal/database/streams.go b/internal/database/streams.go index b4ecbde3..b92312b7 100644 --- a/internal/database/streams.go +++ b/internal/database/streams.go @@ -13,7 +13,7 @@ type Stream struct { UserID string `db:"broadcaster_id" json:"user_id"` UserLogin string `db:"broadcaster_login" json:"user_login" dbi:"false"` UserName string `db:"broadcaster_name" json:"user_name" dbi:"false"` - StreamType string `db:"stream_type" json:"stream_type"` + StreamType string `db:"stream_type" json:"type"` ViewerCount int `db:"viewer_count" json:"viewer_count"` StartedAt string `db:"started_at" json:"started_at"` IsMature bool `db:"is_mature" json:"is_mature"` @@ -24,7 +24,9 @@ type Stream struct { CategoryName sql.NullString `db:"category_name" json:"-" dbi:"false"` RealCategoryName string `json:"game_name"` Title string `db:"title" json:"title" dbi:"false"` - Language string `db:"stream_language" json:"stream_language" dbi:"false"` + Language string `db:"stream_language" json:"language" dbi:"false"` + // calculated fields + ThumbnailURL string `json:"thumbnail_url"` } type StreamTag struct { @@ -83,6 +85,7 @@ func (q *Query) GetStream(s Stream) (*DBResponse, error) { if s.CategoryName.Valid { s.RealCategoryName = s.CategoryName.String } + s.ThumbnailURL = fmt.Sprintf("https://static-cdn.jtvnw.net/previews-ttv/live_user_%v-{width}x{height}.jpg", s.UserLogin) r = append(r, s) } diff --git a/internal/database/subscriptions.go b/internal/database/subscriptions.go index 046273a0..f73b2df6 100644 --- a/internal/database/subscriptions.go +++ b/internal/database/subscriptions.go @@ -4,6 +4,7 @@ package database import ( "database/sql" + "fmt" "log" ) @@ -20,6 +21,8 @@ type Subscription struct { GifterLogin *sql.NullString `db:"gifter_login" json:"gifter_login,omitempty"` Tier string `db:"tier" json:"tier"` CreatedAt string `db:"created_at" json:"-"` + // calculated fields + PlanName string `json:"plan_name"` } type SubscriptionInsert struct { @@ -50,6 +53,15 @@ func (q *Query) GetSubscriptions(s Subscription) (*DBResponse, error) { log.Print(err) return nil, err } + plan := fmt.Sprintf("Channel Subscription (%v)", s.BroadcasterLogin) + switch s.Tier { + case "2000": + plan = plan + ": $9.99 Sub" + case "3000": + plan = plan + ": $24.99 Sub" + default: + } + s.PlanName = plan r = append(r, s) } From 72c7d87e2978bafa6eba2b90110c972834324649 Mon Sep 17 00:00:00 2001 From: lleadbet Date: Fri, 13 Aug 2021 10:56:46 -0400 Subject: [PATCH 34/36] Adding missing ID field. --- internal/events/types/hype_train/hype_train_event.go | 1 + internal/models/hype_train.go | 1 + 2 files changed, 2 insertions(+) diff --git a/internal/events/types/hype_train/hype_train_event.go b/internal/events/types/hype_train/hype_train_event.go index ecc670f3..3c6e51c3 100644 --- a/internal/events/types/hype_train/hype_train_event.go +++ b/internal/events/types/hype_train/hype_train_event.go @@ -63,6 +63,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven CreatedAt: util.GetTimestamp().Format(time.RFC3339Nano), }, Event: models.HypeTrainEventSubEvent{ + ID: params.ID, BroadcasterUserID: params.ToUserID, BroadcasterUserLogin: params.ToUserName, BroadcasterUserName: params.ToUserName, diff --git a/internal/models/hype_train.go b/internal/models/hype_train.go index 3ba1c24b..a13540c6 100644 --- a/internal/models/hype_train.go +++ b/internal/models/hype_train.go @@ -42,6 +42,7 @@ type HypeTrainEventSubResponse struct { } type HypeTrainEventSubEvent struct { + ID string `json:"id"` BroadcasterUserID string `json:"broadcaster_user_id"` BroadcasterUserLogin string `json:"broadcaster_user_login"` BroadcasterUserName string `json:"broadcaster_user_name"` From 153582df1e6f36adbeac7c80ef70c91b14646911 Mon Sep 17 00:00:00 2001 From: Aiden Wallis Date: Fri, 13 Aug 2021 23:37:32 +0100 Subject: [PATCH 35/36] adds template field to api response --- internal/models/api.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/models/api.go b/internal/models/api.go index 7d464b34..732a79b8 100644 --- a/internal/models/api.go +++ b/internal/models/api.go @@ -8,6 +8,7 @@ type APIResponse struct { Error string `json:"error,omitempty"` Status int `json:"status,omitempty"` Message string `json:"message,omitempty"` + Template string `json:"template,omitempty"` Total *int `json:"total,omitempty"` DateRange *BitsLeaderboardDateRange `json:"date_range,omitempty"` } From fa066b0e25fbbd4b5fa2add4c1d4454aaf919754 Mon Sep 17 00:00:00 2001 From: Aiden Wallis Date: Sat, 14 Aug 2021 04:49:52 +0100 Subject: [PATCH 36/36] Update api.go --- internal/api/api.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/api/api.go b/internal/api/api.go index 498f0e38..6c61fe00 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -101,6 +101,8 @@ func NewRequest(method string, path string, queryParameters []string, body []byt return } + data.Template = apiResponse.Template + if resp.StatusCode > 299 || resp.StatusCode < 200 { data = apiResponse break