10 Commits

12 changed files with 159 additions and 59 deletions

View File

@ -1,3 +1,6 @@
variables:
GOTOOLCHAIN: local
build giphy proxy docker: build giphy proxy docker:
image: docker:stable image: docker:stable
stage: build stage: build

View File

@ -4,14 +4,18 @@
server_name: giphy.example.com server_name: giphy.example.com
# Optionally a custom .well-known response. This defaults to `server_name:443` if empty. # Optionally a custom .well-known response. This defaults to `server_name:443` if empty.
well_known_response: well_known_response:
# The proxy will use MSC3860/MSC3916 media download redirects if the requester supports it.
# Optionally, you can force redirects and not allow proxying at all by setting this to false.
allow_proxy: false
# Matrix server signing key to make the federation tester pass, same format as synapse's .signing.key file. # Matrix server signing key to make the federation tester pass, same format as synapse's .signing.key file.
# You can generate one using `giphyproxy -generate-key`. # You can generate one using `giphyproxy -generate-key`.
server_key: CHANGE ME server_key: CHANGE ME
# Should federation authentication be enforced?
# If true, requests to the /_matrix/federation/v1/media/download/... endpoint
# will check the Authorization: X-Matrix header before redirecting.
federation_auth: false
# Hostname where the proxy should listen on # Hostname where the proxy should listen on
hostname: 0.0.0.0 hostname: 0.0.0.0
# Port where the proxy should listen on # Port where the proxy should listen on
port: 8008 port: 8008
# Redirect destination. This can be changed to serve a different format.
destination: https://i.giphy.com/%s.webp

View File

@ -1,24 +1,28 @@
module go.mau.fi/stickerpicker/giphyproxy module go.mau.fi/stickerpicker/giphyproxy
go 1.22.3 go 1.23.0
toolchain go1.24.4
require ( require (
go.mau.fi/util v0.5.0 go.mau.fi/util v0.8.8
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.19.0-beta.1.0.20240619092812-451658374280 maunium.net/go/mautrix v0.24.2-0.20250629161127-7a7d7f70ef92
) )
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/mux v1.8.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/rs/zerolog v1.33.0 // indirect github.com/rs/zerolog v1.34.0 // indirect
github.com/tidwall/gjson v1.17.1 // indirect github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect github.com/tidwall/sjson v1.2.5 // indirect
golang.org/x/crypto v0.24.0 // indirect golang.org/x/crypto v0.39.0 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect
golang.org/x/net v0.26.0 // indirect golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.21.0 // indirect golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
) )

View File

@ -1,47 +1,54 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
go.mau.fi/util v0.5.0 h1:8yELAl+1CDRrwGe9NUmREgVclSs26Z68pTWePHVxuDo= go.mau.fi/util v0.8.8 h1:OnuEEc/sIJFhnq4kFggiImUpcmnmL/xpvQMRu5Fiy5c=
go.mau.fi/util v0.5.0/go.mod h1:DsJzUrJAG53lCZnnYvq9/mOyLuPScWwYhvETiTrpdP4= go.mau.fi/util v0.8.8/go.mod h1:Y/kS3loxTEhy8Vill513EtPXr+CRDdae+Xj2BXXMy/c=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mautrix v0.19.0-beta.1.0.20240619092812-451658374280 h1:+EHJF8h7obPow7kDnsmGoWN+bTCjHGxCKaH99MldZUI= maunium.net/go/mautrix v0.24.2-0.20250629161127-7a7d7f70ef92 h1:j/QuGIVm5/KPlxMgbViphre7shSwnI7LIjNeiLDSnL4=
maunium.net/go/mautrix v0.19.0-beta.1.0.20240619092812-451658374280/go.mod h1:cxv1w6+syudmEpOewHYIQT9yO7TM5UOWmf6xEBVI4H4= maunium.net/go/mautrix v0.24.2-0.20250629161127-7a7d7f70ef92/go.mod h1:Xy6o+pXmbqmgWsUWh15EQ1eozjC+k/VT/7kloByv9PI=

View File

@ -33,12 +33,14 @@ import (
type Config struct { type Config struct {
mediaproxy.BasicConfig `yaml:",inline"` mediaproxy.BasicConfig `yaml:",inline"`
mediaproxy.ServerConfig `yaml:",inline"` mediaproxy.ServerConfig `yaml:",inline"`
Destination string `yaml:"destination"`
} }
var configPath = flag.String("config", "config.yaml", "config file path") var configPath = flag.String("config", "config.yaml", "config file path")
var generateServerKey = flag.Bool("generate-key", false, "generate a new server key and exit") var generateServerKey = flag.Bool("generate-key", false, "generate a new server key and exit")
var giphyIDRegex = regexp.MustCompile(`^[a-zA-Z0-9-_]+$`) var giphyIDRegex = regexp.MustCompile(`^[a-zA-Z0-9-_]+$`)
var destination = "https://i.giphy.com/%s.webp"
func main() { func main() {
flag.Parse() flag.Parse()
@ -49,13 +51,15 @@ func main() {
var cfg Config var cfg Config
exerrors.PanicIfNotNil(yaml.Unmarshal(cfgFile, &cfg)) exerrors.PanicIfNotNil(yaml.Unmarshal(cfgFile, &cfg))
mp := exerrors.Must(mediaproxy.NewFromConfig(cfg.BasicConfig, getMedia)) mp := exerrors.Must(mediaproxy.NewFromConfig(cfg.BasicConfig, getMedia))
mp.KeyServer.Version.Name = "maunium-stickerpicker giphy proxy" mp.KeyServer.Version.Name = "mautrix-go + maunium-stickerpicker giphy proxy"
mp.ForceProxyLegacyFederation = true if cfg.Destination != "" {
destination = cfg.Destination
}
exerrors.PanicIfNotNil(mp.Listen(cfg.ServerConfig)) exerrors.PanicIfNotNil(mp.Listen(cfg.ServerConfig))
} }
} }
func getMedia(_ context.Context, id string) (response mediaproxy.GetMediaResponse, err error) { func getMedia(_ context.Context, id string, _ map[string]string) (response mediaproxy.GetMediaResponse, err error) {
// This is not related to giphy, but random cats are always fun // This is not related to giphy, but random cats are always fun
if id == "cat" { if id == "cat" {
return &mediaproxy.GetMediaResponseURL{ return &mediaproxy.GetMediaResponseURL{
@ -67,6 +71,6 @@ func getMedia(_ context.Context, id string) (response mediaproxy.GetMediaRespons
return nil, mediaproxy.ErrInvalidMediaIDSyntax return nil, mediaproxy.ErrInvalidMediaIDSyntax
} }
return &mediaproxy.GetMediaResponseURL{ return &mediaproxy.GetMediaResponseURL{
URL: fmt.Sprintf("https://i.giphy.com/%s.webp", id), URL: fmt.Sprintf(destination, id),
}, nil }, nil
} }

View File

@ -50,5 +50,6 @@ setuptools.setup(
entry_points={"console_scripts": [ entry_points={"console_scripts": [
"sticker-import=sticker.stickerimport:cmd", "sticker-import=sticker.stickerimport:cmd",
"sticker-pack=sticker.pack:cmd", "sticker-pack=sticker.pack:cmd",
"sticker-download-thumbnails=sticker.download_thumbnails:cmd",
]}, ]},
) )

View File

@ -0,0 +1,58 @@
# maunium-stickerpicker - A fast and simple Matrix sticker picker widget.
# Copyright (C) 2025 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from pathlib import Path
from typing import Dict
import argparse
import asyncio
import json
from aiohttp import ClientSession
from yarl import URL
from .lib import matrix, util
parser = argparse.ArgumentParser()
parser.add_argument("--config",
help="Path to JSON file with Matrix homeserver and access_token",
type=str, default="config.json", metavar="file")
parser.add_argument("path", help="Path to the sticker pack JSON file", type=str)
async def main(args: argparse.Namespace) -> None:
await matrix.load_config(args.config)
with util.open_utf8(args.path) as pack_file:
pack = json.load(pack_file)
print(f"Loaded existing pack meta from {args.path}")
stickers_data: Dict[str, bytes] = {}
async with ClientSession() as sess:
for sticker in pack["stickers"]:
dl_url = URL(matrix.homeserver_url) / "_matrix/client/v1/media/download" / sticker["url"].removeprefix("mxc://")
print("Downloading", sticker["url"])
async with sess.get(dl_url, headers={"Authorization": f"Bearer {matrix.access_token}"}) as resp:
resp.raise_for_status()
stickers_data[sticker["url"]] = await resp.read()
print("All stickers downloaded, generating thumbnails...")
util.add_thumbnails(pack["stickers"], stickers_data, Path(args.path).parent)
print("Done!")
def cmd():
asyncio.run(main(parser.parse_args()))
if __name__ == "__main__":
cmd()

View File

@ -59,7 +59,7 @@ async def load_config(path: str) -> None:
print("Matrix config file not found. Please enter your homeserver and access token.") print("Matrix config file not found. Please enter your homeserver and access token.")
homeserver_url = input("Homeserver URL: ") homeserver_url = input("Homeserver URL: ")
access_token = input("Access token: ") access_token = input("Access token: ")
whoami_url = URL(homeserver_url) / "_matrix" / "client" / "r0" / "account" / "whoami" whoami_url = URL(homeserver_url) / "_matrix" / "client" / "v3" / "account" / "whoami"
if whoami_url.scheme not in ("https", "http"): if whoami_url.scheme not in ("https", "http"):
whoami_url = whoami_url.with_scheme("https") whoami_url = whoami_url.with_scheme("https")
user_id = await whoami(whoami_url, access_token) user_id = await whoami(whoami_url, access_token)
@ -71,7 +71,7 @@ async def load_config(path: str) -> None:
}, config_file) }, config_file)
print(f"Wrote config to {path}") print(f"Wrote config to {path}")
upload_url = URL(homeserver_url) / "_matrix" / "media" / "r0" / "upload" upload_url = URL(homeserver_url) / "_matrix" / "media" / "v3" / "upload"
async def whoami(url: URL, access_token: str) -> str: async def whoami(url: URL, access_token: str) -> str:

View File

@ -17,6 +17,8 @@ from functools import partial
from io import BytesIO from io import BytesIO
import os.path import os.path
import json import json
from pathlib import Path
from typing import Dict, List
from PIL import Image from PIL import Image
@ -24,19 +26,19 @@ from . import matrix
open_utf8 = partial(open, encoding='UTF-8') open_utf8 = partial(open, encoding='UTF-8')
def convert_image(data: bytes) -> (bytes, int, int): def convert_image(data: bytes, max_w=256, max_h=256) -> (bytes, int, int):
image: Image.Image = Image.open(BytesIO(data)).convert("RGBA") image: Image.Image = Image.open(BytesIO(data)).convert("RGBA")
new_file = BytesIO() new_file = BytesIO()
image.save(new_file, "png") image.save(new_file, "png")
w, h = image.size w, h = image.size
if w > 256 or h > 256: if w > max_w or h > max_h:
# Set the width and height to lower values so clients wouldn't show them as huge images # Set the width and height to lower values so clients wouldn't show them as huge images
if w > h: if w > h:
h = int(h / (w / 256)) h = int(h / (w / max_w))
w = 256 w = max_w
else: else:
w = int(w / (h / 256)) w = int(w / (h / max_h))
h = 256 h = max_h
return new_file.getvalue(), w, h return new_file.getvalue(), w, h
@ -78,3 +80,15 @@ def make_sticker(mxc: str, width: int, height: int, size: int,
}, },
"msgtype": "m.sticker", "msgtype": "m.sticker",
} }
def add_thumbnails(stickers: List[matrix.StickerInfo], stickers_data: Dict[str, bytes], output_dir: str) -> None:
thumbnails = Path(output_dir, "thumbnails")
thumbnails.mkdir(parents=True, exist_ok=True)
for sticker in stickers:
image_data, _, _ = convert_image(stickers_data[sticker["url"]], 128, 128)
name = sticker["url"].split("/")[-1]
thumbnail_path = thumbnails / name
thumbnail_path.write_bytes(image_data)

View File

