Skip to content

Commit

Permalink
Updated /channels Mock API endpoint to include content classification…
Browse files Browse the repository at this point in the history
… labels
  • Loading branch information
Xemdo committed Jul 12, 2023
1 parent d07636e commit cddaa62
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 44 deletions.
12 changes: 9 additions & 3 deletions internal/database/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"github.com/jmoiron/sqlx"
)

const currentVersion = 4
const currentVersion = 5

type migrateMap struct {
SQL string
Expand All @@ -37,12 +37,18 @@ var migrateSQL = map[int]migrateMap{
4: {
SQL: `
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';
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 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 '' );
INSERT INTO chat_settings (broadcaster_id) SELECT id FROM users;
ALTER TABLE users ADD COLUMN chat_color text not null default '#9146FF';
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) );`,
Message: `Updating database to include API changes since last version. See Twitch CLI changelog for more info.`,
},
5: {
SQL: `
ALTER TABLE users ADD COLUMN branded_content boolean not null default false;
ALTER TABLE users ADD COLUMN content_labels text not null default '';`,
Message: `Updating database to include Content Classification Label field.`,
},
}

func checkAndUpdate(db sqlx.DB) error {
Expand Down Expand Up @@ -81,7 +87,7 @@ 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, igdb_id 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, chat_color text not null default '#9146FF', foreign key (category_id) references categories(id) );
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) );
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) );
Expand Down
40 changes: 22 additions & 18 deletions internal/database/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,28 @@ import (
)

type User struct {
ID string `db:"id" json:"id" dbs:"u1.id"`
UserLogin string `db:"user_login" json:"login"`
DisplayName string `db:"display_name" json:"display_name"`
Email string `db:"email" json:"email,omitempty"`
UserType string `db:"user_type" json:"type"`
BroadcasterType string `db:"broadcaster_type" json:"broadcaster_type"`
UserDescription string `db:"user_description" json:"description"`
CreatedAt string `db:"created_at" json:"created_at"`
ModifiedAt string `db:"modified_at" json:"-"`
ProfileImageURL string `dbi:"false" json:"profile_image_url" `
OfflineImageURL string `dbi:"false" json:"offline_image_url" `
ViewCount int `dbi:"false" json:"view_count"`
CategoryID sql.NullString `db:"category_id" json:"game_id" dbi:"force"`
CategoryName sql.NullString `db:"category_name" json:"game_name" dbi:"false"`
Title string `db:"title" json:"title"`
Language string `db:"stream_language" json:"stream_language"`
Delay int `db:"delay" json:"delay" dbi:"force"`
ChatColor string `db:"chat_color" json:"-"`
ID string `db:"id" json:"id" dbs:"u1.id"`
UserLogin string `db:"user_login" json:"login"`
DisplayName string `db:"display_name" json:"display_name"`
Email string `db:"email" json:"email,omitempty"`
UserType string `db:"user_type" json:"type"`
BroadcasterType string `db:"broadcaster_type" json:"broadcaster_type"`
UserDescription string `db:"user_description" json:"description"`
CreatedAt string `db:"created_at" json:"created_at"`
ModifiedAt string `db:"modified_at" json:"-"`
ProfileImageURL string `dbi:"false" json:"profile_image_url" `
OfflineImageURL string `dbi:"false" json:"offline_image_url" `
ViewCount int `dbi:"false" json:"view_count"`
CategoryID sql.NullString `db:"category_id" json:"game_id" dbi:"force"`
CategoryName sql.NullString `db:"category_name" json:"game_name" dbi:"false"`
Title string `db:"title" json:"title"`
Language string `db:"stream_language" json:"stream_language"`
Delay int `db:"delay" json:"delay" dbi:"force"`
ChatColor string `db:"chat_color" json:"-"`
IsBrandedContent bool `db:"branded_content" json:"is_branded_content"`

// UnparsedCCLs is a comma seperated array (e.g. "Gambling,ViolentGraphic,ProfanityVulgarity")
UnparsedCCLs string `db:"content_labels" json:"-"`
}

