/ support / question_samples / twitch.go
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  }