Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate Twitch clips to Helix API #144

Merged
merged 26 commits into from
May 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
78d42f3
Initial work, SIGSEGVs for some reason
zneix Jan 28, 2021
6ec4cc5
Fixed some stuff, no longer SIGSEGVs
zneix Jan 28, 2021
eb05a68
Merge remote-tracking branch 'upstream/master' into feature/migrate-t…
zneix Feb 3, 2021
7ef79ca
Merge remote-tracking branch 'upstream/master' into feature/migrate-t…
zneix Mar 14, 2021
0b117dc
Merge remote-tracking branch 'upstream/master' into feature/migrate-t…
zneix Mar 20, 2021
406936e
Merge upstream, tests are broken
zneix Apr 20, 2021
a48fe89
Merge remote-tracking branch 'upstream/master' into feature/migrate-t…
zneix Apr 20, 2021
10234c0
Merge branch 'master' into feature/migrate-to-helix
zneix May 7, 2021
b80126d
Bump version of nicklaw5/helix
zneix May 7, 2021
6be24a7
xd
zneix May 7, 2021
f38585b
Updated code to support duration field
zneix May 7, 2021
d9589f3
Added changelog entries
zneix May 7, 2021
ce33f12
Merge remote-tracking branch 'origin/master' into feature/migrate-to-…
zneix May 7, 2021
5df9ff0
Re-added tools to go.sum
zneix May 7, 2021
f7e5034
I removed more go.sum stuff than I should've...
zneix May 7, 2021
2fc71ad
forsenY
zneix May 7, 2021
79b0ecf
Fixed tests (hopefully?)
zneix May 7, 2021
85cc107
KKoooona (and number update)
zneix May 7, 2021
e813a8a
Made tests more fancy
zneix May 7, 2021
12d8b13
Remove pajlada/jsonapi since it's no longer needed
zneix May 7, 2021
e0f47c6
Remove all references to dankeroni/gotwitch
zneix May 7, 2021
7b6b063
Don't crash on links to wrong clips
zneix May 9, 2021
b6952b8
Handle GetClips errors and 'no clip found' errors separately
pajlada May 9, 2021
7ef5c41
Skip 'temporary' helixAPIxd variable
pajlada May 9, 2021
3cd9c0a
Add channel to Helix Token Refresh call ensuring we don't start
pajlada May 9, 2021
04ca3e4
rename clipHelix to clip
pajlada May 9, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

