Skip to content

Commit

Permalink
feat: generate multi-image thumbnails for multi-image tweets (#373)
Browse files Browse the repository at this point in the history
  • Loading branch information
leon-richardt committed Oct 15, 2022
1 parent b855cd0 commit dbc34af
Show file tree
Hide file tree
Showing 34 changed files with 801 additions and 135 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- Breaking: Go version 1.17 is now the minimum required version to build this. (#292)
- Breaking: Thumbnail generation now requires libvips. See [docs/build.md](./docs/build.md) for prerequisite instructions. (#366, #369)
- Breaking: Resolver caches are now stored in PostgreSQL. See [docs/build.md](./docs/build.md) for prerequisite instructions. (#271)
- Twitter: Generate thumbnails with all images of a tweet. (#373)
- YouTube: Added support for 'YouTube shorts' URLs. (#299)
- Fix: SevenTV emotes now resolve correctly. (#281, #288, #307)
- Fix: YouTube videos are no longer resolved as channels. (#284)
Expand Down
4 changes: 3 additions & 1 deletion internal/caches/twitchusernamecache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,7 @@ func New(ctx context.Context, cfg config.APIConfig, pool db.Pool, helixClient *h
helixClient: helixClient,
}

return cache.NewPostgreSQLCache(ctx, cfg, pool, "twitch:username", usernameLoader, 1*time.Hour)
return cache.NewPostgreSQLCache(
ctx, cfg, pool, cache.NewPrefixKeyProvider("twitch:username"), usernameLoader, 1*time.Hour,
)
}
2 changes: 1 addition & 1 deletion internal/db/pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
)

type Pool interface {
Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error)
QueryRow(ctx context.Context, sql string, args ...interface{}) pgx.Row
Exec(ctx context.Context, sql string, arguments ...interface{}) (pgconn.CommandTag, error)
Ping(ctx context.Context) error
Expand All @@ -21,7 +22,6 @@ type Pool interface {

func NewPool(ctx context.Context, dsn string) (Pool, error) {
pool, err := pgxpool.Connect(ctx, dsn)

if err != nil {
return nil, fmt.Errorf("error connecting to pool: %w", err)
}
Expand Down
45 changes: 45 additions & 0 deletions internal/migration/3_dependent_cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//go:build !test || migrationtest

package migration

import (
"context"

"github.com/jackc/pgx/v4"
)

func init() {
// The version of this migration
const migrationVersion = 3

Register(
migrationVersion,
func(ctx context.Context, tx pgx.Tx) error {
// The Up action of this migration
_, err := tx.Exec(ctx, `
CREATE TABLE dependent_values (
key TEXT UNIQUE NOT NULL,
parent_key TEXT NOT NULL,
value bytea NOT NULL,
http_content_type TEXT NOT NULL,
committed BOOLEAN NOT NULL DEFAULT FALSE,
expiration_timestamp TIMESTAMP NOT NULL
);
CREATE INDEX idx_dependent_values_key ON dependent_values(key);
CREATE INDEX idx_dependent_values_parent_entry_key ON dependent_values(parent_key);
CREATE INDEX idx_dependent_values_committed ON dependent_values(committed);
`)

return err
},
func(ctx context.Context, tx pgx.Tx) error {
// The Down action of this migration
_, err := tx.Exec(ctx, `
DROP TABLE dependent_values;
`)

return err
},
)
}
4 changes: 3 additions & 1 deletion internal/resolvers/betterttv/emote_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ func NewEmoteResolver(ctx context.Context, cfg config.APIConfig, pool db.Pool, e
emoteLoader := NewEmoteLoader(emoteAPIURL)

r := &EmoteResolver{
emoteCache: cache.NewPostgreSQLCache(ctx, cfg, pool, "betterttv:emote", resolver.NewResponseMarshaller(emoteLoader), 1*time.Hour),
emoteCache: cache.NewPostgreSQLCache(
ctx, cfg, pool, cache.NewPrefixKeyProvider("betterttv:emote"),
resolver.NewResponseMarshaller(emoteLoader), 1*time.Hour),
}

return r
Expand Down
6 changes: 3 additions & 3 deletions internal/resolvers/default/initialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,16 @@ const (
<b>URL:</b> {{.URL}}</div>`
)

var (
defaultTooltip = template.Must(template.New("default_tooltip").Parse(defaultTooltipString))
)
var defaultTooltip = template.Must(template.New("default_tooltip").Parse(defaultTooltipString))

func Initialize(ctx context.Context, cfg config.APIConfig, pool db.Pool, router *chi.Mux, helixClient *helix.Client) {
defaultLinkResolver := New(ctx, cfg, pool, helixClient)

cached := stampede.Handler(512, 10*time.Second)
imageCached := stampede.Handler(256, 2*time.Second)
generatedValuesCached := stampede.Handler(256, 2*time.Second)

router.With(cached).Get("/link_resolver/{url}", defaultLinkResolver.HandleRequest)
router.With(imageCached).Get("/thumbnail/{url}", defaultLinkResolver.HandleThumbnailRequest)
router.With(generatedValuesCached).Get("/generated/{url}", defaultLinkResolver.HandleGeneratedValueRequest)
}
60 changes: 56 additions & 4 deletions internal/resolvers/default/link_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type LinkResolver struct {

linkCache cache.Cache
thumbnailCache cache.Cache
generatedCache cache.DependentCache
}

func (r *LinkResolver) HandleRequest(w http.ResponseWriter, req *http.Request) {
Expand Down Expand Up @@ -148,7 +149,6 @@ func (r *LinkResolver) HandleThumbnailRequest(w http.ResponseWriter, req *http.R
}

response, err := r.thumbnailCache.Get(ctx, url, req)

if err != nil {
log.Errorw("Error in thumbnail request",
"url", url,
Expand All @@ -167,7 +167,50 @@ func (r *LinkResolver) HandleThumbnailRequest(w http.ResponseWriter, req *http.R
}
}

func (r *LinkResolver) HandleGeneratedValueRequest(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
log := logger.FromContext(ctx)

url, err := utils.UnescapeURLArgument(req, "url")
if err != nil {
_, err = resolver.WriteInvalidURL(w)
if err != nil {
log.Errorw("Error writing response",
"error", err,
)
}
return
}

payload, contentType, err := r.generatedCache.Get(ctx, url)
if err != nil {
log.Errorw("Error in request for generated value",
"url", url,
"error", err,
)
return
}

if payload == nil {
log.Warnw("Requested generated value does not exist",
"url", url,
)
return
}

w.Header().Add("Content-Type", contentType)
w.WriteHeader(http.StatusOK)
_, err = w.Write(payload)
if err != nil {
log.Errorw("Error writing response",
"error", err,
)
}
}

func New(ctx context.Context, cfg config.APIConfig, pool db.Pool, helixClient *helix.Client) *LinkResolver {
generatedCache := cache.NewPostgreSQLDependentCache(ctx, cfg, pool, cache.NewPrefixKeyProvider("default:dependent"))

customResolvers := []resolver.Resolver{}

// Register Link Resolvers from internal/resolvers/
Expand All @@ -179,7 +222,7 @@ func New(ctx context.Context, cfg config.APIConfig, pool db.Pool, helixClient *h
oembed.Initialize(ctx, cfg, pool, &customResolvers)
supinic.Initialize(ctx, cfg, pool, &customResolvers)
twitch.Initialize(ctx, cfg, pool, helixClient, &customResolvers)
twitter.Initialize(ctx, cfg, pool, &customResolvers)
twitter.Initialize(ctx, cfg, pool, &customResolvers, generatedCache)
wikipedia.Initialize(ctx, cfg, pool, &customResolvers)
youtube.Initialize(ctx, cfg, pool, &customResolvers)
seventv.Initialize(ctx, cfg, pool, &customResolvers)
Expand All @@ -195,11 +238,20 @@ func New(ctx context.Context, cfg config.APIConfig, pool db.Pool, helixClient *h
enableLilliput: cfg.EnableLilliput,
}

thumbnailCache := cache.NewPostgreSQLCache(
ctx, cfg, pool, cache.NewPrefixKeyProvider("default:thumbnail"), thumbnailLoader,
10*time.Minute,
)
linkCache := cache.NewPostgreSQLCache(
ctx, cfg, pool, cache.NewPrefixKeyProvider("default:link"), linkLoader, 10*time.Minute,
)

r := &LinkResolver{
customResolvers: customResolvers,

linkCache: cache.NewPostgreSQLCache(ctx, cfg, pool, "default:link", linkLoader, 10*time.Minute),
thumbnailCache: cache.NewPostgreSQLCache(ctx, cfg, pool, "default:thumbnail", thumbnailLoader, 10*time.Minute),
linkCache: linkCache,
thumbnailCache: thumbnailCache,
generatedCache: generatedCache,
}

return r
Expand Down
4 changes: 3 additions & 1 deletion internal/resolvers/discord/invite_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ func NewInviteResolver(ctx context.Context, cfg config.APIConfig, pool db.Pool)
// We cache invites longer on purpose as the API is pretty strict with its rate limiting, and the information changes very seldomly anyway
// TODO: Log 429 errors from the loader
r := &InviteResolver{
inviteCache: cache.NewPostgreSQLCache(ctx, cfg, pool, "discord:invite", resolver.NewResponseMarshaller(inviteLoader), 6*time.Hour),
inviteCache: cache.NewPostgreSQLCache(
ctx, cfg, pool, cache.NewPrefixKeyProvider("discord:invite"),
resolver.NewResponseMarshaller(inviteLoader), 6*time.Hour),
}

return r
Expand Down
4 changes: 3 additions & 1 deletion internal/resolvers/frankerfacez/emote_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ func NewEmoteResolver(ctx context.Context, cfg config.APIConfig, pool db.Pool, e
emoteLoader := NewEmoteLoader(emoteAPIURL)

r := &EmoteResolver{
emoteCache: cache.NewPostgreSQLCache(ctx, cfg, pool, "frankerfacez:emote", resolver.NewResponseMarshaller(emoteLoader), 1*time.Hour),
emoteCache: cache.NewPostgreSQLCache(
ctx, cfg, pool, cache.NewPrefixKeyProvider("frankerfacez:emote"),
resolver.NewResponseMarshaller(emoteLoader), 1*time.Hour),
}

return r
Expand Down
4 changes: 3 additions & 1 deletion internal/resolvers/imgur/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ func NewResolver(ctx context.Context, cfg config.APIConfig, pool db.Pool, imgurC
}

r := &Resolver{
imgurCache: cache.NewPostgreSQLCache(ctx, cfg, pool, "imgur", resolver.NewResponseMarshaller(loader), 1*time.Hour),
imgurCache: cache.NewPostgreSQLCache(
ctx, cfg, pool, cache.NewPrefixKeyProvider("imgur"),
resolver.NewResponseMarshaller(loader), 1*time.Hour),
}

return r
Expand Down
4 changes: 3 additions & 1 deletion internal/resolvers/livestreamfails/clip_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ func NewClipResolver(ctx context.Context, cfg config.APIConfig, pool db.Pool, ap
}

r := &ClipResolver{
clipCache: cache.NewPostgreSQLCache(ctx, cfg, pool, "livestreamfails:clip", resolver.NewResponseMarshaller(clipLoader), 1*time.Hour),
clipCache: cache.NewPostgreSQLCache(
ctx, cfg, pool, cache.NewPrefixKeyProvider("livestreamfails:clip"),
resolver.NewResponseMarshaller(clipLoader), 1*time.Hour),
}

return r
Expand Down
7 changes: 5 additions & 2 deletions internal/resolvers/oembed/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,11 @@ func NewResolver(ctx context.Context, cfg config.APIConfig, pool db.Pool, data [
}

r := &Resolver{
oEmbedCache: cache.NewPostgreSQLCache(ctx, cfg, pool, "oembed", resolver.NewResponseMarshaller(loader), 1*time.Hour),
oEmbed: oEmbed,
oEmbedCache: cache.NewPostgreSQLCache(
ctx, cfg, pool, cache.NewPrefixKeyProvider("oembed"),
resolver.NewResponseMarshaller(loader), 1*time.Hour,
),
oEmbed: oEmbed,
}

return r, nil
Expand Down
4 changes: 3 additions & 1 deletion internal/resolvers/seventv/emote_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ func NewEmoteResolver(ctx context.Context, cfg config.APIConfig, pool db.Pool, a
emoteLoader := NewEmoteLoader(cfg, apiURL)

r := &EmoteResolver{
emoteCache: cache.NewPostgreSQLCache(ctx, cfg, pool, "seventv:emote", resolver.NewResponseMarshaller(emoteLoader), 1*time.Hour),
emoteCache: cache.NewPostgreSQLCache(
ctx, cfg, pool, cache.NewPrefixKeyProvider("seventv:emote"),
resolver.NewResponseMarshaller(emoteLoader), 1*time.Hour),
}

return r
Expand Down
4 changes: 3 additions & 1 deletion internal/resolvers/supinic/track_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ func NewTrackResolver(ctx context.Context, cfg config.APIConfig, pool db.Pool) *
trackLoader := &TrackLoader{}

r := &TrackResolver{
trackCache: cache.NewPostgreSQLCache(ctx, cfg, pool, "supinic:track", resolver.NewResponseMarshaller(trackLoader), 1*time.Hour),
trackCache: cache.NewPostgreSQLCache(
ctx, cfg, pool, cache.NewPrefixKeyProvider("supinic:track"),
resolver.NewResponseMarshaller(trackLoader), 1*time.Hour),
}

return r
Expand Down
9 changes: 5 additions & 4 deletions internal/resolvers/twitch/clip_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ import (
"github.com/Chatterino/api/pkg/resolver"
)

var (
clipSlugRegex = regexp.MustCompile(`^\/(\w{2,25}\/clip\/)?(clip\/)?([a-zA-Z0-9]+(?:-[-\w]{16})?)$`)
)
var clipSlugRegex = regexp.MustCompile(`^\/(\w{2,25}\/clip\/)?(clip\/)?([a-zA-Z0-9]+(?:-[-\w]{16})?)$`)

type ClipResolver struct {
clipCache cache.Cache
Expand Down Expand Up @@ -76,7 +74,10 @@ func NewClipResolver(ctx context.Context, cfg config.APIConfig, pool db.Pool, he
}

r := &ClipResolver{
clipCache: cache.NewPostgreSQLCache(ctx, cfg, pool, "twitch:clip", resolver.NewResponseMarshaller(clipLoader), 1*time.Hour),
clipCache: cache.NewPostgreSQLCache(
ctx, cfg, pool, cache.NewPrefixKeyProvider("twitch:clip"),
resolver.NewResponseMarshaller(clipLoader), 1*time.Hour,
),
}

return r
Expand Down
42 changes: 0 additions & 42 deletions internal/resolvers/twitter/api.go

This file was deleted.

Loading

0 comments on commit dbc34af

Please sign in to comment.