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

Use 7TV V3 API #456

Merged
merged 7 commits into from
Mar 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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",
Nerixyz marked this conversation as resolved.
Show resolved Hide resolved
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