Skip to content

Commit

Permalink
Merge pull request #76 from twitchdev/feature/api-catchup
Browse files Browse the repository at this point in the history
Feature/api catchup
  • Loading branch information
lleadbet authored Jul 16, 2021
2 parents 6beac57 + e82a6f5 commit 64e8149
Show file tree
Hide file tree
Showing 31 changed files with 1,714 additions and 24 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ The CLI currently supports the following products:
- [api](./docs/api.md)
- [configure](./docs/configure.md)
- [event](docs/event.md)
- [mock-api](docs/mock-api.md)
- [token](docs/token.md)
- [version](docs/version.md)

Expand Down
17 changes: 16 additions & 1 deletion internal/database/_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,8 @@ create table drops_entitlements(
benefit_id text not null,
timestamp text not null,
user_id text not null,
game_id text not null,
game_id text not null,
status text not null default 'CLAIMED',
foreign key (user_id) references users(id),
foreign key (game_id) references categories(id)
);
Expand Down Expand Up @@ -289,3 +290,17 @@ create table clips (
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)
);
6 changes: 6 additions & 0 deletions internal/database/drops.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type DropsEntitlement struct {
BenefitID string `db:"benefit_id" json:"benefit_id"`
GameID string `db:"game_id" json:"game_id"`
Timestamp string `db:"timestamp" json:"timestamp"`
Status string `db:"status" json:"fulfillment_status"`
}

