mirror of
https://github.com/hoernschen/dendrite.git
synced 2025-07-31 13:22:46 +00:00
Add presence module V2 (#2312)
* Syncapi presence * Clientapi http presence handler * Why is this here? * Missing files * FederationAPI presence implementation * Add new presence stream * Pinecone update * Pinecone update * Add passing tests * Make linter happy * Add presence producer * Add presence config option * Set user to unavailable after x minutes * Only set currently_active if online Avoid unneeded presence updates when syncing * Tweaks * Query devices for last_active_ts Fixes & tweaks * Export SharedUsers/SharedUsers * Presence stream in MemoryStorage * Remove status_msg_nil * Fix sytest crashes * Make presence types const and use stringer for it * Change options to allow inbound/outbound presence * Fix option & typo * Update configs Co-authored-by: Neil Alexander <neilalexander@users.noreply.github.com>
This commit is contained in:
parent
16e2d243fc
commit
e5e3350ce1
38 changed files with 1706 additions and 66 deletions
|
@ -17,6 +17,8 @@
|
|||
package sync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
@ -33,8 +35,10 @@ import (
|
|||
"github.com/matrix-org/dendrite/syncapi/streams"
|
||||
"github.com/matrix-org/dendrite/syncapi/types"
|
||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||
"github.com/matrix-org/gomatrixserverlib"
|
||||
"github.com/matrix-org/util"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// RequestPool manages HTTP long-poll connections for /sync
|
||||
|
@ -44,9 +48,15 @@ type RequestPool struct {
|
|||
userAPI userapi.UserInternalAPI
|
||||
keyAPI keyapi.KeyInternalAPI
|
||||
rsAPI roomserverAPI.RoomserverInternalAPI
|
||||
lastseen sync.Map
|
||||
lastseen *sync.Map
|
||||
presence *sync.Map
|
||||
streams *streams.Streams
|
||||
Notifier *notifier.Notifier
|
||||
producer PresencePublisher
|
||||
}
|
||||
|
||||
type PresencePublisher interface {
|
||||
SendPresence(userID string, presence types.Presence, statusMsg *string) error
|
||||
}
|
||||
|
||||
// NewRequestPool makes a new RequestPool
|
||||
|
@ -55,6 +65,7 @@ func NewRequestPool(
|
|||
userAPI userapi.UserInternalAPI, keyAPI keyapi.KeyInternalAPI,
|
||||
rsAPI roomserverAPI.RoomserverInternalAPI,
|
||||
streams *streams.Streams, notifier *notifier.Notifier,
|
||||
producer PresencePublisher,
|
||||
) *RequestPool {
|
||||
rp := &RequestPool{
|
||||
db: db,
|
||||
|
@ -62,11 +73,14 @@ func NewRequestPool(
|
|||
userAPI: userAPI,
|
||||
keyAPI: keyAPI,
|
||||
rsAPI: rsAPI,
|
||||
lastseen: sync.Map{},
|
||||
lastseen: &sync.Map{},
|
||||
presence: &sync.Map{},
|
||||
streams: streams,
|
||||
Notifier: notifier,
|
||||
producer: producer,
|
||||
}
|
||||
go rp.cleanLastSeen()
|
||||
go rp.cleanPresence(db, time.Minute*5)
|
||||
return rp
|
||||
}
|
||||
|
||||
|
@ -80,6 +94,68 @@ func (rp *RequestPool) cleanLastSeen() {
|
|||
}
|
||||
}
|
||||
|
||||
func (rp *RequestPool) cleanPresence(db storage.Presence, cleanupTime time.Duration) {
|
||||
if !rp.cfg.Matrix.Presence.EnableOutbound {
|
||||
return
|
||||
}
|
||||
for {
|
||||
rp.presence.Range(func(key interface{}, v interface{}) bool {
|
||||
p := v.(types.PresenceInternal)
|
||||
if time.Since(p.LastActiveTS.Time()) > cleanupTime {
|
||||
rp.updatePresence(db, types.PresenceUnavailable.String(), p.UserID)
|
||||
rp.presence.Delete(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
time.Sleep(cleanupTime)
|
||||
}
|
||||
}
|
||||
|
||||
// updatePresence sends presence updates to the SyncAPI and FederationAPI
|
||||
func (rp *RequestPool) updatePresence(db storage.Presence, presence string, userID string) {
|
||||
if !rp.cfg.Matrix.Presence.EnableOutbound {
|
||||
return
|
||||
}
|
||||
if presence == "" {
|
||||
presence = types.PresenceOnline.String()
|
||||
}
|
||||
|
||||
presenceID, ok := types.PresenceFromString(presence)
|
||||
if !ok { // this should almost never happen
|
||||
logrus.Errorf("unknown presence '%s'", presence)
|
||||
return
|
||||
}
|
||||
|
||||
newPresence := types.PresenceInternal{
|
||||
ClientFields: types.PresenceClientResponse{
|
||||
Presence: presenceID.String(),
|
||||
},
|
||||
Presence: presenceID,
|
||||
UserID: userID,
|
||||
LastActiveTS: gomatrixserverlib.AsTimestamp(time.Now()),
|
||||
}
|
||||
defer rp.presence.Store(userID, newPresence)
|
||||
// avoid spamming presence updates when syncing
|
||||
existingPresence, ok := rp.presence.LoadOrStore(userID, newPresence)
|
||||
if ok {
|
||||
p := existingPresence.(types.PresenceInternal)
|
||||
if p.ClientFields.Presence == newPresence.ClientFields.Presence {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ensure we also send the current status_msg to federated servers and not nil
|
||||
dbPresence, err := db.GetPresence(context.Background(), userID)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return
|
||||
}
|
||||
|
||||
if err := rp.producer.SendPresence(userID, presenceID, dbPresence.ClientFields.StatusMsg); err != nil {
|
||||
logrus.WithError(err).Error("Unable to publish presence message from sync")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (rp *RequestPool) updateLastSeen(req *http.Request, device *userapi.Device) {
|
||||
if _, ok := rp.lastseen.LoadOrStore(device.UserID+device.ID, struct{}{}); ok {
|
||||
return
|
||||
|
@ -156,6 +232,7 @@ func (rp *RequestPool) OnIncomingSyncRequest(req *http.Request, device *userapi.
|
|||
defer activeSyncRequests.Dec()
|
||||
|
||||
rp.updateLastSeen(req, device)
|
||||
rp.updatePresence(rp.db, req.FormValue("set_presence"), device.UserID)
|
||||
|
||||
waitingSyncRequests.Inc()
|
||||
defer waitingSyncRequests.Dec()
|
||||
|
@ -219,6 +296,9 @@ func (rp *RequestPool) OnIncomingSyncRequest(req *http.Request, device *userapi.
|
|||
DeviceListPosition: rp.streams.DeviceListStreamProvider.CompleteSync(
|
||||
syncReq.Context, syncReq,
|
||||
),
|
||||
PresencePosition: rp.streams.PresenceStreamProvider.CompleteSync(
|
||||
syncReq.Context, syncReq,
|
||||
),
|
||||
}
|
||||
} else {
|
||||
// Incremental sync
|
||||
|
@ -255,6 +335,10 @@ func (rp *RequestPool) OnIncomingSyncRequest(req *http.Request, device *userapi.
|
|||
syncReq.Context, syncReq,
|
||||
syncReq.Since.DeviceListPosition, currentPos.DeviceListPosition,
|
||||
),
|
||||
PresencePosition: rp.streams.PresenceStreamProvider.IncrementalSync(
|
||||
syncReq.Context, syncReq,
|
||||
syncReq.Since.PresencePosition, currentPos.PresencePosition,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
128
syncapi/sync/requestpool_test.go
Normal file
128
syncapi/sync/requestpool_test.go
Normal file
|
@ -0,0 +1,128 @@
|
|||
package sync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/matrix-org/dendrite/setup/config"
|
||||
"github.com/matrix-org/dendrite/syncapi/types"
|
||||
"github.com/matrix-org/gomatrixserverlib"
|
||||
)
|
||||
|
||||
type dummyPublisher struct {
|
||||
count int
|
||||
}
|
||||
|
||||
func (d *dummyPublisher) SendPresence(userID string, presence types.Presence, statusMsg *string) error {
|
||||
d.count++
|
||||
return nil
|
||||
}
|
||||
|
||||
type dummyDB struct{}
|
||||
|
||||
func (d dummyDB) UpdatePresence(ctx context.Context, userID string, presence types.Presence, statusMsg *string, lastActiveTS gomatrixserverlib.Timestamp, fromSync bool) (types.StreamPosition, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (d dummyDB) GetPresence(ctx context.Context, userID string) (*types.PresenceInternal, error) {
|
||||
return &types.PresenceInternal{}, nil
|
||||
}
|
||||
|
||||
func (d dummyDB) PresenceAfter(ctx context.Context, after types.StreamPosition) (map[string]*types.PresenceInternal, error) {
|
||||
return map[string]*types.PresenceInternal{}, nil
|
||||
}
|
||||
|
||||
func (d dummyDB) MaxStreamPositionForPresence(ctx context.Context) (types.StreamPosition, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func TestRequestPool_updatePresence(t *testing.T) {
|
||||
type args struct {
|
||||
presence string
|
||||
userID string
|
||||
sleep time.Duration
|
||||
}
|
||||
publisher := &dummyPublisher{}
|
||||
syncMap := sync.Map{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantIncrease bool
|
||||
}{
|
||||
{
|
||||
name: "new presence is published",
|
||||
wantIncrease: true,
|
||||
args: args{
|
||||
userID: "dummy",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "presence not published, no change",
|
||||
args: args{
|
||||
userID: "dummy",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "new presence is published dummy2",
|
||||
wantIncrease: true,
|
||||
args: args{
|
||||
userID: "dummy2",
|
||||
presence: "online",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "different presence is published dummy2",
|
||||
wantIncrease: true,
|
||||
args: args{
|
||||
userID: "dummy2",
|
||||
presence: "unavailable",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "same presence is not published dummy2",
|
||||
args: args{
|
||||
userID: "dummy2",
|
||||
presence: "unavailable",
|
||||
sleep: time.Millisecond * 150,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "same presence is published after being deleted",
|
||||
wantIncrease: true,
|
||||
args: args{
|
||||
userID: "dummy2",
|
||||
presence: "unavailable",
|
||||
},
|
||||
},
|
||||
}
|
||||
rp := &RequestPool{
|
||||
presence: &syncMap,
|
||||
producer: publisher,
|
||||
cfg: &config.SyncAPI{
|
||||
Matrix: &config.Global{
|
||||
JetStream: config.JetStream{
|
||||
TopicPrefix: "Dendrite",
|
||||
},
|
||||
Presence: config.PresenceOptions{
|
||||
EnableInbound: true,
|
||||
EnableOutbound: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
db := dummyDB{}
|
||||
go rp.cleanPresence(db, time.Millisecond*50)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
beforeCount := publisher.count
|
||||
rp.updatePresence(db, tt.args.presence, tt.args.userID)
|
||||
if tt.wantIncrease && publisher.count <= beforeCount {
|
||||
t.Fatalf("expected count to increase: %d <= %d", publisher.count, beforeCount)
|
||||
}
|
||||
time.Sleep(tt.args.sleep)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue