diff --git a/go.mod b/go.mod index 3345e74e..567b2d00 100644 --- a/go.mod +++ b/go.mod @@ -9,12 +9,12 @@ require ( github.com/docker/go-connections v0.4.0 github.com/gorilla/mux v1.8.0 github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530 - github.com/matrix-org/gomatrixserverlib v0.0.0-20250119093516-0a1b2bafb5cf + github.com/matrix-org/gomatrixserverlib v0.0.0-20250811173135-d6bb4bdbc2ac github.com/matrix-org/util v0.0.0-20221111132719-399730281e66 github.com/sirupsen/logrus v1.9.3 github.com/tidwall/gjson v1.18.0 github.com/tidwall/sjson v1.2.5 - golang.org/x/crypto v0.36.0 + golang.org/x/crypto v0.38.0 golang.org/x/exp v0.0.0-20230905200255-921286631fa9 gonum.org/v1/plot v0.11.0 ) @@ -35,9 +35,11 @@ require ( github.com/go-pdf/fpdf v0.6.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/hashicorp/go-set/v3 v3.0.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/oleiade/lane/v2 v2.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -50,9 +52,9 @@ require ( go.opentelemetry.io/otel/sdk v1.30.0 // indirect go.opentelemetry.io/otel/trace v1.30.0 // indirect golang.org/x/image v0.18.0 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect gotest.tools/v3 v3.0.3 // indirect ) diff --git a/go.sum b/go.sum index 983d9beb..eef584a7 100644 --- a/go.sum +++ b/go.sum @@ -54,8 +54,8 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF0 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= @@ -64,23 +64,29 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjw github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/hashicorp/go-set/v3 v3.0.0 h1:CaJBQvQCOWoftrBcDt7Nwgo0kdpmrKxar/x2o6pV9JA= +github.com/hashicorp/go-set/v3 v3.0.0/go.mod h1:IEghM2MpE5IaNvL+D7X480dfNtxjRXZ6VMpK3C8s2ok= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530 h1:kHKxCOLcHH8r4Fzarl4+Y3K5hjothkVW5z7T1dUM11U= github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s= -github.com/matrix-org/gomatrixserverlib v0.0.0-20250119093516-0a1b2bafb5cf h1:NcRPAlNWXSMrYBOw9oBEX7z5uQxIKA1m/eo51DYQ7KM= -github.com/matrix-org/gomatrixserverlib v0.0.0-20250119093516-0a1b2bafb5cf/go.mod h1:lcYW5K+XQ1MSNUhFJAWXx3oeErkl4f3BohYDboc9vJw= +github.com/matrix-org/gomatrixserverlib v0.0.0-20250811171307-390dbaa8de98 h1:AH19nhwaPYCRddS/s7LgKS+fhntFXg2qG47uFAjwFJ4= +github.com/matrix-org/gomatrixserverlib v0.0.0-20250811171307-390dbaa8de98/go.mod h1:b6KVfDjXjA5Q7vhpOaMqIhFYvu5BuFVZixlNeTV/CLc= +github.com/matrix-org/gomatrixserverlib v0.0.0-20250811173135-d6bb4bdbc2ac h1:iQeN/RdPauAZQQIaUY40eWgxQKUrbWxtuFHARvYV0E4= +github.com/matrix-org/gomatrixserverlib v0.0.0-20250811173135-d6bb4bdbc2ac/go.mod h1:b6KVfDjXjA5Q7vhpOaMqIhFYvu5BuFVZixlNeTV/CLc= github.com/matrix-org/util v0.0.0-20221111132719-399730281e66 h1:6z4KxomXSIGWqhHcfzExgkH3Z3UkIXry4ibJS4Aqz2Y= github.com/matrix-org/util v0.0.0-20221111132719-399730281e66/go.mod h1:iBI1foelCqA09JJgPV0FYz4qA5dUXYOxMi57FxKBdd4= -github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= -github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= +github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 h1:yH0SvLzcbZxcJXho2yh7CqdENGMQe73Cw3woZBpPli0= github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/oleiade/lane/v2 v2.0.0 h1:XW/ex/Inr+bPkLd3O240xrFOhUkTd4Wy176+Gv0E3Qw= +github.com/oleiade/lane/v2 v2.0.0/go.mod h1:i5FBPFAYSWCgLh58UkUGCChjcCzef/MI7PlQm2TKCeg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= @@ -95,6 +101,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= +github.com/shoenig/test v1.11.0 h1:NoPa5GIoBwuqzIviCrnUJa+t5Xb4xi5Z+zODJnIDsEQ= +github.com/shoenig/test v1.11.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -102,8 +110,8 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +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.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -135,8 +143,8 @@ go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -150,20 +158,20 @@ golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -173,13 +181,13 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -188,8 +196,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/tests/v12_test.go b/tests/v12_test.go new file mode 100644 index 00000000..70c86f53 --- /dev/null +++ b/tests/v12_test.go @@ -0,0 +1,1395 @@ +package tests + +import ( + "encoding/json" + "fmt" + "math" + "net/url" + "slices" + "testing" + "time" + + "github.com/matrix-org/complement" + "github.com/matrix-org/complement/b" + "github.com/matrix-org/complement/client" + "github.com/matrix-org/complement/ct" + "github.com/matrix-org/complement/federation" + "github.com/matrix-org/complement/helpers" + "github.com/matrix-org/complement/match" + "github.com/matrix-org/complement/must" + "github.com/matrix-org/complement/runtime" + "github.com/matrix-org/gomatrixserverlib" + "github.com/matrix-org/gomatrixserverlib/fclient" + "github.com/matrix-org/gomatrixserverlib/spec" + "github.com/matrix-org/util" + "github.com/tidwall/gjson" +) + +var maxCanonicalJSONInt = math.Pow(2, 53) - 1 + +const roomVersion12 = "12" + +var V12ServerRoom = federation.ServerRoomImplCustom{ + ProtoEventCreatorFn: Protov12EventCreator, +} + +// Override how Complement makes proto events so we can conditionally disable/enable the inclusion of the create event +// depending on whether we're running in combined mode or not. +// Complement also doesn't set the room version correctly on the ProtoEvent as this was a new addition to GMSL. +func Protov12EventCreator(def federation.ServerRoomImpl, room *federation.ServerRoom, ev federation.Event) (*gomatrixserverlib.ProtoEvent, error) { + var prevEvents interface{} + if ev.PrevEvents != nil { + // We deliberately want to set the prev events. + prevEvents = ev.PrevEvents + } else { + // No other prev events were supplied so we'll just + // use the forward extremities of the room, which is + // the usual behaviour. + prevEvents = room.ForwardExtremities + } + proto := gomatrixserverlib.ProtoEvent{ + SenderID: ev.Sender, + Depth: int64(room.Depth + 1), // depth starts at 1 + Type: ev.Type, + StateKey: ev.StateKey, + RoomID: room.RoomID, + PrevEvents: prevEvents, + AuthEvents: ev.AuthEvents, + Redacts: ev.Redacts, + Version: gomatrixserverlib.MustGetRoomVersion(room.Version), + } + if err := proto.SetContent(ev.Content); err != nil { + return nil, fmt.Errorf("EventCreator: failed to marshal event content: %s - %+v", err, ev.Content) + } + if err := proto.SetUnsigned(ev.Content); err != nil { + return nil, fmt.Errorf("EventCreator: failed to marshal event unsigned: %s - %+v", err, ev.Unsigned) + } + if proto.AuthEvents == nil { + var stateNeeded gomatrixserverlib.StateNeeded + // this does the right thing for v12 + stateNeeded, err := gomatrixserverlib.StateNeededForProtoEvent(&proto) + if err != nil { + return nil, fmt.Errorf("EventCreator: failed to work out auth_events : %s", err) + } + // we never include the create event if the HS supports MSC4291 + stateNeeded.Create = false + proto.AuthEvents = room.AuthEvents(stateNeeded) + } + return &proto, nil +} + +// Test that the creator can kick an admin created both via +// trusted_private_chat and by explicit promotion, including beyond PL100. +// Also checks the creator isn't in the PL event. +func TestMSC4289PrivilegedRoomCreators(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "alice", + }) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "bob", + }) + + kickBob := func(roomID string) { + t.Helper() + alice.MustDo(t, + "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "kick"}, + client.WithJSONBody(t, map[string]any{ + "user_id": bob.UserID, + }), + ) + } + + t.Run("PL event is missing creator in users map", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + }) + content := alice.MustGetStateEventContent(t, roomID, spec.MRoomPowerLevels, "") + must.MatchGJSON(t, content, match.JSONKeyEqual("users", map[string]any{})) + }) + + t.Run("m.room.tombstone needs PL150 in the PL event", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + }) + content := alice.MustGetStateEventContent(t, roomID, spec.MRoomPowerLevels, "") + must.MatchGJSON(t, content, match.JSONKeyEqual("events."+client.GjsonEscape("m.room.tombstone"), 150)) + }) + + t.Run("creator cannot set self in PL event", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + }) + resp := alice.Do(t, "PUT", []string{"_matrix", "client", "v3", "rooms", roomID, "state", spec.MRoomPowerLevels, ""}, client.WithJSONBody(t, map[string]any{ + "users": map[string]int{ + alice.UserID: 100, + }, + })) + must.MatchResponse(t, resp, match.HTTPResponse{ + StatusCode: 400, + }) + }) + + t.Run("creator can kick admin", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "invite": []string{bob.UserID}, + }) + bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + alice.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]interface{}{ + "users": map[string]any{ + bob.UserID: 100, + }, + }, + }) + kickBob(roomID) + }) + t.Run("creator can kick admin above PL100", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "invite": []string{bob.UserID}, + }) + bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + alice.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]interface{}{ + "users": map[string]any{ + bob.UserID: 949342, + }, + }, + }) + kickBob(roomID) + }) + t.Run("creator can kick admin at JSON max value", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "invite": []string{bob.UserID}, + }) + bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + alice.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]interface{}{ + "users": map[string]any{ + bob.UserID: maxCanonicalJSONInt, + }, + }, + }) + kickBob(roomID) + }) + // technically not a MSC4289 thing but implementations may set the creator PL to be + // above the value expressible in canonical JSON to implement "infinite". + t.Run("power level cannot be set beyond max canonical JSON int", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + "invite": []string{bob.UserID}, + }) + bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + resp := alice.Do( + t, "PUT", []string{"_matrix", "client", "v3", "rooms", roomID, "state", spec.MRoomPowerLevels, ""}, + client.WithJSONBody(t, map[string]interface{}{ + "users": map[string]any{ + bob.UserID: maxCanonicalJSONInt + 1, + }, + }), + ) + must.MatchResponse(t, resp, match.HTTPResponse{ + StatusCode: 400, + }) + }) + t.Run("admin with >PL100 cannot kick creator", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "invite": []string{bob.UserID}, + }) + bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + alice.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]interface{}{ + "users": map[string]any{ + bob.UserID: maxCanonicalJSONInt, + }, + }, + }) + resp := bob.Do(t, + "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "kick"}, + client.WithJSONBody(t, map[string]any{ + "user_id": alice.UserID, + }), + ) + must.MatchResponse(t, resp, match.HTTPResponse{ + StatusCode: 403, + JSON: []match.JSON{ + match.JSONKeyEqual("errcode", "M_FORBIDDEN"), + }, + }) + }) + t.Run("admin with >PL100 sorts after the room creator for state resolution", func(t *testing.T) { + srv := federation.NewServer(t, deployment, + federation.HandleKeyRequests(), + federation.HandleMakeSendJoinRequests(), + federation.HandleTransactionRequests(nil, nil), + federation.HandleEventRequests(), + ) + srv.UnexpectedRequestsAreErrors = false + cancel := srv.Listen() + defer cancel() + bob := srv.UserID("bob") + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + }) + room := srv.MustJoinRoom(t, deployment, "hs1", roomID, bob, federation.WithRoomOpts(federation.WithImpl(&V12ServerRoom))) + plEventID := alice.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]interface{}{ + "users": map[string]any{ + bob: 9493420, + }, + }, + }) + room.WaiterForEvent(plEventID).Waitf(t, 5*time.Second, "failed to see PL event giving bob >PL100") + + alice.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomJoinRules, + StateKey: b.Ptr(""), + Content: map[string]any{ + "join_rule": spec.Invite, + }, + }) + // Bob concurrently sets the join rule to 'knock'. + // State resolution will apply power events (join rules) from highest PL to lowest + // so ensure the end result is Bob's 'knock'. + bobJREvent := srv.MustCreateEvent(t, room, federation.Event{ + Type: spec.MRoomJoinRules, + StateKey: b.Ptr(""), + Content: map[string]any{ + "join_rule": spec.Knock, + }, + PrevEvents: []string{plEventID}, + Sender: bob, + }) + room.AddEvent(bobJREvent) + + srv.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{bobJREvent.JSON()}, nil) + alice.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHasEventID(roomID, bobJREvent.EventID())) + joinRuleContent := alice.MustGetStateEventContent(t, roomID, spec.MRoomJoinRules, "") + must.MatchGJSON(t, joinRuleContent, match.JSONKeyEqual("join_rule", "knock")) + }) + // Some servers may apply validation to ensure the creator appears in the power_level_content_override, + // which for v12 rooms is wrong. + t.Run("power_level_content_override can be set", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "invite": []string{bob.UserID}, + "power_level_content_override": map[string]any{ + "users": map[string]int{ + bob.UserID: 100, + }, + }, + }) + plContent := alice.MustGetStateEventContent(t, roomID, spec.MRoomPowerLevels, "") + must.MatchGJSON(t, plContent, match.JSONKeyEqual("users", map[string]float64{ + bob.UserID: 100, + })) + }) + t.Run("power_level_content_override cannot set the room creator", func(t *testing.T) { + resp := alice.CreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "invite": []string{bob.UserID}, + "power_level_content_override": map[string]any{ + "users": map[string]int{ + alice.UserID: 100, + }, + }, + }) + must.MatchResponse(t, resp, match.HTTPResponse{ + StatusCode: 400, + }) + }) +} + +// Check that additional_creators works in the happy case +func TestMSC4289PrivilegedRoomCreators_Additional(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "alice", + }) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "bob", + }) + + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + "creation_content": map[string]any{ + "additional_creators": []string{bob.UserID}, + }, + }) + bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + // we should not be able to kick bob + res := alice.Do(t, + "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "kick"}, + client.WithJSONBody(t, map[string]any{ + "user_id": bob.UserID, + }), + ) + must.MatchResponse(t, res, match.HTTPResponse{ + StatusCode: 403, + JSON: []match.JSON{ + match.JSONKeyEqual("errcode", "M_FORBIDDEN"), + }, + }) + // Bob should be able to do privileged operations like set the room name + bob.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomName, + StateKey: b.Ptr(""), + Content: map[string]any{ + "name": "Bob's room name", + }, + }) + // Bob should not be able to be inserted into content.users in the PL event + // because they are in additional_creators + resp := alice.Do(t, "PUT", []string{"_matrix", "client", "v3", "rooms", roomID, "state", spec.MRoomPowerLevels, ""}, client.WithJSONBody(t, map[string]any{ + "users": map[string]int{ + bob.UserID: 100, + }, + })) + must.MatchResponse(t, resp, match.HTTPResponse{ + StatusCode: 400, + }) +} + +func TestMSC4289PrivilegedRoomCreators_InvitedAreCreators(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "alice", + }) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "bob", + }) + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "trusted_private_chat", + "is_direct": true, + "invite": []string{bob.UserID}, + }) + createContent := alice.MustGetStateEventContent(t, roomID, spec.MRoomCreate, "") + must.MatchGJSON(t, createContent, match.JSONKeyEqual("additional_creators", []string{bob.UserID})) +} + +// Ensure that trusted_private_chat handling doesn't replace additional_creators +func TestMSC4289PrivilegedRoomCreators_AdditionalCreatorsAndInvited(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "alice", + }) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "bob", + }) + charlie := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "charlie", + }) + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "trusted_private_chat", + "is_direct": true, + "invite": []string{bob.UserID}, + "creation_content": map[string]any{ + "additional_creators": []string{charlie.UserID}, + }, + }) + createContent := alice.MustGetStateEventContent(t, roomID, spec.MRoomCreate, "") + must.MatchGJSON(t, createContent, + match.JSONCheckOff("additional_creators", []interface{}{bob.UserID, charlie.UserID}, match.CheckOffMapper(func(r gjson.Result) interface{} { + return r.Str + })), + ) +} + +// Check that 'additional_creators' is validated correctly. +func TestMSC4289PrivilegedRoomCreators_AdditionalValidation(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "alice", + }) + + testCases := []struct { + Name string + AdditionalCreators any + WantSuccess bool + }{ + { + Name: "additional_creators isn't an array", + AdditionalCreators: "not-an-array", + WantSuccess: false, + }, + { + Name: "additional_creators elements aren't strings", + AdditionalCreators: []any{"@foo:example.com", 42}, + WantSuccess: false, + }, + { + Name: "additional_creators elements aren't user ID strings", + AdditionalCreators: []any{"@foo:example.com", "not-a-user-id"}, + WantSuccess: false, + }, + { + Name: "additional_creators elements aren't valid user ID strings (domain)", + AdditionalCreators: []any{"@invalid:dom$ain$.com"}, + WantSuccess: false, + }, + { + Name: "additional_creators are valid", + AdditionalCreators: []any{"@foo:example.com", "@bar:baz.code"}, + WantSuccess: true, + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + resp := alice.CreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + "creation_content": map[string]any{ + "additional_creators": tc.AdditionalCreators, + }, + }) + if tc.WantSuccess { + must.MatchResponse(t, resp, match.HTTPResponse{ + StatusCode: 200, + }) + } else { + must.MatchResponse(t, resp, match.HTTPResponse{ + StatusCode: 400, + }) + } + }) + } +} + +func TestMSC4289PrivilegedRoomCreators_Upgrades(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "alice", + }) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "bob", + }) + charlie := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "charlie", + }) + + testCases := []struct { + name string + initialCreator *client.CSAPI + initialAdditionalCreators []string + initialVersion string + initialUserPLs map[string]int + entitiyDoingUpgrade *client.CSAPI + newAdditionalCreators []string + // assertions + wantAdditionalCreators []string + wantNewUsersMap map[string]int64 + }{ + { + name: "non-creator admins can upgrade v11 rooms to v12", + initialCreator: alice, + initialVersion: "11", + initialUserPLs: map[string]int{ + bob.UserID: 100, + }, + entitiyDoingUpgrade: bob, + wantAdditionalCreators: []string{}, + wantNewUsersMap: map[string]int64{}, + }, + { + name: "non-creator admins can upgrade v11 rooms to v12 with additional moderators", + initialCreator: alice, + initialVersion: "11", + initialUserPLs: map[string]int{ + bob.UserID: 100, + charlie.UserID: 100, + }, + entitiyDoingUpgrade: bob, + wantAdditionalCreators: []string{}, + wantNewUsersMap: map[string]int64{ + charlie.UserID: 100, + }, + }, + { + name: "non-creator admins can upgrade v12 rooms to v12 with different creators", + initialCreator: alice, + initialVersion: roomVersion12, + initialUserPLs: map[string]int{ + bob.UserID: 150, // bob has enough permission to upgrade + }, + entitiyDoingUpgrade: bob, + newAdditionalCreators: []string{charlie.UserID}, + wantAdditionalCreators: []string{charlie.UserID}, + wantNewUsersMap: map[string]int64{}, + }, + { + name: "non-creator admins can upgrade v12 rooms to v12 with different creators with additional moderators", + initialCreator: alice, + initialVersion: roomVersion12, + initialUserPLs: map[string]int{ + bob.UserID: 150, // bob has enough permission to upgrade + charlie.UserID: 50, // gets removed as he will become an additional creator + }, + entitiyDoingUpgrade: bob, + newAdditionalCreators: []string{charlie.UserID}, + wantAdditionalCreators: []string{charlie.UserID}, + wantNewUsersMap: map[string]int64{}, + }, + { + name: "creator admins can upgrade v11 rooms to v12 with additional_creators", + initialCreator: alice, + initialVersion: "11", + initialUserPLs: map[string]int{ + alice.UserID: 100, + bob.UserID: 100, + }, + entitiyDoingUpgrade: alice, + newAdditionalCreators: []string{bob.UserID}, + wantAdditionalCreators: []string{bob.UserID}, + wantNewUsersMap: map[string]int64{}, // both alice and bob are removed as they are now creators. + }, + { + name: "creator admins can upgrade v11 rooms to v12 with additional_creators and moderators", + initialCreator: alice, + initialVersion: "11", + initialUserPLs: map[string]int{ + alice.UserID: 100, + bob.UserID: 100, + charlie.UserID: 50, + }, + entitiyDoingUpgrade: alice, + newAdditionalCreators: []string{bob.UserID}, + wantAdditionalCreators: []string{bob.UserID}, + wantNewUsersMap: map[string]int64{ + charlie.UserID: 50, + }, + }, + } + + for _, tc := range testCases { + createBody := map[string]interface{}{ + "room_version": tc.initialVersion, + "preset": "public_chat", + } + if tc.initialAdditionalCreators != nil { + must.Equal(t, tc.initialVersion, roomVersion12, "can only set additional_creators on v12") + createBody["additional_creators"] = tc.initialAdditionalCreators + } + roomID := tc.initialCreator.MustCreateRoom(t, createBody) + alice.JoinRoom(t, roomID, []spec.ServerName{"hs1"}) + bob.JoinRoom(t, roomID, []spec.ServerName{"hs1"}) + charlie.JoinRoom(t, roomID, []spec.ServerName{"hs1"}) + tc.initialCreator.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]interface{}{ + "users": tc.initialUserPLs, + }, + }) + upgradeBody := map[string]any{ + "new_version": roomVersion12, + } + if tc.newAdditionalCreators != nil { + upgradeBody["additional_creators"] = tc.newAdditionalCreators + } + res := tc.entitiyDoingUpgrade.MustDo(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "upgrade"}, client.WithJSONBody(t, upgradeBody)) + newRoomID := must.ParseJSON(t, res.Body).Get("replacement_room").Str + // New Create event assertions + createContent := tc.entitiyDoingUpgrade.MustGetStateEventContent(t, newRoomID, spec.MRoomCreate, "") + createAssertions := []match.JSON{ + match.JSONKeyEqual("room_version", roomVersion12), + } + if tc.wantAdditionalCreators != nil { + if len(tc.wantAdditionalCreators) > 0 { + createAssertions = append(createAssertions, match.JSONKeyEqual("additional_creators", tc.wantAdditionalCreators)) + } else { + createAssertions = append(createAssertions, match.JSONKeyMissing("additional_creators")) + } + } + must.MatchGJSON( + t, createContent, createAssertions..., + ) + // New PL assertions + plContent := tc.entitiyDoingUpgrade.MustGetStateEventContent(t, newRoomID, spec.MRoomPowerLevels, "") + if tc.wantNewUsersMap != nil { + plContent.Get("users").ForEach(func(key, v gjson.Result) bool { + gotVal := v.Int() + wantVal, ok := tc.wantNewUsersMap[key.Str] + if !ok { + ct.Errorf(t, "%s: upgraded room PL content, user %s has PL %v but want it missing", tc.name, key.Str, gotVal) + return true + } + if gotVal != wantVal { + ct.Errorf(t, "%s: upgraded room PL content, user %s has PL %v want %v", tc.name, key.Str, gotVal, wantVal) + } + delete(tc.wantNewUsersMap, key.Str) + return true + }) + if len(tc.wantNewUsersMap) > 0 { + ct.Errorf(t, "%s: upgraded room PL content missed these users %v", tc.name, tc.wantNewUsersMap) + } + } + t.Logf("OK: %v", tc.name) + } +} + +// Test that the room ID is in fact the hash of the create event. +func TestMSC4291RoomIDAsHashOfCreateEvent(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + }) + assertCreateEventIsRoomID(t, alice, roomID) +} + +func TestMSC4291RoomIDAsHashOfCreateEvent_AuthEventsOmitsCreateEvent(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + }) + + srv := federation.NewServer(t, deployment, + federation.HandleKeyRequests(), + federation.HandleMakeSendJoinRequests(), + federation.HandleTransactionRequests(nil, nil), + federation.HandleEventRequests(), + ) + srv.UnexpectedRequestsAreErrors = false + cancel := srv.Listen() + defer cancel() + bob := srv.UserID("bob") + + room := srv.MustJoinRoom(t, deployment, "hs1", roomID, bob, federation.WithRoomOpts(federation.WithImpl(&V12ServerRoom))) + + createEvent := room.CurrentState(spec.MRoomCreate, "") + if createEvent == nil { + ct.Fatalf(t, "missing create event from /send_join response") + } + t.Logf("Create event is %s", createEvent.EventID()) + createEventID := createEvent.EventID() + must.Equal(t, + roomID, fmt.Sprintf("!%s", createEventID[1:]), // swap $ for ! + "room ID was not the hash of the create event ID", + ) + + for _, event := range room.Timeline { + rawAuthEvents := gjson.GetBytes(event.JSON(), "auth_events") + must.Equal(t, rawAuthEvents.IsArray(), true, "auth_events key is missing / not an array") + var authEventIDs []string + for _, rawAuthEventID := range rawAuthEvents.Array() { + authEventIDs = append(authEventIDs, rawAuthEventID.Str) + } + t.Logf("create=%v authEventIDs=>%v", createEvent.EventID(), authEventIDs) + if slices.Contains(authEventIDs, createEvent.EventID()) { + ct.Fatalf(t, "Event %s (%s) contains the create event in auth_events: %v", event.EventID(), event.Type(), authEventIDs) + } + must.Equal(t, event.RoomID().String(), roomID, fmt.Sprintf("event %s room ID mismatch: got %v want %v", event.EventID(), event.RoomID(), roomID)) + } +} + +// Test that /upgrade also makes a room where the create event ID is the room ID +func TestMSC4291RoomIDAsHashOfCreateEvent_UpgradedRooms(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "alice"}) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "bob"}) + + testCases := []struct { + initialVersion string + }{ + { + initialVersion: roomVersion12, + }, + { + initialVersion: "11", + }, + { + initialVersion: "10", + }, + } + for _, tc := range testCases { + oldRoomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": tc.initialVersion, + "preset": "public_chat", + }) + bob.MustJoinRoom(t, oldRoomID, []spec.ServerName{"hs1"}) + res := alice.MustDo(t, "POST", []string{ + "_matrix", "client", "v3", "rooms", oldRoomID, "upgrade", + }, client.WithJSONBody(t, map[string]any{ + "new_version": roomVersion12, + })) + newRoomID := gjson.GetBytes(client.ParseJSON(t, res), "replacement_room").Str + t.Logf("upgraded from %s (%s) to %s (%s)", tc.initialVersion, oldRoomID, roomVersion12, newRoomID) + assertCreateEventIsRoomID(t, alice, newRoomID) + tombstoneContent := alice.MustGetStateEventContent(t, oldRoomID, "m.room.tombstone", "") + must.MatchGJSON(t, tombstoneContent, match.JSONKeyEqual("replacement_room", newRoomID)) + createContent := alice.MustGetStateEventContent(t, newRoomID, spec.MRoomCreate, "") + must.MatchGJSON(t, createContent, match.JSONKeyEqual("predecessor.room_id", oldRoomID), match.JSONKeyMissing("predecessor.event_id")) + } +} + +// Ensure that clients cannot send an m.room.create event in an existing room. +func TestMSC4291RoomIDAsHashOfCreateEvent_CannotSendCreateEvent(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) + for _, version := range []string{"11", roomVersion12} { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": version, + }) + resp := alice.Do(t, "PUT", []string{"_matrix", "client", "v3", "rooms", roomID, "state", spec.MRoomCreate, ""}, client.WithJSONBody(t, map[string]any{ + "room_version": version, + // some homeservers may not create a new event if the content exactly matches the prior state, + // so just add some entropy. + "entropy": 100, + })) + must.MatchResponse(t, resp, match.HTTPResponse{StatusCode: 400}) + } +} + +// Test that all CS APIs that return events include the room_id for the create event, +// with the exception of /sync as that always removes room IDs. +func TestMSC4291RoomIDAsHashOfCreateEvent_RoomIDIsOnCreateEvent(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + }) + eventID := alice.SendEventSynced(t, roomID, b.Event{ + Type: "m.room.message", + Content: map[string]interface{}{ + "msgtype": "m.text", + "body": "Hello", + }, + }) + createEventID := "$" + roomID[1:] + + testCases := []struct { + name string + path []string + qps url.Values + extractCreateEvent func(resp gjson.Result) *gjson.Result + }{ + { + name: "/state", + path: []string{"_matrix", "client", "v3", "rooms", roomID, "state"}, + extractCreateEvent: func(resp gjson.Result) *gjson.Result { + for _, ev := range resp.Array() { + if ev.Get("type").Str == spec.MRoomCreate { + return &ev + } + } + return nil + }, + }, + { + name: "/messages", + path: []string{"_matrix", "client", "v3", "rooms", roomID, "messages"}, + qps: url.Values{ + "dir": {"b"}, + "limit": {"100"}, + }, + extractCreateEvent: func(resp gjson.Result) *gjson.Result { + for _, ev := range resp.Get("chunk").Array() { + if ev.Get("type").Str == spec.MRoomCreate { + return &ev + } + } + return nil + }, + }, + { + name: "/event/{eventID}", + path: []string{"_matrix", "client", "v3", "rooms", roomID, "event", createEventID}, + extractCreateEvent: func(resp gjson.Result) *gjson.Result { + return &resp + }, + }, + { + name: "/context direct", + path: []string{"_matrix", "client", "v3", "rooms", roomID, "context", createEventID}, + extractCreateEvent: func(resp gjson.Result) *gjson.Result { + ev := resp.Get("event") + return &ev + }, + }, + { + name: "/context indirect", + qps: url.Values{ + "limit": {"100"}, + }, + path: []string{"_matrix", "client", "v3", "rooms", roomID, "context", eventID}, + extractCreateEvent: func(resp gjson.Result) *gjson.Result { + for _, ev := range resp.Get("events_before").Array() { + if ev.Get("type").Str == spec.MRoomCreate { + return &ev + } + } + return nil + }, + }, + { + name: "/context state", + qps: url.Values{ + "limit": {"100"}, + }, + path: []string{"_matrix", "client", "v3", "rooms", roomID, "context", eventID}, + extractCreateEvent: func(resp gjson.Result) *gjson.Result { + for _, ev := range resp.Get("state").Array() { + if ev.Get("type").Str == spec.MRoomCreate { + return &ev + } + } + return nil + }, + }, + { + name: "/state?format=event", + qps: url.Values{ + "format": {"event"}, + }, + path: []string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.create", ""}, + extractCreateEvent: func(resp gjson.Result) *gjson.Result { + return &resp + }, + }, + } + for _, tc := range testCases { + opts := []client.RequestOpt{} + if tc.qps != nil { + opts = append(opts, client.WithQueries(tc.qps)) + } + resp := alice.MustDo(t, "GET", tc.path, opts...) + body := must.ParseJSON(t, resp.Body) + createEvent := tc.extractCreateEvent(body) + if createEvent == nil { + ct.Errorf(t, "%s: failed to find create event", tc.name) + continue + } + must.Equal(t, createEvent.Get("room_id").Str, roomID, fmt.Sprintf("%s: create event is missing room ID", tc.name)) + } +} + +func assertCreateEventIsRoomID(t ct.TestLike, client *client.CSAPI, roomID string) (createEventID string) { + t.Helper() + res := client.MustDo(t, "GET", []string{ + "_matrix", "client", "v3", "rooms", roomID, "state", + }) + stateEvents := must.ParseJSON(t, res.Body) + stateEvents.ForEach(func(_, value gjson.Result) bool { + if value.Get("type").Str == spec.MRoomCreate && value.Get("state_key").Str == "" { + createEventID = value.Get("event_id").Str + return false + } + return true + }) + if createEventID == "" { + ct.Fatalf(t, "failed to find create event ID from /state respone: %v", stateEvents.Raw) + } + must.Equal(t, + roomID, fmt.Sprintf("!%s", createEventID[1:]), + "room ID was not the hash of the create event ID", + ) + return createEventID +} + +// Test that v2.1 has implemented starting from the empty set not the unconflicted set +// This test assumes a few things about the underlying server implementation: +// - It eventually gives up calling /get_missing_events for some n < 250 and hits /state or /state_ids for the historical state. +// - It does not call /event_auth but may call /event/{eventID} +// - On encountering DAG gaps, the current state is the resolution of all the forwards extremities for each section. +// In other words, the server calculates the current state as the merger of (what_we_knew_before, what_we_know_now), +// despite there being no events with >1 prev_events. +// +// The scenario in this test is similar to the one in the MSC but different in two key ways: +// - To force incorrect state, "Charlie changes display name" happens 250 times to force a /state{_ids} request. +// In the MSC this only happened once. +// - "Bob changes display name" does not exist. We rely on the server calculating the current state as the +// merging of the forwards extremitiy before the gap and the forwards extremity after the gap, so +// in other words we apply state resolution to (Alice leave, 250th Charlie display name change). +func TestMSC4297StateResolutionV2_1_starts_from_empty_set(t *testing.T) { + runtime.SkipIf(t, runtime.Dendrite) // needs additional fixes + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + srv := federation.NewServer(t, deployment, + federation.HandleKeyRequests(), + federation.HandleMakeSendJoinRequests(), + federation.HandleTransactionRequests(nil, nil), + federation.HandleEventRequests(), + ) + srv.UnexpectedRequestsAreErrors = false + cancel := srv.Listen() + defer cancel() + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "alice"}) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "bob"}) + charlie := srv.UserID("charlie") + + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + }) + bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + room := srv.MustJoinRoom(t, deployment, "hs1", roomID, charlie, federation.WithRoomOpts(federation.WithImpl(&V12ServerRoom))) + joinRulePublic := room.CurrentState(spec.MRoomJoinRules, "") + aliceJoin := room.CurrentState(spec.MRoomMember, alice.UserID) + synchronisationEventID := bob.SendEventSynced(t, room.RoomID, b.Event{ + Type: "m.room.message", + Content: map[string]interface{}{ + "msgtype": "m.text", + "body": "can you hear me charlie?", + }, + }) + room.WaiterForEvent(synchronisationEventID).Waitf(t, 5*time.Second, "failed to see synchronisation event, is federation working?") + + // Alice makes the room invite-only then leaves + joinRuleInviteOnlyEventID := alice.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomJoinRules, + StateKey: b.Ptr(""), + Sender: alice.UserID, + Content: map[string]interface{}{ + "join_rule": "invite", + }, + }) + room.WaiterForEvent(joinRuleInviteOnlyEventID).Waitf(t, 5*time.Second, "failed to see invite join rule event") + + alice.MustLeaveRoom(t, roomID) + // Wait for Charlie to see it + time.Sleep(time.Second) + aliceLeaveEvent := room.CurrentState(spec.MRoomMember, alice.UserID) + if membership, err := aliceLeaveEvent.Membership(); err != nil || membership != spec.Leave { + ct.Fatalf(t, "failed to see Alice leave the room, alice event is %s", string(aliceLeaveEvent.JSON())) + } + + // Now only Bob (server under test) and Charlie (Complement server) are left in the room. + // Charlie is going to send an event with unknown prev_event, causing /get_missing_events + // until eventually /state_ids is hit. When it is, we'll return incorrect room state, claiming + // that the current join rule is public, not invite. This will cause the join rules to get conflicted + // and replayed. V2 would base the checks off the unconflicted state, and since all servers agree + // that Alice=leave it would start like that, making the join rule transitions invalid and causing the + // room to have no join rule at all. V2.1 fixes this by loading the auth_events of the event being replayed + // which correctly has Alice joined. Alice isn't automatically re-joined to the room though because the + // last step of the algorithm is to apply the unconflicted state on top of the resolved conflicts, without + // any extra checks. + + // We don't know how far back server impls will go, so let's use 250 as a large enough value. + charlieEvents := make([]gomatrixserverlib.PDU, 250) + eventIDToIndex := make(map[string]int) + for i := range charlieEvents { + ev := srv.MustCreateEvent(t, room, federation.Event{ + Type: spec.MRoomMember, + StateKey: &charlie, + Sender: charlie, + Content: map[string]interface{}{ + "membership": spec.Join, + "displayname": fmt.Sprintf("Charlie %d", i), + }, + }) + room.AddEvent(ev) + charlieEvents[i] = ev + eventIDToIndex[ev.EventID()] = i + } + + seenStateIDs := helpers.NewWaiter() + // process requests to walk back through charlie's display name changes + srv.Mux().HandleFunc( + "/_matrix/federation/v1/get_missing_events/{roomID}", + srv.ValidFederationRequest(t, getMissingEventsHandler(t, room, eventIDToIndex, charlieEvents)), + ) + + getIncorrectState := func(atEventID string) (authChain, stateEvents []gomatrixserverlib.PDU) { + t.Logf("calculating state before event %v (i=%v)", atEventID, eventIDToIndex[atEventID]) + // Find the correct state for charlie + // we want to return the state before the requested event hence -1 + stateEvents = append(stateEvents, charlieEvents[eventIDToIndex[atEventID]-1]) + // charlie's auth events are everything prior to this + authChain = append(authChain, charlieEvents[:eventIDToIndex[atEventID]-1]...) + + stateEvents = append(stateEvents, + room.CurrentState(spec.MRoomCreate, ""), + room.CurrentState(spec.MRoomPowerLevels, ""), + room.CurrentState(spec.MRoomMember, bob.UserID), + joinRulePublic, // This is wrong, and should be invite. + aliceLeaveEvent, + ) + authChain = append(authChain, aliceJoin) + return + } + srv.Mux().HandleFunc( + "/_matrix/federation/v1/state_ids/{roomID}", + srv.ValidFederationRequest(t, func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { + if pathParams["roomID"] != room.RoomID { + t.Errorf("Received /state_ids for the wrong room: %s", room.RoomID) + return util.JSONResponse{ + Code: 400, + JSON: "wrong room", + } + } + defer seenStateIDs.Finish() + req, err := fr.HTTPRequest() + must.NotError(t, "failed to get fed http reqest", err) + atEventID := req.URL.Query().Get("event_id") + t.Logf("received /state_ids at event %v (i=%v)", atEventID, eventIDToIndex[atEventID]) + authChain, stateEvents := getIncorrectState(atEventID) + return util.JSONResponse{ + Code: 200, + JSON: struct { + AuthChainIDs []string `json:"auth_chain_ids"` + PDUIDs []string `json:"pdu_ids"` + }{ + AuthChainIDs: asEventIDs(authChain), + PDUIDs: asEventIDs(stateEvents), + }, + } + }), + ) + srv.Mux().HandleFunc( + "/_matrix/federation/v1/state/{roomID}", + srv.ValidFederationRequest(t, func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { + if pathParams["roomID"] != room.RoomID { + t.Errorf("Received /state for the wrong room: %s", room.RoomID) + return util.JSONResponse{ + Code: 400, + JSON: "wrong room", + } + } + defer seenStateIDs.Finish() + req, err := fr.HTTPRequest() + must.NotError(t, "failed to get fed http reqest", err) + atEventID := req.URL.Query().Get("event_id") + t.Logf("received /state at event %v (i=%v)", atEventID, eventIDToIndex[atEventID]) + authChain, stateEvents := getIncorrectState(atEventID) + return util.JSONResponse{ + Code: 200, + JSON: struct { + AuthChain gomatrixserverlib.EventJSONs `json:"auth_chain"` + PDUs gomatrixserverlib.EventJSONs `json:"pdus"` + }{ + AuthChain: gomatrixserverlib.NewEventJSONsFromEvents(authChain), + PDUs: gomatrixserverlib.NewEventJSONsFromEvents(stateEvents), + }, + } + }), + ) + + // Send the last event to trigger /get_missing_events and /state_ids + finalEvent := charlieEvents[len(charlieEvents)-1] + srv.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{ + finalEvent.JSON(), + }, nil) + + seenStateIDs.Waitf(t, 5*time.Second, "failed to see a /state{_ids} request") + + // wait until bob sees the final event + bob.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHasEventID(room.RoomID, finalEvent.EventID())) + + // the join rules should be `invite`. + // Servers which do not implement v2.1 will get a HTTP 404 here. + content := bob.MustGetStateEventContent(t, room.RoomID, spec.MRoomJoinRules, "") + must.MatchGJSON(t, content, match.JSONKeyEqual("join_rule", "invite")) +} + +func TestMSC4297StateResolutionV2_1_includes_conflicted_subgraph(t *testing.T) { + runtime.SkipIf(t, runtime.Dendrite) // needs additional fixes + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + srv := federation.NewServer(t, deployment, + federation.HandleKeyRequests(), + federation.HandleMakeSendJoinRequests(), + federation.HandleTransactionRequests(nil, nil), + federation.HandleEventRequests(), + ) + srv.UnexpectedRequestsAreErrors = false + cancel := srv.Listen() + defer cancel() + creator := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "creator"}) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "alice"}) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "bob"}) + charlie := srv.UserID("charlie") + eve := srv.UserID("eve") + zara := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "zara"}) + + // We play every event from Problem B from the MSC except for Eve's events. We separate out + // Alice and Creator roles for combined MSC compatibility. + roomID := creator.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + }) + creator.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]any{ + "users": map[string]any{ + alice.UserID: 100, + }, + }, + }) + alice.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + room := srv.MustJoinRoom(t, deployment, "hs1", roomID, charlie, federation.WithRoomOpts(federation.WithImpl(&V12ServerRoom))) + firstPowerLevelEvent := room.CurrentState(spec.MRoomPowerLevels, "") + alice.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]any{ + "users": map[string]any{ + alice.UserID: 100, + bob.UserID: 50, + }, + }, + }) + pl3EventID := bob.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]any{ + "users": map[string]any{ + alice.UserID: 100, + bob.UserID: 50, + charlie: 50, + }, + }, + }) + zara.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + + // Now Eve will join from the third PL event + eveJoinEvent := srv.MustCreateEvent(t, room, federation.Event{ + Type: spec.MRoomMember, + StateKey: &eve, + Sender: eve, + Content: map[string]interface{}{ + "membership": spec.Join, + }, + PrevEvents: []string{ + pl3EventID, + }, + }) + room.AddEvent(eveJoinEvent) + srv.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{eveJoinEvent.JSON()}, nil) + aliceSince := alice.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHasEventID(roomID, eveJoinEvent.EventID())) + + // Now change Eve's display name many times to force partial synchronisation + + // We don't know how far back server impls will go, so let's use 250 as a large enough value. + eveEvents := make([]gomatrixserverlib.PDU, 250) + eventIDToIndex := make(map[string]int) + prevEvents := eveJoinEvent.EventID() + for i := range eveEvents { + ev := srv.MustCreateEvent(t, room, federation.Event{ + Type: spec.MRoomMember, + StateKey: &eve, + Sender: eve, + Content: map[string]interface{}{ + "membership": spec.Join, + "displayname": fmt.Sprintf("Eve %d", i), + }, + PrevEvents: []string{prevEvents}, + }) + room.AddEvent(ev) + eveEvents[i] = ev + eventIDToIndex[ev.EventID()] = i + prevEvents = ev.EventID() + } + + seenStateIDs := helpers.NewWaiter() + + // process requests to walk back through eve's display name changes + srv.Mux().HandleFunc( + "/_matrix/federation/v1/get_missing_events/{roomID}", + srv.ValidFederationRequest(t, getMissingEventsHandler(t, room, eventIDToIndex, eveEvents)), + ) + + getIncorrectState := func(atEventID string) (authChain, stateEvents []gomatrixserverlib.PDU) { + t.Logf("calculating state before event %v (i=%v)", atEventID, eventIDToIndex[atEventID]) + // Find the correct state for eve + // we want to return the state before the requested event hence -1 + stateEvents = append(stateEvents, eveEvents[eventIDToIndex[atEventID]-1]) + // charlie's auth events are everything prior to this + authChain = append(authChain, eveEvents[:eventIDToIndex[atEventID]-1]...) + + stateEvents = append(stateEvents, + room.CurrentState(spec.MRoomCreate, ""), + room.CurrentState(spec.MRoomMember, alice.UserID), + firstPowerLevelEvent, // This is wrong, and should be the 3rd PL event. + room.CurrentState(spec.MRoomJoinRules, ""), + room.CurrentState(spec.MRoomMember, bob.UserID), + room.CurrentState(spec.MRoomMember, charlie), + ) + authChain = append(authChain, eveJoinEvent) + return + } + srv.Mux().HandleFunc( + "/_matrix/federation/v1/state_ids/{roomID}", + srv.ValidFederationRequest(t, func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { + if pathParams["roomID"] != room.RoomID { + t.Errorf("Received /state_ids for the wrong room: %s", room.RoomID) + return util.JSONResponse{ + Code: 400, + JSON: "wrong room", + } + } + defer seenStateIDs.Finish() + req, err := fr.HTTPRequest() + must.NotError(t, "failed to get fed http reqest", err) + atEventID := req.URL.Query().Get("event_id") + t.Logf("received /state_ids at event %v (i=%v)", atEventID, eventIDToIndex[atEventID]) + authChain, stateEvents := getIncorrectState(atEventID) + return util.JSONResponse{ + Code: 200, + JSON: struct { + AuthChainIDs []string `json:"auth_chain_ids"` + PDUIDs []string `json:"pdu_ids"` + }{ + AuthChainIDs: asEventIDs(authChain), + PDUIDs: asEventIDs(stateEvents), + }, + } + }), + ) + srv.Mux().HandleFunc( + "/_matrix/federation/v1/state/{roomID}", + srv.ValidFederationRequest(t, func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { + if pathParams["roomID"] != room.RoomID { + t.Errorf("Received /state for the wrong room: %s", room.RoomID) + return util.JSONResponse{ + Code: 400, + JSON: "wrong room", + } + } + defer seenStateIDs.Finish() + req, err := fr.HTTPRequest() + must.NotError(t, "failed to get fed http reqest", err) + atEventID := req.URL.Query().Get("event_id") + t.Logf("received /state at event %v (i=%v)", atEventID, eventIDToIndex[atEventID]) + authChain, stateEvents := getIncorrectState(atEventID) + return util.JSONResponse{ + Code: 200, + JSON: struct { + AuthChain gomatrixserverlib.EventJSONs `json:"auth_chain"` + PDUs gomatrixserverlib.EventJSONs `json:"pdus"` + }{ + AuthChain: gomatrixserverlib.NewEventJSONsFromEvents(authChain), + PDUs: gomatrixserverlib.NewEventJSONsFromEvents(stateEvents), + }, + } + }), + ) + + // Send the last event to trigger /get_missing_events and /state_ids + finalEvent := eveEvents[len(eveEvents)-1] + srv.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{ + finalEvent.JSON(), + }, nil) + + seenStateIDs.Waitf(t, 5*time.Second, "failed to see a /state{_ids} request") + + // wait until bob sees the final event + alice.MustSyncUntil(t, client.SyncReq{Since: aliceSince}, client.SyncTimelineHasEventID(room.RoomID, finalEvent.EventID())) + + // the power levels should be the 3rd one (Alice:PL100, Bob:PL50, Charlie:PL59), not the 1st (Alice: PL100). + // Servers which do not implement v2.1 will see the 1st one. + content := alice.MustGetStateEventContent(t, room.RoomID, spec.MRoomPowerLevels, "") + must.MatchGJSON(t, content, match.JSONKeyEqual("users."+client.GjsonEscape(alice.UserID), 100)) + // v2 Servers will fail here as bob does not exist in this map because it reset to an earlier event. + must.MatchGJSON(t, content, match.JSONKeyEqual("users."+client.GjsonEscape(bob.UserID), 50)) + must.MatchGJSON(t, content, match.JSONKeyEqual("users."+client.GjsonEscape(charlie), 50)) +} + +func getMissingEventsHandler(t ct.TestLike, room *federation.ServerRoom, eventIDToIndex map[string]int, events []gomatrixserverlib.PDU) func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { + return func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { + if pathParams["roomID"] != room.RoomID { + t.Errorf("Received /get_missing_events for the wrong room: %s", room.RoomID) + return util.JSONResponse{ + Code: 400, + JSON: "wrong room", + } + } + latestEventID := gjson.ParseBytes(fr.Content()).Get("latest_events").Array()[0].Str + j, ok := eventIDToIndex[latestEventID] + if !ok { + ct.Fatalf(t, "received /get_missing_events with latest=%s which is unknown", latestEventID) + } + t.Logf("received /get_missing_events with latest=%s (i=%d)", latestEventID, j) + // return 10 earlier events. + slice := events[j-10 : j] + for _, ev := range slice { + t.Logf("/get_missing_events returning %v (i=%v)", ev.EventID(), eventIDToIndex[ev.EventID()]) + } + return util.JSONResponse{ + Code: 200, + JSON: struct { + Events gomatrixserverlib.EventJSONs `json:"events"` + }{ + Events: gomatrixserverlib.NewEventJSONsFromEvents(slice), + }, + } + } +} + +func asEventIDs(pdus []gomatrixserverlib.PDU) []string { + eventIDs := make([]string, len(pdus)) + for i := range pdus { + eventIDs[i] = pdus[i].EventID() + } + return eventIDs +} + +func TestMSC4311FullCreateEventOnStrippedState(t *testing.T) { + runtime.SkipIf(t, runtime.Dendrite) // does not implement it yet + deployment := complement.Deploy(t, 2) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "alice"}) + local := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "local"}) + remote := deployment.Register(t, "hs2", helpers.RegistrationOpts{LocalpartSuffix: "remote"}) + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + }) + for _, target := range []*client.CSAPI{local, remote} { + t.Logf("checking %s", target.UserID) + alice.MustInviteRoom(t, roomID, target.UserID) + resp, _ := target.MustSync(t, client.SyncReq{}) + inviteState := resp.Get( + fmt.Sprintf("rooms.invite.%s.invite_state.events", client.GjsonEscape(roomID)), + ) + must.NotEqual(t, len(inviteState.Array()), 0, "no events in invite_state") + // find the create event + found := false + for _, ev := range inviteState.Array() { + if ev.Get("type").Str == spec.MRoomCreate { + found = true + // we should have extra fields + must.MatchGJSON(t, ev, + match.JSONKeyPresent("origin_server_ts"), + ) + } + } + if !found { + ct.Errorf(t, "failed to find create event in invite_state") + } + } + +}