-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added rich tooltips for 7tv emote links (#155)
Co-authored-by: Leon Richardt <leon.richardt@gmail.com> Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
- Loading branch information
1 parent
292442b
commit 60f6284
Showing
12 changed files
with
477 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package seventv | ||
|
||
import ( | ||
"net/url" | ||
|
||
"github.com/Chatterino/api/pkg/resolver" | ||
) | ||
|
||
func check(url *url.URL) bool { | ||
if match, _ := resolver.MatchesHosts(url, domains); !match { | ||
return false | ||
} | ||
|
||
return emotePathRegex.MatchString(url.Path) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
package seventv | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
"io/ioutil" | ||
"log" | ||
"net/http" | ||
"net/url" | ||
"strings" | ||
"time" | ||
|
||
"github.com/Chatterino/api/pkg/cache" | ||
"github.com/Chatterino/api/pkg/resolver" | ||
"github.com/Chatterino/api/pkg/utils" | ||
) | ||
|
||
func load(emoteHash string, r *http.Request) (interface{}, time.Duration, error) { | ||
log.Println("[SevenTV] GET", 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.Response{ | ||
Status: http.StatusInternalServerError, | ||
Message: "SevenTV API request error" + resolver.CleanResponse(err.Error()), | ||
}, cache.NoSpecialDur, nil | ||
} | ||
|
||
// Execute SevenTV API request | ||
resp, err := resolver.RequestPOST(seventvAPIURL, string(queryBytes)) | ||
if err != nil { | ||
return &resolver.Response{ | ||
Status: http.StatusInternalServerError, | ||
Message: "SevenTV API request error" + resolver.CleanResponse(err.Error()), | ||
}, cache.NoSpecialDur, nil | ||
} | ||
defer resp.Body.Close() | ||
|
||
// Read response into a string | ||
body, err := ioutil.ReadAll(resp.Body) | ||
if err != nil { | ||
return &resolver.Response{ | ||
Status: http.StatusInternalServerError, | ||
Message: "SevenTV API http body read error " + resolver.CleanResponse(err.Error()), | ||
}, cache.NoSpecialDur, nil | ||
} | ||
|
||
// Error out if the emote wasn't found or something else went wrong with the request | ||
if resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusMultipleChoices { | ||
return emoteNotFoundResponse, cache.NoSpecialDur, nil | ||
} | ||
|
||
var jsonResponse EmoteAPIResponse | ||
if err := json.Unmarshal(body, &jsonResponse); err != nil { | ||
return &resolver.Response{ | ||
Status: http.StatusInternalServerError, | ||
Message: "SevenTV API unmarshal error " + resolver.CleanResponse(err.Error()), | ||
}, cache.NoSpecialDur, nil | ||
} | ||
|
||
// 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") | ||
} | ||
|
||
// Default to Shared emote | ||
if len(emoteType) == 0 { | ||
emoteType = append(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), | ||
} | ||
|
||
// 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.Response{ | ||
Status: http.StatusInternalServerError, | ||
Message: "SevenTV emote template error " + resolver.CleanResponse(err.Error()), | ||
}, cache.NoSpecialDur, nil | ||
} | ||
|
||
// Success | ||
successTooltip := &resolver.Response{ | ||
Status: http.StatusOK, | ||
Tooltip: url.PathEscape(tooltip.String()), | ||
Thumbnail: utils.FormatThumbnailURL(baseURL, r, fmt.Sprintf(thumbnailFormat, emoteHash)), | ||
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
package seventv | ||
|
||
import ( | ||
"encoding/json" | ||
"io/ioutil" | ||
"net/http" | ||
"net/http/httptest" | ||
"net/url" | ||
"testing" | ||
|
||
"github.com/Chatterino/api/pkg/resolver" | ||
"github.com/go-chi/chi/v5" | ||
|
||
qt "github.com/frankban/quicktest" | ||
) | ||
|
||
var ( | ||
emotes = map[string]EmoteAPIResponse{} | ||
) | ||
|
||
func init() { | ||
emotes["Pajawalk"] = EmoteAPIResponse{ | ||
Data: EmoteAPIResponseData{ | ||
Emote: &EmoteAPIEmote{ | ||
ID: "604281c81ae70f000d47ffd9", | ||
Name: "Pajawalk", | ||
Visibility: EmoteVisibilityPrivate, | ||
Owner: EmoteAPIUser{ | ||
ID: "603d10e496832ffa787ca53c", | ||
DisplayName: "durado_", | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
emotes["Hidden"] = EmoteAPIResponse{ | ||
Data: EmoteAPIResponseData{ | ||
Emote: &EmoteAPIEmote{ | ||
ID: "604281c81ae70f000d47ffd9", | ||
Name: "Hidden", | ||
Visibility: EmoteVisibilityPrivate | EmoteVisibilityHidden, | ||
Owner: EmoteAPIUser{ | ||
ID: "603d10e496832ffa787ca53c", | ||
DisplayName: "durado_", | ||
}, | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
func TestFoo(t *testing.T) { | ||
c := qt.New(t) | ||
|
||
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, _ := ioutil.ReadAll(r.Body) | ||
err := json.Unmarshal(xd, &q) | ||
if err != nil { | ||
panic(err) | ||
} | ||
if response, ok := emotes[q.Variables["id"]]; ok { | ||
b, _ := json.Marshal(&response) | ||
|
||
w.Header().Set("Content-Type", "application/json") | ||
w.Write(b) | ||
return | ||
} | ||
|
||
// TODO: return 404 | ||
}) | ||
ts := httptest.NewServer(r) | ||
defer ts.Close() | ||
seventvAPIURL = ts.URL + "/v2/gql" | ||
|
||
type tTest struct { | ||
emoteHash string | ||
expectedTooltip string | ||
} | ||
|
||
tests := []tTest{ | ||
{ | ||
emoteHash: "Pajawalk", | ||
expectedTooltip: `<div style="text-align: left;"> | ||
<b>Pajawalk</b><br> | ||
<b>Private SevenTV Emote</b><br> | ||
<b>By:</b> durado_ | ||
</div>`, | ||
}, | ||
{ | ||
emoteHash: "Hidden", | ||
expectedTooltip: `<div style="text-align: left;"> | ||
<b>Hidden</b><br> | ||
<b>Private SevenTV Emote</b><br> | ||
<b>By:</b> durado_ | ||
<li><b><span style="color: red;">UNLISTED</span></b></li> | ||
</div>`, | ||
}, | ||
// TODO: Global emote | ||
// TODO: Private emote | ||
// TODO: Combined emote types | ||
// TODO: Default emote type (shared) | ||
// TODO: Thumbnails | ||
// TODO: emote not found (404) | ||
} | ||
|
||
request, _ := http.NewRequest(http.MethodPost, "https://7tv.app/test", nil) | ||
|
||
for _, test := range tests { | ||
iret, _, err := load(test.emoteHash, request) | ||
|
||
c.Assert(err, qt.IsNil) | ||
c.Assert(iret, qt.Not(qt.IsNil)) | ||
|
||
response := iret.(*resolver.Response) | ||
|
||
c.Assert(response, qt.Not(qt.IsNil)) | ||
|
||
c.Assert(response.Status, qt.Equals, 200) | ||
|
||
// TODO: check thumbnail | ||
// c.Assert(response.Thumbnail, qt.Equals, fmt.Sprintf(thumbnailFormat, test.emoteHash)) | ||
|
||
// TODO: check error | ||
cleanTooltip, unescapeErr := url.PathUnescape(response.Tooltip) | ||
c.Assert(unescapeErr, qt.IsNil) | ||
|
||
c.Assert(cleanTooltip, qt.Equals, test.expectedTooltip) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package seventv | ||
|
||
type EmoteAPIUser struct { | ||
ID string `json:"id"` | ||
DisplayName string `json:"display_name"` | ||
} | ||
|
||
type EmoteAPIEmote struct { | ||
ID string `json:"id"` | ||
Name string `json:"name"` | ||
Visibility int32 `json:"visibility"` | ||
Owner EmoteAPIUser `json:"owner"` | ||
} | ||
|
||
type EmoteAPIResponseData struct { | ||
Emote *EmoteAPIEmote `json:"emote,omitempty"` | ||
} | ||
|
||
type EmoteAPIResponse struct { | ||
Data EmoteAPIResponseData `json:"data"` | ||
} | ||
|
||
type TooltipData struct { | ||
Code string | ||
Type string | ||
Uploader string | ||
|
||
Unlisted bool | ||
} | ||
|
||
const ( | ||
EmoteVisibilityPrivate int32 = 1 << iota | ||
EmoteVisibilityGlobal | ||
EmoteVisibilityHidden | ||
EmoteVisibilityOverrideBTTV | ||
EmoteVisibilityOverrideFFZ | ||
EmoteVisibilityOverrideTwitchGlobal | ||
EmoteVisibilityOverrideTwitchSubscriber | ||
|
||
EmoteVisibilityAll int32 = (1 << iota) - 1 | ||
) |
Oops, something went wrong.