parent
b4c7d4d479
commit
4fcfbd3cfc
@ -0,0 +1,201 @@ |
||||
Apache License |
||||
Version 2.0, January 2004 |
||||
http://www.apache.org/licenses/ |
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION |
||||
|
||||
1. Definitions. |
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, |
||||
and distribution as defined by Sections 1 through 9 of this document. |
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by |
||||
the copyright owner that is granting the License. |
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all |
||||
other entities that control, are controlled by, or are under common |
||||
control with that entity. For the purposes of this definition, |
||||
"control" means (i) the power, direct or indirect, to cause the |
||||
direction or management of such entity, whether by contract or |
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the |
||||
outstanding shares, or (iii) beneficial ownership of such entity. |
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity |
||||
exercising permissions granted by this License. |
||||
|
||||
"Source" form shall mean the preferred form for making modifications, |
||||
including but not limited to software source code, documentation |
||||
source, and configuration files. |
||||
|
||||
"Object" form shall mean any form resulting from mechanical |
||||
transformation or translation of a Source form, including but |
||||
not limited to compiled object code, generated documentation, |
||||
and conversions to other media types. |
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or |
||||
Object form, made available under the License, as indicated by a |
||||
copyright notice that is included in or attached to the work |
||||
(an example is provided in the Appendix below). |
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object |
||||
form, that is based on (or derived from) the Work and for which the |
||||
editorial revisions, annotations, elaborations, or other modifications |
||||
represent, as a whole, an original work of authorship. For the purposes |
||||
of this License, Derivative Works shall not include works that remain |
||||
separable from, or merely link (or bind by name) to the interfaces of, |
||||
the Work and Derivative Works thereof. |
||||
|
||||
"Contribution" shall mean any work of authorship, including |
||||
the original version of the Work and any modifications or additions |
||||
to that Work or Derivative Works thereof, that is intentionally |
||||
submitted to Licensor for inclusion in the Work by the copyright owner |
||||
or by an individual or Legal Entity authorized to submit on behalf of |
||||
the copyright owner. For the purposes of this definition, "submitted" |
||||
means any form of electronic, verbal, or written communication sent |
||||
to the Licensor or its representatives, including but not limited to |
||||
communication on electronic mailing lists, source code control systems, |
||||
and issue tracking systems that are managed by, or on behalf of, the |
||||
Licensor for the purpose of discussing and improving the Work, but |
||||
excluding communication that is conspicuously marked or otherwise |
||||
designated in writing by the copyright owner as "Not a Contribution." |
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity |
||||
on behalf of whom a Contribution has been received by Licensor and |
||||
subsequently incorporated within the Work. |
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of |
||||
this License, each Contributor hereby grants to You a perpetual, |
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||
copyright license to reproduce, prepare Derivative Works of, |
||||
publicly display, publicly perform, sublicense, and distribute the |
||||
Work and such Derivative Works in Source or Object form. |
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of |
||||
this License, each Contributor hereby grants to You a perpetual, |
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||
(except as stated in this section) patent license to make, have made, |
||||
use, offer to sell, sell, import, and otherwise transfer the Work, |
||||
where such license applies only to those patent claims licensable |
||||
by such Contributor that are necessarily infringed by their |
||||
Contribution(s) alone or by combination of their Contribution(s) |
||||
with the Work to which such Contribution(s) was submitted. If You |
||||
institute patent litigation against any entity (including a |
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work |
||||
or a Contribution incorporated within the Work constitutes direct |
||||
or contributory patent infringement, then any patent licenses |
||||
granted to You under this License for that Work shall terminate |
||||
as of the date such litigation is filed. |
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the |
||||
Work or Derivative Works thereof in any medium, with or without |
||||
modifications, and in Source or Object form, provided that You |
||||
meet the following conditions: |
||||
|
||||
(a) You must give any other recipients of the Work or |
||||
Derivative Works a copy of this License; and |
||||
|
||||
(b) You must cause any modified files to carry prominent notices |
||||
stating that You changed the files; and |
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works |
||||
that You distribute, all copyright, patent, trademark, and |
||||
attribution notices from the Source form of the Work, |
||||
excluding those notices that do not pertain to any part of |
||||
the Derivative Works; and |
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its |
||||
distribution, then any Derivative Works that You distribute must |
||||
include a readable copy of the attribution notices contained |
||||
within such NOTICE file, excluding those notices that do not |
||||
pertain to any part of the Derivative Works, in at least one |
||||
of the following places: within a NOTICE text file distributed |
||||
as part of the Derivative Works; within the Source form or |
||||
documentation, if provided along with the Derivative Works; or, |
||||
within a display generated by the Derivative Works, if and |
||||
wherever such third-party notices normally appear. The contents |
||||
of the NOTICE file are for informational purposes only and |
||||
do not modify the License. You may add Your own attribution |
||||
notices within Derivative Works that You distribute, alongside |
||||
or as an addendum to the NOTICE text from the Work, provided |
||||
that such additional attribution notices cannot be construed |
||||
as modifying the License. |
||||
|
||||
You may add Your own copyright statement to Your modifications and |
||||
may provide additional or different license terms and conditions |
||||
for use, reproduction, or distribution of Your modifications, or |
||||
for any such Derivative Works as a whole, provided Your use, |
||||
reproduction, and distribution of the Work otherwise complies with |
||||
the conditions stated in this License. |
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, |
||||
any Contribution intentionally submitted for inclusion in the Work |
||||
by You to the Licensor shall be under the terms and conditions of |
||||
this License, without any additional terms or conditions. |
||||
Notwithstanding the above, nothing herein shall supersede or modify |
||||
the terms of any separate license agreement you may have executed |
||||
with Licensor regarding such Contributions. |
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade |
||||
names, trademarks, service marks, or product names of the Licensor, |
||||
except as required for reasonable and customary use in describing the |
||||
origin of the Work and reproducing the content of the NOTICE file. |
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or |
||||
agreed to in writing, Licensor provides the Work (and each |
||||
Contributor provides its Contributions) on an "AS IS" BASIS, |
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
||||
implied, including, without limitation, any warranties or conditions |
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A |
||||
PARTICULAR PURPOSE. You are solely responsible for determining the |
||||
appropriateness of using or redistributing the Work and assume any |
||||
risks associated with Your exercise of permissions under this License. |
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, |
||||
whether in tort (including negligence), contract, or otherwise, |
||||
unless required by applicable law (such as deliberate and grossly |
||||
negligent acts) or agreed to in writing, shall any Contributor be |
||||
liable to You for damages, including any direct, indirect, special, |
||||
incidental, or consequential damages of any character arising as a |
||||
result of this License or out of the use or inability to use the |
||||
Work (including but not limited to damages for loss of goodwill, |
||||
work stoppage, computer failure or malfunction, or any and all |
||||
other commercial damages or losses), even if such Contributor |
||||
has been advised of the possibility of such damages. |
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing |
||||
the Work or Derivative Works thereof, You may choose to offer, |
||||
and charge a fee for, acceptance of support, warranty, indemnity, |
||||
or other liability obligations and/or rights consistent with this |
||||
License. However, in accepting such obligations, You may act only |
||||
on Your own behalf and on Your sole responsibility, not on behalf |
||||
of any other Contributor, and only if You agree to indemnify, |
||||
defend, and hold each Contributor harmless for any liability |
||||
incurred by, or claims asserted against, such Contributor by reason |
||||
of your accepting any such warranty or additional liability. |
||||
|
||||
END OF TERMS AND CONDITIONS |
||||
|
||||
APPENDIX: How to apply the Apache License to your work. |
||||
|
||||
To apply the Apache License to your work, attach the following |
||||
boilerplate notice, with the fields enclosed by brackets "{}" |
||||
replaced with your own identifying information. (Don't include |
||||
the brackets!) The text should be enclosed in the appropriate |
||||
comment syntax for the file format. We also recommend that a |
||||
file or class name and description of purpose be included on the |
||||
same "printed page" as the copyright notice for easier |
||||
identification within third-party archives. |
||||
|
||||
Copyright {yyyy} {name of copyright owner} |
||||
|
||||
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. |
@ -0,0 +1,703 @@ |
||||
// Package gomatrix implements the Matrix Client-Server API.
|
||||
//
|
||||
// Specification can be found at http://matrix.org/docs/spec/client_server/r0.2.0.html
|
||||
package gomatrix |
||||
|
||||
import ( |
||||
"bytes" |
||||
"encoding/json" |
||||
"fmt" |
||||
"io" |
||||
"io/ioutil" |
||||
"net/http" |
||||
"net/url" |
||||
"path" |
||||
"strconv" |
||||
"sync" |
||||
"time" |
||||
) |
||||
|
||||
// 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.
|
||||
Client *http.Client // The underlying HTTP client which will be used to make HTTP requests.
|
||||
Syncer Syncer // The thing which can process /sync responses
|
||||
Store Storer // The thing which can store rooms/tokens/ids
|
||||
|
||||
// The ?user_id= query parameter for application services. This must be set *prior* to calling a method. If this is empty,
|
||||
// no user_id parameter will be sent.
|
||||
// See http://matrix.org/docs/spec/application_service/unstable.html#identity-assertion
|
||||
AppServiceUserID string |
||||
|
||||
syncingMutex sync.Mutex // protects syncingID
|
||||
syncingID uint32 // Identifies the current Sync. Only one Sync can be active at any given time.
|
||||
} |
||||
|
||||
// HTTPError An HTTP Error response, which may wrap an underlying native Go Error.
|
||||
type HTTPError struct { |
||||
WrappedError error |
||||
Message string |
||||
Code int |
||||
} |
||||
|
||||
func (e HTTPError) Error() string { |
||||
var wrappedErrMsg string |
||||
if e.WrappedError != nil { |
||||
wrappedErrMsg = e.WrappedError.Error() |
||||
} |
||||
return fmt.Sprintf("msg=%s code=%d wrapped=%s", e.Message, e.Code, wrappedErrMsg) |
||||
} |
||||
|
||||
// BuildURL builds a URL with the Client's homserver/prefix/access_token set already.
|
||||
func (cli *Client) BuildURL(urlPath ...string) string { |
||||
ps := []string{cli.Prefix} |
||||
for _, p := range urlPath { |
||||
ps = append(ps, p) |
||||
} |
||||
return cli.BuildBaseURL(ps...) |
||||
} |
||||
|
||||
// BuildBaseURL builds a URL with the Client's homeserver/access_token set already. You must
|
||||
// supply the prefix in the path.
|
||||
func (cli *Client) BuildBaseURL(urlPath ...string) string { |
||||
// copy the URL. Purposefully ignore error as the input is from a valid URL already
|
||||
hsURL, _ := url.Parse(cli.HomeserverURL.String()) |
||||
parts := []string{hsURL.Path} |
||||
parts = append(parts, urlPath...) |
||||
hsURL.Path = path.Join(parts...) |
||||
query := hsURL.Query() |
||||
if cli.AccessToken != "" { |
||||
query.Set("access_token", cli.AccessToken) |
||||
} |
||||
if cli.AppServiceUserID != "" { |
||||
query.Set("user_id", cli.AppServiceUserID) |
||||
} |
||||
hsURL.RawQuery = query.Encode() |
||||
return hsURL.String() |
||||
} |
||||
|
||||
// BuildURLWithQuery builds a URL with query parameters in addition to the Client's homeserver/prefix/access_token set already.
|
||||
func (cli *Client) BuildURLWithQuery(urlPath []string, urlQuery map[string]string) string { |
||||
u, _ := url.Parse(cli.BuildURL(urlPath...)) |
||||
q := u.Query() |
||||
for k, v := range urlQuery { |
||||
q.Set(k, v) |
||||
} |
||||
u.RawQuery = q.Encode() |
||||
return u.String() |
||||
} |
||||
|
||||
// SetCredentials sets the user ID and access token on this client instance.
|
||||
func (cli *Client) SetCredentials(userID, accessToken string) { |
||||
cli.AccessToken = accessToken |
||||
cli.UserID = userID |
||||
} |
||||
|
||||
// ClearCredentials removes the user ID and access token on this client instance.
|
||||
func (cli *Client) ClearCredentials() { |
||||
cli.AccessToken = "" |
||||
cli.UserID = "" |
||||
} |
||||
|
||||
// Sync starts syncing with the provided Homeserver. If Sync() is called twice then the first sync will be stopped and the
|
||||
// error will be nil.
|
||||
//
|
||||
// This function will block until a fatal /sync error occurs, so it should almost always be started as a new goroutine.
|
||||
// Fatal sync errors can be caused by:
|
||||
// - The failure to create a filter.
|
||||
// - Client.Syncer.OnFailedSync returning an error in response to a failed sync.
|
||||
// - Client.Syncer.ProcessResponse returning an error.
|
||||
// If you wish to continue retrying in spite of these fatal errors, call Sync() again.
|
||||
func (cli *Client) Sync() error { |
||||
// Mark the client as syncing.
|
||||
// We will keep syncing until the syncing state changes. Either because
|
||||
// Sync is called or StopSync is called.
|
||||
syncingID := cli.incrementSyncingID() |
||||
nextBatch := cli.Store.LoadNextBatch(cli.UserID) |
||||
filterID := cli.Store.LoadFilterID(cli.UserID) |
||||
if filterID == "" { |
||||
filterJSON := cli.Syncer.GetFilterJSON(cli.UserID) |
||||
resFilter, err := cli.CreateFilter(filterJSON) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
filterID = resFilter.FilterID |
||||
cli.Store.SaveFilterID(cli.UserID, filterID) |
||||
} |
||||
|
||||
for { |
||||
resSync, err := cli.SyncRequest(30000, nextBatch, filterID, false, "") |
||||
if err != nil { |
||||
duration, err2 := cli.Syncer.OnFailedSync(resSync, err) |
||||
if err2 != nil { |
||||
return err2 |
||||
} |
||||
time.Sleep(duration) |
||||
continue |
||||
} |
||||
|
||||
// Check that the syncing state hasn't changed
|
||||
// Either because we've stopped syncing or another sync has been started.
|
||||
// We discard the response from our sync.
|
||||
if cli.getSyncingID() != syncingID { |
||||
return nil |
||||
} |
||||
|
||||
// Save the token now *before* processing it. This means it's possible
|
||||
// to not process some events, but it means that we won't get constantly stuck processing
|
||||
// a malformed/buggy event which keeps making us panic.
|
||||
cli.Store.SaveNextBatch(cli.UserID, resSync.NextBatch) |
||||
if err = cli.Syncer.ProcessResponse(resSync, nextBatch); err != nil { |
||||
return err |
||||
} |
||||
|
||||
nextBatch = resSync.NextBatch |
||||
} |
||||
} |
||||
|
||||
func (cli *Client) incrementSyncingID() uint32 { |
||||
cli.syncingMutex.Lock() |
||||
defer cli.syncingMutex.Unlock() |
||||
cli.syncingID++ |
||||
return cli.syncingID |
||||
} |
||||
|
||||
func (cli *Client) getSyncingID() uint32 { |
||||
cli.syncingMutex.Lock() |
||||
defer cli.syncingMutex.Unlock() |
||||
return cli.syncingID |
||||
} |
||||
|
||||
// StopSync stops the ongoing sync started by Sync.
|
||||
func (cli *Client) StopSync() { |
||||
// Advance the syncing state so that any running Syncs will terminate.
|
||||
cli.incrementSyncingID() |
||||
} |
||||
|
||||
// MakeRequest makes a JSON HTTP request to the given URL.
|
||||
// If "resBody" is not nil, the response body will be json.Unmarshalled into it.
|
||||
//
|
||||
// Returns the HTTP body as bytes on 2xx with a nil error. Returns an error if the response is not 2xx along
|
||||
// with the HTTP body bytes if it got that far. This error is an HTTPError which includes the returned
|
||||
// HTTP status code and possibly a RespError as the WrappedError, if the HTTP body could be decoded as a RespError.
|
||||
func (cli *Client) MakeRequest(method string, httpURL string, reqBody interface{}, resBody interface{}) ([]byte, error) { |
||||
var req *http.Request |
||||
var err error |
||||
if reqBody != nil { |
||||
var jsonStr []byte |
||||
jsonStr, err = json.Marshal(reqBody) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
req, err = http.NewRequest(method, httpURL, bytes.NewBuffer(jsonStr)) |
||||
} else { |
||||
req, err = http.NewRequest(method, httpURL, nil) |
||||
} |
||||
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
req.Header.Set("Content-Type", "application/json") |
||||
res, err := cli.Client.Do(req) |
||||
if res != nil { |
||||
defer res.Body.Close() |
||||
} |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
contents, err := ioutil.ReadAll(res.Body) |
||||
if res.StatusCode/100 != 2 { // not 2xx
|
||||
var wrap error |
||||
var respErr RespError |
||||
if _ = json.Unmarshal(contents, &respErr); respErr.ErrCode != "" { |
||||
wrap = respErr |
||||
} |
||||
|
||||
// If we failed to decode as RespError, don't just drop the HTTP body, include it in the
|
||||
// HTTP error instead (e.g proxy errors which return HTML).
|
||||
msg := "Failed to " + method + " JSON to " + req.URL.Path |
||||
if wrap == nil { |
||||
msg = msg + ": " + string(contents) |
||||
} |
||||
|
||||
return contents, HTTPError{ |
||||
Code: res.StatusCode, |
||||
Message: msg, |
||||
WrappedError: wrap, |
||||
} |
||||
} |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if resBody != nil { |
||||
if err = json.Unmarshal(contents, &resBody); err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
return contents, nil |
||||
} |
||||
|
||||
// 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") |
||||
_, err = cli.MakeRequest("POST", urlPath, &filter, &resp) |
||||
return |
||||
} |
||||
|
||||
// SyncRequest makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-sync
|
||||
func (cli *Client) SyncRequest(timeout int, since, filterID string, fullState bool, setPresence string) (resp *RespSync, err error) { |
||||
query := map[string]string{ |
||||
"timeout": strconv.Itoa(timeout), |
||||
} |
||||
if since != "" { |
||||
query["since"] = since |
||||
} |
||||
if filterID != "" { |
||||
query["filter"] = filterID |
||||
} |
||||
if setPresence != "" { |
||||
query["set_presence"] = setPresence |
||||
} |
||||
if fullState { |
||||
query["full_state"] = "true" |
||||
} |
||||
urlPath := cli.BuildURLWithQuery([]string{"sync"}, query) |
||||
_, err = cli.MakeRequest("GET", urlPath, nil, &resp) |
||||
return |
||||
} |
||||
|
||||
func (cli *Client) register(u string, req *ReqRegister) (resp *RespRegister, uiaResp *RespUserInteractive, err error) { |
||||
var bodyBytes []byte |
||||
bodyBytes, err = cli.MakeRequest("POST", u, req, nil) |
||||
if err != nil { |
||||
httpErr, ok := err.(HTTPError) |
||||
if !ok { // network error
|
||||
return |
||||
} |
||||
if httpErr.Code == 401 { |
||||
// body should be RespUserInteractive, if it isn't, fail with the error
|
||||
err = json.Unmarshal(bodyBytes, &uiaResp) |
||||
return |
||||
} |
||||
return |
||||
} |
||||
// body should be RespRegister
|
||||
err = json.Unmarshal(bodyBytes, &resp) |
||||
return |
||||
} |
||||
|
||||
// Register makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register
|
||||
//
|
||||
// Registers with kind=user. For kind=guest, see RegisterGuest.
|
||||
func (cli *Client) Register(req *ReqRegister) (*RespRegister, *RespUserInteractive, error) { |
||||
u := cli.BuildURL("register") |
||||
return cli.register(u, req) |
||||
} |
||||
|
||||
// RegisterGuest makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register
|
||||
// with kind=guest.
|
||||
//
|
||||
// For kind=user, see Register.
|
||||
func (cli *Client) RegisterGuest(req *ReqRegister) (*RespRegister, *RespUserInteractive, error) { |
||||
query := map[string]string{ |
||||
"kind": "guest", |
||||
} |
||||
u := cli.BuildURLWithQuery([]string{"register"}, query) |
||||
return cli.register(u, req) |
||||
} |
||||
|
||||
// RegisterDummy performs m.login.dummy registration according to https://matrix.org/docs/spec/client_server/r0.2.0.html#dummy-auth
|
||||
//
|
||||
// Only a username and password need to be provided on the ReqRegister struct. Most local/developer homeservers will allow registration
|
||||
// this way. If the homeserver does not, an error is returned.
|
||||
//
|
||||
// This does not set credentials on the client instance. See SetCredentials() instead.
|
||||
//
|
||||
// res, err := cli.RegisterDummy(&gomatrix.ReqRegister{
|
||||
// Username: "alice",
|
||||
// Password: "wonderland",
|
||||
// })
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
// token := res.AccessToken
|
||||
func (cli *Client) RegisterDummy(req *ReqRegister) (*RespRegister, error) { |
||||
res, uia, err := cli.Register(req) |
||||
if err != nil && uia == nil { |
||||
return nil, err |
||||
} |
||||
if uia != nil && uia.HasSingleStageFlow("m.login.dummy") { |
||||
req.Auth = struct { |
||||
Type string `json:"type"` |
||||
Session string `json:"session,omitempty"` |
||||
}{"m.login.dummy", uia.Session} |
||||
res, _, err = cli.Register(req) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
if res == nil { |
||||
return nil, fmt.Errorf("registration failed: does this server support m.login.dummy?") |
||||
} |
||||
return res, nil |
||||
} |
||||
|
||||
// Login a user to the homeserver according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-login
|
||||
// This does not set credentials on this client instance. See SetCredentials() instead.
|
||||
func (cli *Client) Login(req *ReqLogin) (resp *RespLogin, err error) { |
||||
urlPath := cli.BuildURL("login") |
||||
_, err = cli.MakeRequest("POST", urlPath, req, &resp) |
||||
return |
||||
} |
||||
|
||||
// Logout the current user. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-logout
|
||||
// This does not clear the credentials from the client instance. See ClearCredentials() instead.
|
||||
func (cli *Client) Logout() (resp *RespLogout, err error) { |
||||
urlPath := cli.BuildURL("logout") |
||||
_, err = cli.MakeRequest("POST", urlPath, nil, &resp) |
||||
return |
||||
} |
||||
|
||||
// Versions returns the list of supported Matrix versions on this homeserver. See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-versions
|
||||
func (cli *Client) Versions() (resp *RespVersions, err error) { |
||||
urlPath := cli.BuildBaseURL("_matrix", "client", "versions") |
||||
_, err = cli.MakeRequest("GET", urlPath, nil, &resp) |
||||
return |
||||
} |
||||
|
||||
// JoinRoom joins the client to a room ID or alias. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-join-roomidoralias
|
||||
//
|
||||
// If serverName is specified, this will be added as a query param to instruct the homeserver to join via that server. If content is specified, it will
|
||||
// be JSON encoded and used as the request body.
|
||||
func (cli *Client) JoinRoom(roomIDorAlias, serverName string, content interface{}) (resp *RespJoinRoom, err error) { |
||||
var urlPath string |
||||
if serverName != "" { |
||||
urlPath = cli.BuildURLWithQuery([]string{"join", roomIDorAlias}, map[string]string{ |
||||
"server_name": serverName, |
||||
}) |
||||
} else { |
||||
urlPath = cli.BuildURL("join", roomIDorAlias) |
||||
} |
||||
_, err = cli.MakeRequest("POST", urlPath, content, &resp) |
||||
return |
||||
} |
||||
|
||||
// GetDisplayName returns the display name of the user from the specified MXID. See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname
|
||||
func (cli *Client) GetDisplayName(mxid string) (resp *RespUserDisplayName, err error) { |
||||
urlPath := cli.BuildURL("profile", mxid, "displayname") |
||||
_, err = cli.MakeRequest("GET", urlPath, nil, &resp) |
||||
return |
||||
} |
||||
|
||||
// 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") |
||||
_, 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") |
||||
s := struct { |
||||
DisplayName string `json:"displayname"` |
||||
}{displayName} |
||||
_, err = cli.MakeRequest("PUT", urlPath, &s, nil) |
||||
return |
||||
} |
||||
|
||||
// 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") |
||||
s := struct { |
||||
AvatarURL string `json:"avatar_url"` |
||||
}{} |
||||
|
||||
_, err = cli.MakeRequest("GET", urlPath, nil, &s) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
return s.AvatarURL, nil |
||||
} |
||||
|
||||
// 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") |
||||
s := struct { |
||||
AvatarURL string `json:"avatar_url"` |
||||
}{url} |
||||
_, err = cli.MakeRequest("PUT", urlPath, &s, nil) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// 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 string, contentJSON interface{}) (resp *RespSendEvent, err error) { |
||||
txnID := txnID() |
||||
urlPath := cli.BuildURL("rooms", roomID, "send", eventType, txnID) |
||||
_, 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) SendStateEvent(roomID, eventType, stateKey string, contentJSON interface{}) (resp *RespSendEvent, err error) { |
||||
urlPath := cli.BuildURL("rooms", roomID, "state", eventType, stateKey) |
||||
_, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp) |
||||
return |
||||
} |
||||
|
||||
// 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, "m.room.message", |
||||
TextMessage{"m.text", 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, "m.room.message", |
||||
ImageMessage{ |
||||
MsgType: "m.image", |
||||
Body: body, |
||||
URL: url, |
||||
}) |
||||
} |
||||
|
||||
// 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, "m.room.message", |
||||
VideoMessage{ |
||||
MsgType: "m.video", |
||||
Body: body, |
||||
URL: url, |
||||
}) |
||||
} |
||||
|
||||
// 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, "m.room.message", |
||||
TextMessage{"m.notice", text}) |
||||
} |
||||
|
||||
// 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, req *ReqRedact) (resp *RespSendEvent, err error) { |
||||
txnID := txnID() |
||||
urlPath := cli.BuildURL("rooms", roomID, "redact", eventID, txnID) |
||||
_, err = cli.MakeRequest("PUT", urlPath, req, &resp) |
||||
return |
||||
} |
||||
|
||||
// CreateRoom creates a new Matrix room. See https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
|
||||
// resp, err := cli.CreateRoom(&gomatrix.ReqCreateRoom{
|
||||
// Preset: "public_chat",
|
||||
// })
|
||||
// fmt.Println("Room:", resp.RoomID)
|
||||
func (cli *Client) CreateRoom(req *ReqCreateRoom) (resp *RespCreateRoom, err error) { |
||||
urlPath := cli.BuildURL("createRoom") |
||||
_, err = cli.MakeRequest("POST", urlPath, req, &resp) |
||||
return |
||||
} |
||||
|
||||
// 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") |
||||
_, 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") |
||||
_, 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") |
||||
_, 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") |
||||
_, 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") |
||||
_, 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") |
||||
_, 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") |
||||
_, 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) { |
||||
req := ReqTyping{Typing: typing, Timeout: timeout} |
||||
u := cli.BuildURL("rooms", roomID, "typing", cli.UserID) |
||||
_, err = cli.MakeRequest("PUT", u, req, &resp) |
||||
return |
||||
} |
||||
|
||||
// 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, eventType, stateKey string, outContent interface{}) (err error) { |
||||
u := cli.BuildURL("rooms", roomID, "state", eventType, stateKey) |
||||
_, err = cli.MakeRequest("GET", u, nil, outContent) |
||||
return |
||||
} |
||||
|
||||
// UploadLink uploads an HTTP URL and then returns an MXC URI.
|
||||
func (cli *Client) UploadLink(link string) (*RespMediaUpload, error) { |
||||
res, err := cli.Client.Get(link) |
||||
if res != nil { |
||||
defer res.Body.Close() |
||||
} |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return cli.UploadToContentRepo(res.Body, res.Header.Get("Content-Type"), res.ContentLength) |
||||
} |
||||
|
||||
// UploadToContentRepo uploads the given bytes to the content repository and returns an MXC URI.
|
||||
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-media-r0-upload
|
||||
func (cli *Client) UploadToContentRepo(content io.Reader, contentType string, contentLength int64) (*RespMediaUpload, error) { |
||||
req, err := http.NewRequest("POST", cli.BuildBaseURL("_matrix/media/r0/upload"), content) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
req.Header.Set("Content-Type", contentType) |
||||
req.ContentLength = contentLength |
||||
res, err := cli.Client.Do(req) |
||||
if res != nil { |
||||
defer res.Body.Close() |
||||
} |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if res.StatusCode != 200 { |
||||
contents, err := ioutil.ReadAll(res.Body) |
||||
if err != nil { |
||||
return nil, HTTPError{ |
||||
Message: "Upload request failed - Failed to read response body: " + err.Error(), |
||||
Code: res.StatusCode, |
||||
} |
||||
} |
||||
return nil, HTTPError{ |
||||
Message: "Upload request failed: " + string(contents), |
||||
Code: res.StatusCode, |
||||
} |
||||
} |
||||
var m RespMediaUpload |
||||
if err := json.NewDecoder(res.Body).Decode(&m); err != nil { |
||||
return nil, err |
||||
} |
||||
return &m, nil |
||||
} |
||||
|
||||
// JoinedMembers returns a map of joined room members. See TODO-SPEC. https://github.com/matrix-org/synapse/pull/1680
|
||||
//
|
||||
// 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") |
||||
_, err = cli.MakeRequest("GET", u, nil, &resp) |
||||
return |
||||
} |
||||
|
||||
// JoinedRooms returns a list of rooms which the client is joined to. See TODO-SPEC. https://github.com/matrix-org/synapse/pull/1680
|
||||
//
|
||||
// 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 rooms.
|
||||
func (cli *Client) JoinedRooms() (resp *RespJoinedRooms, err error) { |
||||
u := cli.BuildURL("joined_rooms") |
||||
_, err = cli.MakeRequest("GET", u, nil, &resp) |
||||
return |
||||
} |
||||
|
||||
// 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) { |
||||
query := map[string]string{ |
||||
"from": from, |
||||
"dir": string(dir), |
||||
} |
||||
if to != "" { |
||||
query["to"] = to |
||||
} |
||||
if limit != 0 { |
||||
query["limit"] = strconv.Itoa(limit) |
||||
} |
||||
|
||||
urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "messages"}, query) |
||||
_, err = cli.MakeRequest("GET", urlPath, nil, &resp) |
||||
return |
||||
} |
||||
|
||||
// TurnServer returns turn server details and credentials for the client to use when initiating calls.
|
||||
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-voip-turnserver
|
||||
func (cli *Client) TurnServer() (resp *RespTurnServer, err error) { |
||||
urlPath := cli.BuildURL("voip", "turnServer") |
||||
_, err = cli.MakeRequest("GET", urlPath, nil, &resp) |
||||
return |
||||
} |
||||
|
||||
func txnID() string { |
||||
return "go" + strconv.FormatInt(time.Now().UnixNano(), 10) |
||||
} |
||||
|
||||
// NewClient creates a new Matrix Client ready for syncing
|
||||
func NewClient(homeserverURL, userID, accessToken string) (*Client, error) { |
||||
hsURL, err := url.Parse(homeserverURL) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
// By default, use an in-memory store which will never save filter ids / next batch tokens to disk.
|
||||
// The client will work with this storer: it just won't remember across restarts.
|
||||
// In practice, a database backend should be used.
|
||||
store := NewInMemoryStore() |
||||
cli := Client{ |
||||
AccessToken: accessToken, |
||||
HomeserverURL: hsURL, |
||||
UserID: userID, |
||||
Prefix: "/_matrix/client/r0", |
||||
Syncer: NewDefaultSyncer(userID, store), |
||||
Store: store, |
||||
} |
||||
// By default, use the default HTTP client.
|
||||
cli.Client = http.DefaultClient |
||||
|
||||
return &cli, nil |
||||
} |
@ -0,0 +1,102 @@ |
||||
package gomatrix |
||||
|
||||
import ( |
||||
"html" |
||||
"regexp" |
||||
) |
||||
|
||||
// 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 string `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 map[string]interface{} `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
|
||||
} |
||||
|
||||
// Body returns the value of the "body" key in the event content if it is
|
||||
// present and is a string.
|
||||
func (event *Event) Body() (body string, ok bool) { |
||||
value, exists := event.Content["body"] |
||||
if !exists { |
||||
return |
||||
} |
||||
body, ok = value.(string) |
||||
return |
||||
} |
||||
|
||||
// MessageType returns the value of the "msgtype" key in the event content if
|
||||
// it is present and is a string.
|
||||
func (event *Event) MessageType() (msgtype string, ok bool) { |
||||
value, exists := event.Content["msgtype"] |
||||
if !exists { |
||||
return |
||||
} |
||||
msgtype, ok = value.(string) |
||||
return |
||||
} |
||||
|
||||
// TextMessage is the contents of a Matrix formated message event.
|
||||
type TextMessage struct { |
||||
MsgType string `json:"msgtype"` |
||||
Body string `json:"body"` |
||||
} |
||||
|
||||
// ImageInfo contains info about an image - http://matrix.org/docs/spec/client_server/r0.2.0.html#m-image
|
||||
type ImageInfo struct { |
||||
Height uint `json:"h,omitempty"` |
||||
Width uint `json:"w,omitempty"` |
||||
Mimetype string `json:"mimetype,omitempty"` |
||||
Size uint `json:"size,omitempty"` |
||||
} |
||||
|
||||
// VideoInfo contains info about a video - http://matrix.org/docs/spec/client_server/r0.2.0.html#m-video
|
||||
type VideoInfo struct { |
||||
Mimetype string `json:"mimetype,omitempty"` |
||||
ThumbnailInfo ImageInfo `json:"thumbnail_info"` |
||||
ThumbnailURL string `json:"thumbnail_url,omitempty"` |
||||
Height uint `json:"h,omitempty"` |
||||
Width uint `json:"w,omitempty"` |
||||
Duration uint `json:"duration,omitempty"` |
||||
Size uint `json:"size,omitempty"` |
||||
} |
||||
|
||||
// VideoMessage is an m.video - http://matrix.org/docs/spec/client_server/r0.2.0.html#m-video
|
||||
type VideoMessage struct { |
||||
MsgType string `json:"msgtype"` |
||||
Body string `json:"body"` |
||||
URL string `json:"url"` |
||||
Info VideoInfo `json:"info"` |
||||
} |
||||
|
||||
// ImageMessage is an m.image event
|
||||
type ImageMessage struct { |
||||
MsgType string `json:"msgtype"` |
||||
Body string `json:"body"` |
||||
URL string `json:"url"` |
||||
Info ImageInfo `json:"info"` |
||||
} |
||||
|
||||
// An HTMLMessage is the contents of a Matrix HTML formated message event.
|
||||
type HTMLMessage struct { |
||||
Body string `json:"body"` |
||||
MsgType string `json:"msgtype"` |
||||
Format string `json:"format"` |
||||
FormattedBody string `json:"formatted_body"` |
||||
} |
||||
|
||||
var htmlRegex = regexp.MustCompile("<[^<]+?>") |
||||
|
||||
// GetHTMLMessage returns an HTMLMessage with the body set to a stripped version of the provided HTML, in addition
|
||||
// to the provided HTML.
|
||||
func GetHTMLMessage(msgtype, htmlText string) HTMLMessage { |
||||
return HTMLMessage{ |
||||
Body: html.UnescapeString(htmlRegex.ReplaceAllLiteralString(htmlText, "")), |
||||
MsgType: msgtype, |
||||
Format: "org.matrix.custom.html", |
||||
FormattedBody: htmlText, |
||||
} |
||||
} |
@ -0,0 +1,90 @@ |
||||
// 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 gomatrix |
||||
|
||||
import "errors" |
||||
|
||||
//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"` |
||||
} |
||||
|
||||
// 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"` |
||||
} |
||||
|
||||
// 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"` |
||||
} |
||||
|
||||
// Validate checks if the filter contains valid property values
|
||||
func (filter *Filter) Validate() error { |
||||
if filter.EventFormat != "client" && filter.EventFormat != "federation" { |
||||
return errors.New("Bad event_format value. Must be one of [\"client\", \"federation\"]") |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// DefaultFilter returns the default filter used by the Matrix server if no filter is provided in the request
|
||||
func DefaultFilter() Filter { |
||||
return Filter{ |
||||
AccountData: DefaultFilterPart(), |
||||
EventFields: nil, |
||||
EventFormat: "client", |
||||
Presence: DefaultFilterPart(), |
||||
Room: RoomFilter{ |
||||
AccountData: DefaultFilterPart(), |
||||
Ephemeral: DefaultFilterPart(), |
||||
IncludeLeave: false, |
||||
NotRooms: nil, |
||||
Rooms: nil, |
||||
State: DefaultFilterPart(), |
||||
Timeline: DefaultFilterPart(), |
||||
}, |
||||
} |
||||
} |
||||
|
||||
// DefaultFilterPart returns the default filter part used by the Matrix server if no filter is provided in the request
|
||||
func DefaultFilterPart() FilterPart { |
||||
return FilterPart{ |
||||
NotRooms: nil, |
||||
Rooms: nil, |
||||
Limit: 20, |
||||
NotSenders: nil, |
||||
NotTypes: nil, |
||||
Senders: nil, |
||||
Types: nil, |
||||
} |
||||
} |
@ -0,0 +1,78 @@ |
||||
package gomatrix |
||||
|
||||
// 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"` |
||||
InitialDeviceDisplayName string `json:"initial_device_display_name"` |
||||
Auth interface{} `json:"auth,omitempty"` |
||||
} |
||||
|
||||
// ReqLogin is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-login
|
||||
type ReqLogin struct { |
||||
Type string `json:"type"` |
||||
Password string `json:"password,omitempty"` |
||||
Medium string `json:"medium,omitempty"` |
||||
User string `json:"user,omitempty"` |
||||
Address string `json:"address,omitempty"` |
||||
Token string `json:"token,omitempty"` |
||||
DeviceID string `json:"device_id,omitempty"` |
||||
InitialDeviceDisplayName string `json:"initial_device_display_name,omitempty"` |
||||
} |
||||
|
||||
// ReqCreateRoom is the JSON request for https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
|
||||
type ReqCreateRoom struct { |
||||
Visibility string `json:"visibility,omitempty"` |
||||
RoomAliasName string `json:"room_alias_name,omitempty"` |
||||
Name string `json:"name,omitempty"` |
||||
Topic string `json:"topic,omitempty"` |
||||
Invite []string `json:"invite,omitempty"` |
||||
Invite3PID []ReqInvite3PID `json:"invite_3pid,omitempty"` |
||||
CreationContent map[string]interface{} `json:"creation_content,omitempty"` |
||||
InitialState []Event `json:"initial_state,omitempty"` |
||||
Preset string `json:"preset,omitempty"` |
||||
IsDirect bool `json:"is_direct,omitempty"` |
||||
} |
||||
|
||||
// ReqRedact is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-redact-eventid-txnid
|
||||
type ReqRedact struct { |
||||
Reason string `json:"reason,omitempty"` |
||||
} |
||||
|
||||
// ReqInvite3PID is the JSON request for https://matrix.org/docs/spec/client_server/r0.2.0.html#id57
|
||||
// It is also a JSON object used in https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
|
||||
type ReqInvite3PID struct { |
||||
IDServer string `json:"id_server"` |
||||
Medium string `json:"medium"` |
||||
Address string `json:"address"` |
||||
} |
||||
|
||||
// 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"` |
||||
} |
||||
|
||||
// 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"` |
||||
} |
||||
|
||||
// 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"` |
||||
} |
||||
|
||||
// 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"` |
||||
} |
||||
|
||||
// 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
|
||||
type ReqTyping struct { |
||||
Typing bool `json:"typing"` |
||||
Timeout int64 `json:"timeout"` |
||||
} |
@ -0,0 +1,176 @@ |
||||
package gomatrix |
||||
|
||||
// RespError is the standard JSON error response from Homeservers. It also implements the Golang "error" interface.
|
||||
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#api-standards
|
||||
type RespError struct { |
||||
ErrCode string `json:"errcode"` |
||||
Err string `json:"error"` |
||||
} |
||||
|
||||
// Error returns the errcode and error message.
|
||||
func (e RespError) Error() string { |
||||
return e.ErrCode + ": " + e.Err |
||||
} |
||||
|
||||
// RespCreateFilter is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-user-userid-filter
|
||||
type RespCreateFilter struct { |
||||
FilterID string `json:"filter_id"` |
||||
} |
||||
|
||||
// RespVersions is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-versions
|
||||
type RespVersions struct { |
||||
Versions []string `json:"versions"` |
||||
} |
||||
|
||||
// RespJoinRoom is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-join
|
||||
type RespJoinRoom struct { |
||||
RoomID string `json:"room_id"` |
||||
} |
||||
|
||||
// RespLeaveRoom is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-leave
|
||||
type RespLeaveRoom struct{} |
||||
|
||||
// RespForgetRoom is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-forget
|
||||
type RespForgetRoom struct{} |
||||
|
||||
// RespInviteUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-invite
|
||||
type RespInviteUser struct{} |
||||
|
||||
// RespKickUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-kick
|
||||
type RespKickUser struct{} |
||||
|
||||
// RespBanUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-ban
|
||||
type RespBanUser struct{} |
||||
|
||||
// RespUnbanUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-unban
|
||||
type RespUnbanUser struct{} |
||||
|
||||
// RespTyping is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-typing-userid
|
||||
type RespTyping struct{} |
||||
|
||||
// RespJoinedRooms is the JSON response for TODO-SPEC https://github.com/matrix-org/synapse/pull/1680
|
||||
type RespJoinedRooms struct { |
||||
JoinedRooms []string `json:"joined_rooms"` |
||||
} |
||||
|
||||
// RespJoinedMembers is the JSON response for TODO-SPEC https://github.com/matrix-org/synapse/pull/1680
|
||||
type RespJoinedMembers struct { |
||||
Joined map[string]struct { |
||||
DisplayName *string `json:"display_name"` |
||||
AvatarURL *string `json:"avatar_url"` |
||||
} `json:"joined"` |
||||
} |
||||
|
||||
// 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"` |
||||
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"` |
||||
} |
||||
|
||||
// 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"` |
||||
} |
||||
|
||||
// RespUserInteractive is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#user-interactive-authentication-api
|
||||
type RespUserInteractive struct { |
||||
Flows []struct { |
||||
Stages []string `json:"stages"` |
||||
} `json:"flows"` |
||||
Params map[string]interface{} `json:"params"` |
||||
Session string `json:"string"` |
||||
Completed []string `json:"completed"` |
||||
ErrCode string `json:"errcode"` |
||||
Error string `json:"error"` |
||||
} |
||||
|
||||
// HasSingleStageFlow returns true if there exists at least 1 Flow with a single stage of stageName.
|
||||
func (r RespUserInteractive) HasSingleStageFlow(stageName string) bool { |
||||
for _, f := range r.Flows { |
||||
if len(f.Stages) == 1 && f.Stages[0] == stageName { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
// RespUserDisplayName is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname
|
||||
type RespUserDisplayName struct { |
||||
DisplayName string `json:"displayname"` |
||||
} |
||||
|
||||
// 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"` |
||||
} |
||||
|
||||
// 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"` |
||||
} |
||||
|
||||
// RespLogout is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-logout
|
||||
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"` |
||||
} |
||||
|
||||
// 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"` |
||||
AccountData struct { |
||||
Events []Event `json:"events"` |
||||
} `json:"account_data"` |
||||
Presence struct { |
||||
Events []Event `json:"events"` |
||||
} `json:"presence"` |
||||
Rooms struct { |
||||
Leave map[string]struct { |
||||
State struct { |
||||
Events []Event `json:"events"` |
||||
} `json:"state"` |
||||
Timeline struct { |
||||
Events []Event `json:"events"` |
||||
Limited bool `json:"limited"` |
||||
PrevBatch string `json:"prev_batch"` |
||||
} `json:"timeline"` |
||||
} `json:"leave"` |
||||
Join map[string]struct { |
||||
State struct { |
||||
Events []Event `json:"events"` |
||||
} `json:"state"` |
||||
Timeline struct { |
||||
Events []Event `json:"events"` |
||||
Limited bool `json:"limited"` |
||||
PrevBatch string `json:"prev_batch"` |
||||
} `json:"timeline"` |
||||
} `json:"join"` |
||||
Invite map[string]struct { |
||||
State struct { |
||||
Events []Event |
||||
} `json:"invite_state"` |
||||
} `json:"invite"` |
||||
} `json:"rooms"` |
||||
} |
||||
|
||||
type RespTurnServer struct { |
||||
Username string `json:"username"` |
||||
Password string `json:"password"` |
||||
TTL int `json:"ttl"` |
||||
URIs []string `json:"uris"` |
||||
} |
@ -0,0 +1,50 @@ |
||||
package gomatrix |
||||
|
||||
// Room represents a single Matrix room.
|
||||
type Room struct { |
||||
ID string |
||||
State map[string]map[string]*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) { |
||||
_, exists := room.State[event.Type] |
||||
if !exists { |
||||
room.State[event.Type] = make(map[string]*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 string, stateKey string) *Event { |
||||
stateEventMap, _ := room.State[eventType] |
||||
event, _ := stateEventMap[stateKey] |
||||
return 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) string { |
||||
state := "leave" |
||||
event := room.GetStateEvent("m.room.member", userID) |
||||
if event != nil { |
||||
membershipState, found := event.Content["membership"] |
||||
if found { |
||||
mState, isString := membershipState.(string) |
||||
if isString { |
||||
state = mState |
||||
} |
||||
} |
||||
} |
||||
return state |
||||
} |
||||
|
||||
// NewRoom creates a new Room with the given ID
|
||||
func NewRoom(roomID string) *Room { |
||||
// Init the State map and return a pointer to the Room
|
||||
return &Room{ |
||||
ID: roomID, |
||||
State: make(map[string]map[string]*Event), |
||||
} |
||||
} |