Skip to content

Commit

Permalink
Display stream information for twitch users (#714)
Browse files Browse the repository at this point in the history
Co-authored-by: Rasmus Karlsson <[email protected]>
  • Loading branch information
M4tthewDE and pajlada authored Dec 28, 2024
1 parent 552fea6 commit 193fe97
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 19 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

## Unreleased

- Minor: Add stream information to twitch users. (#714)
- Minor: Server perks are now sorted alphabetically. (#711)
- Minor: Use correct domains for twitch user resolver. (#715)
- Dev: Changed the default `twitch-username-cache-duration` from 1 hour to 10 minutes. (#714)
- Dev: Added unit tests for the Discord invite resolver. (#711)
- Dev: Replace github.com/golang/mock with go.uber.org/mock. (#712)
- Dev: Ignore mock files when generating code coverage. (#713)
Expand Down
2 changes: 1 addition & 1 deletion config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
#wikipedia-article-cache-duration: 1h

# Cache duration for Twitch username links
#twitch-username-cache-duration: 1h
#twitch-username-cache-duration: 10m

# Minimum level of log message importance required for the log message to not be filtered out.
# Available levels: debug, info, warn, error
Expand Down
15 changes: 15 additions & 0 deletions internal/mocks/mock_TwitchAPIClient.go

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

20 changes: 17 additions & 3 deletions internal/resolvers/twitch/initialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import (

type TwitchAPIClient interface {
GetClips(params *helix.ClipsParams) (clip *helix.ClipsResponse, err error)
GetUsers(params *helix.UsersParams) (clip *helix.UsersResponse, err error)
GetUsers(params *helix.UsersParams) (user *helix.UsersResponse, err error)
GetStreams(params *helix.StreamsParams) (stream *helix.StreamsResponse, err error)
}

const (
Expand All @@ -36,13 +37,26 @@ const (
`<b>Created:</b> {{.CreatedAt}}<br>` +
`<b>URL:</b> {{.URL}}` +
`</div>`

twitchUserLiveTooltipString = `<div style="text-align: left;">` +
`<b>{{.Name}} - Twitch</b><br>` +
`{{.Description}}<br>` +
`<b>Created:</b> {{.CreatedAt}}<br>` +
`<b>URL:</b> {{.URL}}<br>` +
`<b><span style="color: #ff0000;">Live</span></b><br>` +
`<b>Title</b>: {{.Title}}<br>` +
`<b>Game</b>: {{.Game}}<br>` +
`<b>Viewers</b>: {{.Viewers}}<br>` +
`<b>Uptime</b>: {{.Uptime}}` +
`</div>`
)

var (
errInvalidTwitchClip = errors.New("invalid Twitch clip link")

twitchClipsTooltip = template.Must(template.New("twitchclipsTooltip").Parse(twitchClipsTooltipString))
twitchUserTooltip = template.Must(template.New("twitchUserTooltip").Parse(twitchUserTooltipString))
twitchClipsTooltip = template.Must(template.New("twitchclipsTooltip").Parse(twitchClipsTooltipString))
twitchUserTooltip = template.Must(template.New("twitchUserTooltip").Parse(twitchUserTooltipString))
twitchUserLiveTooltip = template.Must(template.New("twitchUserLiveTooltip").Parse(twitchUserLiveTooltipString))

domains = map[string]struct{}{
"twitch.tv": {},
Expand Down
56 changes: 52 additions & 4 deletions internal/resolvers/twitch/user_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ type twitchUserTooltipData struct {
URL string
}

type twitchUserLiveTooltipData struct {
Name string
CreatedAt string
Description string
URL string
Title string
Game string
Viewers string
Uptime string
}

type UserLoader struct {
helixAPI TwitchAPIClient
}
Expand Down Expand Up @@ -50,15 +61,25 @@ func (l *UserLoader) Load(ctx context.Context, login string, r *http.Request) (*

var user = response.Data.Users[0]

var name string
streamResponse, err := l.helixAPI.GetStreams(&helix.StreamsParams{UserLogins: []string{login}})
if err != nil || len(streamResponse.Data.Streams) == 0 {
return userResponse(login, user)
}

return userLiveResponse(login, user, streamResponse.Data.Streams[0])
}

func buildName(login string, user helix.User) string {
if strings.ToLower(user.DisplayName) == login {
name = user.DisplayName
return user.DisplayName
} else {
name = fmt.Sprintf("%s (%s)", user.DisplayName, user.Login)
return fmt.Sprintf("%s (%s)", user.DisplayName, user.Login)
}
}

func userResponse(login string, user helix.User) (*resolver.Response, time.Duration, error) {
data := twitchUserTooltipData{
Name: name,
Name: buildName(login, user),
CreatedAt: humanize.CreationDate(user.CreatedAt.Time),
Description: user.Description,
URL: fmt.Sprintf("https://twitch.tv/%s", user.Login),
Expand All @@ -75,3 +96,30 @@ func (l *UserLoader) Load(ctx context.Context, login string, r *http.Request) (*
Thumbnail: user.ProfileImageURL,
}, cache.NoSpecialDur, nil
}

func userLiveResponse(login string, user helix.User, stream helix.Stream) (*resolver.Response, time.Duration, error) {
data := twitchUserLiveTooltipData{
Name: buildName(login, user),
CreatedAt: humanize.CreationDate(user.CreatedAt.Time),
Description: user.Description,
URL: fmt.Sprintf("https://twitch.tv/%s", user.Login),
Title: stream.Title,
Game: stream.GameName,
Viewers: humanize.Number(uint64(stream.ViewerCount)),
Uptime: humanize.Duration(time.Since(stream.StartedAt)),
}

var tooltip bytes.Buffer
if err := twitchUserLiveTooltip.Execute(&tooltip, data); err != nil {
return resolver.Errorf("Twitch user template error: %s", err)
}

thumbnail := strings.ReplaceAll(stream.ThumbnailURL, "{width}", "1280")
thumbnail = strings.ReplaceAll(thumbnail, "{height}", "720")

return &resolver.Response{
Status: 200,
Tooltip: url.PathEscape(tooltip.String()),
Thumbnail: thumbnail,
}, cache.NoSpecialDur, nil
}
102 changes: 92 additions & 10 deletions internal/resolvers/twitch/user_resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package twitch

import (
"context"
"errors"
"net/http"
"net/url"
"testing"
Expand Down Expand Up @@ -73,15 +74,17 @@ func TestUserResolver(t *testing.T) {
c.Run("Run", func(c *qt.C) {
c.Run("Not cached", func(c *qt.C) {
type runTest struct {
label string
inputURL *url.URL
login string
inputReq *http.Request
expectedUsersResponse *helix.UsersResponse
expectedUserError error
expectedResponse *cache.Response
expectedError error
rowsReturned int
label string
inputURL *url.URL
login string
inputReq *http.Request
expectedUsersResponse *helix.UsersResponse
expectedUserError error
expectedStreamsResponse *helix.StreamsResponse
expectedStreamsError error
expectedResponse *cache.Response
expectedError error
rowsReturned int
}

tests := []runTest{
Expand All @@ -93,7 +96,7 @@ func TestUserResolver(t *testing.T) {
expectedUsersResponse: &helix.UsersResponse{
Data: helix.ManyUsers{
Users: []helix.User{
helix.User{
{
Login: "twitch",
DisplayName: "Twitch",
CreatedAt: helix.Time{
Expand All @@ -106,18 +109,97 @@ func TestUserResolver(t *testing.T) {
},
},
expectedUserError: nil,
expectedStreamsResponse: &helix.StreamsResponse{
Data: helix.ManyStreams{
Streams: []helix.Stream{},
},
},
expectedStreamsError: nil,
expectedResponse: &cache.Response{
Payload: []byte(`{"status":200,"thumbnail":"https://example.com/thumbnail.png","tooltip":"%3Cdiv%20style=%22text-align:%20left%3B%22%3E%3Cb%3ETwitch%20-%20Twitch%3C%2Fb%3E%3Cbr%3ETwitch%20is%20where%20thousands%20of%20communities%20come%20together%20for%20whatever%2C%20every%20day.%20%3Cbr%3E%3Cb%3ECreated:%3C%2Fb%3E%2022%20May%202007%3Cbr%3E%3Cb%3EURL:%3C%2Fb%3E%20https:%2F%2Ftwitch.tv%2Ftwitch%3C%2Fdiv%3E"}`),
StatusCode: http.StatusOK,
ContentType: "application/json",
},
expectedError: nil,
},
{
label: "twitch stream error",
inputURL: utils.MustParseURL("https://twitch.tv/twitch"),
login: "twitch",
inputReq: nil,
expectedUsersResponse: &helix.UsersResponse{
Data: helix.ManyUsers{
Users: []helix.User{
{
Login: "twitch",
DisplayName: "Twitch",
CreatedAt: helix.Time{
Time: time.Date(2007, 5, 22, 0, 0, 0, 0, time.UTC),
},
Description: "Twitch is where thousands of communities come together for whatever, every day. ",
ProfileImageURL: "https://example.com/thumbnail.png",
},
},
},
},
expectedUserError: nil,
expectedStreamsResponse: nil,
expectedStreamsError: errors.New("error"),
expectedResponse: &cache.Response{
Payload: []byte(`{"status":200,"thumbnail":"https://example.com/thumbnail.png","tooltip":"%3Cdiv%20style=%22text-align:%20left%3B%22%3E%3Cb%3ETwitch%20-%20Twitch%3C%2Fb%3E%3Cbr%3ETwitch%20is%20where%20thousands%20of%20communities%20come%20together%20for%20whatever%2C%20every%20day.%20%3Cbr%3E%3Cb%3ECreated:%3C%2Fb%3E%2022%20May%202007%3Cbr%3E%3Cb%3EURL:%3C%2Fb%3E%20https:%2F%2Ftwitch.tv%2Ftwitch%3C%2Fdiv%3E"}`),
StatusCode: http.StatusOK,
ContentType: "application/json",
},
expectedError: nil,
},
{
label: "twitch live",
inputURL: utils.MustParseURL("https://twitch.tv/twitch"),
login: "twitch",
inputReq: nil,
expectedUsersResponse: &helix.UsersResponse{
Data: helix.ManyUsers{
Users: []helix.User{
{
Login: "twitch",
DisplayName: "Twitch",
CreatedAt: helix.Time{
Time: time.Date(2007, 5, 22, 0, 0, 0, 0, time.UTC),
},
Description: "Twitch is where thousands of communities come together for whatever, every day. ",
ProfileImageURL: "https://example.com/thumbnail.png",
},
},
},
},
expectedUserError: nil,
expectedStreamsResponse: &helix.StreamsResponse{
Data: helix.ManyStreams{
Streams: []helix.Stream{
{
Title: "title",
GameName: "Just Chatting",
ViewerCount: 1234,
StartedAt: time.Now(),
ThumbnailURL: "https://example.com/thumbnail_{width}x{height}.png",
},
},
},
},
expectedStreamsError: nil,
expectedResponse: &cache.Response{
Payload: []byte(`{"status":200,"thumbnail":"https://example.com/thumbnail_1280x720.png","tooltip":"%3Cdiv%20style=%22text-align:%20left%3B%22%3E%3Cb%3ETwitch%20-%20Twitch%3C%2Fb%3E%3Cbr%3ETwitch%20is%20where%20thousands%20of%20communities%20come%20together%20for%20whatever%2C%20every%20day.%20%3Cbr%3E%3Cb%3ECreated:%3C%2Fb%3E%2022%20May%202007%3Cbr%3E%3Cb%3EURL:%3C%2Fb%3E%20https:%2F%2Ftwitch.tv%2Ftwitch%3Cbr%3E%3Cb%3E%3Cspan%20style=%22color:%20%23ff0000%3B%22%3ELive%3C%2Fspan%3E%3C%2Fb%3E%3Cbr%3E%3Cb%3ETitle%3C%2Fb%3E:%20title%3Cbr%3E%3Cb%3EGame%3C%2Fb%3E:%20Just%20Chatting%3Cbr%3E%3Cb%3EViewers%3C%2Fb%3E:%201%2C234%3Cbr%3E%3Cb%3EUptime%3C%2Fb%3E:%2000:00:00%3C%2Fdiv%3E"}`),
StatusCode: http.StatusOK,
ContentType: "application/json",
},
expectedError: nil,
},
}

for _, test := range tests {
c.Run(test.label, func(c *qt.C) {
helixClient.EXPECT().GetUsers(&helix.UsersParams{Logins: []string{test.login}}).Times(1).Return(test.expectedUsersResponse, test.expectedUserError)
helixClient.EXPECT().GetStreams(&helix.StreamsParams{UserLogins: []string{test.login}}).Times(1).Return(test.expectedStreamsResponse, test.expectedStreamsError)
pool.ExpectQuery("SELECT").WillReturnError(pgx.ErrNoRows)
pool.ExpectExec("INSERT INTO cache").
WithArgs("twitch:user:"+test.login, test.expectedResponse.Payload, test.expectedResponse.StatusCode, test.expectedResponse.ContentType, pgxmock.AnyArg()).
Expand Down
2 changes: 1 addition & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func init() {
pflag.Uint64("max-content-length", 5*1024*1024, "Max content size in bytes - requests with body bigger than this value will be skipped")
pflag.Bool("enable-animated-thumbnails", true, "When enabled, will attempt to use libvips library to build animated thumbnails. Can increase CPU usage and cache storage by a lot. Enabled by default")
pflag.Uint("max-thumbnail-size", 300, "Maximum width/height pixel size count of the thumbnails sent to the clients.")
pflag.Duration("twitch-username-cache-duration", 1*time.Hour, "Cache timeout for twitch usernames")
pflag.Duration("twitch-username-cache-duration", 10*time.Minute, "Cache timeout for twitch usernames")
pflag.Duration("bttv-emote-cache-duration", 1*time.Hour, "Cache timeout for bttv emotes")
pflag.Duration("thumbnail-cache-duration", 10*time.Minute, "Cache timeout for default thumbnails")
pflag.Duration("default-link-cache-duration", 10*time.Minute, "Cache timeout for default links")
Expand Down

0 comments on commit 193fe97

Please sign in to comment.