@ -13,6 +13,7 @@
# #
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from pathlib import Path
from typing import Dict, Optional from typing import Dict, Optional
from hashlib import sha256 from hashlib import sha256
import mimetypes import mimetypes
@ -107,9 +108,11 @@ async def main(args: argparse.Namespace) -> None:
old_stickers = {sticker["id"]: sticker for sticker in pack["stickers"]} old_stickers = {sticker["id"]: sticker for sticker in pack["stickers"]}
pack["stickers"] = [] pack["stickers"] = []
stickers_data: Dict[str, bytes] = {}
for file in sorted(os.listdir(args.path)): for file in sorted(os.listdir(args.path)):
sticker = await upload_sticker(file, args.path, old_stickers=old_stickers) sticker = await upload_sticker(file, args.path, old_stickers=old_stickers)
if sticker: if sticker:
stickers_data[sticker["url"]] = Path(args.path, file).read_bytes()
pack["stickers"].append(sticker) pack["stickers"].append(sticker)
with util.open_utf8(meta_path, "w") as pack_file: with util.open_utf8(meta_path, "w") as pack_file:
@ -122,6 +125,8 @@ async def main(args: argparse.Namespace) -> None:
with util.open_utf8(picker_pack_path, "w") as pack_file: with util.open_utf8(picker_pack_path, "w") as pack_file:
json.dump(pack, pack_file) json.dump(pack, pack_file)
print(f"Copied pack to {picker_pack_path}") print(f"Copied pack to {picker_pack_path}")
util.add_thumbnails(pack["stickers"], stickers_data, args.add_to_index)
util.add_to_index(picker_file_name, args.add_to_index) util.add_to_index(picker_file_name, args.add_to_index)
@ -138,7 +143,7 @@ parser.add_argument("path", help="Path to the sticker pack directory", type=str)
def cmd(): def cmd():
asyncio.get_event_loop().run_until_complete(main(parser.parse_args())) asyncio.run(main(parser.parse_args()))
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -13,7 +13,7 @@
# #
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Dict from typing import Dict, Tuple
import argparse import argparse
import asyncio import asyncio
import os.path import os.path
@ -29,7 +29,7 @@ from telethon.tl.types.messages import StickerSet as StickerSetFull
from .lib import matrix, util from .lib import matrix, util
async def reupload_document(client: TelegramClient, document: Document) -> matrix.StickerInfo: async def reupload_document(client: TelegramClient, document: Document) -> Tuple[matrix.StickerInfo, bytes]:
print(f"Reuploading {document.id}", end="", flush=True) print(f"Reuploading {document.id}", end="", flush=True)
data = await client.download_media(document, file=bytes) data = await client.download_media(document, file=bytes)
print(".", end="", flush=True) print(".", end="", flush=True)
@ -37,7 +37,7 @@ async def reupload_document(client: TelegramClient, document: Document) -> matri
print(".", end="", flush=True) print(".", end="", flush=True)
mxc = await matrix.upload(data, "image/png", f"{document.id}.png") mxc = await matrix.upload(data, "image/png", f"{document.id}.png")
print(".", flush=True) print(".", flush=True)
return util.make_sticker(mxc, width, height, len(data)) return util.make_sticker(mxc, width, height, len(data)), data
def add_meta(document: Document, info: matrix.StickerInfo, pack: StickerSetFull) -> None: def add_meta(document: Document, info: matrix.StickerInfo, pack: StickerSetFull) -> None:
@ -75,15 +75,17 @@ async def reupload_pack(client: TelegramClient, pack: StickerSetFull, output_dir
except FileNotFoundError: except FileNotFoundError:
pass pass
stickers_data: Dict[str, bytes] = {}
reuploaded_documents: Dict[int, matrix.StickerInfo] = {} reuploaded_documents: Dict[int, matrix.StickerInfo] = {}
for document in pack.documents: for document in pack.documents:
try: try:
reuploaded_documents[document.id] = already_uploaded[document.id] reuploaded_documents[document.id] = already_uploaded[document.id]
print(f"Skipped reuploading {document.id}") print(f"Skipped reuploading {document.id}")
except KeyError: except KeyError:
reuploaded_documents[document.id] = await reupload_document(client, document) reuploaded_documents[document.id], data = await reupload_document(client, document)
# Always ensure the body and telegram metadata is correct # Always ensure the body and telegram metadata is correct
add_meta(document, reuploaded_documents[document.id], pack) add_meta(document, reuploaded_documents[document.id], pack)
stickers_data[reuploaded_documents[document.id]["url"]] = data
for sticker in pack.packs: for sticker in pack.packs:
if not sticker.emoticon: if not sticker.emoticon:
@ -107,6 +109,7 @@ async def reupload_pack(client: TelegramClient, pack: StickerSetFull, output_dir
}, pack_file, ensure_ascii=False) }, pack_file, ensure_ascii=False)
print(f"Saved {pack.set.title} as {pack.set.short_name}.json") print(f"Saved {pack.set.title} as {pack.set.short_name}.json")
util.add_thumbnails(list(reuploaded_documents.values()), stickers_data, output_dir)
util.add_to_index(os.path.basename(pack_path), output_dir) util.add_to_index(os.path.basename(pack_path), output_dir)
@ -158,7 +161,7 @@ async def main(args: argparse.Namespace) -> None:
def cmd() -> None: def cmd() -> None:
asyncio.get_event_loop().run_until_complete(main(parser.parse_args())) asyncio.run(main(parser.parse_args()))
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -29,10 +29,8 @@ const params = new URLSearchParams(document.location.search)
if (params.has('config')) { if (params.has('config')) {
INDEX = params.get("config") INDEX = params.get("config")
} }
// This is updated from packs/index.json
let HOMESERVER_URL = "https://matrix-client.matrix.org"
const makeThumbnailURL = mxc => `${HOMESERVER_URL}/_matrix/media/v3/thumbnail/${mxc.slice(6)}?height=128&width=128&method=scale` const makeThumbnailURL = mxc => `${PACKS_BASE_URL}/thumbnails/${mxc.split("/").slice(-1)[0]}`
// We need to detect iOS webkit because it has a bug related to scrolling non-fixed divs // We need to detect iOS webkit because it has a bug related to scrolling non-fixed divs
// This is also used to fix scrolling to sections on Element iOS // This is also used to fix scrolling to sections on Element iOS
@ -165,7 +163,6 @@ class App extends Component {
return return
} }
const indexData = await indexRes.json() const indexData = await indexRes.json()
HOMESERVER_URL = indexData.homeserver_url || HOMESERVER_URL
if (indexData.giphy_api_key !== undefined) { if (indexData.giphy_api_key !== undefined) {
setGiphyAPIKey(indexData.giphy_api_key, indexData.giphy_mxc_prefix) setGiphyAPIKey(indexData.giphy_api_key, indexData.giphy_mxc_prefix)
} }