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,
+ },
+ },
+ }
}