Skip to content

Commit cccb0c5

Browse files
committed
Updated /predictions to support multiple options; Added missing tags fields to Get Streams, Get Followed Streams, Search Channels, and Get/Modify Channel Information; Added /chat/shoutouts
1 parent 000dbd4 commit cccb0c5

File tree

8 files changed

+163
-20
lines changed

8 files changed

+163
-20
lines changed

internal/database/streams.go

+4-7
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type Stream struct {
1818
StartedAt string `db:"started_at" json:"started_at"`
1919
IsMature bool `db:"is_mature" json:"is_mature"`
2020
TagIDs []string `json:"tag_ids" dbi:"false"`
21+
Tags []string `json:"tags" dbi:"false"`
2122
// stored in users, but pulled here for json parsing
2223
CategoryID sql.NullString `db:"category_id" json:"-" dbi:"false"`
2324
RealCategoryID string `json:"game_id"`
@@ -89,13 +90,9 @@ func (q *Query) GetStream(s Stream) (*DBResponse, error) {
8990
r = append(r, s)
9091
}
9192

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

10198
dbr := DBResponse{

internal/database/user.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ type SearchChannel struct {
7171
Title string `db:"title" json:"title"`
7272
Language string `db:"stream_language" json:"broadcaster_language"`
7373
TagIDs []string `json:"tag_ids" dbi:"false"`
74+
Tags []string `json:"tags" dbi:"false"`
7475
IsLive bool `json:"is_live" db:"is_live"`
7576
StartedAt *string `db:"started_at" json:"started_at"`
7677
// calculated fields
@@ -335,7 +336,8 @@ func (q *Query) SearchChannels(query string, live_only bool) (*DBResponse, error
335336
if c.StartedAt == nil {
336337
r[i].StartedAt = &emptyString
337338
}
338-
r[i].TagIDs = st
339+
r[i].TagIDs = st // // Needs to be removed from db when this is fully removed from API
340+
r[i].Tags = []string{"English", "CLI Tag"}
339341
r[i].ThumbNailURL = "https://static-cdn.jtvnw.net/jtv_user_pictures/3f13ab61-ec78-4fe6-8481-8682cb3b0ac2-channel_offline_image-300x300.png"
340342
}
341343

internal/mock_api/endpoints/channels/information.go

+10-8
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@ import (
1515
)
1616

1717
type Channel struct {
18-
ID string `db:"id" json:"broadcaster_id"`
19-
UserLogin string `db:"user_login" json:"broadcaster_login"`
20-
DisplayName string `db:"display_name" json:"broadcaster_name"`
21-
CategoryID string `db:"category_id" json:"game_id"`
22-
CategoryName string `db:"category_name" json:"game_name" dbi:"false"`
23-
Title string `db:"title" json:"title"`
24-
Language string `db:"stream_language" json:"broadcaster_language"`
25-
Delay int `dbi:"false" json:"delay"`
18+
ID string `db:"id" json:"broadcaster_id"`
19+
UserLogin string `db:"user_login" json:"broadcaster_login"`
20+
DisplayName string `db:"display_name" json:"broadcaster_name"`
21+
CategoryID string `db:"category_id" json:"game_id"`
22+
CategoryName string `db:"category_name" json:"game_name" dbi:"false"`
23+
Title string `db:"title" json:"title"`
24+
Language string `db:"stream_language" json:"broadcaster_language"`
25+
Delay int `dbi:"false" json:"delay"`
26+
Tags []string `dbi:"false" json:"tags"`
2627
}
2728

2829
var informationMethodsSupported = map[string]bool{
@@ -176,6 +177,7 @@ func convertUsers(users []database.User) []Channel {
176177
CategoryID: u.CategoryID.String,
177178
CategoryName: u.CategoryName.String,
178179
Delay: u.Delay,
180+
Tags: []string{"English", "CLI Tag"},
179181
})
180182
}
181183
return response
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package chat
4+
5+
import (
6+
"net/http"
7+
8+
"github.com/twitchdev/twitch-cli/internal/database"
9+
"github.com/twitchdev/twitch-cli/internal/mock_api/authentication"
10+
"github.com/twitchdev/twitch-cli/internal/mock_api/mock_errors"
11+
)
12+
13+
var shoutoutsMethodsSupported = map[string]bool{
14+
http.MethodGet: false,
15+
http.MethodPost: true,
16+
http.MethodDelete: false,
17+
http.MethodPatch: false,
18+
http.MethodPut: false,
19+
}
20+
21+
var shoutoutsScopesByMethod = map[string][]string{
22+
http.MethodGet: {},
23+
http.MethodPost: {"moderator:manage:shoutout"},
24+
http.MethodDelete: {},
25+
http.MethodPatch: {},
26+
http.MethodPut: {},
27+
}
28+
29+
type PostShoutoutsRequestBody struct {
30+
SlowMode *bool `json:"slow_mode"`
31+
SlowModeWaitTime *int `json:"slow_mode_wait_time"`
32+
FollowerMode *bool `json:"follower_mode"`
33+
FollowerModeDuration *int `json:"follower_mode_duration"`
34+
SubscriberMode *bool `json:"subscriber_mode"`
35+
EmoteMode *bool `json:"emote_mode"`
36+
UniqueChatMode *bool `json:"unique_chat_mode"`
37+
NonModeratorChatDelay *bool `json:"non_moderator_chat_delay"`
38+
NonModeratorChatDelayDuration *int `json:"non_moderator_chat_delay_duration"`
39+
}
40+
type Shoutouts struct{}
41+
42+
func (e Shoutouts) Path() string { return "/chat/shoutouts" }
43+
44+
func (e Shoutouts) GetRequiredScopes(method string) []string {
45+
return shoutoutsScopesByMethod[method]
46+
}
47+
48+
func (e Shoutouts) ValidMethod(method string) bool {
49+
return shoutoutsMethodsSupported[method]
50+
}
51+
52+
func (e Shoutouts) ServeHTTP(w http.ResponseWriter, r *http.Request) {
53+
db = r.Context().Value("db").(database.CLIDatabase)
54+
55+
switch r.Method {
56+
case http.MethodPost:
57+
postShoutouts(w, r)
58+
break
59+
default:
60+
w.WriteHeader(http.StatusMethodNotAllowed)
61+
}
62+
}
63+
64+
func postShoutouts(w http.ResponseWriter, r *http.Request) {
65+
userCtx := r.Context().Value("auth").(authentication.UserAuthentication)
66+
if !userCtx.MatchesModeratorIDParam(r) {
67+
mock_errors.WriteUnauthorized(w, "Moderator ID does not match token.")
68+
return
69+
}
70+
71+
fromBroadcasterId := r.URL.Query().Get("from_broadcaster_id")
72+
if fromBroadcasterId == "" {
73+
mock_errors.WriteBadRequest(w, "Missing required parameter from_broadcaster_id")
74+
return
75+
}
76+
77+
toBroadcasterId := r.URL.Query().Get("to_broadcaster_id")
78+
if toBroadcasterId == "" {
79+
mock_errors.WriteBadRequest(w, "Missing required parameter to_broadcaster_id")
80+
return
81+
}
82+
83+
moderatorID := r.URL.Query().Get("moderator_id")
84+
if moderatorID == "" {
85+
mock_errors.WriteBadRequest(w, "Missing required parameter moderator_id")
86+
return
87+
}
88+
89+
fromBroadcaster, err := db.NewQuery(r, 100).GetUser(database.User{ID: fromBroadcasterId})
90+
if err != nil {
91+
mock_errors.WriteServerError(w, "error fetching fromBrodcasterId")
92+
return
93+
}
94+
if fromBroadcaster.ID == "" {
95+
mock_errors.WriteBadRequest(w, "Invalid from_broadcaser_id: No broadcaster by that ID exists")
96+
return
97+
}
98+
99+
toBroadcaster, err := db.NewQuery(r, 100).GetUser(database.User{ID: toBroadcasterId})
100+
if err != nil {
101+
mock_errors.WriteServerError(w, "error fetching toBrodcasterId")
102+
return
103+
}
104+
if toBroadcaster.ID == "" {
105+
mock_errors.WriteBadRequest(w, "Invalid to_broadcaser_id: No broadcaster by that ID exists")
106+
return
107+
}
108+
109+
// Verify user is a moderator or is the broadcaster
110+
isModerator := false
111+
if fromBroadcasterId == moderatorID {
112+
isModerator = true
113+
} else {
114+
moderatorListDbr, err := db.NewQuery(r, 1000).GetModeratorsForBroadcaster(fromBroadcasterId)
115+
if err != nil {
116+
mock_errors.WriteServerError(w, err.Error())
117+
return
118+
}
119+
for _, mod := range moderatorListDbr.Data.([]database.Moderator) {
120+
if mod.UserID == moderatorID {
121+
isModerator = true
122+
}
123+
}
124+
}
125+
if !isModerator {
126+
mock_errors.WriteUnauthorized(w, "The user specified in parameter moderator_id is not one of the broadcaster's moderators")
127+
return
128+
}
129+
130+
// No connection to chat on here, and no way to GET or PATCH announcements via API
131+
// For the time being, we just ingest it and pretend it worked (HTTP 204)
132+
w.WriteHeader(http.StatusNoContent)
133+
}

internal/mock_api/endpoints/endpoints.go

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ func All() []mock_api.MockEndpoint {
5151
chat.GlobalBadges{},
5252
chat.GlobalEmotes{},
5353
chat.Settings{},
54+
chat.Shoutouts{},
5455
clips.Clips{},
5556
drops.DropsEntitlements{},
5657
goals.Goals{},

internal/mock_api/endpoints/predictions/predictions.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,8 @@ func postPredictions(w http.ResponseWriter, r *http.Request) {
155155
return
156156
}
157157

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

internal/mock_api/endpoints/streams/followed_streams.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ func (e FollowedStreams) ServeHTTP(w http.ResponseWriter, r *http.Request) {
5555
func getFollowedStreams(w http.ResponseWriter, r *http.Request) {
5656
userCtx := r.Context().Value("auth").(authentication.UserAuthentication)
5757

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

internal/mock_api/generate/generate.go

+8
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,14 @@ func generateUsers(ctx context.Context, count int) error {
263263
ChannelPoints: 0,
264264
PredictionID: prediction.ID,
265265
},
266+
{
267+
ID: util.RandomGUID(),
268+
Title: "Choice3",
269+
Color: "BLUE",
270+
Users: 0,
271+
ChannelPoints: 0,
272+
PredictionID: prediction.ID,
273+
},
266274
}
267275

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

0 commit comments

Comments
 (0)