Overhaul event content representation

This commit is contained in:
Tulir Asokan 2020-04-19 03:23:27 +03:00
commit f4fc99cddb
23 changed files with 1279 additions and 496 deletions

View file

@ -358,9 +358,9 @@ func (cli *Client) MakeRequest(method string, httpURL string, reqBody interface{
}
// CreateFilter makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-user-userid-filter
func (cli *Client) CreateFilter(filter json.RawMessage) (resp *RespCreateFilter, err error) {
func (cli *Client) CreateFilter(filter *Filter) (resp *RespCreateFilter, err error) {
urlPath := cli.BuildURL("user", cli.UserID, "filter")
_, err = cli.MakeRequest("POST", urlPath, &filter, &resp)
_, err = cli.MakeRequest("POST", urlPath, filter, &resp)
return
}
@ -620,7 +620,7 @@ func (cli *Client) SendMassagedStateEvent(roomID id.RoomID, eventType event.Type
// SendText sends an m.room.message event into the given room with a msgtype of m.text
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#m-text
func (cli *Client) SendText(roomID id.RoomID, text string) (*RespSendEvent, error) {
return cli.SendMessageEvent(roomID, event.EventMessage, event.Content{
return cli.SendMessageEvent(roomID, event.EventMessage, event.MessageEventContent{
MsgType: event.MsgText,
Body: text,
})
@ -629,7 +629,7 @@ func (cli *Client) SendText(roomID id.RoomID, text string) (*RespSendEvent, erro
// SendImage sends an m.room.message event into the given room with a msgtype of m.image
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-image
func (cli *Client) SendImage(roomID id.RoomID, body string, url id.ContentURI) (*RespSendEvent, error) {
return cli.SendMessageEvent(roomID, event.EventMessage, event.Content{
return cli.SendMessageEvent(roomID, event.EventMessage, event.MessageEventContent{
MsgType: event.MsgImage,
Body: body,
URL: url.CUString(),
@ -639,7 +639,7 @@ func (cli *Client) SendImage(roomID id.RoomID, body string, url id.ContentURI) (
// SendVideo sends an m.room.message event into the given room with a msgtype of m.video
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-video
func (cli *Client) SendVideo(roomID id.RoomID, body string, url id.ContentURI) (*RespSendEvent, error) {
return cli.SendMessageEvent(roomID, event.EventMessage, event.Content{
return cli.SendMessageEvent(roomID, event.EventMessage, event.MessageEventContent{
MsgType: event.MsgVideo,
Body: body,
URL: url.CUString(),
@ -649,15 +649,15 @@ func (cli *Client) SendVideo(roomID id.RoomID, body string, url id.ContentURI) (
// SendNotice sends an m.room.message event into the given room with a msgtype of m.notice
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#m-notice
func (cli *Client) SendNotice(roomID id.RoomID, text string) (*RespSendEvent, error) {
return cli.SendMessageEvent(roomID, event.EventMessage, event.Content{
return cli.SendMessageEvent(roomID, event.EventMessage, event.MessageEventContent{
MsgType: event.MsgNotice,
Body: text,
})
}
func (cli *Client) SendReaction(roomID id.RoomID, eventID id.EventID, reaction string) (*RespSendEvent, error) {
return cli.SendMessageEvent(roomID, event.EventReaction, event.Content{
RelatesTo: &event.RelatesTo{
return cli.SendMessageEvent(roomID, event.EventReaction, event.ReactionEventContent{
RelatesTo: event.RelatesTo{
EventID: eventID,
Type: event.RelAnnotation,
Key: reaction,
@ -882,10 +882,15 @@ func (cli *Client) JoinedRooms() (resp *RespJoinedRooms, err error) {
// pagination query parameters to paginate history in the room.
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-messages
func (cli *Client) Messages(roomID id.RoomID, from, to string, dir rune, limit int) (resp *RespMessages, err error) {
filter := cli.Syncer.GetFilterJSON(cli.UserID)
filterJSON, err := json.Marshal(filter)
if err != nil {
return nil, err
}
query := map[string]string{
"from": from,
"dir": string(dir),
"filter": string(cli.Syncer.GetFilterJSON(cli.UserID)),
"filter": string(filterJSON),
}
if to != "" {
query["to"] = to

51
event/accountdata.go Normal file
View file

@ -0,0 +1,51 @@
// Copyright (c) 2020 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package event
import (
"encoding/json"
"maunium.net/go/mautrix/id"
)
// TagEventContent represents the content of a m.tag room account data event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-tag
type TagEventContent struct {
Tags Tags `json:"tags"`
}
type Tags map[string]Tag
type Tag struct {
Order json.Number `json:"order"`
}
// DirectChatsEventContent represents the content of a m.direct account data event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-direct
type DirectChatsEventContent map[id.UserID][]id.RoomID
// PushRulesEventContent represents the content of a m.push_rules account data event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-push-rules
//type PushRulesEventContent struct {
// Global *pushrules.PushRuleset `json:"global"`
//}
// FullyReadEventContent represents the content of a m.fully_read account data event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-fully-read
type FullyReadEventContent struct {
EventID id.EventID `json:"event_id"`
}
// IgnoredUserListEventContent represents the content of a m.ignored_user_list account data event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-ignored-user-list
type IgnoredUserListEventContent struct {
IgnoredUsers map[id.UserID]IgnoredUser `json:"ignored_users"`
}
type IgnoredUser struct {
// This is an empty object
}

302
event/content.go Normal file
View file

@ -0,0 +1,302 @@
// Copyright (c) 2020 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package event
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"github.com/fatih/structs"
)
// TypeMap is a mapping from event type to the content struct type.
// This is used by Content.ParseRaw() for creating the correct type of struct.
var TypeMap = map[Type]reflect.Type{
StateMember: reflect.TypeOf(MemberEventContent{}),
StatePowerLevels: reflect.TypeOf(PowerLevelsEventContent{}),
StateCanonicalAlias: reflect.TypeOf(CanonicalAliasEventContent{}),
StateRoomName: reflect.TypeOf(RoomNameEventContent{}),
StateRoomAvatar: reflect.TypeOf(RoomAvatarEventContent{}),
StateTopic: reflect.TypeOf(TopicEventContent{}),
StateTombstone: reflect.TypeOf(TombstoneEventContent{}),
StateCreate: reflect.TypeOf(CreateEventContent{}),
StateJoinRules: reflect.TypeOf(JoinRulesEventContent{}),
StateHistoryVisibility: reflect.TypeOf(HistoryVisibilityEventContent{}),
StateGuestAccess: reflect.TypeOf(GuestAccessEventContent{}),
StatePinnedEvents: reflect.TypeOf(PinnedEventsEventContent{}),
EventMessage: reflect.TypeOf(MessageEventContent{}),
EventSticker: reflect.TypeOf(MessageEventContent{}),
EventEncrypted: reflect.TypeOf(EncryptedEventContent{}),
EventRedaction: reflect.TypeOf(RedactionEventContent{}),
EventReaction: reflect.TypeOf(ReactionEventContent{}),
AccountDataRoomTags: reflect.TypeOf(TagEventContent{}),
AccountDataDirectChats: reflect.TypeOf(DirectChatsEventContent{}),
AccountDataFullyRead: reflect.TypeOf(FullyReadEventContent{}),
AccountDataIgnoredUserList: reflect.TypeOf(IgnoredUserListEventContent{}),
//AccountDataPushRules: reflect.TypeOf(PushRulesEventContent{}),
EphemeralEventTyping: reflect.TypeOf(TypingEventContent{}),
EphemeralEventReceipt: reflect.TypeOf(ReceiptEventContent{}),
EphemeralEventPresence: reflect.TypeOf(PresenceEventContent{}),
ToDeviceRoomKey: reflect.TypeOf(RoomKeyEventContent{}),
ToDeviceForwardedRoomKey: reflect.TypeOf(ForwardedRoomKeyEventContent{}),
ToDeviceRoomKeyRequest: reflect.TypeOf(RoomKeyRequestEventContent{}),
}
// Content stores the content of a Matrix event.
//
// By default, the content is only parsed into a map[string]interface{}. However, you can call ParseRaw with the
// correct event type to parse the content into a nicer struct, which you can then access from Parsed or via the
// helper functions.
type Content struct {
VeryRaw json.RawMessage
Raw map[string]interface{}
Parsed interface{}
}
func (content *Content) UnmarshalJSON(data []byte) error {
content.VeryRaw = data
err := json.Unmarshal(data, &content.Raw)
return err
}
func (content *Content) MarshalJSON() ([]byte, error) {
if content.Raw == nil {
if content.Parsed == nil {
if content.VeryRaw == nil {
return nil, errors.New("no content")
}
return content.VeryRaw, nil
}
return json.Marshal(content.Parsed)
} else if content.Parsed != nil {
content.MergeParsedToRaw()
}
return json.Marshal(content.Raw)
}
func mergeMaps(into, from map[string]interface{}) {
for key, newValue := range from {
existingValue, ok := into[key]
if !ok {
into[key] = newValue
continue
}
existingValueMap, okEx := existingValue.(map[string]interface{})
newValueMap, okNew := newValue.(map[string]interface{})
if okEx && okNew {
mergeMaps(existingValueMap, newValueMap)
} else {
into[key] = newValue
}
}
}
func (content *Content) ParseRaw(evtType Type) error {
structType, ok := TypeMap[evtType]
if !ok {
return fmt.Errorf("unsupported content type %s", evtType.String())
}
content.Parsed = reflect.New(structType).Interface()
return json.Unmarshal(content.VeryRaw, &content.Parsed)
}
func (content *Content) MergeParsedToRaw() {
s := structs.New(content.Parsed)
s.TagName = "json"
mergeMaps(content.Raw, s.Map())
}
// Helper cast functions below
func (content *Content) AsMember() *MemberEventContent {
casted, ok := content.Parsed.(*MemberEventContent)
if !ok {
return &MemberEventContent{}
}
return casted
}
func (content *Content) AsPowerLevels() *PowerLevelsEventContent {
casted, ok := content.Parsed.(*PowerLevelsEventContent)
if !ok {
return &PowerLevelsEventContent{}
}
return casted
}
func (content *Content) AsCanonicalAlias() *CanonicalAliasEventContent {
casted, ok := content.Parsed.(*CanonicalAliasEventContent)
if !ok {
return &CanonicalAliasEventContent{}
}
return casted
}
func (content *Content) AsRoomName() *RoomNameEventContent {
casted, ok := content.Parsed.(*RoomNameEventContent)
if !ok {
return &RoomNameEventContent{}
}
return casted
}
func (content *Content) AsRoomAvatar() *RoomAvatarEventContent {
casted, ok := content.Parsed.(*RoomAvatarEventContent)
if !ok {
return &RoomAvatarEventContent{}
}
return casted
}
func (content *Content) AsTopic() *TopicEventContent {
casted, ok := content.Parsed.(*TopicEventContent)
if !ok {
return &TopicEventContent{}
}
return casted
}
func (content *Content) AsTombstone() *TombstoneEventContent {
casted, ok := content.Parsed.(*TombstoneEventContent)
if !ok {
return &TombstoneEventContent{}
}
return casted
}
func (content *Content) AsCreate() *CreateEventContent {
casted, ok := content.Parsed.(*CreateEventContent)
if !ok {
return &CreateEventContent{}
}
return casted
}
func (content *Content) AsJoinRules() *JoinRulesEventContent {
casted, ok := content.Parsed.(*JoinRulesEventContent)
if !ok {
return &JoinRulesEventContent{}
}
return casted
}
func (content *Content) AsHistoryVisibility() *HistoryVisibilityEventContent {
casted, ok := content.Parsed.(*HistoryVisibilityEventContent)
if !ok {
return &HistoryVisibilityEventContent{}
}
return casted
}
func (content *Content) AsGuestAccess() *GuestAccessEventContent {
casted, ok := content.Parsed.(*GuestAccessEventContent)
if !ok {
return &GuestAccessEventContent{}
}
return casted
}
func (content *Content) AsPinnedEvents() *PinnedEventsEventContent {
casted, ok := content.Parsed.(*PinnedEventsEventContent)
if !ok {
return &PinnedEventsEventContent{}
}
return casted
}
func (content *Content) AsMessage() *MessageEventContent {
casted, ok := content.Parsed.(*MessageEventContent)
if !ok {
return &MessageEventContent{}
}
return casted
}
func (content *Content) AsEncrypted() *EncryptedEventContent {
casted, ok := content.Parsed.(*EncryptedEventContent)
if !ok {
return &EncryptedEventContent{}
}
return casted
}
func (content *Content) AsRedaction() *RedactionEventContent {
casted, ok := content.Parsed.(*RedactionEventContent)
if !ok {
return &RedactionEventContent{}
}
return casted
}
func (content *Content) AsReaction() *ReactionEventContent {
casted, ok := content.Parsed.(*ReactionEventContent)
if !ok {
return &ReactionEventContent{}
}
return casted
}
func (content *Content) AsTag() *TagEventContent {
casted, ok := content.Parsed.(*TagEventContent)
if !ok {
return &TagEventContent{}
}
return casted
}
func (content *Content) AsDirectChats() *DirectChatsEventContent {
casted, ok := content.Parsed.(*DirectChatsEventContent)
if !ok {
return &DirectChatsEventContent{}
}
return casted
}
func (content *Content) AsFullyRead() *FullyReadEventContent {
casted, ok := content.Parsed.(*FullyReadEventContent)
if !ok {
return &FullyReadEventContent{}
}
return casted
}
func (content *Content) AsIgnoredUserList() *IgnoredUserListEventContent {
casted, ok := content.Parsed.(*IgnoredUserListEventContent)
if !ok {
return &IgnoredUserListEventContent{}
}
return casted
}
func (content *Content) AsTyping() *TypingEventContent {
casted, ok := content.Parsed.(*TypingEventContent)
if !ok {
return &TypingEventContent{}
}
return casted
}
func (content *Content) AsReceipt() *ReceiptEventContent {
casted, ok := content.Parsed.(*ReceiptEventContent)
if !ok {
return &ReceiptEventContent{}
}
return casted
}
func (content *Content) AsPresence() *PresenceEventContent {
casted, ok := content.Parsed.(*PresenceEventContent)
if !ok {
return &PresenceEventContent{}
}
return casted
}
func (content *Content) AsRoomKey() *RoomKeyEventContent {
casted, ok := content.Parsed.(*RoomKeyEventContent)
if !ok {
return &RoomKeyEventContent{}
}
return casted
}
func (content *Content) AsForwardedRoomKey() *ForwardedRoomKeyEventContent {
casted, ok := content.Parsed.(*ForwardedRoomKeyEventContent)
if !ok {
return &ForwardedRoomKeyEventContent{}
}
return casted
}
func (content *Content) AsRoomKeyRequest() *RoomKeyRequestEventContent {
casted, ok := content.Parsed.(*RoomKeyRequestEventContent)
if !ok {
return &RoomKeyRequestEventContent{}
}
return casted
}

108
event/encryption.go Normal file
View file

@ -0,0 +1,108 @@
// Copyright (c) 2020 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package event
import (
"encoding/base64"
"errors"
"maunium.net/go/mautrix/id"
)
// Algorithm is a Matrix message encryption algorithm.
// https://matrix.org/docs/spec/client_server/r0.6.0#messaging-algorithm-names
type Algorithm string
const (
AlgorithmOlmV1 Algorithm = "m.olm.v1.curve25519-aes-sha2"
AlgorithmMegolmV1 Algorithm = "m.megolm.v1.aes-sha2"
)
var unpaddedBase64 = base64.StdEncoding.WithPadding(base64.NoPadding)
// UnpaddedBase64 is a byte array that implements the JSON Marshaler and Unmarshaler interfaces
// to encode and decode the byte array as unpadded base64.
type UnpaddedBase64 []byte
func (ub64 *UnpaddedBase64) UnmarshalJSON(data []byte) error {
if data[0] != '"' || data[len(data)-1] != '"' {
return errors.New("failed to decode data into bytes: input doesn't look like a JSON string")
}
*ub64 = make([]byte, unpaddedBase64.DecodedLen(len(data)-2))
_, err := unpaddedBase64.Decode(*ub64, data[1:len(data)-1])
return err
}
func (ub64 *UnpaddedBase64) MarshalJSON() ([]byte, error) {
data := make([]byte, unpaddedBase64.EncodedLen(len(*ub64))+2)
data[0] = '"'
data[len(data)-1] = '"'
unpaddedBase64.Encode(data[1:len(data)-1], *ub64)
return data, nil
}
// EncryptionEventContent represents the content of a m.room.encryption state event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-room-encryption
type EncryptionEventContent struct {
// The encryption algorithm to be used to encrypt messages sent in this room. Must be 'm.megolm.v1.aes-sha2'.
Algorithm Algorithm `json:"algorithm"`
// How long the session should be used before changing it. 604800000 (a week) is the recommended default.
RotationPeriodMillis int64 `json:"rotation_period_ms,omitempty"`
// How many messages should be sent before changing the session. 100 is the recommended default.
RotationPeriodMessages int `json:"rotation_period_messages,omitempty"`
}
// EncryptedEventContent represents the content of a m.room.encrypted message event.
// This struct only supports the m.megolm.v1.aes-sha2 algorithm. The legacy m.olm.v1 algorithm is not supported.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-room-encrypted
type EncryptedEventContent struct {
Algorithm Algorithm `json:"algorithm"`
SenderKey string `json:"sender_key"`
DeviceID id.DeviceID `json:"device_id"`
SessionID string `json:"session_id"`
Ciphertext UnpaddedBase64 `json:"ciphertext"`
}
// RoomKeyEventContent represents the content of a m.room_key to_device event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-room-key
type RoomKeyEventContent struct {
Algorithm Algorithm `json:"algorithm"`
RoomID id.RoomID `json:"room_id"`
SessionID string `json:"session_id"`
SessionKey string `json:"session_key"`
}
// ForwardedRoomKeyEventContent represents the content of a m.forwarded_room_key to_device event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-forwarded-room-key
type ForwardedRoomKeyEventContent struct {
RoomKeyEventContent
SenderClaimedKey string `json:"sender_claimed_ed25519_key"`
ForwardingKeyChain []string `json:"forwarding_curve25519_key_chain"`
}
type KeyRequestAction string
const (
KeyRequestActionRequest = "request"
KeyRequestActionCancel = "request_cancellation"
)
// RoomKeyRequestEventContent represents the content of a m.room_key_request to_device event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-room-key-request
type RoomKeyRequestEventContent struct {
Body RequestedKeyInfo `json:"body"`
Action KeyRequestAction `json:"action"`
RequestingDeviceID id.DeviceID `json:"requesting_device_id"`
RequestID string `json:"request_id"`
}
type RequestedKeyInfo struct {
Algorithm Algorithm `json:"algorithm"`
RoomID id.RoomID `json:"room_id"`
SenderKey string `json:"sender_key"`
SessionID string `json:"session_id"`
}

48
event/ephemeral.go Normal file
View file

@ -0,0 +1,48 @@
// Copyright (c) 2020 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package event
import (
"maunium.net/go/mautrix/id"
)
// TagEventContent represents the content of a m.typing ephemeral event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-typing
type TypingEventContent struct {
UserIDs []id.UserID `json:"user_ids"`
}
// ReceiptEventContent represents the content of a m.receipt ephemeral event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-receipt
type ReceiptEventContent map[id.EventID]Receipts
type Receipts struct {
Read map[id.UserID]ReadReceipt `json:"m.read"`
}
type ReadReceipt struct {
Timestamp int64 `json:"ts"`
}
type Presence string
const (
PresenceOnline = "online"
PresenceOffline = "offline"
PresenceUnavailable = "unavailable"
)
// PresenceEventContent represents the content of a m.presence ephemeral event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-presence
type PresenceEventContent struct {
Presence Presence `json:"presence"`
Displayname string `json:"displayname,omitempty"`
AvatarURL id.ContentURIString `json:"avatar_url,omitempty"`
LastActiveAgo int64 `json:"last_active_ago,omitempty"`
CurrentlyActive bool `json:"currently_active,omitempty"`
StatusMessage string `json:"status_msg,omitempty"`
}

View file

@ -7,35 +7,9 @@
package event
import (
"encoding/json"
"strconv"
"sync"
"maunium.net/go/mautrix/crypto/attachment"
"maunium.net/go/mautrix/id"
)
type MessageType string
// Msgtypes
const (
MsgText MessageType = "m.text"
MsgEmote = "m.emote"
MsgNotice = "m.notice"
MsgImage = "m.image"
MsgLocation = "m.location"
MsgVideo = "m.video"
MsgAudio = "m.audio"
MsgFile = "m.file"
)
type Format string
// Message formats
const (
FormatHTML Format = "org.matrix.custom.html"
)
// Event represents a single Matrix event.
type Event struct {
StateKey *string `json:"state_key,omitempty"` // The state key for the event. Only present on State Events.
@ -72,348 +46,3 @@ type Unsigned struct {
RedactedBecause *Event `json:"redacted_because,omitempty"`
InviteRoomState []StrippedState `json:"invite_room_state"`
}
type EncryptedFileInfo struct {
attachment.EncryptedFile
URL id.ContentURIString
}
type Content struct {
VeryRaw json.RawMessage `json:"-"`
Raw map[string]interface{} `json:"-"`
// m.room.message
MsgType MessageType `json:"msgtype,omitempty"`
Body string `json:"body,omitempty"`
Format Format `json:"format,omitempty"`
FormattedBody string `json:"formatted_body,omitempty"`
// media url and info
URL id.ContentURIString `json:"url,omitempty"`
Info *FileInfo `json:"info,omitempty"`
File *EncryptedFileInfo `json:"file,omitempty"`
// edits and relations
NewContent *Content `json:"m.new_content,omitempty"`
RelatesTo *RelatesTo `json:"m.relates_to,omitempty"`
*PowerLevels
// m.room.member state
Member
// Membership key for easy access in m.room.member events
Membership Membership `json:"membership,omitempty"`
// Encryption stuff
Algorithm string `json:"algorithm,omitempty"`
// These are for m.room.encrypted
SenderKey string `json:"sender_key,omitempty"`
DeviceID id.DeviceID `json:"device_id,omitempty"`
SessionID string `json:"session_id,omitempty"`
Ciphertext string `json:"ciphertext,omitempty"`
// These are for m.room_key and m.forwarded_room_key
RoomID id.RoomID `json:"room_id"`
SessionKey string `json:"session_key"`
// These are only for m.forwarded_room_key
SenderClaimedKey string `json:"sender_claimed_ed25519_key"`
ForwardingKeyChain []string `json:"forwarding_curve25519_key_chain"`
// m.room.canonical_alias state
Alias id.RoomAlias `json:"alias,omitempty"`
AltAliases []string `json:"alt_aliases,omitempty"`
// m.room.name state
Name string `json:"name,omitempty"`
// m.room.topic state
Topic string `json:"topic,omitempty"`
// m.room.tombstone state
ReplacementRoom id.RoomID `json:"replacement_room,omitempty"`
// m.tag account data
RoomTags Tags `json:"tags,omitempty"`
// m.typing ephemeral
TypingUserIDs []id.UserID `json:"user_ids,omitempty"`
}
type serializableContent Content
var DisableFancyEventParsing = false
func (content *Content) UnmarshalJSON(data []byte) error {
content.VeryRaw = data
if err := json.Unmarshal(data, &content.Raw); err != nil || DisableFancyEventParsing {
return err
}
return json.Unmarshal(data, (*serializableContent)(content))
}
func (content *Content) MarshalJSON() ([]byte, error) {
if DisableFancyEventParsing {
return json.Marshal(content.Raw)
}
return json.Marshal((*serializableContent)(content))
}
func (content *Content) GetRelatesTo() *RelatesTo {
if content.RelatesTo == nil {
content.RelatesTo = &RelatesTo{}
}
return content.RelatesTo
}
func (content *Content) GetPowerLevels() *PowerLevels {
if content.PowerLevels == nil {
content.PowerLevels = &PowerLevels{}
}
return content.PowerLevels
}
func (content *Content) GetInfo() *FileInfo {
if content.Info == nil {
content.Info = &FileInfo{}
}
return content.Info
}
type Tags map[string]Tag
type Tag struct {
Order json.Number `json:"order,omitempty"`
}
// Membership is an enum specifying the membership state of a room member.
type Membership string
func (ms Membership) IsInviteOrJoin() bool {
return ms == MembershipJoin || ms == MembershipInvite
}
func (ms Membership) IsLeaveOrBan() bool {
return ms == MembershipLeave || ms == MembershipBan
}
// The allowed membership states as specified in spec section 10.5.5.
const (
MembershipJoin Membership = "join"
MembershipLeave Membership = "leave"
MembershipInvite Membership = "invite"
MembershipBan Membership = "ban"
MembershipKnock Membership = "knock"
)
type Member struct {
Membership Membership `json:"membership,omitempty"`
AvatarURL id.ContentURIString `json:"avatar_url,omitempty"`
Displayname string `json:"displayname,omitempty"`
IsDirect bool `json:"is_direct,omitempty"`
ThirdPartyInvite *ThirdPartyInvite `json:"third_party_invite,omitempty"`
Reason string `json:"reason,omitempty"`
}
type ThirdPartyInvite struct {
DisplayName string `json:"display_name"`
Signed struct {
Token string `json:"token"`
Signatures json.RawMessage `json:"signatures"`
MXID string `json:"mxid"`
}
}
type PowerLevels struct {
usersLock sync.RWMutex `json:"-"`
Users map[id.UserID]int `json:"users"`
UsersDefault int `json:"users_default"`
eventsLock sync.RWMutex `json:"-"`
Events map[string]int `json:"events"`
EventsDefault int `json:"events_default"`
StateDefaultPtr *int `json:"state_default,omitempty"`
InvitePtr *int `json:"invite,omitempty"`
KickPtr *int `json:"kick,omitempty"`
BanPtr *int `json:"ban,omitempty"`
RedactPtr *int `json:"redact,omitempty"`
}
func (pl *PowerLevels) Invite() int {
if pl.InvitePtr != nil {
return *pl.InvitePtr
}
return 50
}
func (pl *PowerLevels) Kick() int {
if pl.KickPtr != nil {
return *pl.KickPtr
}
return 50
}
func (pl *PowerLevels) Ban() int {
if pl.BanPtr != nil {
return *pl.BanPtr
}
return 50
}
func (pl *PowerLevels) Redact() int {
if pl.RedactPtr != nil {
return *pl.RedactPtr
}
return 50
}
func (pl *PowerLevels) StateDefault() int {
if pl.StateDefaultPtr != nil {
return *pl.StateDefaultPtr
}
return 50
}
func (pl *PowerLevels) GetUserLevel(userID id.UserID) int {
pl.usersLock.RLock()
defer pl.usersLock.RUnlock()
level, ok := pl.Users[userID]
if !ok {
return pl.UsersDefault
}
return level
}
func (pl *PowerLevels) SetUserLevel(userID id.UserID, level int) {
pl.usersLock.Lock()
defer pl.usersLock.Unlock()
if level == pl.UsersDefault {
delete(pl.Users, userID)
} else {
pl.Users[userID] = level
}
}
func (pl *PowerLevels) EnsureUserLevel(userID id.UserID, level int) bool {
existingLevel := pl.GetUserLevel(userID)
if existingLevel != level {
pl.SetUserLevel(userID, level)
return true
}
return false
}
func (pl *PowerLevels) GetEventLevel(eventType Type) int {
pl.eventsLock.RLock()
defer pl.eventsLock.RUnlock()
level, ok := pl.Events[eventType.String()]
if !ok {
if eventType.IsState() {
return pl.StateDefault()
}
return pl.EventsDefault
}
return level
}
func (pl *PowerLevels) SetEventLevel(eventType Type, level int) {
pl.eventsLock.Lock()
defer pl.eventsLock.Unlock()
if (eventType.IsState() && level == pl.StateDefault()) || (!eventType.IsState() && level == pl.EventsDefault) {
delete(pl.Events, eventType.String())
} else {
pl.Events[eventType.String()] = level
}
}
func (pl *PowerLevels) EnsureEventLevel(eventType Type, level int) bool {
existingLevel := pl.GetEventLevel(eventType)
if existingLevel != level {
pl.SetEventLevel(eventType, level)
return true
}
return false
}
type FileInfo struct {
MimeType string `json:"mimetype,omitempty"`
ThumbnailInfo *FileInfo `json:"thumbnail_info,omitempty"`
ThumbnailURL id.ContentURIString `json:"thumbnail_url,omitempty"`
ThumbnailFile *EncryptedFileInfo `json:"thumbnail_file,omitempty"`
Width int `json:"-"`
Height int `json:"-"`
Duration uint `json:"-"`
Size int `json:"-"`
}
type serializableFileInfo struct {
MimeType string `json:"mimetype,omitempty"`
ThumbnailInfo *serializableFileInfo `json:"thumbnail_info,omitempty"`
ThumbnailURL id.ContentURIString `json:"thumbnail_url,omitempty"`
Width json.Number `json:"w,omitempty"`
Height json.Number `json:"h,omitempty"`
Duration json.Number `json:"duration,omitempty"`
Size json.Number `json:"size,omitempty"`
}
func (sfi *serializableFileInfo) CopyFrom(fileInfo *FileInfo) *serializableFileInfo {
if fileInfo == nil {
return nil
}
*sfi = serializableFileInfo{
MimeType: fileInfo.MimeType,
ThumbnailURL: fileInfo.ThumbnailURL,
ThumbnailInfo: (&serializableFileInfo{}).CopyFrom(fileInfo.ThumbnailInfo),
}
if fileInfo.Width > 0 {
sfi.Width = json.Number(strconv.Itoa(fileInfo.Width))
}
if fileInfo.Height > 0 {
sfi.Height = json.Number(strconv.Itoa(fileInfo.Height))
}
if fileInfo.Size > 0 {
sfi.Size = json.Number(strconv.Itoa(fileInfo.Size))
}
if fileInfo.Duration > 0 {
sfi.Duration = json.Number(strconv.Itoa(int(fileInfo.Duration)))
}
return sfi
}
func (sfi *serializableFileInfo) CopyTo(fileInfo *FileInfo) {
*fileInfo = FileInfo{
Width: int(numberToUint(sfi.Width)),
Height: int(numberToUint(sfi.Height)),
Size: int(numberToUint(sfi.Size)),
Duration: numberToUint(sfi.Duration),
MimeType: sfi.MimeType,
ThumbnailURL: sfi.ThumbnailURL,
}
if sfi.ThumbnailInfo != nil {
fileInfo.ThumbnailInfo = &FileInfo{}
sfi.ThumbnailInfo.CopyTo(fileInfo.ThumbnailInfo)
}
}
func (fileInfo *FileInfo) UnmarshalJSON(data []byte) error {
sfi := &serializableFileInfo{}
if err := json.Unmarshal(data, sfi); err != nil {
return err
}
sfi.CopyTo(fileInfo)
return nil
}
func (fileInfo *FileInfo) MarshalJSON() ([]byte, error) {
return json.Marshal((&serializableFileInfo{}).CopyFrom(fileInfo))
}
func numberToUint(val json.Number) uint {
f64, _ := val.Float64()
if f64 > 0 {
return uint(f64)
}
return 0
}
func (fileInfo *FileInfo) GetThumbnailInfo() *FileInfo {
if fileInfo.ThumbnailInfo == nil {
fileInfo.ThumbnailInfo = &FileInfo{}
}
return fileInfo.ThumbnailInfo
}

53
event/member.go Normal file
View file

@ -0,0 +1,53 @@
// Copyright (c) 2020 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package event
import (
"encoding/json"
"maunium.net/go/mautrix/id"
)
// Membership is an enum specifying the membership state of a room member.
type Membership string
func (ms Membership) IsInviteOrJoin() bool {
return ms == MembershipJoin || ms == MembershipInvite
}
func (ms Membership) IsLeaveOrBan() bool {
return ms == MembershipLeave || ms == MembershipBan
}
// The allowed membership states as specified in spec section 10.5.5.
const (
MembershipJoin Membership = "join"
MembershipLeave Membership = "leave"
MembershipInvite Membership = "invite"
MembershipBan Membership = "ban"
MembershipKnock Membership = "knock"
)
// MemberEventContent represents the content of a m.room.member state event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-room-member
type MemberEventContent struct {
Membership Membership `json:"membership"`
AvatarURL id.ContentURIString `json:"avatar_url,omitempty"`
Displayname string `json:"displayname,omitempty"`
IsDirect bool `json:"is_direct,omitempty"`
ThirdPartyInvite *ThirdPartyInvite `json:"third_party_invite,omitempty"`
Reason string `json:"reason,omitempty"`
}
type ThirdPartyInvite struct {
DisplayName string `json:"display_name"`
Signed struct {
Token string `json:"token"`
Signatures json.RawMessage `json:"signatures"`
MXID string `json:"mxid"`
}
}

197
event/message.go Normal file
View file

@ -0,0 +1,197 @@
// Copyright (c) 2020 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package event
import (
"encoding/json"
"strconv"
"maunium.net/go/mautrix/crypto/attachment"
"maunium.net/go/mautrix/id"
)
// MessageType is the sub-type of a m.room.message event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-room-message-msgtypes
type MessageType string
// Msgtypes
const (
MsgText MessageType = "m.text"
MsgEmote MessageType = "m.emote"
MsgNotice MessageType = "m.notice"
MsgImage MessageType = "m.image"
MsgLocation MessageType = "m.location"
MsgVideo MessageType = "m.video"
MsgAudio MessageType = "m.audio"
MsgFile MessageType = "m.file"
)
// Format specifies the format of the formatted_body in m.room.message events.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-room-message-msgtypes
type Format string
// Message formats
const (
FormatHTML Format = "org.matrix.custom.html"
)
// RedactionEventContent represents the content of a m.room.redaction message event.
//
// The redacted event ID is still at the top level, but will move in a future room version.
// See https://github.com/matrix-org/matrix-doc/pull/2244 and https://github.com/matrix-org/matrix-doc/pull/2174
//
// https://matrix.org/docs/spec/client_server/r0.6.0#m-room-redaction
type RedactionEventContent struct {
Reason string `json:"reason,omitempty"`
}
// ReactionEventContent represents the content of a m.reaction message event.
// This is not yet in a spec release, see https://github.com/matrix-org/matrix-doc/pull/1849
type ReactionEventContent struct {
RelatesTo RelatesTo `json:"m.relates_to"`
}
// MssageEventContent represents the content of a m.room.message event.
//
// It is also used to represent m.sticker events, as they are equivalent to m.room.message
// with the exception of the msgtype field.
//
// https://matrix.org/docs/spec/client_server/r0.6.0#m-room-message
type MessageEventContent struct {
// Base m.room.message fields
MsgType MessageType `json:"msgtype"`
Body string `json:"body"`
// Extra fields for text types
Format Format `json:"format,omitempty"`
FormattedBody string `json:"formatted_body,omitempty"`
// Extra fields for media types
URL id.ContentURIString `json:"url,omitempty"`
Info *FileInfo `json:"info,omitempty"`
File *EncryptedFileInfo `json:"file,omitempty"`
// Edits and relations
NewContent *MessageEventContent `json:"m.new_content,omitempty"`
RelatesTo *RelatesTo `json:"m.relates_to,omitempty"`
}
func (content *MessageEventContent) GetRelatesTo() *RelatesTo {
if content.RelatesTo == nil {
content.RelatesTo = &RelatesTo{}
}
return content.RelatesTo
}
func (content *MessageEventContent) GetFile() *EncryptedFileInfo {
if content.File == nil {
content.File = &EncryptedFileInfo{}
}
return content.File
}
func (content *MessageEventContent) GetInfo() *FileInfo {
if content.Info == nil {
content.Info = &FileInfo{}
}
return content.Info
}
type EncryptedFileInfo struct {
attachment.EncryptedFile
URL id.ContentURIString
}
type FileInfo struct {
MimeType string `json:"mimetype,omitempty"`
ThumbnailInfo *FileInfo `json:"thumbnail_info,omitempty"`
ThumbnailURL id.ContentURIString `json:"thumbnail_url,omitempty"`
ThumbnailFile *EncryptedFileInfo `json:"thumbnail_file,omitempty"`
Width int `json:"-"`
Height int `json:"-"`
Duration int `json:"-"`
Size int `json:"-"`
}
type serializableFileInfo struct {
MimeType string `json:"mimetype,omitempty"`
ThumbnailInfo *serializableFileInfo `json:"thumbnail_info,omitempty"`
ThumbnailURL id.ContentURIString `json:"thumbnail_url,omitempty"`
Width json.Number `json:"w,omitempty"`
Height json.Number `json:"h,omitempty"`
Duration json.Number `json:"duration,omitempty"`
Size json.Number `json:"size,omitempty"`
}
func (sfi *serializableFileInfo) CopyFrom(fileInfo *FileInfo) *serializableFileInfo {
if fileInfo == nil {
return nil
}
*sfi = serializableFileInfo{
MimeType: fileInfo.MimeType,
ThumbnailURL: fileInfo.ThumbnailURL,
ThumbnailInfo: (&serializableFileInfo{}).CopyFrom(fileInfo.ThumbnailInfo),
}
if fileInfo.Width > 0 {
sfi.Width = json.Number(strconv.Itoa(fileInfo.Width))
}
if fileInfo.Height > 0 {
sfi.Height = json.Number(strconv.Itoa(fileInfo.Height))
}
if fileInfo.Size > 0 {
sfi.Size = json.Number(strconv.Itoa(fileInfo.Size))
}
if fileInfo.Duration > 0 {
sfi.Duration = json.Number(strconv.Itoa(int(fileInfo.Duration)))
}
return sfi
}
func (sfi *serializableFileInfo) CopyTo(fileInfo *FileInfo) {
*fileInfo = FileInfo{
Width: numberToInt(sfi.Width),
Height: numberToInt(sfi.Height),
Size: numberToInt(sfi.Size),
Duration: numberToInt(sfi.Duration),
MimeType: sfi.MimeType,
ThumbnailURL: sfi.ThumbnailURL,
}
if sfi.ThumbnailInfo != nil {
fileInfo.ThumbnailInfo = &FileInfo{}
sfi.ThumbnailInfo.CopyTo(fileInfo.ThumbnailInfo)
}
}
func (fileInfo *FileInfo) UnmarshalJSON(data []byte) error {
sfi := &serializableFileInfo{}
if err := json.Unmarshal(data, sfi); err != nil {
return err
}
sfi.CopyTo(fileInfo)
return nil
}
func (fileInfo *FileInfo) MarshalJSON() ([]byte, error) {
return json.Marshal((&serializableFileInfo{}).CopyFrom(fileInfo))
}
func numberToInt(val json.Number) int {
f64, _ := val.Float64()
if f64 > 0 {
return int(f64)
}
return 0
}
func (fileInfo *FileInfo) GetThumbnailInfo() *FileInfo {
if fileInfo.ThumbnailInfo == nil {
fileInfo.ThumbnailInfo = &FileInfo{}
}
return fileInfo.ThumbnailInfo
}

133
event/message_test.go Normal file
View file

@ -0,0 +1,133 @@
// Copyright (c) 2020 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package event_test
import (
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
const invalidMessageEvent = `{
"sender": "@tulir:maunium.net",
"type": "m.room.message",
"origin_server_ts": 1587252684192,
"event_id": "$foo",
"room_id": "!bar",
"content": {
"body": {
"hmm": false
}
}
}`
func TestMessageEventContent__ParseInvalid(t *testing.T) {
var evt *event.Event
err := json.Unmarshal([]byte(invalidMessageEvent), &evt)
assert.Nil(t, err)
assert.Equal(t, id.UserID("@tulir:maunium.net"), evt.Sender)
assert.Equal(t, event.EventMessage, evt.Type)
assert.Equal(t, int64(1587252684192), evt.Timestamp)
assert.Equal(t, id.EventID("$foo"), evt.ID)
assert.Equal(t, id.RoomID("!bar"), evt.RoomID)
err = evt.Content.ParseRaw(evt.Type)
assert.NotNil(t, err)
}
const messageEvent = `{
"sender": "@tulir:maunium.net",
"type": "m.room.message",
"origin_server_ts": 1587252684192,
"event_id": "$foo",
"room_id": "!bar",
"content": {
"msgtype": "m.text",
"body": "* **Hello**, World!",
"format": "org.matrix.custom.html",
"formatted_body": "* <strong>Hello</strong>, World!",
"m.new_content": {
"msgtype": "m.text",
"body": "**Hello**, World!",
"format": "org.matrix.custom.html",
"formatted_body": "<strong>Hello</strong>, World!"
}
}
}`
func TestMessageEventContent__ParseEdit(t *testing.T) {
var evt *event.Event
err := json.Unmarshal([]byte(messageEvent), &evt)
assert.Nil(t, err)
assert.Equal(t, id.UserID("@tulir:maunium.net"), evt.Sender)
assert.Equal(t, event.EventMessage, evt.Type)
assert.Equal(t, int64(1587252684192), evt.Timestamp)
assert.Equal(t, id.EventID("$foo"), evt.ID)
assert.Equal(t, id.RoomID("!bar"), evt.RoomID)
err = evt.Content.ParseRaw(evt.Type)
assert.IsType(t, &event.MessageEventContent{}, evt.Content.Parsed)
content := evt.Content.Parsed.(*event.MessageEventContent)
assert.Equal(t, event.MsgText, content.MsgType)
assert.Equal(t, event.MsgText, content.NewContent.MsgType)
assert.Equal(t, "**Hello**, World!", content.NewContent.Body)
assert.Equal(t, "<strong>Hello</strong>, World!", content.NewContent.FormattedBody)
}
const imageMessageEvent = `{
"sender": "@tulir:maunium.net",
"type": "m.room.message",
"origin_server_ts": 1587252684192,
"event_id": "$foo",
"room_id": "!bar",
"content": {
"msgtype": "m.image",
"body": "image.png",
"url": "mxc://example.com/image",
"info": {
"mimetype": "image/png",
"w": 64,
"h": 64,
"size": 12345,
"thumbnail_url": "mxc://example.com/image_thumb"
}
}
}`
func TestMessageEventContent__ParseMedia(t *testing.T) {
var evt *event.Event
err := json.Unmarshal([]byte(imageMessageEvent), &evt)
assert.Nil(t, err)
assert.Equal(t, id.UserID("@tulir:maunium.net"), evt.Sender)
assert.Equal(t, event.EventMessage, evt.Type)
assert.Equal(t, int64(1587252684192), evt.Timestamp)
assert.Equal(t, id.EventID("$foo"), evt.ID)
assert.Equal(t, id.RoomID("!bar"), evt.RoomID)
err = evt.Content.ParseRaw(evt.Type)
assert.IsType(t, &event.MessageEventContent{}, evt.Content.Parsed)
content := evt.Content.Parsed.(*event.MessageEventContent)
fmt.Println(content)
fmt.Println(content.Info)
assert.Equal(t, event.MsgImage, content.MsgType)
parsedURL, err := content.URL.Parse()
assert.Nil(t, err)
assert.Equal(t, id.ContentURI{Homeserver: "example.com", FileID: "image"}, parsedURL)
assert.Nil(t, content.NewContent)
assert.Equal(t, "image/png", content.GetInfo().MimeType)
assert.EqualValues(t, 64, content.GetInfo().Width)
assert.EqualValues(t, 64, content.GetInfo().Height)
assert.EqualValues(t, 12345, content.GetInfo().Size)
}

128
event/powerlevels.go Normal file
View file

@ -0,0 +1,128 @@
// Copyright (c) 2020 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package event
import (
"sync"
"maunium.net/go/mautrix/id"
)
// PowerLevelsEventContent represents the content of a m.room.power_levels state event content.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-room-power-levels
type PowerLevelsEventContent struct {
usersLock sync.RWMutex `json:"-"`
Users map[id.UserID]int `json:"users"`
UsersDefault int `json:"users_default"`
eventsLock sync.RWMutex `json:"-"`
Events map[string]int `json:"events"`
EventsDefault int `json:"events_default"`
StateDefaultPtr *int `json:"state_default,omitempty"`
InvitePtr *int `json:"invite,omitempty"`
KickPtr *int `json:"kick,omitempty"`
BanPtr *int `json:"ban,omitempty"`
RedactPtr *int `json:"redact,omitempty"`
}
func (pl *PowerLevelsEventContent) Invite() int {
if pl.InvitePtr != nil {
return *pl.InvitePtr
}
return 50
}
func (pl *PowerLevelsEventContent) Kick() int {
if pl.KickPtr != nil {
return *pl.KickPtr
}
return 50
}
func (pl *PowerLevelsEventContent) Ban() int {
if pl.BanPtr != nil {
return *pl.BanPtr
}
return 50
}
func (pl *PowerLevelsEventContent) Redact() int {
if pl.RedactPtr != nil {
return *pl.RedactPtr
}
return 50
}
func (pl *PowerLevelsEventContent) StateDefault() int {
if pl.StateDefaultPtr != nil {
return *pl.StateDefaultPtr
}
return 50
}
func (pl *PowerLevelsEventContent) GetUserLevel(userID id.UserID) int {
pl.usersLock.RLock()
defer pl.usersLock.RUnlock()
level, ok := pl.Users[userID]
if !ok {
return pl.UsersDefault
}
return level
}
func (pl *PowerLevelsEventContent) SetUserLevel(userID id.UserID, level int) {
pl.usersLock.Lock()
defer pl.usersLock.Unlock()
if level == pl.UsersDefault {
delete(pl.Users, userID)
} else {
pl.Users[userID] = level
}
}
func (pl *PowerLevelsEventContent) EnsureUserLevel(userID id.UserID, level int) bool {
existingLevel := pl.GetUserLevel(userID)
if existingLevel != level {
pl.SetUserLevel(userID, level)
return true
}
return false
}
func (pl *PowerLevelsEventContent) GetEventLevel(eventType Type) int {
pl.eventsLock.RLock()
defer pl.eventsLock.RUnlock()
level, ok := pl.Events[eventType.String()]
if !ok {
if eventType.IsState() {
return pl.StateDefault()
}
return pl.EventsDefault
}
return level
}
func (pl *PowerLevelsEventContent) SetEventLevel(eventType Type, level int) {
pl.eventsLock.Lock()
defer pl.eventsLock.Unlock()
if (eventType.IsState() && level == pl.StateDefault()) || (!eventType.IsState() && level == pl.EventsDefault) {
delete(pl.Events, eventType.String())
} else {
pl.Events[eventType.String()] = level
}
}
func (pl *PowerLevelsEventContent) EnsureEventLevel(eventType Type, level int) bool {
existingLevel := pl.GetEventLevel(eventType)
if existingLevel != level {
pl.SetEventLevel(eventType, level)
return true
}
return false
}

View file

@ -34,7 +34,7 @@ func TrimReplyFallbackText(text string) string {
return strings.TrimSpace(strings.Join(lines, "\n"))
}
func (content *Content) RemoveReplyFallback() {
func (content *MessageEventContent) RemoveReplyFallback() {
if len(content.GetReplyTo()) > 0 {
if content.Format == FormatHTML {
content.FormattedBody = TrimReplyFallbackHTML(content.FormattedBody)
@ -43,7 +43,7 @@ func (content *Content) RemoveReplyFallback() {
}
}
func (content *Content) GetReplyTo() id.EventID {
func (content *MessageEventContent) GetReplyTo() id.EventID {
if content.RelatesTo != nil && content.RelatesTo.Type == RelReference {
return content.RelatesTo.EventID
}
@ -53,9 +53,13 @@ func (content *Content) GetReplyTo() id.EventID {
const ReplyFormat = `<mx-reply><blockquote><a href="https://matrix.to/#/%s/%s">In reply to</a> <a href="https://matrix.to/#/%s">%s</a><br>%s</blockquote></mx-reply>`
func (evt *Event) GenerateReplyFallbackHTML() string {
body := evt.Content.FormattedBody
parsedContent, ok := evt.Content.Parsed.(MessageEventContent)
if !ok {
return ""
}
body := parsedContent.FormattedBody
if len(body) == 0 {
body = html.EscapeString(evt.Content.Body)
body = html.EscapeString(parsedContent.Body)
}
senderDisplayName := evt.Sender
@ -64,7 +68,11 @@ func (evt *Event) GenerateReplyFallbackHTML() string {
}
func (evt *Event) GenerateReplyFallbackText() string {
body := evt.Content.Body
parsedContent, ok := evt.Content.Parsed.(MessageEventContent)
if !ok {
return ""
}
body := parsedContent.Body
lines := strings.Split(strings.TrimSpace(body), "\n")
firstLine, lines := lines[0], lines[1:]
@ -79,7 +87,7 @@ func (evt *Event) GenerateReplyFallbackText() string {
return fallbackText.String()
}
func (content *Content) SetReply(inReplyTo *Event) {
func (content *MessageEventContent) SetReply(inReplyTo *Event) {
content.RelatesTo = &RelatesTo{
EventID: inReplyTo.ID,
Type: RelReference,

113
event/state.go Normal file
View file

@ -0,0 +1,113 @@
// Copyright (c) 2020 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package event
import (
"maunium.net/go/mautrix/id"
)
// CanonicalAliasEventContent represents the content of a m.room.canonical_alias state event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-room-canonical-alias
type CanonicalAliasEventContent struct {
Alias id.RoomAlias `json:"alias"`
// This field isn't in a spec release yet.
// MSC2432: https://github.com/matrix-org/matrix-doc/pull/2432
AltAliases []string `json:"alt_aliases,omitempty"`
}
// RoomNameEventContent represents the content of a m.room.name state event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-room-name
type RoomNameEventContent struct {
Name string `json:"name"`
}
// RoomAvatarEventContent represents the content of a m.room.avatar state event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-room-avatar
type RoomAvatarEventContent struct {
URL id.ContentURI `json:"url"`
}
// TopicEventContent represents the content of a m.room.topic state event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-room-topic
type TopicEventContent struct {
Topic string `json:"topic"`
}
// TombstoneEventContent represents the content of a m.room.tombstone state event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-room-tombstone
type TombstoneEventContent struct {
Body string `json:"body"`
ReplacementRoom id.RoomID `json:"replacement_room"`
}
// CreateEventContent represents the content of a m.room.create state event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-room-create
type CreateEventContent struct {
Creator id.UserID `json:"creator"`
Federate bool `json:"m.federate,omitempty"`
RoomVersion string `json:"version,omitempty"`
Predecessor struct {
RoomID id.RoomID `json:"room_id"`
EventID id.EventID `json:"event_id"`
} `json:"predecessor"`
}
// JoinRule specifies how open a room is to new members.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-room-join-rules
type JoinRule string
const (
JoinRulePublic JoinRule = "public"
JoinRuleKnock JoinRule = "knock"
JoinRuleInvite JoinRule = "invite"
JoinRulePrivate JoinRule = "private"
)
// JoinRulesEventContent represents the content of a m.room.join_rules state event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-room-join-rules
type JoinRulesEventContent struct {
JoinRule JoinRule `json:"join_rule"`
}
// PinnedEventsEventContent represents the content of a m.room.pinned_events state event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-room-pinned-events
type PinnedEventsEventContent struct {
Pinned []id.EventID `json:"pinned"`
}
// HistoryVisibility specifies who can see new messages.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-room-history-visibility
type HistoryVisibility string
const (
HistoryVisibilityInvited HistoryVisibility = "invited"
HistoryVisibilityJoined HistoryVisibility = "joined"
HistoryVisibilityShared HistoryVisibility = "shared"
HistoryVisibilityWorldReadable HistoryVisibility = "world_readable"
)
// HistoryVisibilityEventContent represents the content of a m.room.history_visibility state event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-room-history-visibility
type HistoryVisibilityEventContent struct {
HistoryVisibility HistoryVisibility `json:"history_visibility"`
}
// GuestAccess specifies whether or not guest accounts can join.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-room-guest-access
type GuestAccess string
const (
GuestAccessCanJoin GuestAccess = "can_join"
GuestAccessForbidden GuestAccess = "forbidden"
)
// GuestAccessEventContent represents the content of a m.room.guest_access state event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-room-guest-access
type GuestAccessEventContent struct {
GuestAccess GuestAccess `json:"guest_access"`
}

View file

@ -71,7 +71,7 @@ func (et *Type) GuessClass() EventTypeClass {
return AccountDataEventType
case EventRedaction.Type, EventMessage.Type, EventEncrypted.Type, EventReaction.Type, EventSticker.Type:
return MessageEventType
case ToDeviceNewDevice.Type, ToDeviceRoomKey.Type, ToDeviceRoomKeyRequest.Type, ToDeviceForwardedRoomKey.Type:
case ToDeviceRoomKey.Type, ToDeviceRoomKeyRequest.Type, ToDeviceForwardedRoomKey.Type:
return ToDeviceEventType
default:
return UnknownEventType
@ -97,18 +97,20 @@ func (et *Type) String() string {
// State events
var (
StateAliases = Type{"m.room.aliases", StateEventType}
StateCanonicalAlias = Type{"m.room.canonical_alias", StateEventType}
StateCreate = Type{"m.room.create", StateEventType}
StateJoinRules = Type{"m.room.join_rules", StateEventType}
StateMember = Type{"m.room.member", StateEventType}
StatePowerLevels = Type{"m.room.power_levels", StateEventType}
StateRoomName = Type{"m.room.name", StateEventType}
StateTopic = Type{"m.room.topic", StateEventType}
StateRoomAvatar = Type{"m.room.avatar", StateEventType}
StatePinnedEvents = Type{"m.room.pinned_events", StateEventType}
StateTombstone = Type{"m.room.tombstone", StateEventType}
StateEncryption = Type{"m.room.encryption", StateEventType}
StateAliases = Type{"m.room.aliases", StateEventType}
StateCanonicalAlias = Type{"m.room.canonical_alias", StateEventType}
StateCreate = Type{"m.room.create", StateEventType}
StateJoinRules = Type{"m.room.join_rules", StateEventType}
StateHistoryVisibility = Type{"m.room.history_visibility", StateEventType}
StateGuestAccess = Type{"m.room.guest_access", StateEventType}
StateMember = Type{"m.room.member", StateEventType}
StatePowerLevels = Type{"m.room.power_levels", StateEventType}
StateRoomName = Type{"m.room.name", StateEventType}
StateTopic = Type{"m.room.topic", StateEventType}
StateRoomAvatar = Type{"m.room.avatar", StateEventType}
StatePinnedEvents = Type{"m.room.pinned_events", StateEventType}
StateTombstone = Type{"m.room.tombstone", StateEventType}
StateEncryption = Type{"m.room.encryption", StateEventType}
)
// Message events
@ -129,15 +131,16 @@ var (
// Account data events
var (
AccountDataDirectChats = Type{"m.direct", AccountDataEventType}
AccountDataPushRules = Type{"m.push_rules", AccountDataEventType}
AccountDataRoomTags = Type{"m.tag", AccountDataEventType}
AccountDataDirectChats = Type{"m.direct", AccountDataEventType}
AccountDataPushRules = Type{"m.push_rules", AccountDataEventType}
AccountDataRoomTags = Type{"m.tag", AccountDataEventType}
AccountDataFullyRead = Type{"m.fully_read", AccountDataEventType}
AccountDataIgnoredUserList = Type{"m.ignored_user_list", AccountDataEventType}
)
// Device-to-device events
var (
ToDeviceNewDevice = Type{"m.new_device", ToDeviceEventType}
ToDeviceRoomKey = Type{"m.room_key", ToDeviceEventType}
ToDeviceRoomKeyRequest = Type{"m.room_key_request", ToDeviceEventType}
ToDeviceRoomKey = Type{"m.room_key", ToDeviceEventType}
ToDeviceRoomKeyRequest = Type{"m.room_key_request", ToDeviceEventType}
ToDeviceForwardedRoomKey = Type{"m.forwarded_room_key", ToDeviceEventType}
)

View file

@ -17,7 +17,7 @@ const (
)
//Filter is used by clients to specify how the server should filter responses to e.g. sync requests
//Specified by: https://matrix.org/docs/spec/client_server/r0.2.0.html#filtering
//Specified by: https://matrix.org/docs/spec/client_server/r0.6.0.html#filtering
type Filter struct {
AccountData FilterPart `json:"account_data,omitempty"`
EventFields []string `json:"event_fields,omitempty"`

View file

@ -41,7 +41,7 @@ var bfhtml = blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{
var Renderer = blackfriday.WithRenderer(bfhtml)
var NoHTMLRenderer = blackfriday.WithRenderer(&EscapingRenderer{bfhtml})
func RenderMarkdown(text string, allowMarkdown, allowHTML bool) event.Content {
func RenderMarkdown(text string, allowMarkdown, allowHTML bool) event.MessageEventContent {
htmlBody := text
if allowMarkdown {
@ -58,7 +58,7 @@ func RenderMarkdown(text string, allowMarkdown, allowHTML bool) event.Content {
text = HTMLToText(htmlBody)
if htmlBody != text {
return event.Content{
return event.MessageEventContent{
FormattedBody: htmlBody,
Format: event.FormatHTML,
MsgType: event.MsgText,
@ -67,7 +67,7 @@ func RenderMarkdown(text string, allowMarkdown, allowHTML bool) event.Content {
}
}
return event.Content{
return event.MessageEventContent{
MsgType: event.MsgText,
Body: text,
}

2
go.mod
View file

@ -3,6 +3,8 @@ module maunium.net/go/mautrix
go 1.14
require (
github.com/fatih/structs v1.1.0
github.com/russross/blackfriday/v2 v2.0.1
github.com/stretchr/testify v1.5.1
golang.org/x/net v0.0.0-20200301022130-244492dfa37a
)

14
go.sum
View file

@ -1,4 +1,15 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4=
github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -6,3 +17,6 @@ golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3ob
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View file

@ -49,12 +49,12 @@ type PushCondition struct {
var MemberCountFilterRegex = regexp.MustCompile("^(==|[<>]=?)?([0-9]+)$")
// Match checks if this condition is fulfilled for the given event in the given room.
func (cond *PushCondition) Match(room Room, event *event.Event) bool {
func (cond *PushCondition) Match(room Room, evt *event.Event) bool {
switch cond.Kind {
case KindEventMatch:
return cond.matchValue(room, event)
return cond.matchValue(room, evt)
case KindContainsDisplayName:
return cond.matchDisplayName(room, event)
return cond.matchDisplayName(room, evt)
case KindRoomMemberCount:
return cond.matchMemberCount(room)
default:
@ -62,7 +62,7 @@ func (cond *PushCondition) Match(room Room, event *event.Event) bool {
}
}
func (cond *PushCondition) matchValue(room Room, event *event.Event) bool {
func (cond *PushCondition) matchValue(room Room, evt *event.Event) bool {
index := strings.IndexRune(cond.Key, '.')
key := cond.Key
subkey := ""
@ -78,31 +78,35 @@ func (cond *PushCondition) matchValue(room Room, event *event.Event) bool {
switch key {
case "type":
return pattern.MatchString(event.Type.String())
return pattern.MatchString(evt.Type.String())
case "sender":
return pattern.MatchString(string(event.Sender))
return pattern.MatchString(string(evt.Sender))
case "room_id":
return pattern.MatchString(string(event.RoomID))
return pattern.MatchString(string(evt.RoomID))
case "state_key":
if event.StateKey == nil {
if evt.StateKey == nil {
return cond.Pattern == ""
}
return pattern.MatchString(*event.StateKey)
return pattern.MatchString(*evt.StateKey)
case "content":
val, _ := event.Content.Raw[subkey].(string)
val, _ := evt.Content.Raw[subkey].(string)
return pattern.MatchString(val)
default:
return false
}
}
func (cond *PushCondition) matchDisplayName(room Room, event *event.Event) bool {
func (cond *PushCondition) matchDisplayName(room Room, evt *event.Event) bool {
displayname := room.GetOwnDisplayname()
if len(displayname) == 0 {
return false
}
msg := event.Content.Body
msg, ok := evt.Content.Raw["body"].(string)
if !ok {
return false
}
isAcceptable := func(r uint8) bool {
return unicode.IsSpace(rune(r)) || unicode.IsPunct(rune(r))
}

View file

@ -20,7 +20,7 @@ func init() {
}
type PushRuleCollection interface {
GetActions(room Room, event *event.Event) PushActionArray
GetActions(room Room, evt *event.Event) PushActionArray
}
type PushRuleArray []*PushRule
@ -32,9 +32,9 @@ func (rules PushRuleArray) SetType(typ PushRuleType) PushRuleArray {
return rules
}
func (rules PushRuleArray) GetActions(room Room, event *event.Event) PushActionArray {
func (rules PushRuleArray) GetActions(room Room, evt *event.Event) PushActionArray {
for _, rule := range rules {
if !rule.Match(room, event) {
if !rule.Match(room, evt) {
continue
}
return rule.Actions
@ -59,16 +59,16 @@ func (rules PushRuleArray) SetTypeAndMap(typ PushRuleType) PushRuleMap {
return data
}
func (ruleMap PushRuleMap) GetActions(room Room, event *event.Event) PushActionArray {
func (ruleMap PushRuleMap) GetActions(room Room, evt *event.Event) PushActionArray {
var rule *PushRule
var found bool
switch ruleMap.Type {
case RoomRule:
rule, found = ruleMap.Map[string(event.RoomID)]
rule, found = ruleMap.Map[string(evt.RoomID)]
case SenderRule:
rule, found = ruleMap.Map[string(event.Sender)]
rule, found = ruleMap.Map[string(evt.Sender)]
}
if found && rule.Match(room, event) {
if found && rule.Match(room, evt) {
return rule.Actions
}
return nil
@ -114,37 +114,41 @@ type PushRule struct {
Pattern string `json:"pattern,omitempty"`
}
func (rule *PushRule) Match(room Room, event *event.Event) bool {
func (rule *PushRule) Match(room Room, evt *event.Event) bool {
if !rule.Enabled {
return false
}
switch rule.Type {
case OverrideRule, UnderrideRule:
return rule.matchConditions(room, event)
return rule.matchConditions(room, evt)
case ContentRule:
return rule.matchPattern(room, event)
return rule.matchPattern(room, evt)
case RoomRule:
return id.RoomID(rule.RuleID) == event.RoomID
return id.RoomID(rule.RuleID) == evt.RoomID
case SenderRule:
return id.UserID(rule.RuleID) == event.Sender
return id.UserID(rule.RuleID) == evt.Sender
default:
return false
}
}
func (rule *PushRule) matchConditions(room Room, event *event.Event) bool {
func (rule *PushRule) matchConditions(room Room, evt *event.Event) bool {
for _, cond := range rule.Conditions {
if !cond.Match(room, event) {
if !cond.Match(room, evt) {
return false
}
}
return true
}
func (rule *PushRule) matchPattern(room Room, event *event.Event) bool {
func (rule *PushRule) matchPattern(room Room, evt *event.Event) bool {
pattern, err := glob.Compile(rule.Pattern)
if err != nil {
return false
}
return pattern.MatchString(event.Content.Body)
msg, ok := evt.Content.Raw["body"].(string)
if !ok {
return false
}
return pattern.MatchString(msg)
}

View file

@ -70,7 +70,7 @@ var DefaultPushActions = PushActionArray{&PushAction{Action: ActionDontNotify}}
// GetActions matches the given event against all of the push rule
// collections in this push ruleset in the order of priority as
// specified in spec section 11.12.1.4.
func (rs *PushRuleset) GetActions(room Room, event *event.Event) (match PushActionArray) {
func (rs *PushRuleset) GetActions(room Room, evt *event.Event) (match PushActionArray) {
// Add push rule collections to array in priority order
arrays := []PushRuleCollection{rs.Override, rs.Content, rs.Room, rs.Sender, rs.Underride}
// Loop until one of the push rule collections matches the room/event combo.
@ -78,7 +78,7 @@ func (rs *PushRuleset) GetActions(room Room, event *event.Event) (match PushActi
if pra == nil {
continue
}
if match = pra.GetActions(room, event); match != nil {
if match = pra.GetActions(room, evt); match != nil {
// Match found, return it.
return
}

View file

@ -1,8 +1,6 @@
package mautrix
import (
"encoding/json"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
@ -159,13 +157,13 @@ type RespSync struct {
NextBatch string `json:"next_batch"`
AccountData struct {
Events []json.RawMessage `json:"events"`
Events []*event.Event `json:"events"`
} `json:"account_data"`
Presence struct {
Events []json.RawMessage `json:"events"`
Events []*event.Event `json:"events"`
} `json:"presence"`
ToDevice struct {
Events []json.RawMessage `json:"events"`
Events []*event.Event `json:"events"`
} `json:"to_device"`
DeviceLists struct {
@ -178,10 +176,10 @@ type RespSync struct {
Leave map[id.RoomID]struct {
Summary LazyLoadSummary `json:"summary"`
State struct {
Events []json.RawMessage `json:"events"`
Events []*event.Event `json:"events"`
} `json:"state"`
Timeline struct {
Events []json.RawMessage `json:"events"`
Events []*event.Event `json:"events"`
Limited bool `json:"limited"`
PrevBatch string `json:"prev_batch"`
} `json:"timeline"`
@ -189,24 +187,24 @@ type RespSync struct {
Join map[id.RoomID]struct {
Summary LazyLoadSummary `json:"summary"`
State struct {
Events []json.RawMessage `json:"events"`
Events []*event.Event `json:"events"`
} `json:"state"`
Timeline struct {
Events []json.RawMessage `json:"events"`
Events []*event.Event `json:"events"`
Limited bool `json:"limited"`
PrevBatch string `json:"prev_batch"`
} `json:"timeline"`
Ephemeral struct {
Events []json.RawMessage `json:"events"`
Events []*event.Event `json:"events"`
} `json:"ephemeral"`
AccountData struct {
Events []json.RawMessage `json:"events"`
Events []*event.Event `json:"events"`
} `json:"account_data"`
} `json:"join"`
Invite map[id.RoomID]struct {
Summary LazyLoadSummary `json:"summary"`
State struct {
Events []json.RawMessage `json:"events"`
Events []*event.Event `json:"events"`
} `json:"invite_state"`
} `json:"invite"`
} `json:"rooms"`

View file

@ -34,7 +34,10 @@ func (room Room) GetMembershipState(userID id.UserID) event.Membership {
state := event.MembershipLeave
evt := room.GetStateEvent(event.StateMember, string(userID))
if evt != nil {
state = evt.Content.Membership
membership, ok := evt.Content.Raw["membership"].(string)
if ok {
state = event.Membership(membership)
}
}
return state
}

68
sync.go
View file

@ -1,9 +1,7 @@
package mautrix
import (
"encoding/json"
"fmt"
"os"
"runtime/debug"
"time"
@ -20,7 +18,7 @@ type Syncer interface {
// OnFailedSync returns either the time to wait before retrying or an error to stop syncing permanently.
OnFailedSync(res *RespSync, err error) (time.Duration, error)
// GetFilterJSON for the given user ID. NOT the filter ID.
GetFilterJSON(userID id.UserID) json.RawMessage
GetFilterJSON(userID id.UserID) *Filter
}
// DefaultSyncer is the default syncing implementation. You can either write your own syncer, or selectively
@ -44,17 +42,6 @@ func NewDefaultSyncer(userID id.UserID, store Storer) *DefaultSyncer {
}
}
func parseEvent(roomID id.RoomID, data json.RawMessage) *event.Event {
event := &event.Event{}
err := json.Unmarshal(data, event)
if err != nil {
// TODO add separate handler for these
_, _ = fmt.Fprintf(os.Stderr, "Failed to unmarshal event: %v\n%s\n", err, string(data))
return nil
}
return event
}
// ProcessResponse processes the /sync response in a way suitable for bots. "Suitable for bots" means a stream of
// unrepeating events. Returns a fatal error if a listener panics.
func (s *DefaultSyncer) ProcessResponse(res *RespSync, since string) (err error) {
@ -70,38 +57,26 @@ func (s *DefaultSyncer) ProcessResponse(res *RespSync, since string) (err error)
for roomID, roomData := range res.Rooms.Join {
room := s.getOrCreateRoom(roomID)
for _, data := range roomData.State.Events {
event := parseEvent(roomID, data)
if event != nil {
room.UpdateState(event)
s.notifyListeners(event)
}
for _, evt := range roomData.State.Events {
room.UpdateState(evt)
s.notifyListeners(evt)
}
for _, data := range roomData.Timeline.Events {
event := parseEvent(roomID, data)
if event != nil {
s.notifyListeners(event)
}
for _, evt := range roomData.Timeline.Events {
s.notifyListeners(evt)
}
}
for roomID, roomData := range res.Rooms.Invite {
room := s.getOrCreateRoom(roomID)
for _, data := range roomData.State.Events {
event := parseEvent(roomID, data)
if event != nil {
room.UpdateState(event)
s.notifyListeners(event)
}
for _, evt := range roomData.State.Events {
room.UpdateState(evt)
s.notifyListeners(evt)
}
}
for roomID, roomData := range res.Rooms.Leave {
room := s.getOrCreateRoom(roomID)
for _, data := range roomData.Timeline.Events {
event := parseEvent(roomID, data)
if event.StateKey != nil {
room.UpdateState(event)
s.notifyListeners(event)
}
for _, evt := range roomData.Timeline.Events {
room.UpdateState(evt)
s.notifyListeners(evt)
}
}
return
@ -132,11 +107,10 @@ func (s *DefaultSyncer) shouldProcessResponse(resp *RespSync, since string) bool
// TODO: We probably want to process messages from after the last join event in the timeline.
for roomID, roomData := range resp.Rooms.Join {
for i := len(roomData.Timeline.Events) - 1; i >= 0; i-- {
evtData := roomData.Timeline.Events[i]
// TODO this is horribly inefficient since it's also parsed in ProcessResponse
e := parseEvent(roomID, evtData)
if e != nil && e.Type == event.StateMember && e.GetStateKey() == string(s.UserID) {
if e.Content.Membership == "join" {
evt := roomData.Timeline.Events[i]
if evt.Type == event.StateMember && evt.GetStateKey() == string(s.UserID) {
membership, _ := evt.Content.Raw["membership"].(string)
if membership == "join" {
_, ok := resp.Rooms.Join[roomID]
if !ok {
continue
@ -177,6 +151,12 @@ func (s *DefaultSyncer) OnFailedSync(res *RespSync, err error) (time.Duration, e
}
// GetFilterJSON returns a filter with a timeline limit of 50.
func (s *DefaultSyncer) GetFilterJSON(userID id.UserID) json.RawMessage {
return json.RawMessage(`{"room":{"timeline":{"limit":50}}}`)
func (s *DefaultSyncer) GetFilterJSON(userID id.UserID) *Filter {
return &Filter{
Room: RoomFilter{
Timeline: FilterPart{
Limit: 50,
},
},
}
}