- Breaking: Environment variable `CHATTERINO_API_CACHE_TWITCH_CLIENT_ID` was renamed to `CHATTERINO_API_TWITCH_CLIENT_ID`. (#144)
- Dev, Breaking: Replaced `dankeroni/gotwitch` with `nicklaw5/helix`. This change requires you to add new environment variable: `CHATTERINO_API_TWITCH_CLIENT_SECRET` - it's a client secret generated for your Twitch application.

## 1.0.2

- Twitter profile pictures are now returned in their original quality. (#131)
Expand Down
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ go 1.15

require (
github.com/PuerkitoBio/goquery v1.6.1
github.com/dankeroni/gotwitch v0.0.0-20190429150511-5924c422419a
github.com/discord/lilliput v0.0.0-20210410064651-6e127f25858d
github.com/frankban/quicktest v1.12.1
github.com/go-chi/chi/v5 v5.0.3
github.com/golang/mock v1.5.0
github.com/koffeinsource/go-imgur v0.3.0
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/pajlada/jsonapi v0.0.0-20181126225824-ff9660de882a // indirect
github.com/nicklaw5/helix v1.14.0
github.com/patrickmn/go-cache v2.1.0+incompatible
google.golang.org/api v0.46.0
honnef.co/go/tools v0.1.4
Expand Down
6 changes: 2 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,6 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/dankeroni/gotwitch v0.0.0-20190429150511-5924c422419a h1:Y9MLN64hkshOJeudF4UwBQX9kjBlvjBeH3ru59Y2FZs=
github.com/dankeroni/gotwitch v0.0.0-20190429150511-5924c422419a/go.mod h1:lWs5CtxMaI7rSsvehIPXHnuZ3h5ojuCjWtMrYme8sKk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/discord/lilliput v0.0.0-20210410064651-6e127f25858d h1:jWQgeT6mu5HOHTYkG38bK3gEmCDPTl93mtXmFeSvFmY=
github.com/discord/lilliput v0.0.0-20210410064651-6e127f25858d/go.mod h1:0euuUBAD72MAYRm2ElLaG1h0nBR+CgpfnKc/U6y/uE8=
Expand Down Expand Up @@ -155,8 +153,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/pajlada/jsonapi v0.0.0-20181126225824-ff9660de882a h1:1T/17yKaIdv9trE0yD9Izkok3xsPfcjVNA2OB1WDjII=
github.com/pajlada/jsonapi v0.0.0-20181126225824-ff9660de882a/go.mod h1:fzPt+3DvAnKAz1lu+AMv4h6Xc8+2JwLxvmqIJQefs7k=
github.com/nicklaw5/helix v1.14.0 h1:yJI+dUDxFzmlSelNygWs/lhirvuzCqgIXIZy05JdHVk=
github.com/nicklaw5/helix v1.14.0/go.mod h1:XeeXY7oY5W+MVMu6wF4qGm8uvjZ1/Nss0FqprVkXKrg=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down
22 changes: 10 additions & 12 deletions internal/mocks/mock_TwitchAPIClient.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 19 additions & 6 deletions internal/resolvers/twitch/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,35 @@ import (
"github.com/Chatterino/api/pkg/cache"
"github.com/Chatterino/api/pkg/humanize"
"github.com/Chatterino/api/pkg/resolver"
"github.com/nicklaw5/helix"
)

func load(clipSlug string, r *http.Request) (interface{}, time.Duration, error) {
log.Println("[TwitchClip] GET", clipSlug)
clip, _, err := v5API.GetClip(clipSlug)

response, err := helixAPI.GetClips(&helix.ClipsParams{IDs: []string{clipSlug}})
if err != nil {
log.Println("[TwitchClip] Error getting clip", clipSlug, ":", err.Error())

return &resolver.Response{
Status: http.StatusInternalServerError,
Message: "An internal error occured while fetching the Twitch clip",
}, cache.NoSpecialDur, nil
}

if len(response.Data.Clips) != 1 {
return noTwitchClipWithThisIDFound, cache.NoSpecialDur, nil
}

var clip = response.Data.Clips[0]

data := twitchClipsTooltipData{
Title: clip.Title,
AuthorName: clip.Curator.DisplayName,
ChannelName: clip.Broadcaster.DisplayName,
AuthorName: clip.CreatorName,
ChannelName: clip.BroadcasterName,
Duration: humanize.DurationSeconds(time.Duration(clip.Duration) * time.Second),
CreationDate: humanize.CreationDate(clip.CreatedAt),
Views: humanize.Number(uint64(clip.Views)),
CreationDate: humanize.CreationDateRFC3339(clip.CreatedAt),
Views: humanize.Number(uint64(clip.ViewCount)),
}

var tooltip bytes.Buffer
Expand All @@ -39,6 +52,6 @@ func load(clipSlug string, r *http.Request) (interface{}, time.Duration, error)
return &resolver.Response{
Status: 200,
Tooltip: url.PathEscape(tooltip.String()),
Thumbnail: clip.Thumbnails.Medium,
Thumbnail: clip.ThumbnailURL,
}, cache.NoSpecialDur, nil
}
80 changes: 47 additions & 33 deletions internal/resolvers/twitch/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ package twitch
import (
"net/url"
"testing"
"time"

"github.com/Chatterino/api/internal/mocks"
"github.com/Chatterino/api/pkg/resolver"
"github.com/dankeroni/gotwitch"
qt "github.com/frankban/quicktest"
"github.com/golang/mock/gomock"
"github.com/nicklaw5/helix"
)

func testLoadAndUnescape(c *qt.C, clipSlug string) (cleanTooltip string) {
Expand All @@ -32,22 +31,27 @@ func TestLoad(t *testing.T) {
c := qt.New(t)
mockCtrl := gomock.NewController(c)
m := mocks.NewMockTwitchAPIClient(mockCtrl)
v5API = m
helixAPI = m

c.Run("Normal clip", func(c *qt.C) {
const slug = "KKona"
var clipResponse gotwitch.V5GetClipResponse
clipResponse.Title = "Clipped it LUL"
clipResponse.Broadcaster.DisplayName = "pajlada"
clipResponse.Curator.DisplayName = "supinic"
clipResponse.Duration = 30
clipResponse.CreatedAt = time.Date(2019, time.November, 14, 04, 20, 6, 9, time.UTC)
clipResponse.Views = 69

clip := helix.Clip{
Title: "Clipped it LUL",
BroadcasterName: "pajlada",
CreatorName: "supinic",
Duration: 30,
CreatedAt: "2019-11-14T04:20:06.09Z",
ViewCount: 69,
}

response := &helix.ClipsResponse{}
response.Data.Clips = []helix.Clip{clip}

m.
EXPECT().
GetClip(gomock.Eq(slug)).
Return(clipResponse, nil, nil)
GetClips(gomock.Eq(&helix.ClipsParams{IDs: []string{slug}})).
Return(response, nil)

const expectedTooltip = `<div style="text-align: left;"><b>Clipped it LUL</b><hr><b>Clipped by:</b> supinic<br><b>Channel:</b> pajlada<br><b>Duration:</b> 30s<br><b>Created:</b> 14 Nov 2019<br><b>Views:</b> 69</div>`

Expand All @@ -57,41 +61,51 @@ func TestLoad(t *testing.T) {
})

c.Run("Normal clip (Number formatting)", func(c *qt.C) {
const slug = "KKona"
var clipResponse gotwitch.V5GetClipResponse
clipResponse.Title = "Clipped it LUL"
clipResponse.Broadcaster.DisplayName = "pajlada"
clipResponse.Curator.DisplayName = "supinic"
clipResponse.Duration = 30
clipResponse.CreatedAt = time.Date(2019, time.November, 14, 04, 20, 6, 9, time.UTC)
clipResponse.Views = 6969
const slug = "KKaper"

clip := helix.Clip{
Title: "Clipped it LUL",
BroadcasterName: "pajlada",
CreatorName: "suspinic",
Duration: 30.1,
CreatedAt: "2019-11-14T04:20:06.09Z",
ViewCount: 6969,
}

response := &helix.ClipsResponse{}
response.Data.Clips = []helix.Clip{clip}

m.
EXPECT().
GetClip(gomock.Eq(slug)).
Return(clipResponse, nil, nil)
GetClips(gomock.Eq(&helix.ClipsParams{IDs: []string{slug}})).
Return(response, nil)

const expectedTooltip = `<div style="text-align: left;"><b>Clipped it LUL</b><hr><b>Clipped by:</b> supinic<br><b>Channel:</b> pajlada<br><b>Duration:</b> 30s<br><b>Created:</b> 14 Nov 2019<br><b>Views:</b> 6,969</div>`
const expectedTooltip = `<div style="text-align: left;"><b>Clipped it LUL</b><hr><b>Clipped by:</b> suspinic<br><b>Channel:</b> pajlada<br><b>Duration:</b> 30s<br><b>Created:</b> 14 Nov 2019<br><b>Views:</b> 6,969</div>`

cleanTooltip := testLoadAndUnescape(c, slug)

c.Assert(cleanTooltip, qt.Equals, expectedTooltip)
})

c.Run("Normal clip (HTML)", func(c *qt.C) {
const slug = "KKona"
var clipResponse gotwitch.V5GetClipResponse
clipResponse.Title = "Clipped it <b>LUL</b>"
clipResponse.Broadcaster.DisplayName = "<b>pajlada</b>"
clipResponse.Curator.DisplayName = "<b>supinic</b>"
clipResponse.Duration = 30
clipResponse.CreatedAt = time.Date(2019, time.November, 14, 04, 20, 6, 9, time.UTC)
clipResponse.Views = 69
const slug = "KKool"

clip := helix.Clip{
Title: "Clipped it <b>LUL</b>",
BroadcasterName: "<b>pajlada</b>",
CreatorName: "<b>supinic</b>",
Duration: 30,
CreatedAt: "2019-11-14T04:20:06.09Z",
ViewCount: 69,
}

response := &helix.ClipsResponse{}
response.Data.Clips = []helix.Clip{clip}

m.
EXPECT().
GetClip(gomock.Eq(slug)).
Return(clipResponse, nil, nil)
GetClips(gomock.Eq(&helix.ClipsParams{IDs: []string{slug}})).
Return(response, nil)

const expectedTooltip = `<div style="text-align: left;"><b>Clipped it &lt;b&gt;LUL&lt;/b&gt;</b><hr><b>Clipped by:</b> &lt;b&gt;supinic&lt;/b&gt;<br><b>Channel:</b> &lt;b&gt;pajlada&lt;/b&gt;<br><b>Duration:</b> 30s<br><b>Created:</b> 14 Nov 2019<br><b>Views:</b> 69</div>`

Expand Down
38 changes: 29 additions & 9 deletions internal/resolvers/twitch/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,18 @@ import (
"encoding/json"
"html/template"
"log"
"net/http"
"net/url"
"os"
"strings"
"time"

"github.com/Chatterino/api/pkg/cache"
"github.com/Chatterino/api/pkg/resolver"
"github.com/Chatterino/api/pkg/utils"
"github.com/dankeroni/gotwitch"
"github.com/nicklaw5/helix"
)

type TwitchAPIClient interface {
GetClip(clipSlug string) (clip gotwitch.V5GetClipResponse, r *http.Response, err error)
GetClips(params *helix.ClipsParams) (clip *helix.ClipsResponse, err error)
}

const (
Expand All @@ -38,17 +36,39 @@ var (

clipCache = cache.New("twitchclip", load, 1*time.Hour)

v5API TwitchAPIClient
helixAPI TwitchAPIClient
)

func New() (resolvers []resolver.CustomURLManager) {
clientID, exists := os.LookupEnv("CHATTERINO_API_CACHE_TWITCH_CLIENT_ID")
if !exists {
log.Println("No CHATTERINO_API_CACHE_TWITCH_CLIENT_ID specified, won't do special responses for twitch clips")
clientID, existsClientID := utils.LookupEnv("TWITCH_CLIENT_ID")
clientSecret, existsClientSecret := utils.LookupEnv("TWITCH_CLIENT_SECRET")

if !existsClientID {
log.Println("No CHATTERINO_API_TWITCH_CLIENT_ID specified, won't do special responses for Twitch clips")
return
}

v5API = gotwitch.NewV5(clientID)
if !existsClientSecret {
log.Println("No CHATTERINO_API_TWITCH_CLIENT_SECRET specified, won't do special responses for Twitch clips")
return
}

var err error

helixAPI, err = helix.NewClient(&helix.Options{
ClientID: clientID,
ClientSecret: clientSecret,
})

if err != nil {
log.Fatalf("[Helix] Error initializing API client: %s", err.Error())
}

waitForFirstAppAccessToken := make(chan struct{})

// Initialize methods responsible for refreshing oauth
go initAppAccessToken(helixAPI.(*helix.Client), waitForFirstAppAccessToken)
<-waitForFirstAppAccessToken

// Find clips that look like https://clips.twitch.tv/SlugHere
resolvers = append(resolvers, resolver.CustomURLManager{
Expand Down
36 changes: 36 additions & 0 deletions internal/resolvers/twitch/token_refresh.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package twitch

import (
"log"
"time"

"github.com/nicklaw5/helix"
)

// initAppAccessToken requests and sets app access token to the provided helix.Client
// and initializes a ticker running every 24 Hours which re-requests and sets app access token
func initAppAccessToken(helixAPI *helix.Client, tokenFetched chan struct{}) {
response, err := helixAPI.RequestAppAccessToken([]string{})

if err != nil {
log.Fatalf("[Helix] Error requesting app access token: %s , \n %s", err.Error(), response.Error)
}

log.Printf("[Helix] Requested access token, status: %d, expires in: %d", response.StatusCode, response.Data.ExpiresIn)
helixAPI.SetAppAccessToken(response.Data.AccessToken)
close(tokenFetched)

// initialize the ticker
ticker := time.NewTicker(24 * time.Hour)

for range ticker.C {
response, err := helixAPI.RequestAppAccessToken([]string{})
if err != nil {
log.Printf("[Helix] Failed to re-request app access token from ticker, status: %d", response.StatusCode)
continue
}
log.Printf("[Helix] Re-requested access token from ticker, status: %d, expires in: %d", response.StatusCode, response.Data.ExpiresIn)

helixAPI.SetAppAccessToken(response.Data.AccessToken)
}
}