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

feat: generate multi-image thumbnails for multi-image tweets #373

Merged
merged 6 commits into from
Oct 15, 2022
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 @@ -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