package federationapi import ( "context" "crypto/rand" "encoding/base64" "encoding/json" "time" "github.com/dariuszjaniak/warp/server/internal/federation" "github.com/dariuszjaniak/warp/server/internal/matrix/id" ) // EDUs turns the sync API's local typing, receipts and to-device messages // into queued EDUs. It implements syncapi.Outbound. type EDUs struct { queue *Queue rooms RoomServers } // NewEDUs builds the adapter. func NewEDUs(queue *Queue, rooms RoomServers) *EDUs { return &EDUs{queue: queue, rooms: rooms} } // roomServers is everybody in a room but this server; the queue drops // this server anyway, and an error is logged rather than returned, because // a typing notification that does not go out is not worth failing the // client's request over. func (e *EDUs) roomServers(ctx context.Context, roomID string) []string { servers, err := e.rooms.JoinedServers(ctx, roomID) if err != nil { e.queue.log.Warn("could not read a room's servers for an EDU", "room_id", roomID, "error", err) return nil } return servers } // Typing implements syncapi.Outbound. func (e *EDUs) Typing(ctx context.Context, roomID, userID string, typing bool, _ time.Duration) { if !e.queue.Running() { return } content, _ := json.Marshal(federation.TypingContent{RoomID: roomID, UserID: userID, Typing: typing}) e.queue.QueueEDU(ctx, e.roomServers(ctx, roomID), federation.EDU{Type: federation.EDUTyping, Content: content}) } // Receipt implements syncapi.Outbound. func (e *EDUs) Receipt(ctx context.Context, roomID, userID, receiptType, eventID, threadID string, ts int64) { if !e.queue.Running() || receiptType != "m.read" { return } var data federation.ReceiptData data.EventIDs = []string{eventID} data.Data.TS = ts data.Data.ThreadID = threadID content, _ := json.Marshal(federation.ReceiptContent{roomID: {receiptType: {userID: data}}}) e.queue.QueueEDU(ctx, e.roomServers(ctx, roomID), federation.EDU{Type: federation.EDUReceipt, Content: content}) } // ToDevice implements syncapi.Outbound: one m.direct_to_device per // destination, carrying that server's recipients only. func (e *EDUs) ToDevice(ctx context.Context, sender, eventType string, messages map[string]map[string]json.RawMessage) { if !e.queue.Running() { return } byServer := map[string]map[string]map[string]json.RawMessage{} for userID, devices := range messages { server, err := id.ServerNameOf(userID) if err != nil { continue } if byServer[server] == nil { byServer[server] = map[string]map[string]json.RawMessage{} } byServer[server][userID] = devices } for server, recipients := range byServer { content, err := json.Marshal(federation.DirectToDeviceContent{ Sender: sender, Type: eventType, MessageID: messageID(), Messages: recipients, }) if err != nil { continue } e.queue.QueueEDU(ctx, []string{server}, federation.EDU{Type: federation.EDUDirectToDev, Content: content}) } } // messageID is the per-message identifier a receiving server deduplicates // a to-device message by. func messageID() string { buf := make([]byte, 12) _, _ = rand.Read(buf) return base64.RawURLEncoding.EncodeToString(buf) }