diff --git a/client.go b/client.go index 6be8b012..7f6f1831 100644 --- a/client.go +++ b/client.go @@ -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 diff --git a/event/accountdata.go b/event/accountdata.go new file mode 100644 index 00000000..448395d6 --- /dev/null +++ b/event/accountdata.go @@ -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 +} \ No newline at end of file diff --git a/event/content.go b/event/content.go new file mode 100644 index 00000000..1e2c0eca --- /dev/null +++ b/event/content.go @@ -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 +} diff --git a/event/encryption.go b/event/encryption.go new file mode 100644 index 00000000..dac16a6a --- /dev/null +++ b/event/encryption.go @@ -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"` +} diff --git a/event/ephemeral.go b/event/ephemeral.go new file mode 100644 index 00000000..2d6ecd14 --- /dev/null +++ b/event/ephemeral.go @@ -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"` +} diff --git a/event/events.go b/event/events.go index 6681d82c..631239f5 100644 --- a/event/events.go +++ b/event/events.go @@ -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 -} diff --git a/event/member.go b/event/member.go new file mode 100644 index 00000000..9e4e5250 --- /dev/null +++ b/event/member.go @@ -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"` + } +} diff --git a/event/message.go b/event/message.go new file mode 100644 index 00000000..c03596a4 --- /dev/null +++ b/event/message.go @@ -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 +} diff --git a/event/message_test.go b/event/message_test.go new file mode 100644 index 00000000..6413a138 --- /dev/null +++ b/event/message_test.go @@ -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": "* Hello, World!", + "m.new_content": { + "msgtype": "m.text", + "body": "**Hello**, World!", + "format": "org.matrix.custom.html", + "formatted_body": "Hello, 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, "Hello, 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) +} diff --git a/event/powerlevels.go b/event/powerlevels.go new file mode 100644 index 00000000..3cac9cd7 --- /dev/null +++ b/event/powerlevels.go @@ -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 +} diff --git a/event/reply.go b/event/reply.go index f54cebf7..8c76b4d7 100644 --- a/event/reply.go +++ b/event/reply.go @@ -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 = `
In reply to %s
%s
` 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, diff --git a/event/state.go b/event/state.go new file mode 100644 index 00000000..8d2e3b93 --- /dev/null +++ b/event/state.go @@ -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"` +} diff --git a/event/type.go b/event/type.go index fffaa280..00421c90 100644 --- a/event/type.go +++ b/event/type.go @@ -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} ) diff --git a/filter.go b/filter.go index ef9dad66..71235f2d 100644 --- a/filter.go +++ b/filter.go @@ -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"` diff --git a/format/markdown.go b/format/markdown.go index 9ccb7577..2e285b66 100644 --- a/format/markdown.go +++ b/format/markdown.go @@ -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, } diff --git a/go.mod b/go.mod index 4f8ee27b..8fedbe9b 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index f9f31a87..58748d7d 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pushrules/condition.go b/pushrules/condition.go index 018c8c6c..f11d2b8c 100644 --- a/pushrules/condition.go +++ b/pushrules/condition.go @@ -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)) } diff --git a/pushrules/rule.go b/pushrules/rule.go index 66957fe0..8ce2da77 100644 --- a/pushrules/rule.go +++ b/pushrules/rule.go @@ -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) } diff --git a/pushrules/ruleset.go b/pushrules/ruleset.go index 9e0be2f5..48ae1e35 100644 --- a/pushrules/ruleset.go +++ b/pushrules/ruleset.go @@ -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 } diff --git a/responses.go b/responses.go index 2cbf38e1..a89269e7 100644 --- a/responses.go +++ b/responses.go @@ -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"` diff --git a/room.go b/room.go index e84ef4f9..588097f6 100644 --- a/room.go +++ b/room.go @@ -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 } diff --git a/sync.go b/sync.go index 5581bab2..37cbd13f 100644 --- a/sync.go +++ b/sync.go @@ -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, + }, + }, + } }