func (q *Query) GetDropsEntitlements(de DropsEntitlement) (*DBResponse, error) {
Expand Down Expand Up @@ -51,3 +52,8 @@ func (q *Query) InsertDropsEntitlement(d DropsEntitlement) error {
_, err := q.DB.NamedExec(stmt, d)
return err
}

func (q *Query) UpdateDropsEntitlement(d DropsEntitlement) error {
_, err := q.DB.NamedExec(generateUpdateSQL("drops_entitlements", []string{"id"}, d), d)
return err
}
6 changes: 5 additions & 1 deletion internal/database/init.go

Large diffs are not rendered by default.

131 changes: 131 additions & 0 deletions internal/database/schedule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package database

import (
"database/sql"
"errors"
"time"
)

type Schedule struct {
Segments []ScheduleSegment `json:"segments"`
UserID string `db:"broadcaster_id" json:"broadcaster_id"`
UserLogin string `db:"broadcaster_login" json:"broadcaster_login" dbi:"false"`
UserName string `db:"broadcaster_name" json:"broadcaster_name" dbi:"false"`
Vacation *ScheduleVacation `json:"vacation"`
}

type ScheduleSegment struct {
ID string `db:"id" json:"id" dbs:"s.id"`
Title string `db:"title" json:"title"`
StartTime string `db:"starttime" json:"start_time"`
EndTime string `db:"endtime" json:"end_time"`
IsRecurring bool `db:"is_recurring" json:"is_recurring"`
IsVacation bool `db:"is_vacation" json:"-"`
Category *SegmentCategory `json:"category"`
UserID string `db:"broadcaster_id" json:"-"`
Timezone string `db:"timezone" json:"timezone"`
CategoryID *string `db:"category_id" json:"-"`
CategoryName *string `db:"category_name" dbi:"false" json:"-"`
IsCanceled *bool `db:"is_canceled" json:"-"`
CanceledUntil *string `json:"canceled_until"`
}
type ScheduleVacation struct {
ID string `db:"id" json:"-"`
StartTime string `db:"starttime" json:"start_time"`
EndTime string `db:"endtime" json:"end_time"`
}

type SegmentCategory struct {
ID *string `db:"category_id" json:"id" dbs:"category_id"`
CategoryName *string `db:"category_name" json:"name" dbi:"false"`
}

func (q *Query) GetSchedule(p ScheduleSegment, startTime time.Time) (*DBResponse, error) {
r := Schedule{}

u, err := q.GetUser(User{ID: p.UserID})
if err != nil {
return nil, err
}
r.UserID = u.ID
r.UserLogin = u.UserLogin
r.UserName = u.DisplayName

sql := generateSQL("select s.*, c.category_name from stream_schedule s left join categories c on s.category_id = c.id", p, SEP_AND)
p.StartTime = startTime.Format(time.RFC3339)
sql += " and datetime(starttime) >= datetime(:starttime) " + q.SQL
rows, err := q.DB.NamedQuery(sql, p)
if err != nil {
return nil, err
}

for rows.Next() {
var s ScheduleSegment
err := rows.StructScan(&s)
if err != nil {
return nil, err
}
if s.CategoryID != nil {
s.Category = &SegmentCategory{
ID: s.CategoryID,
CategoryName: s.CategoryName,
}
}
if s.IsVacation {
r.Vacation = &ScheduleVacation{
StartTime: s.StartTime,
EndTime: s.EndTime,
}
} else {
r.Segments = append(r.Segments, s)
}
}
v, err := q.GetVacations(ScheduleSegment{UserID: p.UserID})
if err != nil {
return nil, err
}
r.Vacation = &v
dbr := DBResponse{
Data: r,
Limit: q.Limit,
Total: len(r.Segments),
}

if len(r.Segments) != q.Limit {
q.PaginationCursor = ""
}

dbr.Cursor = q.PaginationCursor

return &dbr, err
}

func (q *Query) InsertSchedule(p ScheduleSegment) error {
tx := q.DB.MustBegin()
_, err := tx.NamedExec(generateInsertSQL("stream_schedule", "id", p, false), p)
if err != nil {
return err
}
return tx.Commit()
}

func (q *Query) DeleteSegment(id string, broadcasterID string) error {
_, err := q.DB.Exec("delete from stream_schedule where id=$1 and broadcaster_id=$2", id, broadcasterID)
return err
}

func (q *Query) UpdateSegment(p ScheduleSegment) error {
_, err := q.DB.NamedExec(generateUpdateSQL("stream_schedule", []string{"id"}, p), p)
return err
}

func (q *Query) GetVacations(p ScheduleSegment) (ScheduleVacation, error) {
v := ScheduleVacation{}
err := q.DB.Get(&v, "select id,starttime,endtime from stream_schedule where is_vacation=true and datetime(endtime) > datetime('now') and broadcaster_id= $1 limit 1", p.UserID)
if errors.As(err, &sql.ErrNoRows) {
return v, nil
}
return v, err
}
3 changes: 2 additions & 1 deletion internal/events/trigger/trigger_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package trigger

import (
"fmt"
"log"
"time"

"github.com/twitchdev/twitch-cli/internal/database"
Expand Down Expand Up @@ -111,7 +112,7 @@ func Fire(p TriggerParameters) (string, error) {
}
defer resp.Body.Close()

println(fmt.Sprintf(`[%v] Request Sent`, resp.StatusCode))
log.Println(fmt.Sprintf(`[%v] Request Sent`, resp.StatusCode))
}

return string(resp.JSON), nil
Expand Down
1 change: 1 addition & 0 deletions internal/mock_api/authentication/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type UserAuthentication struct {

func AuthenticationMiddleware(next mock_api.MockEndpoint) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
db := r.Context().Value("db").(database.CLIDatabase)

// skip auth check for unsupported methods
Expand Down
96 changes: 96 additions & 0 deletions internal/mock_api/endpoints/chat/channel_emotes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package chat

import (
"encoding/json"
"fmt"
"net/http"

"github.com/twitchdev/twitch-cli/internal/database"
"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 channelEmotesMethodsSupported = map[string]bool{
http.MethodGet: true,
http.MethodPost: false,
http.MethodDelete: false,
http.MethodPatch: false,
http.MethodPut: false,
}

var channelEmotesScopesByMethod = map[string][]string{
http.MethodGet: {},
http.MethodPost: {},
http.MethodDelete: {},
http.MethodPatch: {},
http.MethodPut: {},
}

type ChannelEmotes struct{}

func (e ChannelEmotes) Path() string { return "/chat/emotes/channel" }

func (e ChannelEmotes) GetRequiredScopes(method string) []string {
return channelEmotesScopesByMethod[method]
}

func (e ChannelEmotes) ValidMethod(method string) bool {
return channelEmotesMethodsSupported[method]
}

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

switch r.Method {
case http.MethodGet:
getChannelEmotes(w, r)
break
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func getChannelEmotes(w http.ResponseWriter, r *http.Request) {
emotes := []EmotesResponse{}
broadcaster := r.URL.Query().Get("broadcaster_id")
if broadcaster == "" {
mock_errors.WriteBadRequest(w, "Missing required parameter broadcaster_id")
return
}

setID := fmt.Sprint(util.RandomInt(10 * 1000))
ownerID := util.RandomUserID()
for _, v := range defaultEmoteTypes {
emoteType := v
for i := 0; i < 5; i++ {
id := util.RandomInt(10 * 1000)
name := util.RandomGUID()
er := EmotesResponse{
ID: fmt.Sprint(id),
Name: name,
Images: EmotesImages{
ImageURL1X: fmt.Sprintf("https://static-cdn.jtvnw.net/emoticons/v1/%v/1.0", id),
ImageURL2X: fmt.Sprintf("https://static-cdn.jtvnw.net/emoticons/v1/%v/2.0", id),
ImageURL4X: fmt.Sprintf("https://static-cdn.jtvnw.net/emoticons/v1/%v/4.0", id),
},
EmoteType: &emoteType,
EmoteSetID: &setID,
OwnerID: &ownerID,
}
if emoteType == "subscription" {
thousand := "1000"
er.Tier = &thousand
} else {
es := ""
er.Tier = &es
}

emotes = append(emotes, er)
}
}

bytes, _ := json.Marshal(models.APIResponse{Data: emotes})
w.Write(bytes)
}
51 changes: 51 additions & 0 deletions internal/mock_api/endpoints/chat/chat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,54 @@ func TestChannelBadges(t *testing.T) {
a.Nil(err)
a.Equal(200, resp.StatusCode)
}

func TestGlobalEmotes(t *testing.T) {
a := test_setup.SetupTestEnv(t)
ts := test_server.SetupTestServer(GlobalEmotes{})

// get
req, _ := http.NewRequest(http.MethodGet, ts.URL+GlobalEmotes{}.Path(), nil)
q := req.URL.Query()
req.URL.RawQuery = q.Encode()
resp, err := http.DefaultClient.Do(req)
a.Nil(err)
a.Equal(200, resp.StatusCode)
}

func TestChannelEmotes(t *testing.T) {
a := test_setup.SetupTestEnv(t)
ts := test_server.SetupTestServer(ChannelEmotes{})

// get
req, _ := http.NewRequest(http.MethodGet, ts.URL+ChannelEmotes{}.Path(), nil)
q := req.URL.Query()
req.URL.RawQuery = q.Encode()
resp, err := http.DefaultClient.Do(req)
a.Nil(err)
a.Equal(400, resp.StatusCode)

q.Set("broadcaster_id", "1")
req.URL.RawQuery = q.Encode()
resp, err = http.DefaultClient.Do(req)
a.Nil(err)
a.Equal(200, resp.StatusCode)
}

func TestEmoteSets(t *testing.T) {
a := test_setup.SetupTestEnv(t)
ts := test_server.SetupTestServer(EmoteSets{})

// get
req, _ := http.NewRequest(http.MethodGet, ts.URL+EmoteSets{}.Path(), nil)
q := req.URL.Query()
req.URL.RawQuery = q.Encode()
resp, err := http.DefaultClient.Do(req)
a.Nil(err)
a.Equal(400, resp.StatusCode)

q.Set("emote_set_id", "1")
req.URL.RawQuery = q.Encode()
resp, err = http.DefaultClient.Do(req)
a.Nil(err)
a.Equal(200, resp.StatusCode)
}
Loading

0 comments on commit 64e8149

Please sign in to comment.