twitch.go
1 package main 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io/ioutil" 7 "log" 8 "net/http" 9 "net/url" 10 "os" 11 "strconv" 12 "strings" 13 "time" 14 ) 15 16 const TWITCH_HELIX_HOST string = "api.twitch.tv" 17 const TWITCH_HELIX_GETSTREAMS string = "helix/streams" 18 const TWITCH_HELIX_TOKEN string = "oauth2/token" 19 const TWITCH_HELIX_VALIDATE string = "oauth2/validate" 20 const TWITCH_IRC_HOST string = "irc-ws.chat.twitch.tv:80" 21 22 type ChannelSet map[string]bool 23 24 type StreamInfo struct { 25 ChannelId uint64 26 UserName string 27 Topic string 28 Title string 29 Viewers uint64 30 StartTS string 31 } 32 33 type helixStreams struct { 34 Data []helixStream `json:"data"` 35 Pagination helixPagination `json:"pagination"` 36 } 37 38 type helixPagination struct { 39 Cursor string `json:"cursor"` 40 } 41 42 type helixValidate struct { 43 ClientId string `json:"client_id"` 44 Login string `json:"login"` 45 Scopes []string `json:"scopes"` 46 UserId string `json:"user_id"` 47 ExpiresIn uint32 `json:"expires_id"` 48 } 49 50 type helixToken struct { 51 AccessToken string `json:"access_token"` 52 ExpiresIn uint32 `json:"expires_in"` 53 RefreshToken string `json:"refresh_token"` 54 Scope []string `json:"scope"` 55 TokenType string `json:"token_type"` 56 } 57 58 type helixStream struct { 59 Id string `json:"id"` 60 UserId string `json:"user_id"` 61 UserLogin string `json:"user_login"` 62 UserName string `json:"user_name"` 63 GameId string `json:"game_id"` 64 GameName string `json:"game_name"` 65 Type string `json:"type"` 66 Title string `json:"title"` 67 ViewerCount uint32 `json:"viewer_count"` 68 StartedAt string `json:"started_at"` 69 Language string `json:"language"` 70 ThumbnailURL string `json:"thumbnail_url"` 71 TagIds []string `json:"tag_ids"` 72 IsMature bool `json:"is_mature"` 73 } 74 75 type helixUserPayload struct { 76 Data []helixUser `json:"data"` 77 } 78 79 type helixUser struct { 80 Id string `json:"id"` 81 Login string `json:"login"` 82 DisplayName string `json:"display_name"` 83 UserType string `json:"type"` 84 BroadcasterType string `json:"broadcaster_type"` 85 Description string `json:"description"` 86 ProfileURL string `json:"profile_image_url"` 87 OfflineURL string `json:"offline_image_url"` 88 ViewCount uint32 `json:"view_count"` 89 CreatedAt string `json:"created_at"` 90 } 91 92 func GetStreams(app App, maxViewers, maxResults int, ignore ChannelSet) []StreamInfo { 93 result := make([]StreamInfo, 0, maxResults) 94 cursor := "" 95 for len(result) < maxResults { 96 helixResult, c := HelixGetStreams(app, cursor, "") 97 for _, r := range helixResult { 98 if ok, _ := ignore[r.UserName]; !ok && int(r.Viewers) < maxViewers { 99 result = append(result, r) 100 } 101 if len(result) == maxResults { 102 return result 103 } 104 } 105 cursor = c 106 } 107 return result 108 } 109 110 func HelixGetStreams(app App, cursor, channelName string) ([]StreamInfo, string) { 111 query := url.Values{} 112 query.Add("language", "en") 113 114 if cursor != "" { 115 query.Add("after", cursor) 116 } 117 118 // query.Add("game_id", "417752") // Podcasts! 119 120 if channelName != "" { 121 query.Add("user_login", string(channelName)) 122 } else { 123 query.Add("first", "100") 124 } 125 126 target := url.URL{ 127 Scheme: "https", 128 Host: TWITCH_HELIX_HOST, 129 Path: TWITCH_HELIX_GETSTREAMS, 130 RawQuery: query.Encode(), 131 } 132 133 req, err := http.NewRequest(http.MethodGet, target.String(), nil) 134 if err != nil { 135 log.Printf("Twitch | Error when making request: %s\n", err) 136 os.Exit(1) 137 } 138 req.Header.Set("Client-Id", app.ClientId) 139 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", app.Token)) 140 141 res, err := app.HTTPClient.Do(req) 142 if err != nil { 143 log.Printf("Twitch | Error when executing request: %s\n", err) 144 os.Exit(1) 145 } 146 147 if res.StatusCode == http.StatusTooManyRequests { 148 log.Printf("Points added to Rate Limit counter for this request: %s\n", res.Header.Get("Ratelimit-Limit")) 149 log.Printf("Requests remaining before limit: %s\n", res.Header.Get("Ratelimit-Remaining")) 150 log.Printf("Time when new requests can be made: %s\n", res.Header.Get("Ratelimit-Reset")) 151 os.Exit(1) 152 } 153 154 // on 401 retry after refreshing token 155 if res.StatusCode == http.StatusUnauthorized { 156 res.Body.Close() 157 158 newTokens := HelixRefreshToken(app) 159 app.Token = newTokens.AccessToken 160 app.RefreshToken = newTokens.RefreshToken 161 162 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", app.Token)) 163 164 res, err = app.HTTPClient.Do(req) 165 if err != nil { 166 log.Printf("Twitch | Error when executing request: %s\n", err) 167 os.Exit(1) 168 } 169 } 170 171 defer res.Body.Close() 172 body, err := ioutil.ReadAll(res.Body) 173 if err != nil { 174 log.Printf("Twitch | Error when reading response body: %s\n", err) 175 os.Exit(1) 176 } 177 178 var r helixStreams 179 if err = json.Unmarshal(body, &r); err != nil { 180 log.Printf("Twitch | Error when decoding JSON response: %s\n", err) 181 os.Exit(1) 182 } 183 184 var result = []StreamInfo{} 185 for _, stream := range r.Data { 186 id_value, err := strconv.ParseUint(stream.UserId, 10, 64) 187 if err != nil { 188 log.Printf("Twitch | Failed to convert Id to uint64: %s\n", err) 189 os.Exit(1) 190 } 191 192 s := StreamInfo{ 193 ChannelId: id_value, 194 UserName: stream.UserLogin, 195 Topic: stream.GameName, 196 Title: stream.Title, 197 Viewers: uint64(stream.ViewerCount), 198 StartTS: stream.StartedAt, 199 } 200 201 result = append(result, s) 202 } 203 204 return result, r.Pagination.Cursor 205 } 206 207 func HelixRefreshToken(app App) helixToken { 208 form := url.Values{} 209 form.Add("client_id", app.ClientId) 210 form.Add("client_secret", app.ClientSecret) 211 form.Add("grant_type", "refresh_token") 212 form.Add("refresh_token", app.RefreshToken) 213 214 target := url.URL{ 215 Scheme: "https", 216 Host: "id.twitch.tv", 217 Path: TWITCH_HELIX_TOKEN, 218 } 219 220 req, err := http.NewRequest(http.MethodPost, target.String(), strings.NewReader(form.Encode())) 221 if err != nil { 222 log.Printf("Twitch | Error when making request: %s\n", err) 223 os.Exit(1) 224 } 225 226 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 227 228 res, err := app.HTTPClient.Do(req) 229 if err != nil { 230 log.Printf("Twitch | Error when executing request: %s\n", err) 231 os.Exit(1) 232 } 233 234 defer res.Body.Close() 235 body, err := ioutil.ReadAll(res.Body) 236 if err != nil { 237 log.Printf("Twitch | Error when reading response body: %s\n", err) 238 os.Exit(1) 239 } 240 241 if res.StatusCode == http.StatusBadRequest { 242 log.Println("Twitch | Refresh Token is invalid. %s") 243 os.Exit(1) 244 } 245 var r helixToken 246 if err = json.Unmarshal(body, &r); err != nil { 247 log.Printf("Twitch | Error when decoding JSON response: %s\n", err) 248 os.Exit(1) 249 } 250 251 return r 252 } 253 254 func HelixValidateToken(app App) bool { 255 target := url.URL{ 256 Scheme: "https", 257 Host: "id.twitch.tv", 258 Path: TWITCH_HELIX_VALIDATE, 259 } 260 261 req, err := http.NewRequest(http.MethodGet, target.String(), nil) 262 if err != nil { 263 log.Printf("Twitch | Error when making request: %s\n", err) 264 os.Exit(1) 265 } 266 267 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", app.Token)) 268 269 res, err := app.HTTPClient.Do(req) 270 if err != nil { 271 log.Printf("Twitch | Error when executing request: %s\n", err) 272 os.Exit(1) 273 } 274 275 defer res.Body.Close() 276 body, err := ioutil.ReadAll(res.Body) 277 if err != nil { 278 log.Printf("Twitch | Error when reading response body: %s\n", err) 279 os.Exit(1) 280 } 281 282 if res.StatusCode == http.StatusUnauthorized { 283 return false 284 } 285 286 var r helixValidate 287 if err = json.Unmarshal(body, &r); err != nil { 288 log.Printf("Twitch | Error when decoding JSON response: %s\n", err) 289 os.Exit(1) 290 } 291 292 return true 293 } 294 295 func HelixGetUserInfo(app App, userName string) helixUserPayload { 296 query := url.Values{} 297 query.Add("login", userName) 298 299 target := url.URL{ 300 Scheme: "https", 301 Host: "api.twitch.tv", 302 Path: "/helix/users", 303 RawQuery: query.Encode(), 304 } 305 306 req, err := http.NewRequest(http.MethodGet, target.String(), nil) 307 if err != nil { 308 log.Printf("Twitch | Error when making request: %s\n", err) 309 os.Exit(1) 310 } 311 312 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", app.Token)) 313 req.Header.Set("Client-Id", app.ClientId) 314 315 res, err := app.HTTPClient.Do(req) 316 if err != nil { 317 log.Printf("Twitch | Error when executing request: %s\n", err) 318 os.Exit(1) 319 } 320 321 if res.StatusCode == http.StatusUnauthorized { 322 log.Printf("Twitch | Unauthorized when getting User Info \n") 323 os.Exit(1) 324 } 325 326 // we will likely hit the rate limit this time, when we do, wait for the reset to try again 327 if res.StatusCode == http.StatusTooManyRequests { 328 res.Body.Close() 329 330 var rateLimitResetTime time.Time 331 332 err = rateLimitResetTime.UnmarshalText([]byte(res.Header.Get("Ratelimit-Reset"))) 333 if err != nil { 334 log.Printf("Failed to make current RFC3339 timestamp from header: %s\n", err) 335 os.Exit(1) 336 } 337 338 waitDuration := time.Until(rateLimitResetTime) 339 waitTimer := time.NewTimer(waitDuration) 340 341 select { 342 case <-waitTimer.C: 343 { 344 res, err = app.HTTPClient.Do(req) 345 if err != nil { 346 log.Printf("Failed to make current RFC3339 timestamp from header: %s\n", err) 347 os.Exit(1) 348 } 349 } 350 } 351 } 352 353 defer res.Body.Close() 354 body, err := ioutil.ReadAll(res.Body) 355 if err != nil { 356 log.Printf("Twitch | Error when reading response body: %s\n", err) 357 os.Exit(1) 358 } 359 360 var r helixUserPayload 361 if err = json.Unmarshal(body, &r); err != nil { 362 log.Printf("Twitch | Error when decoding JSON response: %s\n", err) 363 os.Exit(1) 364 } 365 366 return r 367 }