type Follow struct {
Expand Down
134 changes: 111 additions & 23 deletions internal/mock_api/endpoints/channels/information.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ package channels
import (
"database/sql"
"encoding/json"
"fmt"
"net/http"
"strings"

"github.com/mattn/go-sqlite3"
"github.com/twitchdev/twitch-cli/internal/database"
Expand All @@ -15,15 +17,18 @@ 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"`
Tags []string `dbi:"false" json:"tags"`
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"`
BrandedContent bool `dbi:"false" json:"is_branded_content"`

ContentClassificationLabels []string `dbi:"false" json:"content_classification_labels"`
}

var informationMethodsSupported = map[string]bool{
Expand All @@ -49,6 +54,13 @@ type PatchInformationEndpointRequest struct {
BroadcasterLanguage string `json:"broadcaster_language"`
Title string `json:"title"`
Delay *int `json:"delay"`
// TODO: tags
ContentClassificationLabels []PatchInformationEndpointRequestLabel `json:"content_classification_labels"`
}

type PatchInformationEndpointRequestLabel struct {
ID string `json:"id"`
IsEnabled bool `json:"is_enabled"`
}

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

// Game ID
var gameID = u.CategoryID
if params.GameID == "" || params.GameID == "0" {
gameID = sql.NullString{}
} else if params.GameID != "" {
gameID = sql.NullString{String: params.GameID, Valid: true}
}

// Delay
if params.Delay != nil && u.BroadcasterType != "partner" {
mock_errors.WriteBadRequest(w, "Delay is partner only")
return
Expand All @@ -146,12 +160,23 @@ func patchInformation(w http.ResponseWriter, r *http.Request) {
} else {
delay = *params.Delay
}

// TODO: Branded content

cclDbString, err := handleCCLs(u, params)
if err != nil {
mock_errors.WriteForbidden(w, err.Error())
return
}

// Write
err = db.NewQuery(r, 100).UpdateChannel(broadcasterID, database.User{
ID: broadcasterID,
Title: params.Title,
Language: params.BroadcasterLanguage,
CategoryID: gameID,
Delay: delay,
ID: broadcasterID,
Title: params.Title,
Language: params.BroadcasterLanguage,
CategoryID: gameID,
Delay: delay,
UnparsedCCLs: cclDbString,
})
if err != nil {
if database.DatabaseErrorIs(err, sqlite3.ErrConstraintForeignKey) {
Expand All @@ -168,17 +193,80 @@ func patchInformation(w http.ResponseWriter, r *http.Request) {
func convertUsers(users []database.User) []Channel {
response := []Channel{}
for _, u := range users {
// Convert CCL array into an actual string array
var ccls = []string{}
if u.UnparsedCCLs != "" {
ccls = strings.Split(u.UnparsedCCLs, ",")
}

response = append(response, Channel{
ID: u.ID,
UserLogin: u.UserLogin,
DisplayName: u.DisplayName,
Title: u.Title,
Language: u.Language,
CategoryID: u.CategoryID.String,
CategoryName: u.CategoryName.String,
Delay: u.Delay,
Tags: []string{"English", "CLI Tag"},
ID: u.ID,
UserLogin: u.UserLogin,
DisplayName: u.DisplayName,
Title: u.Title,
Language: u.Language,
CategoryID: u.CategoryID.String,
CategoryName: u.CategoryName.String,
Delay: u.Delay,
Tags: []string{"English", "CLI Tag"},
BrandedContent: u.IsBrandedContent,

ContentClassificationLabels: ccls,
})
}
return response
}

func handleCCLs(u database.User, params PatchInformationEndpointRequest) (string, error) {
// Get list of already enabled CCLs
currentCCLsStrings := []string{}
if u.UnparsedCCLs != "" {
currentCCLsStrings = strings.Split(u.UnparsedCCLs, ",")
}
cclsDetailed := []PatchInformationEndpointRequestLabel{}
for _, ccl := range models.CCL_MAP {
newCCL := PatchInformationEndpointRequestLabel{
ID: ccl.ID,
IsEnabled: false,
}
for _, s := range currentCCLsStrings {
if s == ccl.ID {
newCCL.IsEnabled = true
}
}
cclsDetailed = append(cclsDetailed, newCCL)
}

// Run through user-provided CCLs
for _, ccl := range params.ContentClassificationLabels {
// Validate CCLs provided by the user
foundCCL, ok := models.CCL_MAP[ccl.ID]
if !ok {
return "", fmt.Errorf("ContentClassificationLabels label provided is not supported")
}
if foundCCL.RestrictedGaming {
return "", fmt.Errorf("User requested gaming CCLs to be added to their channel")
}

// Update anything mentioned by the user
for i, updatingThisCCL := range cclsDetailed {
if updatingThisCCL.ID == ccl.ID {
updatingThisCCL.IsEnabled = ccl.IsEnabled
cclsDetailed[i] = updatingThisCCL
}
}
}

// Convert CCL list to CSV for storage
cclDbString := ""
for _, ccl := range cclsDetailed {
if ccl.IsEnabled {
cclDbString += ccl.ID + ","
}
}
if strings.HasSuffix(cclDbString, ",") {
cclDbString = cclDbString[:len(cclDbString)-1]
}

return cclDbString, nil
}
49 changes: 49 additions & 0 deletions internal/models/ccl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package models

type ContentClassificationLabel struct {
Description string
ID string
Name string
RestrictedGaming bool // Restricts users from applying that CCL via the API. Currently only for MatureGame.
}

var CCL_MAP = map[string]ContentClassificationLabel{
"DrugsIntoxication": {
Description: "Excessive tobacco glorification or promotion, any marijuana consumption/use, legal drug and alcohol induced intoxication, discussions of illegal drugs.",
ID: "DrugsIntoxication",
Name: "Drugs, Intoxication, or Excessive Tobacco Use",
RestrictedGaming: false,
},
"Gambling": {
Description: "Participating in online or in-person gambling, poker or fantasy sports, that involve the exchange of real money.",
ID: "Gambling",
Name: "Gambling",
RestrictedGaming: false,
},
"MatureGame": {
Description: "Games that are rated Mature or less suitable for a younger audience.",
ID: "MatureGame",
Name: "Mature-rated game",
RestrictedGaming: true,
},
"ProfanityVulgarity": {
Description: "Prolonged, and repeated use of obscenities, profanities, and vulgarities, especially as a regular part of speech.",
ID: "ProfanityVulgarity",
Name: "Significant Profanity or Vulgarity",
RestrictedGaming: false,
},
"SexualThemes": {
Description: "Content that focuses on sexualized physical attributes and activities, sexual topics, or experiences.",
ID: "SexualThemes",
Name: "Sexual Themes",
RestrictedGaming: false,
},
"ViolentGraphic": {
Description: "Simulations and/or depictions of realistic violence, gore, extreme injury, or death.",
ID: "ViolentGraphic",
Name: "Violent and Graphic Depictions",
RestrictedGaming: false,
},
}

0 comments on commit cddaa62

Please sign in to comment.