diff --git a/client.go b/client.go index 7fa8bf3f..7d5ef6e2 100644 --- a/client.go +++ b/client.go @@ -14,10 +14,12 @@ import ( "net/url" "path" "strconv" - "strings" "sync" "sync/atomic" "time" + + "maunium.net/go/mautrix/events" + "maunium.net/go/mautrix/id" ) type Logger interface { @@ -26,10 +28,10 @@ type Logger interface { // Client represents a Matrix client. type Client struct { - HomeserverURL *url.URL // The base homeserver URL - Prefix []string // The API prefix eg '/_matrix/client/r0' - UserID string // The user ID of the client. Used for forming HTTP paths which use the client's user ID. - AccessToken string // The access_token for the client. + HomeserverURL *url.URL // The base homeserver URL + Prefix []string // The API prefix eg '/_matrix/client/r0' + UserID id.UserID // The user ID of the client. Used for forming HTTP paths which use the client's user ID. + AccessToken string // The access_token for the client. UserAgent string Client *http.Client // The underlying HTTP client which will be used to make HTTP requests. Syncer Syncer // The thing which can process /sync responses @@ -161,7 +163,7 @@ func (cli *Client) BuildURLWithQuery(urlPath []string, urlQuery map[string]strin } // SetCredentials sets the user ID and access token on this client instance. -func (cli *Client) SetCredentials(userID, accessToken string) { +func (cli *Client) SetCredentials(userID id.UserID, accessToken string) { cli.AccessToken = accessToken cli.UserID = userID } @@ -339,7 +341,7 @@ 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) { - urlPath := cli.BuildURL("user", cli.UserID, "filter") + urlPath := cli.BuildURL("user", string(cli.UserID), "filter") _, err = cli.MakeRequest("POST", urlPath, &filter, &resp) return } @@ -366,9 +368,9 @@ func (cli *Client) SyncRequest(timeout int, since, filterID string, fullState bo return } -func (cli *Client) register(u string, req *ReqRegister) (resp *RespRegister, uiaResp *RespUserInteractive, err error) { +func (cli *Client) register(url string, req *ReqRegister) (resp *RespRegister, uiaResp *RespUserInteractive, err error) { var bodyBytes []byte - bodyBytes, err = cli.MakeRequest("POST", u, req, nil) + bodyBytes, err = cli.MakeRequest("POST", url, req, nil) if err != nil { httpErr, ok := err.(HTTPError) if !ok { // network error @@ -497,14 +499,14 @@ func (cli *Client) GetDisplayName(mxid string) (resp *RespUserDisplayName, err e // GetOwnDisplayName returns the user's display name. See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname func (cli *Client) GetOwnDisplayName() (resp *RespUserDisplayName, err error) { - urlPath := cli.BuildURL("profile", cli.UserID, "displayname") + urlPath := cli.BuildURL("profile", string(cli.UserID), "displayname") _, err = cli.MakeRequest("GET", urlPath, nil, &resp) return } // SetDisplayName sets the user's profile display name. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-profile-userid-displayname func (cli *Client) SetDisplayName(displayName string) (err error) { - urlPath := cli.BuildURL("profile", cli.UserID, "displayname") + urlPath := cli.BuildURL("profile", string(cli.UserID), "displayname") s := struct { DisplayName string `json:"displayname"` }{displayName} @@ -514,7 +516,7 @@ func (cli *Client) SetDisplayName(displayName string) (err error) { // GetAvatarURL gets the user's avatar URL. See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-avatar-url func (cli *Client) GetAvatarURL() (url string, err error) { - urlPath := cli.BuildURL("profile", cli.UserID, "avatar_url") + urlPath := cli.BuildURL("profile", string(cli.UserID), "avatar_url") s := struct { AvatarURL string `json:"avatar_url"` }{} @@ -528,10 +530,10 @@ func (cli *Client) GetAvatarURL() (url string, err error) { } // SetAvatarURL sets the user's avatar URL. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-profile-userid-avatar-url -func (cli *Client) SetAvatarURL(url string) (err error) { - urlPath := cli.BuildURL("profile", cli.UserID, "avatar_url") +func (cli *Client) SetAvatarURL(url id.ContentURI) (err error) { + urlPath := cli.BuildURL("profile", string(cli.UserID), "avatar_url") s := struct { - AvatarURL string `json:"avatar_url"` + AvatarURL id.ContentURI `json:"avatar_url"` }{url} _, err = cli.MakeRequest("PUT", urlPath, &s, nil) if err != nil { @@ -546,12 +548,12 @@ type ReqSendEvent struct { TransactionID string ParentID string - RelType RelationType + RelType events.RelationType } // SendMessageEvent sends a message event into a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid // contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal. -func (cli *Client) SendMessageEvent(roomID string, eventType EventType, contentJSON interface{}, extra ...ReqSendEvent) (resp *RespSendEvent, err error) { +func (cli *Client) SendMessageEvent(roomID id.RoomID, eventType events.Type, contentJSON interface{}, extra ...ReqSendEvent) (resp *RespSendEvent, err error) { var req ReqSendEvent if len(extra) > 0 { req = extra[0] @@ -569,9 +571,9 @@ func (cli *Client) SendMessageEvent(roomID string, eventType EventType, contentJ queryParams["ts"] = strconv.FormatInt(req.Timestamp, 10) } - urlData := []string{"rooms", roomID, "send", eventType.String(), txnID} + urlData := []string{"rooms", string(roomID), "send", eventType.String(), txnID} if len(req.ParentID) > 0 { - urlData = []string{"rooms", roomID, "send_relation", req.ParentID, string(req.RelType), eventType.String(), txnID} + urlData = []string{"rooms", string(roomID), "send_relation", req.ParentID, string(req.RelType), eventType.String(), txnID} } urlPath := cli.BuildURLWithQuery(urlData, queryParams) @@ -581,16 +583,16 @@ func (cli *Client) SendMessageEvent(roomID string, eventType EventType, contentJ // SendStateEvent sends a state event into a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-state-eventtype-statekey // contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal. -func (cli *Client) SendStateEvent(roomID string, eventType EventType, stateKey string, contentJSON interface{}) (resp *RespSendEvent, err error) { - urlPath := cli.BuildURL("rooms", roomID, "state", eventType.String(), stateKey) +func (cli *Client) SendStateEvent(roomID id.RoomID, eventType events.Type, stateKey string, contentJSON interface{}) (resp *RespSendEvent, err error) { + urlPath := cli.BuildURL("rooms", string(roomID), "state", eventType.String(), stateKey) _, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp) return } // SendStateEvent sends a state event into a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-state-eventtype-statekey // contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal. -func (cli *Client) SendMassagedStateEvent(roomID string, eventType EventType, stateKey string, contentJSON interface{}, ts int64) (resp *RespSendEvent, err error) { - urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "state", eventType.String(), stateKey}, map[string]string{ +func (cli *Client) SendMassagedStateEvent(roomID id.RoomID, eventType events.Type, stateKey string, contentJSON interface{}, ts int64) (resp *RespSendEvent, err error) { + urlPath := cli.BuildURLWithQuery([]string{"rooms", string(roomID), "state", eventType.String(), stateKey}, map[string]string{ "ts": strconv.FormatInt(ts, 10), }) _, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp) @@ -599,54 +601,54 @@ func (cli *Client) SendMassagedStateEvent(roomID string, eventType EventType, st // 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, text string) (*RespSendEvent, error) { - return cli.SendMessageEvent(roomID, EventMessage, Content{ - MsgType: MsgText, +func (cli *Client) SendText(roomID id.RoomID, text string) (*RespSendEvent, error) { + return cli.SendMessageEvent(roomID, events.EventMessage, events.Content{ + MsgType: events.MsgText, Body: text, }) } // 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, body, url string) (*RespSendEvent, error) { - return cli.SendMessageEvent(roomID, EventMessage, Content{ - MsgType: MsgImage, +func (cli *Client) SendImage(roomID id.RoomID, body string, url id.ContentURI) (*RespSendEvent, error) { + return cli.SendMessageEvent(roomID, events.EventMessage, events.Content{ + MsgType: events.MsgImage, Body: body, - URL: url, + URL: url.CUString(), }) } // 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, body, url string) (*RespSendEvent, error) { - return cli.SendMessageEvent(roomID, EventMessage, Content{ - MsgType: MsgVideo, +func (cli *Client) SendVideo(roomID id.RoomID, body string, url id.ContentURI) (*RespSendEvent, error) { + return cli.SendMessageEvent(roomID, events.EventMessage, events.Content{ + MsgType: events.MsgVideo, Body: body, - URL: url, + URL: url.CUString(), }) } // 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, text string) (*RespSendEvent, error) { - return cli.SendMessageEvent(roomID, EventMessage, Content{ - MsgType: MsgNotice, +func (cli *Client) SendNotice(roomID id.RoomID, text string) (*RespSendEvent, error) { + return cli.SendMessageEvent(roomID, events.EventMessage, events.Content{ + MsgType: events.MsgNotice, Body: text, }) } -func (cli *Client) SendReaction(roomID, eventID, reaction string) (*RespSendEvent, error) { - return cli.SendMessageEvent(roomID, EventReaction, Content{ - RelatesTo: &RelatesTo{ +func (cli *Client) SendReaction(roomID id.RoomID, eventID id.EventID, reaction string) (*RespSendEvent, error) { + return cli.SendMessageEvent(roomID, events.EventReaction, events.Content{ + RelatesTo: &events.RelatesTo{ EventID: eventID, - Type: RelAnnotation, + Type: events.RelAnnotation, Key: reaction, }, }) } // RedactEvent redacts the given event. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-redact-eventid-txnid -func (cli *Client) RedactEvent(roomID, eventID string, extra ...ReqRedact) (resp *RespSendEvent, err error) { +func (cli *Client) RedactEvent(roomID id.RoomID, eventID id.EventID, extra ...ReqRedact) (resp *RespSendEvent, err error) { req := ReqRedact{} if len(extra) > 0 { req = extra[0] @@ -657,7 +659,7 @@ func (cli *Client) RedactEvent(roomID, eventID string, extra ...ReqRedact) (resp } else { txnID = cli.TxnID() } - urlPath := cli.BuildURL("rooms", roomID, "redact", eventID, txnID) + urlPath := cli.BuildURL("rooms", string(roomID), "redact", string(eventID), txnID) _, err = cli.MakeRequest("PUT", urlPath, req, &resp) return } @@ -674,65 +676,65 @@ func (cli *Client) CreateRoom(req *ReqCreateRoom) (resp *RespCreateRoom, err err } // LeaveRoom leaves the given room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-leave -func (cli *Client) LeaveRoom(roomID string) (resp *RespLeaveRoom, err error) { - u := cli.BuildURL("rooms", roomID, "leave") +func (cli *Client) LeaveRoom(roomID id.RoomID) (resp *RespLeaveRoom, err error) { + u := cli.BuildURL("rooms", string(roomID), "leave") _, err = cli.MakeRequest("POST", u, struct{}{}, &resp) return } // ForgetRoom forgets a room entirely. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-forget -func (cli *Client) ForgetRoom(roomID string) (resp *RespForgetRoom, err error) { - u := cli.BuildURL("rooms", roomID, "forget") +func (cli *Client) ForgetRoom(roomID id.RoomID) (resp *RespForgetRoom, err error) { + u := cli.BuildURL("rooms", string(roomID), "forget") _, err = cli.MakeRequest("POST", u, struct{}{}, &resp) return } // InviteUser invites a user to a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-invite -func (cli *Client) InviteUser(roomID string, req *ReqInviteUser) (resp *RespInviteUser, err error) { - u := cli.BuildURL("rooms", roomID, "invite") +func (cli *Client) InviteUser(roomID id.RoomID, req *ReqInviteUser) (resp *RespInviteUser, err error) { + u := cli.BuildURL("rooms", string(roomID), "invite") _, err = cli.MakeRequest("POST", u, req, &resp) return } // InviteUserByThirdParty invites a third-party identifier to a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#invite-by-third-party-id-endpoint -func (cli *Client) InviteUserByThirdParty(roomID string, req *ReqInvite3PID) (resp *RespInviteUser, err error) { - u := cli.BuildURL("rooms", roomID, "invite") +func (cli *Client) InviteUserByThirdParty(roomID id.RoomID, req *ReqInvite3PID) (resp *RespInviteUser, err error) { + u := cli.BuildURL("rooms", string(roomID), "invite") _, err = cli.MakeRequest("POST", u, req, &resp) return } // KickUser kicks a user from a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-kick -func (cli *Client) KickUser(roomID string, req *ReqKickUser) (resp *RespKickUser, err error) { - u := cli.BuildURL("rooms", roomID, "kick") +func (cli *Client) KickUser(roomID id.RoomID, req *ReqKickUser) (resp *RespKickUser, err error) { + u := cli.BuildURL("rooms", string(roomID), "kick") _, err = cli.MakeRequest("POST", u, req, &resp) return } // BanUser bans a user from a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-ban -func (cli *Client) BanUser(roomID string, req *ReqBanUser) (resp *RespBanUser, err error) { - u := cli.BuildURL("rooms", roomID, "ban") +func (cli *Client) BanUser(roomID id.RoomID, req *ReqBanUser) (resp *RespBanUser, err error) { + u := cli.BuildURL("rooms", string(roomID), "ban") _, err = cli.MakeRequest("POST", u, req, &resp) return } // UnbanUser unbans a user from a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-unban -func (cli *Client) UnbanUser(roomID string, req *ReqUnbanUser) (resp *RespUnbanUser, err error) { - u := cli.BuildURL("rooms", roomID, "unban") +func (cli *Client) UnbanUser(roomID id.RoomID, req *ReqUnbanUser) (resp *RespUnbanUser, err error) { + u := cli.BuildURL("rooms", string(roomID), "unban") _, err = cli.MakeRequest("POST", u, req, &resp) return } // UserTyping sets the typing status of the user. See https://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-typing-userid -func (cli *Client) UserTyping(roomID string, typing bool, timeout int64) (resp *RespTyping, err error) { +func (cli *Client) UserTyping(roomID id.RoomID, typing bool, timeout int64) (resp *RespTyping, err error) { req := ReqTyping{Typing: typing, Timeout: timeout} - u := cli.BuildURL("rooms", roomID, "typing", cli.UserID) + u := cli.BuildURL("rooms", string(roomID), "typing", string(cli.UserID)) _, err = cli.MakeRequest("PUT", u, req, &resp) return } func (cli *Client) SetPresence(status string) (err error) { req := ReqPresence{Presence: status} - u := cli.BuildURL("presence", cli.UserID, "status") + u := cli.BuildURL("presence", string(cli.UserID), "status") _, err = cli.MakeRequest("PUT", u, req, nil) return } @@ -740,8 +742,8 @@ func (cli *Client) SetPresence(status string) (err error) { // StateEvent gets a single state event in a room. It will attempt to JSON unmarshal into the given "outContent" struct with // the HTTP response body, or return an error. // See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-state-eventtype-statekey -func (cli *Client) StateEvent(roomID string, eventType EventType, stateKey string, outContent interface{}) (err error) { - u := cli.BuildURL("rooms", roomID, "state", eventType.String(), stateKey) +func (cli *Client) StateEvent(roomID id.RoomID, eventType events.Type, stateKey string, outContent interface{}) (err error) { + u := cli.BuildURL("rooms", string(roomID), "state", eventType.String(), stateKey) _, err = cli.MakeRequest("GET", u, nil, outContent) return } @@ -758,15 +760,8 @@ func (cli *Client) UploadLink(link string) (*RespMediaUpload, error) { return cli.Upload(res.Body, res.Header.Get("Content-Type"), res.ContentLength) } -func (cli *Client) Download(mxcURL string) (io.ReadCloser, error) { - if !strings.HasPrefix(mxcURL, "mxc://") { - return nil, errors.New("invalid Matrix content URL") - } - parts := strings.Split(mxcURL[len("mxc://"):], "/") - if len(parts) != 2 { - return nil, errors.New("invalid Matrix content URL") - } - u := cli.BuildBaseURL("_matrix", "media", "r0", "download", parts[0], parts[1]) +func (cli *Client) Download(mxcURL id.ContentURI) (io.ReadCloser, error) { + u := cli.BuildBaseURL("_matrix", "media", "r0", "download", mxcURL.Homeserver, mxcURL.FileID) resp, err := cli.Client.Get(u) if err != nil { return nil, err @@ -774,7 +769,7 @@ func (cli *Client) Download(mxcURL string) (io.ReadCloser, error) { return resp.Body, nil } -func (cli *Client) DownloadBytes(mxcURL string) ([]byte, error) { +func (cli *Client) DownloadBytes(mxcURL id.ContentURI) ([]byte, error) { resp, err := cli.Download(mxcURL) if err != nil { return nil, err @@ -829,13 +824,13 @@ func (cli *Client) Upload(content io.Reader, contentType string, contentLength i // // In general, usage of this API is discouraged in favour of /sync, as calling this API can race with incoming membership changes. // This API is primarily designed for application services which may want to efficiently look up joined members in a room. -func (cli *Client) JoinedMembers(roomID string) (resp *RespJoinedMembers, err error) { - u := cli.BuildURL("rooms", roomID, "joined_members") +func (cli *Client) JoinedMembers(roomID id.RoomID) (resp *RespJoinedMembers, err error) { + u := cli.BuildURL("rooms", string(roomID), "joined_members") _, err = cli.MakeRequest("GET", u, nil, &resp) return } -func (cli *Client) Members(roomID string, req ...ReqMembers) (resp *RespMembers, err error) { +func (cli *Client) Members(roomID id.RoomID, req ...ReqMembers) (resp *RespMembers, err error) { var extra ReqMembers if len(req) > 0 { extra = req[0] @@ -850,7 +845,7 @@ func (cli *Client) Members(roomID string, req ...ReqMembers) (resp *RespMembers, if len(extra.NotMembership) > 0 { query["not_membership"] = string(extra.NotMembership) } - u := cli.BuildURLWithQuery([]string{"rooms", roomID, "members"}, query) + u := cli.BuildURLWithQuery([]string{"rooms", string(roomID), "members"}, query) _, err = cli.MakeRequest("GET", u, nil, &resp) return } @@ -868,7 +863,7 @@ func (cli *Client) JoinedRooms() (resp *RespJoinedRooms, err error) { // Messages returns a list of message and state events for a room. It uses // 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, from, to string, dir rune, limit int) (resp *RespMessages, err error) { +func (cli *Client) Messages(roomID id.RoomID, from, to string, dir rune, limit int) (resp *RespMessages, err error) { query := map[string]string{ "from": from, "dir": string(dir), @@ -881,12 +876,12 @@ func (cli *Client) Messages(roomID, from, to string, dir rune, limit int) (resp query["limit"] = strconv.Itoa(limit) } - urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "messages"}, query) + urlPath := cli.BuildURLWithQuery([]string{"rooms", string(roomID), "messages"}, query) _, err = cli.MakeRequest("GET", urlPath, nil, &resp) return } -func (cli *Client) GetEvent(roomID, eventID string) (resp *Event, err error) { +func (cli *Client) GetEvent(roomID, eventID string) (resp *events.Event, err error) { urlPath := cli.BuildURL("rooms", roomID, "event", eventID) _, err = cli.MakeRequest("GET", urlPath, nil, &resp) return @@ -898,9 +893,9 @@ func (cli *Client) MarkRead(roomID, eventID string) (err error) { return } -func (cli *Client) AddTag(roomID, tag string, order float64) (err error) { - urlPath := cli.BuildURL("user", cli.UserID, "rooms", roomID, "tags", tag) - var tagData Tag +func (cli *Client) AddTag(roomID id.RoomID, tag string, order float64) (err error) { + urlPath := cli.BuildURL("user", string(cli.UserID), "rooms", string(roomID), "tags", tag) + var tagData events.Tag if order == order { tagData.Order = json.Number(strconv.FormatFloat(order, 'e', -1, 64)) } @@ -908,15 +903,15 @@ func (cli *Client) AddTag(roomID, tag string, order float64) (err error) { return } -func (cli *Client) RemoveTag(roomID, tag string) (err error) { - urlPath := cli.BuildURL("user", cli.UserID, "rooms", roomID, "tags", tag) +func (cli *Client) RemoveTag(roomID id.RoomID, tag string) (err error) { + urlPath := cli.BuildURL("user", string(cli.UserID), "rooms", string(roomID), "tags", tag) _, err = cli.MakeRequest("DELETE", urlPath, nil, nil) return } -func (cli *Client) SetTags(roomID string, tags Tags) (err error ){ - urlPath := cli.BuildURL("user", cli.UserID, "rooms", roomID, "account_data", "m.tag") - _, err = cli.MakeRequest("PUT", urlPath, map[string]Tags{ +func (cli *Client) SetTags(roomID id.RoomID, tags events.Tags) (err error) { + urlPath := cli.BuildURL("user", string(cli.UserID), "rooms", string(roomID), "account_data", "m.tag") + _, err = cli.MakeRequest("PUT", urlPath, map[string]events.Tags{ "tags": tags, }, nil) return @@ -930,31 +925,37 @@ func (cli *Client) TurnServer() (resp *RespTurnServer, err error) { return } -func (cli *Client) CreateAlias(alias, roomID string) (resp *RespAliasCreate, err error) { - urlPath := cli.BuildURL("directory", "room", alias) +func (cli *Client) CreateAlias(alias id.RoomAlias, roomID id.RoomID) (resp *RespAliasCreate, err error) { + urlPath := cli.BuildURL("directory", "room", string(alias)) _, err = cli.MakeRequest("PUT", urlPath, &ReqAliasCreate{RoomID: roomID}, &resp) return } -func (cli *Client) ResolveAlias(alias string) (resp *RespAliasResolve, err error) { - urlPath := cli.BuildURL("directory", "room", alias) +func (cli *Client) ResolveAlias(alias id.RoomAlias) (resp *RespAliasResolve, err error) { + urlPath := cli.BuildURL("directory", "room", string(alias)) _, err = cli.MakeRequest("GET", urlPath, nil, &resp) return } -func (cli *Client) DeleteAlias(alias string) (resp *RespAliasDelete, err error) { - urlPath := cli.BuildURL("directory", "room", alias) +func (cli *Client) DeleteAlias(alias id.RoomAlias) (resp *RespAliasDelete, err error) { + urlPath := cli.BuildURL("directory", "room", string(alias)) _, err = cli.MakeRequest("DELETE", urlPath, nil, &resp) return } +func (cli *Client) UploadKeys(req *ReqUploadKeys) (resp *RespUploadKeys, err error) { + urlPath := cli.BuildURL("keys", "upload") + _, err = cli.MakeRequest("POST", urlPath, req, &resp) + return +} + func (cli *Client) TxnID() string { txnID := atomic.AddInt32(&cli.txnID, 1) - return fmt.Sprintf("go%d%d", time.Now().UnixNano(), txnID) + return fmt.Sprintf("mautrix-go/%d/%d", time.Now().UnixNano(), txnID) } // NewClient creates a new Matrix Client ready for syncing -func NewClient(homeserverURL, userID, accessToken string) (*Client, error) { +func NewClient(homeserverURL string, userID id.UserID, accessToken string) (*Client, error) { hsURL, err := url.Parse(homeserverURL) if err != nil { return nil, err diff --git a/events.go b/events/events.go similarity index 59% rename from events.go rename to events/events.go index 8706fcba..78c2e5c5 100644 --- a/events.go +++ b/events/events.go @@ -1,125 +1,18 @@ -// Copyright 2019 Tulir Asokan -package mautrix +// 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 events import ( "encoding/json" "strconv" - "strings" "sync" -) -type EventTypeClass int - -const ( - // Normal message events - MessageEventType EventTypeClass = iota - // State events - StateEventType - // Ephemeral events - EphemeralEventType - // Account data events - AccountDataEventType - // Unknown events - UnknownEventType -) - -type EventType struct { - Type string - Class EventTypeClass -} - -func NewEventType(name string) EventType { - evtType := EventType{Type: name} - evtType.Class = evtType.GuessClass() - return evtType -} - -func (et *EventType) IsState() bool { - return et.Class == StateEventType -} - -func (et *EventType) IsEphemeral() bool { - return et.Class == EphemeralEventType -} - -func (et *EventType) IsAccountData() bool { - return et.Class == AccountDataEventType -} - -func (et *EventType) IsCustom() bool { - return !strings.HasPrefix(et.Type, "m.") -} - -func (et *EventType) GuessClass() EventTypeClass { - switch et.Type { - case StateAliases.Type, StateCanonicalAlias.Type, StateCreate.Type, StateJoinRules.Type, StateMember.Type, - StatePowerLevels.Type, StateRoomName.Type, StateRoomAvatar.Type, StateTopic.Type, StatePinnedEvents.Type, - StateTombstone.Type: - return StateEventType - case EphemeralEventReceipt.Type, EphemeralEventTyping.Type, EphemeralEventPresence.Type: - return EphemeralEventType - case AccountDataDirectChats.Type, AccountDataPushRules.Type, AccountDataRoomTags.Type: - return AccountDataEventType - case EventRedaction.Type, EventMessage.Type, EventEncrypted.Type, EventReaction.Type, EventSticker.Type: - return MessageEventType - default: - return UnknownEventType - } -} - -func (et *EventType) UnmarshalJSON(data []byte) error { - err := json.Unmarshal(data, &et.Type) - if err != nil { - return err - } - et.Class = et.GuessClass() - return nil -} - -func (et *EventType) MarshalJSON() ([]byte, error) { - return json.Marshal(&et.Type) -} - -func (et *EventType) String() string { - return et.Type -} - -// State events -var ( - StateAliases = EventType{"m.room.aliases", StateEventType} - StateCanonicalAlias = EventType{"m.room.canonical_alias", StateEventType} - StateCreate = EventType{"m.room.create", StateEventType} - StateJoinRules = EventType{"m.room.join_rules", StateEventType} - StateMember = EventType{"m.room.member", StateEventType} - StatePowerLevels = EventType{"m.room.power_levels", StateEventType} - StateRoomName = EventType{"m.room.name", StateEventType} - StateTopic = EventType{"m.room.topic", StateEventType} - StateRoomAvatar = EventType{"m.room.avatar", StateEventType} - StatePinnedEvents = EventType{"m.room.pinned_events", StateEventType} - StateTombstone = EventType{"m.room.tombstone", StateEventType} -) - -// Message events -var ( - EventRedaction = EventType{"m.room.redaction", MessageEventType} - EventMessage = EventType{"m.room.message", MessageEventType} - EventEncrypted = EventType{"m.room.encrypted", MessageEventType} - EventReaction = EventType{"m.reaction", MessageEventType} - EventSticker = EventType{"m.sticker", MessageEventType} -) - -// Ephemeral events -var ( - EphemeralEventReceipt = EventType{"m.receipt", EphemeralEventType} - EphemeralEventTyping = EventType{"m.typing", EphemeralEventType} - EphemeralEventPresence = EventType{"m.presence", EphemeralEventType} -) - -// Account data events -var ( - AccountDataDirectChats = EventType{"m.direct", AccountDataEventType} - AccountDataPushRules = EventType{"m.push_rules", AccountDataEventType} - AccountDataRoomTags = EventType{"m.tag", AccountDataEventType} + "maunium.net/go/mautrix/crypto/attachment" + "maunium.net/go/mautrix/id" ) type MessageType string @@ -145,17 +38,15 @@ const ( // 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. - Sender string `json:"sender"` // The user ID of the sender of the event - Type EventType `json:"type"` // The event type - Timestamp int64 `json:"origin_server_ts"` // The unix timestamp when this message was sent by the origin server - ID string `json:"event_id"` // The unique ID of this event - RoomID string `json:"room_id"` // The room the event was sent to. May be nil (e.g. for presence) - Content Content `json:"content"` // The JSON content of the event. - Redacts string `json:"redacts,omitempty"` // The event ID that was redacted if a m.room.redaction event - Unsigned Unsigned `json:"unsigned,omitempty"` // Unsigned content set by own homeserver. - - InviteRoomState []StrippedState `json:"invite_room_state"` + StateKey *string `json:"state_key,omitempty"` // The state key for the event. Only present on State Events. + Sender id.UserID `json:"sender"` // The user ID of the sender of the event + Type Type `json:"type"` // The event type + Timestamp int64 `json:"origin_server_ts"` // The unix timestamp when this message was sent by the origin server + ID id.EventID `json:"event_id"` // The unique ID of this event + RoomID id.RoomID `json:"room_id"` // The room the event was sent to. May be nil (e.g. for presence) + Content Content `json:"content"` // The JSON content of the event. + Redacts id.EventID `json:"redacts,omitempty"` // The event ID that was redacted if a m.room.redaction event + Unsigned Unsigned `json:"unsigned,omitempty"` // Unsigned content set by own homeserver. } func (evt *Event) GetStateKey() string { @@ -166,51 +57,64 @@ func (evt *Event) GetStateKey() string { } type StrippedState struct { - Content Content `json:"content"` - Type EventType `json:"type"` - StateKey string `json:"state_key"` + Content Content `json:"content"` + Type Type `json:"type"` + StateKey string `json:"state_key"` } type Unsigned struct { - PrevContent *Content `json:"prev_content,omitempty"` - PrevSender string `json:"prev_sender,omitempty"` - ReplacesState string `json:"replaces_state,omitempty"` - Age int64 `json:"age,omitempty"` - TransactionID string `json:"transaction_id,omitempty"` - Relations Relations `json:"m.relations,omitempty"` - RedactedBy string `json:"redacted_by,omitempty"` - RedactedBecause *Event `json:"redacted_because,omitempty"` + PrevContent *Content `json:"prev_content,omitempty"` + PrevSender id.UserID `json:"prev_sender,omitempty"` + ReplacesState id.EventID `json:"replaces_state,omitempty"` + Age int64 `json:"age,omitempty"` + TransactionID string `json:"transaction_id,omitempty"` + Relations Relations `json:"m.relations,omitempty"` + 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"` - - Info *FileInfo `json:"info,omitempty"` - URL string `json:"url,omitempty"` - - // Membership key for easy access in m.room.member events - Membership Membership `json:"membership,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 - Aliases []string `json:"aliases,omitempty"` - Alias string `json:"alias,omitempty"` - Name string `json:"name,omitempty"` - Topic string `json:"topic,omitempty"` + // Membership key for easy access in m.room.member events + Membership Membership `json:"membership,omitempty"` + // 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 string `json:"replacement_room,omitempty"` - - RoomTags Tags `json:"tags,omitempty"` - TypingUserIDs []string `json:"user_ids,omitempty"` + // m.tag account data + RoomTags Tags `json:"tags,omitempty"` + // m.typing ephemeral + TypingUserIDs []id.UserID `json:"user_ids,omitempty"` } type serializableContent Content @@ -388,7 +292,7 @@ func (pl *PowerLevels) EnsureUserLevel(userID string, level int) bool { return false } -func (pl *PowerLevels) GetEventLevel(eventType EventType) int { +func (pl *PowerLevels) GetEventLevel(eventType Type) int { pl.eventsLock.RLock() defer pl.eventsLock.RUnlock() level, ok := pl.Events[eventType.String()] @@ -401,7 +305,7 @@ func (pl *PowerLevels) GetEventLevel(eventType EventType) int { return level } -func (pl *PowerLevels) SetEventLevel(eventType EventType, level int) { +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) { @@ -411,7 +315,7 @@ func (pl *PowerLevels) SetEventLevel(eventType EventType, level int) { } } -func (pl *PowerLevels) EnsureEventLevel(eventType EventType, level int) bool { +func (pl *PowerLevels) EnsureEventLevel(eventType Type, level int) bool { existingLevel := pl.GetEventLevel(eventType) if existingLevel != level { pl.SetEventLevel(eventType, level) @@ -421,13 +325,14 @@ func (pl *PowerLevels) EnsureEventLevel(eventType EventType, level int) bool { } type FileInfo struct { - MimeType string `json:"mimetype,omitempty"` - ThumbnailInfo *FileInfo `json:"thumbnail_info,omitempty"` - ThumbnailURL string `json:"thumbnail_url,omitempty"` - Width int `json:"-"` - Height int `json:"-"` - Duration uint `json:"-"` - Size int `json:"-"` + MimeType string `json:"mimetype,omitempty"` + ThumbnailInfo *FileInfo `json:"thumbnail_info,omitempty"` + ThumbnailURL string `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 { diff --git a/relations.go b/events/relations.go similarity index 87% rename from relations.go rename to events/relations.go index 69b4d365..f99b3f69 100644 --- a/relations.go +++ b/events/relations.go @@ -1,8 +1,15 @@ -// Copyright 2019 Tulir Asokan -package mautrix +// 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 events import ( "encoding/json" + + "maunium.net/go/mautrix/id" ) type RelationType string @@ -15,36 +22,36 @@ const ( type RelatesTo struct { Type RelationType - EventID string + EventID id.EventID Key string } type serializableInReplyTo struct { - EventID string `json:"event_id,omitempty"` + EventID id.EventID `json:"event_id,omitempty"` } type serializableRelatesTo struct { InReplyTo *serializableInReplyTo `json:"m.in_reply_to,omitempty"` Type RelationType `json:"rel_type,omitempty"` - EventID string `json:"event_id,omitempty"` + EventID id.EventID `json:"event_id,omitempty"` Key string `json:"key,omitempty"` } -func (rel *RelatesTo) GetReplaceID() string { +func (rel *RelatesTo) GetReplaceID() id.EventID { if rel.Type == RelReplace { return rel.EventID } return "" } -func (rel *RelatesTo) GetReferenceID() string { +func (rel *RelatesTo) GetReferenceID() id.EventID { if rel.Type == RelReference { return rel.EventID } return "" } -func (rel *RelatesTo) GetAnnotationID() string { +func (rel *RelatesTo) GetAnnotationID() id.EventID { if rel.Type == RelAnnotation { return rel.EventID } diff --git a/reply.go b/events/reply.go similarity index 83% rename from reply.go rename to events/reply.go index 8e068ba8..b0a912c4 100644 --- a/reply.go +++ b/events/reply.go @@ -1,5 +1,10 @@ -// Copyright 2018 Tulir Asokan -package mautrix +// 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 events import ( "fmt" @@ -7,6 +12,8 @@ import ( "strings" "golang.org/x/net/html" + + "maunium.net/go/mautrix/id" ) var HTMLReplyFallbackRegex = regexp.MustCompile(`^[\s\S]+?`) @@ -36,7 +43,7 @@ func (content *Content) RemoveReplyFallback() { } } -func (content *Content) GetReplyTo() string { +func (content *Content) GetReplyTo() id.EventID { if content.RelatesTo != nil && content.RelatesTo.Type == RelReference { return content.RelatesTo.EventID } @@ -64,9 +71,9 @@ func (evt *Event) GenerateReplyFallbackText() string { senderDisplayName := evt.Sender var fallbackText strings.Builder - fmt.Fprintf(&fallbackText, "> <%s> %s", senderDisplayName, firstLine) + _, _ = fmt.Fprintf(&fallbackText, "> <%s> %s", senderDisplayName, firstLine) for _, line := range lines { - fmt.Fprintf(&fallbackText, "\n> %s", line) + _, _ = fmt.Fprintf(&fallbackText, "\n> %s", line) } fallbackText.WriteString("\n\n") return fallbackText.String() diff --git a/events/type.go b/events/type.go new file mode 100644 index 00000000..2b4b10a0 --- /dev/null +++ b/events/type.go @@ -0,0 +1,140 @@ +// 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 events + +import ( + "encoding/json" + "strings" +) + +type EventTypeClass int + +const ( + // Normal message events + MessageEventType EventTypeClass = iota + // State events + StateEventType + // Ephemeral events + EphemeralEventType + // Account data events + AccountDataEventType + // Device-to-device events + ToDeviceEventType + // Unknown events + UnknownEventType +) + +type Type struct { + Type string + Class EventTypeClass +} + +func NewEventType(name string) Type { + evtType := Type{Type: name} + evtType.Class = evtType.GuessClass() + return evtType +} + +func (et *Type) IsState() bool { + return et.Class == StateEventType +} + +func (et *Type) IsEphemeral() bool { + return et.Class == EphemeralEventType +} + +func (et *Type) IsAccountData() bool { + return et.Class == AccountDataEventType +} + +func (et *Type) IsToDevice() bool { + return et.Class == ToDeviceEventType +} + +func (et *Type) IsCustom() bool { + return !strings.HasPrefix(et.Type, "m.") +} + +func (et *Type) GuessClass() EventTypeClass { + switch et.Type { + case StateAliases.Type, StateCanonicalAlias.Type, StateCreate.Type, StateJoinRules.Type, StateMember.Type, + StatePowerLevels.Type, StateRoomName.Type, StateRoomAvatar.Type, StateTopic.Type, StatePinnedEvents.Type, + StateTombstone.Type, StateEncryption.Type: + return StateEventType + case EphemeralEventReceipt.Type, EphemeralEventTyping.Type, EphemeralEventPresence.Type: + return EphemeralEventType + case AccountDataDirectChats.Type, AccountDataPushRules.Type, AccountDataRoomTags.Type: + return AccountDataEventType + case EventRedaction.Type, EventMessage.Type, EventEncrypted.Type, EventReaction.Type, EventSticker.Type: + return MessageEventType + case ToDeviceNewDevice.Type: + return ToDeviceEventType + default: + return UnknownEventType + } +} + +func (et *Type) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, &et.Type) + if err != nil { + return err + } + et.Class = et.GuessClass() + return nil +} + +func (et *Type) MarshalJSON() ([]byte, error) { + return json.Marshal(&et.Type) +} + +func (et *Type) String() string { + return et.Type +} + +// 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} +) + +// Message events +var ( + EventRedaction = Type{"m.room.redaction", MessageEventType} + EventMessage = Type{"m.room.message", MessageEventType} + EventEncrypted = Type{"m.room.encrypted", MessageEventType} + EventReaction = Type{"m.reaction", MessageEventType} + EventSticker = Type{"m.sticker", MessageEventType} +) + +// Ephemeral events +var ( + EphemeralEventReceipt = Type{"m.receipt", EphemeralEventType} + EphemeralEventTyping = Type{"m.typing", EphemeralEventType} + EphemeralEventPresence = Type{"m.presence", EphemeralEventType} +) + +// Account data events +var ( + AccountDataDirectChats = Type{"m.direct", AccountDataEventType} + AccountDataPushRules = Type{"m.push_rules", AccountDataEventType} + AccountDataRoomTags = Type{"m.tag", AccountDataEventType} +) + +// Device-to-device events +var ( + ToDeviceNewDevice = Type{"m.new_device", ToDeviceEventType} +) diff --git a/filter.go b/filter.go index a3e18310..850d5d28 100644 --- a/filter.go +++ b/filter.go @@ -1,52 +1,52 @@ // Copyright 2017 Jan Christian Grünhage -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. package mautrix -import "errors" +import ( + "errors" + + "maunium.net/go/mautrix/events" + "maunium.net/go/mautrix/id" +) + +type EventFormat string + +const ( + EventFormatClient EventFormat = "client" + EventFormatFederation EventFormat = "federation" +) //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 type Filter struct { - AccountData FilterPart `json:"account_data,omitempty"` - EventFields []string `json:"event_fields,omitempty"` - EventFormat string `json:"event_format,omitempty"` - Presence FilterPart `json:"presence,omitempty"` - Room RoomFilter `json:"room,omitempty"` + AccountData FilterPart `json:"account_data,omitempty"` + EventFields []string `json:"event_fields,omitempty"` + EventFormat EventFormat `json:"event_format,omitempty"` + Presence FilterPart `json:"presence,omitempty"` + Room RoomFilter `json:"room,omitempty"` } // RoomFilter is used to define filtering rules for room events type RoomFilter struct { - AccountData FilterPart `json:"account_data,omitempty"` - Ephemeral FilterPart `json:"ephemeral,omitempty"` - IncludeLeave bool `json:"include_leave,omitempty"` - NotRooms []string `json:"not_rooms,omitempty"` - Rooms []string `json:"rooms,omitempty"` - State FilterPart `json:"state,omitempty"` - Timeline FilterPart `json:"timeline,omitempty"` + AccountData FilterPart `json:"account_data,omitempty"` + Ephemeral FilterPart `json:"ephemeral,omitempty"` + IncludeLeave bool `json:"include_leave,omitempty"` + NotRooms []id.RoomID `json:"not_rooms,omitempty"` + Rooms []id.RoomID `json:"rooms,omitempty"` + State FilterPart `json:"state,omitempty"` + Timeline FilterPart `json:"timeline,omitempty"` } // FilterPart is used to define filtering rules for specific categories of events type FilterPart struct { - NotRooms []string `json:"not_rooms,omitempty"` - Rooms []string `json:"rooms,omitempty"` - Limit int `json:"limit,omitempty"` - NotSenders []string `json:"not_senders,omitempty"` - NotTypes []string `json:"not_types,omitempty"` - Senders []string `json:"senders,omitempty"` - Types []string `json:"types,omitempty"` - ContainsURL *bool `json:"contains_url,omitempty"` + NotRooms []id.RoomID `json:"not_rooms,omitempty"` + Rooms []id.RoomID `json:"rooms,omitempty"` + Limit int `json:"limit,omitempty"` + NotSenders []id.UserID `json:"not_senders,omitempty"` + NotTypes []events.Type `json:"not_types,omitempty"` + Senders []id.UserID `json:"senders,omitempty"` + Types []events.Type `json:"types,omitempty"` + ContainsURL *bool `json:"contains_url,omitempty"` LazyLoadMembers bool `json:"lazy_load_members,omitempty"` IncludeRedundantMembers bool `json:"include_redundant_members,omitempty"` @@ -54,7 +54,7 @@ type FilterPart struct { // Validate checks if the filter contains valid property values func (filter *Filter) Validate() error { - if filter.EventFormat != "client" && filter.EventFormat != "federation" { + if filter.EventFormat != EventFormatClient && filter.EventFormat != EventFormatFederation { return errors.New("Bad event_format value. Must be one of [\"client\", \"federation\"]") } return nil diff --git a/format/htmlparser.go b/format/htmlparser.go index f7573dbe..fc58dccb 100644 --- a/format/htmlparser.go +++ b/format/htmlparser.go @@ -1,4 +1,9 @@ -// Copyright 2018 Tulir Asokan +// 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 format import ( diff --git a/format/markdown.go b/format/markdown.go index e9592319..17e79c74 100644 --- a/format/markdown.go +++ b/format/markdown.go @@ -1,4 +1,9 @@ -// Copyright 2018 Tulir Asokan +// 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 format import ( @@ -8,7 +13,7 @@ import ( "github.com/russross/blackfriday/v2" - "maunium.net/go/mautrix" + "maunium.net/go/mautrix/events" ) type EscapingRenderer struct { @@ -36,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) mautrix.Content { +func RenderMarkdown(text string, allowMarkdown, allowHTML bool) events.Content { htmlBody := text if allowMarkdown { @@ -53,17 +58,17 @@ func RenderMarkdown(text string, allowMarkdown, allowHTML bool) mautrix.Content text = HTMLToText(htmlBody) if htmlBody != text { - return mautrix.Content{ + return events.Content{ FormattedBody: htmlBody, - Format: mautrix.FormatHTML, - MsgType: mautrix.MsgText, + Format: events.FormatHTML, + MsgType: events.MsgText, Body: text, } } } - return mautrix.Content{ - MsgType: mautrix.MsgText, + return events.Content{ + MsgType: events.MsgText, Body: text, } } diff --git a/contenturi.go b/id/contenturi.go similarity index 66% rename from contenturi.go rename to id/contenturi.go index f863b731..eb56a41e 100644 --- a/contenturi.go +++ b/id/contenturi.go @@ -1,4 +1,10 @@ -package mautrix +// 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 id import ( "errors" @@ -8,6 +14,12 @@ import ( var InvalidContentURI = errors.New("invalid Matrix content URI") +type ContentURIString string + +func (uriString ContentURIString) Parse() (ContentURI, error) { + return ParseContentURI(string(uriString)) +} + type ContentURI struct { Homeserver string FileID string @@ -42,6 +54,10 @@ func (uri *ContentURI) String() string { return fmt.Sprintf("mxc://%s/%s", uri.Homeserver, uri.FileID) } +func (uri *ContentURI) CUString() ContentURIString { + return ContentURIString(uri.String()) +} + func (uri *ContentURI) IsEmpty() bool { return len(uri.Homeserver) == 0 || len(uri.FileID) == 0 } \ No newline at end of file diff --git a/id/opaque.go b/id/opaque.go new file mode 100644 index 00000000..acfe9765 --- /dev/null +++ b/id/opaque.go @@ -0,0 +1,18 @@ +// 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 id + +// A RoomID is a string starting with ! that references a specific room. +type RoomID string +// A RoomAlias is a string starting with # that can be resolved into +type RoomAlias string +// An EventID is a string starting with $ that references a specific event. +type EventID string +// A DeviceID is an arbitrary string that references a specific device. +type DeviceID string +// A KeyID is a string usually formatted as : that is used as the key in deviceid-key mappings. +type KeyID string \ No newline at end of file diff --git a/userids.go b/id/userid.go similarity index 80% rename from userids.go rename to id/userid.go index cf02db5e..0fb574fb 100644 --- a/userids.go +++ b/id/userid.go @@ -1,4 +1,10 @@ -package mautrix +// 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 id import ( "bytes" @@ -7,6 +13,40 @@ import ( "strings" ) +type UserID string + +func NewUserID(localpart, homeserver string) UserID { + return UserID(fmt.Sprintf("@%s:%s", localpart, homeserver)) +} + +func NewEncodedUserID(localpart, homeserver string) UserID { + return NewUserID(EncodeUserLocalpart(localpart), homeserver) +} + +// Parse parses the user ID into the localpart and server name. +// See http://matrix.org/docs/spec/intro.html#user-identifiers +func (userID UserID) Parse() (localpart, homeserver string, err error) { + if len(userID) == 0 || userID[0] != '@' || !strings.ContainsRune(string(userID), ':') { + err = fmt.Errorf("%s is not a valid user id", userID) + return + } + parts := strings.SplitN(string(userID), ":", 2) + localpart, homeserver = strings.TrimPrefix(parts[0], "@"), parts[1] + return +} + +func (userID UserID) ParseAndDecode() (localpart, homeserver string, err error) { + localpart, homeserver, err = userID.Parse() + if err == nil { + localpart, err = DecodeUserLocalpart(localpart) + } + return +} + +func (userID UserID) String() string { + return string(userID) +} + const lowerhex = "0123456789abcdef" // encode the given byte using quoted-printable encoding (e.g "=2f") @@ -116,15 +156,3 @@ func DecodeUserLocalpart(str string) (string, error) { } return outputBuffer.String(), nil } - -// ParseUserID parses a user ID into the localpart and server name. -// See http://matrix.org/docs/spec/intro.html#user-identifiers -func ParseUserID(userID string) (localpart, homeserver string, err error) { - if len(userID) == 0 || userID[0] != '@' || !strings.ContainsRune(userID, ':') { - err = fmt.Errorf("%s is not a valid user id", userID) - return - } - parts := strings.SplitN(userID, ":", 2) - localpart, homeserver = strings.TrimPrefix(parts[0], "@"), parts[1] - return -} \ No newline at end of file diff --git a/requests.go b/requests.go index 81102be2..72b9127f 100644 --- a/requests.go +++ b/requests.go @@ -1,11 +1,16 @@ package mautrix +import ( + "maunium.net/go/mautrix/events" + "maunium.net/go/mautrix/id" +) + // ReqRegister is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register type ReqRegister struct { Username string `json:"username,omitempty"` BindEmail bool `json:"bind_email,omitempty"` Password string `json:"password,omitempty"` - DeviceID string `json:"device_id,omitempty"` + DeviceID id.DeviceID `json:"device_id,omitempty"` InitialDeviceDisplayName string `json:"initial_device_display_name"` Auth interface{} `json:"auth,omitempty"` } @@ -28,7 +33,7 @@ type ReqLogin struct { Identifier UserIdentifier `json:"identifier"` Password string `json:"password,omitempty"` Token string `json:"token,omitempty"` - DeviceID string `json:"device_id,omitempty"` + DeviceID id.DeviceID `json:"device_id,omitempty"` InitialDeviceDisplayName string `json:"initial_device_display_name,omitempty"` } @@ -38,10 +43,10 @@ type ReqCreateRoom struct { RoomAliasName string `json:"room_alias_name,omitempty"` Name string `json:"name,omitempty"` Topic string `json:"topic,omitempty"` - Invite []string `json:"invite,omitempty"` + Invite []id.UserID `json:"invite,omitempty"` Invite3PID []ReqInvite3PID `json:"invite_3pid,omitempty"` CreationContent map[string]interface{} `json:"creation_content,omitempty"` - InitialState []*Event `json:"initial_state,omitempty"` + InitialState []*events.Event `json:"initial_state,omitempty"` Preset string `json:"preset,omitempty"` IsDirect bool `json:"is_direct,omitempty"` } @@ -53,9 +58,9 @@ type ReqRedact struct { } type ReqMembers struct { - At string `json:"at"` - Membership Membership `json:"membership"` - NotMembership Membership `json:"not_membership"` + At string `json:"at"` + Membership events.Membership `json:"membership"` + NotMembership events.Membership `json:"not_membership"` } // ReqInvite3PID is the JSON request for https://matrix.org/docs/spec/client_server/r0.2.0.html#id57 @@ -68,24 +73,24 @@ type ReqInvite3PID struct { // ReqInviteUser is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-invite type ReqInviteUser struct { - UserID string `json:"user_id"` + UserID id.UserID `json:"user_id"` } // ReqKickUser is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-kick type ReqKickUser struct { - Reason string `json:"reason,omitempty"` - UserID string `json:"user_id"` + Reason string `json:"reason,omitempty"` + UserID id.UserID `json:"user_id"` } // ReqBanUser is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-ban type ReqBanUser struct { - Reason string `json:"reason,omitempty"` - UserID string `json:"user_id"` + Reason string `json:"reason,omitempty"` + UserID id.UserID `json:"user_id"` } // ReqUnbanUser is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-unban type ReqUnbanUser struct { - UserID string `json:"user_id"` + UserID id.UserID `json:"user_id"` } // ReqTyping is the JSON request for https://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-typing-userid @@ -99,5 +104,18 @@ type ReqPresence struct { } type ReqAliasCreate struct { - RoomID string `json:"room_id"` + RoomID id.RoomID `json:"room_id"` +} + +type ReqUploadKeys struct { + DeviceKeys DeviceKeys `json:"device_keys,omitempty"` + OneTimeKeys map[id.KeyID]string `json:"one_time_keys"` +} + +type DeviceKeys struct { + UserID id.UserID `json:"user_id"` + DeviceID id.DeviceID `json:"device_id"` + Algorithms []string `json:"algorithms"` + Keys map[id.KeyID]string `json:"keys"` + Signatures map[id.UserID]map[string]string `json:"signatures"` } diff --git a/responses.go b/responses.go index 910db5e3..10ce09ae 100644 --- a/responses.go +++ b/responses.go @@ -2,6 +2,9 @@ package mautrix import ( "encoding/json" + + "maunium.net/go/mautrix/events" + "maunium.net/go/mautrix/id" ) // RespError is the standard JSON error response from Homeservers. It also implements the Golang "error" interface. @@ -54,7 +57,7 @@ type RespTyping struct{} // RespJoinedRooms is the JSON response for https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-joined-rooms type RespJoinedRooms struct { - JoinedRooms []string `json:"joined_rooms"` + JoinedRooms []id.RoomID `json:"joined_rooms"` } // RespJoinedMembers is the JSON response for https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-joined-rooms @@ -67,20 +70,20 @@ type RespJoinedMembers struct { // RespMessages is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-messages type RespMessages struct { - Start string `json:"start"` - Chunk []*Event `json:"chunk"` - State []*Event `json:"state"` - End string `json:"end"` + Start string `json:"start"` + Chunk []*events.Event `json:"chunk"` + State []*events.Event `json:"state"` + End string `json:"end"` } // RespSendEvent is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid type RespSendEvent struct { - EventID string `json:"event_id"` + EventID id.EventID `json:"event_id"` } // RespMediaUpload is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-media-r0-upload type RespMediaUpload struct { - ContentURI string `json:"content_uri"` + ContentURI id.ContentURI `json:"content_uri"` } // RespUserInteractive is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#user-interactive-authentication-api @@ -112,11 +115,11 @@ type RespUserDisplayName struct { // RespRegister is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register type RespRegister struct { - AccessToken string `json:"access_token"` - DeviceID string `json:"device_id"` - HomeServer string `json:"home_server"` - RefreshToken string `json:"refresh_token"` - UserID string `json:"user_id"` + AccessToken string `json:"access_token"` + DeviceID id.DeviceID `json:"device_id"` + HomeServer string `json:"home_server"` + RefreshToken string `json:"refresh_token"` + UserID id.UserID `json:"user_id"` } type RespLoginFlows struct { @@ -127,10 +130,10 @@ type RespLoginFlows struct { // RespLogin is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-login type RespLogin struct { - AccessToken string `json:"access_token"` - DeviceID string `json:"device_id"` - HomeServer string `json:"home_server"` - UserID string `json:"user_id"` + AccessToken string `json:"access_token"` + DeviceID id.DeviceID `json:"device_id"` + HomeServer string `json:"home_server"` + UserID id.UserID `json:"user_id"` } // RespLogout is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-logout @@ -138,30 +141,41 @@ type RespLogout struct{} // RespCreateRoom is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom type RespCreateRoom struct { - RoomID string `json:"room_id"` + RoomID id.RoomID `json:"room_id"` } type RespMembers struct { - Chunk []*Event `json:"chunk"` + Chunk []*events.Event `json:"chunk"` } type LazyLoadSummary struct { - Heroes []string `json:"m.heroes,omitempty"` - JoinedMemberCount *int `json:"m.joined_member_count,omitempty"` - InvitedMemberCount *int `json:"m.invited_member_count,omitempty"` + Heroes []id.UserID `json:"m.heroes,omitempty"` + JoinedMemberCount *int `json:"m.joined_member_count,omitempty"` + InvitedMemberCount *int `json:"m.invited_member_count,omitempty"` } // RespSync is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-sync type RespSync struct { - NextBatch string `json:"next_batch"` + NextBatch string `json:"next_batch"` + AccountData struct { Events []json.RawMessage `json:"events"` } `json:"account_data"` Presence struct { Events []json.RawMessage `json:"events"` } `json:"presence"` + ToDevice struct { + Events []json.RawMessage `json:"events"` + } `json:"to_device"` + + DeviceLists struct { + Changed []id.UserID `json:"changed"` + Left []id.UserID `json:"left"` + } `json:"device_lists"` + DeviceOneTimeKeysCount map[string]int `json:"device_one_time_keys_count"` + Rooms struct { - Leave map[string]struct { + Leave map[id.RoomID]struct { Summary LazyLoadSummary `json:"summary"` State struct { Events []json.RawMessage `json:"events"` @@ -172,7 +186,7 @@ type RespSync struct { PrevBatch string `json:"prev_batch"` } `json:"timeline"` } `json:"leave"` - Join map[string]struct { + Join map[id.RoomID]struct { Summary LazyLoadSummary `json:"summary"` State struct { Events []json.RawMessage `json:"events"` @@ -189,7 +203,7 @@ type RespSync struct { Events []json.RawMessage `json:"events"` } `json:"account_data"` } `json:"join"` - Invite map[string]struct { + Invite map[id.RoomID]struct { Summary LazyLoadSummary `json:"summary"` State struct { Events []json.RawMessage `json:"events"` @@ -208,6 +222,9 @@ type RespTurnServer struct { type RespAliasCreate struct{} type RespAliasDelete struct{} type RespAliasResolve struct { - RoomID string `json:"room_id"` - Servers []string `json:"servers"` + RoomID id.RoomID `json:"room_id"` + Servers []string `json:"servers"` +} + +type RespUploadKeys struct { } diff --git a/room.go b/room.go index 086e2592..1206bdc3 100644 --- a/room.go +++ b/room.go @@ -1,23 +1,28 @@ package mautrix +import ( + "maunium.net/go/mautrix/events" + "maunium.net/go/mautrix/id" +) + // Room represents a single Matrix room. type Room struct { - ID string - State map[EventType]map[string]*Event + ID id.RoomID + State map[events.Type]map[string]*events.Event } // UpdateState updates the room's current state with the given Event. This will clobber events based // on the type/state_key combination. -func (room Room) UpdateState(event *Event) { +func (room Room) UpdateState(event *events.Event) { _, exists := room.State[event.Type] if !exists { - room.State[event.Type] = make(map[string]*Event) + room.State[event.Type] = make(map[string]*events.Event) } room.State[event.Type][*event.StateKey] = event } // GetStateEvent returns the state event for the given type/state_key combo, or nil. -func (room Room) GetStateEvent(eventType EventType, stateKey string) *Event { +func (room Room) GetStateEvent(eventType events.Type, stateKey string) *events.Event { stateEventMap, _ := room.State[eventType] event, _ := stateEventMap[stateKey] return event @@ -25,9 +30,9 @@ func (room Room) GetStateEvent(eventType EventType, stateKey string) *Event { // GetMembershipState returns the membership state of the given user ID in this room. If there is // no entry for this member, 'leave' is returned for consistency with left users. -func (room Room) GetMembershipState(userID string) Membership { - state := MembershipLeave - event := room.GetStateEvent(StateMember, userID) +func (room Room) GetMembershipState(userID id.UserID) events.Membership { + state := events.MembershipLeave + event := room.GetStateEvent(events.StateMember, string(userID)) if event != nil { state = event.Content.Membership } @@ -35,10 +40,10 @@ func (room Room) GetMembershipState(userID string) Membership { } // NewRoom creates a new Room with the given ID -func NewRoom(roomID string) *Room { +func NewRoom(roomID id.RoomID) *Room { // Init the State map and return a pointer to the Room return &Room{ ID: roomID, - State: make(map[EventType]map[string]*Event), + State: make(map[events.Type]map[string]*events.Event), } } diff --git a/store.go b/store.go index 774398e2..975fbde4 100644 --- a/store.go +++ b/store.go @@ -1,17 +1,21 @@ package mautrix +import ( + "maunium.net/go/mautrix/id" +) + // Storer is an interface which must be satisfied to store client data. // // You can either write a struct which persists this data to disk, or you can use the // provided "InMemoryStore" which just keeps data around in-memory which is lost on // restarts. type Storer interface { - SaveFilterID(userID, filterID string) - LoadFilterID(userID string) string - SaveNextBatch(userID, nextBatchToken string) - LoadNextBatch(userID string) string + SaveFilterID(userID id.UserID, filterID string) + LoadFilterID(userID id.UserID) string + SaveNextBatch(userID id.UserID, nextBatchToken string) + LoadNextBatch(userID id.UserID) string SaveRoom(room *Room) - LoadRoom(roomID string) *Room + LoadRoom(roomID id.RoomID) *Room } // InMemoryStore implements the Storer interface. @@ -20,28 +24,28 @@ type Storer interface { // or next batch tokens on any goroutine other than the syncing goroutine: the one // which called Client.Sync(). type InMemoryStore struct { - Filters map[string]string - NextBatch map[string]string - Rooms map[string]*Room + Filters map[id.UserID]string + NextBatch map[id.UserID]string + Rooms map[id.RoomID]*Room } // SaveFilterID to memory. -func (s *InMemoryStore) SaveFilterID(userID, filterID string) { +func (s *InMemoryStore) SaveFilterID(userID id.UserID, filterID string) { s.Filters[userID] = filterID } // LoadFilterID from memory. -func (s *InMemoryStore) LoadFilterID(userID string) string { +func (s *InMemoryStore) LoadFilterID(userID id.UserID) string { return s.Filters[userID] } // SaveNextBatch to memory. -func (s *InMemoryStore) SaveNextBatch(userID, nextBatchToken string) { +func (s *InMemoryStore) SaveNextBatch(userID id.UserID, nextBatchToken string) { s.NextBatch[userID] = nextBatchToken } // LoadNextBatch from memory. -func (s *InMemoryStore) LoadNextBatch(userID string) string { +func (s *InMemoryStore) LoadNextBatch(userID id.UserID) string { return s.NextBatch[userID] } @@ -51,15 +55,15 @@ func (s *InMemoryStore) SaveRoom(room *Room) { } // LoadRoom from memory. -func (s *InMemoryStore) LoadRoom(roomID string) *Room { +func (s *InMemoryStore) LoadRoom(roomID id.RoomID) *Room { return s.Rooms[roomID] } // NewInMemoryStore constructs a new InMemoryStore. func NewInMemoryStore() *InMemoryStore { return &InMemoryStore{ - Filters: make(map[string]string), - NextBatch: make(map[string]string), - Rooms: make(map[string]*Room), + Filters: make(map[id.UserID]string), + NextBatch: make(map[id.UserID]string), + Rooms: make(map[id.RoomID]*Room), } } diff --git a/sync.go b/sync.go index c543abe5..db152278 100644 --- a/sync.go +++ b/sync.go @@ -6,6 +6,9 @@ import ( "os" "runtime/debug" "time" + + "maunium.net/go/mautrix/events" + "maunium.net/go/mautrix/id" ) // Syncer represents an interface that must be satisfied in order to do /sync requests on a client. @@ -17,32 +20,32 @@ 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 string) json.RawMessage + GetFilterJSON(userID id.UserID) json.RawMessage } // DefaultSyncer is the default syncing implementation. You can either write your own syncer, or selectively // replace parts of this default syncer (e.g. the ProcessResponse method). The default syncer uses the observer // pattern to notify callers about incoming events. See DefaultSyncer.OnEventType for more information. type DefaultSyncer struct { - UserID string + UserID id.UserID Store Storer - listeners map[EventType][]OnEventListener // event type to listeners array + listeners map[events.Type][]OnEventListener // event type to listeners array } // OnEventListener can be used with DefaultSyncer.OnEventType to be informed of incoming events. -type OnEventListener func(*Event) +type OnEventListener func(*events.Event) // NewDefaultSyncer returns an instantiated DefaultSyncer -func NewDefaultSyncer(userID string, store Storer) *DefaultSyncer { +func NewDefaultSyncer(userID id.UserID, store Storer) *DefaultSyncer { return &DefaultSyncer{ UserID: userID, Store: store, - listeners: make(map[EventType][]OnEventListener), + listeners: make(map[events.Type][]OnEventListener), } } -func parseEvent(roomID string, data json.RawMessage) *Event { - event := &Event{} +func parseEvent(roomID id.RoomID, data json.RawMessage) *events.Event { + event := &events.Event{} err := json.Unmarshal(data, event) if err != nil { // TODO add separate handler for these @@ -106,7 +109,7 @@ func (s *DefaultSyncer) ProcessResponse(res *RespSync, since string) (err error) // OnEventType allows callers to be notified when there are new events for the given event type. // There are no duplicate checks. -func (s *DefaultSyncer) OnEventType(eventType EventType, callback OnEventListener) { +func (s *DefaultSyncer) OnEventType(eventType events.Type, callback OnEventListener) { _, exists := s.listeners[eventType] if !exists { s.listeners[eventType] = []OnEventListener{} @@ -132,7 +135,7 @@ func (s *DefaultSyncer) shouldProcessResponse(resp *RespSync, since string) bool 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 == StateMember && e.GetStateKey() == s.UserID { + if e != nil && e.Type == events.StateMember && e.GetStateKey() == string(s.UserID) { if e.Content.Membership == "join" { _, ok := resp.Rooms.Join[roomID] if !ok { @@ -149,7 +152,7 @@ func (s *DefaultSyncer) shouldProcessResponse(resp *RespSync, since string) bool } // getOrCreateRoom must only be called by the Sync() goroutine which calls ProcessResponse() -func (s *DefaultSyncer) getOrCreateRoom(roomID string) *Room { +func (s *DefaultSyncer) getOrCreateRoom(roomID id.RoomID) *Room { room := s.Store.LoadRoom(roomID) if room == nil { // create a new Room room = NewRoom(roomID) @@ -158,7 +161,7 @@ func (s *DefaultSyncer) getOrCreateRoom(roomID string) *Room { return room } -func (s *DefaultSyncer) notifyListeners(event *Event) { +func (s *DefaultSyncer) notifyListeners(event *events.Event) { listeners, exists := s.listeners[event.Type] if !exists { return @@ -174,6 +177,6 @@ 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 string) json.RawMessage { +func (s *DefaultSyncer) GetFilterJSON(userID id.UserID) json.RawMessage { return json.RawMessage(`{"room":{"timeline":{"limit":50}}}`) } diff --git a/userids_test.go b/userids_test.go deleted file mode 100644 index c3eb5da9..00000000 --- a/userids_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package mautrix - -import ( - "testing" -) - -var useridtests = []struct { - Input string - Output string -}{ - {"Alph@Bet_50up", "_alph=40_bet__50up"}, // The doc example - {"abcdef", "abcdef"}, // no-op - {"i_like_pie_", "i__like__pie__"}, // double underscore escaping - {"ABCDEF", "_a_b_c_d_e_f"}, // all-caps - {"!£", "=21=c2=a3"}, // punctuation and outside ascii range (U+00A3 => c2 a3) - {"___", "______"}, // literal underscores - {"hello-world.", "hello-world."}, // allowed punctuation - {"5+5=10", "5=2b5=3d10"}, // equals sign - {"東方Project", "=e6=9d=b1=e6=96=b9_project"}, // CJK mixed - {" foo bar", "=09foo=20bar"}, // whitespace (tab and space) -} - -func TestEncodeUserLocalpart(t *testing.T) { - for _, u := range useridtests { - out := EncodeUserLocalpart(u.Input) - if out != u.Output { - t.Fatalf("TestEncodeUserLocalpart(%s) => Got: %s Expected: %s", u.Input, out, u.Output) - } - } -} - -func TestDecodeUserLocalpart(t *testing.T) { - for _, u := range useridtests { - in, _ := DecodeUserLocalpart(u.Output) - if in != u.Input { - t.Fatalf("TestDecodeUserLocalpart(%s) => Got: %s Expected: %s", u.Output, in, u.Input) - } - } -} - -var errtests = []struct { - Input string -}{ - {"foo@bar"}, // invalid character @ - {"foo_5bar"}, // invalid character after _ - {"foo_._-bar"}, // multiple invalid characters after _ - {"foo=2Hbar"}, // invalid hex after = - {"foo=2hbar"}, // invalid hex after = (lower-case) - {"foo=======2fbar"}, // multiple invalid hex after = - {"foo=2"}, // end of string after = - {"foo_"}, // end of string after _ -} - -func TestDecodeUserLocalpartErrors(t *testing.T) { - for _, u := range errtests { - out, err := DecodeUserLocalpart(u.Input) - if out != "" { - t.Fatalf("TestDecodeUserLocalpartErrors(%s) => Got: %s Expected: empty string", u.Input, out) - } - if err == nil { - t.Fatalf("TestDecodeUserLocalpartErrors(%s) => Got: nil error Expected: error", u.Input) - } - } -} - -var localparttests = []struct { - Input string - ExpectOutput string -}{ - {"@foo:bar", "foo"}, - {"@foo:bar:8448", "foo"}, - {"@foo.bar:baz.quuz", "foo.bar"}, -} - -func TestExtractUserLocalpart(t *testing.T) { - for _, u := range localparttests { - out, err := ExtractUserLocalpart(u.Input) - if err != nil { - t.Errorf("TestExtractUserLocalpart(%s) => Error: %s", u.Input, err) - continue - } - if out != u.ExpectOutput { - t.Errorf("TestExtractUserLocalpart(%s) => Got: %s, Want %s", u.Input, out, u.ExpectOutput) - } - } -}