mirror of
https://github.com/matrix-org/dendrite.git
synced 2026-01-16 18:43:10 -06:00
Merge branch 'main' into patch-1
This commit is contained in:
commit
81f35687a9
41
.github/workflows/schedules.yaml
vendored
41
.github/workflows/schedules.yaml
vendored
|
|
@ -219,7 +219,7 @@ jobs:
|
||||||
flags: complement
|
flags: complement
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
|
|
||||||
element_web:
|
element-web:
|
||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
@ -257,3 +257,42 @@ jobs:
|
||||||
env:
|
env:
|
||||||
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
|
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
|
||||||
TMPDIR: ${{ runner.temp }}
|
TMPDIR: ${{ runner.temp }}
|
||||||
|
|
||||||
|
element-web-pinecone:
|
||||||
|
timeout-minutes: 120
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: tecolicom/actions-use-apt-tools@v1
|
||||||
|
with:
|
||||||
|
# Our test suite includes some screenshot tests with unusual diacritics, which are
|
||||||
|
# supposed to be covered by STIXGeneral.
|
||||||
|
tools: fonts-stix
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
repository: matrix-org/matrix-react-sdk
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: 'yarn'
|
||||||
|
- name: Fetch layered build
|
||||||
|
run: scripts/ci/layered.sh
|
||||||
|
- name: Copy config
|
||||||
|
run: cp element.io/develop/config.json config.json
|
||||||
|
working-directory: ./element-web
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
CI_PACKAGE: true
|
||||||
|
NODE_OPTIONS: "--openssl-legacy-provider"
|
||||||
|
run: yarn build
|
||||||
|
working-directory: ./element-web
|
||||||
|
- name: Edit Test Config
|
||||||
|
run: |
|
||||||
|
sed -i '/HOMESERVER/c\ HOMESERVER: "dendritePinecone",' cypress.config.ts
|
||||||
|
- name: "Run cypress tests"
|
||||||
|
uses: cypress-io/github-action@v4.1.1
|
||||||
|
with:
|
||||||
|
browser: chrome
|
||||||
|
start: npx serve -p 8080 ./element-web/webapp
|
||||||
|
wait-on: 'http://localhost:8080'
|
||||||
|
env:
|
||||||
|
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
|
||||||
|
TMPDIR: ${{ runner.temp }}
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,12 @@ import (
|
||||||
"github.com/matrix-org/dendrite/internal/caching"
|
"github.com/matrix-org/dendrite/internal/caching"
|
||||||
"github.com/matrix-org/dendrite/internal/sqlutil"
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||||
"github.com/matrix-org/dendrite/roomserver"
|
"github.com/matrix-org/dendrite/roomserver"
|
||||||
|
rsapi "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/setup/jetstream"
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
"github.com/matrix-org/dendrite/test"
|
"github.com/matrix-org/dendrite/test"
|
||||||
"github.com/matrix-org/dendrite/userapi"
|
"github.com/matrix-org/dendrite/userapi"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/test/testrig"
|
"github.com/matrix-org/dendrite/test/testrig"
|
||||||
)
|
)
|
||||||
|
|
@ -212,3 +214,86 @@ func testProtocol(t *testing.T, asAPI api.AppServiceInternalAPI, proto string, w
|
||||||
t.Errorf("unexpected result for Protocols(%s): %+v, expected %+v", proto, protoResp.Protocols[proto], wantResult)
|
t.Errorf("unexpected result for Protocols(%s): %+v, expected %+v", proto, protoResp.Protocols[proto], wantResult)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests that the roomserver consumer only receives one invite
|
||||||
|
func TestRoomserverConsumerOneInvite(t *testing.T) {
|
||||||
|
|
||||||
|
alice := test.NewUser(t)
|
||||||
|
bob := test.NewUser(t)
|
||||||
|
room := test.NewRoom(t, alice)
|
||||||
|
|
||||||
|
// Invite Bob
|
||||||
|
room.CreateAndInsert(t, alice, gomatrixserverlib.MRoomMember, map[string]interface{}{
|
||||||
|
"membership": "invite",
|
||||||
|
}, test.WithStateKey(bob.ID))
|
||||||
|
|
||||||
|
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
||||||
|
cfg, processCtx, closeDB := testrig.CreateConfig(t, dbType)
|
||||||
|
defer closeDB()
|
||||||
|
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||||
|
natsInstance := &jetstream.NATSInstance{}
|
||||||
|
|
||||||
|
evChan := make(chan struct{})
|
||||||
|
// create a dummy AS url, handling the events
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var txn gomatrixserverlib.ApplicationServiceTransaction
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&txn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, ev := range txn.Events {
|
||||||
|
if ev.Type != gomatrixserverlib.MRoomMember {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Usually we would check the event content for the membership, but since
|
||||||
|
// we only invited bob, this should be fine for this test.
|
||||||
|
if ev.StateKey != nil && *ev.StateKey == bob.ID {
|
||||||
|
evChan <- struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
// Create a dummy application service
|
||||||
|
cfg.AppServiceAPI.Derived.ApplicationServices = []config.ApplicationService{
|
||||||
|
{
|
||||||
|
ID: "someID",
|
||||||
|
URL: srv.URL,
|
||||||
|
ASToken: "",
|
||||||
|
HSToken: "",
|
||||||
|
SenderLocalpart: "senderLocalPart",
|
||||||
|
NamespaceMap: map[string][]config.ApplicationServiceNamespace{
|
||||||
|
"users": {{RegexpObject: regexp.MustCompile(bob.ID)}},
|
||||||
|
"aliases": {{RegexpObject: regexp.MustCompile(room.ID)}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
||||||
|
// Create required internal APIs
|
||||||
|
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, natsInstance, caches, caching.DisableMetrics)
|
||||||
|
rsAPI.SetFederationAPI(nil, nil)
|
||||||
|
usrAPI := userapi.NewInternalAPI(processCtx, cfg, cm, natsInstance, rsAPI, nil)
|
||||||
|
// start the consumer
|
||||||
|
appservice.NewInternalAPI(processCtx, cfg, natsInstance, usrAPI, rsAPI)
|
||||||
|
|
||||||
|
// Create the room
|
||||||
|
if err := rsapi.SendEvents(context.Background(), rsAPI, rsapi.KindNew, room.Events(), "test", "test", "test", nil, false); err != nil {
|
||||||
|
t.Fatalf("failed to send events: %v", err)
|
||||||
|
}
|
||||||
|
var seenInvitesForBob int
|
||||||
|
waitLoop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-time.After(time.Millisecond * 50): // wait for the AS to process the events
|
||||||
|
break waitLoop
|
||||||
|
case <-evChan:
|
||||||
|
seenInvitesForBob++
|
||||||
|
if seenInvitesForBob != 1 {
|
||||||
|
t.Fatalf("received unexpected invites: %d", seenInvitesForBob)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(evChan)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -140,12 +140,6 @@ func (s *OutputRoomEventConsumer) onMessage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case api.OutputTypeNewInviteEvent:
|
|
||||||
if output.NewInviteEvent == nil || !s.appserviceIsInterestedInEvent(ctx, output.NewInviteEvent.Event, state.ApplicationService) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
events = append(events, output.NewInviteEvent.Event)
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,17 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
|
||||||
"github.com/matrix-org/dendrite/federationapi"
|
"github.com/matrix-org/dendrite/federationapi"
|
||||||
"github.com/matrix-org/dendrite/internal/caching"
|
"github.com/matrix-org/dendrite/internal/caching"
|
||||||
"github.com/matrix-org/dendrite/internal/httputil"
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
"github.com/matrix-org/dendrite/internal/sqlutil"
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||||
"github.com/matrix-org/dendrite/roomserver"
|
"github.com/matrix-org/dendrite/roomserver"
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
basepkg "github.com/matrix-org/dendrite/setup/base"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/setup/jetstream"
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
"github.com/matrix-org/dendrite/syncapi"
|
"github.com/matrix-org/dendrite/syncapi"
|
||||||
|
|
@ -52,39 +53,12 @@ func TestAdminResetPassword(t *testing.T) {
|
||||||
AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, caching.DisableMetrics)
|
AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, caching.DisableMetrics)
|
||||||
|
|
||||||
// Create the users in the userapi and login
|
// Create the users in the userapi and login
|
||||||
accessTokens := map[*test.User]string{
|
accessTokens := map[*test.User]userDevice{
|
||||||
aliceAdmin: "",
|
aliceAdmin: {},
|
||||||
bob: "",
|
bob: {},
|
||||||
vhUser: "",
|
vhUser: {},
|
||||||
}
|
|
||||||
for u := range accessTokens {
|
|
||||||
localpart, serverName, _ := gomatrixserverlib.SplitID('@', u.ID)
|
|
||||||
userRes := &uapi.PerformAccountCreationResponse{}
|
|
||||||
password := util.RandomString(8)
|
|
||||||
if err := userAPI.PerformAccountCreation(ctx, &uapi.PerformAccountCreationRequest{
|
|
||||||
AccountType: u.AccountType,
|
|
||||||
Localpart: localpart,
|
|
||||||
ServerName: serverName,
|
|
||||||
Password: password,
|
|
||||||
}, userRes); err != nil {
|
|
||||||
t.Errorf("failed to create account: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := test.NewRequest(t, http.MethodPost, "/_matrix/client/v3/login", test.WithJSONBody(t, map[string]interface{}{
|
|
||||||
"type": authtypes.LoginTypePassword,
|
|
||||||
"identifier": map[string]interface{}{
|
|
||||||
"type": "m.id.user",
|
|
||||||
"user": u.ID,
|
|
||||||
},
|
|
||||||
"password": password,
|
|
||||||
}))
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
routers.Client.ServeHTTP(rec, req)
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
t.Fatalf("failed to login: %s", rec.Body.String())
|
|
||||||
}
|
|
||||||
accessTokens[u] = gjson.GetBytes(rec.Body.Bytes(), "access_token").String()
|
|
||||||
}
|
}
|
||||||
|
createAccessTokens(t, accessTokens, userAPI, ctx, routers)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
@ -128,7 +102,7 @@ func TestAdminResetPassword(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if tc.withHeader {
|
if tc.withHeader {
|
||||||
req.Header.Set("Authorization", "Bearer "+accessTokens[tc.requestingUser])
|
req.Header.Set("Authorization", "Bearer "+accessTokens[tc.requestingUser].accessToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
|
|
@ -179,37 +153,10 @@ func TestPurgeRoom(t *testing.T) {
|
||||||
AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, caching.DisableMetrics)
|
AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, caching.DisableMetrics)
|
||||||
|
|
||||||
// Create the users in the userapi and login
|
// Create the users in the userapi and login
|
||||||
accessTokens := map[*test.User]string{
|
accessTokens := map[*test.User]userDevice{
|
||||||
aliceAdmin: "",
|
aliceAdmin: {},
|
||||||
}
|
|
||||||
for u := range accessTokens {
|
|
||||||
localpart, serverName, _ := gomatrixserverlib.SplitID('@', u.ID)
|
|
||||||
userRes := &uapi.PerformAccountCreationResponse{}
|
|
||||||
password := util.RandomString(8)
|
|
||||||
if err := userAPI.PerformAccountCreation(ctx, &uapi.PerformAccountCreationRequest{
|
|
||||||
AccountType: u.AccountType,
|
|
||||||
Localpart: localpart,
|
|
||||||
ServerName: serverName,
|
|
||||||
Password: password,
|
|
||||||
}, userRes); err != nil {
|
|
||||||
t.Errorf("failed to create account: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := test.NewRequest(t, http.MethodPost, "/_matrix/client/v3/login", test.WithJSONBody(t, map[string]interface{}{
|
|
||||||
"type": authtypes.LoginTypePassword,
|
|
||||||
"identifier": map[string]interface{}{
|
|
||||||
"type": "m.id.user",
|
|
||||||
"user": u.ID,
|
|
||||||
},
|
|
||||||
"password": password,
|
|
||||||
}))
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
routers.Client.ServeHTTP(rec, req)
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
t.Fatalf("failed to login: %s", rec.Body.String())
|
|
||||||
}
|
|
||||||
accessTokens[u] = gjson.GetBytes(rec.Body.Bytes(), "access_token").String()
|
|
||||||
}
|
}
|
||||||
|
createAccessTokens(t, accessTokens, userAPI, ctx, routers)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
@ -226,7 +173,7 @@ func TestPurgeRoom(t *testing.T) {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
req := test.NewRequest(t, http.MethodPost, "/_dendrite/admin/purgeRoom/"+tc.roomID)
|
req := test.NewRequest(t, http.MethodPost, "/_dendrite/admin/purgeRoom/"+tc.roomID)
|
||||||
|
|
||||||
req.Header.Set("Authorization", "Bearer "+accessTokens[aliceAdmin])
|
req.Header.Set("Authorization", "Bearer "+accessTokens[aliceAdmin].accessToken)
|
||||||
|
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
routers.DendriteAdmin.ServeHTTP(rec, req)
|
routers.DendriteAdmin.ServeHTTP(rec, req)
|
||||||
|
|
@ -239,3 +186,237 @@ func TestPurgeRoom(t *testing.T) {
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAdminEvacuateRoom(t *testing.T) {
|
||||||
|
aliceAdmin := test.NewUser(t, test.WithAccountType(uapi.AccountTypeAdmin))
|
||||||
|
bob := test.NewUser(t)
|
||||||
|
room := test.NewRoom(t, aliceAdmin)
|
||||||
|
|
||||||
|
// Join Bob
|
||||||
|
room.CreateAndInsert(t, bob, gomatrixserverlib.MRoomMember, map[string]interface{}{
|
||||||
|
"membership": "join",
|
||||||
|
}, test.WithStateKey(bob.ID))
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
||||||
|
cfg, processCtx, close := testrig.CreateConfig(t, dbType)
|
||||||
|
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
||||||
|
natsInstance := jetstream.NATSInstance{}
|
||||||
|
defer close()
|
||||||
|
|
||||||
|
routers := httputil.NewRouters()
|
||||||
|
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||||
|
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
|
||||||
|
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil)
|
||||||
|
|
||||||
|
// this starts the JetStream consumers
|
||||||
|
fsAPI := federationapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, nil, rsAPI, caches, nil, true)
|
||||||
|
rsAPI.SetFederationAPI(fsAPI, nil)
|
||||||
|
|
||||||
|
// Create the room
|
||||||
|
if err := api.SendEvents(ctx, rsAPI, api.KindNew, room.Events(), "test", "test", api.DoNotSendToOtherServers, nil, false); err != nil {
|
||||||
|
t.Fatalf("failed to send events: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We mostly need the rsAPI for this test, so nil for other APIs/caches etc.
|
||||||
|
AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, caching.DisableMetrics)
|
||||||
|
|
||||||
|
// Create the users in the userapi and login
|
||||||
|
accessTokens := map[*test.User]userDevice{
|
||||||
|
aliceAdmin: {},
|
||||||
|
}
|
||||||
|
createAccessTokens(t, accessTokens, userAPI, ctx, routers)
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
roomID string
|
||||||
|
wantOK bool
|
||||||
|
wantAffected []string
|
||||||
|
}{
|
||||||
|
{name: "Can evacuate existing room", wantOK: true, roomID: room.ID, wantAffected: []string{aliceAdmin.ID, bob.ID}},
|
||||||
|
{name: "Can not evacuate non-existent room", wantOK: false, roomID: "!doesnotexist:localhost", wantAffected: []string{}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
req := test.NewRequest(t, http.MethodPost, "/_dendrite/admin/evacuateRoom/"+tc.roomID)
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Bearer "+accessTokens[aliceAdmin].accessToken)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
routers.DendriteAdmin.ServeHTTP(rec, req)
|
||||||
|
t.Logf("%s", rec.Body.String())
|
||||||
|
if tc.wantOK && rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected http status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
affectedArr := gjson.GetBytes(rec.Body.Bytes(), "affected").Array()
|
||||||
|
affected := make([]string, 0, len(affectedArr))
|
||||||
|
for _, x := range affectedArr {
|
||||||
|
affected = append(affected, x.Str)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(affected, tc.wantAffected) {
|
||||||
|
t.Fatalf("expected affected %#v, but got %#v", tc.wantAffected, affected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdminEvacuateUser(t *testing.T) {
|
||||||
|
aliceAdmin := test.NewUser(t, test.WithAccountType(uapi.AccountTypeAdmin))
|
||||||
|
bob := test.NewUser(t)
|
||||||
|
room := test.NewRoom(t, aliceAdmin)
|
||||||
|
room2 := test.NewRoom(t, aliceAdmin)
|
||||||
|
|
||||||
|
// Join Bob
|
||||||
|
room.CreateAndInsert(t, bob, gomatrixserverlib.MRoomMember, map[string]interface{}{
|
||||||
|
"membership": "join",
|
||||||
|
}, test.WithStateKey(bob.ID))
|
||||||
|
room2.CreateAndInsert(t, bob, gomatrixserverlib.MRoomMember, map[string]interface{}{
|
||||||
|
"membership": "join",
|
||||||
|
}, test.WithStateKey(bob.ID))
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
||||||
|
cfg, processCtx, close := testrig.CreateConfig(t, dbType)
|
||||||
|
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
||||||
|
natsInstance := jetstream.NATSInstance{}
|
||||||
|
defer close()
|
||||||
|
|
||||||
|
routers := httputil.NewRouters()
|
||||||
|
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||||
|
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
|
||||||
|
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil)
|
||||||
|
|
||||||
|
// this starts the JetStream consumers
|
||||||
|
fsAPI := federationapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, basepkg.CreateFederationClient(cfg, nil), rsAPI, caches, nil, true)
|
||||||
|
rsAPI.SetFederationAPI(fsAPI, nil)
|
||||||
|
|
||||||
|
// Create the room
|
||||||
|
if err := api.SendEvents(ctx, rsAPI, api.KindNew, room.Events(), "test", "test", api.DoNotSendToOtherServers, nil, false); err != nil {
|
||||||
|
t.Fatalf("failed to send events: %v", err)
|
||||||
|
}
|
||||||
|
if err := api.SendEvents(ctx, rsAPI, api.KindNew, room2.Events(), "test", "test", api.DoNotSendToOtherServers, nil, false); err != nil {
|
||||||
|
t.Fatalf("failed to send events: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We mostly need the rsAPI for this test, so nil for other APIs/caches etc.
|
||||||
|
AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, caching.DisableMetrics)
|
||||||
|
|
||||||
|
// Create the users in the userapi and login
|
||||||
|
accessTokens := map[*test.User]userDevice{
|
||||||
|
aliceAdmin: {},
|
||||||
|
}
|
||||||
|
createAccessTokens(t, accessTokens, userAPI, ctx, routers)
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
userID string
|
||||||
|
wantOK bool
|
||||||
|
wantAffectedRooms []string
|
||||||
|
}{
|
||||||
|
{name: "Can evacuate existing user", wantOK: true, userID: bob.ID, wantAffectedRooms: []string{room.ID, room2.ID}},
|
||||||
|
{name: "invalid userID is rejected", wantOK: false, userID: "!notauserid:test", wantAffectedRooms: []string{}},
|
||||||
|
{name: "Can not evacuate user from different server", wantOK: false, userID: "@doesnotexist:localhost", wantAffectedRooms: []string{}},
|
||||||
|
{name: "Can not evacuate non-existent user", wantOK: false, userID: "@doesnotexist:test", wantAffectedRooms: []string{}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
req := test.NewRequest(t, http.MethodPost, "/_dendrite/admin/evacuateUser/"+tc.userID)
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Bearer "+accessTokens[aliceAdmin].accessToken)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
routers.DendriteAdmin.ServeHTTP(rec, req)
|
||||||
|
t.Logf("%s", rec.Body.String())
|
||||||
|
if tc.wantOK && rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected http status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
affectedArr := gjson.GetBytes(rec.Body.Bytes(), "affected").Array()
|
||||||
|
affected := make([]string, 0, len(affectedArr))
|
||||||
|
for _, x := range affectedArr {
|
||||||
|
affected = append(affected, x.Str)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(affected, tc.wantAffectedRooms) {
|
||||||
|
t.Fatalf("expected affected %#v, but got %#v", tc.wantAffectedRooms, affected)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Wait for the FS API to have consumed every message
|
||||||
|
js, _ := natsInstance.Prepare(processCtx, &cfg.Global.JetStream)
|
||||||
|
timeout := time.After(time.Second)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-timeout:
|
||||||
|
t.Fatalf("FS API didn't process all events in time")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
info, err := js.ConsumerInfo(cfg.Global.JetStream.Prefixed(jetstream.OutputRoomEvent), cfg.Global.JetStream.Durable("FederationAPIRoomServerConsumer")+"Pull")
|
||||||
|
if err != nil {
|
||||||
|
time.Sleep(time.Millisecond * 10)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if info.NumPending == 0 && info.NumAckPending == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdminMarkAsStale(t *testing.T) {
|
||||||
|
aliceAdmin := test.NewUser(t, test.WithAccountType(uapi.AccountTypeAdmin))
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
||||||
|
cfg, processCtx, close := testrig.CreateConfig(t, dbType)
|
||||||
|
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
||||||
|
natsInstance := jetstream.NATSInstance{}
|
||||||
|
defer close()
|
||||||
|
|
||||||
|
routers := httputil.NewRouters()
|
||||||
|
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||||
|
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
|
||||||
|
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil)
|
||||||
|
|
||||||
|
// We mostly need the rsAPI for this test, so nil for other APIs/caches etc.
|
||||||
|
AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, caching.DisableMetrics)
|
||||||
|
|
||||||
|
// Create the users in the userapi and login
|
||||||
|
accessTokens := map[*test.User]userDevice{
|
||||||
|
aliceAdmin: {},
|
||||||
|
}
|
||||||
|
createAccessTokens(t, accessTokens, userAPI, ctx, routers)
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
userID string
|
||||||
|
wantOK bool
|
||||||
|
}{
|
||||||
|
{name: "local user is not allowed", userID: aliceAdmin.ID},
|
||||||
|
{name: "invalid userID", userID: "!notvalid:test"},
|
||||||
|
{name: "remote user is allowed", userID: "@alice:localhost", wantOK: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
req := test.NewRequest(t, http.MethodPost, "/_dendrite/admin/refreshDevices/"+tc.userID)
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Bearer "+accessTokens[aliceAdmin].accessToken)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
routers.DendriteAdmin.ServeHTTP(rec, req)
|
||||||
|
t.Logf("%s", rec.Body.String())
|
||||||
|
if tc.wantOK && rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected http status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
373
clientapi/clientapi_test.go
Normal file
373
clientapi/clientapi_test.go
Normal file
|
|
@ -0,0 +1,373 @@
|
||||||
|
package clientapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
|
"github.com/matrix-org/dendrite/internal/caching"
|
||||||
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||||
|
"github.com/matrix-org/dendrite/roomserver"
|
||||||
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
|
"github.com/matrix-org/dendrite/test"
|
||||||
|
"github.com/matrix-org/dendrite/test/testrig"
|
||||||
|
"github.com/matrix-org/dendrite/userapi"
|
||||||
|
uapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type userDevice struct {
|
||||||
|
accessToken string
|
||||||
|
deviceID string
|
||||||
|
password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPutDevices(t *testing.T) {
|
||||||
|
alice := test.NewUser(t)
|
||||||
|
bob := test.NewUser(t)
|
||||||
|
|
||||||
|
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
requestUser *test.User
|
||||||
|
deviceUser *test.User
|
||||||
|
request *http.Request
|
||||||
|
wantStatusCode int
|
||||||
|
validateFunc func(t *testing.T, device userDevice, routers httputil.Routers)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "can get all devices",
|
||||||
|
requestUser: alice,
|
||||||
|
request: httptest.NewRequest(http.MethodGet, "/_matrix/client/v3/devices", strings.NewReader("")),
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "can get specific own device",
|
||||||
|
requestUser: alice,
|
||||||
|
deviceUser: alice,
|
||||||
|
request: httptest.NewRequest(http.MethodGet, "/_matrix/client/v3/devices/", strings.NewReader("")),
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "can not get device for different user",
|
||||||
|
requestUser: alice,
|
||||||
|
deviceUser: bob,
|
||||||
|
request: httptest.NewRequest(http.MethodGet, "/_matrix/client/v3/devices/", strings.NewReader("")),
|
||||||
|
wantStatusCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "can update own device",
|
||||||
|
requestUser: alice,
|
||||||
|
deviceUser: alice,
|
||||||
|
request: httptest.NewRequest(http.MethodPut, "/_matrix/client/v3/devices/", strings.NewReader(`{"display_name":"my new displayname"}`)),
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
validateFunc: func(t *testing.T, device userDevice, routers httputil.Routers) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/_matrix/client/v3/devices/"+device.deviceID, strings.NewReader(""))
|
||||||
|
req.Header.Set("Authorization", "Bearer "+device.accessToken)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
routers.Client.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected HTTP 200, got %d: %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
gotDisplayName := gjson.GetBytes(rec.Body.Bytes(), "display_name").Str
|
||||||
|
if gotDisplayName != "my new displayname" {
|
||||||
|
t.Fatalf("expected displayname '%s', got '%s'", "my new displayname", gotDisplayName)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// this should return "device does not exist"
|
||||||
|
name: "can not update device for different user",
|
||||||
|
requestUser: alice,
|
||||||
|
deviceUser: bob,
|
||||||
|
request: httptest.NewRequest(http.MethodPut, "/_matrix/client/v3/devices/", strings.NewReader(`{"display_name":"my new displayname"}`)),
|
||||||
|
wantStatusCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, processCtx, close := testrig.CreateConfig(t, dbType)
|
||||||
|
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
||||||
|
natsInstance := jetstream.NATSInstance{}
|
||||||
|
defer close()
|
||||||
|
|
||||||
|
routers := httputil.NewRouters()
|
||||||
|
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||||
|
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
|
||||||
|
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil)
|
||||||
|
|
||||||
|
// We mostly need the rsAPI for this test, so nil for other APIs/caches etc.
|
||||||
|
AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, caching.DisableMetrics)
|
||||||
|
|
||||||
|
accessTokens := map[*test.User]userDevice{
|
||||||
|
alice: {},
|
||||||
|
bob: {},
|
||||||
|
}
|
||||||
|
createAccessTokens(t, accessTokens, userAPI, processCtx.Context(), routers)
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
dev := accessTokens[tc.requestUser]
|
||||||
|
if tc.deviceUser != nil {
|
||||||
|
tc.request = httptest.NewRequest(tc.request.Method, tc.request.RequestURI+accessTokens[tc.deviceUser].deviceID, tc.request.Body)
|
||||||
|
}
|
||||||
|
tc.request.Header.Set("Authorization", "Bearer "+dev.accessToken)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
routers.Client.ServeHTTP(rec, tc.request)
|
||||||
|
if rec.Code != tc.wantStatusCode {
|
||||||
|
t.Fatalf("expected HTTP 200, got %d: %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
if tc.wantStatusCode != http.StatusOK && rec.Code != http.StatusOK {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tc.validateFunc != nil {
|
||||||
|
tc.validateFunc(t, dev, routers)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deleting devices requires the UIA dance, so do this in a different test
|
||||||
|
func TestDeleteDevice(t *testing.T) {
|
||||||
|
alice := test.NewUser(t)
|
||||||
|
localpart, serverName, _ := gomatrixserverlib.SplitID('@', alice.ID)
|
||||||
|
|
||||||
|
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
||||||
|
cfg, processCtx, closeDB := testrig.CreateConfig(t, dbType)
|
||||||
|
defer closeDB()
|
||||||
|
|
||||||
|
natsInstance := jetstream.NATSInstance{}
|
||||||
|
routers := httputil.NewRouters()
|
||||||
|
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||||
|
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
||||||
|
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
|
||||||
|
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil)
|
||||||
|
|
||||||
|
// We mostly need the rsAPI/ for this test, so nil for other APIs/caches etc.
|
||||||
|
AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, caching.DisableMetrics)
|
||||||
|
|
||||||
|
accessTokens := map[*test.User]userDevice{
|
||||||
|
alice: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the account and an initial device
|
||||||
|
createAccessTokens(t, accessTokens, userAPI, processCtx.Context(), routers)
|
||||||
|
|
||||||
|
// create some more devices
|
||||||
|
accessToken := util.RandomString(8)
|
||||||
|
devRes := &uapi.PerformDeviceCreationResponse{}
|
||||||
|
if err := userAPI.PerformDeviceCreation(processCtx.Context(), &uapi.PerformDeviceCreationRequest{
|
||||||
|
Localpart: localpart,
|
||||||
|
ServerName: serverName,
|
||||||
|
AccessToken: accessToken,
|
||||||
|
NoDeviceListUpdate: true,
|
||||||
|
}, devRes); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !devRes.DeviceCreated {
|
||||||
|
t.Fatalf("failed to create device")
|
||||||
|
}
|
||||||
|
secondDeviceID := devRes.Device.ID
|
||||||
|
|
||||||
|
// initiate UIA for the second device
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/_matrix/client/v3/devices/"+secondDeviceID, strings.NewReader(""))
|
||||||
|
req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
|
||||||
|
routers.Client.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusUnauthorized {
|
||||||
|
t.Fatalf("expected HTTP 401, got %d: %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
// get the session ID
|
||||||
|
sessionID := gjson.GetBytes(rec.Body.Bytes(), "session").Str
|
||||||
|
|
||||||
|
// prepare UIA request body
|
||||||
|
reqBody := bytes.Buffer{}
|
||||||
|
if err := json.NewEncoder(&reqBody).Encode(map[string]interface{}{
|
||||||
|
"auth": map[string]string{
|
||||||
|
"session": sessionID,
|
||||||
|
"type": authtypes.LoginTypePassword,
|
||||||
|
"user": alice.ID,
|
||||||
|
"password": accessTokens[alice].password,
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy the request body, so we can use it again for the successful delete
|
||||||
|
reqBody2 := reqBody
|
||||||
|
|
||||||
|
// do the same request again, this time with our UIA, but for a different device ID, this should fail
|
||||||
|
rec = httptest.NewRecorder()
|
||||||
|
|
||||||
|
req = httptest.NewRequest(http.MethodDelete, "/_matrix/client/v3/devices/"+accessTokens[alice].deviceID, &reqBody)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
|
||||||
|
routers.Client.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusForbidden {
|
||||||
|
t.Fatalf("expected HTTP 403, got %d: %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// do the same request again, this time with our UIA, but for the correct device ID, this should be fine
|
||||||
|
rec = httptest.NewRecorder()
|
||||||
|
req = httptest.NewRequest(http.MethodDelete, "/_matrix/client/v3/devices/"+secondDeviceID, &reqBody2)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
|
||||||
|
routers.Client.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected HTTP 200, got %d: %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify devices are deleted
|
||||||
|
rec = httptest.NewRecorder()
|
||||||
|
req = httptest.NewRequest(http.MethodGet, "/_matrix/client/v3/devices", strings.NewReader(""))
|
||||||
|
req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
|
||||||
|
routers.Client.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected HTTP 200, got %d: %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
for _, device := range gjson.GetBytes(rec.Body.Bytes(), "devices.#.device_id").Array() {
|
||||||
|
if device.Str == secondDeviceID {
|
||||||
|
t.Fatalf("expected device %s to be deleted, but wasn't", secondDeviceID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deleting devices requires the UIA dance, so do this in a different test
|
||||||
|
func TestDeleteDevices(t *testing.T) {
|
||||||
|
alice := test.NewUser(t)
|
||||||
|
localpart, serverName, _ := gomatrixserverlib.SplitID('@', alice.ID)
|
||||||
|
|
||||||
|
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
||||||
|
cfg, processCtx, closeDB := testrig.CreateConfig(t, dbType)
|
||||||
|
defer closeDB()
|
||||||
|
|
||||||
|
natsInstance := jetstream.NATSInstance{}
|
||||||
|
routers := httputil.NewRouters()
|
||||||
|
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||||
|
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
||||||
|
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
|
||||||
|
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil)
|
||||||
|
|
||||||
|
// We mostly need the rsAPI/ for this test, so nil for other APIs/caches etc.
|
||||||
|
AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, caching.DisableMetrics)
|
||||||
|
|
||||||
|
accessTokens := map[*test.User]userDevice{
|
||||||
|
alice: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the account and an initial device
|
||||||
|
createAccessTokens(t, accessTokens, userAPI, processCtx.Context(), routers)
|
||||||
|
|
||||||
|
// create some more devices
|
||||||
|
var devices []string
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
accessToken := util.RandomString(8)
|
||||||
|
devRes := &uapi.PerformDeviceCreationResponse{}
|
||||||
|
if err := userAPI.PerformDeviceCreation(processCtx.Context(), &uapi.PerformDeviceCreationRequest{
|
||||||
|
Localpart: localpart,
|
||||||
|
ServerName: serverName,
|
||||||
|
AccessToken: accessToken,
|
||||||
|
NoDeviceListUpdate: true,
|
||||||
|
}, devRes); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !devRes.DeviceCreated {
|
||||||
|
t.Fatalf("failed to create device")
|
||||||
|
}
|
||||||
|
devices = append(devices, devRes.Device.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// initiate UIA
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/_matrix/client/v3/delete_devices", strings.NewReader(""))
|
||||||
|
req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
|
||||||
|
routers.Client.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusUnauthorized {
|
||||||
|
t.Fatalf("expected HTTP 401, got %d: %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
// get the session ID
|
||||||
|
sessionID := gjson.GetBytes(rec.Body.Bytes(), "session").Str
|
||||||
|
|
||||||
|
// prepare UIA request body
|
||||||
|
reqBody := bytes.Buffer{}
|
||||||
|
if err := json.NewEncoder(&reqBody).Encode(map[string]interface{}{
|
||||||
|
"auth": map[string]string{
|
||||||
|
"session": sessionID,
|
||||||
|
"type": authtypes.LoginTypePassword,
|
||||||
|
"user": alice.ID,
|
||||||
|
"password": accessTokens[alice].password,
|
||||||
|
},
|
||||||
|
"devices": devices[5:],
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// do the same request again, this time with our UIA,
|
||||||
|
rec = httptest.NewRecorder()
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/_matrix/client/v3/delete_devices", &reqBody)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
|
||||||
|
routers.Client.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected HTTP 200, got %d: %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify devices are deleted
|
||||||
|
rec = httptest.NewRecorder()
|
||||||
|
req = httptest.NewRequest(http.MethodGet, "/_matrix/client/v3/devices", strings.NewReader(""))
|
||||||
|
req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
|
||||||
|
routers.Client.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected HTTP 200, got %d: %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
for _, device := range gjson.GetBytes(rec.Body.Bytes(), "devices.#.device_id").Array() {
|
||||||
|
for _, deletedDevice := range devices[5:] {
|
||||||
|
if device.Str == deletedDevice {
|
||||||
|
t.Fatalf("expected device %s to be deleted, but wasn't", deletedDevice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAccessTokens(t *testing.T, accessTokens map[*test.User]userDevice, userAPI uapi.UserInternalAPI, ctx context.Context, routers httputil.Routers) {
|
||||||
|
t.Helper()
|
||||||
|
for u := range accessTokens {
|
||||||
|
localpart, serverName, _ := gomatrixserverlib.SplitID('@', u.ID)
|
||||||
|
userRes := &uapi.PerformAccountCreationResponse{}
|
||||||
|
password := util.RandomString(8)
|
||||||
|
if err := userAPI.PerformAccountCreation(ctx, &uapi.PerformAccountCreationRequest{
|
||||||
|
AccountType: u.AccountType,
|
||||||
|
Localpart: localpart,
|
||||||
|
ServerName: serverName,
|
||||||
|
Password: password,
|
||||||
|
}, userRes); err != nil {
|
||||||
|
t.Errorf("failed to create account: %s", err)
|
||||||
|
}
|
||||||
|
req := test.NewRequest(t, http.MethodPost, "/_matrix/client/v3/login", test.WithJSONBody(t, map[string]interface{}{
|
||||||
|
"type": authtypes.LoginTypePassword,
|
||||||
|
"identifier": map[string]interface{}{
|
||||||
|
"type": "m.id.user",
|
||||||
|
"user": u.ID,
|
||||||
|
},
|
||||||
|
"password": password,
|
||||||
|
}))
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
routers.Client.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("failed to login: %s", rec.Body.String())
|
||||||
|
}
|
||||||
|
accessTokens[u] = userDevice{
|
||||||
|
accessToken: gjson.GetBytes(rec.Body.Bytes(), "access_token").String(),
|
||||||
|
deviceID: gjson.GetBytes(rec.Body.Bytes(), "device_id").String(),
|
||||||
|
password: password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,23 +22,16 @@ import (
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func AdminEvacuateRoom(req *http.Request, cfg *config.ClientAPI, device *api.Device, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
func AdminEvacuateRoom(req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
||||||
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err)
|
||||||
}
|
}
|
||||||
roomID, ok := vars["roomID"]
|
|
||||||
if !ok {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: jsonerror.MissingArgument("Expecting room ID."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res := &roomserverAPI.PerformAdminEvacuateRoomResponse{}
|
res := &roomserverAPI.PerformAdminEvacuateRoomResponse{}
|
||||||
if err := rsAPI.PerformAdminEvacuateRoom(
|
if err := rsAPI.PerformAdminEvacuateRoom(
|
||||||
req.Context(),
|
req.Context(),
|
||||||
&roomserverAPI.PerformAdminEvacuateRoomRequest{
|
&roomserverAPI.PerformAdminEvacuateRoomRequest{
|
||||||
RoomID: roomID,
|
RoomID: vars["roomID"],
|
||||||
},
|
},
|
||||||
res,
|
res,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|
@ -55,18 +48,13 @@ func AdminEvacuateRoom(req *http.Request, cfg *config.ClientAPI, device *api.Dev
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func AdminEvacuateUser(req *http.Request, cfg *config.ClientAPI, device *api.Device, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
func AdminEvacuateUser(req *http.Request, cfg *config.ClientAPI, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
||||||
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err)
|
||||||
}
|
}
|
||||||
userID, ok := vars["userID"]
|
userID := vars["userID"]
|
||||||
if !ok {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: jsonerror.MissingArgument("Expecting user ID."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_, domain, err := gomatrixserverlib.SplitID('@', userID)
|
_, domain, err := gomatrixserverlib.SplitID('@', userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.MessageResponse(http.StatusBadRequest, err.Error())
|
return util.MessageResponse(http.StatusBadRequest, err.Error())
|
||||||
|
|
@ -103,13 +91,8 @@ func AdminPurgeRoom(req *http.Request, cfg *config.ClientAPI, device *api.Device
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err)
|
||||||
}
|
}
|
||||||
roomID, ok := vars["roomID"]
|
roomID := vars["roomID"]
|
||||||
if !ok {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: jsonerror.MissingArgument("Expecting room ID."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res := &roomserverAPI.PerformAdminPurgeRoomResponse{}
|
res := &roomserverAPI.PerformAdminPurgeRoomResponse{}
|
||||||
if err := rsAPI.PerformAdminPurgeRoom(
|
if err := rsAPI.PerformAdminPurgeRoom(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ func Deactivate(
|
||||||
return *errRes
|
return *errRes
|
||||||
}
|
}
|
||||||
|
|
||||||
localpart, _, err := gomatrixserverlib.SplitID('@', login.Username())
|
localpart, serverName, err := gomatrixserverlib.SplitID('@', login.Username())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
||||||
return jsonerror.InternalServerError()
|
return jsonerror.InternalServerError()
|
||||||
|
|
@ -42,6 +42,7 @@ func Deactivate(
|
||||||
var res api.PerformAccountDeactivationResponse
|
var res api.PerformAccountDeactivationResponse
|
||||||
err = accountAPI.PerformAccountDeactivation(ctx, &api.PerformAccountDeactivationRequest{
|
err = accountAPI.PerformAccountDeactivation(ctx, &api.PerformAccountDeactivationRequest{
|
||||||
Localpart: localpart,
|
Localpart: localpart,
|
||||||
|
ServerName: serverName,
|
||||||
}, &res)
|
}, &res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("userAPI.PerformAccountDeactivation failed")
|
util.GetLogger(ctx).WithError(err).Error("userAPI.PerformAccountDeactivation failed")
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -146,12 +147,6 @@ func UpdateDeviceByID(
|
||||||
JSON: jsonerror.Forbidden("device does not exist"),
|
JSON: jsonerror.Forbidden("device does not exist"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if performRes.Forbidden {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: jsonerror.Forbidden("device not owned by current user"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
|
|
@ -189,7 +184,7 @@ func DeleteDeviceById(
|
||||||
if dev != deviceID {
|
if dev != deviceID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("session & device mismatch"),
|
JSON: jsonerror.Forbidden("session and device mismatch"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -242,16 +237,37 @@ func DeleteDeviceById(
|
||||||
|
|
||||||
// DeleteDevices handles POST requests to /delete_devices
|
// DeleteDevices handles POST requests to /delete_devices
|
||||||
func DeleteDevices(
|
func DeleteDevices(
|
||||||
req *http.Request, userAPI api.ClientUserAPI, device *api.Device,
|
req *http.Request, userInteractiveAuth *auth.UserInteractive, userAPI api.ClientUserAPI, device *api.Device,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
ctx := req.Context()
|
ctx := req.Context()
|
||||||
payload := devicesDeleteJSON{}
|
|
||||||
|
|
||||||
if resErr := httputil.UnmarshalJSONRequest(req, &payload); resErr != nil {
|
bodyBytes, err := io.ReadAll(req.Body)
|
||||||
return *resErr
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON("The request body could not be read: " + err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer req.Body.Close() // nolint:errcheck
|
||||||
|
|
||||||
|
// initiate UIA
|
||||||
|
login, errRes := userInteractiveAuth.Verify(ctx, bodyBytes, device)
|
||||||
|
if errRes != nil {
|
||||||
|
return *errRes
|
||||||
}
|
}
|
||||||
|
|
||||||
defer req.Body.Close() // nolint: errcheck
|
if login.Username() != device.UserID {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: jsonerror.Forbidden("unable to delete devices for other user"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := devicesDeleteJSON{}
|
||||||
|
if err = json.Unmarshal(bodyBytes, &payload); err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("unable to unmarshal device deletion request")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
|
||||||
var res api.PerformDeviceDeletionResponse
|
var res api.PerformDeviceDeletionResponse
|
||||||
if err := userAPI.PerformDeviceDeletion(ctx, &api.PerformDeviceDeletionRequest{
|
if err := userAPI.PerformDeviceDeletion(ctx, &api.PerformDeviceDeletionRequest{
|
||||||
|
|
|
||||||
|
|
@ -155,15 +155,15 @@ func Setup(
|
||||||
|
|
||||||
dendriteAdminRouter.Handle("/admin/evacuateRoom/{roomID}",
|
dendriteAdminRouter.Handle("/admin/evacuateRoom/{roomID}",
|
||||||
httputil.MakeAdminAPI("admin_evacuate_room", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAdminAPI("admin_evacuate_room", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
return AdminEvacuateRoom(req, cfg, device, rsAPI)
|
return AdminEvacuateRoom(req, rsAPI)
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodGet, http.MethodOptions)
|
).Methods(http.MethodPost, http.MethodOptions)
|
||||||
|
|
||||||
dendriteAdminRouter.Handle("/admin/evacuateUser/{userID}",
|
dendriteAdminRouter.Handle("/admin/evacuateUser/{userID}",
|
||||||
httputil.MakeAdminAPI("admin_evacuate_user", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAdminAPI("admin_evacuate_user", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
return AdminEvacuateUser(req, cfg, device, rsAPI)
|
return AdminEvacuateUser(req, cfg, rsAPI)
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodGet, http.MethodOptions)
|
).Methods(http.MethodPost, http.MethodOptions)
|
||||||
|
|
||||||
dendriteAdminRouter.Handle("/admin/purgeRoom/{roomID}",
|
dendriteAdminRouter.Handle("/admin/purgeRoom/{roomID}",
|
||||||
httputil.MakeAdminAPI("admin_purge_room", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAdminAPI("admin_purge_room", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
|
@ -1115,7 +1115,7 @@ func Setup(
|
||||||
|
|
||||||
v3mux.Handle("/delete_devices",
|
v3mux.Handle("/delete_devices",
|
||||||
httputil.MakeAuthAPI("delete_devices", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAuthAPI("delete_devices", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
return DeleteDevices(req, userAPI, device)
|
return DeleteDevices(req, userInteractiveAuth, userAPI, device)
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodPost, http.MethodOptions)
|
).Methods(http.MethodPost, http.MethodOptions)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,7 @@ func (p *P2PMonolith) Stop() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *P2PMonolith) WaitForShutdown() {
|
func (p *P2PMonolith) WaitForShutdown() {
|
||||||
p.ProcessCtx.WaitForShutdown()
|
base.WaitForShutdown(p.ProcessCtx)
|
||||||
p.closeAllResources()
|
p.closeAllResources()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ UPDATE userapi_accounts SET account_type = 3 WHERE localpart = '$localpart';
|
||||||
|
|
||||||
Where `$localpart` is the username only (e.g. `alice`).
|
Where `$localpart` is the username only (e.g. `alice`).
|
||||||
|
|
||||||
## GET `/_dendrite/admin/evacuateRoom/{roomID}`
|
## POST `/_dendrite/admin/evacuateRoom/{roomID}`
|
||||||
|
|
||||||
This endpoint will instruct Dendrite to part all local users from the given `roomID`
|
This endpoint will instruct Dendrite to part all local users from the given `roomID`
|
||||||
in the URL. It may take some time to complete. A JSON body will be returned containing
|
in the URL. It may take some time to complete. A JSON body will be returned containing
|
||||||
|
|
@ -41,7 +41,7 @@ the user IDs of all affected users.
|
||||||
If the room has an alias set (e.g. is published), the room's ID will not be visible in the URL, but it can
|
If the room has an alias set (e.g. is published), the room's ID will not be visible in the URL, but it can
|
||||||
be found as the room's "internal ID" in Element Web (Settings -> Advanced)
|
be found as the room's "internal ID" in Element Web (Settings -> Advanced)
|
||||||
|
|
||||||
## GET `/_dendrite/admin/evacuateUser/{userID}`
|
## POST `/_dendrite/admin/evacuateUser/{userID}`
|
||||||
|
|
||||||
This endpoint will instruct Dendrite to part the given local `userID` in the URL from
|
This endpoint will instruct Dendrite to part the given local `userID` in the URL from
|
||||||
all rooms which they are currently joined. A JSON body will be returned containing
|
all rooms which they are currently joined. A JSON body will be returned containing
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
name: dendrite
|
name: dendrite
|
||||||
version: "0.12.1"
|
version: "0.12.2"
|
||||||
appVersion: "0.12.0"
|
appVersion: "0.12.0"
|
||||||
description: Dendrite Matrix Homeserver
|
description: Dendrite Matrix Homeserver
|
||||||
type: application
|
type: application
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,9 @@
|
||||||
{{- define "validate.config" }}
|
{{- define "validate.config" }}
|
||||||
{{- if not .Values.signing_key.create -}}
|
{{- if and (not .Values.signing_key.create) (eq .Values.signing_key.existingSecret "") -}}
|
||||||
{{- fail "You must create a signing key for configuration.signing_key. (see https://github.com/matrix-org/dendrite/blob/master/docs/INSTALL.md#server-key-generation)" -}}
|
{{- fail "You must create a signing key for configuration.signing_key OR specify an existing secret name in .Values.signing_key.existingSecret to mount it. (see https://github.com/matrix-org/dendrite/blob/master/docs/INSTALL.md#server-key-generation)" -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- if not (or .Values.dendrite_config.global.database.host .Values.postgresql.enabled) -}}
|
{{- if and (not .Values.postgresql.enabled) (eq .Values.dendrite_config.global.database.connection_string "") -}}
|
||||||
{{- fail "Database server must be set." -}}
|
{{- fail "Database connection string must be set." -}}
|
||||||
{{- end -}}
|
|
||||||
{{- if not (or .Values.dendrite_config.global.database.user .Values.postgresql.enabled) -}}
|
|
||||||
{{- fail "Database user must be set." -}}
|
|
||||||
{{- end -}}
|
|
||||||
{{- if not (or .Values.dendrite_config.global.database.password .Values.postgresql.enabled) -}}
|
|
||||||
{{- fail "Database password must be set." -}}
|
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,19 @@ spec:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
{{- include "dendrite.selectorLabels" . | nindent 6 }}
|
{{- include "dendrite.selectorLabels" . | nindent 6 }}
|
||||||
replicas: 1
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: {{ $.Values.strategy.type }}
|
||||||
|
{{- if eq $.Values.strategy.type "RollingUpdate" }}
|
||||||
|
rollingUpdate:
|
||||||
|
maxSurge: {{ $.Values.strategy.rollingUpdate.maxSurge }}
|
||||||
|
maxUnavailable: {{ $.Values.strategy.rollingUpdate.maxUnavailable }}
|
||||||
|
{{- end }}
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
{{- include "dendrite.selectorLabels" . | nindent 8 }}
|
{{- include "dendrite.selectorLabels" . | nindent 8 }}
|
||||||
annotations:
|
annotations:
|
||||||
confighash-global: secret-{{ .Values.global | toYaml | sha256sum | trunc 32 }}
|
confighash: secret-{{ .Values.dendrite_config | toYaml | sha256sum | trunc 32 }}
|
||||||
confighash-clientapi: clientapi-{{ .Values.clientapi | toYaml | sha256sum | trunc 32 }}
|
|
||||||
confighash-federationapi: federationapi-{{ .Values.federationapi | toYaml | sha256sum | trunc 32 }}
|
|
||||||
confighash-mediaapi: mediaapi-{{ .Values.mediaapi | toYaml | sha256sum | trunc 32 }}
|
|
||||||
confighash-syncapi: syncapi-{{ .Values.syncapi | toYaml | sha256sum | trunc 32 }}
|
|
||||||
spec:
|
spec:
|
||||||
volumes:
|
volumes:
|
||||||
- name: {{ include "dendrite.fullname" . }}-conf-vol
|
- name: {{ include "dendrite.fullname" . }}-conf-vol
|
||||||
|
|
@ -44,6 +47,9 @@ spec:
|
||||||
- name: {{ include "dendrite.fullname" . }}-search
|
- name: {{ include "dendrite.fullname" . }}-search
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: {{ default (print ( include "dendrite.fullname" . ) "-search-pvc") $.Values.persistence.search.existingClaim | quote }}
|
claimName: {{ default (print ( include "dendrite.fullname" . ) "-search-pvc") $.Values.persistence.search.existingClaim | quote }}
|
||||||
|
{{- with .Values.extraVolumes }}
|
||||||
|
{{ . | toYaml | nindent 6 }}
|
||||||
|
{{- end }}
|
||||||
containers:
|
containers:
|
||||||
- name: {{ .Chart.Name }}
|
- name: {{ .Chart.Name }}
|
||||||
{{- include "image.name" . | nindent 8 }}
|
{{- include "image.name" . | nindent 8 }}
|
||||||
|
|
@ -57,7 +63,7 @@ spec:
|
||||||
{{- if $.Values.dendrite_config.global.profiling.enabled }}
|
{{- if $.Values.dendrite_config.global.profiling.enabled }}
|
||||||
env:
|
env:
|
||||||
- name: PPROFLISTEN
|
- name: PPROFLISTEN
|
||||||
value: "localhost:{{- $.Values.global.profiling.port -}}"
|
value: "localhost:{{- $.Values.dendrite_config.global.profiling.port -}}"
|
||||||
{{- end }}
|
{{- end }}
|
||||||
resources:
|
resources:
|
||||||
{{- toYaml $.Values.resources | nindent 10 }}
|
{{- toYaml $.Values.resources | nindent 10 }}
|
||||||
|
|
@ -77,6 +83,9 @@ spec:
|
||||||
name: {{ include "dendrite.fullname" . }}-jetstream
|
name: {{ include "dendrite.fullname" . }}-jetstream
|
||||||
- mountPath: {{ .Values.dendrite_config.sync_api.search.index_path }}
|
- mountPath: {{ .Values.dendrite_config.sync_api.search.index_path }}
|
||||||
name: {{ include "dendrite.fullname" . }}-search
|
name: {{ include "dendrite.fullname" . }}-search
|
||||||
|
{{- with .Values.extraVolumeMounts }}
|
||||||
|
{{ . | toYaml | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
initialDelaySeconds: 10
|
initialDelaySeconds: 10
|
||||||
periodSeconds: 10
|
periodSeconds: 10
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,30 @@ persistence:
|
||||||
# -- PVC Storage Request for the search volume
|
# -- PVC Storage Request for the search volume
|
||||||
capacity: "1Gi"
|
capacity: "1Gi"
|
||||||
|
|
||||||
|
# Add additional volumes to the Dendrite Pod
|
||||||
|
extraVolumes: []
|
||||||
|
# ex.
|
||||||
|
# - name: extra-config
|
||||||
|
# secret:
|
||||||
|
# secretName: extra-config
|
||||||
|
|
||||||
|
|
||||||
|
# Configure additional mount points volumes in the Dendrite Pod
|
||||||
|
extraVolumeMounts: []
|
||||||
|
# ex.
|
||||||
|
# - mountPath: /etc/dendrite/extra-config
|
||||||
|
# name: extra-config
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
# -- Strategy to use for rolling updates (e.g. Recreate, RollingUpdate)
|
||||||
|
# If you are using ReadWriteOnce volumes, you should probably use Recreate
|
||||||
|
type: RollingUpdate
|
||||||
|
rollingUpdate:
|
||||||
|
# -- Maximum number of pods that can be unavailable during the update process
|
||||||
|
maxUnavailable: 25%
|
||||||
|
# -- Maximum number of pods that can be scheduled above the desired number of pods
|
||||||
|
maxSurge: 25%
|
||||||
|
|
||||||
dendrite_config:
|
dendrite_config:
|
||||||
version: 2
|
version: 2
|
||||||
global:
|
global:
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
package fulltext
|
package fulltext
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/blevesearch/bleve/v2"
|
"github.com/blevesearch/bleve/v2"
|
||||||
|
|
@ -60,6 +61,7 @@ type Indexer interface {
|
||||||
Index(elements ...IndexElement) error
|
Index(elements ...IndexElement) error
|
||||||
Delete(eventID string) error
|
Delete(eventID string) error
|
||||||
Search(term string, roomIDs, keys []string, limit, from int, orderByStreamPos bool) (*bleve.SearchResult, error)
|
Search(term string, roomIDs, keys []string, limit, from int, orderByStreamPos bool) (*bleve.SearchResult, error)
|
||||||
|
GetHighlights(result *bleve.SearchResult) []string
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,6 +126,47 @@ func (f *Search) Delete(eventID string) error {
|
||||||
return f.FulltextIndex.Delete(eventID)
|
return f.FulltextIndex.Delete(eventID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var highlightMatcher = regexp.MustCompile("<mark>(.*?)</mark>")
|
||||||
|
|
||||||
|
// GetHighlights extracts the highlights from a SearchResult.
|
||||||
|
func (f *Search) GetHighlights(result *bleve.SearchResult) []string {
|
||||||
|
if result == nil {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
seenMatches := make(map[string]struct{})
|
||||||
|
|
||||||
|
for _, hit := range result.Hits {
|
||||||
|
if hit.Fragments == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fragments, ok := hit.Fragments["Content"]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, x := range fragments {
|
||||||
|
substringMatches := highlightMatcher.FindAllStringSubmatch(x, -1)
|
||||||
|
for _, matches := range substringMatches {
|
||||||
|
for i := range matches {
|
||||||
|
if i == 0 { // skip first match, this is the complete substring match
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seenMatches[matches[i]]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenMatches[matches[i]] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res := make([]string, 0, len(seenMatches))
|
||||||
|
for m := range seenMatches {
|
||||||
|
res = append(res, m)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
// Search searches the index given a search term, roomIDs and keys.
|
// Search searches the index given a search term, roomIDs and keys.
|
||||||
func (f *Search) Search(term string, roomIDs, keys []string, limit, from int, orderByStreamPos bool) (*bleve.SearchResult, error) {
|
func (f *Search) Search(term string, roomIDs, keys []string, limit, from int, orderByStreamPos bool) (*bleve.SearchResult, error) {
|
||||||
qry := bleve.NewConjunctionQuery()
|
qry := bleve.NewConjunctionQuery()
|
||||||
|
|
@ -163,6 +206,10 @@ func (f *Search) Search(term string, roomIDs, keys []string, limit, from int, or
|
||||||
s.SortBy([]string{"-StreamPosition"})
|
s.SortBy([]string{"-StreamPosition"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Highlight some words
|
||||||
|
s.Highlight = bleve.NewHighlight()
|
||||||
|
s.Highlight.Fields = []string{"Content"}
|
||||||
|
|
||||||
return f.FulltextIndex.Search(s)
|
return f.FulltextIndex.Search(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -164,10 +164,12 @@ func TestSearch(t *testing.T) {
|
||||||
args args
|
args args
|
||||||
wantCount int
|
wantCount int
|
||||||
wantErr bool
|
wantErr bool
|
||||||
|
wantHighlights []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Can search for many results in one room",
|
name: "Can search for many results in one room",
|
||||||
wantCount: 16,
|
wantCount: 16,
|
||||||
|
wantHighlights: []string{"lorem"},
|
||||||
args: args{
|
args: args{
|
||||||
term: "lorem",
|
term: "lorem",
|
||||||
roomIndex: []int{0},
|
roomIndex: []int{0},
|
||||||
|
|
@ -177,6 +179,7 @@ func TestSearch(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "Can search for one result in one room",
|
name: "Can search for one result in one room",
|
||||||
wantCount: 1,
|
wantCount: 1,
|
||||||
|
wantHighlights: []string{"lorem"},
|
||||||
args: args{
|
args: args{
|
||||||
term: "lorem",
|
term: "lorem",
|
||||||
roomIndex: []int{16},
|
roomIndex: []int{16},
|
||||||
|
|
@ -186,6 +189,7 @@ func TestSearch(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "Can search for many results in multiple rooms",
|
name: "Can search for many results in multiple rooms",
|
||||||
wantCount: 17,
|
wantCount: 17,
|
||||||
|
wantHighlights: []string{"lorem"},
|
||||||
args: args{
|
args: args{
|
||||||
term: "lorem",
|
term: "lorem",
|
||||||
roomIndex: []int{0, 16},
|
roomIndex: []int{0, 16},
|
||||||
|
|
@ -195,6 +199,7 @@ func TestSearch(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "Can search for many results in all rooms, reversed",
|
name: "Can search for many results in all rooms, reversed",
|
||||||
wantCount: 30,
|
wantCount: 30,
|
||||||
|
wantHighlights: []string{"lorem"},
|
||||||
args: args{
|
args: args{
|
||||||
term: "lorem",
|
term: "lorem",
|
||||||
limit: 30,
|
limit: 30,
|
||||||
|
|
@ -204,6 +209,7 @@ func TestSearch(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "Can search for specific search room name",
|
name: "Can search for specific search room name",
|
||||||
wantCount: 1,
|
wantCount: 1,
|
||||||
|
wantHighlights: []string{"testing"},
|
||||||
args: args{
|
args: args{
|
||||||
term: "testing",
|
term: "testing",
|
||||||
roomIndex: []int{},
|
roomIndex: []int{},
|
||||||
|
|
@ -214,6 +220,7 @@ func TestSearch(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "Can search for specific search room topic",
|
name: "Can search for specific search room topic",
|
||||||
wantCount: 1,
|
wantCount: 1,
|
||||||
|
wantHighlights: []string{"fulltext"},
|
||||||
args: args{
|
args: args{
|
||||||
term: "fulltext",
|
term: "fulltext",
|
||||||
roomIndex: []int{},
|
roomIndex: []int{},
|
||||||
|
|
@ -222,6 +229,7 @@ func TestSearch(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
f, ctx := mustOpenIndex(t, "")
|
f, ctx := mustOpenIndex(t, "")
|
||||||
|
|
@ -238,6 +246,12 @@ func TestSearch(t *testing.T) {
|
||||||
t.Errorf("Search() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("Search() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
highlights := f.GetHighlights(got)
|
||||||
|
if !reflect.DeepEqual(highlights, tt.wantHighlights) {
|
||||||
|
t.Errorf("Search() got highligts = %v, want %v", highlights, tt.wantHighlights)
|
||||||
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(len(got.Hits), tt.wantCount) {
|
if !reflect.DeepEqual(len(got.Hits), tt.wantCount) {
|
||||||
t.Errorf("Search() got = %v, want %v", len(got.Hits), tt.wantCount)
|
t.Errorf("Search() got = %v, want %v", len(got.Hits), tt.wantCount)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ type Indexer interface {
|
||||||
Index(elements ...IndexElement) error
|
Index(elements ...IndexElement) error
|
||||||
Delete(eventID string) error
|
Delete(eventID string) error
|
||||||
Search(term string, roomIDs, keys []string, limit, from int, orderByStreamPos bool) (SearchResult, error)
|
Search(term string, roomIDs, keys []string, limit, from int, orderByStreamPos bool) (SearchResult, error)
|
||||||
|
GetHighlights(result SearchResult) []string
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,3 +72,7 @@ func (f *Search) Delete(eventID string) error {
|
||||||
func (f *Search) Search(term string, roomIDs, keys []string, limit, from int, orderByStreamPos bool) (SearchResult, error) {
|
func (f *Search) Search(term string, roomIDs, keys []string, limit, from int, orderByStreamPos bool) (SearchResult, error) {
|
||||||
return SearchResult{}, nil
|
return SearchResult{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Search) GetHighlights(result SearchResult) []string {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -227,6 +227,7 @@ func (r *Admin) PerformAdminEvacuateUser(
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
res.Affected = append(res.Affected, roomID)
|
||||||
if len(outputEvents) == 0 {
|
if len(outputEvents) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -237,8 +238,6 @@ func (r *Admin) PerformAdminEvacuateUser(
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
res.Affected = append(res.Affected, roomID)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/blevesearch/bleve/v2/search"
|
"github.com/blevesearch/bleve/v2/search"
|
||||||
|
|
@ -123,8 +122,8 @@ func Search(req *http.Request, device *api.Device, syncDB storage.Database, fts
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
JSON: SearchResponse{
|
JSON: SearchResponse{
|
||||||
SearchCategories: SearchCategories{
|
SearchCategories: SearchCategoriesResponse{
|
||||||
RoomEvents: RoomEvents{
|
RoomEvents: RoomEventsResponse{
|
||||||
Count: int(result.Total),
|
Count: int(result.Total),
|
||||||
NextBatch: nil,
|
NextBatch: nil,
|
||||||
},
|
},
|
||||||
|
|
@ -158,7 +157,7 @@ func Search(req *http.Request, device *api.Device, syncDB storage.Database, fts
|
||||||
}
|
}
|
||||||
|
|
||||||
groups := make(map[string]RoomResult)
|
groups := make(map[string]RoomResult)
|
||||||
knownUsersProfiles := make(map[string]ProfileInfo)
|
knownUsersProfiles := make(map[string]ProfileInfoResponse)
|
||||||
|
|
||||||
// Sort the events by depth, as the returned values aren't ordered
|
// Sort the events by depth, as the returned values aren't ordered
|
||||||
if orderByTime {
|
if orderByTime {
|
||||||
|
|
@ -180,7 +179,7 @@ func Search(req *http.Request, device *api.Device, syncDB storage.Database, fts
|
||||||
return jsonerror.InternalServerError()
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
|
|
||||||
profileInfos := make(map[string]ProfileInfo)
|
profileInfos := make(map[string]ProfileInfoResponse)
|
||||||
for _, ev := range append(eventsBefore, eventsAfter...) {
|
for _, ev := range append(eventsBefore, eventsAfter...) {
|
||||||
profile, ok := knownUsersProfiles[event.Sender()]
|
profile, ok := knownUsersProfiles[event.Sender()]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -192,7 +191,7 @@ func Search(req *http.Request, device *api.Device, syncDB storage.Database, fts
|
||||||
if stateEvent == nil {
|
if stateEvent == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
profile = ProfileInfo{
|
profile = ProfileInfoResponse{
|
||||||
AvatarURL: gjson.GetBytes(stateEvent.Content(), "avatar_url").Str,
|
AvatarURL: gjson.GetBytes(stateEvent.Content(), "avatar_url").Str,
|
||||||
DisplayName: gjson.GetBytes(stateEvent.Content(), "displayname").Str,
|
DisplayName: gjson.GetBytes(stateEvent.Content(), "displayname").Str,
|
||||||
}
|
}
|
||||||
|
|
@ -237,13 +236,13 @@ func Search(req *http.Request, device *api.Device, syncDB storage.Database, fts
|
||||||
}
|
}
|
||||||
|
|
||||||
res := SearchResponse{
|
res := SearchResponse{
|
||||||
SearchCategories: SearchCategories{
|
SearchCategories: SearchCategoriesResponse{
|
||||||
RoomEvents: RoomEvents{
|
RoomEvents: RoomEventsResponse{
|
||||||
Count: int(result.Total),
|
Count: int(result.Total),
|
||||||
Groups: Groups{RoomID: groups},
|
Groups: Groups{RoomID: groups},
|
||||||
Results: results,
|
Results: results,
|
||||||
NextBatch: nextBatchResult,
|
NextBatch: nextBatchResult,
|
||||||
Highlights: strings.Split(searchReq.SearchCategories.RoomEvents.SearchTerm, " "),
|
Highlights: fts.GetHighlights(result),
|
||||||
State: stateForRooms,
|
State: stateForRooms,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -286,30 +285,40 @@ func contextEvents(
|
||||||
return eventsBefore, eventsAfter, err
|
return eventsBefore, eventsAfter, err
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchRequest struct {
|
type EventContext struct {
|
||||||
SearchCategories struct {
|
|
||||||
RoomEvents struct {
|
|
||||||
EventContext struct {
|
|
||||||
AfterLimit int `json:"after_limit,omitempty"`
|
AfterLimit int `json:"after_limit,omitempty"`
|
||||||
BeforeLimit int `json:"before_limit,omitempty"`
|
BeforeLimit int `json:"before_limit,omitempty"`
|
||||||
IncludeProfile bool `json:"include_profile,omitempty"`
|
IncludeProfile bool `json:"include_profile,omitempty"`
|
||||||
} `json:"event_context"`
|
}
|
||||||
Filter gomatrixserverlib.RoomEventFilter `json:"filter"`
|
|
||||||
Groupings struct {
|
type GroupBy struct {
|
||||||
GroupBy []struct {
|
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
} `json:"group_by"`
|
}
|
||||||
} `json:"groupings"`
|
|
||||||
|
type Groupings struct {
|
||||||
|
GroupBy []GroupBy `json:"group_by"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoomEvents struct {
|
||||||
|
EventContext EventContext `json:"event_context"`
|
||||||
|
Filter gomatrixserverlib.RoomEventFilter `json:"filter"`
|
||||||
|
Groupings Groupings `json:"groupings"`
|
||||||
IncludeState bool `json:"include_state"`
|
IncludeState bool `json:"include_state"`
|
||||||
Keys []string `json:"keys"`
|
Keys []string `json:"keys"`
|
||||||
OrderBy string `json:"order_by"`
|
OrderBy string `json:"order_by"`
|
||||||
SearchTerm string `json:"search_term"`
|
SearchTerm string `json:"search_term"`
|
||||||
} `json:"room_events"`
|
}
|
||||||
} `json:"search_categories"`
|
|
||||||
|
type SearchCategories struct {
|
||||||
|
RoomEvents RoomEvents `json:"room_events"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchRequest struct {
|
||||||
|
SearchCategories SearchCategories `json:"search_categories"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchResponse struct {
|
type SearchResponse struct {
|
||||||
SearchCategories SearchCategories `json:"search_categories"`
|
SearchCategories SearchCategoriesResponse `json:"search_categories"`
|
||||||
}
|
}
|
||||||
type RoomResult struct {
|
type RoomResult struct {
|
||||||
NextBatch *string `json:"next_batch,omitempty"`
|
NextBatch *string `json:"next_batch,omitempty"`
|
||||||
|
|
@ -332,15 +341,15 @@ type SearchContextResponse struct {
|
||||||
EventsAfter []gomatrixserverlib.ClientEvent `json:"events_after"`
|
EventsAfter []gomatrixserverlib.ClientEvent `json:"events_after"`
|
||||||
EventsBefore []gomatrixserverlib.ClientEvent `json:"events_before"`
|
EventsBefore []gomatrixserverlib.ClientEvent `json:"events_before"`
|
||||||
Start string `json:"start"`
|
Start string `json:"start"`
|
||||||
ProfileInfo map[string]ProfileInfo `json:"profile_info"`
|
ProfileInfo map[string]ProfileInfoResponse `json:"profile_info"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProfileInfo struct {
|
type ProfileInfoResponse struct {
|
||||||
AvatarURL string `json:"avatar_url"`
|
AvatarURL string `json:"avatar_url"`
|
||||||
DisplayName string `json:"display_name"`
|
DisplayName string `json:"display_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RoomEvents struct {
|
type RoomEventsResponse struct {
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
Groups Groups `json:"groups"`
|
Groups Groups `json:"groups"`
|
||||||
Highlights []string `json:"highlights"`
|
Highlights []string `json:"highlights"`
|
||||||
|
|
@ -348,6 +357,6 @@ type RoomEvents struct {
|
||||||
Results []Result `json:"results"`
|
Results []Result `json:"results"`
|
||||||
State map[string][]gomatrixserverlib.ClientEvent `json:"state,omitempty"`
|
State map[string][]gomatrixserverlib.ClientEvent `json:"state,omitempty"`
|
||||||
}
|
}
|
||||||
type SearchCategories struct {
|
type SearchCategoriesResponse struct {
|
||||||
RoomEvents RoomEvents `json:"room_events"`
|
RoomEvents RoomEventsResponse `json:"room_events"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
264
syncapi/routing/search_test.go
Normal file
264
syncapi/routing/search_test.go
Normal file
|
|
@ -0,0 +1,264 @@
|
||||||
|
package routing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/internal/fulltext"
|
||||||
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||||
|
"github.com/matrix-org/dendrite/syncapi/storage"
|
||||||
|
"github.com/matrix-org/dendrite/syncapi/types"
|
||||||
|
"github.com/matrix-org/dendrite/test"
|
||||||
|
"github.com/matrix-org/dendrite/test/testrig"
|
||||||
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSearch(t *testing.T) {
|
||||||
|
alice := test.NewUser(t)
|
||||||
|
aliceDevice := userapi.Device{UserID: alice.ID}
|
||||||
|
room := test.NewRoom(t, alice)
|
||||||
|
room.CreateAndInsert(t, alice, "m.room.message", map[string]interface{}{"body": "context before"})
|
||||||
|
room.CreateAndInsert(t, alice, "m.room.message", map[string]interface{}{"body": "hello world3!"})
|
||||||
|
room.CreateAndInsert(t, alice, "m.room.message", map[string]interface{}{"body": "context after"})
|
||||||
|
|
||||||
|
roomsFilter := []string{room.ID}
|
||||||
|
roomsFilterUnknown := []string{"!unknown"}
|
||||||
|
|
||||||
|
emptyFromString := ""
|
||||||
|
fromStringValid := "1"
|
||||||
|
fromStringInvalid := "iCantBeParsed"
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
wantOK bool
|
||||||
|
searchReq SearchRequest
|
||||||
|
device *userapi.Device
|
||||||
|
wantResponseCount int
|
||||||
|
from *string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no user ID",
|
||||||
|
searchReq: SearchRequest{},
|
||||||
|
device: &userapi.Device{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with alice ID",
|
||||||
|
wantOK: true,
|
||||||
|
searchReq: SearchRequest{},
|
||||||
|
device: &aliceDevice,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "searchTerm specified, found at the beginning",
|
||||||
|
wantOK: true,
|
||||||
|
searchReq: SearchRequest{
|
||||||
|
SearchCategories: SearchCategories{RoomEvents: RoomEvents{SearchTerm: "hello"}},
|
||||||
|
},
|
||||||
|
device: &aliceDevice,
|
||||||
|
wantResponseCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "searchTerm specified, found at the end",
|
||||||
|
wantOK: true,
|
||||||
|
searchReq: SearchRequest{
|
||||||
|
SearchCategories: SearchCategories{RoomEvents: RoomEvents{SearchTerm: "world3"}},
|
||||||
|
},
|
||||||
|
device: &aliceDevice,
|
||||||
|
wantResponseCount: 1,
|
||||||
|
},
|
||||||
|
/* the following would need matchQuery.SetFuzziness(1) in bleve.go
|
||||||
|
{
|
||||||
|
name: "searchTerm fuzzy search",
|
||||||
|
wantOK: true,
|
||||||
|
searchReq: SearchRequest{
|
||||||
|
SearchCategories: SearchCategories{RoomEvents: RoomEvents{SearchTerm: "hell"}}, // this still should find hello world
|
||||||
|
},
|
||||||
|
device: &aliceDevice,
|
||||||
|
wantResponseCount: 1,
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
name: "searchTerm specified but no result",
|
||||||
|
wantOK: true,
|
||||||
|
searchReq: SearchRequest{
|
||||||
|
SearchCategories: SearchCategories{RoomEvents: RoomEvents{SearchTerm: "i don't match"}},
|
||||||
|
},
|
||||||
|
device: &aliceDevice,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "filter on room",
|
||||||
|
wantOK: true,
|
||||||
|
searchReq: SearchRequest{
|
||||||
|
SearchCategories: SearchCategories{
|
||||||
|
RoomEvents: RoomEvents{
|
||||||
|
SearchTerm: "hello",
|
||||||
|
Filter: gomatrixserverlib.RoomEventFilter{
|
||||||
|
Rooms: &roomsFilter,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
device: &aliceDevice,
|
||||||
|
wantResponseCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "filter on unknown room",
|
||||||
|
searchReq: SearchRequest{
|
||||||
|
SearchCategories: SearchCategories{
|
||||||
|
RoomEvents: RoomEvents{
|
||||||
|
SearchTerm: "hello",
|
||||||
|
Filter: gomatrixserverlib.RoomEventFilter{
|
||||||
|
Rooms: &roomsFilterUnknown,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
device: &aliceDevice,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "include state",
|
||||||
|
wantOK: true,
|
||||||
|
searchReq: SearchRequest{
|
||||||
|
SearchCategories: SearchCategories{
|
||||||
|
RoomEvents: RoomEvents{
|
||||||
|
SearchTerm: "hello",
|
||||||
|
Filter: gomatrixserverlib.RoomEventFilter{
|
||||||
|
Rooms: &roomsFilter,
|
||||||
|
},
|
||||||
|
IncludeState: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
device: &aliceDevice,
|
||||||
|
wantResponseCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty from does not error",
|
||||||
|
wantOK: true,
|
||||||
|
searchReq: SearchRequest{
|
||||||
|
SearchCategories: SearchCategories{
|
||||||
|
RoomEvents: RoomEvents{
|
||||||
|
SearchTerm: "hello",
|
||||||
|
Filter: gomatrixserverlib.RoomEventFilter{
|
||||||
|
Rooms: &roomsFilter,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantResponseCount: 1,
|
||||||
|
device: &aliceDevice,
|
||||||
|
from: &emptyFromString,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid from does not error",
|
||||||
|
wantOK: true,
|
||||||
|
searchReq: SearchRequest{
|
||||||
|
SearchCategories: SearchCategories{
|
||||||
|
RoomEvents: RoomEvents{
|
||||||
|
SearchTerm: "hello",
|
||||||
|
Filter: gomatrixserverlib.RoomEventFilter{
|
||||||
|
Rooms: &roomsFilter,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantResponseCount: 1,
|
||||||
|
device: &aliceDevice,
|
||||||
|
from: &fromStringValid,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid from does error",
|
||||||
|
searchReq: SearchRequest{
|
||||||
|
SearchCategories: SearchCategories{
|
||||||
|
RoomEvents: RoomEvents{
|
||||||
|
SearchTerm: "hello",
|
||||||
|
Filter: gomatrixserverlib.RoomEventFilter{
|
||||||
|
Rooms: &roomsFilter,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
device: &aliceDevice,
|
||||||
|
from: &fromStringInvalid,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "order by stream position",
|
||||||
|
wantOK: true,
|
||||||
|
searchReq: SearchRequest{
|
||||||
|
SearchCategories: SearchCategories{RoomEvents: RoomEvents{SearchTerm: "hello", OrderBy: "recent"}},
|
||||||
|
},
|
||||||
|
device: &aliceDevice,
|
||||||
|
wantResponseCount: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
||||||
|
cfg, processCtx, closeDB := testrig.CreateConfig(t, dbType)
|
||||||
|
defer closeDB()
|
||||||
|
|
||||||
|
// create requisites
|
||||||
|
fts, err := fulltext.New(processCtx, cfg.SyncAPI.Fulltext)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, fts)
|
||||||
|
|
||||||
|
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||||
|
db, err := storage.NewSyncServerDatasource(processCtx.Context(), cm, &cfg.SyncAPI.Database)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
elements := []fulltext.IndexElement{}
|
||||||
|
// store the events in the database
|
||||||
|
var sp types.StreamPosition
|
||||||
|
for _, x := range room.Events() {
|
||||||
|
var stateEvents []*gomatrixserverlib.HeaderedEvent
|
||||||
|
var stateEventIDs []string
|
||||||
|
if x.Type() == gomatrixserverlib.MRoomMember {
|
||||||
|
stateEvents = append(stateEvents, x)
|
||||||
|
stateEventIDs = append(stateEventIDs, x.EventID())
|
||||||
|
}
|
||||||
|
sp, err = db.WriteEvent(processCtx.Context(), x, stateEvents, stateEventIDs, nil, nil, false, gomatrixserverlib.HistoryVisibilityShared)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if x.Type() != "m.room.message" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
elements = append(elements, fulltext.IndexElement{
|
||||||
|
EventID: x.EventID(),
|
||||||
|
RoomID: x.RoomID(),
|
||||||
|
Content: string(x.Content()),
|
||||||
|
ContentType: x.Type(),
|
||||||
|
StreamPosition: int64(sp),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Index the events
|
||||||
|
err = fts.Index(elements...)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// run the tests
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
reqBody := &bytes.Buffer{}
|
||||||
|
err = json.NewEncoder(reqBody).Encode(tc.searchReq)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/", reqBody)
|
||||||
|
|
||||||
|
res := Search(req, tc.device, db, fts, tc.from)
|
||||||
|
if !tc.wantOK && !res.Is2xx() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, ok := res.JSON.(SearchResponse)
|
||||||
|
if !ok && !tc.wantOK {
|
||||||
|
t.Fatalf("not a SearchResponse: %T: %s", res.JSON, res.JSON)
|
||||||
|
}
|
||||||
|
assert.Equal(t, tc.wantResponseCount, resp.SearchCategories.RoomEvents.Count)
|
||||||
|
|
||||||
|
// if we requested state, it should not be empty
|
||||||
|
if tc.searchReq.SearchCategories.RoomEvents.IncludeState {
|
||||||
|
assert.NotEmpty(t, resp.SearchCategories.RoomEvents.State)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -224,7 +224,6 @@ type PerformDeviceUpdateRequest struct {
|
||||||
}
|
}
|
||||||
type PerformDeviceUpdateResponse struct {
|
type PerformDeviceUpdateResponse struct {
|
||||||
DeviceExists bool
|
DeviceExists bool
|
||||||
Forbidden bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PerformDeviceDeletionRequest struct {
|
type PerformDeviceDeletionRequest struct {
|
||||||
|
|
|
||||||
|
|
@ -386,11 +386,6 @@ func (a *UserInternalAPI) PerformDeviceUpdate(ctx context.Context, req *api.Perf
|
||||||
}
|
}
|
||||||
res.DeviceExists = true
|
res.DeviceExists = true
|
||||||
|
|
||||||
if dev.UserID != req.RequestingUserID {
|
|
||||||
res.Forbidden = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = a.DB.UpdateDevice(ctx, localpart, domain, req.DeviceID, req.DisplayName)
|
err = a.DB.UpdateDevice(ctx, localpart, domain, req.DeviceID, req.DisplayName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("deviceDB.UpdateDevice failed")
|
util.GetLogger(ctx).WithError(err).Error("deviceDB.UpdateDevice failed")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue