diff --git a/go.mod b/go.mod index 76e367f..26d8aaf 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/Lomanic/presence-button-web go 1.12 + +require github.com/matrix-org/gomatrix v0.0.0-20200827122206-7dd5e2a05bcd diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f20dbc3 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/matrix-org/gomatrix v0.0.0-20200827122206-7dd5e2a05bcd h1:xVrqJK3xHREMNjwjljkAUaadalWc0rRbmVuQatzmgwg= +github.com/matrix-org/gomatrix v0.0.0-20200827122206-7dd5e2a05bcd/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s= diff --git a/main.go b/main.go index a5b2c64..0340e02 100644 --- a/main.go +++ b/main.go @@ -1,18 +1,17 @@ package main import ( - "bytes" "encoding/json" "fmt" - "io/ioutil" "log" "net/http" - "net/url" "os" "path/filepath" "strconv" "strings" "time" + + "github.com/matrix-org/gomatrix" ) type Status struct { @@ -54,7 +53,8 @@ var ( // formerly https://www.flaticon.com/free-icon/closed_1234190, maybe try https://flaticons.net/customize.php?dir=Miscellaneous&icon=Closed.png without attribution false: ``, } - db *os.File + db *os.File + matrix *gomatrix.Client ) func init() { @@ -79,6 +79,11 @@ func init() { if config.MATRIXACCESSTOKEN == "" { panic("MATRIXACCESSTOKEN is empty") } + var err error + matrix, err = gomatrix.NewClient(fmt.Sprintf("https://%s", config.MATRIXUSERNAME[strings.Index(config.MATRIXUSERNAME, ":")+1:]), config.MATRIXUSERNAME, config.MATRIXACCESSTOKEN) + if err != nil { + panic(fmt.Sprintf("error creating matrix client: %s", err)) + } if config.MATRIXROOM == "" { panic("MATRIXROOM is empty") @@ -97,7 +102,7 @@ func init() { panic("ESPPASSWORD is empty") } - err := os.MkdirAll(filepath.Dir(dbPath), 0755) + err = os.MkdirAll(filepath.Dir(dbPath), 0755) if err != nil { panic(err) } @@ -124,7 +129,7 @@ func checkClosure() { // the Fuz is newly closed, notify on matrix and write file to survive reboot // TODO: matrix msg fmt.Println("the Fuz is newly closed, notify on matrix and write file to survive reboot") - err := sendMatrixMessage(config.MATRIXUSERNAME, config.MATRIXACCESSTOKEN, config.MATRIXROOM, config.MATRIXCLOSINGMESSAGE) + _, err := matrix.SendText(config.MATRIXROOM, config.MATRIXCLOSINGMESSAGE) if err != nil { fmt.Println("err:", err) time.Sleep(10 * time.Second) @@ -210,7 +215,7 @@ func statusHandler(w http.ResponseWriter, r *http.Request) { if status.FuzIsOpen && (status.LastOpened.Equal(status.LastClosed) || status.LastOpened.Before(status.LastClosed)) { // the Fuz is newly opened, notify on matrix and write file to survive reboot fmt.Println("the Fuz is newly opened, notify on matrix and write file to survive reboot") - err := sendMatrixMessage(config.MATRIXUSERNAME, config.MATRIXACCESSTOKEN, config.MATRIXROOM, config.MATRIXOPENINGMESSAGE) + _, err := matrix.SendText(config.MATRIXROOM, config.MATRIXOPENINGMESSAGE) if err != nil { fmt.Println("err:", err) return @@ -225,46 +230,6 @@ func statusHandler(w http.ResponseWriter, r *http.Request) { } } -func sendMatrixMessage(username, accessToken, room, messageText string) error { - type Message struct { - Msgtype string `json:"msgtype"` - Body string `json:"body"` - } - client := &http.Client{} - message := Message{ - Msgtype: "m.text", - Body: messageText, - } - body := new(bytes.Buffer) - err := json.NewEncoder(body).Encode(message) - if err != nil { - return err - } - v := url.Values{} - v.Set("access_token", accessToken) - v.Set("limit", "1") - url := url.URL{ - Scheme: "https", - Host: username[strings.Index(username, ":")+1:], - Path: fmt.Sprintf("/_matrix/client/r0/rooms/%s/send/m.room.message/%d", room, time.Now().UnixNano()/1000000), - RawQuery: v.Encode(), - } - req, err := http.NewRequest(http.MethodPut, url.String(), body) - if err != nil { - return err - } - req.Header.Set("Content-Type", "application/json") - res, err := client.Do(req) - if err != nil { - return err - } - defer res.Body.Close() - resBody, _ := ioutil.ReadAll(res.Body) - fmt.Println(string(resBody)) - - return nil -} - func main() { go updateUptime() go checkClosure() diff --git a/vendor/github.com/matrix-org/gomatrix/.gitignore b/vendor/github.com/matrix-org/gomatrix/.gitignore new file mode 100644 index 0000000..daf913b --- /dev/null +++ b/vendor/github.com/matrix-org/gomatrix/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/github.com/matrix-org/gomatrix/.golangci.yml b/vendor/github.com/matrix-org/gomatrix/.golangci.yml new file mode 100644 index 0000000..15eb6ef --- /dev/null +++ b/vendor/github.com/matrix-org/gomatrix/.golangci.yml @@ -0,0 +1,21 @@ +run: + timeout: 5m + linters: + enable: + - vet + - vetshadow + - typecheck + - deadcode + - gocyclo + - golint + - varcheck + - structcheck + - maligned + - ineffassign + - misspell + - unparam + - goimports + - goconst + - unconvert + - errcheck + - interfacer diff --git a/vendor/github.com/matrix-org/gomatrix/.travis.yml b/vendor/github.com/matrix-org/gomatrix/.travis.yml new file mode 100644 index 0000000..46997ab --- /dev/null +++ b/vendor/github.com/matrix-org/gomatrix/.travis.yml @@ -0,0 +1,7 @@ +language: go +go: + - 1.13.10 +install: + - go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.24.0 + - go build +script: ./hooks/pre-commit diff --git a/vendor/github.com/matrix-org/gomatrix/LICENSE b/vendor/github.com/matrix-org/gomatrix/LICENSE new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/vendor/github.com/matrix-org/gomatrix/LICENSE @@ -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. diff --git a/vendor/github.com/matrix-org/gomatrix/README.md b/vendor/github.com/matrix-org/gomatrix/README.md new file mode 100644 index 0000000..ea9109a --- /dev/null +++ b/vendor/github.com/matrix-org/gomatrix/README.md @@ -0,0 +1,6 @@ +# gomatrix +[![GoDoc](https://godoc.org/github.com/matrix-org/gomatrix?status.svg)](https://godoc.org/github.com/matrix-org/gomatrix) + +A Golang Matrix client. + +**THIS IS UNDER ACTIVE DEVELOPMENT: BREAKING CHANGES ARE FREQUENT.** diff --git a/vendor/github.com/matrix-org/gomatrix/client.go b/vendor/github.com/matrix-org/gomatrix/client.go new file mode 100644 index 0000000..fd77fce --- /dev/null +++ b/vendor/github.com/matrix-org/gomatrix/client.go @@ -0,0 +1,794 @@ +// 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" + "strings" + "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 { + Contents []byte + 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("contents=%v msg=%s code=%d wrapped=%s", e.Contents, 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 := append([]string{cli.Prefix}, urlPath...) + 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...) + // Manually add the trailing slash back to the end of the path if it's explicitly needed + if strings.HasSuffix(urlPath[len(urlPath)-1], "/") { + hsURL.Path = hsURL.Path + "/" + } + 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. +// The response body will be stream decoded into an interface. This will automatically stop if the response +// body is nil. +// +// 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, byte contents of the response body 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{}) error { + var req *http.Request + var err error + if reqBody != nil { + buf := new(bytes.Buffer) + if err := json.NewEncoder(buf).Encode(reqBody); err != nil { + return err + } + req, err = http.NewRequest(method, httpURL, buf) + } else { + req, err = http.NewRequest(method, httpURL, nil) + } + + if err != nil { + return 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 err + } + if res.StatusCode/100 != 2 { // not 2xx + contents, err := ioutil.ReadAll(res.Body) + if err != nil { + return err + } + + 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 HTTPError{ + Contents: contents, + Code: res.StatusCode, + Message: msg, + WrappedError: wrap, + } + } + + if resBody != nil && res.Body != nil { + return json.NewDecoder(res.Body).Decode(&resBody) + } + + return 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) { + err = cli.MakeRequest("POST", u, req, &resp) + 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(httpErr.Contents, &uiaResp) + return + } + } + 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.6.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 +} + +// LogoutAll logs the current user out on all devices. See https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-logout-all +// This does not clear the credentials from the client instance. See ClearCredentails() instead. +func (cli *Client) LogoutAll() (resp *RespLogoutAll, err error) { + urlPath := cli.BuildURL("logout/all") + 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 +} + +// PublicRooms returns the list of public rooms on target server. See https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-unstable-publicrooms +func (cli *Client) PublicRooms(limit int, since string, server string) (resp *RespPublicRooms, err error) { + args := map[string]string{} + + if limit != 0 { + args["limit"] = strconv.Itoa(limit) + } + if since != "" { + args["since"] = since + } + if server != "" { + args["server"] = server + } + + urlPath := cli.BuildURLWithQuery([]string{"publicRooms"}, args) + err = cli.MakeRequest("GET", urlPath, nil, &resp) + return +} + +// PublicRoomsFiltered returns a subset of PublicRooms filtered server side. +// See https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-unstable-publicrooms +func (cli *Client) PublicRoomsFiltered(limit int, since string, server string, filter string) (resp *RespPublicRooms, err error) { + content := map[string]string{} + + if limit != 0 { + content["limit"] = strconv.Itoa(limit) + } + if since != "" { + content["since"] = since + } + if filter != "" { + content["filter"] = filter + } + + var urlPath string + if server == "" { + urlPath = cli.BuildURL("publicRooms") + } else { + urlPath = cli.BuildURLWithQuery([]string{"publicRooms"}, map[string]string{ + "server": server, + }) + } + + err = cli.MakeRequest("POST", urlPath, content, &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() (string, 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) 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 +} + +// GetStatus returns the status of the user from the specified MXID. See https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-presence-userid-status +func (cli *Client) GetStatus(mxid string) (resp *RespUserStatus, err error) { + urlPath := cli.BuildURL("presence", mxid, "status") + err = cli.MakeRequest("GET", urlPath, nil, &resp) + return +} + +// GetOwnStatus returns the user's status. See https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-presence-userid-status +func (cli *Client) GetOwnStatus() (resp *RespUserStatus, err error) { + return cli.GetStatus(cli.UserID) +} + +// SetStatus sets the user's status. See https://matrix.org/docs/spec/client_server/r0.6.0#put-matrix-client-r0-presence-userid-status +func (cli *Client) SetStatus(presence, status string) (err error) { + urlPath := cli.BuildURL("presence", cli.UserID, "status") + s := struct { + Presence string `json:"presence"` + StatusMsg string `json:"status_msg"` + }{presence, status} + err = cli.MakeRequest("PUT", urlPath, &s, nil) + return +} + +// 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{MsgType: "m.text", Body: text}) +} + +// SendFormattedText sends an m.room.message event into the given room with a msgtype of m.text, supports a subset of HTML for formatting. +// See https://matrix.org/docs/spec/client_server/r0.6.0#m-text +func (cli *Client) SendFormattedText(roomID, text, formattedText string) (*RespSendEvent, error) { + return cli.SendMessageEvent(roomID, "m.room.message", + TextMessage{MsgType: "m.text", Body: text, FormattedBody: formattedText, Format: "org.matrix.custom.html"}) +} + +// 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{MsgType: "m.notice", Body: 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 +} + +// MarkRead marks eventID in roomID as read, signifying the event, and all before it have been read. See https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-rooms-roomid-receipt-receipttype-eventid +func (cli *Client) MarkRead(roomID, eventID string) error { + urlPath := cli.BuildURL("rooms", roomID, "receipt", "m.read", eventID) + return cli.MakeRequest("POST", urlPath, nil, nil) +} + +// 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{ + Contents: contents, + 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 +} diff --git a/vendor/github.com/matrix-org/gomatrix/events.go b/vendor/github.com/matrix-org/gomatrix/events.go new file mode 100644 index 0000000..cbc70a8 --- /dev/null +++ b/vendor/github.com/matrix-org/gomatrix/events.go @@ -0,0 +1,157 @@ +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) + Redacts string `json:"redacts,omitempty"` // The event ID that was redacted if a m.room.redaction event + Unsigned map[string]interface{} `json:"unsigned"` // The unsigned portions of the event, such as age and prev_content + Content map[string]interface{} `json:"content"` // The JSON content of the event. + PrevContent map[string]interface{} `json:"prev_content,omitempty"` // The JSON prev_content of the 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"` + FormattedBody string `json:"formatted_body"` + Format string `json:"format"` +} + +// ThumbnailInfo contains info about an thumbnail image - http://matrix.org/docs/spec/client_server/r0.2.0.html#m-image +type ThumbnailInfo struct { + Height uint `json:"h,omitempty"` + Width uint `json:"w,omitempty"` + Mimetype string `json:"mimetype,omitempty"` + Size uint `json:"size,omitempty"` +} + +// 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"` + ThumbnailInfo ThumbnailInfo `json:"thumbnail_info,omitempty"` + ThumbnailURL string `json:"thumbnail_url,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 ThumbnailInfo `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"` +} + +// FileInfo contains info about an file - http://matrix.org/docs/spec/client_server/r0.2.0.html#m-file +type FileInfo struct { + Mimetype string `json:"mimetype,omitempty"` + Size uint `json:"size,omitempty"` //filesize in bytes +} + +// FileMessage is an m.file event - http://matrix.org/docs/spec/client_server/r0.2.0.html#m-file +type FileMessage struct { + MsgType string `json:"msgtype"` + Body string `json:"body"` + URL string `json:"url"` + Filename string `json:"filename"` + Info FileInfo `json:"info,omitempty"` + ThumbnailURL string `json:"thumbnail_url,omitempty"` + ThumbnailInfo ImageInfo `json:"thumbnail_info,omitempty"` +} + +// LocationMessage is an m.location event - http://matrix.org/docs/spec/client_server/r0.2.0.html#m-location +type LocationMessage struct { + MsgType string `json:"msgtype"` + Body string `json:"body"` + GeoURI string `json:"geo_uri"` + ThumbnailURL string `json:"thumbnail_url,omitempty"` + ThumbnailInfo ImageInfo `json:"thumbnail_info,omitempty"` +} + +// AudioInfo contains info about an file - http://matrix.org/docs/spec/client_server/r0.2.0.html#m-audio +type AudioInfo struct { + Mimetype string `json:"mimetype,omitempty"` + Size uint `json:"size,omitempty"` //filesize in bytes + Duration uint `json:"duration,omitempty"` //audio duration in ms +} + +// AudioMessage is an m.audio event - http://matrix.org/docs/spec/client_server/r0.2.0.html#m-audio +type AudioMessage struct { + MsgType string `json:"msgtype"` + Body string `json:"body"` + URL string `json:"url"` + Info AudioInfo `json:"info,omitempty"` +} + +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, + } +} diff --git a/vendor/github.com/matrix-org/gomatrix/filter.go b/vendor/github.com/matrix-org/gomatrix/filter.go new file mode 100644 index 0000000..2a0c37f --- /dev/null +++ b/vendor/github.com/matrix-org/gomatrix/filter.go @@ -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, + } +} diff --git a/vendor/github.com/matrix-org/gomatrix/go.mod b/vendor/github.com/matrix-org/gomatrix/go.mod new file mode 100644 index 0000000..18e69bf --- /dev/null +++ b/vendor/github.com/matrix-org/gomatrix/go.mod @@ -0,0 +1,3 @@ +module github.com/matrix-org/gomatrix + +go 1.12 diff --git a/vendor/github.com/matrix-org/gomatrix/identifier.go b/vendor/github.com/matrix-org/gomatrix/identifier.go new file mode 100644 index 0000000..4a61d08 --- /dev/null +++ b/vendor/github.com/matrix-org/gomatrix/identifier.go @@ -0,0 +1,69 @@ +package gomatrix + +// Identifier is the interface for https://matrix.org/docs/spec/client_server/r0.6.0#identifier-types +type Identifier interface { + // Returns the identifier type + // https://matrix.org/docs/spec/client_server/r0.6.0#identifier-types + Type() string +} + +// UserIdentifier is the Identifier for https://matrix.org/docs/spec/client_server/r0.6.0#matrix-user-id +type UserIdentifier struct { + IDType string `json:"type"` // Set by NewUserIdentifer + User string `json:"user"` +} + +// Type implements the Identifier interface +func (i UserIdentifier) Type() string { + return "m.id.user" +} + +// NewUserIdentifier creates a new UserIdentifier with IDType set to "m.id.user" +func NewUserIdentifier(user string) UserIdentifier { + return UserIdentifier{ + IDType: "m.id.user", + User: user, + } +} + +// ThirdpartyIdentifier is the Identifier for https://matrix.org/docs/spec/client_server/r0.6.0#third-party-id +type ThirdpartyIdentifier struct { + IDType string `json:"type"` // Set by NewThirdpartyIdentifier + Medium string `json:"medium"` + Address string `json:"address"` +} + +// Type implements the Identifier interface +func (i ThirdpartyIdentifier) Type() string { + return "m.id.thirdparty" +} + +// NewThirdpartyIdentifier creates a new UserIdentifier with IDType set to "m.id.user" +func NewThirdpartyIdentifier(medium, address string) ThirdpartyIdentifier { + return ThirdpartyIdentifier{ + IDType: "m.id.thirdparty", + Medium: medium, + Address: address, + } +} + +// PhoneIdentifier is the Identifier for https://matrix.org/docs/spec/client_server/r0.6.0#phone-number +type PhoneIdentifier struct { + IDType string `json:"type"` // Set by NewPhoneIdentifier + Country string `json:"country"` + Phone string `json:"phone"` +} + +// Type implements the Identifier interface +func (i PhoneIdentifier) Type() string { + return "m.id.phone" +} + +// NewPhoneIdentifier creates a new UserIdentifier with IDType set to "m.id.user" +func NewPhoneIdentifier(country, phone string) PhoneIdentifier { + return PhoneIdentifier{ + IDType: "m.id.phone", + Country: country, + Phone: phone, + } +} diff --git a/vendor/github.com/matrix-org/gomatrix/requests.go b/vendor/github.com/matrix-org/gomatrix/requests.go new file mode 100644 index 0000000..31c426d --- /dev/null +++ b/vendor/github.com/matrix-org/gomatrix/requests.go @@ -0,0 +1,79 @@ +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.6.0.html#post-matrix-client-r0-login +type ReqLogin struct { + Type string `json:"type"` + Identifier Identifier `json:"identifier,omitempty"` + 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"` +} diff --git a/vendor/github.com/matrix-org/gomatrix/responses.go b/vendor/github.com/matrix-org/gomatrix/responses.go new file mode 100644 index 0000000..f488e69 --- /dev/null +++ b/vendor/github.com/matrix-org/gomatrix/responses.go @@ -0,0 +1,210 @@ +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"` +} + +// RespPublicRooms is the JSON response for http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#get-matrix-client-unstable-publicrooms +type RespPublicRooms struct { + TotalRoomCountEstimate int `json:"total_room_count_estimate"` + PrevBatch string `json:"prev_batch"` + NextBatch string `json:"next_batch"` + Chunk []PublicRoom `json:"chunk"` +} + +// 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:"session"` + 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"` +} + +// RespUserStatus is the JSON response for https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-presence-userid-status +type RespUserStatus struct { + Presence string `json:"presence"` + StatusMsg string `json:"status_msg"` + LastActiveAgo int `json:"last_active_ago"` + CurrentlyActive bool `json:"currently_active"` +} + +// 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.6.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"` + WellKnown DiscoveryInformation `json:"well_known"` +} + +// DiscoveryInformation is the JSON Response for https://matrix.org/docs/spec/client_server/r0.6.0#get-well-known-matrix-client and a part of the JSON Response for https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-login +type DiscoveryInformation struct { + Homeserver struct { + BaseURL string `json:"base_url"` + } `json:"m.homeserver"` + IdentityServer struct { + BaseURL string `json:"base_url"` + } `json:"m.identitiy_server"` +} + +// RespLogout is the JSON response for http://matrix.org/docs/spec/client_server/r0.6.0.html#post-matrix-client-r0-logout +type RespLogout struct{} + +// RespLogoutAll is the JSON response for https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-logout-all +type RespLogoutAll 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"` + Ephemeral struct { + Events []Event `json:"events"` + } `json:"ephemeral"` + } `json:"join"` + Invite map[string]struct { + State struct { + Events []Event + } `json:"invite_state"` + } `json:"invite"` + } `json:"rooms"` +} + +// RespTurnServer is the JSON response from a Turn Server +type RespTurnServer struct { + Username string `json:"username"` + Password string `json:"password"` + TTL int `json:"ttl"` + URIs []string `json:"uris"` +} diff --git a/vendor/github.com/matrix-org/gomatrix/room.go b/vendor/github.com/matrix-org/gomatrix/room.go new file mode 100644 index 0000000..364deab --- /dev/null +++ b/vendor/github.com/matrix-org/gomatrix/room.go @@ -0,0 +1,63 @@ +package gomatrix + +// Room represents a single Matrix room. +type Room struct { + ID string + State map[string]map[string]*Event +} + +// PublicRoom represents the information about a public room obtainable from the room directory +type PublicRoom struct { + CanonicalAlias string `json:"canonical_alias"` + Name string `json:"name"` + WorldReadable bool `json:"world_readable"` + Topic string `json:"topic"` + NumJoinedMembers int `json:"num_joined_members"` + AvatarURL string `json:"avatar_url"` + RoomID string `json:"room_id"` + GuestCanJoin bool `json:"guest_can_join"` + Aliases []string `json:"aliases"` +} + +// 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), + } +} diff --git a/vendor/github.com/matrix-org/gomatrix/store.go b/vendor/github.com/matrix-org/gomatrix/store.go new file mode 100644 index 0000000..6dc687e --- /dev/null +++ b/vendor/github.com/matrix-org/gomatrix/store.go @@ -0,0 +1,65 @@ +package gomatrix + +// 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 + SaveRoom(room *Room) + LoadRoom(roomID string) *Room +} + +// InMemoryStore implements the Storer interface. +// +// Everything is persisted in-memory as maps. It is not safe to load/save filter IDs +// 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 +} + +// SaveFilterID to memory. +func (s *InMemoryStore) SaveFilterID(userID, filterID string) { + s.Filters[userID] = filterID +} + +// LoadFilterID from memory. +func (s *InMemoryStore) LoadFilterID(userID string) string { + return s.Filters[userID] +} + +// SaveNextBatch to memory. +func (s *InMemoryStore) SaveNextBatch(userID, nextBatchToken string) { + s.NextBatch[userID] = nextBatchToken +} + +// LoadNextBatch from memory. +func (s *InMemoryStore) LoadNextBatch(userID string) string { + return s.NextBatch[userID] +} + +// SaveRoom to memory. +func (s *InMemoryStore) SaveRoom(room *Room) { + s.Rooms[room.ID] = room +} + +// LoadRoom from memory. +func (s *InMemoryStore) LoadRoom(roomID string) *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), + } +} diff --git a/vendor/github.com/matrix-org/gomatrix/sync.go b/vendor/github.com/matrix-org/gomatrix/sync.go new file mode 100644 index 0000000..ac326c3 --- /dev/null +++ b/vendor/github.com/matrix-org/gomatrix/sync.go @@ -0,0 +1,168 @@ +package gomatrix + +import ( + "encoding/json" + "fmt" + "runtime/debug" + "time" +) + +// Syncer represents an interface that must be satisfied in order to do /sync requests on a client. +type Syncer interface { + // Process the /sync response. The since parameter is the since= value that was used to produce the response. + // This is useful for detecting the very first sync (since=""). If an error is return, Syncing will be stopped + // permanently. + ProcessResponse(resp *RespSync, since string) error + // 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 +} + +// 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 + Store Storer + listeners map[string][]OnEventListener // event type to listeners array +} + +// OnEventListener can be used with DefaultSyncer.OnEventType to be informed of incoming events. +type OnEventListener func(*Event) + +// NewDefaultSyncer returns an instantiated DefaultSyncer +func NewDefaultSyncer(userID string, store Storer) *DefaultSyncer { + return &DefaultSyncer{ + UserID: userID, + Store: store, + listeners: make(map[string][]OnEventListener), + } +} + +// ProcessResponse processes the /sync response in a way suitable for bots. "Suitable for bots" means a stream of +// unrepeating events. Returns a fatal error if a listener panics. +func (s *DefaultSyncer) ProcessResponse(res *RespSync, since string) (err error) { + if !s.shouldProcessResponse(res, since) { + return + } + + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("ProcessResponse panicked! userID=%s since=%s panic=%s\n%s", s.UserID, since, r, debug.Stack()) + } + }() + + for roomID, roomData := range res.Rooms.Join { + room := s.getOrCreateRoom(roomID) + for _, event := range roomData.State.Events { + event.RoomID = roomID + room.UpdateState(&event) + s.notifyListeners(&event) + } + for _, event := range roomData.Timeline.Events { + event.RoomID = roomID + s.notifyListeners(&event) + } + for _, event := range roomData.Ephemeral.Events { + event.RoomID = roomID + s.notifyListeners(&event) + } + } + for roomID, roomData := range res.Rooms.Invite { + room := s.getOrCreateRoom(roomID) + for _, event := range roomData.State.Events { + event.RoomID = roomID + room.UpdateState(&event) + s.notifyListeners(&event) + } + } + for roomID, roomData := range res.Rooms.Leave { + room := s.getOrCreateRoom(roomID) + for _, event := range roomData.Timeline.Events { + if event.StateKey != nil { + event.RoomID = roomID + room.UpdateState(&event) + s.notifyListeners(&event) + } + } + } + return +} + +// 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 string, callback OnEventListener) { + _, exists := s.listeners[eventType] + if !exists { + s.listeners[eventType] = []OnEventListener{} + } + s.listeners[eventType] = append(s.listeners[eventType], callback) +} + +// shouldProcessResponse returns true if the response should be processed. May modify the response to remove +// stuff that shouldn't be processed. +func (s *DefaultSyncer) shouldProcessResponse(resp *RespSync, since string) bool { + if since == "" { + return false + } + // This is a horrible hack because /sync will return the most recent messages for a room + // as soon as you /join it. We do NOT want to process those events in that particular room + // because they may have already been processed (if you toggle the bot in/out of the room). + // + // Work around this by inspecting each room's timeline and seeing if an m.room.member event for us + // exists and is "join" and then discard processing that room entirely if so. + // TODO: We probably want to process messages from after the last join event in the timeline. + for roomID, roomData := range resp.Rooms.Join { + for i := len(roomData.Timeline.Events) - 1; i >= 0; i-- { + e := roomData.Timeline.Events[i] + if e.Type == "m.room.member" && e.StateKey != nil && *e.StateKey == s.UserID { + m := e.Content["membership"] + mship, ok := m.(string) + if !ok { + continue + } + if mship == "join" { + _, ok := resp.Rooms.Join[roomID] + if !ok { + continue + } + delete(resp.Rooms.Join, roomID) // don't re-process messages + delete(resp.Rooms.Invite, roomID) // don't re-process invites + break + } + } + } + } + return true +} + +// getOrCreateRoom must only be called by the Sync() goroutine which calls ProcessResponse() +func (s *DefaultSyncer) getOrCreateRoom(roomID string) *Room { + room := s.Store.LoadRoom(roomID) + if room == nil { // create a new Room + room = NewRoom(roomID) + s.Store.SaveRoom(room) + } + return room +} + +func (s *DefaultSyncer) notifyListeners(event *Event) { + listeners, exists := s.listeners[event.Type] + if !exists { + return + } + for _, fn := range listeners { + fn(event) + } +} + +// OnFailedSync always returns a 10 second wait period between failed /syncs, never a fatal error. +func (s *DefaultSyncer) OnFailedSync(res *RespSync, err error) (time.Duration, error) { + return 10 * time.Second, nil +} + +// GetFilterJSON returns a filter with a timeline limit of 50. +func (s *DefaultSyncer) GetFilterJSON(userID string) json.RawMessage { + return json.RawMessage(`{"room":{"timeline":{"limit":50}}}`) +} diff --git a/vendor/github.com/matrix-org/gomatrix/tags.go b/vendor/github.com/matrix-org/gomatrix/tags.go new file mode 100644 index 0000000..956fb11 --- /dev/null +++ b/vendor/github.com/matrix-org/gomatrix/tags.go @@ -0,0 +1,26 @@ +// Copyright 2019 Sumukha PK +// +// 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 + +// TagContent contains the data for an m.tag message type +// https://matrix.org/docs/spec/client_server/r0.4.0.html#m-tag +type TagContent struct { + Tags map[string]TagProperties `json:"tags"` +} + +// TagProperties contains the properties of a Tag +type TagProperties struct { + Order float32 `json:"order,omitempty"` // Empty values must be neglected +} diff --git a/vendor/github.com/matrix-org/gomatrix/userids.go b/vendor/github.com/matrix-org/gomatrix/userids.go new file mode 100644 index 0000000..70002c5 --- /dev/null +++ b/vendor/github.com/matrix-org/gomatrix/userids.go @@ -0,0 +1,130 @@ +package gomatrix + +import ( + "bytes" + "encoding/hex" + "fmt" + "strings" +) + +const lowerhex = "0123456789abcdef" + +// encode the given byte using quoted-printable encoding (e.g "=2f") +// and writes it to the buffer +// See https://golang.org/src/mime/quotedprintable/writer.go +func encode(buf *bytes.Buffer, b byte) { + buf.WriteByte('=') + buf.WriteByte(lowerhex[b>>4]) + buf.WriteByte(lowerhex[b&0x0f]) +} + +// escape the given alpha character and writes it to the buffer +func escape(buf *bytes.Buffer, b byte) { + buf.WriteByte('_') + if b == '_' { + buf.WriteByte('_') // another _ + } else { + buf.WriteByte(b + 0x20) // ASCII shift A-Z to a-z + } +} + +func shouldEncode(b byte) bool { + return b != '-' && b != '.' && b != '_' && !(b >= '0' && b <= '9') && !(b >= 'a' && b <= 'z') && !(b >= 'A' && b <= 'Z') +} + +func shouldEscape(b byte) bool { + return (b >= 'A' && b <= 'Z') || b == '_' +} + +func isValidByte(b byte) bool { + return isValidEscapedChar(b) || (b >= '0' && b <= '9') || b == '.' || b == '=' || b == '-' +} + +func isValidEscapedChar(b byte) bool { + return b == '_' || (b >= 'a' && b <= 'z') +} + +// EncodeUserLocalpart encodes the given string into Matrix-compliant user ID localpart form. +// See http://matrix.org/docs/spec/intro.html#mapping-from-other-character-sets +// +// This returns a string with only the characters "a-z0-9._=-". The uppercase range A-Z +// are encoded using leading underscores ("_"). Characters outside the aforementioned ranges +// (including literal underscores ("_") and equals ("=")) are encoded as UTF8 code points (NOT NCRs) +// and converted to lower-case hex with a leading "=". For example: +// Alph@Bet_50up => _alph=40_bet=5f50up +func EncodeUserLocalpart(str string) string { + strBytes := []byte(str) + var outputBuffer bytes.Buffer + for _, b := range strBytes { + if shouldEncode(b) { + encode(&outputBuffer, b) + } else if shouldEscape(b) { + escape(&outputBuffer, b) + } else { + outputBuffer.WriteByte(b) + } + } + return outputBuffer.String() +} + +// DecodeUserLocalpart decodes the given string back into the original input string. +// Returns an error if the given string is not a valid user ID localpart encoding. +// See http://matrix.org/docs/spec/intro.html#mapping-from-other-character-sets +// +// This decodes quoted-printable bytes back into UTF8, and unescapes casing. For +// example: +// _alph=40_bet=5f50up => Alph@Bet_50up +// Returns an error if the input string contains characters outside the +// range "a-z0-9._=-", has an invalid quote-printable byte (e.g. not hex), or has +// an invalid _ escaped byte (e.g. "_5"). +func DecodeUserLocalpart(str string) (string, error) { + strBytes := []byte(str) + var outputBuffer bytes.Buffer + for i := 0; i < len(strBytes); i++ { + b := strBytes[i] + if !isValidByte(b) { + return "", fmt.Errorf("Byte pos %d: Invalid byte", i) + } + + if b == '_' { // next byte is a-z and should be upper-case or is another _ and should be a literal _ + if i+1 >= len(strBytes) { + return "", fmt.Errorf("Byte pos %d: expected _[a-z_] encoding but ran out of string", i) + } + if !isValidEscapedChar(strBytes[i+1]) { // invalid escaping + return "", fmt.Errorf("Byte pos %d: expected _[a-z_] encoding", i) + } + if strBytes[i+1] == '_' { + outputBuffer.WriteByte('_') + } else { + outputBuffer.WriteByte(strBytes[i+1] - 0x20) // ASCII shift a-z to A-Z + } + i++ // skip next byte since we just handled it + } else if b == '=' { // next 2 bytes are hex and should be buffered ready to be read as utf8 + if i+2 >= len(strBytes) { + return "", fmt.Errorf("Byte pos: %d: expected quote-printable encoding but ran out of string", i) + } + dst := make([]byte, 1) + _, err := hex.Decode(dst, strBytes[i+1:i+3]) + if err != nil { + return "", err + } + outputBuffer.WriteByte(dst[0]) + i += 2 // skip next 2 bytes since we just handled it + } else { // pass through + outputBuffer.WriteByte(b) + } + } + return outputBuffer.String(), nil +} + +// ExtractUserLocalpart extracts the localpart portion of a user ID. +// See http://matrix.org/docs/spec/intro.html#user-identifiers +func ExtractUserLocalpart(userID string) (string, error) { + if len(userID) == 0 || userID[0] != '@' { + return "", fmt.Errorf("%s is not a valid user id", userID) + } + return strings.TrimPrefix( + strings.SplitN(userID, ":", 2)[0], // @foo:bar:8448 => [ "@foo", "bar:8448" ] + "@", // remove "@" prefix + ), nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt new file mode 100644 index 0000000..69f5fc2 --- /dev/null +++ b/vendor/modules.txt @@ -0,0 +1,2 @@ +# github.com/matrix-org/gomatrix v0.0.0-20200827122206-7dd5e2a05bcd +github.com/matrix-org/gomatrix