Skip to content

Commit 0af248d

Browse files
author
lleadbet
committed
adding schedules api support
1 parent fa52492 commit 0af248d

File tree

6 files changed

+546
-0
lines changed

6 files changed

+546
-0
lines changed

internal/database/schedule.go

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package database
4+
5+
import (
6+
"time"
7+
)
8+
9+
type Schedule struct {
10+
Segments []ScheduleSegment `json:"segments"`
11+
UserID string `db:"broadcaster_id" json:"broadcaster_id"`
12+
UserLogin string `db:"broadcaster_login" json:"broadcaster_login" dbi:"false"`
13+
UserName string `db:"broadcaster_name" json:"broadcaster_name" dbi:"false"`
14+
Vacation *ScheduleVacation `json:"vacation"`
15+
}
16+
17+
type ScheduleSegment struct {
18+
ID string `db:"id" json:"id" dbs:"s.id"`
19+
Title string `db:"title" json:"title"`
20+
StartTime string `db:"starttime" json:"start_time"`
21+
EndTime string `db:"endtime" json:"end_time"`
22+
IsRecurring bool `db:"is_recurring" json:"is_recurring"`
23+
IsVacation bool `db:"is_vacation" json:"-"`
24+
Category *SegmentCategory `json:"category"`
25+
UserID string `db:"broadcaster_id" json:"-"`
26+
Timezone string `db:"timezone" json:"timezone"`
27+
CategoryID *string `db:"category_id" json:"-"`
28+
CategoryName *string `db:"category_name" json:"-"`
29+
}
30+
type ScheduleVacation struct {
31+
StartTime string `json:"start_time"`
32+
EndTime string `json:"end_time"`
33+
}
34+
35+
type SegmentCategory struct {
36+
ID *string `db:"category_id" json:"id" dbs:"category_id"`
37+
CategoryName *string `db:"category_name" json:"name" dbi:"false"`
38+
}
39+
40+
func (q *Query) GetSchedule(p ScheduleSegment, startTime time.Time) (*DBResponse, error) {
41+
r := Schedule{}
42+
43+
u, err := q.GetUser(User{ID: p.UserID})
44+
if err != nil {
45+
return nil, err
46+
}
47+
r.UserID = u.ID
48+
r.UserLogin = u.UserLogin
49+
r.UserName = u.DisplayName
50+
51+
sql := generateSQL("select s.*, c.category_name from stream_schedule s left join categories c on s.category_id = c.id", p, SEP_AND)
52+
p.StartTime = startTime.Format(time.RFC3339)
53+
sql += " and datetime(starttime) >= datetime(:starttime) " + q.SQL
54+
rows, err := q.DB.NamedQuery(sql, p)
55+
if err != nil {
56+
return nil, err
57+
}
58+
59+
for rows.Next() {
60+
var s ScheduleSegment
61+
err := rows.StructScan(&s)
62+
if err != nil {
63+
return nil, err
64+
}
65+
if s.CategoryID != nil {
66+
s.Category = &SegmentCategory{
67+
ID: s.CategoryID,
68+
CategoryName: s.CategoryName,
69+
}
70+
}
71+
if s.IsVacation {
72+
r.Vacation = &ScheduleVacation{
73+
StartTime: s.StartTime,
74+
EndTime: s.EndTime,
75+
}
76+
} else {
77+
r.Segments = append(r.Segments, s)
78+
}
79+
}
80+
81+
dbr := DBResponse{
82+
Data: r,
83+
Limit: q.Limit,
84+
Total: len(r.Segments),
85+
}
86+
87+
if len(r.Segments) != q.Limit {
88+
q.PaginationCursor = ""
89+
}
90+
91+
dbr.Cursor = q.PaginationCursor
92+
93+
return &dbr, err
94+
}
95+
96+
func (q *Query) InsertSchedule(p ScheduleSegment) error {
97+
tx := q.DB.MustBegin()
98+
_, err := tx.NamedExec(generateInsertSQL("stream_schedule", "id", p, false), p)
99+
if err != nil {
100+
return err
101+
}
102+
return tx.Commit()
103+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package schedule
4+
5+
import (
6+
"net/http"
7+
8+
"github.com/twitchdev/twitch-cli/internal/database"
9+
"github.com/twitchdev/twitch-cli/internal/mock_api/mock_errors"
10+
)
11+
12+
var scheduleICalMethodsSupported = map[string]bool{
13+
http.MethodGet: true,
14+
http.MethodPost: false,
15+
http.MethodDelete: false,
16+
http.MethodPatch: false,
17+
http.MethodPut: false,
18+
}
19+
20+
var scheduleICalScopesByMethod = map[string][]string{
21+
http.MethodGet: {},
22+
http.MethodPost: {},
23+
http.MethodDelete: {},
24+
http.MethodPatch: {},
25+
http.MethodPut: {},
26+
}
27+
28+
type ScheduleICal struct{}
29+
30+
func (e ScheduleICal) Path() string { return "/schedule/icalendar" }
31+
32+
func (e ScheduleICal) GetRequiredScopes(method string) []string {
33+
return scheduleICalScopesByMethod[method]
34+
}
35+
36+
func (e ScheduleICal) ValidMethod(method string) bool {
37+
return scheduleICalMethodsSupported[method]
38+
}
39+
40+
func (e ScheduleICal) ServeHTTP(w http.ResponseWriter, r *http.Request) {
41+
db = r.Context().Value("db").(database.CLIDatabase)
42+
43+
switch r.Method {
44+
case http.MethodGet:
45+
e.getIcal(w, r)
46+
default:
47+
w.WriteHeader(http.StatusMethodNotAllowed)
48+
}
49+
}
50+
51+
// stubbed with fake data for now, since .ics generation libraries are far and few between for golang
52+
// and it's just useful for mock data
53+
func (e ScheduleICal) getIcal(w http.ResponseWriter, r *http.Request) {
54+
broadcaster := r.URL.Query().Get("broadcaster_id")
55+
if broadcaster == "" {
56+
mock_errors.WriteBadRequest(w, "Missing required paramater broadaster_id")
57+
return
58+
}
59+
60+
body :=
61+
`BEGIN:VCALENDAR
62+
PRODID:-//twitch.tv//StreamSchedule//1.0
63+
VERSION:2.0
64+
CALSCALE:GREGORIAN
65+
REFRESH-INTERVAL;VALUE=DURATION:PT1H
66+
NAME:TwitchDev
67+
BEGIN:VEVENT
68+
UID:e4acc724-371f-402c-81ca-23ada79759d4
69+
DTSTAMP:20210323T040131Z
70+
DTSTART;TZID=/America/New_York:20210701T140000
71+
DTEND;TZID=/America/New_York:20210701T150000
72+
SUMMARY:TwitchDev Monthly Update // July 1, 2021
73+
DESCRIPTION:Science & Technology.
74+
CATEGORIES:Science & Technology
75+
END:VEVENT
76+
END:VCALENDAR%`
77+
w.Header().Add("Content-Type", "text/calendar")
78+
w.Write([]byte(body))
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package schedule
4+
5+
import (
6+
"encoding/json"
7+
"net/http"
8+
"strconv"
9+
"time"
10+
11+
"github.com/twitchdev/twitch-cli/internal/database"
12+
"github.com/twitchdev/twitch-cli/internal/mock_api/mock_errors"
13+
"github.com/twitchdev/twitch-cli/internal/models"
14+
)
15+
16+
var scheduleMethodsSupported = map[string]bool{
17+
http.MethodGet: true,
18+
http.MethodPost: false,
19+
http.MethodDelete: false,
20+
http.MethodPatch: false,
21+
http.MethodPut: false,
22+
}
23+
24+
var scheduleScopesByMethod = map[string][]string{
25+
http.MethodGet: {},
26+
http.MethodPost: {},
27+
http.MethodDelete: {},
28+
http.MethodPatch: {},
29+
http.MethodPut: {},
30+
}
31+
32+
type Schedule struct{}
33+
34+
func (e Schedule) Path() string { return "/schedule" }
35+
36+
func (e Schedule) GetRequiredScopes(method string) []string {
37+
return scheduleScopesByMethod[method]
38+
}
39+
40+
func (e Schedule) ValidMethod(method string) bool {
41+
return scheduleMethodsSupported[method]
42+
}
43+
44+
func (e Schedule) ServeHTTP(w http.ResponseWriter, r *http.Request) {
45+
db = r.Context().Value("db").(database.CLIDatabase)
46+
47+
switch r.Method {
48+
case http.MethodGet:
49+
e.getSchedule(w, r)
50+
default:
51+
w.WriteHeader(http.StatusMethodNotAllowed)
52+
}
53+
}
54+
55+
func (e Schedule) getSchedule(w http.ResponseWriter, r *http.Request) {
56+
broadcasterID := r.URL.Query().Get("broadcaster_id")
57+
queryTime := r.URL.Query().Get("start_time")
58+
offset := r.URL.Query().Get("utc_offset")
59+
ids := r.URL.Query()["id"]
60+
schedule := database.Schedule{}
61+
startTime := time.Now().UTC()
62+
apiResponse := models.APIResponse{}
63+
64+
if broadcasterID == "" {
65+
mock_errors.WriteBadRequest(w, "Required parameter broadcaster_id is missing")
66+
return
67+
}
68+
69+
if queryTime != "" {
70+
st, err := time.Parse(time.RFC3339, queryTime)
71+
if err != nil {
72+
mock_errors.WriteBadRequest(w, "Parameter start_time is in an invalid format")
73+
return
74+
}
75+
startTime = st.UTC()
76+
}
77+
78+
if offset != "" {
79+
o, err := strconv.Atoi(offset)
80+
if err != nil {
81+
mock_errors.WriteBadRequest(w, "Error decoding parameter offset")
82+
return
83+
}
84+
tz := time.FixedZone("", o*60)
85+
startTime = startTime.In(tz)
86+
}
87+
88+
segments := []database.ScheduleSegment{}
89+
if len(ids) > 0 {
90+
if len(ids) > 100 {
91+
mock_errors.WriteBadRequest(w, "Parameter id may only have a maximum of 100 values")
92+
return
93+
}
94+
for _, id := range ids {
95+
dbr, err := db.NewQuery(r, 25).GetSchedule(database.ScheduleSegment{ID: id, UserID: broadcasterID}, startTime)
96+
if err != nil {
97+
mock_errors.WriteServerError(w, err.Error())
98+
}
99+
response := dbr.Data.(database.Schedule)
100+
schedule = response
101+
segments = append(segments, response.Segments...)
102+
}
103+
schedule.Segments = segments
104+
apiResponse = models.APIResponse{
105+
Data: schedule,
106+
}
107+
} else {
108+
dbr, err := db.NewQuery(r, 25).GetSchedule(database.ScheduleSegment{UserID: broadcasterID}, startTime)
109+
if err != nil {
110+
mock_errors.WriteServerError(w, err.Error())
111+
return
112+
}
113+
response := dbr.Data.(database.Schedule)
114+
segments = append(segments, response.Segments...)
115+
schedule = response
116+
schedule.Segments = segments
117+
apiResponse = models.APIResponse{
118+
Data: schedule,
119+
}
120+
121+
if len(schedule.Segments) == dbr.Limit {
122+
apiResponse.Pagination = &models.APIPagination{
123+
Cursor: dbr.Cursor,
124+
}
125+
}
126+
}
127+
128+
bytes, _ := json.Marshal(apiResponse)
129+
w.Write(bytes)
130+
}

0 commit comments

Comments
 (0)