Skip to content

Commit cddaa62

Browse files
committed
Updated /channels Mock API endpoint to include content classification labels
1 parent d07636e commit cddaa62

File tree

4 files changed

+191
-44
lines changed

4 files changed

+191
-44
lines changed

internal/database/init.go

+9-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"github.com/jmoiron/sqlx"
1111
)
1212

13-
const currentVersion = 4
13+
const currentVersion = 5
1414

1515
type migrateMap struct {
1616
SQL string
@@ -37,12 +37,18 @@ var migrateSQL = map[int]migrateMap{
3737
4: {
3838
SQL: `
3939
ALTER TABLE categories ADD COLUMN igdb_id text not null default 0; UPDATE categories SET igdb_id = abs(random() % 100000); ALTER TABLE clips ADD COLUMN vod_offset int default 0; UPDATE clips SET vod_offset = abs(random() % 3000); ALTER TABLE drops_entitlements ADD COLUMN last_updated text default '2023-01-01T04:17:53.325Z';
40-
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 '' );
40+
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 '' );
4141
INSERT INTO chat_settings (broadcaster_id) SELECT id FROM users;
4242
ALTER TABLE users ADD COLUMN chat_color text not null default '#9146FF';
4343
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) );`,
4444
Message: `Updating database to include API changes since last version. See Twitch CLI changelog for more info.`,
4545
},
46+
5: {
47+
SQL: `
48+
ALTER TABLE users ADD COLUMN branded_content boolean not null default false;
49+
ALTER TABLE users ADD COLUMN content_labels text not null default '';`,
50+
Message: `Updating database to include Content Classification Label field.`,
51+
},
4652
}
4753

4854
func checkAndUpdate(db sqlx.DB) error {
@@ -81,7 +87,7 @@ func initDatabase(db sqlx.DB) error {
8187
createSQL := `
8288
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);
8389
create table categories( id text not null primary key, category_name text not null, igdb_id text not null );
84-
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, chat_color text not null default '#9146FF', foreign key (category_id) references categories(id) );
90+
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, chat_color text not null default '#9146FF', branded_content boolean not null default false, content_labels text not null default '', foreign key (category_id) references categories(id) );
8591
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) );
8692
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) );
8793
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) );

internal/database/user.go

+22-18
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,28 @@ import (
1313
)
1414

1515
type User struct {
16-
ID string `db:"id" json:"id" dbs:"u1.id"`
17-
UserLogin string `db:"user_login" json:"login"`
18-
DisplayName string `db:"display_name" json:"display_name"`
19-
Email string `db:"email" json:"email,omitempty"`
20-
UserType string `db:"user_type" json:"type"`
21-
BroadcasterType string `db:"broadcaster_type" json:"broadcaster_type"`
22-
UserDescription string `db:"user_description" json:"description"`
23-
CreatedAt string `db:"created_at" json:"created_at"`
24-
ModifiedAt string `db:"modified_at" json:"-"`
25-
ProfileImageURL string `dbi:"false" json:"profile_image_url" `
26-
OfflineImageURL string `dbi:"false" json:"offline_image_url" `
27-
ViewCount int `dbi:"false" json:"view_count"`
28-
CategoryID sql.NullString `db:"category_id" json:"game_id" dbi:"force"`
29-
CategoryName sql.NullString `db:"category_name" json:"game_name" dbi:"false"`
30-
Title string `db:"title" json:"title"`
31-
Language string `db:"stream_language" json:"stream_language"`
32-
Delay int `db:"delay" json:"delay" dbi:"force"`
33-
ChatColor string `db:"chat_color" json:"-"`
16+
ID string `db:"id" json:"id" dbs:"u1.id"`
17+
UserLogin string `db:"user_login" json:"login"`
18+
DisplayName string `db:"display_name" json:"display_name"`
19+
Email string `db:"email" json:"email,omitempty"`
20+
UserType string `db:"user_type" json:"type"`
21+
BroadcasterType string `db:"broadcaster_type" json:"broadcaster_type"`
22+
UserDescription string `db:"user_description" json:"description"`
23+
CreatedAt string `db:"created_at" json:"created_at"`
24+
ModifiedAt string `db:"modified_at" json:"-"`
25+
ProfileImageURL string `dbi:"false" json:"profile_image_url" `
26+
OfflineImageURL string `dbi:"false" json:"offline_image_url" `
27+
ViewCount int `dbi:"false" json:"view_count"`
28+
CategoryID sql.NullString `db:"category_id" json:"game_id" dbi:"force"`
29+
CategoryName sql.NullString `db:"category_name" json:"game_name" dbi:"false"`
30+
Title string `db:"title" json:"title"`
31+
Language string `db:"stream_language" json:"stream_language"`
32+
Delay int `db:"delay" json:"delay" dbi:"force"`
33+
ChatColor string `db:"chat_color" json:"-"`
34+
IsBrandedContent bool `db:"branded_content" json:"is_branded_content"`
35+
36+
// UnparsedCCLs is a comma seperated array (e.g. "Gambling,ViolentGraphic,ProfanityVulgarity")
37+
UnparsedCCLs string `db:"content_labels" json:"-"`
3438
}
3539

3640
type Follow struct {

internal/mock_api/endpoints/channels/information.go

+111-23
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ package channels
55
import (
66
"database/sql"
77
"encoding/json"
8+
"fmt"
89
"net/http"
10+
"strings"
911

1012
"github.com/mattn/go-sqlite3"
1113
"github.com/twitchdev/twitch-cli/internal/database"
@@ -15,15 +17,18 @@ import (
1517
)
1618

1719
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"`
26-
Tags []string `dbi:"false" json:"tags"`
20+
ID string `db:"id" json:"broadcaster_id"`
21+
UserLogin string `db:"user_login" json:"broadcaster_login"`
22+
DisplayName string `db:"display_name" json:"broadcaster_name"`
23+
CategoryID string `db:"category_id" json:"game_id"`
24+
CategoryName string `db:"category_name" json:"game_name" dbi:"false"`
25+
Title string `db:"title" json:"title"`
26+
Language string `db:"stream_language" json:"broadcaster_language"`
27+
Delay int `dbi:"false" json:"delay"`
28+
Tags []string `dbi:"false" json:"tags"`
29+
BrandedContent bool `dbi:"false" json:"is_branded_content"`
30+
31+
ContentClassificationLabels []string `dbi:"false" json:"content_classification_labels"`
2732
}
2833

2934
var informationMethodsSupported = map[string]bool{
@@ -49,6 +54,13 @@ type PatchInformationEndpointRequest struct {
4954
BroadcasterLanguage string `json:"broadcaster_language"`
5055
Title string `json:"title"`
5156
Delay *int `json:"delay"`
57+
// TODO: tags
58+
ContentClassificationLabels []PatchInformationEndpointRequestLabel `json:"content_classification_labels"`
59+
}
60+
61+
type PatchInformationEndpointRequestLabel struct {
62+
ID string `json:"id"`
63+
IsEnabled bool `json:"is_enabled"`
5264
}
5365

5466
func (e InformationEndpoint) Path() string { return "/channels" }
@@ -128,13 +140,15 @@ func patchInformation(w http.ResponseWriter, r *http.Request) {
128140
return
129141
}
130142

143+
// Game ID
131144
var gameID = u.CategoryID
132145
if params.GameID == "" || params.GameID == "0" {
133146
gameID = sql.NullString{}
134147
} else if params.GameID != "" {
135148
gameID = sql.NullString{String: params.GameID, Valid: true}
136149
}
137150

151+
// Delay
138152
if params.Delay != nil && u.BroadcasterType != "partner" {
139153
mock_errors.WriteBadRequest(w, "Delay is partner only")
140154
return
@@ -146,12 +160,23 @@ func patchInformation(w http.ResponseWriter, r *http.Request) {
146160
} else {
147161
delay = *params.Delay
148162
}
163+
164+
// TODO: Branded content
165+
166+
cclDbString, err := handleCCLs(u, params)
167+
if err != nil {
168+
mock_errors.WriteForbidden(w, err.Error())
169+
return
170+
}
171+
172+
// Write
149173
err = db.NewQuery(r, 100).UpdateChannel(broadcasterID, database.User{
150-
ID: broadcasterID,
151-
Title: params.Title,
152-
Language: params.BroadcasterLanguage,
153-
CategoryID: gameID,
154-
Delay: delay,
174+
ID: broadcasterID,
175+
Title: params.Title,
176+
Language: params.BroadcasterLanguage,
177+
CategoryID: gameID,
178+
Delay: delay,
179+
UnparsedCCLs: cclDbString,
155180
})
156181
if err != nil {
157182
if database.DatabaseErrorIs(err, sqlite3.ErrConstraintForeignKey) {
@@ -168,17 +193,80 @@ func patchInformation(w http.ResponseWriter, r *http.Request) {
168193
func convertUsers(users []database.User) []Channel {
169194
response := []Channel{}
170195
for _, u := range users {
196+
// Convert CCL array into an actual string array
197+
var ccls = []string{}
198+
if u.UnparsedCCLs != "" {
199+
ccls = strings.Split(u.UnparsedCCLs, ",")
200+
}
201+
171202
response = append(response, Channel{
172-
ID: u.ID,
173-
UserLogin: u.UserLogin,
174-
DisplayName: u.DisplayName,
175-
Title: u.Title,
176-
Language: u.Language,
177-
CategoryID: u.CategoryID.String,
178-
CategoryName: u.CategoryName.String,
179-
Delay: u.Delay,
180-
Tags: []string{"English", "CLI Tag"},
203+
ID: u.ID,
204+
UserLogin: u.UserLogin,
205+
DisplayName: u.DisplayName,
206+
Title: u.Title,
207+
Language: u.Language,
208+
CategoryID: u.CategoryID.String,
209+
CategoryName: u.CategoryName.String,
210+
Delay: u.Delay,
211+
Tags: []string{"English", "CLI Tag"},
212+
BrandedContent: u.IsBrandedContent,
213+
214+
ContentClassificationLabels: ccls,
181215
})
182216
}
183217
return response
184218
}
219+
220+
func handleCCLs(u database.User, params PatchInformationEndpointRequest) (string, error) {
221+
// Get list of already enabled CCLs
222+
currentCCLsStrings := []string{}
223+
if u.UnparsedCCLs != "" {
224+
currentCCLsStrings = strings.Split(u.UnparsedCCLs, ",")
225+
}
226+
cclsDetailed := []PatchInformationEndpointRequestLabel{}
227+
for _, ccl := range models.CCL_MAP {
228+
newCCL := PatchInformationEndpointRequestLabel{
229+
ID: ccl.ID,
230+
IsEnabled: false,
231+
}
232+
for _, s := range currentCCLsStrings {
233+
if s == ccl.ID {
234+
newCCL.IsEnabled = true
235+
}
236+
}
237+
cclsDetailed = append(cclsDetailed, newCCL)
238+
}
239+
240+
// Run through user-provided CCLs
241+
for _, ccl := range params.ContentClassificationLabels {
242+
// Validate CCLs provided by the user
243+
foundCCL, ok := models.CCL_MAP[ccl.ID]
244+
if !ok {
245+
return "", fmt.Errorf("ContentClassificationLabels label provided is not supported")
246+
}
247+
if foundCCL.RestrictedGaming {
248+
return "", fmt.Errorf("User requested gaming CCLs to be added to their channel")
249+
}
250+
251+
// Update anything mentioned by the user
252+
for i, updatingThisCCL := range cclsDetailed {
253+
if updatingThisCCL.ID == ccl.ID {
254+
updatingThisCCL.IsEnabled = ccl.IsEnabled
255+
cclsDetailed[i] = updatingThisCCL
256+
}
257+
}
258+
}
259+
260+
// Convert CCL list to CSV for storage
261+
cclDbString := ""
262+
for _, ccl := range cclsDetailed {
263+
if ccl.IsEnabled {
264+
cclDbString += ccl.ID + ","
265+
}
266+
}
267+
if strings.HasSuffix(cclDbString, ",") {
268+
cclDbString = cclDbString[:len(cclDbString)-1]
269+
}
270+
271+
return cclDbString, nil
272+
}

internal/models/ccl.go

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package models
4+
5+
type ContentClassificationLabel struct {
6+
Description string
7+
ID string
8+
Name string
9+
RestrictedGaming bool // Restricts users from applying that CCL via the API. Currently only for MatureGame.
10+
}
11+
12+
var CCL_MAP = map[string]ContentClassificationLabel{
13+
"DrugsIntoxication": {
14+
Description: "Excessive tobacco glorification or promotion, any marijuana consumption/use, legal drug and alcohol induced intoxication, discussions of illegal drugs.",
15+
ID: "DrugsIntoxication",
16+
Name: "Drugs, Intoxication, or Excessive Tobacco Use",
17+
RestrictedGaming: false,
18+
},
19+
"Gambling": {
20+
Description: "Participating in online or in-person gambling, poker or fantasy sports, that involve the exchange of real money.",
21+
ID: "Gambling",
22+
Name: "Gambling",
23+
RestrictedGaming: false,
24+
},
25+
"MatureGame": {
26+
Description: "Games that are rated Mature or less suitable for a younger audience.",
27+
ID: "MatureGame",
28+
Name: "Mature-rated game",
29+
RestrictedGaming: true,
30+
},
31+
"ProfanityVulgarity": {
32+
Description: "Prolonged, and repeated use of obscenities, profanities, and vulgarities, especially as a regular part of speech.",
33+
ID: "ProfanityVulgarity",
34+
Name: "Significant Profanity or Vulgarity",
35+
RestrictedGaming: false,
36+
},
37+
"SexualThemes": {
38+
Description: "Content that focuses on sexualized physical attributes and activities, sexual topics, or experiences.",
39+
ID: "SexualThemes",
40+
Name: "Sexual Themes",
41+
RestrictedGaming: false,
42+
},
43+
"ViolentGraphic": {
44+
Description: "Simulations and/or depictions of realistic violence, gore, extreme injury, or death.",
45+
ID: "ViolentGraphic",
46+
Name: "Violent and Graphic Depictions",
47+
RestrictedGaming: false,
48+
},
49+
}

0 commit comments

Comments
 (0)