Skip to content

Commit

Permalink
Use 7TV V3 API (#456)
Browse files Browse the repository at this point in the history
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
  • Loading branch information
Nerixyz and pajlada authored Mar 30, 2023
1 parent 13079e1 commit c2e082c
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 174 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

- Minor: Added support for FrankerFaceZ animated emote links. (#455)
- Fix: 7TV emote images now resolve correctly. (#456)
- Dev: Only upload Ubuntu binaries to releases. (#454)

## 2.0.0
Expand Down
151 changes: 83 additions & 68 deletions internal/resolvers/seventv/data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,103 +2,118 @@ package seventv

import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"

"github.com/go-chi/chi/v5"
)

var (
emotes = map[string]EmoteAPIResponse{}
emotes = map[string]EmoteModel{}
)

func init() {
// Private emote: Pajawalk
emotes["604281c81ae70f000d47ffd9"] = EmoteAPIResponse{
Data: EmoteAPIResponseData{
Emote: &EmoteAPIEmote{
ID: "604281c81ae70f000d47ffd9",
Name: "Pajawalk",
Visibility: EmoteVisibilityPrivate,
Owner: EmoteAPIUser{
ID: "603d10e496832ffa787ca53c",
DisplayName: "durado_",
},
},
emotes["604281c81ae70f000d47ffd9"] = EmoteModel{
ID: "604281c81ae70f000d47ffd9",
Name: "Pajawalk",
Flags: EmoteFlagsPrivate,
Listed: true,
Host: ImageHost{
URL: "//cdn.7tv.app/emote/604281c81ae70f000d47ffd9",
Files: []ImageFile{{Name: "best.avif", Width: 100, Height: 30, Format: ImageFormatAVIF}, {Name: "best.webp", Width: 90, Height: 28, Format: ImageFormatWEBP}},
},
Owner: UserPartialModel{
ID: "603d10e496832ffa787ca53c",
DisplayName: "durado_",
},
}

// Hidden emote: Bedge
emotes["60ae8d9ff39a7552b658b60d"] = EmoteAPIResponse{
Data: EmoteAPIResponseData{
Emote: &EmoteAPIEmote{
ID: "60ae8d9ff39a7552b658b60d",
Name: "Bedge",
Visibility: EmoteVisibilityPrivate | EmoteVisibilityHidden,
Owner: EmoteAPIUser{
ID: "605394d9b4d31e459ff05f40",
DisplayName: "Paruna",
},
},
// Unlisted emote: Bedge
emotes["60ae8d9ff39a7552b658b60d"] = EmoteModel{
ID: "60ae8d9ff39a7552b658b60d",
Name: "Bedge",
Flags: 0,
Listed: false,
Host: ImageHost{
URL: "//cdn.7tv.app/emote/60ae8d9ff39a7552b658b60d",
Files: []ImageFile{{Name: "best.webp", Width: 90, Height: 28, Format: ImageFormatWEBP}},
},
Owner: UserPartialModel{
ID: "605394d9b4d31e459ff05f40",
DisplayName: "Paruna",
},
}

// Global emote: FeelsOkayMan
emotes["6042998c1d4963000d9dae34"] = EmoteAPIResponse{
Data: EmoteAPIResponseData{
Emote: &EmoteAPIEmote{
ID: "6042998c1d4963000d9dae34",
Name: "FeelsOkayMan",
Visibility: EmoteVisibilityGlobal,
Owner: EmoteAPIUser{
ID: "603bb6a596832ffa78e7b27b",
DisplayName: "MegaKill3",
},
},
// Regular emote: monkaE
emotes["603cb219c20d020014423c34"] = EmoteModel{
ID: "603cb219c20d020014423c34",
Name: "monkaE",
Flags: 0,
Listed: true,
Host: ImageHost{
URL: "https://cdn.7tv.app/emote/603cb219c20d020014423c34",
Files: []ImageFile{{Name: "1x.webp", Width: 28, Height: 28, Format: ImageFormatWEBP}, {Name: "best.webp", Width: 128, Height: 128, Format: ImageFormatWEBP}},
},
Owner: UserPartialModel{
ID: "6042058896832ffa785800fe",
DisplayName: "Zhark",
},
}

// Regular emote (in global set): FeelsDankMan
emotes["63071bb9464de28875c52531"] = EmoteModel{
ID: "63071bb9464de28875c52531",
Name: "FeelsDankMan",
Flags: 0,
Listed: true,
Host: ImageHost{
URL: "https://cdn.7tv.app/emote/63071bb9464de28875c52531",
Files: []ImageFile{{Name: "1x.webp", Width: 28, Height: 28, Format: ImageFormatWEBP}, {Name: "best.webp", Width: 128, Height: 128, Format: ImageFormatWEBP}},
},
Owner: UserPartialModel{
ID: "603cb1c696832ffa78cc3bc2",
DisplayName: "clyverE",
},
}

// No visiblity tag emote: monkaE
emotes["603cb219c20d020014423c34"] = EmoteAPIResponse{
Data: EmoteAPIResponseData{
Emote: &EmoteAPIEmote{
ID: "603cb219c20d020014423c34",
Name: "monkaE",
// No visibility, should default to shared
Owner: EmoteAPIUser{
ID: "6042058896832ffa785800fe",
DisplayName: "Zhark",
},
},
// Regular emote, no webp images: Hmm
emotes["60ae3e54259ac5a73e56a426"] = EmoteModel{
ID: "60ae3e54259ac5a73e56a426",
Name: "Hmm",
Flags: 0,
Listed: true,
Host: ImageHost{
URL: "https://cdn.7tv.app/emote/60ae3e54259ac5a73e56a426",
Files: []ImageFile{{Name: "jebaited.webp", Width: 128, Height: 128, Format: ImageFormatAVIF}},
},
Owner: UserPartialModel{
ID: "60772a85a807bed00612d1ee",
DisplayName: "lnsc",
},
}

// No emote
emotes["f0f0f0"] = EmoteAPIResponse{
Data: EmoteAPIResponseData{
Emote: nil,
// Private, unlisted emote: Okayge
emotes["60bcb44f7229037ee386d1ab"] = EmoteModel{
ID: "60bcb44f7229037ee386d1ab",
Name: "Okayge",
Flags: EmoteFlagsPrivate,
Listed: false,
Host: ImageHost{
URL: "//cdn.7tv.app/emote/60bcb44f7229037ee386d1ab",
Files: []ImageFile{{Name: "1x.webp", Width: 28, Height: 28, Format: ImageFormatWEBP}, {Name: "best.webp", Width: 128, Height: 128, Format: ImageFormatWEBP}, {Name: "2x.webp", Width: 42, Height: 42, Format: ImageFormatWEBP}},
},
Owner: UserPartialModel{
ID: "60aeabfff6a2c3b332dd6a35",
DisplayName: "joonwi",
},
}
}

func testServer() *httptest.Server {
r := chi.NewRouter()
r.Post("/v2/gql", func(w http.ResponseWriter, r *http.Request) {
type gqlQuery struct {
Query string `json:"string"`
Variables map[string]string `json:"variables"`
}

var q gqlQuery

xd, _ := io.ReadAll(r.Body)
err := json.Unmarshal(xd, &q)
if err != nil {
panic(err)
}

emoteID := q.Variables["id"]
r.Get("/v3/emotes/{id}", func(w http.ResponseWriter, r *http.Request) {
emoteID := chi.URLParam(r, "id")

if emoteID == "bad" {
w.Header().Set("Content-Type", "application/json")
Expand Down
92 changes: 34 additions & 58 deletions internal/resolvers/seventv/emote_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,33 +29,10 @@ func (l *EmoteLoader) Load(ctx context.Context, emoteHash string, r *http.Reques
"emoteHash", emoteHash,
)

queryMap := map[string]interface{}{
"query": `
query fetchEmote($id: String!) {
emote(id: $id) {
visibility
id
name
owner {
id
display_name
}
}
}`,
"variables": map[string]string{
"id": emoteHash,
},
}

queryBytes, err := json.Marshal(queryMap)
if err != nil {
return resolver.Errorf("SevenTV API request marshal error: %s", err)
}

// Execute SevenTV API request
resp, err := resolver.RequestPOST(l.apiURL, string(queryBytes))
resp, err := resolver.RequestGET(ctx, fmt.Sprintf("%s/%s", l.apiURL, emoteHash))
if err != nil {
return resolver.Errorf("SevenTV API request error: %s", err)
return resolver.Errorf("7TV API request error: %s", err)
}
defer resp.Body.Close()

Expand All @@ -64,60 +41,59 @@ query fetchEmote($id: String!) {
return emoteNotFoundResponse, cache.NoSpecialDur, nil
}

var jsonResponse EmoteAPIResponse
var jsonResponse EmoteModel
if err := json.NewDecoder(resp.Body).Decode(&jsonResponse); err != nil {
return resolver.Errorf("SevenTV API response decode error: %s", err)
}

// API returns Data.Emote as null if the emote wasn't found
if jsonResponse.Data.Emote == nil {
return emoteNotFoundResponse, cache.NoSpecialDur, nil
}

// Determine type of the emote based on visibility flags
visibility := jsonResponse.Data.Emote.Visibility
var emoteType []string

if utils.HasBits(visibility, EmoteVisibilityGlobal) {
emoteType = append(emoteType, "Global")
}

if utils.HasBits(visibility, EmoteVisibilityPrivate) {
emoteType = append(emoteType, "Private")
return resolver.Errorf("7TV API response decode error: %s", err)
}

// Default to Shared emote
if len(emoteType) == 0 {
emoteType = append(emoteType, "Shared")
var emoteType string
if utils.HasBits(int32(jsonResponse.Flags), int32(EmoteFlagsPrivate)) {
emoteType = "Private"
} else {
emoteType = "Shared"
}

// Build tooltip data from the API response
data := TooltipData{
Code: jsonResponse.Data.Emote.Name,
Type: strings.Join(emoteType, " "),
Uploader: jsonResponse.Data.Emote.Owner.DisplayName,
Unlisted: utils.HasBits(visibility, EmoteVisibilityHidden),
Code: jsonResponse.Name,
Type: emoteType,
Uploader: jsonResponse.Owner.DisplayName,
Unlisted: !jsonResponse.Listed,
}

// Build a tooltip using the tooltip template (see tooltipTemplate) with the data we massaged above
var tooltip bytes.Buffer
if err := seventvEmoteTemplate.Execute(&tooltip, data); err != nil {
return resolver.Errorf("SevenTV emote template error: %s", err)
return resolver.Errorf("7TV emote template error: %s", err)
}

var bestFile *ImageFile
var bestWidth int32
for _, file := range jsonResponse.Host.Files {
if file.Format == ImageFormatWEBP && file.Width > bestWidth {
bestFile = &file
bestWidth = file.Width
}
}
var thumbnail string
// Hide thumbnail for unlisted or hidden emotes pajaS
if !data.Unlisted && bestFile != nil {
if strings.HasPrefix(jsonResponse.Host.URL, "//") {
thumbnail = fmt.Sprintf("https:%s/%s", jsonResponse.Host.URL, bestFile.Name)
} else {
thumbnail = fmt.Sprintf("%s/%s", jsonResponse.Host.URL, bestFile.Name)
}
thumbnail = utils.FormatThumbnailURL(l.baseURL, r, thumbnail)
}

// Success
successTooltip := &resolver.Response{
Status: http.StatusOK,
Tooltip: url.PathEscape(tooltip.String()),
Thumbnail: utils.FormatThumbnailURL(l.baseURL, r, fmt.Sprintf(thumbnailFormat, emoteHash)),
Thumbnail: thumbnail,
Link: fmt.Sprintf("https://7tv.app/emotes/%s", emoteHash),
}

// Hide thumbnail for unlisted or hidden emotes pajaS
if data.Unlisted {
successTooltip.Thumbnail = ""
}

return successTooltip, cache.NoSpecialDur, nil
}

Expand Down
Loading

0 comments on commit c2e082c

Please sign in to comment.