diff --git a/internal/database/_schema.sql b/internal/database/_schema.sql index b79aeabc..1f8d1798 100644 --- a/internal/database/_schema.sql +++ b/internal/database/_schema.sql @@ -294,9 +294,9 @@ 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, + 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, diff --git a/internal/database/channel_points_redemptions.go b/internal/database/channel_points_redemptions.go index 0a59c6b0..2fc99ccc 100644 --- a/internal/database/channel_points_redemptions.go +++ b/internal/database/channel_points_redemptions.go @@ -15,7 +15,7 @@ type ChannelPointsRedemption struct { UserLogin string `db:"user_login" dbi:"false" json:"user_login"` UserName string `db:"user_name" dbi:"false" json:"user_name"` UserInput sql.NullString `db:"user_input" json:"-"` - RealUserInput *string `json:"user_input"` + RealUserInput string `json:"user_input"` RedemptionStatus string `db:"redemption_status" json:"status"` RedeemedAt string `db:"redeemed_at" json:"redeemed_at"` RewardID string `db:"reward_id" json:"-"` @@ -25,7 +25,7 @@ type ChannelPointsRedemption struct { type ChannelPointsRedemptionRewardInfo struct { ID string `dbi:"false" db:"red_id" json:"id" dbs:"red.id"` Title string `dbi:"false" db:"title" json:"title"` - RewardPrompt string `dbi:"false" db:"reward_prompt" json:"reward_prompt"` + RewardPrompt string `dbi:"false" db:"reward_prompt" json:"prompt"` Cost int `dbi:"false" db:"cost" json:"cost"` } @@ -50,9 +50,9 @@ func (q *Query) GetChannelPointsRedemption(cpr ChannelPointsRedemption, sort str if err != nil { return nil, err } - red.RealUserInput = &red.UserInput.String + red.RealUserInput = red.UserInput.String if !red.UserInput.Valid { - red.RealUserInput = nil + red.RealUserInput = "" } r = append(r, red) } diff --git a/internal/database/init.go b/internal/database/init.go index cdc46fcc..99ae6f35 100644 --- a/internal/database/init.go +++ b/internal/database/init.go @@ -10,7 +10,7 @@ import ( "github.com/jmoiron/sqlx" ) -const currentVersion = 6 +const currentVersion = 7 type migrateMap struct { SQL string @@ -53,6 +53,10 @@ ALTER TABLE users ADD COLUMN content_labels text not null default '';`, SQL: `DROP TABLE IF EXISTS stream_tags;`, Message: `Removing deprecated stream_tags from database.`, }, + 7: { + SQL: `ALTER TABLE stream_schedule DROP COLUMN timezone;`, + Message: `Removing deprecated stream_schedule.timezone from database`, + }, } func checkAndUpdate(db sqlx.DB) error { @@ -120,7 +124,7 @@ create table predictions ( id text not null primary key, broadcaster_id text not 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, vod_offset int default 0, 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)); +create table stream_schedule( id text not null primary key, broadcaster_id text not null, starttime text not null, endtime 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)); create table chat_settings( broadcaster_id text not null primary key, slow_mode boolean not null default 0, slow_mode_wait_time int not null default 10, follower_mode boolean not null default 0, follower_mode_duration int not null default 60, subscriber_mode boolean not null default 0, emote_mode boolean not null default 0, unique_chat_mode boolean not null default 0, non_moderator_chat_delay boolean not null default 0, non_moderator_chat_delay_duration int not null default 10, shieldmode_is_active boolean not null default 0, shieldmode_moderator_id text not null default '', shieldmode_moderator_login text not null default '', shieldmode_moderator_name text not null default '', shieldmode_last_activated text not null default '' ); create table vips ( broadcaster_id text not null, user_id text not null, created_at text not null default '', primary key (broadcaster_id, user_id), foreign key (broadcaster_id) references users(id), foreign key (user_id) references users(id) );` diff --git a/internal/database/predictions.go b/internal/database/predictions.go index 75551276..58248263 100644 --- a/internal/database/predictions.go +++ b/internal/database/predictions.go @@ -11,7 +11,7 @@ type Prediction struct { WinningOutcomeID *string `db:"winning_outcome_id" json:"winning_outcome_id"` PredictionWindow int `db:"prediction_window" json:"prediction_window"` Status string `db:"status" json:"status"` - StartedAt string `db:"created_at" json:"started_at"` + StartedAt string `db:"created_at" json:"created_at"` EndedAt *string `db:"ended_at" json:"ended_at"` LockedAt *string `db:"locked_at" json:"locked_at"` Outcomes []PredictionOutcome `json:"outcomes"` diff --git a/internal/database/schedule.go b/internal/database/schedule.go index 5bf9c4e9..c3516358 100644 --- a/internal/database/schedule.go +++ b/internal/database/schedule.go @@ -24,7 +24,6 @@ type ScheduleSegment struct { IsVacation bool `db:"is_vacation" json:"-"` Category *SegmentCategory `json:"category"` UserID string `db:"broadcaster_id" json:"-"` - Timezone string `db:"timezone" json:"timezone,omitempty"` CategoryID *string `db:"category_id" json:"-"` CategoryName *string `db:"category_name" dbi:"false" json:"-"` IsCanceled *bool `db:"is_canceled" json:"-"` diff --git a/internal/database/subscriptions.go b/internal/database/subscriptions.go index f73b2df6..ae462ba3 100644 --- a/internal/database/subscriptions.go +++ b/internal/database/subscriptions.go @@ -16,9 +16,9 @@ type Subscription struct { UserLogin string `db:"user_login" json:"user_login"` UserName string `db:"user_name" json:"user_name"` IsGift bool `db:"is_gift" json:"is_gift"` - GifterID *sql.NullString `db:"gifter_id" json:"gifter_id,omitempty"` - GifterName *sql.NullString `db:"gifter_name" json:"gifter_name,omitempty"` - GifterLogin *sql.NullString `db:"gifter_login" json:"gifter_login,omitempty"` + GifterID *sql.NullString `db:"gifter_id" json:"gifter_id"` + GifterName *sql.NullString `db:"gifter_name" json:"gifter_name"` + GifterLogin *sql.NullString `db:"gifter_login" json:"gifter_login"` Tier string `db:"tier" json:"tier"` CreatedAt string `db:"created_at" json:"-"` // calculated fields diff --git a/internal/database/videos.go b/internal/database/videos.go index 556f2021..180b4499 100644 --- a/internal/database/videos.go +++ b/internal/database/videos.go @@ -20,7 +20,7 @@ type Video struct { Viewable string `db:"viewable" json:"viewable"` ViewCount int `db:"view_count" json:"view_count"` Duration string `db:"duration" json:"duration"` - VideoLanguage string `db:"video_language" json:"video_language"` + VideoLanguage string `db:"video_language" json:"language"` MutedSegments []VideoMutedSegment `json:"muted_segments"` CategoryID *string `db:"category_id" dbs:"v.category_id" json:"-"` Type string `db:"type" json:"type"` @@ -32,7 +32,7 @@ type Video struct { type VideoMutedSegment struct { VideoID string `db:"video_id" json:"-"` - VideoOffset int `db:"video_offset" json:"video_offset"` + VideoOffset int `db:"video_offset" json:"offset"` Duration int `db:"duration" json:"duration"` } @@ -54,7 +54,7 @@ type Clip struct { // calculated fields URL string `json:"url"` ThumbnailURL string `json:"thumbnail_url"` - EmbedURL string `json:"embed_urL"` + EmbedURL string `json:"embed_url"` StartedAt string `db:"started_at" dbi:"false" json:"-"` EndedAt string `db:"ended_at" dbi:"false" json:"-"` } @@ -129,6 +129,7 @@ func (q *Query) InsertVideo(v Video) error { func (q *Query) DeleteVideo(id string) error { tx := q.DB.MustBegin() + tx.MustExec("delete from stream_markers where video_id=$1", id) tx.MustExec("delete from video_muted_segments where video_id=$1", id) tx.MustExec("delete from videos where id = $1", id) return tx.Commit() diff --git a/internal/mock_api/endpoints/bits/leaderboard.go b/internal/mock_api/endpoints/bits/leaderboard.go index 67753c2e..95386122 100644 --- a/internal/mock_api/endpoints/bits/leaderboard.go +++ b/internal/mock_api/endpoints/bits/leaderboard.go @@ -124,7 +124,9 @@ func getBitsLeaderboard(w http.ResponseWriter, r *http.Request) { // check if the started_at date is valid and then add it to the start/end range if period != "all" { if startedAt == "" { - startedAt = time.Now().Format(time.RFC3339) + w.Write(mock_errors.GetErrorBytes(http.StatusBadRequest, errors.New("Bad Request"), "invalid value provided for started_at")) + w.WriteHeader(http.StatusBadRequest) + return } sa, err := time.Parse(time.RFC3339, startedAt) @@ -199,12 +201,9 @@ func getBitsLeaderboard(w http.ResponseWriter, r *http.Request) { length := len(bl) apiR := models.APIResponse{ - Data: bl, - Total: &length, - } - - if dateRange.StartedAt != "" { - apiR.DateRange = &dateRange + Data: bl, + DateRange: &dateRange, + Total: &length, } body, _ := json.Marshal(apiR) diff --git a/internal/mock_api/endpoints/channel_points/channel_points_test.go b/internal/mock_api/endpoints/channel_points/channel_points_test.go index 878fe05b..cdffae19 100644 --- a/internal/mock_api/endpoints/channel_points/channel_points_test.go +++ b/internal/mock_api/endpoints/channel_points/channel_points_test.go @@ -86,6 +86,7 @@ func TestRedemption(t *testing.T) { a.Equal(400, resp.StatusCode) q.Set("broadcaster_id", "2") + q.Set("status", "FULFILLED") req.URL.RawQuery = q.Encode() resp, err = http.DefaultClient.Do(req) a.Nil(err) diff --git a/internal/mock_api/endpoints/channel_points/redemptions.go b/internal/mock_api/endpoints/channel_points/redemptions.go index 39b7909a..0f099e7d 100644 --- a/internal/mock_api/endpoints/channel_points/redemptions.go +++ b/internal/mock_api/endpoints/channel_points/redemptions.go @@ -68,6 +68,11 @@ func getRedemptions(w http.ResponseWriter, r *http.Request) { status := r.URL.Query().Get("status") sort := r.URL.Query().Get("sort") + if id == "" && status == "" { + mock_errors.WriteBadRequest(w, "The status query parameter is required if you don't specify the id query parameter.") + return + } + if !userCtx.MatchesBroadcasterIDParam(r) { mock_errors.WriteUnauthorized(w, "Broadcaster ID mismatch") return diff --git a/internal/mock_api/endpoints/chat/color.go b/internal/mock_api/endpoints/chat/color.go index fb1d10ca..bfba5a21 100644 --- a/internal/mock_api/endpoints/chat/color.go +++ b/internal/mock_api/endpoints/chat/color.go @@ -1,188 +1,190 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -package chat - -import ( - "encoding/json" - "log" - "net/http" - "regexp" - "strings" - - "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/models" -) - -var colorMethodsSupported = map[string]bool{ - http.MethodGet: true, - http.MethodPost: false, - http.MethodDelete: false, - http.MethodPatch: false, - http.MethodPut: true, -} - -var colorScopesByMethod = map[string][]string{ - http.MethodGet: {}, - http.MethodPost: {}, - http.MethodDelete: {}, - http.MethodPatch: {}, - http.MethodPut: {"user:manage:chat_color"}, -} - -type GetColorRequestBody struct { - UserID string `json:"user_id"` - UserName string `json:"user_name"` - UserLogin string `json:"user_login"` - Color string `json:"color"` -} - -type Color struct{} - -func (e Color) Path() string { return "/chat/color" } - -func (e Color) GetRequiredScopes(method string) []string { - return colorScopesByMethod[method] -} - -func (e Color) ValidMethod(method string) bool { - return colorMethodsSupported[method] -} - -func (e Color) ServeHTTP(w http.ResponseWriter, r *http.Request) { - db = r.Context().Value("db").(database.CLIDatabase) - - switch r.Method { - case http.MethodGet: - getColor(w, r) - break - case http.MethodPut: - putColor(w, r) - default: - w.WriteHeader(http.StatusMethodNotAllowed) - } -} - -var validHexColorRegexp *regexp.Regexp = regexp.MustCompile("^#[a-fA-F0-9]{6}$") -var validNamedColorsLower = map[string]string{ - "blue": "#0000FF", - "blue_violet": "#8A2BE2", - "cadet_blue": "#5F9EA0", - "chocolate": "#D2691E", - "coral": "#FF7F50", - "dodger_blue": "#1E90FF", - "firebrick": "#B22222", - "golden_rod": "#DAA520", - "green": "#008000", - "hot_pink": "#FF69B4", - "orange_red": "#FF4500", - "red": "#FF0000", - "sea_green": "#2E8B57", - "spring_green": "#00FF7F", - "yellow_green": "#9ACD32", -} - -func getColor(w http.ResponseWriter, r *http.Request) { - q := r.URL.Query() - userIDs := q["user_id"] - results := []GetColorRequestBody{} - - if len(userIDs) == 0 { - mock_errors.WriteBadRequest(w, "Missing required parameter user_id") - return - } - - if len(userIDs) > 100 { - mock_errors.WriteBadRequest(w, "You may only specify up to 100 user_id query parameters") - return - } - - for _, i := range userIDs { - user := database.User{ - ID: i, - } - u, err := db.NewQuery(r, 100).GetUser(user) - if err != nil { - log.Print(err.Error()) - w.WriteHeader(500) - return - } - if u.ID == "" { - continue - } - - duplicate := false - for _, d := range results { - if d.UserID == u.ID { - duplicate = true - } - } - if duplicate { - continue - } - - results = append(results, GetColorRequestBody{ - UserID: u.ID, - UserName: u.DisplayName, - UserLogin: u.UserLogin, - Color: u.ChatColor, - }) - } - - bytes, _ := json.Marshal(models.APIResponse{ - Data: results, - }) - w.Write(bytes) -} - -func putColor(w http.ResponseWriter, r *http.Request) { - userCtx := r.Context().Value("auth").(authentication.UserAuthentication) - if !userCtx.MatchesUserIDParam(r) { - mock_errors.WriteUnauthorized(w, "User ID does not match token.") - return - } - - userID := r.URL.Query().Get("user_id") - if userID == "" { - mock_errors.WriteBadRequest(w, "Missing required parameter user_id") - return - } - - color := r.URL.Query().Get("color") - if color == "" { - mock_errors.WriteBadRequest(w, "Missing required parameter color") - return - } - - // Users need to input %23 instead of # into their query. We store as # internally, so this needs to be converted - color = strings.ReplaceAll(color, "%23", "#") - - // Check if named color is valid. If so, change the above color variable to the hex so it can pass the regex below. - // This allows us to store color directly into the database without an additional variable. - if namedColorHex, ok := validNamedColorsLower[strings.ToLower(color)]; ok { - color = namedColorHex - } - - validHex := validHexColorRegexp.MatchString(color) - - if !validHex { - mock_errors.WriteBadRequest(w, "The color specified in the color query paramter is not valid") - return - } - - // Store in database, and return no body, just HTTP 200 - u, err := db.NewQuery(r, 100).GetUser(database.User{ID: userID}) - if err != nil { - mock_errors.WriteServerError(w, "Error fetching user: "+err.Error()) - return - } - - u.ChatColor = color - log.Printf("%v", u) - - err = db.NewQuery(r, 100).InsertUser(u, true) - if err != nil { - mock_errors.WriteServerError(w, "Error writing to database: "+err.Error()) - } -} +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package chat + +import ( + "encoding/json" + "log" + "net/http" + "regexp" + "strings" + + "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/models" +) + +var colorMethodsSupported = map[string]bool{ + http.MethodGet: true, + http.MethodPost: false, + http.MethodDelete: false, + http.MethodPatch: false, + http.MethodPut: true, +} + +var colorScopesByMethod = map[string][]string{ + http.MethodGet: {}, + http.MethodPost: {}, + http.MethodDelete: {}, + http.MethodPatch: {}, + http.MethodPut: {"user:manage:chat_color"}, +} + +type GetColorRequestBody struct { + UserID string `json:"user_id"` + UserName string `json:"user_name"` + UserLogin string `json:"user_login"` + Color string `json:"color"` +} + +type Color struct{} + +func (e Color) Path() string { return "/chat/color" } + +func (e Color) GetRequiredScopes(method string) []string { + return colorScopesByMethod[method] +} + +func (e Color) ValidMethod(method string) bool { + return colorMethodsSupported[method] +} + +func (e Color) ServeHTTP(w http.ResponseWriter, r *http.Request) { + db = r.Context().Value("db").(database.CLIDatabase) + + switch r.Method { + case http.MethodGet: + getColor(w, r) + break + case http.MethodPut: + putColor(w, r) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +var validHexColorRegexp *regexp.Regexp = regexp.MustCompile("^#[a-fA-F0-9]{6}$") +var validNamedColorsLower = map[string]string{ + "blue": "#0000FF", + "blue_violet": "#8A2BE2", + "cadet_blue": "#5F9EA0", + "chocolate": "#D2691E", + "coral": "#FF7F50", + "dodger_blue": "#1E90FF", + "firebrick": "#B22222", + "golden_rod": "#DAA520", + "green": "#008000", + "hot_pink": "#FF69B4", + "orange_red": "#FF4500", + "red": "#FF0000", + "sea_green": "#2E8B57", + "spring_green": "#00FF7F", + "yellow_green": "#9ACD32", +} + +func getColor(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + userIDs := q["user_id"] + results := []GetColorRequestBody{} + + if len(userIDs) == 0 { + mock_errors.WriteBadRequest(w, "Missing required parameter user_id") + return + } + + if len(userIDs) > 100 { + mock_errors.WriteBadRequest(w, "You may only specify up to 100 user_id query parameters") + return + } + + for _, i := range userIDs { + user := database.User{ + ID: i, + } + u, err := db.NewQuery(r, 100).GetUser(user) + if err != nil { + log.Print(err.Error()) + w.WriteHeader(500) + return + } + if u.ID == "" { + continue + } + + duplicate := false + for _, d := range results { + if d.UserID == u.ID { + duplicate = true + } + } + if duplicate { + continue + } + + results = append(results, GetColorRequestBody{ + UserID: u.ID, + UserName: u.DisplayName, + UserLogin: u.UserLogin, + Color: u.ChatColor, + }) + } + + bytes, _ := json.Marshal(models.APIResponse{ + Data: results, + }) + w.Write(bytes) +} + +func putColor(w http.ResponseWriter, r *http.Request) { + userCtx := r.Context().Value("auth").(authentication.UserAuthentication) + if !userCtx.MatchesUserIDParam(r) { + mock_errors.WriteUnauthorized(w, "User ID does not match token.") + return + } + + userID := r.URL.Query().Get("user_id") + if userID == "" { + mock_errors.WriteBadRequest(w, "Missing required parameter user_id") + return + } + + color := r.URL.Query().Get("color") + if color == "" { + mock_errors.WriteBadRequest(w, "Missing required parameter color") + return + } + + // Users need to input %23 instead of # into their query. We store as # internally, so this needs to be converted + color = strings.ReplaceAll(color, "%23", "#") + + // Check if named color is valid. If so, change the above color variable to the hex so it can pass the regex below. + // This allows us to store color directly into the database without an additional variable. + if namedColorHex, ok := validNamedColorsLower[strings.ToLower(color)]; ok { + color = namedColorHex + } + + validHex := validHexColorRegexp.MatchString(color) + + if !validHex { + mock_errors.WriteBadRequest(w, "The color specified in the color query paramter is not valid") + return + } + + // Store in database, and return no body, just HTTP 204 + u, err := db.NewQuery(r, 100).GetUser(database.User{ID: userID}) + if err != nil { + mock_errors.WriteServerError(w, "Error fetching user: "+err.Error()) + return + } + + u.ChatColor = color + log.Printf("%v", u) + + err = db.NewQuery(r, 100).InsertUser(u, true) + if err != nil { + mock_errors.WriteServerError(w, "Error writing to database: "+err.Error()) + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/internal/mock_api/endpoints/raids/raids.go b/internal/mock_api/endpoints/raids/raids.go index 8d9ef184..30452fe2 100644 --- a/internal/mock_api/endpoints/raids/raids.go +++ b/internal/mock_api/endpoints/raids/raids.go @@ -1,133 +1,135 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -package raids - -import ( - "encoding/json" - "math/rand" - "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/models" - "github.com/twitchdev/twitch-cli/internal/util" -) - -var raidsMethodsSupported = map[string]bool{ - http.MethodGet: false, - http.MethodPost: true, - http.MethodDelete: true, - http.MethodPatch: false, - http.MethodPut: false, -} - -var raidsScopesByMethod = map[string][]string{ - http.MethodGet: {}, - http.MethodPost: {"channel:manage:raids"}, - http.MethodDelete: {"channel:manage:raids"}, - http.MethodPatch: {}, - http.MethodPut: {}, -} - -type GetVIPsResponseBody struct { - CreatedAt string `json:"created_at"` - IsMature bool `json:"is_mature"` -} - -type Raids struct{} - -func (e Raids) Path() string { return "/raids" } - -func (e Raids) GetRequiredScopes(method string) []string { - return raidsScopesByMethod[method] -} - -func (e Raids) ValidMethod(method string) bool { - return raidsMethodsSupported[method] -} - -func (e Raids) ServeHTTP(w http.ResponseWriter, r *http.Request) { - db = r.Context().Value("db").(database.CLIDatabase) - - switch r.Method { - case http.MethodPost: - postRaids(w, r) - break - case http.MethodDelete: - deleteRaids(w, r) - break - default: - w.WriteHeader(http.StatusMethodNotAllowed) - } -} - -func postRaids(w http.ResponseWriter, r *http.Request) { - userCtx := r.Context().Value("auth").(authentication.UserAuthentication) - if !userCtx.MatchesSpecifiedIDParam(r, "from_broadcaster_id") { - mock_errors.WriteUnauthorized(w, "from_broadcaster_id does not match token") - return - } - - fromBroadcasterID := r.URL.Query().Get("from_broadcaster_id") - if fromBroadcasterID == "" { - mock_errors.WriteBadRequest(w, "Missing required parameter from_broadcaster_id") - return - } - - toBroadcasterID := r.URL.Query().Get("to_broadcaster_id") - if toBroadcasterID == "" { - mock_errors.WriteBadRequest(w, "Missing required parameter to_broadcaster_id") - return - } - - if fromBroadcasterID == toBroadcasterID { - mock_errors.WriteBadRequest(w, "The IDs on from_broadcaster_id and to_broadcaster_id cannot be the same ID") - return - } - - // Check if user exists - user, err := db.NewQuery(r, 100).GetUser(database.User{ID: toBroadcasterID}) - if err != nil { - mock_errors.WriteServerError(w, "error pulling to_broadcaster_id from user database: "+err.Error()) - return - } - if user.ID == "" { - mock_errors.WriteBadRequest(w, "User specified in to_broadcaster_id doesn't exist") - return - } - - rand.Seed(util.GetTimestamp().UnixNano()) - isMature := rand.Float32() < 0.5 - - bytes, _ := json.Marshal(models.APIResponse{ - Data: GetVIPsResponseBody{ - CreatedAt: util.GetTimestamp().Format(time.RFC3339Nano), - IsMature: isMature, - }, - }) - w.Write(bytes) - - // There's no real channel handling in the mock API, so we'll just ingest this and say it happened. - // Right now this means no 409 Conflict handling -} - -func deleteRaids(w http.ResponseWriter, r *http.Request) { - userCtx := r.Context().Value("auth").(authentication.UserAuthentication) - if !userCtx.MatchesBroadcasterIDParam(r) { - mock_errors.WriteUnauthorized(w, "broadcaster_id does not match token") - return - } - - broadcasterID := r.URL.Query().Get("broadcaster_id") - if broadcasterID == "" { - mock_errors.WriteBadRequest(w, "Missing required parameter broadcaster_id") - return - } - - // There's no real channel handling in the mock API, so we'll just ingest this and say it happened. - // Right now this means no 404 Not Found handling - - w.WriteHeader(http.StatusNoContent) -} +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package raids + +import ( + "encoding/json" + "math/rand" + "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/models" + "github.com/twitchdev/twitch-cli/internal/util" +) + +var raidsMethodsSupported = map[string]bool{ + http.MethodGet: false, + http.MethodPost: true, + http.MethodDelete: true, + http.MethodPatch: false, + http.MethodPut: false, +} + +var raidsScopesByMethod = map[string][]string{ + http.MethodGet: {}, + http.MethodPost: {"channel:manage:raids"}, + http.MethodDelete: {"channel:manage:raids"}, + http.MethodPatch: {}, + http.MethodPut: {}, +} + +type GetVIPsResponseBody struct { + CreatedAt string `json:"created_at"` + IsMature bool `json:"is_mature"` +} + +type Raids struct{} + +func (e Raids) Path() string { return "/raids" } + +func (e Raids) GetRequiredScopes(method string) []string { + return raidsScopesByMethod[method] +} + +func (e Raids) ValidMethod(method string) bool { + return raidsMethodsSupported[method] +} + +func (e Raids) ServeHTTP(w http.ResponseWriter, r *http.Request) { + db = r.Context().Value("db").(database.CLIDatabase) + + switch r.Method { + case http.MethodPost: + postRaids(w, r) + break + case http.MethodDelete: + deleteRaids(w, r) + break + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +func postRaids(w http.ResponseWriter, r *http.Request) { + userCtx := r.Context().Value("auth").(authentication.UserAuthentication) + if !userCtx.MatchesSpecifiedIDParam(r, "from_broadcaster_id") { + mock_errors.WriteUnauthorized(w, "from_broadcaster_id does not match token") + return + } + + fromBroadcasterID := r.URL.Query().Get("from_broadcaster_id") + if fromBroadcasterID == "" { + mock_errors.WriteBadRequest(w, "Missing required parameter from_broadcaster_id") + return + } + + toBroadcasterID := r.URL.Query().Get("to_broadcaster_id") + if toBroadcasterID == "" { + mock_errors.WriteBadRequest(w, "Missing required parameter to_broadcaster_id") + return + } + + if fromBroadcasterID == toBroadcasterID { + mock_errors.WriteBadRequest(w, "The IDs on from_broadcaster_id and to_broadcaster_id cannot be the same ID") + return + } + + // Check if user exists + user, err := db.NewQuery(r, 100).GetUser(database.User{ID: toBroadcasterID}) + if err != nil { + mock_errors.WriteServerError(w, "error pulling to_broadcaster_id from user database: "+err.Error()) + return + } + if user.ID == "" { + mock_errors.WriteBadRequest(w, "User specified in to_broadcaster_id doesn't exist") + return + } + + rand.Seed(util.GetTimestamp().UnixNano()) + isMature := rand.Float32() < 0.5 + + bytes, _ := json.Marshal(models.APIResponse{ + Data: []GetVIPsResponseBody{ + { + CreatedAt: util.GetTimestamp().Format(time.RFC3339Nano), + IsMature: isMature, + }, + }, + }) + w.Write(bytes) + + // There's no real channel handling in the mock API, so we'll just ingest this and say it happened. + // Right now this means no 409 Conflict handling +} + +func deleteRaids(w http.ResponseWriter, r *http.Request) { + userCtx := r.Context().Value("auth").(authentication.UserAuthentication) + if !userCtx.MatchesBroadcasterIDParam(r) { + mock_errors.WriteUnauthorized(w, "broadcaster_id does not match token") + return + } + + broadcasterID := r.URL.Query().Get("broadcaster_id") + if broadcasterID == "" { + mock_errors.WriteBadRequest(w, "Missing required parameter broadcaster_id") + return + } + + // There's no real channel handling in the mock API, so we'll just ingest this and say it happened. + // Right now this means no 404 Not Found handling + + w.WriteHeader(http.StatusNoContent) +} diff --git a/internal/mock_api/endpoints/schedule/scehdule_test.go b/internal/mock_api/endpoints/schedule/schedule_test.go similarity index 96% rename from internal/mock_api/endpoints/schedule/scehdule_test.go rename to internal/mock_api/endpoints/schedule/schedule_test.go index fc3928f3..f726e60f 100644 --- a/internal/mock_api/endpoints/schedule/scehdule_test.go +++ b/internal/mock_api/endpoints/schedule/schedule_test.go @@ -185,16 +185,6 @@ func TestSegment(t *testing.T) { 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) @@ -217,6 +207,7 @@ func TestSegment(t *testing.T) { // good request body.Title = "patched_title" + body.Timezone = "America/Los_Angeles" b, _ = json.Marshal(body) req, _ = http.NewRequest(http.MethodPatch, ts.URL+ScheduleSegment{}.Path(), bytes.NewBuffer(b)) q.Set("broadcaster_id", "1") diff --git a/internal/mock_api/endpoints/schedule/segment.go b/internal/mock_api/endpoints/schedule/segment.go index 701adad8..a86a15e8 100644 --- a/internal/mock_api/endpoints/schedule/segment.go +++ b/internal/mock_api/endpoints/schedule/segment.go @@ -6,6 +6,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "github.com/twitchdev/twitch-cli/internal/models" "net/http" "strconv" "time" @@ -95,6 +96,7 @@ func (e ScheduleSegment) postSegment(w http.ResponseWriter, r *http.Request) { mock_errors.WriteBadRequest(w, "Invalid/malformed start_time provided") return } + if body.Timezone == "" { mock_errors.WriteBadRequest(w, "Missing timezone") return @@ -138,7 +140,6 @@ func (e ScheduleSegment) postSegment(w http.ResponseWriter, r *http.Request) { CategoryID: body.CategoryID, Title: body.Title, UserID: userCtx.UserID, - Timezone: "America/Los_Angeles", IsCanceled: &f, } err = db.NewQuery(nil, 100).InsertSchedule(segment) @@ -163,7 +164,6 @@ func (e ScheduleSegment) postSegment(w http.ResponseWriter, r *http.Request) { CategoryID: body.CategoryID, Title: body.Title, UserID: userCtx.UserID, - Timezone: body.Timezone, IsCanceled: &f, } @@ -181,16 +181,15 @@ func (e ScheduleSegment) postSegment(w http.ResponseWriter, r *http.Request) { } b := dbr.Data.(database.Schedule) - // Remove timezone from JSON given in response - for i := range b.Segments { - b.Segments[i].Timezone = "" - } - if b.Vacation.StartTime == "" && b.Vacation.EndTime == "" { b.Vacation = nil } - bytes, _ := json.Marshal(b) + apiResponse := models.APIResponse{ + Data: b, + } + + bytes, _ := json.Marshal(apiResponse) w.Write(bytes) } @@ -261,26 +260,21 @@ func (e ScheduleSegment) patchSegment(w http.ResponseWriter, r *http.Request) { } } - // timezone - tz, err := time.LoadLocation(segment.Timezone) - if err != nil { - mock_errors.WriteServerError(w, err.Error()) - return + // is_canceled + isCanceled := false + if body.IsCanceled != nil { + isCanceled = *body.IsCanceled } + + // timezone if body.Timezone != "" { - tz, err = time.LoadLocation(body.Timezone) + _, 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 != "" { @@ -312,7 +306,6 @@ func (e ScheduleSegment) patchSegment(w http.ResponseWriter, r *http.Request) { StartTime: st.UTC().Format(time.RFC3339), EndTime: et.UTC().Format(time.RFC3339), IsCanceled: &isCanceled, - Timezone: tz.String(), Title: title, } @@ -329,15 +322,14 @@ func (e ScheduleSegment) patchSegment(w http.ResponseWriter, r *http.Request) { } b = dbr.Data.(database.Schedule) - // Remove timezone from JSON given in response - for i := range b.Segments { - b.Segments[i].Timezone = "" - } - if b.Vacation.StartTime == "" && b.Vacation.EndTime == "" { b.Vacation = nil } - bytes, _ := json.Marshal(b) + apiResponse := models.APIResponse{ + Data: b, + } + + bytes, _ := json.Marshal(apiResponse) w.Write(bytes) } diff --git a/internal/mock_api/endpoints/subscriptions/subscriptions.go b/internal/mock_api/endpoints/subscriptions/subscriptions.go index da68f91a..d7b51c06 100644 --- a/internal/mock_api/endpoints/subscriptions/subscriptions.go +++ b/internal/mock_api/endpoints/subscriptions/subscriptions.go @@ -78,6 +78,8 @@ func getBroadcasterSubscriptions(w http.ResponseWriter, r *http.Request) { body := models.APIResponse{ Data: dbr.Data, Total: &dbr.Total, + // This would usually be something like tier 1 = 1 pt, tier 2 = 2 pts, tier 3 = 6 pts. For simplicity, return total instead + Points: dbr.Total, } if dbr.Cursor != "" { diff --git a/internal/mock_api/generate/generate.go b/internal/mock_api/generate/generate.go index c81aab71..bc6d7808 100644 --- a/internal/mock_api/generate/generate.go +++ b/internal/mock_api/generate/generate.go @@ -297,7 +297,6 @@ func generateUsers(ctx context.Context, count int) error { CategoryID: &dropsGameID, Title: "Test Title", UserID: broadcaster.ID, - Timezone: "America/Los_Angeles", IsCanceled: &f, } diff --git a/internal/models/api.go b/internal/models/api.go index 4502bb07..3987ae3f 100644 --- a/internal/models/api.go +++ b/internal/models/api.go @@ -11,6 +11,7 @@ type APIResponse struct { Template string `json:"template,omitempty"` Total *int `json:"total,omitempty"` DateRange *BitsLeaderboardDateRange `json:"date_range,omitempty"` + Points int `json:"points,omitempty"` } type APIPagination struct {