Skip to content

Commit

Permalink
Updated /predictions to support multiple options; Added missing tags …
Browse files Browse the repository at this point in the history
…fields to Get Streams, Get Followed Streams, Search Channels, and Get/Modify Channel Information; Added /chat/shoutouts
  • Loading branch information
Xemdo committed Feb 3, 2023
1 parent 000dbd4 commit cccb0c5
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 20 deletions.
11 changes: 4 additions & 7 deletions internal/database/streams.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Stream struct {
StartedAt string `db:"started_at" json:"started_at"`
IsMature bool `db:"is_mature" json:"is_mature"`
TagIDs []string `json:"tag_ids" dbi:"false"`
Tags []string `json:"tags" dbi:"false"`
// stored in users, but pulled here for json parsing
CategoryID sql.NullString `db:"category_id" json:"-" dbi:"false"`
RealCategoryID string `json:"game_id"`
Expand Down Expand Up @@ -89,13 +90,9 @@ func (q *Query) GetStream(s Stream) (*DBResponse, error) {
r = append(r, s)
}

for i, s := range r {
st := []string{}
err = q.DB.Select(&st, "select tag_id from stream_tags where user_id=$1", s.UserID)
if err != nil {
return nil, err
}
r[i].TagIDs = st
for i := range r {
r[i].TagIDs = []string{} // Needs to be removed from db when this is fully removed from API
r[i].Tags = []string{"English", "CLI Tag"}
}

dbr := DBResponse{
Expand Down
4 changes: 3 additions & 1 deletion internal/database/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ type SearchChannel struct {
Title string `db:"title" json:"title"`
Language string `db:"stream_language" json:"broadcaster_language"`
TagIDs []string `json:"tag_ids" dbi:"false"`
Tags []string `json:"tags" dbi:"false"`
IsLive bool `json:"is_live" db:"is_live"`
StartedAt *string `db:"started_at" json:"started_at"`
// calculated fields
Expand Down Expand Up @@ -335,7 +336,8 @@ func (q *Query) SearchChannels(query string, live_only bool) (*DBResponse, error
if c.StartedAt == nil {
r[i].StartedAt = &emptyString
}
r[i].TagIDs = st
r[i].TagIDs = st // // Needs to be removed from db when this is fully removed from API
r[i].Tags = []string{"English", "CLI Tag"}
r[i].ThumbNailURL = "https://static-cdn.jtvnw.net/jtv_user_pictures/3f13ab61-ec78-4fe6-8481-8682cb3b0ac2-channel_offline_image-300x300.png"
}

Expand Down
18 changes: 10 additions & 8 deletions internal/mock_api/endpoints/channels/information.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ import (
)

type Channel struct {
ID string `db:"id" json:"broadcaster_id"`
UserLogin string `db:"user_login" json:"broadcaster_login"`
DisplayName string `db:"display_name" json:"broadcaster_name"`
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:"broadcaster_language"`
Delay int `dbi:"false" json:"delay"`
ID string `db:"id" json:"broadcaster_id"`
UserLogin string `db:"user_login" json:"broadcaster_login"`
DisplayName string `db:"display_name" json:"broadcaster_name"`
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:"broadcaster_language"`
Delay int `dbi:"false" json:"delay"`
Tags []string `dbi:"false" json:"tags"`
}

var informationMethodsSupported = map[string]bool{
Expand Down Expand Up @@ -176,6 +177,7 @@ func convertUsers(users []database.User) []Channel {
CategoryID: u.CategoryID.String,
CategoryName: u.CategoryName.String,
Delay: u.Delay,
Tags: []string{"English", "CLI Tag"},
})
}
return response
Expand Down
133 changes: 133 additions & 0 deletions internal/mock_api/endpoints/chat/shoutouts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package chat

import (
"net/http"

"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"
)

var shoutoutsMethodsSupported = map[string]bool{
http.MethodGet: false,
http.MethodPost: true,
http.MethodDelete: false,
http.MethodPatch: false,
http.MethodPut: false,
}

var shoutoutsScopesByMethod = map[string][]string{
http.MethodGet: {},
http.MethodPost: {"moderator:manage:shoutout"},
http.MethodDelete: {},
http.MethodPatch: {},
http.MethodPut: {},
}

type PostShoutoutsRequestBody struct {
SlowMode *bool `json:"slow_mode"`
SlowModeWaitTime *int `json:"slow_mode_wait_time"`
FollowerMode *bool `json:"follower_mode"`
FollowerModeDuration *int `json:"follower_mode_duration"`
SubscriberMode *bool `json:"subscriber_mode"`
EmoteMode *bool `json:"emote_mode"`
UniqueChatMode *bool `json:"unique_chat_mode"`
NonModeratorChatDelay *bool `json:"non_moderator_chat_delay"`
NonModeratorChatDelayDuration *int `json:"non_moderator_chat_delay_duration"`
}
type Shoutouts struct{}

func (e Shoutouts) Path() string { return "/chat/shoutouts" }

func (e Shoutouts) GetRequiredScopes(method string) []string {
return shoutoutsScopesByMethod[method]
}

func (e Shoutouts) ValidMethod(method string) bool {
return shoutoutsMethodsSupported[method]
}

func (e Shoutouts) ServeHTTP(w http.ResponseWriter, r *http.Request) {
db = r.Context().Value("db").(database.CLIDatabase)

switch r.Method {
case http.MethodPost:
postShoutouts(w, r)
break
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}

func postShoutouts(w http.ResponseWriter, r *http.Request) {
userCtx := r.Context().Value("auth").(authentication.UserAuthentication)
if !userCtx.MatchesModeratorIDParam(r) {
mock_errors.WriteUnauthorized(w, "Moderator 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
}

moderatorID := r.URL.Query().Get("moderator_id")
if moderatorID == "" {
mock_errors.WriteBadRequest(w, "Missing required parameter moderator_id")
return
}

fromBroadcaster, err := db.NewQuery(r, 100).GetUser(database.User{ID: fromBroadcasterId})
if err != nil {
mock_errors.WriteServerError(w, "error fetching fromBrodcasterId")
return
}
if fromBroadcaster.ID == "" {
mock_errors.WriteBadRequest(w, "Invalid from_broadcaser_id: No broadcaster by that ID exists")
return
}

toBroadcaster, err := db.NewQuery(r, 100).GetUser(database.User{ID: toBroadcasterId})
if err != nil {
mock_errors.WriteServerError(w, "error fetching toBrodcasterId")
return
}
if toBroadcaster.ID == "" {
mock_errors.WriteBadRequest(w, "Invalid to_broadcaser_id: No broadcaster by that ID exists")
return
}

// Verify user is a moderator or is the broadcaster
isModerator := false
if fromBroadcasterId == moderatorID {
isModerator = true
} else {
moderatorListDbr, err := db.NewQuery(r, 1000).GetModeratorsForBroadcaster(fromBroadcasterId)
if err != nil {
mock_errors.WriteServerError(w, err.Error())
return
}
for _, mod := range moderatorListDbr.Data.([]database.Moderator) {
if mod.UserID == moderatorID {
isModerator = true
}
}
}
if !isModerator {
mock_errors.WriteUnauthorized(w, "The user specified in parameter moderator_id is not one of the broadcaster's moderators")
return
}

// No connection to chat on here, and no way to GET or PATCH announcements via API
// For the time being, we just ingest it and pretend it worked (HTTP 204)
w.WriteHeader(http.StatusNoContent)
}
1 change: 1 addition & 0 deletions internal/mock_api/endpoints/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func All() []mock_api.MockEndpoint {
chat.GlobalBadges{},
chat.GlobalEmotes{},
chat.Settings{},
chat.Shoutouts{},
clips.Clips{},
drops.DropsEntitlements{},
goals.Goals{},
Expand Down
4 changes: 2 additions & 2 deletions internal/mock_api/endpoints/predictions/predictions.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@ func postPredictions(w http.ResponseWriter, r *http.Request) {
return
}

if len(body.Outcomes) != 2 {
mock_errors.WriteBadRequest(w, "outcomes must be exactly 2 items")
if len(body.Outcomes) < 2 || len(body.Outcomes) > 10 {
mock_errors.WriteBadRequest(w, "Number of outcomes in the prediction must be equal to or above 2, and equal to or below 10")
return
}

Expand Down
4 changes: 2 additions & 2 deletions internal/mock_api/endpoints/streams/followed_streams.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ func (e FollowedStreams) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func getFollowedStreams(w http.ResponseWriter, r *http.Request) {
userCtx := r.Context().Value("auth").(authentication.UserAuthentication)

if userCtx.UserID != r.URL.Query().Get("user_id") {
mock_errors.WriteUnauthorized(w, "user_id must match the token user")
if !userCtx.MatchesUserIDParam(r) {
mock_errors.WriteUnauthorized(w, "user_id param does not match token")
return
}

Expand Down
8 changes: 8 additions & 0 deletions internal/mock_api/generate/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,14 @@ func generateUsers(ctx context.Context, count int) error {
ChannelPoints: 0,
PredictionID: prediction.ID,
},
{
ID: util.RandomGUID(),
Title: "Choice3",
Color: "BLUE",
Users: 0,
ChannelPoints: 0,
PredictionID: prediction.ID,
},
}

err = db.NewQuery(nil, 100).InsertPrediction(prediction)
Expand Down

0 comments on commit cccb0c5

Please sign in to comment.