diff options
author | msglm <msglm@techchud.xyz> | 2023-10-27 07:08:25 -0500 |
---|---|---|
committer | msglm <msglm@techchud.xyz> | 2023-10-27 07:08:25 -0500 |
commit | 032a7e0c1188d3507b8d9a9571f2446a43cf775b (patch) | |
tree | 2bd38c01bc7761a6195e426082ce7191ebc765a1 /vendor | |
parent | 56e7bd01ca09ad52b0c4f48f146a20a4f1b78696 (diff) | |
download | matterbridge-msglm-032a7e0c1188d3507b8d9a9571f2446a43cf775b.tar.gz matterbridge-msglm-032a7e0c1188d3507b8d9a9571f2446a43cf775b.tar.bz2 matterbridge-msglm-032a7e0c1188d3507b8d9a9571f2446a43cf775b.zip |
apply https://github.com/42wim/matterbridge/pull/1864v1.26.0+0.1.0
Diffstat (limited to 'vendor')
160 files changed, 30308 insertions, 2186 deletions
diff --git a/vendor/github.com/gorilla/mux/AUTHORS b/vendor/github.com/gorilla/mux/AUTHORS new file mode 100644 index 00000000..b722392e --- /dev/null +++ b/vendor/github.com/gorilla/mux/AUTHORS @@ -0,0 +1,8 @@ +# This is the official list of gorilla/mux authors for copyright purposes. +# +# Please keep the list sorted. + +Google LLC (https://opensource.google.com/) +Kamil Kisielk <kamil@kamilkisiel.net> +Matt Silverlock <matt@eatsleeprepeat.net> +Rodrigo Moraes (https://github.com/moraes) diff --git a/vendor/github.com/gorilla/mux/LICENSE b/vendor/github.com/gorilla/mux/LICENSE new file mode 100644 index 00000000..6903df63 --- /dev/null +++ b/vendor/github.com/gorilla/mux/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012-2018 The Gorilla Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/gorilla/mux/README.md b/vendor/github.com/gorilla/mux/README.md new file mode 100644 index 00000000..35eea9f1 --- /dev/null +++ b/vendor/github.com/gorilla/mux/README.md @@ -0,0 +1,805 @@ +# gorilla/mux + +[![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux) +[![CircleCI](https://circleci.com/gh/gorilla/mux.svg?style=svg)](https://circleci.com/gh/gorilla/mux) +[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/mux/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/mux?badge) + +![Gorilla Logo](https://cloud-cdn.questionable.services/gorilla-icon-64.png) + +https://www.gorillatoolkit.org/pkg/mux + +Package `gorilla/mux` implements a request router and dispatcher for matching incoming requests to +their respective handler. + +The name mux stands for "HTTP request multiplexer". Like the standard `http.ServeMux`, `mux.Router` matches incoming requests against a list of registered routes and calls a handler for the route that matches the URL or other conditions. The main features are: + +* It implements the `http.Handler` interface so it is compatible with the standard `http.ServeMux`. +* Requests can be matched based on URL host, path, path prefix, schemes, header and query values, HTTP methods or using custom matchers. +* URL hosts, paths and query values can have variables with an optional regular expression. +* Registered URLs can be built, or "reversed", which helps maintaining references to resources. +* Routes can be used as subrouters: nested routes are only tested if the parent route matches. This is useful to define groups of routes that share common conditions like a host, a path prefix or other repeated attributes. As a bonus, this optimizes request matching. + +--- + +* [Install](#install) +* [Examples](#examples) +* [Matching Routes](#matching-routes) +* [Static Files](#static-files) +* [Serving Single Page Applications](#serving-single-page-applications) (e.g. React, Vue, Ember.js, etc.) +* [Registered URLs](#registered-urls) +* [Walking Routes](#walking-routes) +* [Graceful Shutdown](#graceful-shutdown) +* [Middleware](#middleware) +* [Handling CORS Requests](#handling-cors-requests) +* [Testing Handlers](#testing-handlers) +* [Full Example](#full-example) + +--- + +## Install + +With a [correctly configured](https://golang.org/doc/install#testing) Go toolchain: + +```sh +go get -u github.com/gorilla/mux +``` + +## Examples + +Let's start registering a couple of URL paths and handlers: + +```go +func main() { + r := mux.NewRouter() + r.HandleFunc("/", HomeHandler) + r.HandleFunc("/products", ProductsHandler) + r.HandleFunc("/articles", ArticlesHandler) + http.Handle("/", r) +} +``` + +Here we register three routes mapping URL paths to handlers. This is equivalent to how `http.HandleFunc()` works: if an incoming request URL matches one of the paths, the corresponding handler is called passing (`http.ResponseWriter`, `*http.Request`) as parameters. + +Paths can have variables. They are defined using the format `{name}` or `{name:pattern}`. If a regular expression pattern is not defined, the matched variable will be anything until the next slash. For example: + +```go +r := mux.NewRouter() +r.HandleFunc("/products/{key}", ProductHandler) +r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) +r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) +``` + +The names are used to create a map of route variables which can be retrieved calling `mux.Vars()`: + +```go +func ArticlesCategoryHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "Category: %v\n", vars["category"]) +} +``` + +And this is all you need to know about the basic usage. More advanced options are explained below. + +### Matching Routes + +Routes can also be restricted to a domain or subdomain. Just define a host pattern to be matched. They can also have variables: + +```go +r := mux.NewRouter() +// Only matches if domain is "www.example.com". +r.Host("www.example.com") +// Matches a dynamic subdomain. +r.Host("{subdomain:[a-z]+}.example.com") +``` + +There are several other matchers that can be added. To match path prefixes: + +```go +r.PathPrefix("/products/") +``` + +...or HTTP methods: + +```go +r.Methods("GET", "POST") +``` + +...or URL schemes: + +```go +r.Schemes("https") +``` + +...or header values: + +```go +r.Headers("X-Requested-With", "XMLHttpRequest") +``` + +...or query values: + +```go +r.Queries("key", "value") +``` + +...or to use a custom matcher function: + +```go +r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { + return r.ProtoMajor == 0 +}) +``` + +...and finally, it is possible to combine several matchers in a single route: + +```go +r.HandleFunc("/products", ProductsHandler). + Host("www.example.com"). + Methods("GET"). + Schemes("http") +``` + +Routes are tested in the order they were added to the router. If two routes match, the first one wins: + +```go +r := mux.NewRouter() +r.HandleFunc("/specific", specificHandler) +r.PathPrefix("/").Handler(catchAllHandler) +``` + +Setting the same matching conditions again and again can be boring, so we have a way to group several routes that share the same requirements. We call it "subrouting". + +For example, let's say we have several URLs that should only match when the host is `www.example.com`. Create a route for that host and get a "subrouter" from it: + +```go +r := mux.NewRouter() +s := r.Host("www.example.com").Subrouter() +``` + +Then register routes in the subrouter: + +```go +s.HandleFunc("/products/", ProductsHandler) +s.HandleFunc("/products/{key}", ProductHandler) +s.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) +``` + +The three URL paths we registered above will only be tested if the domain is `www.example.com`, because the subrouter is tested first. This is not only convenient, but also optimizes request matching. You can create subrouters combining any attribute matchers accepted by a route. + +Subrouters can be used to create domain or path "namespaces": you define subrouters in a central place and then parts of the app can register its paths relatively to a given subrouter. + +There's one more thing about subroutes. When a subrouter has a path prefix, the inner routes use it as base for their paths: + +```go +r := mux.NewRouter() +s := r.PathPrefix("/products").Subrouter() +// "/products/" +s.HandleFunc("/", ProductsHandler) +// "/products/{key}/" +s.HandleFunc("/{key}/", ProductHandler) +// "/products/{key}/details" +s.HandleFunc("/{key}/details", ProductDetailsHandler) +``` + + +### Static Files + +Note that the path provided to `PathPrefix()` represents a "wildcard": calling +`PathPrefix("/static/").Handler(...)` means that the handler will be passed any +request that matches "/static/\*". This makes it easy to serve static files with mux: + +```go +func main() { + var dir string + + flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir") + flag.Parse() + r := mux.NewRouter() + + // This will serve files under http://localhost:8000/static/<filename> + r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))) + + srv := &http.Server{ + Handler: r, + Addr: "127.0.0.1:8000", + // Good practice: enforce timeouts for servers you create! + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + + log.Fatal(srv.ListenAndServe()) +} +``` + +### Serving Single Page Applications + +Most of the time it makes sense to serve your SPA on a separate web server from your API, +but sometimes it's desirable to serve them both from one place. It's possible to write a simple +handler for serving your SPA (for use with React Router's [BrowserRouter](https://reacttraining.com/react-router/web/api/BrowserRouter) for example), and leverage +mux's powerful routing for your API endpoints. + +```go +package main + +import ( + "encoding/json" + "log" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/gorilla/mux" +) + +// spaHandler implements the http.Handler interface, so we can use it +// to respond to HTTP requests. The path to the static directory and +// path to the index file within that static directory are used to +// serve the SPA in the given static directory. +type spaHandler struct { + staticPath string + indexPath string +} + +// ServeHTTP inspects the URL path to locate a file within the static dir +// on the SPA handler. If a file is found, it will be served. If not, the +// file located at the index path on the SPA handler will be served. This +// is suitable behavior for serving an SPA (single page application). +func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // get the absolute path to prevent directory traversal + path, err := filepath.Abs(r.URL.Path) + if err != nil { + // if we failed to get the absolute path respond with a 400 bad request + // and stop + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // prepend the path with the path to the static directory + path = filepath.Join(h.staticPath, path) + + // check whether a file exists at the given path + _, err = os.Stat(path) + if os.IsNotExist(err) { + // file does not exist, serve index.html + http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath)) + return + } else if err != nil { + // if we got an error (that wasn't that the file doesn't exist) stating the + // file, return a 500 internal server error and stop + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // otherwise, use http.FileServer to serve the static dir + http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r) +} + +func main() { + router := mux.NewRouter() + + router.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) { + // an example API handler + json.NewEncoder(w).Encode(map[string]bool{"ok": true}) + }) + + spa := spaHandler{staticPath: "build", indexPath: "index.html"} + router.PathPrefix("/").Handler(spa) + + srv := &http.Server{ + Handler: router, + Addr: "127.0.0.1:8000", + // Good practice: enforce timeouts for servers you create! + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + + log.Fatal(srv.ListenAndServe()) +} +``` + +### Registered URLs + +Now let's see how to build registered URLs. + +Routes can be named. All routes that define a name can have their URLs built, or "reversed". We define a name calling `Name()` on a route. For example: + +```go +r := mux.NewRouter() +r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). + Name("article") +``` + +To build a URL, get the route and call the `URL()` method, passing a sequence of key/value pairs for the route variables. For the previous route, we would do: + +```go +url, err := r.Get("article").URL("category", "technology", "id", "42") +``` + +...and the result will be a `url.URL` with the following path: + +``` +"/articles/technology/42" +``` + +This also works for host and query value variables: + +```go +r := mux.NewRouter() +r.Host("{subdomain}.example.com"). + Path("/articles/{category}/{id:[0-9]+}"). + Queries("filter", "{filter}"). + HandlerFunc(ArticleHandler). + Name("article") + +// url.String() will be "http://news.example.com/articles/technology/42?filter=gorilla" +url, err := r.Get("article").URL("subdomain", "news", + "category", "technology", + "id", "42", + "filter", "gorilla") +``` + +All variables defined in the route are required, and their values must conform to the corresponding patterns. These requirements guarantee that a generated URL will always match a registered route -- the only exception is for explicitly defined "build-only" routes which never match. + +Regex support also exists for matching Headers within a route. For example, we could do: + +```go +r.HeadersRegexp("Content-Type", "application/(text|json)") +``` + +...and the route will match both requests with a Content-Type of `application/json` as well as `application/text` + +There's also a way to build only the URL host or path for a route: use the methods `URLHost()` or `URLPath()` instead. For the previous route, we would do: + +```go +// "http://news.example.com/" +host, err := r.Get("article").URLHost("subdomain", "news") + +// "/articles/technology/42" +path, err := r.Get("article").URLPath("category", "technology", "id", "42") +``` + +And if you use subrouters, host and path defined separately can be built as well: + +```go +r := mux.NewRouter() +s := r.Host("{subdomain}.example.com").Subrouter() +s.Path("/articles/{category}/{id:[0-9]+}"). + HandlerFunc(ArticleHandler). + Name("article") + +// "http://news.example.com/articles/technology/42" +url, err := r.Get("article").URL("subdomain", "news", + "category", "technology", + "id", "42") +``` + +### Walking Routes + +The `Walk` function on `mux.Router` can be used to visit all of the routes that are registered on a router. For example, +the following prints all of the registered routes: + +```go +package main + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gorilla/mux" +) + +func handler(w http.ResponseWriter, r *http.Request) { + return +} + +func main() { + r := mux.NewRouter() + r.HandleFunc("/", handler) + r.HandleFunc("/products", handler).Methods("POST") + r.HandleFunc("/articles", handler).Methods("GET") + r.HandleFunc("/articles/{id}", handler).Methods("GET", "PUT") + r.HandleFunc("/authors", handler).Queries("surname", "{surname}") + err := r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { + pathTemplate, err := route.GetPathTemplate() + if err == nil { + fmt.Println("ROUTE:", pathTemplate) + } + pathRegexp, err := route.GetPathRegexp() + if err == nil { + fmt.Println("Path regexp:", pathRegexp) + } + queriesTemplates, err := route.GetQueriesTemplates() + if err == nil { + fmt.Println("Queries templates:", strings.Join(queriesTemplates, ",")) + } + queriesRegexps, err := route.GetQueriesRegexp() + if err == nil { + fmt.Println("Queries regexps:", strings.Join(queriesRegexps, ",")) + } + methods, err := route.GetMethods() + if err == nil { + fmt.Println("Methods:", strings.Join(methods, ",")) + } + fmt.Println() + return nil + }) + + if err != nil { + fmt.Println(err) + } + + http.Handle("/", r) +} +``` + +### Graceful Shutdown + +Go 1.8 introduced the ability to [gracefully shutdown](https://golang.org/doc/go1.8#http_shutdown) a `*http.Server`. Here's how to do that alongside `mux`: + +```go +package main + +import ( + "context" + "flag" + "log" + "net/http" + "os" + "os/signal" + "time" + + "github.com/gorilla/mux" +) + +func main() { + var wait time.Duration + flag.DurationVar(&wait, "graceful-timeout", time.Second * 15, "the duration for which the server gracefully wait for existing connections to finish - e.g. 15s or 1m") + flag.Parse() + + r := mux.NewRouter() + // Add your routes as needed + + srv := &http.Server{ + Addr: "0.0.0.0:8080", + // Good practice to set timeouts to avoid Slowloris attacks. + WriteTimeout: time.Second * 15, + ReadTimeout: time.Second * 15, + IdleTimeout: time.Second * 60, + Handler: r, // Pass our instance of gorilla/mux in. + } + + // Run our server in a goroutine so that it doesn't block. + go func() { + if err := srv.ListenAndServe(); err != nil { + log.Println(err) + } + }() + + c := make(chan os.Signal, 1) + // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C) + // SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught. + signal.Notify(c, os.Interrupt) + + // Block until we receive our signal. + <-c + + // Create a deadline to wait for. + ctx, cancel := context.WithTimeout(context.Background(), wait) + defer cancel() + // Doesn't block if no connections, but will otherwise wait + // until the timeout deadline. + srv.Shutdown(ctx) + // Optionally, you could run srv.Shutdown in a goroutine and block on + // <-ctx.Done() if your application should wait for other services + // to finalize based on context cancellation. + log.Println("shutting down") + os.Exit(0) +} +``` + +### Middleware + +Mux supports the addition of middlewares to a [Router](https://godoc.org/github.com/gorilla/mux#Router), which are executed in the order they are added if a match is found, including its subrouters. +Middlewares are (typically) small pieces of code which take one request, do something with it, and pass it down to another middleware or the final handler. Some common use cases for middleware are request logging, header manipulation, or `ResponseWriter` hijacking. + +Mux middlewares are defined using the de facto standard type: + +```go +type MiddlewareFunc func(http.Handler) http.Handler +``` + +Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed to it, and then calls the handler passed as parameter to the MiddlewareFunc. This takes advantage of closures being able access variables from the context where they are created, while retaining the signature enforced by the receivers. + +A very basic middleware which logs the URI of the request being handled could be written as: + +```go +func loggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Do stuff here + log.Println(r.RequestURI) + // Call the next handler, which can be another middleware in the chain, or the final handler. + next.ServeHTTP(w, r) + }) +} +``` + +Middlewares can be added to a router using `Router.Use()`: + +```go +r := mux.NewRouter() +r.HandleFunc("/", handler) +r.Use(loggingMiddleware) +``` + +A more complex authentication middleware, which maps session token to users, could be written as: + +```go +// Define our struct +type authenticationMiddleware struct { + tokenUsers map[string]string +} + +// Initialize it somewhere +func (amw *authenticationMiddleware) Populate() { + amw.tokenUsers["00000000"] = "user0" + amw.tokenUsers["aaaaaaaa"] = "userA" + amw.tokenUsers["05f717e5"] = "randomUser" + amw.tokenUsers["deadbeef"] = "user0" +} + +// Middleware function, which will be called for each request +func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("X-Session-Token") + + if user, found := amw.tokenUsers[token]; found { + // We found the token in our map + log.Printf("Authenticated user %s\n", user) + // Pass down the request to the next middleware (or final handler) + next.ServeHTTP(w, r) + } else { + // Write an error and stop the handler chain + http.Error(w, "Forbidden", http.StatusForbidden) + } + }) +} +``` + +```go +r := mux.NewRouter() +r.HandleFunc("/", handler) + +amw := authenticationMiddleware{} +amw.Populate() + +r.Use(amw.Middleware) +``` + +Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. Middlewares _should_ write to `ResponseWriter` if they _are_ going to terminate the request, and they _should not_ write to `ResponseWriter` if they _are not_ going to terminate it. + +### Handling CORS Requests + +[CORSMethodMiddleware](https://godoc.org/github.com/gorilla/mux#CORSMethodMiddleware) intends to make it easier to strictly set the `Access-Control-Allow-Methods` response header. + +* You will still need to use your own CORS handler to set the other CORS headers such as `Access-Control-Allow-Origin` +* The middleware will set the `Access-Control-Allow-Methods` header to all the method matchers (e.g. `r.Methods(http.MethodGet, http.MethodPut, http.MethodOptions)` -> `Access-Control-Allow-Methods: GET,PUT,OPTIONS`) on a route +* If you do not specify any methods, then: +> _Important_: there must be an `OPTIONS` method matcher for the middleware to set the headers. + +Here is an example of using `CORSMethodMiddleware` along with a custom `OPTIONS` handler to set all the required CORS headers: + +```go +package main + +import ( + "net/http" + "github.com/gorilla/mux" +) + +func main() { + r := mux.NewRouter() + + // IMPORTANT: you must specify an OPTIONS method matcher for the middleware to set CORS headers + r.HandleFunc("/foo", fooHandler).Methods(http.MethodGet, http.MethodPut, http.MethodPatch, http.MethodOptions) + r.Use(mux.CORSMethodMiddleware(r)) + + http.ListenAndServe(":8080", r) +} + +func fooHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + if r.Method == http.MethodOptions { + return + } + + w.Write([]byte("foo")) +} +``` + +And an request to `/foo` using something like: + +```bash +curl localhost:8080/foo -v +``` + +Would look like: + +```bash +* Trying ::1... +* TCP_NODELAY set +* Connected to localhost (::1) port 8080 (#0) +> GET /foo HTTP/1.1 +> Host: localhost:8080 +> User-Agent: curl/7.59.0 +> Accept: */* +> +< HTTP/1.1 200 OK +< Access-Control-Allow-Methods: GET,PUT,PATCH,OPTIONS +< Access-Control-Allow-Origin: * +< Date: Fri, 28 Jun 2019 20:13:30 GMT +< Content-Length: 3 +< Content-Type: text/plain; charset=utf-8 +< +* Connection #0 to host localhost left intact +foo +``` + +### Testing Handlers + +Testing handlers in a Go web application is straightforward, and _mux_ doesn't complicate this any further. Given two files: `endpoints.go` and `endpoints_test.go`, here's how we'd test an application using _mux_. + +First, our simple HTTP handler: + +```go +// endpoints.go +package main + +func HealthCheckHandler(w http.ResponseWriter, r *http.Request) { + // A very simple health check. + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + // In the future we could report back on the status of our DB, or our cache + // (e.g. Redis) by performing a simple PING, and include them in the response. + io.WriteString(w, `{"alive": true}`) +} + +func main() { + r := mux.NewRouter() + r.HandleFunc("/health", HealthCheckHandler) + + log.Fatal(http.ListenAndServe("localhost:8080", r)) +} +``` + +Our test code: + +```go +// endpoints_test.go +package main + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestHealthCheckHandler(t *testing.T) { + // Create a request to pass to our handler. We don't have any query parameters for now, so we'll + // pass 'nil' as the third parameter. + req, err := http.NewRequest("GET", "/health", nil) + if err != nil { + t.Fatal(err) + } + + // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. + rr := httptest.NewRecorder() + handler := http.HandlerFunc(HealthCheckHandler) + + // Our handlers satisfy http.Handler, so we can call their ServeHTTP method + // directly and pass in our Request and ResponseRecorder. + handler.ServeHTTP(rr, req) + + // Check the status code is what we expect. + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + // Check the response body is what we expect. + expected := `{"alive": true}` + if rr.Body.String() != expected { + t.Errorf("handler returned unexpected body: got %v want %v", + rr.Body.String(), expected) + } +} +``` + +In the case that our routes have [variables](#examples), we can pass those in the request. We could write +[table-driven tests](https://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go) to test multiple +possible route variables as needed. + +```go +// endpoints.go +func main() { + r := mux.NewRouter() + // A route with a route variable: + r.HandleFunc("/metrics/{type}", MetricsHandler) + + log.Fatal(http.ListenAndServe("localhost:8080", r)) +} +``` + +Our test file, with a table-driven test of `routeVariables`: + +```go +// endpoints_test.go +func TestMetricsHandler(t *testing.T) { + tt := []struct{ + routeVariable string + shouldPass bool + }{ + {"goroutines", true}, + {"heap", true}, + {"counters", true}, + {"queries", true}, + {"adhadaeqm3k", false}, + } + + for _, tc := range tt { + path := fmt.Sprintf("/metrics/%s", tc.routeVariable) + req, err := http.NewRequest("GET", path, nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + + // Need to create a router that we can pass the request through so that the vars will be added to the context + router := mux.NewRouter() + router.HandleFunc("/metrics/{type}", MetricsHandler) + router.ServeHTTP(rr, req) + + // In this case, our MetricsHandler returns a non-200 response + // for a route variable it doesn't know about. + if rr.Code == http.StatusOK && !tc.shouldPass { + t.Errorf("handler should have failed on routeVariable %s: got %v want %v", + tc.routeVariable, rr.Code, http.StatusOK) + } + } +} +``` + +## Full Example + +Here's a complete, runnable example of a small `mux` based server: + +```go +package main + +import ( + "net/http" + "log" + "github.com/gorilla/mux" +) + +func YourHandler(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Gorilla!\n")) +} + +func main() { + r := mux.NewRouter() + // Routes consist of a path and a handler function. + r.HandleFunc("/", YourHandler) + + // Bind to a port and pass our router in + log.Fatal(http.ListenAndServe(":8000", r)) +} +``` + +## License + +BSD licensed. See the LICENSE file for details. diff --git a/vendor/github.com/gorilla/mux/doc.go b/vendor/github.com/gorilla/mux/doc.go new file mode 100644 index 00000000..bd5a38b5 --- /dev/null +++ b/vendor/github.com/gorilla/mux/doc.go @@ -0,0 +1,306 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package mux implements a request router and dispatcher. + +The name mux stands for "HTTP request multiplexer". Like the standard +http.ServeMux, mux.Router matches incoming requests against a list of +registered routes and calls a handler for the route that matches the URL +or other conditions. The main features are: + + * Requests can be matched based on URL host, path, path prefix, schemes, + header and query values, HTTP methods or using custom matchers. + * URL hosts, paths and query values can have variables with an optional + regular expression. + * Registered URLs can be built, or "reversed", which helps maintaining + references to resources. + * Routes can be used as subrouters: nested routes are only tested if the + parent route matches. This is useful to define groups of routes that + share common conditions like a host, a path prefix or other repeated + attributes. As a bonus, this optimizes request matching. + * It implements the http.Handler interface so it is compatible with the + standard http.ServeMux. + +Let's start registering a couple of URL paths and handlers: + + func main() { + r := mux.NewRouter() + r.HandleFunc("/", HomeHandler) + r.HandleFunc("/products", ProductsHandler) + r.HandleFunc("/articles", ArticlesHandler) + http.Handle("/", r) + } + +Here we register three routes mapping URL paths to handlers. This is +equivalent to how http.HandleFunc() works: if an incoming request URL matches +one of the paths, the corresponding handler is called passing +(http.ResponseWriter, *http.Request) as parameters. + +Paths can have variables. They are defined using the format {name} or +{name:pattern}. If a regular expression pattern is not defined, the matched +variable will be anything until the next slash. For example: + + r := mux.NewRouter() + r.HandleFunc("/products/{key}", ProductHandler) + r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) + r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) + +Groups can be used inside patterns, as long as they are non-capturing (?:re). For example: + + r.HandleFunc("/articles/{category}/{sort:(?:asc|desc|new)}", ArticlesCategoryHandler) + +The names are used to create a map of route variables which can be retrieved +calling mux.Vars(): + + vars := mux.Vars(request) + category := vars["category"] + +Note that if any capturing groups are present, mux will panic() during parsing. To prevent +this, convert any capturing groups to non-capturing, e.g. change "/{sort:(asc|desc)}" to +"/{sort:(?:asc|desc)}". This is a change from prior versions which behaved unpredictably +when capturing groups were present. + +And this is all you need to know about the basic usage. More advanced options +are explained below. + +Routes can also be restricted to a domain or subdomain. Just define a host +pattern to be matched. They can also have variables: + + r := mux.NewRouter() + // Only matches if domain is "www.example.com". + r.Host("www.example.com") + // Matches a dynamic subdomain. + r.Host("{subdomain:[a-z]+}.domain.com") + +There are several other matchers that can be added. To match path prefixes: + + r.PathPrefix("/products/") + +...or HTTP methods: + + r.Methods("GET", "POST") + +...or URL schemes: + + r.Schemes("https") + +...or header values: + + r.Headers("X-Requested-With", "XMLHttpRequest") + +...or query values: + + r.Queries("key", "value") + +...or to use a custom matcher function: + + r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { + return r.ProtoMajor == 0 + }) + +...and finally, it is possible to combine several matchers in a single route: + + r.HandleFunc("/products", ProductsHandler). + Host("www.example.com"). + Methods("GET"). + Schemes("http") + +Setting the same matching conditions again and again can be boring, so we have +a way to group several routes that share the same requirements. +We call it "subrouting". + +For example, let's say we have several URLs that should only match when the +host is "www.example.com". Create a route for that host and get a "subrouter" +from it: + + r := mux.NewRouter() + s := r.Host("www.example.com").Subrouter() + +Then register routes in the subrouter: + + s.HandleFunc("/products/", ProductsHandler) + s.HandleFunc("/products/{key}", ProductHandler) + s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) + +The three URL paths we registered above will only be tested if the domain is +"www.example.com", because the subrouter is tested first. This is not +only convenient, but also optimizes request matching. You can create +subrouters combining any attribute matchers accepted by a route. + +Subrouters can be used to create domain or path "namespaces": you define +subrouters in a central place and then parts of the app can register its +paths relatively to a given subrouter. + +There's one more thing about subroutes. When a subrouter has a path prefix, +the inner routes use it as base for their paths: + + r := mux.NewRouter() + s := r.PathPrefix("/products").Subrouter() + // "/products/" + s.HandleFunc("/", ProductsHandler) + // "/products/{key}/" + s.HandleFunc("/{key}/", ProductHandler) + // "/products/{key}/details" + s.HandleFunc("/{key}/details", ProductDetailsHandler) + +Note that the path provided to PathPrefix() represents a "wildcard": calling +PathPrefix("/static/").Handler(...) means that the handler will be passed any +request that matches "/static/*". This makes it easy to serve static files with mux: + + func main() { + var dir string + + flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir") + flag.Parse() + r := mux.NewRouter() + + // This will serve files under http://localhost:8000/static/<filename> + r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))) + + srv := &http.Server{ + Handler: r, + Addr: "127.0.0.1:8000", + // Good practice: enforce timeouts for servers you create! + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + + log.Fatal(srv.ListenAndServe()) + } + +Now let's see how to build registered URLs. + +Routes can be named. All routes that define a name can have their URLs built, +or "reversed". We define a name calling Name() on a route. For example: + + r := mux.NewRouter() + r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). + Name("article") + +To build a URL, get the route and call the URL() method, passing a sequence of +key/value pairs for the route variables. For the previous route, we would do: + + url, err := r.Get("article").URL("category", "technology", "id", "42") + +...and the result will be a url.URL with the following path: + + "/articles/technology/42" + +This also works for host and query value variables: + + r := mux.NewRouter() + r.Host("{subdomain}.domain.com"). + Path("/articles/{category}/{id:[0-9]+}"). + Queries("filter", "{filter}"). + HandlerFunc(ArticleHandler). + Name("article") + + // url.String() will be "http://news.domain.com/articles/technology/42?filter=gorilla" + url, err := r.Get("article").URL("subdomain", "news", + "category", "technology", + "id", "42", + "filter", "gorilla") + +All variables defined in the route are required, and their values must +conform to the corresponding patterns. These requirements guarantee that a +generated URL will always match a registered route -- the only exception is +for explicitly defined "build-only" routes which never match. + +Regex support also exists for matching Headers within a route. For example, we could do: + + r.HeadersRegexp("Content-Type", "application/(text|json)") + +...and the route will match both requests with a Content-Type of `application/json` as well as +`application/text` + +There's also a way to build only the URL host or path for a route: +use the methods URLHost() or URLPath() instead. For the previous route, +we would do: + + // "http://news.domain.com/" + host, err := r.Get("article").URLHost("subdomain", "news") + + // "/articles/technology/42" + path, err := r.Get("article").URLPath("category", "technology", "id", "42") + +And if you use subrouters, host and path defined separately can be built +as well: + + r := mux.NewRouter() + s := r.Host("{subdomain}.domain.com").Subrouter() + s.Path("/articles/{category}/{id:[0-9]+}"). + HandlerFunc(ArticleHandler). + Name("article") + + // "http://news.domain.com/articles/technology/42" + url, err := r.Get("article").URL("subdomain", "news", + "category", "technology", + "id", "42") + +Mux supports the addition of middlewares to a Router, which are executed in the order they are added if a match is found, including its subrouters. Middlewares are (typically) small pieces of code which take one request, do something with it, and pass it down to another middleware or the final handler. Some common use cases for middleware are request logging, header manipulation, or ResponseWriter hijacking. + + type MiddlewareFunc func(http.Handler) http.Handler + +Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed to it, and then calls the handler passed as parameter to the MiddlewareFunc (closures can access variables from the context where they are created). + +A very basic middleware which logs the URI of the request being handled could be written as: + + func simpleMw(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Do stuff here + log.Println(r.RequestURI) + // Call the next handler, which can be another middleware in the chain, or the final handler. + next.ServeHTTP(w, r) + }) + } + +Middlewares can be added to a router using `Router.Use()`: + + r := mux.NewRouter() + r.HandleFunc("/", handler) + r.Use(simpleMw) + +A more complex authentication middleware, which maps session token to users, could be written as: + + // Define our struct + type authenticationMiddleware struct { + tokenUsers map[string]string + } + + // Initialize it somewhere + func (amw *authenticationMiddleware) Populate() { + amw.tokenUsers["00000000"] = "user0" + amw.tokenUsers["aaaaaaaa"] = "userA" + amw.tokenUsers["05f717e5"] = "randomUser" + amw.tokenUsers["deadbeef"] = "user0" + } + + // Middleware function, which will be called for each request + func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("X-Session-Token") + + if user, found := amw.tokenUsers[token]; found { + // We found the token in our map + log.Printf("Authenticated user %s\n", user) + next.ServeHTTP(w, r) + } else { + http.Error(w, "Forbidden", http.StatusForbidden) + } + }) + } + + r := mux.NewRouter() + r.HandleFunc("/", handler) + + amw := authenticationMiddleware{tokenUsers: make(map[string]string)} + amw.Populate() + + r.Use(amw.Middleware) + +Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. + +*/ +package mux diff --git a/vendor/github.com/gorilla/mux/middleware.go b/vendor/github.com/gorilla/mux/middleware.go new file mode 100644 index 00000000..cb51c565 --- /dev/null +++ b/vendor/github.com/gorilla/mux/middleware.go @@ -0,0 +1,74 @@ +package mux + +import ( + "net/http" + "strings" +) + +// MiddlewareFunc is a function which receives an http.Handler and returns another http.Handler. +// Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed +// to it, and then calls the handler passed as parameter to the MiddlewareFunc. +type MiddlewareFunc func(http.Handler) http.Handler + +// middleware interface is anything which implements a MiddlewareFunc named Middleware. +type middleware interface { + Middleware(handler http.Handler) http.Handler +} + +// Middleware allows MiddlewareFunc to implement the middleware interface. +func (mw MiddlewareFunc) Middleware(handler http.Handler) http.Handler { + return mw(handler) +} + +// Use appends a MiddlewareFunc to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router. +func (r *Router) Use(mwf ...MiddlewareFunc) { + for _, fn := range mwf { + r.middlewares = append(r.middlewares, fn) + } +} + +// useInterface appends a middleware to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router. +func (r *Router) useInterface(mw middleware) { + r.middlewares = append(r.middlewares, mw) +} + +// CORSMethodMiddleware automatically sets the Access-Control-Allow-Methods response header +// on requests for routes that have an OPTIONS method matcher to all the method matchers on +// the route. Routes that do not explicitly handle OPTIONS requests will not be processed +// by the middleware. See examples for usage. +func CORSMethodMiddleware(r *Router) MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + allMethods, err := getAllMethodsForRoute(r, req) + if err == nil { + for _, v := range allMethods { + if v == http.MethodOptions { + w.Header().Set("Access-Control-Allow-Methods", strings.Join(allMethods, ",")) + } + } + } + + next.ServeHTTP(w, req) + }) + } +} + +// getAllMethodsForRoute returns all the methods from method matchers matching a given +// request. +func getAllMethodsForRoute(r *Router, req *http.Request) ([]string, error) { + var allMethods []string + + for _, route := range r.routes { + var match RouteMatch + if route.Match(req, &match) || match.MatchErr == ErrMethodMismatch { + methods, err := route.GetMethods() + if err != nil { + return nil, err + } + + allMethods = append(allMethods, methods...) + } + } + + return allMethods, nil +} diff --git a/vendor/github.com/gorilla/mux/mux.go b/vendor/github.com/gorilla/mux/mux.go new file mode 100644 index 00000000..782a34b2 --- /dev/null +++ b/vendor/github.com/gorilla/mux/mux.go @@ -0,0 +1,606 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mux + +import ( + "context" + "errors" + "fmt" + "net/http" + "path" + "regexp" +) + +var ( + // ErrMethodMismatch is returned when the method in the request does not match + // the method defined against the route. + ErrMethodMismatch = errors.New("method is not allowed") + // ErrNotFound is returned when no route match is found. + ErrNotFound = errors.New("no matching route was found") +) + +// NewRouter returns a new router instance. +func NewRouter() *Router { + return &Router{namedRoutes: make(map[string]*Route)} +} + +// Router registers routes to be matched and dispatches a handler. +// +// It implements the http.Handler interface, so it can be registered to serve +// requests: +// +// var router = mux.NewRouter() +// +// func main() { +// http.Handle("/", router) +// } +// +// Or, for Google App Engine, register it in a init() function: +// +// func init() { +// http.Handle("/", router) +// } +// +// This will send all incoming requests to the router. +type Router struct { + // Configurable Handler to be used when no route matches. + NotFoundHandler http.Handler + + // Configurable Handler to be used when the request method does not match the route. + MethodNotAllowedHandler http.Handler + + // Routes to be matched, in order. + routes []*Route + + // Routes by name for URL building. + namedRoutes map[string]*Route + + // If true, do not clear the request context after handling the request. + // + // Deprecated: No effect, since the context is stored on the request itself. + KeepContext bool + + // Slice of middlewares to be called after a match is found + middlewares []middleware + + // configuration shared with `Route` + routeConf +} + +// common route configuration shared between `Router` and `Route` +type routeConf struct { + // If true, "/path/foo%2Fbar/to" will match the path "/path/{var}/to" + useEncodedPath bool + + // If true, when the path pattern is "/path/", accessing "/path" will + // redirect to the former and vice versa. + strictSlash bool + + // If true, when the path pattern is "/path//to", accessing "/path//to" + // will not redirect + skipClean bool + + // Manager for the variables from host and path. + regexp routeRegexpGroup + + // List of matchers. + matchers []matcher + + // The scheme used when building URLs. + buildScheme string + + buildVarsFunc BuildVarsFunc +} + +// returns an effective deep copy of `routeConf` +func copyRouteConf(r routeConf) routeConf { + c := r + + if r.regexp.path != nil { + c.regexp.path = copyRouteRegexp(r.regexp.path) + } + + if r.regexp.host != nil { + c.regexp.host = copyRouteRegexp(r.regexp.host) + } + + c.regexp.queries = make([]*routeRegexp, 0, len(r.regexp.queries)) + for _, q := range r.regexp.queries { + c.regexp.queries = append(c.regexp.queries, copyRouteRegexp(q)) + } + + c.matchers = make([]matcher, len(r.matchers)) + copy(c.matchers, r.matchers) + + return c +} + +func copyRouteRegexp(r *routeRegexp) *routeRegexp { + c := *r + return &c +} + +// Match attempts to match the given request against the router's registered routes. +// +// If the request matches a route of this router or one of its subrouters the Route, +// Handler, and Vars fields of the the match argument are filled and this function +// returns true. +// +// If the request does not match any of this router's or its subrouters' routes +// then this function returns false. If available, a reason for the match failure +// will be filled in the match argument's MatchErr field. If the match failure type +// (eg: not found) has a registered handler, the handler is assigned to the Handler +// field of the match argument. +func (r *Router) Match(req *http.Request, match *RouteMatch) bool { + for _, route := range r.routes { + if route.Match(req, match) { + // Build middleware chain if no error was found + if match.MatchErr == nil { + for i := len(r.middlewares) - 1; i >= 0; i-- { + match.Handler = r.middlewares[i].Middleware(match.Handler) + } + } + return true + } + } + + if match.MatchErr == ErrMethodMismatch { + if r.MethodNotAllowedHandler != nil { + match.Handler = r.MethodNotAllowedHandler + return true + } + + return false + } + + // Closest match for a router (includes sub-routers) + if r.NotFoundHandler != nil { + match.Handler = r.NotFoundHandler + match.MatchErr = ErrNotFound + return true + } + + match.MatchErr = ErrNotFound + return false +} + +// ServeHTTP dispatches the handler registered in the matched route. +// +// When there is a match, the route variables can be retrieved calling +// mux.Vars(request). +func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if !r.skipClean { + path := req.URL.Path + if r.useEncodedPath { + path = req.URL.EscapedPath() + } + // Clean path to canonical form and redirect. + if p := cleanPath(path); p != path { + + // Added 3 lines (Philip Schlump) - It was dropping the query string and #whatever from query. + // This matches with fix in go 1.2 r.c. 4 for same problem. Go Issue: + // http://code.google.com/p/go/issues/detail?id=5252 + url := *req.URL + url.Path = p + p = url.String() + + w.Header().Set("Location", p) + w.WriteHeader(http.StatusMovedPermanently) + return + } + } + var match RouteMatch + var handler http.Handler + if r.Match(req, &match) { + handler = match.Handler + req = requestWithVars(req, match.Vars) + req = requestWithRoute(req, match.Route) + } + + if handler == nil && match.MatchErr == ErrMethodMismatch { + handler = methodNotAllowedHandler() + } + + if handler == nil { + handler = http.NotFoundHandler() + } + + handler.ServeHTTP(w, req) +} + +// Get returns a route registered with the given name. +func (r *Router) Get(name string) *Route { + return r.namedRoutes[name] +} + +// GetRoute returns a route registered with the given name. This method +// was renamed to Get() and remains here for backwards compatibility. +func (r *Router) GetRoute(name string) *Route { + return r.namedRoutes[name] +} + +// StrictSlash defines the trailing slash behavior for new routes. The initial +// value is false. +// +// When true, if the route path is "/path/", accessing "/path" will perform a redirect +// to the former and vice versa. In other words, your application will always +// see the path as specified in the route. +// +// When false, if the route path is "/path", accessing "/path/" will not match +// this route and vice versa. +// +// The re-direct is a HTTP 301 (Moved Permanently). Note that when this is set for +// routes with a non-idempotent method (e.g. POST, PUT), the subsequent re-directed +// request will be made as a GET by most clients. Use middleware or client settings +// to modify this behaviour as needed. +// +// Special case: when a route sets a path prefix using the PathPrefix() method, +// strict slash is ignored for that route because the redirect behavior can't +// be determined from a prefix alone. However, any subrouters created from that +// route inherit the original StrictSlash setting. +func (r *Router) StrictSlash(value bool) *Router { + r.strictSlash = value + return r +} + +// SkipClean defines the path cleaning behaviour for new routes. The initial +// value is false. Users should be careful about which routes are not cleaned +// +// When true, if the route path is "/path//to", it will remain with the double +// slash. This is helpful if you have a route like: /fetch/http://xkcd.com/534/ +// +// When false, the path will be cleaned, so /fetch/http://xkcd.com/534/ will +// become /fetch/http/xkcd.com/534 +func (r *Router) SkipClean(value bool) *Router { + r.skipClean = value + return r +} + +// UseEncodedPath tells the router to match the encoded original path +// to the routes. +// For eg. "/path/foo%2Fbar/to" will match the path "/path/{var}/to". +// +// If not called, the router will match the unencoded path to the routes. +// For eg. "/path/foo%2Fbar/to" will match the path "/path/foo/bar/to" +func (r *Router) UseEncodedPath() *Router { + r.useEncodedPath = true + return r +} + +// ---------------------------------------------------------------------------- +// Route factories +// ---------------------------------------------------------------------------- + +// NewRoute registers an empty route. +func (r *Router) NewRoute() *Route { + // initialize a route with a copy of the parent router's configuration + route := &Route{routeConf: copyRouteConf(r.routeConf), namedRoutes: r.namedRoutes} + r.routes = append(r.routes, route) + return route +} + +// Name registers a new route with a name. +// See Route.Name(). +func (r *Router) Name(name string) *Route { + return r.NewRoute().Name(name) +} + +// Handle registers a new route with a matcher for the URL path. +// See Route.Path() and Route.Handler(). +func (r *Router) Handle(path string, handler http.Handler) *Route { + return r.NewRoute().Path(path).Handler(handler) +} + +// HandleFunc registers a new route with a matcher for the URL path. +// See Route.Path() and Route.HandlerFunc(). +func (r *Router) HandleFunc(path string, f func(http.ResponseWriter, + *http.Request)) *Route { + return r.NewRoute().Path(path).HandlerFunc(f) +} + +// Headers registers a new route with a matcher for request header values. +// See Route.Headers(). +func (r *Router) Headers(pairs ...string) *Route { + return r.NewRoute().Headers(pairs...) +} + +// Host registers a new route with a matcher for the URL host. +// See Route.Host(). +func (r *Router) Host(tpl string) *Route { + return r.NewRoute().Host(tpl) +} + +// MatcherFunc registers a new route with a custom matcher function. +// See Route.MatcherFunc(). +func (r *Router) MatcherFunc(f MatcherFunc) *Route { + return r.NewRoute().MatcherFunc(f) +} + +// Methods registers a new route with a matcher for HTTP methods. +// See Route.Methods(). +func (r *Router) Methods(methods ...string) *Route { + return r.NewRoute().Methods(methods...) +} + +// Path registers a new route with a matcher for the URL path. +// See Route.Path(). +func (r *Router) Path(tpl string) *Route { + return r.NewRoute().Path(tpl) +} + +// PathPrefix registers a new route with a matcher for the URL path prefix. +// See Route.PathPrefix(). +func (r *Router) PathPrefix(tpl string) *Route { + return r.NewRoute().PathPrefix(tpl) +} + +// Queries registers a new route with a matcher for URL query values. +// See Route.Queries(). +func (r *Router) Queries(pairs ...string) *Route { + return r.NewRoute().Queries(pairs...) +} + +// Schemes registers a new route with a matcher for URL schemes. +// See Route.Schemes(). +func (r *Router) Schemes(schemes ...string) *Route { + return r.NewRoute().Schemes(schemes...) +} + +// BuildVarsFunc registers a new route with a custom function for modifying +// route variables before building a URL. +func (r *Router) BuildVarsFunc(f BuildVarsFunc) *Route { + return r.NewRoute().BuildVarsFunc(f) +} + +// Walk walks the router and all its sub-routers, calling walkFn for each route +// in the tree. The routes are walked in the order they were added. Sub-routers +// are explored depth-first. +func (r *Router) Walk(walkFn WalkFunc) error { + return r.walk(walkFn, []*Route{}) +} + +// SkipRouter is used as a return value from WalkFuncs to indicate that the +// router that walk is about to descend down to should be skipped. +var SkipRouter = errors.New("skip this router") + +// WalkFunc is the type of the function called for each route visited by Walk. +// At every invocation, it is given the current route, and the current router, +// and a list of ancestor routes that lead to the current route. +type WalkFunc func(route *Route, router *Router, ancestors []*Route) error + +func (r *Router) walk(walkFn WalkFunc, ancestors []*Route) error { + for _, t := range r.routes { + err := walkFn(t, r, ancestors) + if err == SkipRouter { + continue + } + if err != nil { + return err + } + for _, sr := range t.matchers { + if h, ok := sr.(*Router); ok { + ancestors = append(ancestors, t) + err := h.walk(walkFn, ancestors) + if err != nil { + return err + } + ancestors = ancestors[:len(ancestors)-1] + } + } + if h, ok := t.handler.(*Router); ok { + ancestors = append(ancestors, t) + err := h.walk(walkFn, ancestors) + if err != nil { + return err + } + ancestors = ancestors[:len(ancestors)-1] + } + } + return nil +} + +// ---------------------------------------------------------------------------- +// Context +// ---------------------------------------------------------------------------- + +// RouteMatch stores information about a matched route. +type RouteMatch struct { + Route *Route + Handler http.Handler + Vars map[string]string + + // MatchErr is set to appropriate matching error + // It is set to ErrMethodMismatch if there is a mismatch in + // the request method and route method + MatchErr error +} + +type contextKey int + +const ( + varsKey contextKey = iota + routeKey +) + +// Vars returns the route variables for the current request, if any. +func Vars(r *http.Request) map[string]string { + if rv := r.Context().Value(varsKey); rv != nil { + return rv.(map[string]string) + } + return nil +} + +// CurrentRoute returns the matched route for the current request, if any. +// This only works when called inside the handler of the matched route +// because the matched route is stored in the request context which is cleared +// after the handler returns. +func CurrentRoute(r *http.Request) *Route { + if rv := r.Context().Value(routeKey); rv != nil { + return rv.(*Route) + } + return nil +} + +func requestWithVars(r *http.Request, vars map[string]string) *http.Request { + ctx := context.WithValue(r.Context(), varsKey, vars) + return r.WithContext(ctx) +} + +func requestWithRoute(r *http.Request, route *Route) *http.Request { + ctx := context.WithValue(r.Context(), routeKey, route) + return r.WithContext(ctx) +} + +// ---------------------------------------------------------------------------- +// Helpers +// ---------------------------------------------------------------------------- + +// cleanPath returns the canonical path for p, eliminating . and .. elements. +// Borrowed from the net/http package. +func cleanPath(p string) string { + if p == "" { + return "/" + } + if p[0] != '/' { + p = "/" + p + } + np := path.Clean(p) + // path.Clean removes trailing slash except for root; + // put the trailing slash back if necessary. + if p[len(p)-1] == '/' && np != "/" { + np += "/" + } + + return np +} + +// uniqueVars returns an error if two slices contain duplicated strings. +func uniqueVars(s1, s2 []string) error { + for _, v1 := range s1 { + for _, v2 := range s2 { + if v1 == v2 { + return fmt.Errorf("mux: duplicated route variable %q", v2) + } + } + } + return nil +} + +// checkPairs returns the count of strings passed in, and an error if +// the count is not an even number. +func checkPairs(pairs ...string) (int, error) { + length := len(pairs) + if length%2 != 0 { + return length, fmt.Errorf( + "mux: number of parameters must be multiple of 2, got %v", pairs) + } + return length, nil +} + +// mapFromPairsToString converts variadic string parameters to a +// string to string map. +func mapFromPairsToString(pairs ...string) (map[string]string, error) { + length, err := checkPairs(pairs...) + if err != nil { + return nil, err + } + m := make(map[string]string, length/2) + for i := 0; i < length; i += 2 { + m[pairs[i]] = pairs[i+1] + } + return m, nil +} + +// mapFromPairsToRegex converts variadic string parameters to a +// string to regex map. +func mapFromPairsToRegex(pairs ...string) (map[string]*regexp.Regexp, error) { + length, err := checkPairs(pairs...) + if err != nil { + return nil, err + } + m := make(map[string]*regexp.Regexp, length/2) + for i := 0; i < length; i += 2 { + regex, err := regexp.Compile(pairs[i+1]) + if err != nil { + return nil, err + } + m[pairs[i]] = regex + } + return m, nil +} + +// matchInArray returns true if the given string value is in the array. +func matchInArray(arr []string, value string) bool { + for _, v := range arr { + if v == value { + return true + } + } + return false +} + +// matchMapWithString returns true if the given key/value pairs exist in a given map. +func matchMapWithString(toCheck map[string]string, toMatch map[string][]string, canonicalKey bool) bool { + for k, v := range toCheck { + // Check if key exists. + if canonicalKey { + k = http.CanonicalHeaderKey(k) + } + if values := toMatch[k]; values == nil { + return false + } else if v != "" { + // If value was defined as an empty string we only check that the + // key exists. Otherwise we also check for equality. + valueExists := false + for _, value := range values { + if v == value { + valueExists = true + break + } + } + if !valueExists { + return false + } + } + } + return true +} + +// matchMapWithRegex returns true if the given key/value pairs exist in a given map compiled against +// the given regex +func matchMapWithRegex(toCheck map[string]*regexp.Regexp, toMatch map[string][]string, canonicalKey bool) bool { + for k, v := range toCheck { + // Check if key exists. + if canonicalKey { + k = http.CanonicalHeaderKey(k) + } + if values := toMatch[k]; values == nil { + return false + } else if v != nil { + // If value was defined as an empty string we only check that the + // key exists. Otherwise we also check for equality. + valueExists := false + for _, value := range values { + if v.MatchString(value) { + valueExists = true + break + } + } + if !valueExists { + return false + } + } + } + return true +} + +// methodNotAllowed replies to the request with an HTTP status code 405. +func methodNotAllowed(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusMethodNotAllowed) +} + +// methodNotAllowedHandler returns a simple request handler +// that replies to each request with a status code 405. +func methodNotAllowedHandler() http.Handler { return http.HandlerFunc(methodNotAllowed) } diff --git a/vendor/github.com/gorilla/mux/regexp.go b/vendor/github.com/gorilla/mux/regexp.go new file mode 100644 index 00000000..0144842b --- /dev/null +++ b/vendor/github.com/gorilla/mux/regexp.go @@ -0,0 +1,388 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mux + +import ( + "bytes" + "fmt" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" +) + +type routeRegexpOptions struct { + strictSlash bool + useEncodedPath bool +} + +type regexpType int + +const ( + regexpTypePath regexpType = 0 + regexpTypeHost regexpType = 1 + regexpTypePrefix regexpType = 2 + regexpTypeQuery regexpType = 3 +) + +// newRouteRegexp parses a route template and returns a routeRegexp, +// used to match a host, a path or a query string. +// +// It will extract named variables, assemble a regexp to be matched, create +// a "reverse" template to build URLs and compile regexps to validate variable +// values used in URL building. +// +// Previously we accepted only Python-like identifiers for variable +// names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that +// name and pattern can't be empty, and names can't contain a colon. +func newRouteRegexp(tpl string, typ regexpType, options routeRegexpOptions) (*routeRegexp, error) { + // Check if it is well-formed. + idxs, errBraces := braceIndices(tpl) + if errBraces != nil { + return nil, errBraces + } + // Backup the original. + template := tpl + // Now let's parse it. + defaultPattern := "[^/]+" + if typ == regexpTypeQuery { + defaultPattern = ".*" + } else if typ == regexpTypeHost { + defaultPattern = "[^.]+" + } + // Only match strict slash if not matching + if typ != regexpTypePath { + options.strictSlash = false + } + // Set a flag for strictSlash. + endSlash := false + if options.strictSlash && strings.HasSuffix(tpl, "/") { + tpl = tpl[:len(tpl)-1] + endSlash = true + } + varsN := make([]string, len(idxs)/2) + varsR := make([]*regexp.Regexp, len(idxs)/2) + pattern := bytes.NewBufferString("") + pattern.WriteByte('^') + reverse := bytes.NewBufferString("") + var end int + var err error + for i := 0; i < len(idxs); i += 2 { + // Set all values we are interested in. + raw := tpl[end:idxs[i]] + end = idxs[i+1] + parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2) + name := parts[0] + patt := defaultPattern + if len(parts) == 2 { + patt = parts[1] + } + // Name or pattern can't be empty. + if name == "" || patt == "" { + return nil, fmt.Errorf("mux: missing name or pattern in %q", + tpl[idxs[i]:end]) + } + // Build the regexp pattern. + fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(i/2), patt) + + // Build the reverse template. + fmt.Fprintf(reverse, "%s%%s", raw) + + // Append variable name and compiled pattern. + varsN[i/2] = name + varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt)) + if err != nil { + return nil, err + } + } + // Add the remaining. + raw := tpl[end:] + pattern.WriteString(regexp.QuoteMeta(raw)) + if options.strictSlash { + pattern.WriteString("[/]?") + } + if typ == regexpTypeQuery { + // Add the default pattern if the query value is empty + if queryVal := strings.SplitN(template, "=", 2)[1]; queryVal == "" { + pattern.WriteString(defaultPattern) + } + } + if typ != regexpTypePrefix { + pattern.WriteByte('$') + } + + var wildcardHostPort bool + if typ == regexpTypeHost { + if !strings.Contains(pattern.String(), ":") { + wildcardHostPort = true + } + } + reverse.WriteString(raw) + if endSlash { + reverse.WriteByte('/') + } + // Compile full regexp. + reg, errCompile := regexp.Compile(pattern.String()) + if errCompile != nil { + return nil, errCompile + } + + // Check for capturing groups which used to work in older versions + if reg.NumSubexp() != len(idxs)/2 { + panic(fmt.Sprintf("route %s contains capture groups in its regexp. ", template) + + "Only non-capturing groups are accepted: e.g. (?:pattern) instead of (pattern)") + } + + // Done! + return &routeRegexp{ + template: template, + regexpType: typ, + options: options, + regexp: reg, + reverse: reverse.String(), + varsN: varsN, + varsR: varsR, + wildcardHostPort: wildcardHostPort, + }, nil +} + +// routeRegexp stores a regexp to match a host or path and information to +// collect and validate route variables. +type routeRegexp struct { + // The unmodified template. + template string + // The type of match + regexpType regexpType + // Options for matching + options routeRegexpOptions + // Expanded regexp. + regexp *regexp.Regexp + // Reverse template. + reverse string + // Variable names. + varsN []string + // Variable regexps (validators). + varsR []*regexp.Regexp + // Wildcard host-port (no strict port match in hostname) + wildcardHostPort bool +} + +// Match matches the regexp against the URL host or path. +func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { + if r.regexpType == regexpTypeHost { + host := getHost(req) + if r.wildcardHostPort { + // Don't be strict on the port match + if i := strings.Index(host, ":"); i != -1 { + host = host[:i] + } + } + return r.regexp.MatchString(host) + } + + if r.regexpType == regexpTypeQuery { + return r.matchQueryString(req) + } + path := req.URL.Path + if r.options.useEncodedPath { + path = req.URL.EscapedPath() + } + return r.regexp.MatchString(path) +} + +// url builds a URL part using the given values. +func (r *routeRegexp) url(values map[string]string) (string, error) { + urlValues := make([]interface{}, len(r.varsN), len(r.varsN)) + for k, v := range r.varsN { + value, ok := values[v] + if !ok { + return "", fmt.Errorf("mux: missing route variable %q", v) + } + if r.regexpType == regexpTypeQuery { + value = url.QueryEscape(value) + } + urlValues[k] = value + } + rv := fmt.Sprintf(r.reverse, urlValues...) + if !r.regexp.MatchString(rv) { + // The URL is checked against the full regexp, instead of checking + // individual variables. This is faster but to provide a good error + // message, we check individual regexps if the URL doesn't match. + for k, v := range r.varsN { + if !r.varsR[k].MatchString(values[v]) { + return "", fmt.Errorf( + "mux: variable %q doesn't match, expected %q", values[v], + r.varsR[k].String()) + } + } + } + return rv, nil +} + +// getURLQuery returns a single query parameter from a request URL. +// For a URL with foo=bar&baz=ding, we return only the relevant key +// value pair for the routeRegexp. +func (r *routeRegexp) getURLQuery(req *http.Request) string { + if r.regexpType != regexpTypeQuery { + return "" + } + templateKey := strings.SplitN(r.template, "=", 2)[0] + val, ok := findFirstQueryKey(req.URL.RawQuery, templateKey) + if ok { + return templateKey + "=" + val + } + return "" +} + +// findFirstQueryKey returns the same result as (*url.URL).Query()[key][0]. +// If key was not found, empty string and false is returned. +func findFirstQueryKey(rawQuery, key string) (value string, ok bool) { + query := []byte(rawQuery) + for len(query) > 0 { + foundKey := query + if i := bytes.IndexAny(foundKey, "&;"); i >= 0 { + foundKey, query = foundKey[:i], foundKey[i+1:] + } else { + query = query[:0] + } + if len(foundKey) == 0 { + continue + } + var value []byte + if i := bytes.IndexByte(foundKey, '='); i >= 0 { + foundKey, value = foundKey[:i], foundKey[i+1:] + } + if len(foundKey) < len(key) { + // Cannot possibly be key. + continue + } + keyString, err := url.QueryUnescape(string(foundKey)) + if err != nil { + continue + } + if keyString != key { + continue + } + valueString, err := url.QueryUnescape(string(value)) + if err != nil { + continue + } + return valueString, true + } + return "", false +} + +func (r *routeRegexp) matchQueryString(req *http.Request) bool { + return r.regexp.MatchString(r.getURLQuery(req)) +} + +// braceIndices returns the first level curly brace indices from a string. +// It returns an error in case of unbalanced braces. +func braceIndices(s string) ([]int, error) { + var level, idx int + var idxs []int + for i := 0; i < len(s); i++ { + switch s[i] { + case '{': + if level++; level == 1 { + idx = i + } + case '}': + if level--; level == 0 { + idxs = append(idxs, idx, i+1) + } else if level < 0 { + return nil, fmt.Errorf("mux: unbalanced braces in %q", s) + } + } + } + if level != 0 { + return nil, fmt.Errorf("mux: unbalanced braces in %q", s) + } + return idxs, nil +} + +// varGroupName builds a capturing group name for the indexed variable. +func varGroupName(idx int) string { + return "v" + strconv.Itoa(idx) +} + +// ---------------------------------------------------------------------------- +// routeRegexpGroup +// ---------------------------------------------------------------------------- + +// routeRegexpGroup groups the route matchers that carry variables. +type routeRegexpGroup struct { + host *routeRegexp + path *routeRegexp + queries []*routeRegexp +} + +// setMatch extracts the variables from the URL once a route matches. +func (v routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) { + // Store host variables. + if v.host != nil { + host := getHost(req) + if v.host.wildcardHostPort { + // Don't be strict on the port match + if i := strings.Index(host, ":"); i != -1 { + host = host[:i] + } + } + matches := v.host.regexp.FindStringSubmatchIndex(host) + if len(matches) > 0 { + extractVars(host, matches, v.host.varsN, m.Vars) + } + } + path := req.URL.Path + if r.useEncodedPath { + path = req.URL.EscapedPath() + } + // Store path variables. + if v.path != nil { + matches := v.path.regexp.FindStringSubmatchIndex(path) + if len(matches) > 0 { + extractVars(path, matches, v.path.varsN, m.Vars) + // Check if we should redirect. + if v.path.options.strictSlash { + p1 := strings.HasSuffix(path, "/") + p2 := strings.HasSuffix(v.path.template, "/") + if p1 != p2 { + u, _ := url.Parse(req.URL.String()) + if p1 { + u.Path = u.Path[:len(u.Path)-1] + } else { + u.Path += "/" + } + m.Handler = http.RedirectHandler(u.String(), http.StatusMovedPermanently) + } + } + } + } + // Store query string variables. + for _, q := range v.queries { + queryURL := q.getURLQuery(req) + matches := q.regexp.FindStringSubmatchIndex(queryURL) + if len(matches) > 0 { + extractVars(queryURL, matches, q.varsN, m.Vars) + } + } +} + +// getHost tries its best to return the request host. +// According to section 14.23 of RFC 2616 the Host header +// can include the port number if the default value of 80 is not used. +func getHost(r *http.Request) string { + if r.URL.IsAbs() { + return r.URL.Host + } + return r.Host +} + +func extractVars(input string, matches []int, names []string, output map[string]string) { + for i, name := range names { + output[name] = input[matches[2*i+2]:matches[2*i+3]] + } +} diff --git a/vendor/github.com/gorilla/mux/route.go b/vendor/github.com/gorilla/mux/route.go new file mode 100644 index 00000000..750afe57 --- /dev/null +++ b/vendor/github.com/gorilla/mux/route.go @@ -0,0 +1,736 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mux + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "regexp" + "strings" +) + +// Route stores information to match a request and build URLs. +type Route struct { + // Request handler for the route. + handler http.Handler + // If true, this route never matches: it is only used to build URLs. + buildOnly bool + // The name used to build URLs. + name string + // Error resulted from building a route. + err error + + // "global" reference to all named routes + namedRoutes map[string]*Route + + // config possibly passed in from `Router` + routeConf +} + +// SkipClean reports whether path cleaning is enabled for this route via +// Router.SkipClean. +func (r *Route) SkipClean() bool { + return r.skipClean +} + +// Match matches the route against the request. +func (r *Route) Match(req *http.Request, match *RouteMatch) bool { + if r.buildOnly || r.err != nil { + return false + } + + var matchErr error + + // Match everything. + for _, m := range r.matchers { + if matched := m.Match(req, match); !matched { + if _, ok := m.(methodMatcher); ok { + matchErr = ErrMethodMismatch + continue + } + + // Ignore ErrNotFound errors. These errors arise from match call + // to Subrouters. + // + // This prevents subsequent matching subrouters from failing to + // run middleware. If not ignored, the middleware would see a + // non-nil MatchErr and be skipped, even when there was a + // matching route. + if match.MatchErr == ErrNotFound { + match.MatchErr = nil + } + + matchErr = nil + return false + } + } + + if matchErr != nil { + match.MatchErr = matchErr + return false + } + + if match.MatchErr == ErrMethodMismatch && r.handler != nil { + // We found a route which matches request method, clear MatchErr + match.MatchErr = nil + // Then override the mis-matched handler + match.Handler = r.handler + } + + // Yay, we have a match. Let's collect some info about it. + if match.Route == nil { + match.Route = r + } + if match.Handler == nil { + match.Handler = r.handler + } + if match.Vars == nil { + match.Vars = make(map[string]string) + } + + // Set variables. + r.regexp.setMatch(req, match, r) + return true +} + +// ---------------------------------------------------------------------------- +// Route attributes +// ---------------------------------------------------------------------------- + +// GetError returns an error resulted from building the route, if any. +func (r *Route) GetError() error { + return r.err +} + +// BuildOnly sets the route to never match: it is only used to build URLs. +func (r *Route) BuildOnly() *Route { + r.buildOnly = true + return r +} + +// Handler -------------------------------------------------------------------- + +// Handler sets a handler for the route. +func (r *Route) Handler(handler http.Handler) *Route { + if r.err == nil { + r.handler = handler + } + return r +} + +// HandlerFunc sets a handler function for the route. +func (r *Route) HandlerFunc(f func(http.ResponseWriter, *http.Request)) *Route { + return r.Handler(http.HandlerFunc(f)) +} + +// GetHandler returns the handler for the route, if any. +func (r *Route) GetHandler() http.Handler { + return r.handler +} + +// Name ----------------------------------------------------------------------- + +// Name sets the name for the route, used to build URLs. +// It is an error to call Name more than once on a route. +func (r *Route) Name(name string) *Route { + if r.name != "" { + r.err = fmt.Errorf("mux: route already has name %q, can't set %q", + r.name, name) + } + if r.err == nil { + r.name = name + r.namedRoutes[name] = r + } + return r +} + +// GetName returns the name for the route, if any. +func (r *Route) GetName() string { + return r.name +} + +// ---------------------------------------------------------------------------- +// Matchers +// ---------------------------------------------------------------------------- + +// matcher types try to match a request. +type matcher interface { + Match(*http.Request, *RouteMatch) bool +} + +// addMatcher adds a matcher to the route. +func (r *Route) addMatcher(m matcher) *Route { + if r.err == nil { + r.matchers = append(r.matchers, m) + } + return r +} + +// addRegexpMatcher adds a host or path matcher and builder to a route. +func (r *Route) addRegexpMatcher(tpl string, typ regexpType) error { + if r.err != nil { + return r.err + } + if typ == regexpTypePath || typ == regexpTypePrefix { + if len(tpl) > 0 && tpl[0] != '/' { + return fmt.Errorf("mux: path must start with a slash, got %q", tpl) + } + if r.regexp.path != nil { + tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl + } + } + rr, err := newRouteRegexp(tpl, typ, routeRegexpOptions{ + strictSlash: r.strictSlash, + useEncodedPath: r.useEncodedPath, + }) + if err != nil { + return err + } + for _, q := range r.regexp.queries { + if err = uniqueVars(rr.varsN, q.varsN); err != nil { + return err + } + } + if typ == regexpTypeHost { + if r.regexp.path != nil { + if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil { + return err + } + } + r.regexp.host = rr + } else { + if r.regexp.host != nil { + if err = uniqueVars(rr.varsN, r.regexp.host.varsN); err != nil { + return err + } + } + if typ == regexpTypeQuery { + r.regexp.queries = append(r.regexp.queries, rr) + } else { + r.regexp.path = rr + } + } + r.addMatcher(rr) + return nil +} + +// Headers -------------------------------------------------------------------- + +// headerMatcher matches the request against header values. +type headerMatcher map[string]string + +func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool { + return matchMapWithString(m, r.Header, true) +} + +// Headers adds a matcher for request header values. +// It accepts a sequence of key/value pairs to be matched. For example: +// +// r := mux.NewRouter() +// r.Headers("Content-Type", "application/json", +// "X-Requested-With", "XMLHttpRequest") +// +// The above route will only match if both request header values match. +// If the value is an empty string, it will match any value if the key is set. +func (r *Route) Headers(pairs ...string) *Route { + if r.err == nil { + var headers map[string]string + headers, r.err = mapFromPairsToString(pairs...) + return r.addMatcher(headerMatcher(headers)) + } + return r +} + +// headerRegexMatcher matches the request against the route given a regex for the header +type headerRegexMatcher map[string]*regexp.Regexp + +func (m headerRegexMatcher) Match(r *http.Request, match *RouteMatch) bool { + return matchMapWithRegex(m, r.Header, true) +} + +// HeadersRegexp accepts a sequence of key/value pairs, where the value has regex +// support. For example: +// +// r := mux.NewRouter() +// r.HeadersRegexp("Content-Type", "application/(text|json)", +// "X-Requested-With", "XMLHttpRequest") +// +// The above route will only match if both the request header matches both regular expressions. +// If the value is an empty string, it will match any value if the key is set. +// Use the start and end of string anchors (^ and $) to match an exact value. +func (r *Route) HeadersRegexp(pairs ...string) *Route { + if r.err == nil { + var headers map[string]*regexp.Regexp + headers, r.err = mapFromPairsToRegex(pairs...) + return r.addMatcher(headerRegexMatcher(headers)) + } + return r +} + +// Host ----------------------------------------------------------------------- + +// Host adds a matcher for the URL host. +// It accepts a template with zero or more URL variables enclosed by {}. +// Variables can define an optional regexp pattern to be matched: +// +// - {name} matches anything until the next dot. +// +// - {name:pattern} matches the given regexp pattern. +// +// For example: +// +// r := mux.NewRouter() +// r.Host("www.example.com") +// r.Host("{subdomain}.domain.com") +// r.Host("{subdomain:[a-z]+}.domain.com") +// +// Variable names must be unique in a given route. They can be retrieved +// calling mux.Vars(request). +func (r *Route) Host(tpl string) *Route { + r.err = r.addRegexpMatcher(tpl, regexpTypeHost) + return r +} + +// MatcherFunc ---------------------------------------------------------------- + +// MatcherFunc is the function signature used by custom matchers. +type MatcherFunc func(*http.Request, *RouteMatch) bool + +// Match returns the match for a given request. +func (m MatcherFunc) Match(r *http.Request, match *RouteMatch) bool { + return m(r, match) +} + +// MatcherFunc adds a custom function to be used as request matcher. +func (r *Route) MatcherFunc(f MatcherFunc) *Route { + return r.addMatcher(f) +} + +// Methods -------------------------------------------------------------------- + +// methodMatcher matches the request against HTTP methods. +type methodMatcher []string + +func (m methodMatcher) Match(r *http.Request, match *RouteMatch) bool { + return matchInArray(m, r.Method) +} + +// Methods adds a matcher for HTTP methods. +// It accepts a sequence of one or more methods to be matched, e.g.: +// "GET", "POST", "PUT". +func (r *Route) Methods(methods ...string) *Route { + for k, v := range methods { + methods[k] = strings.ToUpper(v) + } + return r.addMatcher(methodMatcher(methods)) +} + +// Path ----------------------------------------------------------------------- + +// Path adds a matcher for the URL path. +// It accepts a template with zero or more URL variables enclosed by {}. The +// template must start with a "/". +// Variables can define an optional regexp pattern to be matched: +// +// - {name} matches anything until the next slash. +// +// - {name:pattern} matches the given regexp pattern. +// +// For example: +// +// r := mux.NewRouter() +// r.Path("/products/").Handler(ProductsHandler) +// r.Path("/products/{key}").Handler(ProductsHandler) +// r.Path("/articles/{category}/{id:[0-9]+}"). +// Handler(ArticleHandler) +// +// Variable names must be unique in a given route. They can be retrieved +// calling mux.Vars(request). +func (r *Route) Path(tpl string) *Route { + r.err = r.addRegexpMatcher(tpl, regexpTypePath) + return r +} + +// PathPrefix ----------------------------------------------------------------- + +// PathPrefix adds a matcher for the URL path prefix. This matches if the given +// template is a prefix of the full URL path. See Route.Path() for details on +// the tpl argument. +// +// Note that it does not treat slashes specially ("/foobar/" will be matched by +// the prefix "/foo") so you may want to use a trailing slash here. +// +// Also note that the setting of Router.StrictSlash() has no effect on routes +// with a PathPrefix matcher. +func (r *Route) PathPrefix(tpl string) *Route { + r.err = r.addRegexpMatcher(tpl, regexpTypePrefix) + return r +} + +// Query ---------------------------------------------------------------------- + +// Queries adds a matcher for URL query values. +// It accepts a sequence of key/value pairs. Values may define variables. +// For example: +// +// r := mux.NewRouter() +// r.Queries("foo", "bar", "id", "{id:[0-9]+}") +// +// The above route will only match if the URL contains the defined queries +// values, e.g.: ?foo=bar&id=42. +// +// If the value is an empty string, it will match any value if the key is set. +// +// Variables can define an optional regexp pattern to be matched: +// +// - {name} matches anything until the next slash. +// +// - {name:pattern} matches the given regexp pattern. +func (r *Route) Queries(pairs ...string) *Route { + length := len(pairs) + if length%2 != 0 { + r.err = fmt.Errorf( + "mux: number of parameters must be multiple of 2, got %v", pairs) + return nil + } + for i := 0; i < length; i += 2 { + if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], regexpTypeQuery); r.err != nil { + return r + } + } + + return r +} + +// Schemes -------------------------------------------------------------------- + +// schemeMatcher matches the request against URL schemes. +type schemeMatcher []string + +func (m schemeMatcher) Match(r *http.Request, match *RouteMatch) bool { + scheme := r.URL.Scheme + // https://golang.org/pkg/net/http/#Request + // "For [most] server requests, fields other than Path and RawQuery will be + // empty." + // Since we're an http muxer, the scheme is either going to be http or https + // though, so we can just set it based on the tls termination state. + if scheme == "" { + if r.TLS == nil { + scheme = "http" + } else { + scheme = "https" + } + } + return matchInArray(m, scheme) +} + +// Schemes adds a matcher for URL schemes. +// It accepts a sequence of schemes to be matched, e.g.: "http", "https". +// If the request's URL has a scheme set, it will be matched against. +// Generally, the URL scheme will only be set if a previous handler set it, +// such as the ProxyHeaders handler from gorilla/handlers. +// If unset, the scheme will be determined based on the request's TLS +// termination state. +// The first argument to Schemes will be used when constructing a route URL. +func (r *Route) Schemes(schemes ...string) *Route { + for k, v := range schemes { + schemes[k] = strings.ToLower(v) + } + if len(schemes) > 0 { + r.buildScheme = schemes[0] + } + return r.addMatcher(schemeMatcher(schemes)) +} + +// BuildVarsFunc -------------------------------------------------------------- + +// BuildVarsFunc is the function signature used by custom build variable +// functions (which can modify route variables before a route's URL is built). +type BuildVarsFunc func(map[string]string) map[string]string + +// BuildVarsFunc adds a custom function to be used to modify build variables +// before a route's URL is built. +func (r *Route) BuildVarsFunc(f BuildVarsFunc) *Route { + if r.buildVarsFunc != nil { + // compose the old and new functions + old := r.buildVarsFunc + r.buildVarsFunc = func(m map[string]string) map[string]string { + return f(old(m)) + } + } else { + r.buildVarsFunc = f + } + return r +} + +// Subrouter ------------------------------------------------------------------ + +// Subrouter creates a subrouter for the route. +// +// It will test the inner routes only if the parent route matched. For example: +// +// r := mux.NewRouter() +// s := r.Host("www.example.com").Subrouter() +// s.HandleFunc("/products/", ProductsHandler) +// s.HandleFunc("/products/{key}", ProductHandler) +// s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) +// +// Here, the routes registered in the subrouter won't be tested if the host +// doesn't match. +func (r *Route) Subrouter() *Router { + // initialize a subrouter with a copy of the parent route's configuration + router := &Router{routeConf: copyRouteConf(r.routeConf), namedRoutes: r.namedRoutes} + r.addMatcher(router) + return router +} + +// ---------------------------------------------------------------------------- +// URL building +// ---------------------------------------------------------------------------- + +// URL builds a URL for the route. +// +// It accepts a sequence of key/value pairs for the route variables. For +// example, given this route: +// +// r := mux.NewRouter() +// r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). +// Name("article") +// +// ...a URL for it can be built using: +// +// url, err := r.Get("article").URL("category", "technology", "id", "42") +// +// ...which will return an url.URL with the following path: +// +// "/articles/technology/42" +// +// This also works for host variables: +// +// r := mux.NewRouter() +// r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). +// Host("{subdomain}.domain.com"). +// Name("article") +// +// // url.String() will be "http://news.domain.com/articles/technology/42" +// url, err := r.Get("article").URL("subdomain", "news", +// "category", "technology", +// "id", "42") +// +// The scheme of the resulting url will be the first argument that was passed to Schemes: +// +// // url.String() will be "https://example.com" +// r := mux.NewRouter() +// url, err := r.Host("example.com") +// .Schemes("https", "http").URL() +// +// All variables defined in the route are required, and their values must +// conform to the corresponding patterns. +func (r *Route) URL(pairs ...string) (*url.URL, error) { + if r.err != nil { + return nil, r.err + } + values, err := r.prepareVars(pairs...) + if err != nil { + return nil, err + } + var scheme, host, path string + queries := make([]string, 0, len(r.regexp.queries)) + if r.regexp.host != nil { + if host, err = r.regexp.host.url(values); err != nil { + return nil, err + } + scheme = "http" + if r.buildScheme != "" { + scheme = r.buildScheme + } + } + if r.regexp.path != nil { + if path, err = r.regexp.path.url(values); err != nil { + return nil, err + } + } + for _, q := range r.regexp.queries { + var query string + if query, err = q.url(values); err != nil { + return nil, err + } + queries = append(queries, query) + } + return &url.URL{ + Scheme: scheme, + Host: host, + Path: path, + RawQuery: strings.Join(queries, "&"), + }, nil +} + +// URLHost builds the host part of the URL for a route. See Route.URL(). +// +// The route must have a host defined. +func (r *Route) URLHost(pairs ...string) (*url.URL, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp.host == nil { + return nil, errors.New("mux: route doesn't have a host") + } + values, err := r.prepareVars(pairs...) + if err != nil { + return nil, err + } + host, err := r.regexp.host.url(values) + if err != nil { + return nil, err + } + u := &url.URL{ + Scheme: "http", + Host: host, + } + if r.buildScheme != "" { + u.Scheme = r.buildScheme + } + return u, nil +} + +// URLPath builds the path part of the URL for a route. See Route.URL(). +// +// The route must have a path defined. +func (r *Route) URLPath(pairs ...string) (*url.URL, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp.path == nil { + return nil, errors.New("mux: route doesn't have a path") + } + values, err := r.prepareVars(pairs...) + if err != nil { + return nil, err + } + path, err := r.regexp.path.url(values) + if err != nil { + return nil, err + } + return &url.URL{ + Path: path, + }, nil +} + +// GetPathTemplate returns the template used to build the +// route match. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not define a path. +func (r *Route) GetPathTemplate() (string, error) { + if r.err != nil { + return "", r.err + } + if r.regexp.path == nil { + return "", errors.New("mux: route doesn't have a path") + } + return r.regexp.path.template, nil +} + +// GetPathRegexp returns the expanded regular expression used to match route path. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not define a path. +func (r *Route) GetPathRegexp() (string, error) { + if r.err != nil { + return "", r.err + } + if r.regexp.path == nil { + return "", errors.New("mux: route does not have a path") + } + return r.regexp.path.regexp.String(), nil +} + +// GetQueriesRegexp returns the expanded regular expressions used to match the +// route queries. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not have queries. +func (r *Route) GetQueriesRegexp() ([]string, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp.queries == nil { + return nil, errors.New("mux: route doesn't have queries") + } + queries := make([]string, 0, len(r.regexp.queries)) + for _, query := range r.regexp.queries { + queries = append(queries, query.regexp.String()) + } + return queries, nil +} + +// GetQueriesTemplates returns the templates used to build the +// query matching. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not define queries. +func (r *Route) GetQueriesTemplates() ([]string, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp.queries == nil { + return nil, errors.New("mux: route doesn't have queries") + } + queries := make([]string, 0, len(r.regexp.queries)) + for _, query := range r.regexp.queries { + queries = append(queries, query.template) + } + return queries, nil +} + +// GetMethods returns the methods the route matches against +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if route does not have methods. +func (r *Route) GetMethods() ([]string, error) { + if r.err != nil { + return nil, r.err + } + for _, m := range r.matchers { + if methods, ok := m.(methodMatcher); ok { + return []string(methods), nil + } + } + return nil, errors.New("mux: route doesn't have methods") +} + +// GetHostTemplate returns the template used to build the +// route match. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not define a host. +func (r *Route) GetHostTemplate() (string, error) { + if r.err != nil { + return "", r.err + } + if r.regexp.host == nil { + return "", errors.New("mux: route doesn't have a host") + } + return r.regexp.host.template, nil +} + +// prepareVars converts the route variable pairs into a map. If the route has a +// BuildVarsFunc, it is invoked. +func (r *Route) prepareVars(pairs ...string) (map[string]string, error) { + m, err := mapFromPairsToString(pairs...) + if err != nil { + return nil, err + } + return r.buildVars(m), nil +} + +func (r *Route) buildVars(m map[string]string) map[string]string { + if r.buildVarsFunc != nil { + m = r.buildVarsFunc(m) + } + return m +} diff --git a/vendor/github.com/gorilla/mux/test_helpers.go b/vendor/github.com/gorilla/mux/test_helpers.go new file mode 100644 index 00000000..5f5c496d --- /dev/null +++ b/vendor/github.com/gorilla/mux/test_helpers.go @@ -0,0 +1,19 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mux + +import "net/http" + +// SetURLVars sets the URL variables for the given request, to be accessed via +// mux.Vars for testing route behaviour. Arguments are not modified, a shallow +// copy is returned. +// +// This API should only be used for testing purposes; it provides a way to +// inject variables into the request context. Alternatively, URL variables +// can be set by making a route that captures the required variables, +// starting a server and sending the request to that server. +func SetURLVars(r *http.Request, val map[string]string) *http.Request { + return requestWithVars(r, val) +} diff --git a/vendor/github.com/matterbridge/gomatrix/.golangci.yml b/vendor/github.com/matterbridge/gomatrix/.golangci.yml deleted file mode 100644 index 15eb6ef7..00000000 --- a/vendor/github.com/matterbridge/gomatrix/.golangci.yml +++ /dev/null @@ -1,21 +0,0 @@ -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/matterbridge/gomatrix/.travis.yml b/vendor/github.com/matterbridge/gomatrix/.travis.yml deleted file mode 100644 index 46997ab6..00000000 --- a/vendor/github.com/matterbridge/gomatrix/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -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/matterbridge/gomatrix/CHANGELOG.md b/vendor/github.com/matterbridge/gomatrix/CHANGELOG.md deleted file mode 100644 index b6a9a167..00000000 --- a/vendor/github.com/matterbridge/gomatrix/CHANGELOG.md +++ /dev/null @@ -1 +0,0 @@ -## Release 0.1.0 (UNRELEASED) diff --git a/vendor/github.com/matterbridge/gomatrix/LICENSE b/vendor/github.com/matterbridge/gomatrix/LICENSE deleted file mode 100644 index 8dada3ed..00000000 --- a/vendor/github.com/matterbridge/gomatrix/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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/matterbridge/gomatrix/README.md b/vendor/github.com/matterbridge/gomatrix/README.md deleted file mode 100644 index a083b46c..00000000 --- a/vendor/github.com/matterbridge/gomatrix/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# 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.** - -# Contributing - -All contributions are greatly appreciated! - -## How to report issues - -Please check the current open issues for similar reports -in order to avoid duplicates. - -Some general guidelines: - -- Include a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) when possible. -- Describe the expected behaviour and what actually happened - including a full trace-back in case of exceptions. -- Make sure to list details about your environment - -## Setting up your environment - -If you intend to contribute to gomatrix you'll first need Go installed on your machine (version 1.12+ is required). Also, make sure to have golangci-lint properly set up since we use it for pre-commit hooks (for instructions on how to install it, check the [official docs](https://golangci-lint.run/usage/install/#local-installation)). - -- Fork gomatrix to your GitHub account by clicking the [Fork](https://github.com/matrix-org/gomatrix/fork) button. -- [Clone](https://help.github.com/en/articles/fork-a-repo#step-2-create-a-local-clone-of-your-fork) the main repository (not your fork) to your local machine. - - - $ git clone https://github.com/matrix-org/gomatrix - $ cd gomatrix - - -- Add your fork as a remote to push your contributions.Replace - ``{username}`` with your username. - - git remote add fork https://github.com/{username}/gomatrix - -- Create a new branch to identify what feature you are working on. - - $ git fetch origin - $ git checkout -b your-branch-name origin/master - - -- Make your changes, including tests that cover any code changes you make, and run them as described below. - -- Execute pre-commit hooks by running - - <gomatrix dir>/hooks/pre-commit - -- Push your changes to your fork and [create a pull request](https://help.github.com/en/articles/creating-a-pull-request) describing your changes. - - $ git push --set-upstream fork your-branch-name - -- Finally, create a [pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) - -## How to run tests - -You can run the test suite and example code with `$ go test -v` - -# Running Coverage - -To run coverage, first generate the coverage report using `go test` - - go test -v -cover -coverprofile=coverage.out - -You can now show the generated report as a html page with `go tool` - - go tool cover -html=coverage.out diff --git a/vendor/github.com/matterbridge/gomatrix/client.go b/vendor/github.com/matterbridge/gomatrix/client.go deleted file mode 100644 index aa9e7e2c..00000000 --- a/vendor/github.com/matterbridge/gomatrix/client.go +++ /dev/null @@ -1,805 +0,0 @@ -// 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 homeserver/prefix 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 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.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 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") - - if cli.AccessToken != "" { - req.Header.Set("Authorization", "Bearer "+cli.AccessToken) - } - - 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, struct{}{}, 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.Header.Set("Authorization", "Bearer "+cli.AccessToken) - - 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/matterbridge/gomatrix/events.go b/vendor/github.com/matterbridge/gomatrix/events.go deleted file mode 100644 index d555dc3f..00000000 --- a/vendor/github.com/matterbridge/gomatrix/events.go +++ /dev/null @@ -1,157 +0,0 @@ -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,omitempty"` - Format string `json:"format,omitempty"` -} - -// 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/matterbridge/gomatrix/filter.go b/vendor/github.com/matterbridge/gomatrix/filter.go deleted file mode 100644 index 2a0c37fa..00000000 --- a/vendor/github.com/matterbridge/gomatrix/filter.go +++ /dev/null @@ -1,90 +0,0 @@ -// 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/matterbridge/gomatrix/identifier.go b/vendor/github.com/matterbridge/gomatrix/identifier.go deleted file mode 100644 index 4a61d080..00000000 --- a/vendor/github.com/matterbridge/gomatrix/identifier.go +++ /dev/null @@ -1,69 +0,0 @@ -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/matterbridge/gomatrix/requests.go b/vendor/github.com/matterbridge/gomatrix/requests.go deleted file mode 100644 index 31c426d4..00000000 --- a/vendor/github.com/matterbridge/gomatrix/requests.go +++ /dev/null @@ -1,79 +0,0 @@ -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/matterbridge/gomatrix/responses.go b/vendor/github.com/matterbridge/gomatrix/responses.go deleted file mode 100644 index f488e69e..00000000 --- a/vendor/github.com/matterbridge/gomatrix/responses.go +++ /dev/null @@ -1,210 +0,0 @@ -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/matterbridge/gomatrix/room.go b/vendor/github.com/matterbridge/gomatrix/room.go deleted file mode 100644 index 364deab2..00000000 --- a/vendor/github.com/matterbridge/gomatrix/room.go +++ /dev/null @@ -1,63 +0,0 @@ -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/matterbridge/gomatrix/store.go b/vendor/github.com/matterbridge/gomatrix/store.go deleted file mode 100644 index 6dc687e5..00000000 --- a/vendor/github.com/matterbridge/gomatrix/store.go +++ /dev/null @@ -1,65 +0,0 @@ -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/matterbridge/gomatrix/sync.go b/vendor/github.com/matterbridge/gomatrix/sync.go deleted file mode 100644 index ac326c3a..00000000 --- a/vendor/github.com/matterbridge/gomatrix/sync.go +++ /dev/null @@ -1,168 +0,0 @@ -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/matterbridge/gomatrix/tags.go b/vendor/github.com/matterbridge/gomatrix/tags.go deleted file mode 100644 index 956fb11e..00000000 --- a/vendor/github.com/matterbridge/gomatrix/tags.go +++ /dev/null @@ -1,26 +0,0 @@ -// 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/matterbridge/gomatrix/userids.go b/vendor/github.com/matterbridge/gomatrix/userids.go deleted file mode 100644 index 70002c5b..00000000 --- a/vendor/github.com/matterbridge/gomatrix/userids.go +++ /dev/null @@ -1,130 +0,0 @@ -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/github.com/matterbridge/gomatrix/.gitignore b/vendor/github.com/rs/zerolog/.gitignore index 0dd56286..8ebe58b1 100644 --- a/vendor/github.com/matterbridge/gomatrix/.gitignore +++ b/vendor/github.com/rs/zerolog/.gitignore @@ -2,11 +2,11 @@ *.o *.a *.so -*.out # Folders _obj _test +tmp # Architecture specific extensions/prefixes *.[568vq] @@ -23,6 +23,3 @@ _testmain.go *.exe *.test *.prof - -# test editor files -*.swp diff --git a/vendor/github.com/rs/zerolog/CNAME b/vendor/github.com/rs/zerolog/CNAME new file mode 100644 index 00000000..9ce57a6e --- /dev/null +++ b/vendor/github.com/rs/zerolog/CNAME @@ -0,0 +1 @@ +zerolog.io
\ No newline at end of file diff --git a/vendor/github.com/rs/zerolog/LICENSE b/vendor/github.com/rs/zerolog/LICENSE new file mode 100644 index 00000000..677e07f7 --- /dev/null +++ b/vendor/github.com/rs/zerolog/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Olivier Poitrey + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/rs/zerolog/README.md b/vendor/github.com/rs/zerolog/README.md new file mode 100644 index 00000000..e8cbfc28 --- /dev/null +++ b/vendor/github.com/rs/zerolog/README.md @@ -0,0 +1,716 @@ +# Zero Allocation JSON Logger + +[![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/rs/zerolog) [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/rs/zerolog/master/LICENSE) [![Build Status](https://travis-ci.org/rs/zerolog.svg?branch=master)](https://travis-ci.org/rs/zerolog) [![Coverage](http://gocover.io/_badge/github.com/rs/zerolog)](http://gocover.io/github.com/rs/zerolog) + +The zerolog package provides a fast and simple logger dedicated to JSON output. + +Zerolog's API is designed to provide both a great developer experience and stunning [performance](#benchmarks). Its unique chaining API allows zerolog to write JSON (or CBOR) log events by avoiding allocations and reflection. + +Uber's [zap](https://godoc.org/go.uber.org/zap) library pioneered this approach. Zerolog is taking this concept to the next level with a simpler to use API and even better performance. + +To keep the code base and the API simple, zerolog focuses on efficient structured logging only. Pretty logging on the console is made possible using the provided (but inefficient) [`zerolog.ConsoleWriter`](#pretty-logging). + +![Pretty Logging Image](pretty.png) + +## Who uses zerolog + +Find out [who uses zerolog](https://github.com/rs/zerolog/wiki/Who-uses-zerolog) and add your company / project to the list. + +## Features + +* [Blazing fast](#benchmarks) +* [Low to zero allocation](#benchmarks) +* [Leveled logging](#leveled-logging) +* [Sampling](#log-sampling) +* [Hooks](#hooks) +* [Contextual fields](#contextual-logging) +* `context.Context` integration +* [Integration with `net/http`](#integration-with-nethttp) +* [JSON and CBOR encoding formats](#binary-encoding) +* [Pretty logging for development](#pretty-logging) +* [Error Logging (with optional Stacktrace)](#error-logging) + +## Installation + +```bash +go get -u github.com/rs/zerolog/log +``` + +## Getting Started + +### Simple Logging Example + +For simple logging, import the global logger package **github.com/rs/zerolog/log** + +```go +package main + +import ( + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +func main() { + // UNIX Time is faster and smaller than most timestamps + zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + + log.Print("hello world") +} + +// Output: {"time":1516134303,"level":"debug","message":"hello world"} +``` +> Note: By default log writes to `os.Stderr` +> Note: The default log level for `log.Print` is *debug* + +### Contextual Logging + +**zerolog** allows data to be added to log messages in the form of key:value pairs. The data added to the message adds "context" about the log event that can be critical for debugging as well as myriad other purposes. An example of this is below: + +```go +package main + +import ( + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +func main() { + zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + + log.Debug(). + Str("Scale", "833 cents"). + Float64("Interval", 833.09). + Msg("Fibonacci is everywhere") + + log.Debug(). + Str("Name", "Tom"). + Send() +} + +// Output: {"level":"debug","Scale":"833 cents","Interval":833.09,"time":1562212768,"message":"Fibonacci is everywhere"} +// Output: {"level":"debug","Name":"Tom","time":1562212768} +``` + +> You'll note in the above example that when adding contextual fields, the fields are strongly typed. You can find the full list of supported fields [here](#standard-types) + +### Leveled Logging + +#### Simple Leveled Logging Example + +```go +package main + +import ( + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +func main() { + zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + + log.Info().Msg("hello world") +} + +// Output: {"time":1516134303,"level":"info","message":"hello world"} +``` + +> It is very important to note that when using the **zerolog** chaining API, as shown above (`log.Info().Msg("hello world"`), the chain must have either the `Msg` or `Msgf` method call. If you forget to add either of these, the log will not occur and there is no compile time error to alert you of this. + +**zerolog** allows for logging at the following levels (from highest to lowest): + +* panic (`zerolog.PanicLevel`, 5) +* fatal (`zerolog.FatalLevel`, 4) +* error (`zerolog.ErrorLevel`, 3) +* warn (`zerolog.WarnLevel`, 2) +* info (`zerolog.InfoLevel`, 1) +* debug (`zerolog.DebugLevel`, 0) +* trace (`zerolog.TraceLevel`, -1) + +You can set the Global logging level to any of these options using the `SetGlobalLevel` function in the zerolog package, passing in one of the given constants above, e.g. `zerolog.InfoLevel` would be the "info" level. Whichever level is chosen, all logs with a level greater than or equal to that level will be written. To turn off logging entirely, pass the `zerolog.Disabled` constant. + +#### Setting Global Log Level + +This example uses command-line flags to demonstrate various outputs depending on the chosen log level. + +```go +package main + +import ( + "flag" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +func main() { + zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + debug := flag.Bool("debug", false, "sets log level to debug") + + flag.Parse() + + // Default level for this example is info, unless debug flag is present + zerolog.SetGlobalLevel(zerolog.InfoLevel) + if *debug { + zerolog.SetGlobalLevel(zerolog.DebugLevel) + } + + log.Debug().Msg("This message appears only when log level set to Debug") + log.Info().Msg("This message appears when log level set to Debug or Info") + + if e := log.Debug(); e.Enabled() { + // Compute log output only if enabled. + value := "bar" + e.Str("foo", value).Msg("some debug message") + } +} +``` + +Info Output (no flag) + +```bash +$ ./logLevelExample +{"time":1516387492,"level":"info","message":"This message appears when log level set to Debug or Info"} +``` + +Debug Output (debug flag set) + +```bash +$ ./logLevelExample -debug +{"time":1516387573,"level":"debug","message":"This message appears only when log level set to Debug"} +{"time":1516387573,"level":"info","message":"This message appears when log level set to Debug or Info"} +{"time":1516387573,"level":"debug","foo":"bar","message":"some debug message"} +``` + +#### Logging without Level or Message + +You may choose to log without a specific level by using the `Log` method. You may also write without a message by setting an empty string in the `msg string` parameter of the `Msg` method. Both are demonstrated in the example below. + +```go +package main + +import ( + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +func main() { + zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + + log.Log(). + Str("foo", "bar"). + Msg("") +} + +// Output: {"time":1494567715,"foo":"bar"} +``` + +### Error Logging + +You can log errors using the `Err` method + +```go +package main + +import ( + "errors" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +func main() { + zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + + err := errors.New("seems we have an error here") + log.Error().Err(err).Msg("") +} + +// Output: {"level":"error","error":"seems we have an error here","time":1609085256} +``` + +> The default field name for errors is `error`, you can change this by setting `zerolog.ErrorFieldName` to meet your needs. + +#### Error Logging with Stacktrace + +Using `github.com/pkg/errors`, you can add a formatted stacktrace to your errors. + +```go +package main + +import ( + "github.com/pkg/errors" + "github.com/rs/zerolog/pkgerrors" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +func main() { + zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack + + err := outer() + log.Error().Stack().Err(err).Msg("") +} + +func inner() error { + return errors.New("seems we have an error here") +} + +func middle() error { + err := inner() + if err != nil { + return err + } + return nil +} + +func outer() error { + err := middle() + if err != nil { + return err + } + return nil +} + +// Output: {"level":"error","stack":[{"func":"inner","line":"20","source":"errors.go"},{"func":"middle","line":"24","source":"errors.go"},{"func":"outer","line":"32","source":"errors.go"},{"func":"main","line":"15","source":"errors.go"},{"func":"main","line":"204","source":"proc.go"},{"func":"goexit","line":"1374","source":"asm_amd64.s"}],"error":"seems we have an error here","time":1609086683} +``` + +> zerolog.ErrorStackMarshaler must be set in order for the stack to output anything. + +#### Logging Fatal Messages + +```go +package main + +import ( + "errors" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +func main() { + err := errors.New("A repo man spends his life getting into tense situations") + service := "myservice" + + zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + + log.Fatal(). + Err(err). + Str("service", service). + Msgf("Cannot start %s", service) +} + +// Output: {"time":1516133263,"level":"fatal","error":"A repo man spends his life getting into tense situations","service":"myservice","message":"Cannot start myservice"} +// exit status 1 +``` + +> NOTE: Using `Msgf` generates one allocation even when the logger is disabled. + + +### Create logger instance to manage different outputs + +```go +logger := zerolog.New(os.Stderr).With().Timestamp().Logger() + +logger.Info().Str("foo", "bar").Msg("hello world") + +// Output: {"level":"info","time":1494567715,"message":"hello world","foo":"bar"} +``` + +### Sub-loggers let you chain loggers with additional context + +```go +sublogger := log.With(). + Str("component", "foo"). + Logger() +sublogger.Info().Msg("hello world") + +// Output: {"level":"info","time":1494567715,"message":"hello world","component":"foo"} +``` + +### Pretty logging + +To log a human-friendly, colorized output, use `zerolog.ConsoleWriter`: + +```go +log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + +log.Info().Str("foo", "bar").Msg("Hello world") + +// Output: 3:04PM INF Hello World foo=bar +``` + +To customize the configuration and formatting: + +```go +output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339} +output.FormatLevel = func(i interface{}) string { + return strings.ToUpper(fmt.Sprintf("| %-6s|", i)) +} +output.FormatMessage = func(i interface{}) string { + return fmt.Sprintf("***%s****", i) +} +output.FormatFieldName = func(i interface{}) string { + return fmt.Sprintf("%s:", i) +} +output.FormatFieldValue = func(i interface{}) string { + return strings.ToUpper(fmt.Sprintf("%s", i)) +} + +log := zerolog.New(output).With().Timestamp().Logger() + +log.Info().Str("foo", "bar").Msg("Hello World") + +// Output: 2006-01-02T15:04:05Z07:00 | INFO | ***Hello World**** foo:BAR +``` + +### Sub dictionary + +```go +log.Info(). + Str("foo", "bar"). + Dict("dict", zerolog.Dict(). + Str("bar", "baz"). + Int("n", 1), + ).Msg("hello world") + +// Output: {"level":"info","time":1494567715,"foo":"bar","dict":{"bar":"baz","n":1},"message":"hello world"} +``` + +### Customize automatic field names + +```go +zerolog.TimestampFieldName = "t" +zerolog.LevelFieldName = "l" +zerolog.MessageFieldName = "m" + +log.Info().Msg("hello world") + +// Output: {"l":"info","t":1494567715,"m":"hello world"} +``` + +### Add contextual fields to the global logger + +```go +log.Logger = log.With().Str("foo", "bar").Logger() +``` + +### Add file and line number to log + +Equivalent of `Llongfile`: + +```go +log.Logger = log.With().Caller().Logger() +log.Info().Msg("hello world") + +// Output: {"level": "info", "message": "hello world", "caller": "/go/src/your_project/some_file:21"} +``` + +Equivalent of `Lshortfile`: + +```go +zerolog.CallerMarshalFunc = func(pc uintptr, file string, line int) string { + short := file + for i := len(file) - 1; i > 0; i-- { + if file[i] == '/' { + short = file[i+1:] + break + } + } + file = short + return file + ":" + strconv.Itoa(line) +} +log.Logger = log.With().Caller().Logger() +log.Info().Msg("hello world") + +// Output: {"level": "info", "message": "hello world", "caller": "some_file:21"} +``` + +### Thread-safe, lock-free, non-blocking writer + +If your writer might be slow or not thread-safe and you need your log producers to never get slowed down by a slow writer, you can use a `diode.Writer` as follows: + +```go +wr := diode.NewWriter(os.Stdout, 1000, 10*time.Millisecond, func(missed int) { + fmt.Printf("Logger Dropped %d messages", missed) + }) +log := zerolog.New(wr) +log.Print("test") +``` + +You will need to install `code.cloudfoundry.org/go-diodes` to use this feature. + +### Log Sampling + +```go +sampled := log.Sample(&zerolog.BasicSampler{N: 10}) +sampled.Info().Msg("will be logged every 10 messages") + +// Output: {"time":1494567715,"level":"info","message":"will be logged every 10 messages"} +``` + +More advanced sampling: + +```go +// Will let 5 debug messages per period of 1 second. +// Over 5 debug message, 1 every 100 debug messages are logged. +// Other levels are not sampled. +sampled := log.Sample(zerolog.LevelSampler{ + DebugSampler: &zerolog.BurstSampler{ + Burst: 5, + Period: 1*time.Second, + NextSampler: &zerolog.BasicSampler{N: 100}, + }, +}) +sampled.Debug().Msg("hello world") + +// Output: {"time":1494567715,"level":"debug","message":"hello world"} +``` + +### Hooks + +```go +type SeverityHook struct{} + +func (h SeverityHook) Run(e *zerolog.Event, level zerolog.Level, msg string) { + if level != zerolog.NoLevel { + e.Str("severity", level.String()) + } +} + +hooked := log.Hook(SeverityHook{}) +hooked.Warn().Msg("") + +// Output: {"level":"warn","severity":"warn"} +``` + +### Pass a sub-logger by context + +```go +ctx := log.With().Str("component", "module").Logger().WithContext(ctx) + +log.Ctx(ctx).Info().Msg("hello world") + +// Output: {"component":"module","level":"info","message":"hello world"} +``` + +### Set as standard logger output + +```go +log := zerolog.New(os.Stdout).With(). + Str("foo", "bar"). + Logger() + +stdlog.SetFlags(0) +stdlog.SetOutput(log) + +stdlog.Print("hello world") + +// Output: {"foo":"bar","message":"hello world"} +``` + +### Integration with `net/http` + +The `github.com/rs/zerolog/hlog` package provides some helpers to integrate zerolog with `http.Handler`. + +In this example we use [alice](https://github.com/justinas/alice) to install logger for better readability. + +```go +log := zerolog.New(os.Stdout).With(). + Timestamp(). + Str("role", "my-service"). + Str("host", host). + Logger() + +c := alice.New() + +// Install the logger handler with default output on the console +c = c.Append(hlog.NewHandler(log)) + +// Install some provided extra handler to set some request's context fields. +// Thanks to that handler, all our logs will come with some prepopulated fields. +c = c.Append(hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) { + hlog.FromRequest(r).Info(). + Str("method", r.Method). + Stringer("url", r.URL). + Int("status", status). + Int("size", size). + Dur("duration", duration). + Msg("") +})) +c = c.Append(hlog.RemoteAddrHandler("ip")) +c = c.Append(hlog.UserAgentHandler("user_agent")) +c = c.Append(hlog.RefererHandler("referer")) +c = c.Append(hlog.RequestIDHandler("req_id", "Request-Id")) + +// Here is your final handler +h := c.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Get the logger from the request's context. You can safely assume it + // will be always there: if the handler is removed, hlog.FromRequest + // will return a no-op logger. + hlog.FromRequest(r).Info(). + Str("user", "current user"). + Str("status", "ok"). + Msg("Something happened") + + // Output: {"level":"info","time":"2001-02-03T04:05:06Z","role":"my-service","host":"local-hostname","req_id":"b4g0l5t6tfid6dtrapu0","user":"current user","status":"ok","message":"Something happened"} +})) +http.Handle("/", h) + +if err := http.ListenAndServe(":8080", nil); err != nil { + log.Fatal().Err(err).Msg("Startup failed") +} +``` + +## Multiple Log Output +`zerolog.MultiLevelWriter` may be used to send the log message to multiple outputs. +In this example, we send the log message to both `os.Stdout` and the in-built ConsoleWriter. +```go +func main() { + consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout} + + multi := zerolog.MultiLevelWriter(consoleWriter, os.Stdout) + + logger := zerolog.New(multi).With().Timestamp().Logger() + + logger.Info().Msg("Hello World!") +} + +// Output (Line 1: Console; Line 2: Stdout) +// 12:36PM INF Hello World! +// {"level":"info","time":"2019-11-07T12:36:38+03:00","message":"Hello World!"} +``` + +## Global Settings + +Some settings can be changed and will be applied to all loggers: + +* `log.Logger`: You can set this value to customize the global logger (the one used by package level methods). +* `zerolog.SetGlobalLevel`: Can raise the minimum level of all loggers. Call this with `zerolog.Disabled` to disable logging altogether (quiet mode). +* `zerolog.DisableSampling`: If argument is `true`, all sampled loggers will stop sampling and issue 100% of their log events. +* `zerolog.TimestampFieldName`: Can be set to customize `Timestamp` field name. +* `zerolog.LevelFieldName`: Can be set to customize level field name. +* `zerolog.MessageFieldName`: Can be set to customize message field name. +* `zerolog.ErrorFieldName`: Can be set to customize `Err` field name. +* `zerolog.TimeFieldFormat`: Can be set to customize `Time` field value formatting. If set with `zerolog.TimeFormatUnix`, `zerolog.TimeFormatUnixMs` or `zerolog.TimeFormatUnixMicro`, times are formated as UNIX timestamp. +* `zerolog.DurationFieldUnit`: Can be set to customize the unit for time.Duration type fields added by `Dur` (default: `time.Millisecond`). +* `zerolog.DurationFieldInteger`: If set to `true`, `Dur` fields are formatted as integers instead of floats (default: `false`). +* `zerolog.ErrorHandler`: Called whenever zerolog fails to write an event on its output. If not set, an error is printed on the stderr. This handler must be thread safe and non-blocking. + +## Field Types + +### Standard Types + +* `Str` +* `Bool` +* `Int`, `Int8`, `Int16`, `Int32`, `Int64` +* `Uint`, `Uint8`, `Uint16`, `Uint32`, `Uint64` +* `Float32`, `Float64` + +### Advanced Fields + +* `Err`: Takes an `error` and renders it as a string using the `zerolog.ErrorFieldName` field name. +* `Func`: Run a `func` only if the level is enabled. +* `Timestamp`: Inserts a timestamp field with `zerolog.TimestampFieldName` field name, formatted using `zerolog.TimeFieldFormat`. +* `Time`: Adds a field with time formatted with `zerolog.TimeFieldFormat`. +* `Dur`: Adds a field with `time.Duration`. +* `Dict`: Adds a sub-key/value as a field of the event. +* `RawJSON`: Adds a field with an already encoded JSON (`[]byte`) +* `Hex`: Adds a field with value formatted as a hexadecimal string (`[]byte`) +* `Interface`: Uses reflection to marshal the type. + +Most fields are also available in the slice format (`Strs` for `[]string`, `Errs` for `[]error` etc.) + +## Binary Encoding + +In addition to the default JSON encoding, `zerolog` can produce binary logs using [CBOR](https://cbor.io) encoding. The choice of encoding can be decided at compile time using the build tag `binary_log` as follows: + +```bash +go build -tags binary_log . +``` + +To Decode binary encoded log files you can use any CBOR decoder. One has been tested to work +with zerolog library is [CSD](https://github.com/toravir/csd/). + +## Related Projects + +* [grpc-zerolog](https://github.com/cheapRoc/grpc-zerolog): Implementation of `grpclog.LoggerV2` interface using `zerolog` +* [overlog](https://github.com/Trendyol/overlog): Implementation of `Mapped Diagnostic Context` interface using `zerolog` +* [zerologr](https://github.com/go-logr/zerologr): Implementation of `logr.LogSink` interface using `zerolog` + +## Benchmarks + +See [logbench](http://hackemist.com/logbench/) for more comprehensive and up-to-date benchmarks. + +All operations are allocation free (those numbers *include* JSON encoding): + +```text +BenchmarkLogEmpty-8 100000000 19.1 ns/op 0 B/op 0 allocs/op +BenchmarkDisabled-8 500000000 4.07 ns/op 0 B/op 0 allocs/op +BenchmarkInfo-8 30000000 42.5 ns/op 0 B/op 0 allocs/op +BenchmarkContextFields-8 30000000 44.9 ns/op 0 B/op 0 allocs/op +BenchmarkLogFields-8 10000000 184 ns/op 0 B/op 0 allocs/op +``` + +There are a few Go logging benchmarks and comparisons that include zerolog. + +* [imkira/go-loggers-bench](https://github.com/imkira/go-loggers-bench) +* [uber-common/zap](https://github.com/uber-go/zap#performance) + +Using Uber's zap comparison benchmark: + +Log a message and 10 fields: + +| Library | Time | Bytes Allocated | Objects Allocated | +| :--- | :---: | :---: | :---: | +| zerolog | 767 ns/op | 552 B/op | 6 allocs/op | +| :zap: zap | 848 ns/op | 704 B/op | 2 allocs/op | +| :zap: zap (sugared) | 1363 ns/op | 1610 B/op | 20 allocs/op | +| go-kit | 3614 ns/op | 2895 B/op | 66 allocs/op | +| lion | 5392 ns/op | 5807 B/op | 63 allocs/op | +| logrus | 5661 ns/op | 6092 B/op | 78 allocs/op | +| apex/log | 15332 ns/op | 3832 B/op | 65 allocs/op | +| log15 | 20657 ns/op | 5632 B/op | 93 allocs/op | + +Log a message with a logger that already has 10 fields of context: + +| Library | Time | Bytes Allocated | Objects Allocated | +| :--- | :---: | :---: | :---: | +| zerolog | 52 ns/op | 0 B/op | 0 allocs/op | +| :zap: zap | 283 ns/op | 0 B/op | 0 allocs/op | +| :zap: zap (sugared) | 337 ns/op | 80 B/op | 2 allocs/op | +| lion | 2702 ns/op | 4074 B/op | 38 allocs/op | +| go-kit | 3378 ns/op | 3046 B/op | 52 allocs/op | +| logrus | 4309 ns/op | 4564 B/op | 63 allocs/op | +| apex/log | 13456 ns/op | 2898 B/op | 51 allocs/op | +| log15 | 14179 ns/op | 2642 B/op | 44 allocs/op | + +Log a static string, without any context or `printf`-style templating: + +| Library | Time | Bytes Allocated | Objects Allocated | +| :--- | :---: | :---: | :---: | +| zerolog | 50 ns/op | 0 B/op | 0 allocs/op | +| :zap: zap | 236 ns/op | 0 B/op | 0 allocs/op | +| standard library | 453 ns/op | 80 B/op | 2 allocs/op | +| :zap: zap (sugared) | 337 ns/op | 80 B/op | 2 allocs/op | +| go-kit | 508 ns/op | 656 B/op | 13 allocs/op | +| lion | 771 ns/op | 1224 B/op | 10 allocs/op | +| logrus | 1244 ns/op | 1505 B/op | 27 allocs/op | +| apex/log | 2751 ns/op | 584 B/op | 11 allocs/op | +| log15 | 5181 ns/op | 1592 B/op | 26 allocs/op | + +## Caveats + +Note that zerolog does no de-duplication of fields. Using the same key multiple times creates multiple keys in final JSON: + +```go +logger := zerolog.New(os.Stderr).With().Timestamp().Logger() +logger.Info(). + Timestamp(). + Msg("dup") +// Output: {"level":"info","time":1494567715,"time":1494567715,"message":"dup"} +``` + +In this case, many consumers will take the last value, but this is not guaranteed; check yours if in doubt. diff --git a/vendor/github.com/rs/zerolog/_config.yml b/vendor/github.com/rs/zerolog/_config.yml new file mode 100644 index 00000000..a1e896d7 --- /dev/null +++ b/vendor/github.com/rs/zerolog/_config.yml @@ -0,0 +1 @@ +remote_theme: rs/gh-readme diff --git a/vendor/github.com/rs/zerolog/array.go b/vendor/github.com/rs/zerolog/array.go new file mode 100644 index 00000000..c75c0520 --- /dev/null +++ b/vendor/github.com/rs/zerolog/array.go @@ -0,0 +1,240 @@ +package zerolog + +import ( + "net" + "sync" + "time" +) + +var arrayPool = &sync.Pool{ + New: func() interface{} { + return &Array{ + buf: make([]byte, 0, 500), + } + }, +} + +// Array is used to prepopulate an array of items +// which can be re-used to add to log messages. +type Array struct { + buf []byte +} + +func putArray(a *Array) { + // Proper usage of a sync.Pool requires each entry to have approximately + // the same memory cost. To obtain this property when the stored type + // contains a variably-sized buffer, we add a hard limit on the maximum buffer + // to place back in the pool. + // + // See https://golang.org/issue/23199 + const maxSize = 1 << 16 // 64KiB + if cap(a.buf) > maxSize { + return + } + arrayPool.Put(a) +} + +// Arr creates an array to be added to an Event or Context. +func Arr() *Array { + a := arrayPool.Get().(*Array) + a.buf = a.buf[:0] + return a +} + +// MarshalZerologArray method here is no-op - since data is +// already in the needed format. +func (*Array) MarshalZerologArray(*Array) { +} + +func (a *Array) write(dst []byte) []byte { + dst = enc.AppendArrayStart(dst) + if len(a.buf) > 0 { + dst = append(dst, a.buf...) + } + dst = enc.AppendArrayEnd(dst) + putArray(a) + return dst +} + +// Object marshals an object that implement the LogObjectMarshaler +// interface and append append it to the array. +func (a *Array) Object(obj LogObjectMarshaler) *Array { + e := Dict() + obj.MarshalZerologObject(e) + e.buf = enc.AppendEndMarker(e.buf) + a.buf = append(enc.AppendArrayDelim(a.buf), e.buf...) + putEvent(e) + return a +} + +// Str append append the val as a string to the array. +func (a *Array) Str(val string) *Array { + a.buf = enc.AppendString(enc.AppendArrayDelim(a.buf), val) + return a +} + +// Bytes append append the val as a string to the array. +func (a *Array) Bytes(val []byte) *Array { + a.buf = enc.AppendBytes(enc.AppendArrayDelim(a.buf), val) + return a +} + +// Hex append append the val as a hex string to the array. +func (a *Array) Hex(val []byte) *Array { + a.buf = enc.AppendHex(enc.AppendArrayDelim(a.buf), val) + return a +} + +// RawJSON adds already encoded JSON to the array. +func (a *Array) RawJSON(val []byte) *Array { + a.buf = appendJSON(enc.AppendArrayDelim(a.buf), val) + return a +} + +// Err serializes and appends the err to the array. +func (a *Array) Err(err error) *Array { + switch m := ErrorMarshalFunc(err).(type) { + case LogObjectMarshaler: + e := newEvent(nil, 0) + e.buf = e.buf[:0] + e.appendObject(m) + a.buf = append(enc.AppendArrayDelim(a.buf), e.buf...) + putEvent(e) + case error: + if m == nil || isNilValue(m) { + a.buf = enc.AppendNil(enc.AppendArrayDelim(a.buf)) + } else { + a.buf = enc.AppendString(enc.AppendArrayDelim(a.buf), m.Error()) + } + case string: + a.buf = enc.AppendString(enc.AppendArrayDelim(a.buf), m) + default: + a.buf = enc.AppendInterface(enc.AppendArrayDelim(a.buf), m) + } + + return a +} + +// Bool append append the val as a bool to the array. +func (a *Array) Bool(b bool) *Array { + a.buf = enc.AppendBool(enc.AppendArrayDelim(a.buf), b) + return a +} + +// Int append append i as a int to the array. +func (a *Array) Int(i int) *Array { + a.buf = enc.AppendInt(enc.AppendArrayDelim(a.buf), i) + return a +} + +// Int8 append append i as a int8 to the array. +func (a *Array) Int8(i int8) *Array { + a.buf = enc.AppendInt8(enc.AppendArrayDelim(a.buf), i) + return a +} + +// Int16 append append i as a int16 to the array. +func (a *Array) Int16(i int16) *Array { + a.buf = enc.AppendInt16(enc.AppendArrayDelim(a.buf), i) + return a +} + +// Int32 append append i as a int32 to the array. +func (a *Array) Int32(i int32) *Array { + a.buf = enc.AppendInt32(enc.AppendArrayDelim(a.buf), i) + return a +} + +// Int64 append append i as a int64 to the array. +func (a *Array) Int64(i int64) *Array { + a.buf = enc.AppendInt64(enc.AppendArrayDelim(a.buf), i) + return a +} + +// Uint append append i as a uint to the array. +func (a *Array) Uint(i uint) *Array { + a.buf = enc.AppendUint(enc.AppendArrayDelim(a.buf), i) + return a +} + +// Uint8 append append i as a uint8 to the array. +func (a *Array) Uint8(i uint8) *Array { + a.buf = enc.AppendUint8(enc.AppendArrayDelim(a.buf), i) + return a +} + +// Uint16 append append i as a uint16 to the array. +func (a *Array) Uint16(i uint16) *Array { + a.buf = enc.AppendUint16(enc.AppendArrayDelim(a.buf), i) + return a +} + +// Uint32 append append i as a uint32 to the array. +func (a *Array) Uint32(i uint32) *Array { + a.buf = enc.AppendUint32(enc.AppendArrayDelim(a.buf), i) + return a +} + +// Uint64 append append i as a uint64 to the array. +func (a *Array) Uint64(i uint64) *Array { + a.buf = enc.AppendUint64(enc.AppendArrayDelim(a.buf), i) + return a +} + +// Float32 append append f as a float32 to the array. +func (a *Array) Float32(f float32) *Array { + a.buf = enc.AppendFloat32(enc.AppendArrayDelim(a.buf), f) + return a +} + +// Float64 append append f as a float64 to the array. +func (a *Array) Float64(f float64) *Array { + a.buf = enc.AppendFloat64(enc.AppendArrayDelim(a.buf), f) + return a +} + +// Time append append t formatted as string using zerolog.TimeFieldFormat. +func (a *Array) Time(t time.Time) *Array { + a.buf = enc.AppendTime(enc.AppendArrayDelim(a.buf), t, TimeFieldFormat) + return a +} + +// Dur append append d to the array. +func (a *Array) Dur(d time.Duration) *Array { + a.buf = enc.AppendDuration(enc.AppendArrayDelim(a.buf), d, DurationFieldUnit, DurationFieldInteger) + return a +} + +// Interface append append i marshaled using reflection. +func (a *Array) Interface(i interface{}) *Array { + if obj, ok := i.(LogObjectMarshaler); ok { + return a.Object(obj) + } + a.buf = enc.AppendInterface(enc.AppendArrayDelim(a.buf), i) + return a +} + +// IPAddr adds IPv4 or IPv6 address to the array +func (a *Array) IPAddr(ip net.IP) *Array { + a.buf = enc.AppendIPAddr(enc.AppendArrayDelim(a.buf), ip) + return a +} + +// IPPrefix adds IPv4 or IPv6 Prefix (IP + mask) to the array +func (a *Array) IPPrefix(pfx net.IPNet) *Array { + a.buf = enc.AppendIPPrefix(enc.AppendArrayDelim(a.buf), pfx) + return a +} + +// MACAddr adds a MAC (Ethernet) address to the array +func (a *Array) MACAddr(ha net.HardwareAddr) *Array { + a.buf = enc.AppendMACAddr(enc.AppendArrayDelim(a.buf), ha) + return a +} + +// Dict adds the dict Event to the array +func (a *Array) Dict(dict *Event) *Array { + dict.buf = enc.AppendEndMarker(dict.buf) + a.buf = append(enc.AppendArrayDelim(a.buf), dict.buf...) + return a +} diff --git a/vendor/github.com/rs/zerolog/console.go b/vendor/github.com/rs/zerolog/console.go new file mode 100644 index 00000000..8b0e0c61 --- /dev/null +++ b/vendor/github.com/rs/zerolog/console.go @@ -0,0 +1,450 @@ +package zerolog + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/mattn/go-colorable" +) + +const ( + colorBlack = iota + 30 + colorRed + colorGreen + colorYellow + colorBlue + colorMagenta + colorCyan + colorWhite + + colorBold = 1 + colorDarkGray = 90 +) + +var ( + consoleBufPool = sync.Pool{ + New: func() interface{} { + return bytes.NewBuffer(make([]byte, 0, 100)) + }, + } +) + +const ( + consoleDefaultTimeFormat = time.Kitchen +) + +// Formatter transforms the input into a formatted string. +type Formatter func(interface{}) string + +// ConsoleWriter parses the JSON input and writes it in an +// (optionally) colorized, human-friendly format to Out. +type ConsoleWriter struct { + // Out is the output destination. + Out io.Writer + + // NoColor disables the colorized output. + NoColor bool + + // TimeFormat specifies the format for timestamp in output. + TimeFormat string + + // PartsOrder defines the order of parts in output. + PartsOrder []string + + // PartsExclude defines parts to not display in output. + PartsExclude []string + + // FieldsExclude defines contextual fields to not display in output. + FieldsExclude []string + + FormatTimestamp Formatter + FormatLevel Formatter + FormatCaller Formatter + FormatMessage Formatter + FormatFieldName Formatter + FormatFieldValue Formatter + FormatErrFieldName Formatter + FormatErrFieldValue Formatter + + FormatExtra func(map[string]interface{}, *bytes.Buffer) error +} + +// NewConsoleWriter creates and initializes a new ConsoleWriter. +func NewConsoleWriter(options ...func(w *ConsoleWriter)) ConsoleWriter { + w := ConsoleWriter{ + Out: os.Stdout, + TimeFormat: consoleDefaultTimeFormat, + PartsOrder: consoleDefaultPartsOrder(), + } + + for _, opt := range options { + opt(&w) + } + + // Fix color on Windows + if w.Out == os.Stdout || w.Out == os.Stderr { + w.Out = colorable.NewColorable(w.Out.(*os.File)) + } + + return w +} + +// Write transforms the JSON input with formatters and appends to w.Out. +func (w ConsoleWriter) Write(p []byte) (n int, err error) { + // Fix color on Windows + if w.Out == os.Stdout || w.Out == os.Stderr { + w.Out = colorable.NewColorable(w.Out.(*os.File)) + } + + if w.PartsOrder == nil { + w.PartsOrder = consoleDefaultPartsOrder() + } + + var buf = consoleBufPool.Get().(*bytes.Buffer) + defer func() { + buf.Reset() + consoleBufPool.Put(buf) + }() + + var evt map[string]interface{} + p = decodeIfBinaryToBytes(p) + d := json.NewDecoder(bytes.NewReader(p)) + d.UseNumber() + err = d.Decode(&evt) + if err != nil { + return n, fmt.Errorf("cannot decode event: %s", err) + } + + for _, p := range w.PartsOrder { + w.writePart(buf, evt, p) + } + + w.writeFields(evt, buf) + + if w.FormatExtra != nil { + err = w.FormatExtra(evt, buf) + if err != nil { + return n, err + } + } + + err = buf.WriteByte('\n') + if err != nil { + return n, err + } + + _, err = buf.WriteTo(w.Out) + return len(p), err +} + +// writeFields appends formatted key-value pairs to buf. +func (w ConsoleWriter) writeFields(evt map[string]interface{}, buf *bytes.Buffer) { + var fields = make([]string, 0, len(evt)) + for field := range evt { + var isExcluded bool + for _, excluded := range w.FieldsExclude { + if field == excluded { + isExcluded = true + break + } + } + if isExcluded { + continue + } + + switch field { + case LevelFieldName, TimestampFieldName, MessageFieldName, CallerFieldName: + continue + } + fields = append(fields, field) + } + sort.Strings(fields) + + // Write space only if something has already been written to the buffer, and if there are fields. + if buf.Len() > 0 && len(fields) > 0 { + buf.WriteByte(' ') + } + + // Move the "error" field to the front + ei := sort.Search(len(fields), func(i int) bool { return fields[i] >= ErrorFieldName }) + if ei < len(fields) && fields[ei] == ErrorFieldName { + fields[ei] = "" + fields = append([]string{ErrorFieldName}, fields...) + var xfields = make([]string, 0, len(fields)) + for _, field := range fields { + if field == "" { // Skip empty fields + continue + } + xfields = append(xfields, field) + } + fields = xfields + } + + for i, field := range fields { + var fn Formatter + var fv Formatter + + if field == ErrorFieldName { + if w.FormatErrFieldName == nil { + fn = consoleDefaultFormatErrFieldName(w.NoColor) + } else { + fn = w.FormatErrFieldName + } + + if w.FormatErrFieldValue == nil { + fv = consoleDefaultFormatErrFieldValue(w.NoColor) + } else { + fv = w.FormatErrFieldValue + } + } else { + if w.FormatFieldName == nil { + fn = consoleDefaultFormatFieldName(w.NoColor) + } else { + fn = w.FormatFieldName + } + + if w.FormatFieldValue == nil { + fv = consoleDefaultFormatFieldValue + } else { + fv = w.FormatFieldValue + } + } + + buf.WriteString(fn(field)) + + switch fValue := evt[field].(type) { + case string: + if needsQuote(fValue) { + buf.WriteString(fv(strconv.Quote(fValue))) + } else { + buf.WriteString(fv(fValue)) + } + case json.Number: + buf.WriteString(fv(fValue)) + default: + b, err := InterfaceMarshalFunc(fValue) + if err != nil { + fmt.Fprintf(buf, colorize("[error: %v]", colorRed, w.NoColor), err) + } else { + fmt.Fprint(buf, fv(b)) + } + } + + if i < len(fields)-1 { // Skip space for last field + buf.WriteByte(' ') + } + } +} + +// writePart appends a formatted part to buf. +func (w ConsoleWriter) writePart(buf *bytes.Buffer, evt map[string]interface{}, p string) { + var f Formatter + + if w.PartsExclude != nil && len(w.PartsExclude) > 0 { + for _, exclude := range w.PartsExclude { + if exclude == p { + return + } + } + } + + switch p { + case LevelFieldName: + if w.FormatLevel == nil { + f = consoleDefaultFormatLevel(w.NoColor) + } else { + f = w.FormatLevel + } + case TimestampFieldName: + if w.FormatTimestamp == nil { + f = consoleDefaultFormatTimestamp(w.TimeFormat, w.NoColor) + } else { + f = w.FormatTimestamp + } + case MessageFieldName: + if w.FormatMessage == nil { + f = consoleDefaultFormatMessage + } else { + f = w.FormatMessage + } + case CallerFieldName: + if w.FormatCaller == nil { + f = consoleDefaultFormatCaller(w.NoColor) + } else { + f = w.FormatCaller + } + default: + if w.FormatFieldValue == nil { + f = consoleDefaultFormatFieldValue + } else { + f = w.FormatFieldValue + } + } + + var s = f(evt[p]) + + if len(s) > 0 { + if buf.Len() > 0 { + buf.WriteByte(' ') // Write space only if not the first part + } + buf.WriteString(s) + } +} + +// needsQuote returns true when the string s should be quoted in output. +func needsQuote(s string) bool { + for i := range s { + if s[i] < 0x20 || s[i] > 0x7e || s[i] == ' ' || s[i] == '\\' || s[i] == '"' { + return true + } + } + return false +} + +// colorize returns the string s wrapped in ANSI code c, unless disabled is true. +func colorize(s interface{}, c int, disabled bool) string { + if disabled { + return fmt.Sprintf("%s", s) + } + return fmt.Sprintf("\x1b[%dm%v\x1b[0m", c, s) +} + +// ----- DEFAULT FORMATTERS --------------------------------------------------- + +func consoleDefaultPartsOrder() []string { + return []string{ + TimestampFieldName, + LevelFieldName, + CallerFieldName, + MessageFieldName, + } +} + +func consoleDefaultFormatTimestamp(timeFormat string, noColor bool) Formatter { + if timeFormat == "" { + timeFormat = consoleDefaultTimeFormat + } + return func(i interface{}) string { + t := "<nil>" + switch tt := i.(type) { + case string: + ts, err := time.ParseInLocation(TimeFieldFormat, tt, time.Local) + if err != nil { + t = tt + } else { + t = ts.Local().Format(timeFormat) + } + case json.Number: + i, err := tt.Int64() + if err != nil { + t = tt.String() + } else { + var sec, nsec int64 + + switch TimeFieldFormat { + case TimeFormatUnixNano: + sec, nsec = 0, i + case TimeFormatUnixMicro: + sec, nsec = 0, int64(time.Duration(i)*time.Microsecond) + case TimeFormatUnixMs: + sec, nsec = 0, int64(time.Duration(i)*time.Millisecond) + default: + sec, nsec = i, 0 + } + + ts := time.Unix(sec, nsec) + t = ts.Format(timeFormat) + } + } + return colorize(t, colorDarkGray, noColor) + } +} + +func consoleDefaultFormatLevel(noColor bool) Formatter { + return func(i interface{}) string { + var l string + if ll, ok := i.(string); ok { + switch ll { + case LevelTraceValue: + l = colorize("TRC", colorMagenta, noColor) + case LevelDebugValue: + l = colorize("DBG", colorYellow, noColor) + case LevelInfoValue: + l = colorize("INF", colorGreen, noColor) + case LevelWarnValue: + l = colorize("WRN", colorRed, noColor) + case LevelErrorValue: + l = colorize(colorize("ERR", colorRed, noColor), colorBold, noColor) + case LevelFatalValue: + l = colorize(colorize("FTL", colorRed, noColor), colorBold, noColor) + case LevelPanicValue: + l = colorize(colorize("PNC", colorRed, noColor), colorBold, noColor) + default: + l = colorize(ll, colorBold, noColor) + } + } else { + if i == nil { + l = colorize("???", colorBold, noColor) + } else { + l = strings.ToUpper(fmt.Sprintf("%s", i))[0:3] + } + } + return l + } +} + +func consoleDefaultFormatCaller(noColor bool) Formatter { + return func(i interface{}) string { + var c string + if cc, ok := i.(string); ok { + c = cc + } + if len(c) > 0 { + if cwd, err := os.Getwd(); err == nil { + if rel, err := filepath.Rel(cwd, c); err == nil { + c = rel + } + } + c = colorize(c, colorBold, noColor) + colorize(" >", colorCyan, noColor) + } + return c + } +} + +func consoleDefaultFormatMessage(i interface{}) string { + if i == nil { + return "" + } + return fmt.Sprintf("%s", i) +} + +func consoleDefaultFormatFieldName(noColor bool) Formatter { + return func(i interface{}) string { + return colorize(fmt.Sprintf("%s=", i), colorCyan, noColor) + } +} + +func consoleDefaultFormatFieldValue(i interface{}) string { + return fmt.Sprintf("%s", i) +} + +func consoleDefaultFormatErrFieldName(noColor bool) Formatter { + return func(i interface{}) string { + return colorize(fmt.Sprintf("%s=", i), colorCyan, noColor) + } +} + +func consoleDefaultFormatErrFieldValue(noColor bool) Formatter { + return func(i interface{}) string { + return colorize(fmt.Sprintf("%s", i), colorRed, noColor) + } +} diff --git a/vendor/github.com/rs/zerolog/context.go b/vendor/github.com/rs/zerolog/context.go new file mode 100644 index 00000000..f398e319 --- /dev/null +++ b/vendor/github.com/rs/zerolog/context.go @@ -0,0 +1,433 @@ +package zerolog + +import ( + "fmt" + "io/ioutil" + "math" + "net" + "time" +) + +// Context configures a new sub-logger with contextual fields. +type Context struct { + l Logger +} + +// Logger returns the logger with the context previously set. +func (c Context) Logger() Logger { + return c.l +} + +// Fields is a helper function to use a map or slice to set fields using type assertion. +// Only map[string]interface{} and []interface{} are accepted. []interface{} must +// alternate string keys and arbitrary values, and extraneous ones are ignored. +func (c Context) Fields(fields interface{}) Context { + c.l.context = appendFields(c.l.context, fields) + return c +} + +// Dict adds the field key with the dict to the logger context. +func (c Context) Dict(key string, dict *Event) Context { + dict.buf = enc.AppendEndMarker(dict.buf) + c.l.context = append(enc.AppendKey(c.l.context, key), dict.buf...) + putEvent(dict) + return c +} + +// Array adds the field key with an array to the event context. +// Use zerolog.Arr() to create the array or pass a type that +// implement the LogArrayMarshaler interface. +func (c Context) Array(key string, arr LogArrayMarshaler) Context { + c.l.context = enc.AppendKey(c.l.context, key) + if arr, ok := arr.(*Array); ok { + c.l.context = arr.write(c.l.context) + return c + } + var a *Array + if aa, ok := arr.(*Array); ok { + a = aa + } else { + a = Arr() + arr.MarshalZerologArray(a) + } + c.l.context = a.write(c.l.context) + return c +} + +// Object marshals an object that implement the LogObjectMarshaler interface. +func (c Context) Object(key string, obj LogObjectMarshaler) Context { + e := newEvent(levelWriterAdapter{ioutil.Discard}, 0) + e.Object(key, obj) + c.l.context = enc.AppendObjectData(c.l.context, e.buf) + putEvent(e) + return c +} + +// EmbedObject marshals and Embeds an object that implement the LogObjectMarshaler interface. +func (c Context) EmbedObject(obj LogObjectMarshaler) Context { + e := newEvent(levelWriterAdapter{ioutil.Discard}, 0) + e.EmbedObject(obj) + c.l.context = enc.AppendObjectData(c.l.context, e.buf) + putEvent(e) + return c +} + +// Str adds the field key with val as a string to the logger context. +func (c Context) Str(key, val string) Context { + c.l.context = enc.AppendString(enc.AppendKey(c.l.context, key), val) + return c +} + +// Strs adds the field key with val as a string to the logger context. +func (c Context) Strs(key string, vals []string) Context { + c.l.context = enc.AppendStrings(enc.AppendKey(c.l.context, key), vals) + return c +} + +// Stringer adds the field key with val.String() (or null if val is nil) to the logger context. +func (c Context) Stringer(key string, val fmt.Stringer) Context { + if val != nil { + c.l.context = enc.AppendString(enc.AppendKey(c.l.context, key), val.String()) + return c + } + + c.l.context = enc.AppendInterface(enc.AppendKey(c.l.context, key), nil) + return c +} + +// Bytes adds the field key with val as a []byte to the logger context. +func (c Context) Bytes(key string, val []byte) Context { + c.l.context = enc.AppendBytes(enc.AppendKey(c.l.context, key), val) + return c +} + +// Hex adds the field key with val as a hex string to the logger context. +func (c Context) Hex(key string, val []byte) Context { + c.l.context = enc.AppendHex(enc.AppendKey(c.l.context, key), val) + return c +} + +// RawJSON adds already encoded JSON to context. +// +// No sanity check is performed on b; it must not contain carriage returns and +// be valid JSON. +func (c Context) RawJSON(key string, b []byte) Context { + c.l.context = appendJSON(enc.AppendKey(c.l.context, key), b) + return c +} + +// AnErr adds the field key with serialized err to the logger context. +func (c Context) AnErr(key string, err error) Context { + switch m := ErrorMarshalFunc(err).(type) { + case nil: + return c + case LogObjectMarshaler: + return c.Object(key, m) + case error: + if m == nil || isNilValue(m) { + return c + } else { + return c.Str(key, m.Error()) + } + case string: + return c.Str(key, m) + default: + return c.Interface(key, m) + } +} + +// Errs adds the field key with errs as an array of serialized errors to the +// logger context. +func (c Context) Errs(key string, errs []error) Context { + arr := Arr() + for _, err := range errs { + switch m := ErrorMarshalFunc(err).(type) { + case LogObjectMarshaler: + arr = arr.Object(m) + case error: + if m == nil || isNilValue(m) { + arr = arr.Interface(nil) + } else { + arr = arr.Str(m.Error()) + } + case string: + arr = arr.Str(m) + default: + arr = arr.Interface(m) + } + } + + return c.Array(key, arr) +} + +// Err adds the field "error" with serialized err to the logger context. +func (c Context) Err(err error) Context { + return c.AnErr(ErrorFieldName, err) +} + +// Bool adds the field key with val as a bool to the logger context. +func (c Context) Bool(key string, b bool) Context { + c.l.context = enc.AppendBool(enc.AppendKey(c.l.context, key), b) + return c +} + +// Bools adds the field key with val as a []bool to the logger context. +func (c Context) Bools(key string, b []bool) Context { + c.l.context = enc.AppendBools(enc.AppendKey(c.l.context, key), b) + return c +} + +// Int adds the field key with i as a int to the logger context. +func (c Context) Int(key string, i int) Context { + c.l.context = enc.AppendInt(enc.AppendKey(c.l.context, key), i) + return c +} + +// Ints adds the field key with i as a []int to the logger context. +func (c Context) Ints(key string, i []int) Context { + c.l.context = enc.AppendInts(enc.AppendKey(c.l.context, key), i) + return c +} + +// Int8 adds the field key with i as a int8 to the logger context. +func (c Context) Int8(key string, i int8) Context { + c.l.context = enc.AppendInt8(enc.AppendKey(c.l.context, key), i) + return c +} + +// Ints8 adds the field key with i as a []int8 to the logger context. +func (c Context) Ints8(key string, i []int8) Context { + c.l.context = enc.AppendInts8(enc.AppendKey(c.l.context, key), i) + return c +} + +// Int16 adds the field key with i as a int16 to the logger context. +func (c Context) Int16(key string, i int16) Context { + c.l.context = enc.AppendInt16(enc.AppendKey(c.l.context, key), i) + return c +} + +// Ints16 adds the field key with i as a []int16 to the logger context. +func (c Context) Ints16(key string, i []int16) Context { + c.l.context = enc.AppendInts16(enc.AppendKey(c.l.context, key), i) + return c +} + +// Int32 adds the field key with i as a int32 to the logger context. +func (c Context) Int32(key string, i int32) Context { + c.l.context = enc.AppendInt32(enc.AppendKey(c.l.context, key), i) + return c +} + +// Ints32 adds the field key with i as a []int32 to the logger context. +func (c Context) Ints32(key string, i []int32) Context { + c.l.context = enc.AppendInts32(enc.AppendKey(c.l.context, key), i) + return c +} + +// Int64 adds the field key with i as a int64 to the logger context. +func (c Context) Int64(key string, i int64) Context { + c.l.context = enc.AppendInt64(enc.AppendKey(c.l.context, key), i) + return c +} + +// Ints64 adds the field key with i as a []int64 to the logger context. +func (c Context) Ints64(key string, i []int64) Context { + c.l.context = enc.AppendInts64(enc.AppendKey(c.l.context, key), i) + return c +} + +// Uint adds the field key with i as a uint to the logger context. +func (c Context) Uint(key string, i uint) Context { + c.l.context = enc.AppendUint(enc.AppendKey(c.l.context, key), i) + return c +} + +// Uints adds the field key with i as a []uint to the logger context. +func (c Context) Uints(key string, i []uint) Context { + c.l.context = enc.AppendUints(enc.AppendKey(c.l.context, key), i) + return c +} + +// Uint8 adds the field key with i as a uint8 to the logger context. +func (c Context) Uint8(key string, i uint8) Context { + c.l.context = enc.AppendUint8(enc.AppendKey(c.l.context, key), i) + return c +} + +// Uints8 adds the field key with i as a []uint8 to the logger context. +func (c Context) Uints8(key string, i []uint8) Context { + c.l.context = enc.AppendUints8(enc.AppendKey(c.l.context, key), i) + return c +} + +// Uint16 adds the field key with i as a uint16 to the logger context. +func (c Context) Uint16(key string, i uint16) Context { + c.l.context = enc.AppendUint16(enc.AppendKey(c.l.context, key), i) + return c +} + +// Uints16 adds the field key with i as a []uint16 to the logger context. +func (c Context) Uints16(key string, i []uint16) Context { + c.l.context = enc.AppendUints16(enc.AppendKey(c.l.context, key), i) + return c +} + +// Uint32 adds the field key with i as a uint32 to the logger context. +func (c Context) Uint32(key string, i uint32) Context { + c.l.context = enc.AppendUint32(enc.AppendKey(c.l.context, key), i) + return c +} + +// Uints32 adds the field key with i as a []uint32 to the logger context. +func (c Context) Uints32(key string, i []uint32) Context { + c.l.context = enc.AppendUints32(enc.AppendKey(c.l.context, key), i) + return c +} + +// Uint64 adds the field key with i as a uint64 to the logger context. +func (c Context) Uint64(key string, i uint64) Context { + c.l.context = enc.AppendUint64(enc.AppendKey(c.l.context, key), i) + return c +} + +// Uints64 adds the field key with i as a []uint64 to the logger context. +func (c Context) Uints64(key string, i []uint64) Context { + c.l.context = enc.AppendUints64(enc.AppendKey(c.l.context, key), i) + return c +} + +// Float32 adds the field key with f as a float32 to the logger context. +func (c Context) Float32(key string, f float32) Context { + c.l.context = enc.AppendFloat32(enc.AppendKey(c.l.context, key), f) + return c +} + +// Floats32 adds the field key with f as a []float32 to the logger context. +func (c Context) Floats32(key string, f []float32) Context { + c.l.context = enc.AppendFloats32(enc.AppendKey(c.l.context, key), f) + return c +} + +// Float64 adds the field key with f as a float64 to the logger context. +func (c Context) Float64(key string, f float64) Context { + c.l.context = enc.AppendFloat64(enc.AppendKey(c.l.context, key), f) + return c +} + +// Floats64 adds the field key with f as a []float64 to the logger context. +func (c Context) Floats64(key string, f []float64) Context { + c.l.context = enc.AppendFloats64(enc.AppendKey(c.l.context, key), f) + return c +} + +type timestampHook struct{} + +func (ts timestampHook) Run(e *Event, level Level, msg string) { + e.Timestamp() +} + +var th = timestampHook{} + +// Timestamp adds the current local time as UNIX timestamp to the logger context with the "time" key. +// To customize the key name, change zerolog.TimestampFieldName. +// +// NOTE: It won't dedupe the "time" key if the *Context has one already. +func (c Context) Timestamp() Context { + c.l = c.l.Hook(th) + return c +} + +// Time adds the field key with t formated as string using zerolog.TimeFieldFormat. +func (c Context) Time(key string, t time.Time) Context { + c.l.context = enc.AppendTime(enc.AppendKey(c.l.context, key), t, TimeFieldFormat) + return c +} + +// Times adds the field key with t formated as string using zerolog.TimeFieldFormat. +func (c Context) Times(key string, t []time.Time) Context { + c.l.context = enc.AppendTimes(enc.AppendKey(c.l.context, key), t, TimeFieldFormat) + return c +} + +// Dur adds the fields key with d divided by unit and stored as a float. +func (c Context) Dur(key string, d time.Duration) Context { + c.l.context = enc.AppendDuration(enc.AppendKey(c.l.context, key), d, DurationFieldUnit, DurationFieldInteger) + return c +} + +// Durs adds the fields key with d divided by unit and stored as a float. +func (c Context) Durs(key string, d []time.Duration) Context { + c.l.context = enc.AppendDurations(enc.AppendKey(c.l.context, key), d, DurationFieldUnit, DurationFieldInteger) + return c +} + +// Interface adds the field key with obj marshaled using reflection. +func (c Context) Interface(key string, i interface{}) Context { + c.l.context = enc.AppendInterface(enc.AppendKey(c.l.context, key), i) + return c +} + +type callerHook struct { + callerSkipFrameCount int +} + +func newCallerHook(skipFrameCount int) callerHook { + return callerHook{callerSkipFrameCount: skipFrameCount} +} + +func (ch callerHook) Run(e *Event, level Level, msg string) { + switch ch.callerSkipFrameCount { + case useGlobalSkipFrameCount: + // Extra frames to skip (added by hook infra). + e.caller(CallerSkipFrameCount + contextCallerSkipFrameCount) + default: + // Extra frames to skip (added by hook infra). + e.caller(ch.callerSkipFrameCount + contextCallerSkipFrameCount) + } +} + +// useGlobalSkipFrameCount acts as a flag to informat callerHook.Run +// to use the global CallerSkipFrameCount. +const useGlobalSkipFrameCount = math.MinInt32 + +// ch is the default caller hook using the global CallerSkipFrameCount. +var ch = newCallerHook(useGlobalSkipFrameCount) + +// Caller adds the file:line of the caller with the zerolog.CallerFieldName key. +func (c Context) Caller() Context { + c.l = c.l.Hook(ch) + return c +} + +// CallerWithSkipFrameCount adds the file:line of the caller with the zerolog.CallerFieldName key. +// The specified skipFrameCount int will override the global CallerSkipFrameCount for this context's respective logger. +// If set to -1 the global CallerSkipFrameCount will be used. +func (c Context) CallerWithSkipFrameCount(skipFrameCount int) Context { + c.l = c.l.Hook(newCallerHook(skipFrameCount)) + return c +} + +// Stack enables stack trace printing for the error passed to Err(). +func (c Context) Stack() Context { + c.l.stack = true + return c +} + +// IPAddr adds IPv4 or IPv6 Address to the context +func (c Context) IPAddr(key string, ip net.IP) Context { + c.l.context = enc.AppendIPAddr(enc.AppendKey(c.l.context, key), ip) + return c +} + +// IPPrefix adds IPv4 or IPv6 Prefix (address and mask) to the context +func (c Context) IPPrefix(key string, pfx net.IPNet) Context { + c.l.context = enc.AppendIPPrefix(enc.AppendKey(c.l.context, key), pfx) + return c +} + +// MACAddr adds MAC address to the context +func (c Context) MACAddr(key string, ha net.HardwareAddr) Context { + c.l.context = enc.AppendMACAddr(enc.AppendKey(c.l.context, key), ha) + return c +} diff --git a/vendor/github.com/rs/zerolog/ctx.go b/vendor/github.com/rs/zerolog/ctx.go new file mode 100644 index 00000000..60432d15 --- /dev/null +++ b/vendor/github.com/rs/zerolog/ctx.go @@ -0,0 +1,52 @@ +package zerolog + +import ( + "context" +) + +var disabledLogger *Logger + +func init() { + SetGlobalLevel(TraceLevel) + l := Nop() + disabledLogger = &l +} + +type ctxKey struct{} + +// WithContext returns a copy of ctx with the receiver attached. The Logger +// attached to the provided Context (if any) will not be effected. If the +// receiver's log level is Disabled it will only be attached to the returned +// Context if the provided Context has a previously attached Logger. If the +// provided Context has no attached Logger, a Disabled Logger will not be +// attached. +// +// Note: to modify the existing Logger attached to a Context (instead of +// replacing it in a new Context), use UpdateContext with the following +// notation: +// +// ctx := r.Context() +// l := zerolog.Ctx(ctx) +// l.UpdateContext(func(c Context) Context { +// return c.Str("bar", "baz") +// }) +// +func (l Logger) WithContext(ctx context.Context) context.Context { + if _, ok := ctx.Value(ctxKey{}).(*Logger); !ok && l.level == Disabled { + // Do not store disabled logger. + return ctx + } + return context.WithValue(ctx, ctxKey{}, &l) +} + +// Ctx returns the Logger associated with the ctx. If no logger +// is associated, DefaultContextLogger is returned, unless DefaultContextLogger +// is nil, in which case a disabled logger is returned. +func Ctx(ctx context.Context) *Logger { + if l, ok := ctx.Value(ctxKey{}).(*Logger); ok { + return l + } else if l = DefaultContextLogger; l != nil { + return l + } + return disabledLogger +} diff --git a/vendor/github.com/rs/zerolog/encoder.go b/vendor/github.com/rs/zerolog/encoder.go new file mode 100644 index 00000000..09b24e80 --- /dev/null +++ b/vendor/github.com/rs/zerolog/encoder.go @@ -0,0 +1,56 @@ +package zerolog + +import ( + "net" + "time" +) + +type encoder interface { + AppendArrayDelim(dst []byte) []byte + AppendArrayEnd(dst []byte) []byte + AppendArrayStart(dst []byte) []byte + AppendBeginMarker(dst []byte) []byte + AppendBool(dst []byte, val bool) []byte + AppendBools(dst []byte, vals []bool) []byte + AppendBytes(dst, s []byte) []byte + AppendDuration(dst []byte, d time.Duration, unit time.Duration, useInt bool) []byte + AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, useInt bool) []byte + AppendEndMarker(dst []byte) []byte + AppendFloat32(dst []byte, val float32) []byte + AppendFloat64(dst []byte, val float64) []byte + AppendFloats32(dst []byte, vals []float32) []byte + AppendFloats64(dst []byte, vals []float64) []byte + AppendHex(dst, s []byte) []byte + AppendIPAddr(dst []byte, ip net.IP) []byte + AppendIPPrefix(dst []byte, pfx net.IPNet) []byte + AppendInt(dst []byte, val int) []byte + AppendInt16(dst []byte, val int16) []byte + AppendInt32(dst []byte, val int32) []byte + AppendInt64(dst []byte, val int64) []byte + AppendInt8(dst []byte, val int8) []byte + AppendInterface(dst []byte, i interface{}) []byte + AppendInts(dst []byte, vals []int) []byte + AppendInts16(dst []byte, vals []int16) []byte + AppendInts32(dst []byte, vals []int32) []byte + AppendInts64(dst []byte, vals []int64) []byte + AppendInts8(dst []byte, vals []int8) []byte + AppendKey(dst []byte, key string) []byte + AppendLineBreak(dst []byte) []byte + AppendMACAddr(dst []byte, ha net.HardwareAddr) []byte + AppendNil(dst []byte) []byte + AppendObjectData(dst []byte, o []byte) []byte + AppendString(dst []byte, s string) []byte + AppendStrings(dst []byte, vals []string) []byte + AppendTime(dst []byte, t time.Time, format string) []byte + AppendTimes(dst []byte, vals []time.Time, format string) []byte + AppendUint(dst []byte, val uint) []byte + AppendUint16(dst []byte, val uint16) []byte + AppendUint32(dst []byte, val uint32) []byte + AppendUint64(dst []byte, val uint64) []byte + AppendUint8(dst []byte, val uint8) []byte + AppendUints(dst []byte, vals []uint) []byte + AppendUints16(dst []byte, vals []uint16) []byte + AppendUints32(dst []byte, vals []uint32) []byte + AppendUints64(dst []byte, vals []uint64) []byte + AppendUints8(dst []byte, vals []uint8) []byte +} diff --git a/vendor/github.com/rs/zerolog/encoder_cbor.go b/vendor/github.com/rs/zerolog/encoder_cbor.go new file mode 100644 index 00000000..7b0dafef --- /dev/null +++ b/vendor/github.com/rs/zerolog/encoder_cbor.go @@ -0,0 +1,42 @@ +// +build binary_log + +package zerolog + +// This file contains bindings to do binary encoding. + +import ( + "github.com/rs/zerolog/internal/cbor" +) + +var ( + _ encoder = (*cbor.Encoder)(nil) + + enc = cbor.Encoder{} +) + +func init() { + // using closure to reflect the changes at runtime. + cbor.JSONMarshalFunc = func(v interface{}) ([]byte, error) { + return InterfaceMarshalFunc(v) + } +} + +func appendJSON(dst []byte, j []byte) []byte { + return cbor.AppendEmbeddedJSON(dst, j) +} + +// decodeIfBinaryToString - converts a binary formatted log msg to a +// JSON formatted String Log message. +func decodeIfBinaryToString(in []byte) string { + return cbor.DecodeIfBinaryToString(in) +} + +func decodeObjectToStr(in []byte) string { + return cbor.DecodeObjectToStr(in) +} + +// decodeIfBinaryToBytes - converts a binary formatted log msg to a +// JSON formatted Bytes Log message. +func decodeIfBinaryToBytes(in []byte) []byte { + return cbor.DecodeIfBinaryToBytes(in) +} diff --git a/vendor/github.com/rs/zerolog/encoder_json.go b/vendor/github.com/rs/zerolog/encoder_json.go new file mode 100644 index 00000000..0e0450e2 --- /dev/null +++ b/vendor/github.com/rs/zerolog/encoder_json.go @@ -0,0 +1,39 @@ +// +build !binary_log + +package zerolog + +// encoder_json.go file contains bindings to generate +// JSON encoded byte stream. + +import ( + "github.com/rs/zerolog/internal/json" +) + +var ( + _ encoder = (*json.Encoder)(nil) + + enc = json.Encoder{} +) + +func init() { + // using closure to reflect the changes at runtime. + json.JSONMarshalFunc = func(v interface{}) ([]byte, error) { + return InterfaceMarshalFunc(v) + } +} + +func appendJSON(dst []byte, j []byte) []byte { + return append(dst, j...) +} + +func decodeIfBinaryToString(in []byte) string { + return string(in) +} + +func decodeObjectToStr(in []byte) string { + return string(in) +} + +func decodeIfBinaryToBytes(in []byte) []byte { + return in +} diff --git a/vendor/github.com/rs/zerolog/event.go b/vendor/github.com/rs/zerolog/event.go new file mode 100644 index 00000000..2e736c83 --- /dev/null +++ b/vendor/github.com/rs/zerolog/event.go @@ -0,0 +1,794 @@ +package zerolog + +import ( + "fmt" + "net" + "os" + "runtime" + "sync" + "time" +) + +var eventPool = &sync.Pool{ + New: func() interface{} { + return &Event{ + buf: make([]byte, 0, 500), + } + }, +} + +// Event represents a log event. It is instanced by one of the level method of +// Logger and finalized by the Msg or Msgf method. +type Event struct { + buf []byte + w LevelWriter + level Level + done func(msg string) + stack bool // enable error stack trace + ch []Hook // hooks from context + skipFrame int // The number of additional frames to skip when printing the caller. +} + +func putEvent(e *Event) { + // Proper usage of a sync.Pool requires each entry to have approximately + // the same memory cost. To obtain this property when the stored type + // contains a variably-sized buffer, we add a hard limit on the maximum buffer + // to place back in the pool. + // + // See https://golang.org/issue/23199 + const maxSize = 1 << 16 // 64KiB + if cap(e.buf) > maxSize { + return + } + eventPool.Put(e) +} + +// LogObjectMarshaler provides a strongly-typed and encoding-agnostic interface +// to be implemented by types used with Event/Context's Object methods. +type LogObjectMarshaler interface { + MarshalZerologObject(e *Event) +} + +// LogArrayMarshaler provides a strongly-typed and encoding-agnostic interface +// to be implemented by types used with Event/Context's Array methods. +type LogArrayMarshaler interface { + MarshalZerologArray(a *Array) +} + +func newEvent(w LevelWriter, level Level) *Event { + e := eventPool.Get().(*Event) + e.buf = e.buf[:0] + e.ch = nil + e.buf = enc.AppendBeginMarker(e.buf) + e.w = w + e.level = level + e.stack = false + e.skipFrame = 0 + return e +} + +func (e *Event) write() (err error) { + if e == nil { + return nil + } + if e.level != Disabled { + e.buf = enc.AppendEndMarker(e.buf) + e.buf = enc.AppendLineBreak(e.buf) + if e.w != nil { + _, err = e.w.WriteLevel(e.level, e.buf) + } + } + putEvent(e) + return +} + +// Enabled return false if the *Event is going to be filtered out by +// log level or sampling. +func (e *Event) Enabled() bool { + return e != nil && e.level != Disabled +} + +// Discard disables the event so Msg(f) won't print it. +func (e *Event) Discard() *Event { + if e == nil { + return e + } + e.level = Disabled + return nil +} + +// Msg sends the *Event with msg added as the message field if not empty. +// +// NOTICE: once this method is called, the *Event should be disposed. +// Calling Msg twice can have unexpected result. +func (e *Event) Msg(msg string) { + if e == nil { + return + } + e.msg(msg) +} + +// Send is equivalent to calling Msg(""). +// +// NOTICE: once this method is called, the *Event should be disposed. +func (e *Event) Send() { + if e == nil { + return + } + e.msg("") +} + +// Msgf sends the event with formatted msg added as the message field if not empty. +// +// NOTICE: once this method is called, the *Event should be disposed. +// Calling Msgf twice can have unexpected result. +func (e *Event) Msgf(format string, v ...interface{}) { + if e == nil { + return + } + e.msg(fmt.Sprintf(format, v...)) +} + +func (e *Event) MsgFunc(createMsg func() string) { + if e == nil { + return + } + e.msg(createMsg()) +} + +func (e *Event) msg(msg string) { + for _, hook := range e.ch { + hook.Run(e, e.level, msg) + } + if msg != "" { + e.buf = enc.AppendString(enc.AppendKey(e.buf, MessageFieldName), msg) + } + if e.done != nil { + defer e.done(msg) + } + if err := e.write(); err != nil { + if ErrorHandler != nil { + ErrorHandler(err) + } else { + fmt.Fprintf(os.Stderr, "zerolog: could not write event: %v\n", err) + } + } +} + +// Fields is a helper function to use a map or slice to set fields using type assertion. +// Only map[string]interface{} and []interface{} are accepted. []interface{} must +// alternate string keys and arbitrary values, and extraneous ones are ignored. +func (e *Event) Fields(fields interface{}) *Event { + if e == nil { + return e + } + e.buf = appendFields(e.buf, fields) + return e +} + +// Dict adds the field key with a dict to the event context. +// Use zerolog.Dict() to create the dictionary. +func (e *Event) Dict(key string, dict *Event) *Event { + if e == nil { + return e + } + dict.buf = enc.AppendEndMarker(dict.buf) + e.buf = append(enc.AppendKey(e.buf, key), dict.buf...) + putEvent(dict) + return e +} + +// Dict creates an Event to be used with the *Event.Dict method. +// Call usual field methods like Str, Int etc to add fields to this +// event and give it as argument the *Event.Dict method. +func Dict() *Event { + return newEvent(nil, 0) +} + +// Array adds the field key with an array to the event context. +// Use zerolog.Arr() to create the array or pass a type that +// implement the LogArrayMarshaler interface. +func (e *Event) Array(key string, arr LogArrayMarshaler) *Event { + if e == nil { + return e + } + e.buf = enc.AppendKey(e.buf, key) + var a *Array + if aa, ok := arr.(*Array); ok { + a = aa + } else { + a = Arr() + arr.MarshalZerologArray(a) + } + e.buf = a.write(e.buf) + return e +} + +func (e *Event) appendObject(obj LogObjectMarshaler) { + e.buf = enc.AppendBeginMarker(e.buf) + obj.MarshalZerologObject(e) + e.buf = enc.AppendEndMarker(e.buf) +} + +// Object marshals an object that implement the LogObjectMarshaler interface. +func (e *Event) Object(key string, obj LogObjectMarshaler) *Event { + if e == nil { + return e + } + e.buf = enc.AppendKey(e.buf, key) + if obj == nil { + e.buf = enc.AppendNil(e.buf) + + return e + } + + e.appendObject(obj) + return e +} + +// Func allows an anonymous func to run only if the event is enabled. +func (e *Event) Func(f func(e *Event)) *Event { + if e != nil && e.Enabled() { + f(e) + } + return e +} + +// EmbedObject marshals an object that implement the LogObjectMarshaler interface. +func (e *Event) EmbedObject(obj LogObjectMarshaler) *Event { + if e == nil { + return e + } + if obj == nil { + return e + } + obj.MarshalZerologObject(e) + return e +} + +// Str adds the field key with val as a string to the *Event context. +func (e *Event) Str(key, val string) *Event { + if e == nil { + return e + } + e.buf = enc.AppendString(enc.AppendKey(e.buf, key), val) + return e +} + +// Strs adds the field key with vals as a []string to the *Event context. +func (e *Event) Strs(key string, vals []string) *Event { + if e == nil { + return e + } + e.buf = enc.AppendStrings(enc.AppendKey(e.buf, key), vals) + return e +} + +// Stringer adds the field key with val.String() (or null if val is nil) +// to the *Event context. +func (e *Event) Stringer(key string, val fmt.Stringer) *Event { + if e == nil { + return e + } + e.buf = enc.AppendStringer(enc.AppendKey(e.buf, key), val) + return e +} + +// Stringers adds the field key with vals where each individual val +// is used as val.String() (or null if val is empty) to the *Event +// context. +func (e *Event) Stringers(key string, vals []fmt.Stringer) *Event { + if e == nil { + return e + } + e.buf = enc.AppendStringers(enc.AppendKey(e.buf, key), vals) + return e +} + +// Bytes adds the field key with val as a string to the *Event context. +// +// Runes outside of normal ASCII ranges will be hex-encoded in the resulting +// JSON. +func (e *Event) Bytes(key string, val []byte) *Event { + if e == nil { + return e + } + e.buf = enc.AppendBytes(enc.AppendKey(e.buf, key), val) + return e +} + +// Hex adds the field key with val as a hex string to the *Event context. +func (e *Event) Hex(key string, val []byte) *Event { + if e == nil { + return e + } + e.buf = enc.AppendHex(enc.AppendKey(e.buf, key), val) + return e +} + +// RawJSON adds already encoded JSON to the log line under key. +// +// No sanity check is performed on b; it must not contain carriage returns and +// be valid JSON. +func (e *Event) RawJSON(key string, b []byte) *Event { + if e == nil { + return e + } + e.buf = appendJSON(enc.AppendKey(e.buf, key), b) + return e +} + +// AnErr adds the field key with serialized err to the *Event context. +// If err is nil, no field is added. +func (e *Event) AnErr(key string, err error) *Event { + if e == nil { + return e + } + switch m := ErrorMarshalFunc(err).(type) { + case nil: + return e + case LogObjectMarshaler: + return e.Object(key, m) + case error: + if m == nil || isNilValue(m) { + return e + } else { + return e.Str(key, m.Error()) + } + case string: + return e.Str(key, m) + default: + return e.Interface(key, m) + } +} + +// Errs adds the field key with errs as an array of serialized errors to the +// *Event context. +func (e *Event) Errs(key string, errs []error) *Event { + if e == nil { + return e + } + arr := Arr() + for _, err := range errs { + switch m := ErrorMarshalFunc(err).(type) { + case LogObjectMarshaler: + arr = arr.Object(m) + case error: + arr = arr.Err(m) + case string: + arr = arr.Str(m) + default: + arr = arr.Interface(m) + } + } + + return e.Array(key, arr) +} + +// Err adds the field "error" with serialized err to the *Event context. +// If err is nil, no field is added. +// +// To customize the key name, change zerolog.ErrorFieldName. +// +// If Stack() has been called before and zerolog.ErrorStackMarshaler is defined, +// the err is passed to ErrorStackMarshaler and the result is appended to the +// zerolog.ErrorStackFieldName. +func (e *Event) Err(err error) *Event { + if e == nil { + return e + } + if e.stack && ErrorStackMarshaler != nil { + switch m := ErrorStackMarshaler(err).(type) { + case nil: + case LogObjectMarshaler: + e.Object(ErrorStackFieldName, m) + case error: + if m != nil && !isNilValue(m) { + e.Str(ErrorStackFieldName, m.Error()) + } + case string: + e.Str(ErrorStackFieldName, m) + default: + e.Interface(ErrorStackFieldName, m) + } + } + return e.AnErr(ErrorFieldName, err) +} + +// Stack enables stack trace printing for the error passed to Err(). +// +// ErrorStackMarshaler must be set for this method to do something. +func (e *Event) Stack() *Event { + if e != nil { + e.stack = true + } + return e +} + +// Bool adds the field key with val as a bool to the *Event context. +func (e *Event) Bool(key string, b bool) *Event { + if e == nil { + return e + } + e.buf = enc.AppendBool(enc.AppendKey(e.buf, key), b) + return e +} + +// Bools adds the field key with val as a []bool to the *Event context. +func (e *Event) Bools(key string, b []bool) *Event { + if e == nil { + return e + } + e.buf = enc.AppendBools(enc.AppendKey(e.buf, key), b) + return e +} + +// Int adds the field key with i as a int to the *Event context. +func (e *Event) Int(key string, i int) *Event { + if e == nil { + return e + } + e.buf = enc.AppendInt(enc.AppendKey(e.buf, key), i) + return e +} + +// Ints adds the field key with i as a []int to the *Event context. +func (e *Event) Ints(key string, i []int) *Event { + if e == nil { + return e + } + e.buf = enc.AppendInts(enc.AppendKey(e.buf, key), i) + return e +} + +// Int8 adds the field key with i as a int8 to the *Event context. +func (e *Event) Int8(key string, i int8) *Event { + if e == nil { + return e + } + e.buf = enc.AppendInt8(enc.AppendKey(e.buf, key), i) + return e +} + +// Ints8 adds the field key with i as a []int8 to the *Event context. +func (e *Event) Ints8(key string, i []int8) *Event { + if e == nil { + return e + } + e.buf = enc.AppendInts8(enc.AppendKey(e.buf, key), i) + return e +} + +// Int16 adds the field key with i as a int16 to the *Event context. +func (e *Event) Int16(key string, i int16) *Event { + if e == nil { + return e + } + e.buf = enc.AppendInt16(enc.AppendKey(e.buf, key), i) + return e +} + +// Ints16 adds the field key with i as a []int16 to the *Event context. +func (e *Event) Ints16(key string, i []int16) *Event { + if e == nil { + return e + } + e.buf = enc.AppendInts16(enc.AppendKey(e.buf, key), i) + return e +} + +// Int32 adds the field key with i as a int32 to the *Event context. +func (e *Event) Int32(key string, i int32) *Event { + if e == nil { + return e + } + e.buf = enc.AppendInt32(enc.AppendKey(e.buf, key), i) + return e +} + +// Ints32 adds the field key with i as a []int32 to the *Event context. +func (e *Event) Ints32(key string, i []int32) *Event { + if e == nil { + return e + } + e.buf = enc.AppendInts32(enc.AppendKey(e.buf, key), i) + return e +} + +// Int64 adds the field key with i as a int64 to the *Event context. +func (e *Event) Int64(key string, i int64) *Event { + if e == nil { + return e + } + e.buf = enc.AppendInt64(enc.AppendKey(e.buf, key), i) + return e +} + +// Ints64 adds the field key with i as a []int64 to the *Event context. +func (e *Event) Ints64(key string, i []int64) *Event { + if e == nil { + return e + } + e.buf = enc.AppendInts64(enc.AppendKey(e.buf, key), i) + return e +} + +// Uint adds the field key with i as a uint to the *Event context. +func (e *Event) Uint(key string, i uint) *Event { + if e == nil { + return e + } + e.buf = enc.AppendUint(enc.AppendKey(e.buf, key), i) + return e +} + +// Uints adds the field key with i as a []int to the *Event context. +func (e *Event) Uints(key string, i []uint) *Event { + if e == nil { + return e + } + e.buf = enc.AppendUints(enc.AppendKey(e.buf, key), i) + return e +} + +// Uint8 adds the field key with i as a uint8 to the *Event context. +func (e *Event) Uint8(key string, i uint8) *Event { + if e == nil { + return e + } + e.buf = enc.AppendUint8(enc.AppendKey(e.buf, key), i) + return e +} + +// Uints8 adds the field key with i as a []int8 to the *Event context. +func (e *Event) Uints8(key string, i []uint8) *Event { + if e == nil { + return e + } + e.buf = enc.AppendUints8(enc.AppendKey(e.buf, key), i) + return e +} + +// Uint16 adds the field key with i as a uint16 to the *Event context. +func (e *Event) Uint16(key string, i uint16) *Event { + if e == nil { + return e + } + e.buf = enc.AppendUint16(enc.AppendKey(e.buf, key), i) + return e +} + +// Uints16 adds the field key with i as a []int16 to the *Event context. +func (e *Event) Uints16(key string, i []uint16) *Event { + if e == nil { + return e + } + e.buf = enc.AppendUints16(enc.AppendKey(e.buf, key), i) + return e +} + +// Uint32 adds the field key with i as a uint32 to the *Event context. +func (e *Event) Uint32(key string, i uint32) *Event { + if e == nil { + return e + } + e.buf = enc.AppendUint32(enc.AppendKey(e.buf, key), i) + return e +} + +// Uints32 adds the field key with i as a []int32 to the *Event context. +func (e *Event) Uints32(key string, i []uint32) *Event { + if e == nil { + return e + } + e.buf = enc.AppendUints32(enc.AppendKey(e.buf, key), i) + return e +} + +// Uint64 adds the field key with i as a uint64 to the *Event context. +func (e *Event) Uint64(key string, i uint64) *Event { + if e == nil { + return e + } + e.buf = enc.AppendUint64(enc.AppendKey(e.buf, key), i) + return e +} + +// Uints64 adds the field key with i as a []int64 to the *Event context. +func (e *Event) Uints64(key string, i []uint64) *Event { + if e == nil { + return e + } + e.buf = enc.AppendUints64(enc.AppendKey(e.buf, key), i) + return e +} + +// Float32 adds the field key with f as a float32 to the *Event context. +func (e *Event) Float32(key string, f float32) *Event { + if e == nil { + return e + } + e.buf = enc.AppendFloat32(enc.AppendKey(e.buf, key), f) + return e +} + +// Floats32 adds the field key with f as a []float32 to the *Event context. +func (e *Event) Floats32(key string, f []float32) *Event { + if e == nil { + return e + } + e.buf = enc.AppendFloats32(enc.AppendKey(e.buf, key), f) + return e +} + +// Float64 adds the field key with f as a float64 to the *Event context. +func (e *Event) Float64(key string, f float64) *Event { + if e == nil { + return e + } + e.buf = enc.AppendFloat64(enc.AppendKey(e.buf, key), f) + return e +} + +// Floats64 adds the field key with f as a []float64 to the *Event context. +func (e *Event) Floats64(key string, f []float64) *Event { + if e == nil { + return e + } + e.buf = enc.AppendFloats64(enc.AppendKey(e.buf, key), f) + return e +} + +// Timestamp adds the current local time as UNIX timestamp to the *Event context with the "time" key. +// To customize the key name, change zerolog.TimestampFieldName. +// +// NOTE: It won't dedupe the "time" key if the *Event (or *Context) has one +// already. +func (e *Event) Timestamp() *Event { + if e == nil { + return e + } + e.buf = enc.AppendTime(enc.AppendKey(e.buf, TimestampFieldName), TimestampFunc(), TimeFieldFormat) + return e +} + +// Time adds the field key with t formatted as string using zerolog.TimeFieldFormat. +func (e *Event) Time(key string, t time.Time) *Event { + if e == nil { + return e + } + e.buf = enc.AppendTime(enc.AppendKey(e.buf, key), t, TimeFieldFormat) + return e +} + +// Times adds the field key with t formatted as string using zerolog.TimeFieldFormat. +func (e *Event) Times(key string, t []time.Time) *Event { + if e == nil { + return e + } + e.buf = enc.AppendTimes(enc.AppendKey(e.buf, key), t, TimeFieldFormat) + return e +} + +// Dur adds the field key with duration d stored as zerolog.DurationFieldUnit. +// If zerolog.DurationFieldInteger is true, durations are rendered as integer +// instead of float. +func (e *Event) Dur(key string, d time.Duration) *Event { + if e == nil { + return e + } + e.buf = enc.AppendDuration(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldInteger) + return e +} + +// Durs adds the field key with duration d stored as zerolog.DurationFieldUnit. +// If zerolog.DurationFieldInteger is true, durations are rendered as integer +// instead of float. +func (e *Event) Durs(key string, d []time.Duration) *Event { + if e == nil { + return e + } + e.buf = enc.AppendDurations(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldInteger) + return e +} + +// TimeDiff adds the field key with positive duration between time t and start. +// If time t is not greater than start, duration will be 0. +// Duration format follows the same principle as Dur(). +func (e *Event) TimeDiff(key string, t time.Time, start time.Time) *Event { + if e == nil { + return e + } + var d time.Duration + if t.After(start) { + d = t.Sub(start) + } + e.buf = enc.AppendDuration(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldInteger) + return e +} + +// Any is a wrapper around Event.Interface. +func (e *Event) Any(key string, i interface{}) *Event { + return e.Interface(key, i) +} + +// Interface adds the field key with i marshaled using reflection. +func (e *Event) Interface(key string, i interface{}) *Event { + if e == nil { + return e + } + if obj, ok := i.(LogObjectMarshaler); ok { + return e.Object(key, obj) + } + e.buf = enc.AppendInterface(enc.AppendKey(e.buf, key), i) + return e +} + +// Type adds the field key with val's type using reflection. +func (e *Event) Type(key string, val interface{}) *Event { + if e == nil { + return e + } + e.buf = enc.AppendType(enc.AppendKey(e.buf, key), val) + return e +} + +// CallerSkipFrame instructs any future Caller calls to skip the specified number of frames. +// This includes those added via hooks from the context. +func (e *Event) CallerSkipFrame(skip int) *Event { + if e == nil { + return e + } + e.skipFrame += skip + return e +} + +// Caller adds the file:line of the caller with the zerolog.CallerFieldName key. +// The argument skip is the number of stack frames to ascend +// Skip If not passed, use the global variable CallerSkipFrameCount +func (e *Event) Caller(skip ...int) *Event { + sk := CallerSkipFrameCount + if len(skip) > 0 { + sk = skip[0] + CallerSkipFrameCount + } + return e.caller(sk) +} + +func (e *Event) caller(skip int) *Event { + if e == nil { + return e + } + pc, file, line, ok := runtime.Caller(skip + e.skipFrame) + if !ok { + return e + } + e.buf = enc.AppendString(enc.AppendKey(e.buf, CallerFieldName), CallerMarshalFunc(pc, file, line)) + return e +} + +// IPAddr adds IPv4 or IPv6 Address to the event +func (e *Event) IPAddr(key string, ip net.IP) *Event { + if e == nil { + return e + } + e.buf = enc.AppendIPAddr(enc.AppendKey(e.buf, key), ip) + return e +} + +// IPPrefix adds IPv4 or IPv6 Prefix (address and mask) to the event +func (e *Event) IPPrefix(key string, pfx net.IPNet) *Event { + if e == nil { + return e + } + e.buf = enc.AppendIPPrefix(enc.AppendKey(e.buf, key), pfx) + return e +} + +// MACAddr adds MAC address to the event +func (e *Event) MACAddr(key string, ha net.HardwareAddr) *Event { + if e == nil { + return e + } + e.buf = enc.AppendMACAddr(enc.AppendKey(e.buf, key), ha) + return e +} diff --git a/vendor/github.com/rs/zerolog/fields.go b/vendor/github.com/rs/zerolog/fields.go new file mode 100644 index 00000000..c1eb5ce7 --- /dev/null +++ b/vendor/github.com/rs/zerolog/fields.go @@ -0,0 +1,277 @@ +package zerolog + +import ( + "encoding/json" + "net" + "sort" + "time" + "unsafe" +) + +func isNilValue(i interface{}) bool { + return (*[2]uintptr)(unsafe.Pointer(&i))[1] == 0 +} + +func appendFields(dst []byte, fields interface{}) []byte { + switch fields := fields.(type) { + case []interface{}: + if n := len(fields); n&0x1 == 1 { // odd number + fields = fields[:n-1] + } + dst = appendFieldList(dst, fields) + case map[string]interface{}: + keys := make([]string, 0, len(fields)) + for key := range fields { + keys = append(keys, key) + } + sort.Strings(keys) + kv := make([]interface{}, 2) + for _, key := range keys { + kv[0], kv[1] = key, fields[key] + dst = appendFieldList(dst, kv) + } + } + return dst +} + +func appendFieldList(dst []byte, kvList []interface{}) []byte { + for i, n := 0, len(kvList); i < n; i += 2 { + key, val := kvList[i], kvList[i+1] + if key, ok := key.(string); ok { + dst = enc.AppendKey(dst, key) + } else { + continue + } + if val, ok := val.(LogObjectMarshaler); ok { + e := newEvent(nil, 0) + e.buf = e.buf[:0] + e.appendObject(val) + dst = append(dst, e.buf...) + putEvent(e) + continue + } + switch val := val.(type) { + case string: + dst = enc.AppendString(dst, val) + case []byte: + dst = enc.AppendBytes(dst, val) + case error: + switch m := ErrorMarshalFunc(val).(type) { + case LogObjectMarshaler: + e := newEvent(nil, 0) + e.buf = e.buf[:0] + e.appendObject(m) + dst = append(dst, e.buf...) + putEvent(e) + case error: + if m == nil || isNilValue(m) { + dst = enc.AppendNil(dst) + } else { + dst = enc.AppendString(dst, m.Error()) + } + case string: + dst = enc.AppendString(dst, m) + default: + dst = enc.AppendInterface(dst, m) + } + case []error: + dst = enc.AppendArrayStart(dst) + for i, err := range val { + switch m := ErrorMarshalFunc(err).(type) { + case LogObjectMarshaler: + e := newEvent(nil, 0) + e.buf = e.buf[:0] + e.appendObject(m) + dst = append(dst, e.buf...) + putEvent(e) + case error: + if m == nil || isNilValue(m) { + dst = enc.AppendNil(dst) + } else { + dst = enc.AppendString(dst, m.Error()) + } + case string: + dst = enc.AppendString(dst, m) + default: + dst = enc.AppendInterface(dst, m) + } + + if i < (len(val) - 1) { + enc.AppendArrayDelim(dst) + } + } + dst = enc.AppendArrayEnd(dst) + case bool: + dst = enc.AppendBool(dst, val) + case int: + dst = enc.AppendInt(dst, val) + case int8: + dst = enc.AppendInt8(dst, val) + case int16: + dst = enc.AppendInt16(dst, val) + case int32: + dst = enc.AppendInt32(dst, val) + case int64: + dst = enc.AppendInt64(dst, val) + case uint: + dst = enc.AppendUint(dst, val) + case uint8: + dst = enc.AppendUint8(dst, val) + case uint16: + dst = enc.AppendUint16(dst, val) + case uint32: + dst = enc.AppendUint32(dst, val) + case uint64: + dst = enc.AppendUint64(dst, val) + case float32: + dst = enc.AppendFloat32(dst, val) + case float64: + dst = enc.AppendFloat64(dst, val) + case time.Time: + dst = enc.AppendTime(dst, val, TimeFieldFormat) + case time.Duration: + dst = enc.AppendDuration(dst, val, DurationFieldUnit, DurationFieldInteger) + case *string: + if val != nil { + dst = enc.AppendString(dst, *val) + } else { + dst = enc.AppendNil(dst) + } + case *bool: + if val != nil { + dst = enc.AppendBool(dst, *val) + } else { + dst = enc.AppendNil(dst) + } + case *int: + if val != nil { + dst = enc.AppendInt(dst, *val) + } else { + dst = enc.AppendNil(dst) + } + case *int8: + if val != nil { + dst = enc.AppendInt8(dst, *val) + } else { + dst = enc.AppendNil(dst) + } + case *int16: + if val != nil { + dst = enc.AppendInt16(dst, *val) + } else { + dst = enc.AppendNil(dst) + } + case *int32: + if val != nil { + dst = enc.AppendInt32(dst, *val) + } else { + dst = enc.AppendNil(dst) + } + case *int64: + if val != nil { + dst = enc.AppendInt64(dst, *val) + } else { + dst = enc.AppendNil(dst) + } + case *uint: + if val != nil { + dst = enc.AppendUint(dst, *val) + } else { + dst = enc.AppendNil(dst) + } + case *uint8: + if val != nil { + dst = enc.AppendUint8(dst, *val) + } else { + dst = enc.AppendNil(dst) + } + case *uint16: + if val != nil { + dst = enc.AppendUint16(dst, *val) + } else { + dst = enc.AppendNil(dst) + } + case *uint32: + if val != nil { + dst = enc.AppendUint32(dst, *val) + } else { + dst = enc.AppendNil(dst) + } + case *uint64: + if val != nil { + dst = enc.AppendUint64(dst, *val) + } else { + dst = enc.AppendNil(dst) + } + case *float32: + if val != nil { + dst = enc.AppendFloat32(dst, *val) + } else { + dst = enc.AppendNil(dst) + } + case *float64: + if val != nil { + dst = enc.AppendFloat64(dst, *val) + } else { + dst = enc.AppendNil(dst) + } + case *time.Time: + if val != nil { + dst = enc.AppendTime(dst, *val, TimeFieldFormat) + } else { + dst = enc.AppendNil(dst) + } + case *time.Duration: + if val != nil { + dst = enc.AppendDuration(dst, *val, DurationFieldUnit, DurationFieldInteger) + } else { + dst = enc.AppendNil(dst) + } + case []string: + dst = enc.AppendStrings(dst, val) + case []bool: + dst = enc.AppendBools(dst, val) + case []int: + dst = enc.AppendInts(dst, val) + case []int8: + dst = enc.AppendInts8(dst, val) + case []int16: + dst = enc.AppendInts16(dst, val) + case []int32: + dst = enc.AppendInts32(dst, val) + case []int64: + dst = enc.AppendInts64(dst, val) + case []uint: + dst = enc.AppendUints(dst, val) + // case []uint8: + // dst = enc.AppendUints8(dst, val) + case []uint16: + dst = enc.AppendUints16(dst, val) + case []uint32: + dst = enc.AppendUints32(dst, val) + case []uint64: + dst = enc.AppendUints64(dst, val) + case []float32: + dst = enc.AppendFloats32(dst, val) + case []float64: + dst = enc.AppendFloats64(dst, val) + case []time.Time: + dst = enc.AppendTimes(dst, val, TimeFieldFormat) + case []time.Duration: + dst = enc.AppendDurations(dst, val, DurationFieldUnit, DurationFieldInteger) + case nil: + dst = enc.AppendNil(dst) + case net.IP: + dst = enc.AppendIPAddr(dst, val) + case net.IPNet: + dst = enc.AppendIPPrefix(dst, val) + case net.HardwareAddr: + dst = enc.AppendMACAddr(dst, val) + case json.RawMessage: + dst = appendJSON(dst, val) + default: + dst = enc.AppendInterface(dst, val) + } + } + return dst +} diff --git a/vendor/github.com/rs/zerolog/globals.go b/vendor/github.com/rs/zerolog/globals.go new file mode 100644 index 00000000..e1067deb --- /dev/null +++ b/vendor/github.com/rs/zerolog/globals.go @@ -0,0 +1,142 @@ +package zerolog + +import ( + "encoding/json" + "strconv" + "sync/atomic" + "time" +) + +const ( + // TimeFormatUnix defines a time format that makes time fields to be + // serialized as Unix timestamp integers. + TimeFormatUnix = "" + + // TimeFormatUnixMs defines a time format that makes time fields to be + // serialized as Unix timestamp integers in milliseconds. + TimeFormatUnixMs = "UNIXMS" + + // TimeFormatUnixMicro defines a time format that makes time fields to be + // serialized as Unix timestamp integers in microseconds. + TimeFormatUnixMicro = "UNIXMICRO" + + // TimeFormatUnixNano defines a time format that makes time fields to be + // serialized as Unix timestamp integers in nanoseconds. + TimeFormatUnixNano = "UNIXNANO" +) + +var ( + // TimestampFieldName is the field name used for the timestamp field. + TimestampFieldName = "time" + + // LevelFieldName is the field name used for the level field. + LevelFieldName = "level" + + // LevelTraceValue is the value used for the trace level field. + LevelTraceValue = "trace" + // LevelDebugValue is the value used for the debug level field. + LevelDebugValue = "debug" + // LevelInfoValue is the value used for the info level field. + LevelInfoValue = "info" + // LevelWarnValue is the value used for the warn level field. + LevelWarnValue = "warn" + // LevelErrorValue is the value used for the error level field. + LevelErrorValue = "error" + // LevelFatalValue is the value used for the fatal level field. + LevelFatalValue = "fatal" + // LevelPanicValue is the value used for the panic level field. + LevelPanicValue = "panic" + + // LevelFieldMarshalFunc allows customization of global level field marshaling. + LevelFieldMarshalFunc = func(l Level) string { + return l.String() + } + + // MessageFieldName is the field name used for the message field. + MessageFieldName = "message" + + // ErrorFieldName is the field name used for error fields. + ErrorFieldName = "error" + + // CallerFieldName is the field name used for caller field. + CallerFieldName = "caller" + + // CallerSkipFrameCount is the number of stack frames to skip to find the caller. + CallerSkipFrameCount = 2 + + // CallerMarshalFunc allows customization of global caller marshaling + CallerMarshalFunc = func(pc uintptr, file string, line int) string { + return file + ":" + strconv.Itoa(line) + } + + // ErrorStackFieldName is the field name used for error stacks. + ErrorStackFieldName = "stack" + + // ErrorStackMarshaler extract the stack from err if any. + ErrorStackMarshaler func(err error) interface{} + + // ErrorMarshalFunc allows customization of global error marshaling + ErrorMarshalFunc = func(err error) interface{} { + return err + } + + // InterfaceMarshalFunc allows customization of interface marshaling. + // Default: "encoding/json.Marshal" + InterfaceMarshalFunc = json.Marshal + + // TimeFieldFormat defines the time format of the Time field type. If set to + // TimeFormatUnix, TimeFormatUnixMs, TimeFormatUnixMicro or TimeFormatUnixNano, the time is formatted as a UNIX + // timestamp as integer. + TimeFieldFormat = time.RFC3339 + + // TimestampFunc defines the function called to generate a timestamp. + TimestampFunc = time.Now + + // DurationFieldUnit defines the unit for time.Duration type fields added + // using the Dur method. + DurationFieldUnit = time.Millisecond + + // DurationFieldInteger renders Dur fields as integer instead of float if + // set to true. + DurationFieldInteger = false + + // ErrorHandler is called whenever zerolog fails to write an event on its + // output. If not set, an error is printed on the stderr. This handler must + // be thread safe and non-blocking. + ErrorHandler func(err error) + + // DefaultContextLogger is returned from Ctx() if there is no logger associated + // with the context. + DefaultContextLogger *Logger +) + +var ( + gLevel = new(int32) + disableSampling = new(int32) +) + +// SetGlobalLevel sets the global override for log level. If this +// values is raised, all Loggers will use at least this value. +// +// To globally disable logs, set GlobalLevel to Disabled. +func SetGlobalLevel(l Level) { + atomic.StoreInt32(gLevel, int32(l)) +} + +// GlobalLevel returns the current global log level +func GlobalLevel() Level { + return Level(atomic.LoadInt32(gLevel)) +} + +// DisableSampling will disable sampling in all Loggers if true. +func DisableSampling(v bool) { + var i int32 + if v { + i = 1 + } + atomic.StoreInt32(disableSampling, i) +} + +func samplingDisabled() bool { + return atomic.LoadInt32(disableSampling) == 1 +} diff --git a/vendor/github.com/rs/zerolog/go112.go b/vendor/github.com/rs/zerolog/go112.go new file mode 100644 index 00000000..e7b5a1bd --- /dev/null +++ b/vendor/github.com/rs/zerolog/go112.go @@ -0,0 +1,7 @@ +// +build go1.12 + +package zerolog + +// Since go 1.12, some auto generated init functions are hidden from +// runtime.Caller. +const contextCallerSkipFrameCount = 2 diff --git a/vendor/github.com/rs/zerolog/hook.go b/vendor/github.com/rs/zerolog/hook.go new file mode 100644 index 00000000..ec6effc1 --- /dev/null +++ b/vendor/github.com/rs/zerolog/hook.go @@ -0,0 +1,64 @@ +package zerolog + +// Hook defines an interface to a log hook. +type Hook interface { + // Run runs the hook with the event. + Run(e *Event, level Level, message string) +} + +// HookFunc is an adaptor to allow the use of an ordinary function +// as a Hook. +type HookFunc func(e *Event, level Level, message string) + +// Run implements the Hook interface. +func (h HookFunc) Run(e *Event, level Level, message string) { + h(e, level, message) +} + +// LevelHook applies a different hook for each level. +type LevelHook struct { + NoLevelHook, TraceHook, DebugHook, InfoHook, WarnHook, ErrorHook, FatalHook, PanicHook Hook +} + +// Run implements the Hook interface. +func (h LevelHook) Run(e *Event, level Level, message string) { + switch level { + case TraceLevel: + if h.TraceHook != nil { + h.TraceHook.Run(e, level, message) + } + case DebugLevel: + if h.DebugHook != nil { + h.DebugHook.Run(e, level, message) + } + case InfoLevel: + if h.InfoHook != nil { + h.InfoHook.Run(e, level, message) + } + case WarnLevel: + if h.WarnHook != nil { + h.WarnHook.Run(e, level, message) + } + case ErrorLevel: + if h.ErrorHook != nil { + h.ErrorHook.Run(e, level, message) + } + case FatalLevel: + if h.FatalHook != nil { + h.FatalHook.Run(e, level, message) + } + case PanicLevel: + if h.PanicHook != nil { + h.PanicHook.Run(e, level, message) + } + case NoLevel: + if h.NoLevelHook != nil { + h.NoLevelHook.Run(e, level, message) + } + } +} + +// NewLevelHook returns a new LevelHook. +func NewLevelHook() LevelHook { + return LevelHook{} +} diff --git a/vendor/github.com/rs/zerolog/internal/cbor/README.md b/vendor/github.com/rs/zerolog/internal/cbor/README.md new file mode 100644 index 00000000..92c2e8c7 --- /dev/null +++ b/vendor/github.com/rs/zerolog/internal/cbor/README.md @@ -0,0 +1,56 @@ +## Reference: + CBOR Encoding is described in [RFC7049](https://tools.ietf.org/html/rfc7049) + +## Comparison of JSON vs CBOR + +Two main areas of reduction are: + +1. CPU usage to write a log msg +2. Size (in bytes) of log messages. + + +CPU Usage savings are below: +``` +name JSON time/op CBOR time/op delta +Info-32 15.3ns ± 1% 11.7ns ± 3% -23.78% (p=0.000 n=9+10) +ContextFields-32 16.2ns ± 2% 12.3ns ± 3% -23.97% (p=0.000 n=9+9) +ContextAppend-32 6.70ns ± 0% 6.20ns ± 0% -7.44% (p=0.000 n=9+9) +LogFields-32 66.4ns ± 0% 24.6ns ± 2% -62.89% (p=0.000 n=10+9) +LogArrayObject-32 911ns ±11% 768ns ± 6% -15.64% (p=0.000 n=10+10) +LogFieldType/Floats-32 70.3ns ± 2% 29.5ns ± 1% -57.98% (p=0.000 n=10+10) +LogFieldType/Err-32 14.0ns ± 3% 12.1ns ± 8% -13.20% (p=0.000 n=8+10) +LogFieldType/Dur-32 17.2ns ± 2% 13.1ns ± 1% -24.27% (p=0.000 n=10+9) +LogFieldType/Object-32 54.3ns ±11% 52.3ns ± 7% ~ (p=0.239 n=10+10) +LogFieldType/Ints-32 20.3ns ± 2% 15.1ns ± 2% -25.50% (p=0.000 n=9+10) +LogFieldType/Interfaces-32 642ns ±11% 621ns ± 9% ~ (p=0.118 n=10+10) +LogFieldType/Interface(Objects)-32 635ns ±13% 632ns ± 9% ~ (p=0.592 n=10+10) +LogFieldType/Times-32 294ns ± 0% 27ns ± 1% -90.71% (p=0.000 n=10+9) +LogFieldType/Durs-32 121ns ± 0% 33ns ± 2% -72.44% (p=0.000 n=9+9) +LogFieldType/Interface(Object)-32 56.6ns ± 8% 52.3ns ± 8% -7.54% (p=0.007 n=10+10) +LogFieldType/Errs-32 17.8ns ± 3% 16.1ns ± 2% -9.71% (p=0.000 n=10+9) +LogFieldType/Time-32 40.5ns ± 1% 12.7ns ± 6% -68.66% (p=0.000 n=8+9) +LogFieldType/Bool-32 12.0ns ± 5% 10.2ns ± 2% -15.18% (p=0.000 n=10+8) +LogFieldType/Bools-32 17.2ns ± 2% 12.6ns ± 4% -26.63% (p=0.000 n=10+10) +LogFieldType/Int-32 12.3ns ± 2% 11.2ns ± 4% -9.27% (p=0.000 n=9+10) +LogFieldType/Float-32 16.7ns ± 1% 12.6ns ± 2% -24.42% (p=0.000 n=7+9) +LogFieldType/Str-32 12.7ns ± 7% 11.3ns ± 7% -10.88% (p=0.000 n=10+9) +LogFieldType/Strs-32 20.3ns ± 3% 18.2ns ± 3% -10.25% (p=0.000 n=9+10) +LogFieldType/Interface-32 183ns ±12% 175ns ± 9% ~ (p=0.078 n=10+10) +``` + +Log message size savings is greatly dependent on the number and type of fields in the log message. +Assuming this log message (with an Integer, timestamp and string, in addition to level). + +`{"level":"error","Fault":41650,"time":"2018-04-01T15:18:19-07:00","message":"Some Message"}` + +Two measurements were done for the log file sizes - one without any compression, second +using [compress/zlib](https://golang.org/pkg/compress/zlib/). + +Results for 10,000 log messages: + +| Log Format | Plain File Size (in KB) | Compressed File Size (in KB) | +| :--- | :---: | :---: | +| JSON | 920 | 28 | +| CBOR | 550 | 28 | + +The example used to calculate the above data is available in [Examples](examples). diff --git a/vendor/github.com/rs/zerolog/internal/cbor/base.go b/vendor/github.com/rs/zerolog/internal/cbor/base.go new file mode 100644 index 00000000..51fe86c9 --- /dev/null +++ b/vendor/github.com/rs/zerolog/internal/cbor/base.go @@ -0,0 +1,19 @@ +package cbor + +// JSONMarshalFunc is used to marshal interface to JSON encoded byte slice. +// Making it package level instead of embedded in Encoder brings +// some extra efforts at importing, but avoids value copy when the functions +// of Encoder being invoked. +// DO REMEMBER to set this variable at importing, or +// you might get a nil pointer dereference panic at runtime. +var JSONMarshalFunc func(v interface{}) ([]byte, error) + +type Encoder struct{} + +// AppendKey adds a key (string) to the binary encoded log message +func (e Encoder) AppendKey(dst []byte, key string) []byte { + if len(dst) < 1 { + dst = e.AppendBeginMarker(dst) + } + return e.AppendString(dst, key) +} diff --git a/vendor/github.com/rs/zerolog/internal/cbor/cbor.go b/vendor/github.com/rs/zerolog/internal/cbor/cbor.go new file mode 100644 index 00000000..bc54e37a --- /dev/null +++ b/vendor/github.com/rs/zerolog/internal/cbor/cbor.go @@ -0,0 +1,101 @@ +// Package cbor provides primitives for storing different data +// in the CBOR (binary) format. CBOR is defined in RFC7049. +package cbor + +import "time" + +const ( + majorOffset = 5 + additionalMax = 23 + + // Non Values. + additionalTypeBoolFalse byte = 20 + additionalTypeBoolTrue byte = 21 + additionalTypeNull byte = 22 + + // Integer (+ve and -ve) Sub-types. + additionalTypeIntUint8 byte = 24 + additionalTypeIntUint16 byte = 25 + additionalTypeIntUint32 byte = 26 + additionalTypeIntUint64 byte = 27 + + // Float Sub-types. + additionalTypeFloat16 byte = 25 + additionalTypeFloat32 byte = 26 + additionalTypeFloat64 byte = 27 + additionalTypeBreak byte = 31 + + // Tag Sub-types. + additionalTypeTimestamp byte = 01 + + // Extended Tags - from https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml + additionalTypeTagNetworkAddr uint16 = 260 + additionalTypeTagNetworkPrefix uint16 = 261 + additionalTypeEmbeddedJSON uint16 = 262 + additionalTypeTagHexString uint16 = 263 + + // Unspecified number of elements. + additionalTypeInfiniteCount byte = 31 +) +const ( + majorTypeUnsignedInt byte = iota << majorOffset // Major type 0 + majorTypeNegativeInt // Major type 1 + majorTypeByteString // Major type 2 + majorTypeUtf8String // Major type 3 + majorTypeArray // Major type 4 + majorTypeMap // Major type 5 + majorTypeTags // Major type 6 + majorTypeSimpleAndFloat // Major type 7 +) + +const ( + maskOutAdditionalType byte = (7 << majorOffset) + maskOutMajorType byte = 31 +) + +const ( + float32Nan = "\xfa\x7f\xc0\x00\x00" + float32PosInfinity = "\xfa\x7f\x80\x00\x00" + float32NegInfinity = "\xfa\xff\x80\x00\x00" + float64Nan = "\xfb\x7f\xf8\x00\x00\x00\x00\x00\x00" + float64PosInfinity = "\xfb\x7f\xf0\x00\x00\x00\x00\x00\x00" + float64NegInfinity = "\xfb\xff\xf0\x00\x00\x00\x00\x00\x00" +) + +// IntegerTimeFieldFormat indicates the format of timestamp decoded +// from an integer (time in seconds). +var IntegerTimeFieldFormat = time.RFC3339 + +// NanoTimeFieldFormat indicates the format of timestamp decoded +// from a float value (time in seconds and nanoseconds). +var NanoTimeFieldFormat = time.RFC3339Nano + +func appendCborTypePrefix(dst []byte, major byte, number uint64) []byte { + byteCount := 8 + var minor byte + switch { + case number < 256: + byteCount = 1 + minor = additionalTypeIntUint8 + + case number < 65536: + byteCount = 2 + minor = additionalTypeIntUint16 + + case number < 4294967296: + byteCount = 4 + minor = additionalTypeIntUint32 + + default: + byteCount = 8 + minor = additionalTypeIntUint64 + + } + + dst = append(dst, major|minor) + byteCount-- + for ; byteCount >= 0; byteCount-- { + dst = append(dst, byte(number>>(uint(byteCount)*8))) + } + return dst +} diff --git a/vendor/github.com/rs/zerolog/internal/cbor/decode_stream.go b/vendor/github.com/rs/zerolog/internal/cbor/decode_stream.go new file mode 100644 index 00000000..fc16f98c --- /dev/null +++ b/vendor/github.com/rs/zerolog/internal/cbor/decode_stream.go @@ -0,0 +1,614 @@ +package cbor + +// This file contains code to decode a stream of CBOR Data into JSON. + +import ( + "bufio" + "bytes" + "fmt" + "io" + "math" + "net" + "runtime" + "strconv" + "strings" + "time" + "unicode/utf8" +) + +var decodeTimeZone *time.Location + +const hexTable = "0123456789abcdef" + +const isFloat32 = 4 +const isFloat64 = 8 + +func readNBytes(src *bufio.Reader, n int) []byte { + ret := make([]byte, n) + for i := 0; i < n; i++ { + ch, e := src.ReadByte() + if e != nil { + panic(fmt.Errorf("Tried to Read %d Bytes.. But hit end of file", n)) + } + ret[i] = ch + } + return ret +} + +func readByte(src *bufio.Reader) byte { + b, e := src.ReadByte() + if e != nil { + panic(fmt.Errorf("Tried to Read 1 Byte.. But hit end of file")) + } + return b +} + +func decodeIntAdditionalType(src *bufio.Reader, minor byte) int64 { + val := int64(0) + if minor <= 23 { + val = int64(minor) + } else { + bytesToRead := 0 + switch minor { + case additionalTypeIntUint8: + bytesToRead = 1 + case additionalTypeIntUint16: + bytesToRead = 2 + case additionalTypeIntUint32: + bytesToRead = 4 + case additionalTypeIntUint64: + bytesToRead = 8 + default: + panic(fmt.Errorf("Invalid Additional Type: %d in decodeInteger (expected <28)", minor)) + } + pb := readNBytes(src, bytesToRead) + for i := 0; i < bytesToRead; i++ { + val = val * 256 + val += int64(pb[i]) + } + } + return val +} + +func decodeInteger(src *bufio.Reader) int64 { + pb := readByte(src) + major := pb & maskOutAdditionalType + minor := pb & maskOutMajorType + if major != majorTypeUnsignedInt && major != majorTypeNegativeInt { + panic(fmt.Errorf("Major type is: %d in decodeInteger!! (expected 0 or 1)", major)) + } + val := decodeIntAdditionalType(src, minor) + if major == 0 { + return val + } + return (-1 - val) +} + +func decodeFloat(src *bufio.Reader) (float64, int) { + pb := readByte(src) + major := pb & maskOutAdditionalType + minor := pb & maskOutMajorType + if major != majorTypeSimpleAndFloat { + panic(fmt.Errorf("Incorrect Major type is: %d in decodeFloat", major)) + } + + switch minor { + case additionalTypeFloat16: + panic(fmt.Errorf("float16 is not suppported in decodeFloat")) + + case additionalTypeFloat32: + pb := readNBytes(src, 4) + switch string(pb) { + case float32Nan: + return math.NaN(), isFloat32 + case float32PosInfinity: + return math.Inf(0), isFloat32 + case float32NegInfinity: + return math.Inf(-1), isFloat32 + } + n := uint32(0) + for i := 0; i < 4; i++ { + n = n * 256 + n += uint32(pb[i]) + } + val := math.Float32frombits(n) + return float64(val), isFloat32 + case additionalTypeFloat64: + pb := readNBytes(src, 8) + switch string(pb) { + case float64Nan: + return math.NaN(), isFloat64 + case float64PosInfinity: + return math.Inf(0), isFloat64 + case float64NegInfinity: + return math.Inf(-1), isFloat64 + } + n := uint64(0) + for i := 0; i < 8; i++ { + n = n * 256 + n += uint64(pb[i]) + } + val := math.Float64frombits(n) + return val, isFloat64 + } + panic(fmt.Errorf("Invalid Additional Type: %d in decodeFloat", minor)) +} + +func decodeStringComplex(dst []byte, s string, pos uint) []byte { + i := int(pos) + start := 0 + + for i < len(s) { + b := s[i] + if b >= utf8.RuneSelf { + r, size := utf8.DecodeRuneInString(s[i:]) + if r == utf8.RuneError && size == 1 { + // In case of error, first append previous simple characters to + // the byte slice if any and append a replacement character code + // in place of the invalid sequence. + if start < i { + dst = append(dst, s[start:i]...) + } + dst = append(dst, `\ufffd`...) + i += size + start = i + continue + } + i += size + continue + } + if b >= 0x20 && b <= 0x7e && b != '\\' && b != '"' { + i++ + continue + } + // We encountered a character that needs to be encoded. + // Let's append the previous simple characters to the byte slice + // and switch our operation to read and encode the remainder + // characters byte-by-byte. + if start < i { + dst = append(dst, s[start:i]...) + } + switch b { + case '"', '\\': + dst = append(dst, '\\', b) + case '\b': + dst = append(dst, '\\', 'b') + case '\f': + dst = append(dst, '\\', 'f') + case '\n': + dst = append(dst, '\\', 'n') + case '\r': + dst = append(dst, '\\', 'r') + case '\t': + dst = append(dst, '\\', 't') + default: + dst = append(dst, '\\', 'u', '0', '0', hexTable[b>>4], hexTable[b&0xF]) + } + i++ + start = i + } + if start < len(s) { + dst = append(dst, s[start:]...) + } + return dst +} + +func decodeString(src *bufio.Reader, noQuotes bool) []byte { + pb := readByte(src) + major := pb & maskOutAdditionalType + minor := pb & maskOutMajorType + if major != majorTypeByteString { + panic(fmt.Errorf("Major type is: %d in decodeString", major)) + } + result := []byte{} + if !noQuotes { + result = append(result, '"') + } + length := decodeIntAdditionalType(src, minor) + len := int(length) + pbs := readNBytes(src, len) + result = append(result, pbs...) + if noQuotes { + return result + } + return append(result, '"') +} + +func decodeUTF8String(src *bufio.Reader) []byte { + pb := readByte(src) + major := pb & maskOutAdditionalType + minor := pb & maskOutMajorType + if major != majorTypeUtf8String { + panic(fmt.Errorf("Major type is: %d in decodeUTF8String", major)) + } + result := []byte{'"'} + length := decodeIntAdditionalType(src, minor) + len := int(length) + pbs := readNBytes(src, len) + + for i := 0; i < len; i++ { + // Check if the character needs encoding. Control characters, slashes, + // and the double quote need json encoding. Bytes above the ascii + // boundary needs utf8 encoding. + if pbs[i] < 0x20 || pbs[i] > 0x7e || pbs[i] == '\\' || pbs[i] == '"' { + // We encountered a character that needs to be encoded. Switch + // to complex version of the algorithm. + dst := []byte{'"'} + dst = decodeStringComplex(dst, string(pbs), uint(i)) + return append(dst, '"') + } + } + // The string has no need for encoding and therefore is directly + // appended to the byte slice. + result = append(result, pbs...) + return append(result, '"') +} + +func array2Json(src *bufio.Reader, dst io.Writer) { + dst.Write([]byte{'['}) + pb := readByte(src) + major := pb & maskOutAdditionalType + minor := pb & maskOutMajorType + if major != majorTypeArray { + panic(fmt.Errorf("Major type is: %d in array2Json", major)) + } + len := 0 + unSpecifiedCount := false + if minor == additionalTypeInfiniteCount { + unSpecifiedCount = true + } else { + length := decodeIntAdditionalType(src, minor) + len = int(length) + } + for i := 0; unSpecifiedCount || i < len; i++ { + if unSpecifiedCount { + pb, e := src.Peek(1) + if e != nil { + panic(e) + } + if pb[0] == majorTypeSimpleAndFloat|additionalTypeBreak { + readByte(src) + break + } + } + cbor2JsonOneObject(src, dst) + if unSpecifiedCount { + pb, e := src.Peek(1) + if e != nil { + panic(e) + } + if pb[0] == majorTypeSimpleAndFloat|additionalTypeBreak { + readByte(src) + break + } + dst.Write([]byte{','}) + } else if i+1 < len { + dst.Write([]byte{','}) + } + } + dst.Write([]byte{']'}) +} + +func map2Json(src *bufio.Reader, dst io.Writer) { + pb := readByte(src) + major := pb & maskOutAdditionalType + minor := pb & maskOutMajorType + if major != majorTypeMap { + panic(fmt.Errorf("Major type is: %d in map2Json", major)) + } + len := 0 + unSpecifiedCount := false + if minor == additionalTypeInfiniteCount { + unSpecifiedCount = true + } else { + length := decodeIntAdditionalType(src, minor) + len = int(length) + } + dst.Write([]byte{'{'}) + for i := 0; unSpecifiedCount || i < len; i++ { + if unSpecifiedCount { + pb, e := src.Peek(1) + if e != nil { + panic(e) + } + if pb[0] == majorTypeSimpleAndFloat|additionalTypeBreak { + readByte(src) + break + } + } + cbor2JsonOneObject(src, dst) + if i%2 == 0 { + // Even position values are keys. + dst.Write([]byte{':'}) + } else { + if unSpecifiedCount { + pb, e := src.Peek(1) + if e != nil { + panic(e) + } + if pb[0] == majorTypeSimpleAndFloat|additionalTypeBreak { + readByte(src) + break + } + dst.Write([]byte{','}) + } else if i+1 < len { + dst.Write([]byte{','}) + } + } + } + dst.Write([]byte{'}'}) +} + +func decodeTagData(src *bufio.Reader) []byte { + pb := readByte(src) + major := pb & maskOutAdditionalType + minor := pb & maskOutMajorType + if major != majorTypeTags { + panic(fmt.Errorf("Major type is: %d in decodeTagData", major)) + } + switch minor { + case additionalTypeTimestamp: + return decodeTimeStamp(src) + + // Tag value is larger than 256 (so uint16). + case additionalTypeIntUint16: + val := decodeIntAdditionalType(src, minor) + + switch uint16(val) { + case additionalTypeEmbeddedJSON: + pb := readByte(src) + dataMajor := pb & maskOutAdditionalType + if dataMajor != majorTypeByteString { + panic(fmt.Errorf("Unsupported embedded Type: %d in decodeEmbeddedJSON", dataMajor)) + } + src.UnreadByte() + return decodeString(src, true) + + case additionalTypeTagNetworkAddr: + octets := decodeString(src, true) + ss := []byte{'"'} + switch len(octets) { + case 6: // MAC address. + ha := net.HardwareAddr(octets) + ss = append(append(ss, ha.String()...), '"') + case 4: // IPv4 address. + fallthrough + case 16: // IPv6 address. + ip := net.IP(octets) + ss = append(append(ss, ip.String()...), '"') + default: + panic(fmt.Errorf("Unexpected Network Address length: %d (expected 4,6,16)", len(octets))) + } + return ss + + case additionalTypeTagNetworkPrefix: + pb := readByte(src) + if pb != majorTypeMap|0x1 { + panic(fmt.Errorf("IP Prefix is NOT of MAP of 1 elements as expected")) + } + octets := decodeString(src, true) + val := decodeInteger(src) + ip := net.IP(octets) + var mask net.IPMask + pfxLen := int(val) + if len(octets) == 4 { + mask = net.CIDRMask(pfxLen, 32) + } else { + mask = net.CIDRMask(pfxLen, 128) + } + ipPfx := net.IPNet{IP: ip, Mask: mask} + ss := []byte{'"'} + ss = append(append(ss, ipPfx.String()...), '"') + return ss + + case additionalTypeTagHexString: + octets := decodeString(src, true) + ss := []byte{'"'} + for _, v := range octets { + ss = append(ss, hexTable[v>>4], hexTable[v&0x0f]) + } + return append(ss, '"') + + default: + panic(fmt.Errorf("Unsupported Additional Tag Type: %d in decodeTagData", val)) + } + } + panic(fmt.Errorf("Unsupported Additional Type: %d in decodeTagData", minor)) +} + +func decodeTimeStamp(src *bufio.Reader) []byte { + pb := readByte(src) + src.UnreadByte() + tsMajor := pb & maskOutAdditionalType + if tsMajor == majorTypeUnsignedInt || tsMajor == majorTypeNegativeInt { + n := decodeInteger(src) + t := time.Unix(n, 0) + if decodeTimeZone != nil { + t = t.In(decodeTimeZone) + } else { + t = t.In(time.UTC) + } + tsb := []byte{} + tsb = append(tsb, '"') + tsb = t.AppendFormat(tsb, IntegerTimeFieldFormat) + tsb = append(tsb, '"') + return tsb + } else if tsMajor == majorTypeSimpleAndFloat { + n, _ := decodeFloat(src) + secs := int64(n) + n -= float64(secs) + n *= float64(1e9) + t := time.Unix(secs, int64(n)) + if decodeTimeZone != nil { + t = t.In(decodeTimeZone) + } else { + t = t.In(time.UTC) + } + tsb := []byte{} + tsb = append(tsb, '"') + tsb = t.AppendFormat(tsb, NanoTimeFieldFormat) + tsb = append(tsb, '"') + return tsb + } + panic(fmt.Errorf("TS format is neigther int nor float: %d", tsMajor)) +} + +func decodeSimpleFloat(src *bufio.Reader) []byte { + pb := readByte(src) + major := pb & maskOutAdditionalType + minor := pb & maskOutMajorType + if major != majorTypeSimpleAndFloat { + panic(fmt.Errorf("Major type is: %d in decodeSimpleFloat", major)) + } + switch minor { + case additionalTypeBoolTrue: + return []byte("true") + case additionalTypeBoolFalse: + return []byte("false") + case additionalTypeNull: + return []byte("null") + case additionalTypeFloat16: + fallthrough + case additionalTypeFloat32: + fallthrough + case additionalTypeFloat64: + src.UnreadByte() + v, bc := decodeFloat(src) + ba := []byte{} + switch { + case math.IsNaN(v): + return []byte("\"NaN\"") + case math.IsInf(v, 1): + return []byte("\"+Inf\"") + case math.IsInf(v, -1): + return []byte("\"-Inf\"") + } + if bc == isFloat32 { + ba = strconv.AppendFloat(ba, v, 'f', -1, 32) + } else if bc == isFloat64 { + ba = strconv.AppendFloat(ba, v, 'f', -1, 64) + } else { + panic(fmt.Errorf("Invalid Float precision from decodeFloat: %d", bc)) + } + return ba + default: + panic(fmt.Errorf("Invalid Additional Type: %d in decodeSimpleFloat", minor)) + } +} + +func cbor2JsonOneObject(src *bufio.Reader, dst io.Writer) { + pb, e := src.Peek(1) + if e != nil { + panic(e) + } + major := (pb[0] & maskOutAdditionalType) + + switch major { + case majorTypeUnsignedInt: + fallthrough + case majorTypeNegativeInt: + n := decodeInteger(src) + dst.Write([]byte(strconv.Itoa(int(n)))) + + case majorTypeByteString: + s := decodeString(src, false) + dst.Write(s) + + case majorTypeUtf8String: + s := decodeUTF8String(src) + dst.Write(s) + + case majorTypeArray: + array2Json(src, dst) + + case majorTypeMap: + map2Json(src, dst) + + case majorTypeTags: + s := decodeTagData(src) + dst.Write(s) + + case majorTypeSimpleAndFloat: + s := decodeSimpleFloat(src) + dst.Write(s) + } +} + +func moreBytesToRead(src *bufio.Reader) bool { + _, e := src.ReadByte() + if e == nil { + src.UnreadByte() + return true + } + return false +} + +// Cbor2JsonManyObjects decodes all the CBOR Objects read from src +// reader. It keeps on decoding until reader returns EOF (error when reading). +// Decoded string is written to the dst. At the end of every CBOR Object +// newline is written to the output stream. +// +// Returns error (if any) that was encountered during decode. +// The child functions will generate a panic when error is encountered and +// this function will recover non-runtime Errors and return the reason as error. +func Cbor2JsonManyObjects(src io.Reader, dst io.Writer) (err error) { + defer func() { + if r := recover(); r != nil { + if _, ok := r.(runtime.Error); ok { + panic(r) + } + err = r.(error) + } + }() + bufRdr := bufio.NewReader(src) + for moreBytesToRead(bufRdr) { + cbor2JsonOneObject(bufRdr, dst) + dst.Write([]byte("\n")) + } + return nil +} + +// Detect if the bytes to be printed is Binary or not. +func binaryFmt(p []byte) bool { + if len(p) > 0 && p[0] > 0x7F { + return true + } + return false +} + +func getReader(str string) *bufio.Reader { + return bufio.NewReader(strings.NewReader(str)) +} + +// DecodeIfBinaryToString converts a binary formatted log msg to a +// JSON formatted String Log message - suitable for printing to Console/Syslog. +func DecodeIfBinaryToString(in []byte) string { + if binaryFmt(in) { + var b bytes.Buffer + Cbor2JsonManyObjects(strings.NewReader(string(in)), &b) + return b.String() + } + return string(in) +} + +// DecodeObjectToStr checks if the input is a binary format, if so, +// it will decode a single Object and return the decoded string. +func DecodeObjectToStr(in []byte) string { + if binaryFmt(in) { + var b bytes.Buffer + cbor2JsonOneObject(getReader(string(in)), &b) + return b.String() + } + return string(in) +} + +// DecodeIfBinaryToBytes checks if the input is a binary format, if so, +// it will decode all Objects and return the decoded string as byte array. +func DecodeIfBinaryToBytes(in []byte) []byte { + if binaryFmt(in) { + var b bytes.Buffer + Cbor2JsonManyObjects(bytes.NewReader(in), &b) + return b.Bytes() + } + return in +} diff --git a/vendor/github.com/rs/zerolog/internal/cbor/string.go b/vendor/github.com/rs/zerolog/internal/cbor/string.go new file mode 100644 index 00000000..a33890a5 --- /dev/null +++ b/vendor/github.com/rs/zerolog/internal/cbor/string.go @@ -0,0 +1,95 @@ +package cbor + +import "fmt" + +// AppendStrings encodes and adds an array of strings to the dst byte array. +func (e Encoder) AppendStrings(dst []byte, vals []string) []byte { + major := majorTypeArray + l := len(vals) + if l <= additionalMax { + lb := byte(l) + dst = append(dst, major|lb) + } else { + dst = appendCborTypePrefix(dst, major, uint64(l)) + } + for _, v := range vals { + dst = e.AppendString(dst, v) + } + return dst +} + +// AppendString encodes and adds a string to the dst byte array. +func (Encoder) AppendString(dst []byte, s string) []byte { + major := majorTypeUtf8String + + l := len(s) + if l <= additionalMax { + lb := byte(l) + dst = append(dst, major|lb) + } else { + dst = appendCborTypePrefix(dst, majorTypeUtf8String, uint64(l)) + } + return append(dst, s...) +} + +// AppendStringers encodes and adds an array of Stringer values +// to the dst byte array. +func (e Encoder) AppendStringers(dst []byte, vals []fmt.Stringer) []byte { + if len(vals) == 0 { + return e.AppendArrayEnd(e.AppendArrayStart(dst)) + } + dst = e.AppendArrayStart(dst) + dst = e.AppendStringer(dst, vals[0]) + if len(vals) > 1 { + for _, val := range vals[1:] { + dst = e.AppendStringer(dst, val) + } + } + return e.AppendArrayEnd(dst) +} + +// AppendStringer encodes and adds the Stringer value to the dst +// byte array. +func (e Encoder) AppendStringer(dst []byte, val fmt.Stringer) []byte { + if val == nil { + return e.AppendNil(dst) + } + return e.AppendString(dst, val.String()) +} + +// AppendBytes encodes and adds an array of bytes to the dst byte array. +func (Encoder) AppendBytes(dst, s []byte) []byte { + major := majorTypeByteString + + l := len(s) + if l <= additionalMax { + lb := byte(l) + dst = append(dst, major|lb) + } else { + dst = appendCborTypePrefix(dst, major, uint64(l)) + } + return append(dst, s...) +} + +// AppendEmbeddedJSON adds a tag and embeds input JSON as such. +func AppendEmbeddedJSON(dst, s []byte) []byte { + major := majorTypeTags + minor := additionalTypeEmbeddedJSON + + // Append the TAG to indicate this is Embedded JSON. + dst = append(dst, major|additionalTypeIntUint16) + dst = append(dst, byte(minor>>8)) + dst = append(dst, byte(minor&0xff)) + + // Append the JSON Object as Byte String. + major = majorTypeByteString + + l := len(s) + if l <= additionalMax { + lb := byte(l) + dst = append(dst, major|lb) + } else { + dst = appendCborTypePrefix(dst, major, uint64(l)) + } + return append(dst, s...) +} diff --git a/vendor/github.com/rs/zerolog/internal/cbor/time.go b/vendor/github.com/rs/zerolog/internal/cbor/time.go new file mode 100644 index 00000000..d81fb125 --- /dev/null +++ b/vendor/github.com/rs/zerolog/internal/cbor/time.go @@ -0,0 +1,93 @@ +package cbor + +import ( + "time" +) + +func appendIntegerTimestamp(dst []byte, t time.Time) []byte { + major := majorTypeTags + minor := additionalTypeTimestamp + dst = append(dst, major|minor) + secs := t.Unix() + var val uint64 + if secs < 0 { + major = majorTypeNegativeInt + val = uint64(-secs - 1) + } else { + major = majorTypeUnsignedInt + val = uint64(secs) + } + dst = appendCborTypePrefix(dst, major, val) + return dst +} + +func (e Encoder) appendFloatTimestamp(dst []byte, t time.Time) []byte { + major := majorTypeTags + minor := additionalTypeTimestamp + dst = append(dst, major|minor) + secs := t.Unix() + nanos := t.Nanosecond() + var val float64 + val = float64(secs)*1.0 + float64(nanos)*1e-9 + return e.AppendFloat64(dst, val) +} + +// AppendTime encodes and adds a timestamp to the dst byte array. +func (e Encoder) AppendTime(dst []byte, t time.Time, unused string) []byte { + utc := t.UTC() + if utc.Nanosecond() == 0 { + return appendIntegerTimestamp(dst, utc) + } + return e.appendFloatTimestamp(dst, utc) +} + +// AppendTimes encodes and adds an array of timestamps to the dst byte array. +func (e Encoder) AppendTimes(dst []byte, vals []time.Time, unused string) []byte { + major := majorTypeArray + l := len(vals) + if l == 0 { + return e.AppendArrayEnd(e.AppendArrayStart(dst)) + } + if l <= additionalMax { + lb := byte(l) + dst = append(dst, major|lb) + } else { + dst = appendCborTypePrefix(dst, major, uint64(l)) + } + + for _, t := range vals { + dst = e.AppendTime(dst, t, unused) + } + return dst +} + +// AppendDuration encodes and adds a duration to the dst byte array. +// useInt field indicates whether to store the duration as seconds (integer) or +// as seconds+nanoseconds (float). +func (e Encoder) AppendDuration(dst []byte, d time.Duration, unit time.Duration, useInt bool) []byte { + if useInt { + return e.AppendInt64(dst, int64(d/unit)) + } + return e.AppendFloat64(dst, float64(d)/float64(unit)) +} + +// AppendDurations encodes and adds an array of durations to the dst byte array. +// useInt field indicates whether to store the duration as seconds (integer) or +// as seconds+nanoseconds (float). +func (e Encoder) AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, useInt bool) []byte { + major := majorTypeArray + l := len(vals) + if l == 0 { + return e.AppendArrayEnd(e.AppendArrayStart(dst)) + } + if l <= additionalMax { + lb := byte(l) + dst = append(dst, major|lb) + } else { + dst = appendCborTypePrefix(dst, major, uint64(l)) + } + for _, d := range vals { + dst = e.AppendDuration(dst, d, unit, useInt) + } + return dst +} diff --git a/vendor/github.com/rs/zerolog/internal/cbor/types.go b/vendor/github.com/rs/zerolog/internal/cbor/types.go new file mode 100644 index 00000000..6f538328 --- /dev/null +++ b/vendor/github.com/rs/zerolog/internal/cbor/types.go @@ -0,0 +1,486 @@ +package cbor + +import ( + "fmt" + "math" + "net" + "reflect" +) + +// AppendNil inserts a 'Nil' object into the dst byte array. +func (Encoder) AppendNil(dst []byte) []byte { + return append(dst, majorTypeSimpleAndFloat|additionalTypeNull) +} + +// AppendBeginMarker inserts a map start into the dst byte array. +func (Encoder) AppendBeginMarker(dst []byte) []byte { + return append(dst, majorTypeMap|additionalTypeInfiniteCount) +} + +// AppendEndMarker inserts a map end into the dst byte array. +func (Encoder) AppendEndMarker(dst []byte) []byte { + return append(dst, majorTypeSimpleAndFloat|additionalTypeBreak) +} + +// AppendObjectData takes an object in form of a byte array and appends to dst. +func (Encoder) AppendObjectData(dst []byte, o []byte) []byte { + // BeginMarker is present in the dst, which + // should not be copied when appending to existing data. + return append(dst, o[1:]...) +} + +// AppendArrayStart adds markers to indicate the start of an array. +func (Encoder) AppendArrayStart(dst []byte) []byte { + return append(dst, majorTypeArray|additionalTypeInfiniteCount) +} + +// AppendArrayEnd adds markers to indicate the end of an array. +func (Encoder) AppendArrayEnd(dst []byte) []byte { + return append(dst, majorTypeSimpleAndFloat|additionalTypeBreak) +} + +// AppendArrayDelim adds markers to indicate end of a particular array element. +func (Encoder) AppendArrayDelim(dst []byte) []byte { + //No delimiters needed in cbor + return dst +} + +// AppendLineBreak is a noop that keep API compat with json encoder. +func (Encoder) AppendLineBreak(dst []byte) []byte { + // No line breaks needed in binary format. + return dst +} + +// AppendBool encodes and inserts a boolean value into the dst byte array. +func (Encoder) AppendBool(dst []byte, val bool) []byte { + b := additionalTypeBoolFalse + if val { + b = additionalTypeBoolTrue + } + return append(dst, majorTypeSimpleAndFloat|b) +} + +// AppendBools encodes and inserts an array of boolean values into the dst byte array. +func (e Encoder) AppendBools(dst []byte, vals []bool) []byte { + major := majorTypeArray + l := len(vals) + if l == 0 { + return e.AppendArrayEnd(e.AppendArrayStart(dst)) + } + if l <= additionalMax { + lb := byte(l) + dst = append(dst, major|lb) + } else { + dst = appendCborTypePrefix(dst, major, uint64(l)) + } + for _, v := range vals { + dst = e.AppendBool(dst, v) + } + return dst +} + +// AppendInt encodes and inserts an integer value into the dst byte array. +func (Encoder) AppendInt(dst []byte, val int) []byte { + major := majorTypeUnsignedInt + contentVal := val + if val < 0 { + major = majorTypeNegativeInt + contentVal = -val - 1 + } + if contentVal <= additionalMax { + lb := byte(contentVal) + dst = append(dst, major|lb) + } else { + dst = appendCborTypePrefix(dst, major, uint64(contentVal)) + } + return dst +} + +// AppendInts encodes and inserts an array of integer values into the dst byte array. +func (e Encoder) AppendInts(dst []byte, vals []int) []byte { + major := majorTypeArray + l := len(vals) + if l == 0 { + return e.AppendArrayEnd(e.AppendArrayStart(dst)) + } + if l <= additionalMax { + lb := byte(l) + dst = append(dst, major|lb) + } else { + dst = appendCborTypePrefix(dst, major, uint64(l)) + } + for _, v := range vals { + dst = e.AppendInt(dst, v) + } + return dst +} + +// AppendInt8 encodes and inserts an int8 value into the dst byte array. +func (e Encoder) AppendInt8(dst []byte, val int8) []byte { + return e.AppendInt(dst, int(val)) +} + +// AppendInts8 encodes and inserts an array of integer values into the dst byte array. +func (e Encoder) AppendInts8(dst []byte, vals []int8) []byte { + major := majorTypeArray + l := len(vals) + if l == 0 { + return e.AppendArrayEnd(e.AppendArrayStart(dst)) + } + if l <= additionalMax { + lb := byte(l) + dst = append(dst, major|lb) + } else { + dst = appendCborTypePrefix(dst, major, uint64(l)) + } + for _, v := range vals { + dst = e.AppendInt(dst, int(v)) + } + return dst +} + +// AppendInt16 encodes and inserts a int16 value into the dst byte array. +func (e Encoder) AppendInt16(dst []byte, val int16) []byte { + return e.AppendInt(dst, int(val)) +} + +// AppendInts16 encodes and inserts an array of int16 values into the dst byte array. +func (e Encoder) AppendInts16(dst []byte, vals []int16) []byte { + major := majorTypeArray + l := len(vals) + if l == 0 { + return e.AppendArrayEnd(e.AppendArrayStart(dst)) + } + if l <= additionalMax { + lb := byte(l) + dst = append(dst, major|lb) + } else { + dst = appendCborTypePrefix(dst, major, uint64(l)) + } + for _, v := range vals { + dst = e.AppendInt(dst, int(v)) + } + return dst +} + +// AppendInt32 encodes and inserts a int32 value into the dst byte array. +func (e Encoder) AppendInt32(dst []byte, val int32) []byte { + return e.AppendInt(dst, int(val)) +} + +// AppendInts32 encodes and inserts an array of int32 values into the dst byte array. +func (e Encoder) AppendInts32(dst []byte, vals []int32) []byte { + major := majorTypeArray + l := len(vals) + if l == 0 { + return e.AppendArrayEnd(e.AppendArrayStart(dst)) + } + if l <= additionalMax { + lb := byte(l) + dst = append(dst, major|lb) + } else { + dst = appendCborTypePrefix(dst, major, uint64(l)) + } + for _, v := range vals { + dst = e.AppendInt(dst, int(v)) + } + return dst +} + +// AppendInt64 encodes and inserts a int64 value into the dst byte array. +func (Encoder) AppendInt64(dst []byte, val int64) []byte { + major := majorTypeUnsignedInt + contentVal := val + if val < 0 { + major = majorTypeNegativeInt + contentVal = -val - 1 + } + if contentVal <= additionalMax { + lb := byte(contentVal) + dst = append(dst, major|lb) + } else { + dst = appendCborTypePrefix(dst, major, uint64(contentVal)) + } + return dst +} + +// AppendInts64 encodes and inserts an array of int64 values into the dst byte array. +func (e Encoder) AppendInts64(dst []byte, vals []int64) []byte { + major := majorTypeArray + l := len(vals) + if l == 0 { + return e.AppendArrayEnd(e.AppendArrayStart(dst)) + } + if l <= additionalMax { + lb := byte(l) + dst = append(dst, major|lb) + } else { + dst = appendCborTypePrefix(dst, major, uint64(l)) + } + for _, v := range vals { + dst = e.AppendInt64(dst, v) + } + return dst +} + +// AppendUint encodes and inserts an unsigned integer value into the dst byte array. +func (e Encoder) AppendUint(dst []byte, val uint) []byte { + return e.AppendInt64(dst, int64(val)) +} + +// AppendUints encodes and inserts an array of unsigned integer values into the dst byte array. +func (e Encoder) AppendUints(dst []byte, vals []uint) []byte { + major := majorTypeArray + l := len(vals) + if l == 0 { + return e.AppendArrayEnd(e.AppendArrayStart(dst)) + } + if l <= additionalMax { + lb := byte(l) + dst = append(dst, major|lb) + } else { + dst = appendCborTypePrefix(dst, major, uint64(l)) + } + for _, v := range vals { + dst = e.AppendUint(dst, v) + } + return dst +} + +// AppendUint8 encodes and inserts a unsigned int8 value into the dst byte array. +func (e Encoder) AppendUint8(dst []byte, val uint8) []byte { + return e.AppendUint(dst, uint(val)) +} + +// AppendUints8 encodes and inserts an array of uint8 values into the dst byte array. +func (e Encoder) AppendUints8(dst []byte, vals []uint8) []byte { + major := majorTypeArray + l := len(vals) + if l == 0 { + return e.AppendArrayEnd(e.AppendArrayStart(dst)) + } + if l <= additionalMax { + lb := byte(l) + dst = append(dst, major|lb) + } else { + dst = appendCborTypePrefix(dst, major, uint64(l)) + } + for _, v := range vals { + dst = e.AppendUint8(dst, v) + } + return dst +} + +// AppendUint16 encodes and inserts a uint16 value into the dst byte array. +func (e Encoder) AppendUint16(dst []byte, val uint16) []byte { + return e.AppendUint(dst, uint(val)) +} + +// AppendUints16 encodes and inserts an array of uint16 values into the dst byte array. +func (e Encoder) AppendUints16(dst []byte, vals []uint16) []byte { + major := majorTypeArray + l := len(vals) + if l == 0 { + return e.AppendArrayEnd(e.AppendArrayStart(dst)) + } + if l <= additionalMax { + lb := byte(l) + dst = append(dst, major|lb) + } else { + dst = appendCborTypePrefix(dst, major, uint64(l)) + } + for _, v := range vals { + dst = e.AppendUint16(dst, v) + } + return dst +} + +// AppendUint32 encodes and inserts a uint32 value into the dst byte array. +func (e Encoder) AppendUint32(dst []byte, val uint32) []byte { + return e.AppendUint(dst, uint(val)) +} + +// AppendUints32 encodes and inserts an array of uint32 values into the dst byte array. +func (e Encoder) AppendUints32(dst []byte, vals []uint32) []byte { + major := majorTypeArray + l := len(vals) + if l == 0 { + return e.AppendArrayEnd(e.AppendArrayStart(dst)) + } + if l <= additionalMax { + lb := byte(l) + dst = append(dst, major|lb) + } else { + dst = appendCborTypePrefix(dst, major, uint64(l)) + } + for _, v := range vals { + dst = e.AppendUint32(dst, v) + } + return dst +} + +// AppendUint64 encodes and inserts a uint64 value into the dst byte array. +func (Encoder) AppendUint64(dst []byte, val uint64) []byte { + major := majorTypeUnsignedInt + contentVal := val + if contentVal <= additionalMax { + lb := byte(contentVal) + dst = append(dst, major|lb) + } else { + dst = appendCborTypePrefix(dst, major, contentVal) + } + return dst +} + +// AppendUints64 encodes and inserts an array of uint64 values into the dst byte array. +func (e Encoder) AppendUints64(dst []byte, vals []uint64) []byte { + major := majorTypeArray + l := len(vals) + if l == 0 { + return e.AppendArrayEnd(e.AppendArrayStart(dst)) + } + if l <= additionalMax { + lb := byte(l) + dst = append(dst, major|lb) + } else { + dst = appendCborTypePrefix(dst, major, uint64(l)) + } + for _, v := range vals { + dst = e.AppendUint64(dst, v) + } + return dst +} + +// AppendFloat32 encodes and inserts a single precision float value into the dst byte array. +func (Encoder) AppendFloat32(dst []byte, val float32) []byte { + switch { + case math.IsNaN(float64(val)): + return append(dst, "\xfa\x7f\xc0\x00\x00"...) + case math.IsInf(float64(val), 1): + return append(dst, "\xfa\x7f\x80\x00\x00"...) + case math.IsInf(float64(val), -1): + return append(dst, "\xfa\xff\x80\x00\x00"...) + } + major := majorTypeSimpleAndFloat + subType := additionalTypeFloat32 + n := math.Float32bits(val) + var buf [4]byte + for i := uint(0); i < 4; i++ { + buf[i] = byte(n >> ((3 - i) * 8)) + } + return append(append(dst, major|subType), buf[0], buf[1], buf[2], buf[3]) +} + +// AppendFloats32 encodes and inserts an array of single precision float value into the dst byte array. +func (e Encoder) AppendFloats32(dst []byte, vals []float32) []byte { + major := majorTypeArray + l := len(vals) + if l == 0 { + return e.AppendArrayEnd(e.AppendArrayStart(dst)) + } + if l <= additionalMax { + lb := byte(l) + dst = append(dst, major|lb) + } else { + dst = appendCborTypePrefix(dst, major, uint64(l)) + } + for _, v := range vals { + dst = e.AppendFloat32(dst, v) + } + return dst +} + +// AppendFloat64 encodes and inserts a double precision float value into the dst byte array. +func (Encoder) AppendFloat64(dst []byte, val float64) []byte { + switch { + case math.IsNaN(val): + return append(dst, "\xfb\x7f\xf8\x00\x00\x00\x00\x00\x00"...) + case math.IsInf(val, 1): + return append(dst, "\xfb\x7f\xf0\x00\x00\x00\x00\x00\x00"...) + case math.IsInf(val, -1): + return append(dst, "\xfb\xff\xf0\x00\x00\x00\x00\x00\x00"...) + } + major := majorTypeSimpleAndFloat + subType := additionalTypeFloat64 + n := math.Float64bits(val) + dst = append(dst, major|subType) + for i := uint(1); i <= 8; i++ { + b := byte(n >> ((8 - i) * 8)) + dst = append(dst, b) + } + return dst +} + +// AppendFloats64 encodes and inserts an array of double precision float values into the dst byte array. +func (e Encoder) AppendFloats64(dst []byte, vals []float64) []byte { + major := majorTypeArray + l := len(vals) + if l == 0 { + return e.AppendArrayEnd(e.AppendArrayStart(dst)) + } + if l <= additionalMax { + lb := byte(l) + dst = append(dst, major|lb) + } else { + dst = appendCborTypePrefix(dst, major, uint64(l)) + } + for _, v := range vals { + dst = e.AppendFloat64(dst, v) + } + return dst +} + +// AppendInterface takes an arbitrary object and converts it to JSON and embeds it dst. +func (e Encoder) AppendInterface(dst []byte, i interface{}) []byte { + marshaled, err := JSONMarshalFunc(i) + if err != nil { + return e.AppendString(dst, fmt.Sprintf("marshaling error: %v", err)) + } + return AppendEmbeddedJSON(dst, marshaled) +} + +// AppendType appends the parameter type (as a string) to the input byte slice. +func (e Encoder) AppendType(dst []byte, i interface{}) []byte { + if i == nil { + return e.AppendString(dst, "<nil>") + } + return e.AppendString(dst, reflect.TypeOf(i).String()) +} + +// AppendIPAddr encodes and inserts an IP Address (IPv4 or IPv6). +func (e Encoder) AppendIPAddr(dst []byte, ip net.IP) []byte { + dst = append(dst, majorTypeTags|additionalTypeIntUint16) + dst = append(dst, byte(additionalTypeTagNetworkAddr>>8)) + dst = append(dst, byte(additionalTypeTagNetworkAddr&0xff)) + return e.AppendBytes(dst, ip) +} + +// AppendIPPrefix encodes and inserts an IP Address Prefix (Address + Mask Length). +func (e Encoder) AppendIPPrefix(dst []byte, pfx net.IPNet) []byte { + dst = append(dst, majorTypeTags|additionalTypeIntUint16) + dst = append(dst, byte(additionalTypeTagNetworkPrefix>>8)) + dst = append(dst, byte(additionalTypeTagNetworkPrefix&0xff)) + + // Prefix is a tuple (aka MAP of 1 pair of elements) - + // first element is prefix, second is mask length. + dst = append(dst, majorTypeMap|0x1) + dst = e.AppendBytes(dst, pfx.IP) + maskLen, _ := pfx.Mask.Size() + return e.AppendUint8(dst, uint8(maskLen)) +} + +// AppendMACAddr encodes and inserts a Hardware (MAC) address. +func (e Encoder) AppendMACAddr(dst []byte, ha net.HardwareAddr) []byte { + dst = append(dst, majorTypeTags|additionalTypeIntUint16) + dst = append(dst, byte(additionalTypeTagNetworkAddr>>8)) + dst = append(dst, byte(additionalTypeTagNetworkAddr&0xff)) + return e.AppendBytes(dst, ha) +} + +// AppendHex adds a TAG and inserts a hex bytes as a string. +func (e Encoder) AppendHex(dst []byte, val []byte) []byte { + dst = append(dst, majorTypeTags|additionalTypeIntUint16) + dst = append(dst, byte(additionalTypeTagHexString>>8)) + dst = append(dst, byte(additionalTypeTagHexString&0xff)) + return e.AppendBytes(dst, val) +} diff --git a/vendor/github.com/rs/zerolog/internal/json/base.go b/vendor/github.com/rs/zerolog/internal/json/base.go new file mode 100644 index 00000000..09ec59f4 --- /dev/null +++ b/vendor/github.com/rs/zerolog/internal/json/base.go @@ -0,0 +1,19 @@ +package json + +// JSONMarshalFunc is used to marshal interface to JSON encoded byte slice. +// Making it package level instead of embedded in Encoder brings +// some extra efforts at importing, but avoids value copy when the functions +// of Encoder being invoked. +// DO REMEMBER to set this variable at importing, or +// you might get a nil pointer dereference panic at runtime. +var JSONMarshalFunc func(v interface{}) ([]byte, error) + +type Encoder struct{} + +// AppendKey appends a new key to the output JSON. +func (e Encoder) AppendKey(dst []byte, key string) []byte { + if dst[len(dst)-1] != '{' { + dst = append(dst, ',') + } + return append(e.AppendString(dst, key), ':') +} diff --git a/vendor/github.com/rs/zerolog/internal/json/bytes.go b/vendor/github.com/rs/zerolog/internal/json/bytes.go new file mode 100644 index 00000000..de64120d --- /dev/null +++ b/vendor/github.com/rs/zerolog/internal/json/bytes.go @@ -0,0 +1,85 @@ +package json + +import "unicode/utf8" + +// AppendBytes is a mirror of appendString with []byte arg +func (Encoder) AppendBytes(dst, s []byte) []byte { + dst = append(dst, '"') + for i := 0; i < len(s); i++ { + if !noEscapeTable[s[i]] { + dst = appendBytesComplex(dst, s, i) + return append(dst, '"') + } + } + dst = append(dst, s...) + return append(dst, '"') +} + +// AppendHex encodes the input bytes to a hex string and appends +// the encoded string to the input byte slice. +// +// The operation loops though each byte and encodes it as hex using +// the hex lookup table. +func (Encoder) AppendHex(dst, s []byte) []byte { + dst = append(dst, '"') + for _, v := range s { + dst = append(dst, hex[v>>4], hex[v&0x0f]) + } + return append(dst, '"') +} + +// appendBytesComplex is a mirror of the appendStringComplex +// with []byte arg +func appendBytesComplex(dst, s []byte, i int) []byte { + start := 0 + for i < len(s) { + b := s[i] + if b >= utf8.RuneSelf { + r, size := utf8.DecodeRune(s[i:]) + if r == utf8.RuneError && size == 1 { + if start < i { + dst = append(dst, s[start:i]...) + } + dst = append(dst, `\ufffd`...) + i += size + start = i + continue + } + i += size + continue + } + if noEscapeTable[b] { + i++ + continue + } + // We encountered a character that needs to be encoded. + // Let's append the previous simple characters to the byte slice + // and switch our operation to read and encode the remainder + // characters byte-by-byte. + if start < i { + dst = append(dst, s[start:i]...) + } + switch b { + case '"', '\\': + dst = append(dst, '\\', b) + case '\b': + dst = append(dst, '\\', 'b') + case '\f': + dst = append(dst, '\\', 'f') + case '\n': + dst = append(dst, '\\', 'n') + case '\r': + dst = append(dst, '\\', 'r') + case '\t': + dst = append(dst, '\\', 't') + default: + dst = append(dst, '\\', 'u', '0', '0', hex[b>>4], hex[b&0xF]) + } + i++ + start = i + } + if start < len(s) { + dst = append(dst, s[start:]...) + } + return dst +} diff --git a/vendor/github.com/rs/zerolog/internal/json/string.go b/vendor/github.com/rs/zerolog/internal/json/string.go new file mode 100644 index 00000000..fd7770f2 --- /dev/null +++ b/vendor/github.com/rs/zerolog/internal/json/string.go @@ -0,0 +1,149 @@ +package json + +import ( + "fmt" + "unicode/utf8" +) + +const hex = "0123456789abcdef" + +var noEscapeTable = [256]bool{} + +func init() { + for i := 0; i <= 0x7e; i++ { + noEscapeTable[i] = i >= 0x20 && i != '\\' && i != '"' + } +} + +// AppendStrings encodes the input strings to json and +// appends the encoded string list to the input byte slice. +func (e Encoder) AppendStrings(dst []byte, vals []string) []byte { + if len(vals) == 0 { + return append(dst, '[', ']') + } + dst = append(dst, '[') + dst = e.AppendString(dst, vals[0]) + if len(vals) > 1 { + for _, val := range vals[1:] { + dst = e.AppendString(append(dst, ','), val) + } + } + dst = append(dst, ']') + return dst +} + +// AppendString encodes the input string to json and appends +// the encoded string to the input byte slice. +// +// The operation loops though each byte in the string looking +// for characters that need json or utf8 encoding. If the string +// does not need encoding, then the string is appended in its +// entirety to the byte slice. +// If we encounter a byte that does need encoding, switch up +// the operation and perform a byte-by-byte read-encode-append. +func (Encoder) AppendString(dst []byte, s string) []byte { + // Start with a double quote. + dst = append(dst, '"') + // Loop through each character in the string. + for i := 0; i < len(s); i++ { + // Check if the character needs encoding. Control characters, slashes, + // and the double quote need json encoding. Bytes above the ascii + // boundary needs utf8 encoding. + if !noEscapeTable[s[i]] { + // We encountered a character that needs to be encoded. Switch + // to complex version of the algorithm. + dst = appendStringComplex(dst, s, i) + return append(dst, '"') + } + } + // The string has no need for encoding and therefore is directly + // appended to the byte slice. + dst = append(dst, s...) + // End with a double quote + return append(dst, '"') +} + +// AppendStringers encodes the provided Stringer list to json and +// appends the encoded Stringer list to the input byte slice. +func (e Encoder) AppendStringers(dst []byte, vals []fmt.Stringer) []byte { + if len(vals) == 0 { + return append(dst, '[', ']') + } + dst = append(dst, '[') + dst = e.AppendStringer(dst, vals[0]) + if len(vals) > 1 { + for _, val := range vals[1:] { + dst = e.AppendStringer(append(dst, ','), val) + } + } + return append(dst, ']') +} + +// AppendStringer encodes the input Stringer to json and appends the +// encoded Stringer value to the input byte slice. +func (e Encoder) AppendStringer(dst []byte, val fmt.Stringer) []byte { + if val == nil { + return e.AppendInterface(dst, nil) + } + return e.AppendString(dst, val.String()) +} + +//// appendStringComplex is used by appendString to take over an in +// progress JSON string encoding that encountered a character that needs +// to be encoded. +func appendStringComplex(dst []byte, s string, i int) []byte { + start := 0 + for i < len(s) { + b := s[i] + if b >= utf8.RuneSelf { + r, size := utf8.DecodeRuneInString(s[i:]) + if r == utf8.RuneError && size == 1 { + // In case of error, first append previous simple characters to + // the byte slice if any and append a replacement character code + // in place of the invalid sequence. + if start < i { + dst = append(dst, s[start:i]...) + } + dst = append(dst, `\ufffd`...) + i += size + start = i + continue + } + i += size + continue + } + if noEscapeTable[b] { + i++ + continue + } + // We encountered a character that needs to be encoded. + // Let's append the previous simple characters to the byte slice + // and switch our operation to read and encode the remainder + // characters byte-by-byte. + if start < i { + dst = append(dst, s[start:i]...) + } + switch b { + case '"', '\\': + dst = append(dst, '\\', b) + case '\b': + dst = append(dst, '\\', 'b') + case '\f': + dst = append(dst, '\\', 'f') + case '\n': + dst = append(dst, '\\', 'n') + case '\r': + dst = append(dst, '\\', 'r') + case '\t': + dst = append(dst, '\\', 't') + default: + dst = append(dst, '\\', 'u', '0', '0', hex[b>>4], hex[b&0xF]) + } + i++ + start = i + } + if start < len(s) { + dst = append(dst, s[start:]...) + } + return dst +} diff --git a/vendor/github.com/rs/zerolog/internal/json/time.go b/vendor/github.com/rs/zerolog/internal/json/time.go new file mode 100644 index 00000000..6a8dc912 --- /dev/null +++ b/vendor/github.com/rs/zerolog/internal/json/time.go @@ -0,0 +1,113 @@ +package json + +import ( + "strconv" + "time" +) + +const ( + // Import from zerolog/global.go + timeFormatUnix = "" + timeFormatUnixMs = "UNIXMS" + timeFormatUnixMicro = "UNIXMICRO" + timeFormatUnixNano = "UNIXNANO" +) + +// AppendTime formats the input time with the given format +// and appends the encoded string to the input byte slice. +func (e Encoder) AppendTime(dst []byte, t time.Time, format string) []byte { + switch format { + case timeFormatUnix: + return e.AppendInt64(dst, t.Unix()) + case timeFormatUnixMs: + return e.AppendInt64(dst, t.UnixNano()/1000000) + case timeFormatUnixMicro: + return e.AppendInt64(dst, t.UnixNano()/1000) + case timeFormatUnixNano: + return e.AppendInt64(dst, t.UnixNano()) + } + return append(t.AppendFormat(append(dst, '"'), format), '"') +} + +// AppendTimes converts the input times with the given format +// and appends the encoded string list to the input byte slice. +func (Encoder) AppendTimes(dst []byte, vals []time.Time, format string) []byte { + switch format { + case timeFormatUnix: + return appendUnixTimes(dst, vals) + case timeFormatUnixMs: + return appendUnixNanoTimes(dst, vals, 1000000) + case timeFormatUnixMicro: + return appendUnixNanoTimes(dst, vals, 1000) + case timeFormatUnixNano: + return appendUnixNanoTimes(dst, vals, 1) + } + if len(vals) == 0 { + return append(dst, '[', ']') + } + dst = append(dst, '[') + dst = append(vals[0].AppendFormat(append(dst, '"'), format), '"') + if len(vals) > 1 { + for _, t := range vals[1:] { + dst = append(t.AppendFormat(append(dst, ',', '"'), format), '"') + } + } + dst = append(dst, ']') + return dst +} + +func appendUnixTimes(dst []byte, vals []time.Time) []byte { + if len(vals) == 0 { + return append(dst, '[', ']') + } + dst = append(dst, '[') + dst = strconv.AppendInt(dst, vals[0].Unix(), 10) + if len(vals) > 1 { + for _, t := range vals[1:] { + dst = strconv.AppendInt(append(dst, ','), t.Unix(), 10) + } + } + dst = append(dst, ']') + return dst +} + +func appendUnixNanoTimes(dst []byte, vals []time.Time, div int64) []byte { + if len(vals) == 0 { + return append(dst, '[', ']') + } + dst = append(dst, '[') + dst = strconv.AppendInt(dst, vals[0].UnixNano()/div, 10) + if len(vals) > 1 { + for _, t := range vals[1:] { + dst = strconv.AppendInt(append(dst, ','), t.UnixNano()/div, 10) + } + } + dst = append(dst, ']') + return dst +} + +// AppendDuration formats the input duration with the given unit & format +// and appends the encoded string to the input byte slice. +func (e Encoder) AppendDuration(dst []byte, d time.Duration, unit time.Duration, useInt bool) []byte { + if useInt { + return strconv.AppendInt(dst, int64(d/unit), 10) + } + return e.AppendFloat64(dst, float64(d)/float64(unit)) +} + +// AppendDurations formats the input durations with the given unit & format +// and appends the encoded string list to the input byte slice. +func (e Encoder) AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, useInt bool) []byte { + if len(vals) == 0 { + return append(dst, '[', ']') + } + dst = append(dst, '[') + dst = e.AppendDuration(dst, vals[0], unit, useInt) + if len(vals) > 1 { + for _, d := range vals[1:] { + dst = e.AppendDuration(append(dst, ','), d, unit, useInt) + } + } + dst = append(dst, ']') + return dst +} diff --git a/vendor/github.com/rs/zerolog/internal/json/types.go b/vendor/github.com/rs/zerolog/internal/json/types.go new file mode 100644 index 00000000..ef3a2a7a --- /dev/null +++ b/vendor/github.com/rs/zerolog/internal/json/types.go @@ -0,0 +1,414 @@ +package json + +import ( + "fmt" + "math" + "net" + "reflect" + "strconv" +) + +// AppendNil inserts a 'Nil' object into the dst byte array. +func (Encoder) AppendNil(dst []byte) []byte { + return append(dst, "null"...) +} + +// AppendBeginMarker inserts a map start into the dst byte array. +func (Encoder) AppendBeginMarker(dst []byte) []byte { + return append(dst, '{') +} + +// AppendEndMarker inserts a map end into the dst byte array. +func (Encoder) AppendEndMarker(dst []byte) []byte { + return append(dst, '}') +} + +// AppendLineBreak appends a line break. +func (Encoder) AppendLineBreak(dst []byte) []byte { + return append(dst, '\n') +} + +// AppendArrayStart adds markers to indicate the start of an array. +func (Encoder) AppendArrayStart(dst []byte) []byte { + return append(dst, '[') +} + +// AppendArrayEnd adds markers to indicate the end of an array. +func (Encoder) AppendArrayEnd(dst []byte) []byte { + return append(dst, ']') +} + +// AppendArrayDelim adds markers to indicate end of a particular array element. +func (Encoder) AppendArrayDelim(dst []byte) []byte { + if len(dst) > 0 { + return append(dst, ',') + } + return dst +} + +// AppendBool converts the input bool to a string and +// appends the encoded string to the input byte slice. +func (Encoder) AppendBool(dst []byte, val bool) []byte { + return strconv.AppendBool(dst, val) +} + +// AppendBools encodes the input bools to json and +// appends the encoded string list to the input byte slice. +func (Encoder) AppendBools(dst []byte, vals []bool) []byte { + if len(vals) == 0 { + return append(dst, '[', ']') + } + dst = append(dst, '[') + dst = strconv.AppendBool(dst, vals[0]) + if len(vals) > 1 { + for _, val := range vals[1:] { + dst = strconv.AppendBool(append(dst, ','), val) + } + } + dst = append(dst, ']') + return dst +} + +// AppendInt converts the input int to a string and +// appends the encoded string to the input byte slice. +func (Encoder) AppendInt(dst []byte, val int) []byte { + return strconv.AppendInt(dst, int64(val), 10) +} + +// AppendInts encodes the input ints to json and +// appends the encoded string list to the input byte slice. +func (Encoder) AppendInts(dst []byte, vals []int) []byte { + if len(vals) == 0 { + return append(dst, '[', ']') + } + dst = append(dst, '[') + dst = strconv.AppendInt(dst, int64(vals[0]), 10) + if len(vals) > 1 { + for _, val := range vals[1:] { + dst = strconv.AppendInt(append(dst, ','), int64(val), 10) + } + } + dst = append(dst, ']') + return dst +} + +// AppendInt8 converts the input []int8 to a string and +// appends the encoded string to the input byte slice. +func (Encoder) AppendInt8(dst []byte, val int8) []byte { + return strconv.AppendInt(dst, int64(val), 10) +} + +// AppendInts8 encodes the input int8s to json and +// appends the encoded string list to the input byte slice. +func (Encoder) AppendInts8(dst []byte, vals []int8) []byte { + if len(vals) == 0 { + return append(dst, '[', ']') + } + dst = append(dst, '[') + dst = strconv.AppendInt(dst, int64(vals[0]), 10) + if len(vals) > 1 { + for _, val := range vals[1:] { + dst = strconv.AppendInt(append(dst, ','), int64(val), 10) + } + } + dst = append(dst, ']') + return dst +} + +// AppendInt16 converts the input int16 to a string and +// appends the encoded string to the input byte slice. +func (Encoder) AppendInt16(dst []byte, val int16) []byte { + return strconv.AppendInt(dst, int64(val), 10) +} + +// AppendInts16 encodes the input int16s to json and +// appends the encoded string list to the input byte slice. +func (Encoder) AppendInts16(dst []byte, vals []int16) []byte { + if len(vals) == 0 { + return append(dst, '[', ']') + } + dst = append(dst, '[') + dst = strconv.AppendInt(dst, int64(vals[0]), 10) + if len(vals) > 1 { + for _, val := range vals[1:] { + dst = strconv.AppendInt(append(dst, ','), int64(val), 10) + } + } + dst = append(dst, ']') + return dst +} + +// AppendInt32 converts the input int32 to a string and +// appends the encoded string to the input byte slice. +func (Encoder) AppendInt32(dst []byte, val int32) []byte { + return strconv.AppendInt(dst, int64(val), 10) +} + +// AppendInts32 encodes the input int32s to json and +// appends the encoded string list to the input byte slice. +func (Encoder) AppendInts32(dst []byte, vals []int32) []byte { + if len(vals) == 0 { + return append(dst, '[', ']') + } + dst = append(dst, '[') + dst = strconv.AppendInt(dst, int64(vals[0]), 10) + if len(vals) > 1 { + for _, val := range vals[1:] { + dst = strconv.AppendInt(append(dst, ','), int64(val), 10) + } + } + dst = append(dst, ']') + return dst +} + +// AppendInt64 converts the input int64 to a string and +// appends the encoded string to the input byte slice. +func (Encoder) AppendInt64(dst []byte, val int64) []byte { + return strconv.AppendInt(dst, val, 10) +} + +// AppendInts64 encodes the input int64s to json and +// appends the encoded string list to the input byte slice. +func (Encoder) AppendInts64(dst []byte, vals []int64) []byte { + if len(vals) == 0 { + return append(dst, '[', ']') + } + dst = append(dst, '[') + dst = strconv.AppendInt(dst, vals[0], 10) + if len(vals) > 1 { + for _, val := range vals[1:] { + dst = strconv.AppendInt(append(dst, ','), val, 10) + } + } + dst = append(dst, ']') + return dst +} + +// AppendUint converts the input uint to a string and +// appends the encoded string to the input byte slice. +func (Encoder) AppendUint(dst []byte, val uint) []byte { + return strconv.AppendUint(dst, uint64(val), 10) +} + +// AppendUints encodes the input uints to json and +// appends the encoded string list to the input byte slice. +func (Encoder) AppendUints(dst []byte, vals []uint) []byte { + if len(vals) == 0 { + return append(dst, '[', ']') + } + dst = append(dst, '[') + dst = strconv.AppendUint(dst, uint64(vals[0]), 10) + if len(vals) > 1 { + for _, val := range vals[1:] { + dst = strconv.AppendUint(append(dst, ','), uint64(val), 10) + } + } + dst = append(dst, ']') + return dst +} + +// AppendUint8 converts the input uint8 to a string and +// appends the encoded string to the input byte slice. +func (Encoder) AppendUint8(dst []byte, val uint8) []byte { + return strconv.AppendUint(dst, uint64(val), 10) +} + +// AppendUints8 encodes the input uint8s to json and +// appends the encoded string list to the input byte slice. +func (Encoder) AppendUints8(dst []byte, vals []uint8) []byte { + if len(vals) == 0 { + return append(dst, '[', ']') + } + dst = append(dst, '[') + dst = strconv.AppendUint(dst, uint64(vals[0]), 10) + if len(vals) > 1 { + for _, val := range vals[1:] { + dst = strconv.AppendUint(append(dst, ','), uint64(val), 10) + } + } + dst = append(dst, ']') + return dst +} + +// AppendUint16 converts the input uint16 to a string and +// appends the encoded string to the input byte slice. +func (Encoder) AppendUint16(dst []byte, val uint16) []byte { + return strconv.AppendUint(dst, uint64(val), 10) +} + +// AppendUints16 encodes the input uint16s to json and +// appends the encoded string list to the input byte slice. +func (Encoder) AppendUints16(dst []byte, vals []uint16) []byte { + if len(vals) == 0 { + return append(dst, '[', ']') + } + dst = append(dst, '[') + dst = strconv.AppendUint(dst, uint64(vals[0]), 10) + if len(vals) > 1 { + for _, val := range vals[1:] { + dst = strconv.AppendUint(append(dst, ','), uint64(val), 10) + } + } + dst = append(dst, ']') + return dst +} + +// AppendUint32 converts the input uint32 to a string and +// appends the encoded string to the input byte slice. +func (Encoder) AppendUint32(dst []byte, val uint32) []byte { + return strconv.AppendUint(dst, uint64(val), 10) +} + +// AppendUints32 encodes the input uint32s to json and +// appends the encoded string list to the input byte slice. +func (Encoder) AppendUints32(dst []byte, vals []uint32) []byte { + if len(vals) == 0 { + return append(dst, '[', ']') + } + dst = append(dst, '[') + dst = strconv.AppendUint(dst, uint64(vals[0]), 10) + if len(vals) > 1 { + for _, val := range vals[1:] { + dst = strconv.AppendUint(append(dst, ','), uint64(val), 10) + } + } + dst = append(dst, ']') + return dst +} + +// AppendUint64 converts the input uint64 to a string and +// appends the encoded string to the input byte slice. +func (Encoder) AppendUint64(dst []byte, val uint64) []byte { + return strconv.AppendUint(dst, val, 10) +} + +// AppendUints64 encodes the input uint64s to json and +// appends the encoded string list to the input byte slice. +func (Encoder) AppendUints64(dst []byte, vals []uint64) []byte { + if len(vals) == 0 { + return append(dst, '[', ']') + } + dst = append(dst, '[') + dst = strconv.AppendUint(dst, vals[0], 10) + if len(vals) > 1 { + for _, val := range vals[1:] { + dst = strconv.AppendUint(append(dst, ','), val, 10) + } + } + dst = append(dst, ']') + return dst +} + +func appendFloat(dst []byte, val float64, bitSize int) []byte { + // JSON does not permit NaN or Infinity. A typical JSON encoder would fail + // with an error, but a logging library wants the data to get through so we + // make a tradeoff and store those types as string. + switch { + case math.IsNaN(val): + return append(dst, `"NaN"`...) + case math.IsInf(val, 1): + return append(dst, `"+Inf"`...) + case math.IsInf(val, -1): + return append(dst, `"-Inf"`...) + } + return strconv.AppendFloat(dst, val, 'f', -1, bitSize) +} + +// AppendFloat32 converts the input float32 to a string and +// appends the encoded string to the input byte slice. +func (Encoder) AppendFloat32(dst []byte, val float32) []byte { + return appendFloat(dst, float64(val), 32) +} + +// AppendFloats32 encodes the input float32s to json and +// appends the encoded string list to the input byte slice. +func (Encoder) AppendFloats32(dst []byte, vals []float32) []byte { + if len(vals) == 0 { + return append(dst, '[', ']') + } + dst = append(dst, '[') + dst = appendFloat(dst, float64(vals[0]), 32) + if len(vals) > 1 { + for _, val := range vals[1:] { + dst = appendFloat(append(dst, ','), float64(val), 32) + } + } + dst = append(dst, ']') + return dst +} + +// AppendFloat64 converts the input float64 to a string and +// appends the encoded string to the input byte slice. +func (Encoder) AppendFloat64(dst []byte, val float64) []byte { + return appendFloat(dst, val, 64) +} + +// AppendFloats64 encodes the input float64s to json and +// appends the encoded string list to the input byte slice. +func (Encoder) AppendFloats64(dst []byte, vals []float64) []byte { + if len(vals) == 0 { + return append(dst, '[', ']') + } + dst = append(dst, '[') + dst = appendFloat(dst, vals[0], 64) + if len(vals) > 1 { + for _, val := range vals[1:] { + dst = appendFloat(append(dst, ','), val, 64) + } + } + dst = append(dst, ']') + return dst +} + +// AppendInterface marshals the input interface to a string and +// appends the encoded string to the input byte slice. +func (e Encoder) AppendInterface(dst []byte, i interface{}) []byte { + marshaled, err := JSONMarshalFunc(i) + if err != nil { + return e.AppendString(dst, fmt.Sprintf("marshaling error: %v", err)) + } + return append(dst, marshaled...) +} + +// AppendType appends the parameter type (as a string) to the input byte slice. +func (e Encoder) AppendType(dst []byte, i interface{}) []byte { + if i == nil { + return e.AppendString(dst, "<nil>") + } + return e.AppendString(dst, reflect.TypeOf(i).String()) +} + +// AppendObjectData takes in an object that is already in a byte array +// and adds it to the dst. +func (Encoder) AppendObjectData(dst []byte, o []byte) []byte { + // Three conditions apply here: + // 1. new content starts with '{' - which should be dropped OR + // 2. new content starts with '{' - which should be replaced with ',' + // to separate with existing content OR + // 3. existing content has already other fields + if o[0] == '{' { + if len(dst) > 1 { + dst = append(dst, ',') + } + o = o[1:] + } else if len(dst) > 1 { + dst = append(dst, ',') + } + return append(dst, o...) +} + +// AppendIPAddr adds IPv4 or IPv6 address to dst. +func (e Encoder) AppendIPAddr(dst []byte, ip net.IP) []byte { + return e.AppendString(dst, ip.String()) +} + +// AppendIPPrefix adds IPv4 or IPv6 Prefix (address & mask) to dst. +func (e Encoder) AppendIPPrefix(dst []byte, pfx net.IPNet) []byte { + return e.AppendString(dst, pfx.String()) + +} + +// AppendMACAddr adds MAC address to dst. +func (e Encoder) AppendMACAddr(dst []byte, ha net.HardwareAddr) []byte { + return e.AppendString(dst, ha.String()) +} diff --git a/vendor/github.com/rs/zerolog/log.go b/vendor/github.com/rs/zerolog/log.go new file mode 100644 index 00000000..c8dbc425 --- /dev/null +++ b/vendor/github.com/rs/zerolog/log.go @@ -0,0 +1,477 @@ +// Package zerolog provides a lightweight logging library dedicated to JSON logging. +// +// A global Logger can be use for simple logging: +// +// import "github.com/rs/zerolog/log" +// +// log.Info().Msg("hello world") +// // Output: {"time":1494567715,"level":"info","message":"hello world"} +// +// NOTE: To import the global logger, import the "log" subpackage "github.com/rs/zerolog/log". +// +// Fields can be added to log messages: +// +// log.Info().Str("foo", "bar").Msg("hello world") +// // Output: {"time":1494567715,"level":"info","message":"hello world","foo":"bar"} +// +// Create logger instance to manage different outputs: +// +// logger := zerolog.New(os.Stderr).With().Timestamp().Logger() +// logger.Info(). +// Str("foo", "bar"). +// Msg("hello world") +// // Output: {"time":1494567715,"level":"info","message":"hello world","foo":"bar"} +// +// Sub-loggers let you chain loggers with additional context: +// +// sublogger := log.With().Str("component": "foo").Logger() +// sublogger.Info().Msg("hello world") +// // Output: {"time":1494567715,"level":"info","message":"hello world","component":"foo"} +// +// Level logging +// +// zerolog.SetGlobalLevel(zerolog.InfoLevel) +// +// log.Debug().Msg("filtered out message") +// log.Info().Msg("routed message") +// +// if e := log.Debug(); e.Enabled() { +// // Compute log output only if enabled. +// value := compute() +// e.Str("foo": value).Msg("some debug message") +// } +// // Output: {"level":"info","time":1494567715,"routed message"} +// +// Customize automatic field names: +// +// log.TimestampFieldName = "t" +// log.LevelFieldName = "p" +// log.MessageFieldName = "m" +// +// log.Info().Msg("hello world") +// // Output: {"t":1494567715,"p":"info","m":"hello world"} +// +// Log with no level and message: +// +// log.Log().Str("foo","bar").Msg("") +// // Output: {"time":1494567715,"foo":"bar"} +// +// Add contextual fields to global Logger: +// +// log.Logger = log.With().Str("foo", "bar").Logger() +// +// Sample logs: +// +// sampled := log.Sample(&zerolog.BasicSampler{N: 10}) +// sampled.Info().Msg("will be logged every 10 messages") +// +// Log with contextual hooks: +// +// // Create the hook: +// type SeverityHook struct{} +// +// func (h SeverityHook) Run(e *zerolog.Event, level zerolog.Level, msg string) { +// if level != zerolog.NoLevel { +// e.Str("severity", level.String()) +// } +// } +// +// // And use it: +// var h SeverityHook +// log := zerolog.New(os.Stdout).Hook(h) +// log.Warn().Msg("") +// // Output: {"level":"warn","severity":"warn"} +// +// +// Caveats +// +// There is no fields deduplication out-of-the-box. +// Using the same key multiple times creates new key in final JSON each time. +// +// logger := zerolog.New(os.Stderr).With().Timestamp().Logger() +// logger.Info(). +// Timestamp(). +// Msg("dup") +// // Output: {"level":"info","time":1494567715,"time":1494567715,"message":"dup"} +// +// In this case, many consumers will take the last value, +// but this is not guaranteed; check yours if in doubt. +package zerolog + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "strconv" + "strings" +) + +// Level defines log levels. +type Level int8 + +const ( + // DebugLevel defines debug log level. + DebugLevel Level = iota + // InfoLevel defines info log level. + InfoLevel + // WarnLevel defines warn log level. + WarnLevel + // ErrorLevel defines error log level. + ErrorLevel + // FatalLevel defines fatal log level. + FatalLevel + // PanicLevel defines panic log level. + PanicLevel + // NoLevel defines an absent log level. + NoLevel + // Disabled disables the logger. + Disabled + + // TraceLevel defines trace log level. + TraceLevel Level = -1 + // Values less than TraceLevel are handled as numbers. +) + +func (l Level) String() string { + switch l { + case TraceLevel: + return LevelTraceValue + case DebugLevel: + return LevelDebugValue + case InfoLevel: + return LevelInfoValue + case WarnLevel: + return LevelWarnValue + case ErrorLevel: + return LevelErrorValue + case FatalLevel: + return LevelFatalValue + case PanicLevel: + return LevelPanicValue + case Disabled: + return "disabled" + case NoLevel: + return "" + } + return strconv.Itoa(int(l)) +} + +// ParseLevel converts a level string into a zerolog Level value. +// returns an error if the input string does not match known values. +func ParseLevel(levelStr string) (Level, error) { + switch strings.ToLower(levelStr) { + case LevelFieldMarshalFunc(TraceLevel): + return TraceLevel, nil + case LevelFieldMarshalFunc(DebugLevel): + return DebugLevel, nil + case LevelFieldMarshalFunc(InfoLevel): + return InfoLevel, nil + case LevelFieldMarshalFunc(WarnLevel): + return WarnLevel, nil + case LevelFieldMarshalFunc(ErrorLevel): + return ErrorLevel, nil + case LevelFieldMarshalFunc(FatalLevel): + return FatalLevel, nil + case LevelFieldMarshalFunc(PanicLevel): + return PanicLevel, nil + case LevelFieldMarshalFunc(Disabled): + return Disabled, nil + case LevelFieldMarshalFunc(NoLevel): + return NoLevel, nil + } + i, err := strconv.Atoi(levelStr) + if err != nil { + return NoLevel, fmt.Errorf("Unknown Level String: '%s', defaulting to NoLevel", levelStr) + } + if i > 127 || i < -128 { + return NoLevel, fmt.Errorf("Out-Of-Bounds Level: '%d', defaulting to NoLevel", i) + } + return Level(i), nil +} + +// UnmarshalText implements encoding.TextUnmarshaler to allow for easy reading from toml/yaml/json formats +func (l *Level) UnmarshalText(text []byte) error { + if l == nil { + return errors.New("can't unmarshal a nil *Level") + } + var err error + *l, err = ParseLevel(string(text)) + return err +} + +// MarshalText implements encoding.TextMarshaler to allow for easy writing into toml/yaml/json formats +func (l Level) MarshalText() ([]byte, error) { + return []byte(LevelFieldMarshalFunc(l)), nil +} + +// A Logger represents an active logging object that generates lines +// of JSON output to an io.Writer. Each logging operation makes a single +// call to the Writer's Write method. There is no guarantee on access +// serialization to the Writer. If your Writer is not thread safe, +// you may consider a sync wrapper. +type Logger struct { + w LevelWriter + level Level + sampler Sampler + context []byte + hooks []Hook + stack bool +} + +// New creates a root logger with given output writer. If the output writer implements +// the LevelWriter interface, the WriteLevel method will be called instead of the Write +// one. +// +// Each logging operation makes a single call to the Writer's Write method. There is no +// guarantee on access serialization to the Writer. If your Writer is not thread safe, +// you may consider using sync wrapper. +func New(w io.Writer) Logger { + if w == nil { + w = ioutil.Discard + } + lw, ok := w.(LevelWriter) + if !ok { + lw = levelWriterAdapter{w} + } + return Logger{w: lw, level: TraceLevel} +} + +// Nop returns a disabled logger for which all operation are no-op. +func Nop() Logger { + return New(nil).Level(Disabled) +} + +// Output duplicates the current logger and sets w as its output. +func (l Logger) Output(w io.Writer) Logger { + l2 := New(w) + l2.level = l.level + l2.sampler = l.sampler + l2.stack = l.stack + if len(l.hooks) > 0 { + l2.hooks = append(l2.hooks, l.hooks...) + } + if l.context != nil { + l2.context = make([]byte, len(l.context), cap(l.context)) + copy(l2.context, l.context) + } + return l2 +} + +// With creates a child logger with the field added to its context. +func (l Logger) With() Context { + context := l.context + l.context = make([]byte, 0, 500) + if context != nil { + l.context = append(l.context, context...) + } else { + // This is needed for AppendKey to not check len of input + // thus making it inlinable + l.context = enc.AppendBeginMarker(l.context) + } + return Context{l} +} + +// UpdateContext updates the internal logger's context. +// +// Use this method with caution. If unsure, prefer the With method. +func (l *Logger) UpdateContext(update func(c Context) Context) { + if l == disabledLogger { + return + } + if cap(l.context) == 0 { + l.context = make([]byte, 0, 500) + } + if len(l.context) == 0 { + l.context = enc.AppendBeginMarker(l.context) + } + c := update(Context{*l}) + l.context = c.l.context +} + +// Level creates a child logger with the minimum accepted level set to level. +func (l Logger) Level(lvl Level) Logger { + l.level = lvl + return l +} + +// GetLevel returns the current Level of l. +func (l Logger) GetLevel() Level { + return l.level +} + +// Sample returns a logger with the s sampler. +func (l Logger) Sample(s Sampler) Logger { + l.sampler = s + return l +} + +// Hook returns a logger with the h Hook. +func (l Logger) Hook(h Hook) Logger { + l.hooks = append(l.hooks, h) + return l +} + +// Trace starts a new message with trace level. +// +// You must call Msg on the returned event in order to send the event. +func (l *Logger) Trace() *Event { + return l.newEvent(TraceLevel, nil) +} + +// Debug starts a new message with debug level. +// +// You must call Msg on the returned event in order to send the event. +func (l *Logger) Debug() *Event { + return l.newEvent(DebugLevel, nil) +} + +// Info starts a new message with info level. +// +// You must call Msg on the returned event in order to send the event. +func (l *Logger) Info() *Event { + return l.newEvent(InfoLevel, nil) +} + +// Warn starts a new message with warn level. +// +// You must call Msg on the returned event in order to send the event. +func (l *Logger) Warn() *Event { + return l.newEvent(WarnLevel, nil) +} + +// Error starts a new message with error level. +// +// You must call Msg on the returned event in order to send the event. +func (l *Logger) Error() *Event { + return l.newEvent(ErrorLevel, nil) +} + +// Err starts a new message with error level with err as a field if not nil or +// with info level if err is nil. +// +// You must call Msg on the returned event in order to send the event. +func (l *Logger) Err(err error) *Event { + if err != nil { + return l.Error().Err(err) + } + + return l.Info() +} + +// Fatal starts a new message with fatal level. The os.Exit(1) function +// is called by the Msg method, which terminates the program immediately. +// +// You must call Msg on the returned event in order to send the event. +func (l *Logger) Fatal() *Event { + return l.newEvent(FatalLevel, func(msg string) { os.Exit(1) }) +} + +// Panic starts a new message with panic level. The panic() function +// is called by the Msg method, which stops the ordinary flow of a goroutine. +// +// You must call Msg on the returned event in order to send the event. +func (l *Logger) Panic() *Event { + return l.newEvent(PanicLevel, func(msg string) { panic(msg) }) +} + +// WithLevel starts a new message with level. Unlike Fatal and Panic +// methods, WithLevel does not terminate the program or stop the ordinary +// flow of a goroutine when used with their respective levels. +// +// You must call Msg on the returned event in order to send the event. +func (l *Logger) WithLevel(level Level) *Event { + switch level { + case TraceLevel: + return l.Trace() + case DebugLevel: + return l.Debug() + case InfoLevel: + return l.Info() + case WarnLevel: + return l.Warn() + case ErrorLevel: + return l.Error() + case FatalLevel: + return l.newEvent(FatalLevel, nil) + case PanicLevel: + return l.newEvent(PanicLevel, nil) + case NoLevel: + return l.Log() + case Disabled: + return nil + default: + return l.newEvent(level, nil) + } +} + +// Log starts a new message with no level. Setting GlobalLevel to Disabled +// will still disable events produced by this method. +// +// You must call Msg on the returned event in order to send the event. +func (l *Logger) Log() *Event { + return l.newEvent(NoLevel, nil) +} + +// Print sends a log event using debug level and no extra field. +// Arguments are handled in the manner of fmt.Print. +func (l *Logger) Print(v ...interface{}) { + if e := l.Debug(); e.Enabled() { + e.CallerSkipFrame(1).Msg(fmt.Sprint(v...)) + } +} + +// Printf sends a log event using debug level and no extra field. +// Arguments are handled in the manner of fmt.Printf. +func (l *Logger) Printf(format string, v ...interface{}) { + if e := l.Debug(); e.Enabled() { + e.CallerSkipFrame(1).Msg(fmt.Sprintf(format, v...)) + } +} + +// Write implements the io.Writer interface. This is useful to set as a writer +// for the standard library log. +func (l Logger) Write(p []byte) (n int, err error) { + n = len(p) + if n > 0 && p[n-1] == '\n' { + // Trim CR added by stdlog. + p = p[0 : n-1] + } + l.Log().CallerSkipFrame(1).Msg(string(p)) + return +} + +func (l *Logger) newEvent(level Level, done func(string)) *Event { + enabled := l.should(level) + if !enabled { + if done != nil { + done("") + } + return nil + } + e := newEvent(l.w, level) + e.done = done + e.ch = l.hooks + if level != NoLevel && LevelFieldName != "" { + e.Str(LevelFieldName, LevelFieldMarshalFunc(level)) + } + if l.context != nil && len(l.context) > 1 { + e.buf = enc.AppendObjectData(e.buf, l.context) + } + if l.stack { + e.Stack() + } + return e +} + +// should returns true if the log event should be logged. +func (l *Logger) should(lvl Level) bool { + if lvl < l.level || lvl < GlobalLevel() { + return false + } + if l.sampler != nil && !samplingDisabled() { + return l.sampler.Sample(lvl) + } + return true +} diff --git a/vendor/github.com/rs/zerolog/not_go112.go b/vendor/github.com/rs/zerolog/not_go112.go new file mode 100644 index 00000000..4c43c9e7 --- /dev/null +++ b/vendor/github.com/rs/zerolog/not_go112.go @@ -0,0 +1,5 @@ +// +build !go1.12 + +package zerolog + +const contextCallerSkipFrameCount = 3 diff --git a/vendor/github.com/rs/zerolog/pretty.png b/vendor/github.com/rs/zerolog/pretty.png Binary files differnew file mode 100644 index 00000000..24203368 --- /dev/null +++ b/vendor/github.com/rs/zerolog/pretty.png diff --git a/vendor/github.com/rs/zerolog/sampler.go b/vendor/github.com/rs/zerolog/sampler.go new file mode 100644 index 00000000..1be98c4f --- /dev/null +++ b/vendor/github.com/rs/zerolog/sampler.go @@ -0,0 +1,134 @@ +package zerolog + +import ( + "math/rand" + "sync/atomic" + "time" +) + +var ( + // Often samples log every ~ 10 events. + Often = RandomSampler(10) + // Sometimes samples log every ~ 100 events. + Sometimes = RandomSampler(100) + // Rarely samples log every ~ 1000 events. + Rarely = RandomSampler(1000) +) + +// Sampler defines an interface to a log sampler. +type Sampler interface { + // Sample returns true if the event should be part of the sample, false if + // the event should be dropped. + Sample(lvl Level) bool +} + +// RandomSampler use a PRNG to randomly sample an event out of N events, +// regardless of their level. +type RandomSampler uint32 + +// Sample implements the Sampler interface. +func (s RandomSampler) Sample(lvl Level) bool { + if s <= 0 { + return false + } + if rand.Intn(int(s)) != 0 { + return false + } + return true +} + +// BasicSampler is a sampler that will send every Nth events, regardless of +// their level. +type BasicSampler struct { + N uint32 + counter uint32 +} + +// Sample implements the Sampler interface. +func (s *BasicSampler) Sample(lvl Level) bool { + n := s.N + if n == 1 { + return true + } + c := atomic.AddUint32(&s.counter, 1) + return c%n == 1 +} + +// BurstSampler lets Burst events pass per Period then pass the decision to +// NextSampler. If Sampler is not set, all subsequent events are rejected. +type BurstSampler struct { + // Burst is the maximum number of event per period allowed before calling + // NextSampler. + Burst uint32 + // Period defines the burst period. If 0, NextSampler is always called. + Period time.Duration + // NextSampler is the sampler used after the burst is reached. If nil, + // events are always rejected after the burst. + NextSampler Sampler + + counter uint32 + resetAt int64 +} + +// Sample implements the Sampler interface. +func (s *BurstSampler) Sample(lvl Level) bool { + if s.Burst > 0 && s.Period > 0 { + if s.inc() <= s.Burst { + return true + } + } + if s.NextSampler == nil { + return false + } + return s.NextSampler.Sample(lvl) +} + +func (s *BurstSampler) inc() uint32 { + now := time.Now().UnixNano() + resetAt := atomic.LoadInt64(&s.resetAt) + var c uint32 + if now > resetAt { + c = 1 + atomic.StoreUint32(&s.counter, c) + newResetAt := now + s.Period.Nanoseconds() + reset := atomic.CompareAndSwapInt64(&s.resetAt, resetAt, newResetAt) + if !reset { + // Lost the race with another goroutine trying to reset. + c = atomic.AddUint32(&s.counter, 1) + } + } else { + c = atomic.AddUint32(&s.counter, 1) + } + return c +} + +// LevelSampler applies a different sampler for each level. +type LevelSampler struct { + TraceSampler, DebugSampler, InfoSampler, WarnSampler, ErrorSampler Sampler +} + +func (s LevelSampler) Sample(lvl Level) bool { + switch lvl { + case TraceLevel: + if s.TraceSampler != nil { + return s.TraceSampler.Sample(lvl) + } + case DebugLevel: + if s.DebugSampler != nil { + return s.DebugSampler.Sample(lvl) + } + case InfoLevel: + if s.InfoSampler != nil { + return s.InfoSampler.Sample(lvl) + } + case WarnLevel: + if s.WarnSampler != nil { + return s.WarnSampler.Sample(lvl) + } + case ErrorLevel: + if s.ErrorSampler != nil { + return s.ErrorSampler.Sample(lvl) + } + } + return true +} diff --git a/vendor/github.com/rs/zerolog/syslog.go b/vendor/github.com/rs/zerolog/syslog.go new file mode 100644 index 00000000..c4082830 --- /dev/null +++ b/vendor/github.com/rs/zerolog/syslog.go @@ -0,0 +1,80 @@ +// +build !windows +// +build !binary_log + +package zerolog + +import ( + "io" +) + +// See http://cee.mitre.org/language/1.0-beta1/clt.html#syslog +// or https://www.rsyslog.com/json-elasticsearch/ +const ceePrefix = "@cee:" + +// SyslogWriter is an interface matching a syslog.Writer struct. +type SyslogWriter interface { + io.Writer + Debug(m string) error + Info(m string) error + Warning(m string) error + Err(m string) error + Emerg(m string) error + Crit(m string) error +} + +type syslogWriter struct { + w SyslogWriter + prefix string +} + +// SyslogLevelWriter wraps a SyslogWriter and call the right syslog level +// method matching the zerolog level. +func SyslogLevelWriter(w SyslogWriter) LevelWriter { + return syslogWriter{w, ""} +} + +// SyslogCEEWriter wraps a SyslogWriter with a SyslogLevelWriter that adds a +// MITRE CEE prefix for JSON syslog entries, compatible with rsyslog +// and syslog-ng JSON logging support. +// See https://www.rsyslog.com/json-elasticsearch/ +func SyslogCEEWriter(w SyslogWriter) LevelWriter { + return syslogWriter{w, ceePrefix} +} + +func (sw syslogWriter) Write(p []byte) (n int, err error) { + var pn int + if sw.prefix != "" { + pn, err = sw.w.Write([]byte(sw.prefix)) + if err != nil { + return pn, err + } + } + n, err = sw.w.Write(p) + return pn + n, err +} + +// WriteLevel implements LevelWriter interface. +func (sw syslogWriter) WriteLevel(level Level, p []byte) (n int, err error) { + switch level { + case TraceLevel: + case DebugLevel: + err = sw.w.Debug(sw.prefix + string(p)) + case InfoLevel: + err = sw.w.Info(sw.prefix + string(p)) + case WarnLevel: + err = sw.w.Warning(sw.prefix + string(p)) + case ErrorLevel: + err = sw.w.Err(sw.prefix + string(p)) + case FatalLevel: + err = sw.w.Emerg(sw.prefix + string(p)) + case PanicLevel: + err = sw.w.Crit(sw.prefix + string(p)) + case NoLevel: + err = sw.w.Info(sw.prefix + string(p)) + default: + panic("invalid level") + } + // Any CEE prefix is not part of the message, so we don't include its length + n = len(p) + return +} diff --git a/vendor/github.com/rs/zerolog/writer.go b/vendor/github.com/rs/zerolog/writer.go new file mode 100644 index 00000000..26f5e632 --- /dev/null +++ b/vendor/github.com/rs/zerolog/writer.go @@ -0,0 +1,154 @@ +package zerolog + +import ( + "bytes" + "io" + "path" + "runtime" + "strconv" + "strings" + "sync" +) + +// LevelWriter defines as interface a writer may implement in order +// to receive level information with payload. +type LevelWriter interface { + io.Writer + WriteLevel(level Level, p []byte) (n int, err error) +} + +type levelWriterAdapter struct { + io.Writer +} + +func (lw levelWriterAdapter) WriteLevel(l Level, p []byte) (n int, err error) { + return lw.Write(p) +} + +type syncWriter struct { + mu sync.Mutex + lw LevelWriter +} + +// SyncWriter wraps w so that each call to Write is synchronized with a mutex. +// This syncer can be used to wrap the call to writer's Write method if it is +// not thread safe. Note that you do not need this wrapper for os.File Write +// operations on POSIX and Windows systems as they are already thread-safe. +func SyncWriter(w io.Writer) io.Writer { + if lw, ok := w.(LevelWriter); ok { + return &syncWriter{lw: lw} + } + return &syncWriter{lw: levelWriterAdapter{w}} +} + +// Write implements the io.Writer interface. +func (s *syncWriter) Write(p []byte) (n int, err error) { + s.mu.Lock() + defer s.mu.Unlock() + return s.lw.Write(p) +} + +// WriteLevel implements the LevelWriter interface. +func (s *syncWriter) WriteLevel(l Level, p []byte) (n int, err error) { + s.mu.Lock() + defer s.mu.Unlock() + return s.lw.WriteLevel(l, p) +} + +type multiLevelWriter struct { + writers []LevelWriter +} + +func (t multiLevelWriter) Write(p []byte) (n int, err error) { + for _, w := range t.writers { + if _n, _err := w.Write(p); err == nil { + n = _n + if _err != nil { + err = _err + } else if _n != len(p) { + err = io.ErrShortWrite + } + } + } + return n, err +} + +func (t multiLevelWriter) WriteLevel(l Level, p []byte) (n int, err error) { + for _, w := range t.writers { + if _n, _err := w.WriteLevel(l, p); err == nil { + n = _n + if _err != nil { + err = _err + } else if _n != len(p) { + err = io.ErrShortWrite + } + } + } + return n, err +} + +// MultiLevelWriter creates a writer that duplicates its writes to all the +// provided writers, similar to the Unix tee(1) command. If some writers +// implement LevelWriter, their WriteLevel method will be used instead of Write. +func MultiLevelWriter(writers ...io.Writer) LevelWriter { + lwriters := make([]LevelWriter, 0, len(writers)) + for _, w := range writers { + if lw, ok := w.(LevelWriter); ok { + lwriters = append(lwriters, lw) + } else { + lwriters = append(lwriters, levelWriterAdapter{w}) + } + } + return multiLevelWriter{lwriters} +} + +// TestingLog is the logging interface of testing.TB. +type TestingLog interface { + Log(args ...interface{}) + Logf(format string, args ...interface{}) + Helper() +} + +// TestWriter is a writer that writes to testing.TB. +type TestWriter struct { + T TestingLog + + // Frame skips caller frames to capture the original file and line numbers. + Frame int +} + +// NewTestWriter creates a writer that logs to the testing.TB. +func NewTestWriter(t TestingLog) TestWriter { + return TestWriter{T: t} +} + +// Write to testing.TB. +func (t TestWriter) Write(p []byte) (n int, err error) { + t.T.Helper() + + n = len(p) + + // Strip trailing newline because t.Log always adds one. + p = bytes.TrimRight(p, "\n") + + // Try to correct the log file and line number to the caller. + if t.Frame > 0 { + _, origFile, origLine, _ := runtime.Caller(1) + _, frameFile, frameLine, ok := runtime.Caller(1 + t.Frame) + if ok { + erase := strings.Repeat("\b", len(path.Base(origFile))+len(strconv.Itoa(origLine))+3) + t.T.Logf("%s%s:%d: %s", erase, path.Base(frameFile), frameLine, p) + return n, err + } + } + t.T.Log(string(p)) + + return n, err +} + +// ConsoleTestWriter creates an option that correctly sets the file frame depth for testing.TB log. +func ConsoleTestWriter(t TestingLog) func(w *ConsoleWriter) { + return func(w *ConsoleWriter) { + w.Out = TestWriter{T: t, Frame: 6} + } +} diff --git a/vendor/github.com/tidwall/gjson/LICENSE b/vendor/github.com/tidwall/gjson/LICENSE new file mode 100644 index 00000000..58f5819a --- /dev/null +++ b/vendor/github.com/tidwall/gjson/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2016 Josh Baker + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/tidwall/gjson/README.md b/vendor/github.com/tidwall/gjson/README.md new file mode 100644 index 00000000..c8db11f1 --- /dev/null +++ b/vendor/github.com/tidwall/gjson/README.md @@ -0,0 +1,497 @@ +<p align="center"> +<img + src="logo.png" + width="240" height="78" border="0" alt="GJSON"> +<br> +<a href="https://godoc.org/github.com/tidwall/gjson"><img src="https://img.shields.io/badge/api-reference-blue.svg?style=flat-square" alt="GoDoc"></a> +<a href="https://tidwall.com/gjson-play"><img src="https://img.shields.io/badge/%F0%9F%8F%90-playground-9900cc.svg?style=flat-square" alt="GJSON Playground"></a> +<a href="SYNTAX.md"><img src="https://img.shields.io/badge/{}-syntax-33aa33.svg?style=flat-square" alt="GJSON Syntax"></a> + +</p> + +<p align="center">get json values quickly</a></p> + +GJSON is a Go package that provides a [fast](#performance) and [simple](#get-a-value) way to get values from a json document. +It has features such as [one line retrieval](#get-a-value), [dot notation paths](#path-syntax), [iteration](#iterate-through-an-object-or-array), and [parsing json lines](#json-lines). + +Also check out [SJSON](https://github.com/tidwall/sjson) for modifying json, and the [JJ](https://github.com/tidwall/jj) command line tool. + +This README is a quick overview of how to use GJSON, for more information check out [GJSON Syntax](SYNTAX.md). + +GJSON is also available for [Python](https://github.com/volans-/gjson-py) and [Rust](https://github.com/tidwall/gjson.rs) + +Getting Started +=============== + +## Installing + +To start using GJSON, install Go and run `go get`: + +```sh +$ go get -u github.com/tidwall/gjson +``` + +This will retrieve the library. + +## Get a value +Get searches json for the specified path. A path is in dot syntax, such as "name.last" or "age". When the value is found it's returned immediately. + +```go +package main + +import "github.com/tidwall/gjson" + +const json = `{"name":{"first":"Janet","last":"Prichard"},"age":47}` + +func main() { + value := gjson.Get(json, "name.last") + println(value.String()) +} +``` + +This will print: + +``` +Prichard +``` +*There's also the [GetMany](#get-multiple-values-at-once) function to get multiple values at once, and [GetBytes](#working-with-bytes) for working with JSON byte slices.* + +## Path Syntax + +Below is a quick overview of the path syntax, for more complete information please +check out [GJSON Syntax](SYNTAX.md). + +A path is a series of keys separated by a dot. +A key may contain special wildcard characters '\*' and '?'. +To access an array value use the index as the key. +To get the number of elements in an array or to access a child path, use the '#' character. +The dot and wildcard characters can be escaped with '\\'. + +```json +{ + "name": {"first": "Tom", "last": "Anderson"}, + "age":37, + "children": ["Sara","Alex","Jack"], + "fav.movie": "Deer Hunter", + "friends": [ + {"first": "Dale", "last": "Murphy", "age": 44, "nets": ["ig", "fb", "tw"]}, + {"first": "Roger", "last": "Craig", "age": 68, "nets": ["fb", "tw"]}, + {"first": "Jane", "last": "Murphy", "age": 47, "nets": ["ig", "tw"]} + ] +} +``` +``` +"name.last" >> "Anderson" +"age" >> 37 +"children" >> ["Sara","Alex","Jack"] +"children.#" >> 3 +"children.1" >> "Alex" +"child*.2" >> "Jack" +"c?ildren.0" >> "Sara" +"fav\.movie" >> "Deer Hunter" +"friends.#.first" >> ["Dale","Roger","Jane"] +"friends.1.last" >> "Craig" +``` + +You can also query an array for the first match by using `#(...)`, or find all +matches with `#(...)#`. Queries support the `==`, `!=`, `<`, `<=`, `>`, `>=` +comparison operators and the simple pattern matching `%` (like) and `!%` +(not like) operators. + +``` +friends.#(last=="Murphy").first >> "Dale" +friends.#(last=="Murphy")#.first >> ["Dale","Jane"] +friends.#(age>45)#.last >> ["Craig","Murphy"] +friends.#(first%"D*").last >> "Murphy" +friends.#(first!%"D*").last >> "Craig" +friends.#(nets.#(=="fb"))#.first >> ["Dale","Roger"] +``` + +*Please note that prior to v1.3.0, queries used the `#[...]` brackets. This was +changed in v1.3.0 as to avoid confusion with the new +[multipath](SYNTAX.md#multipaths) syntax. For backwards compatibility, +`#[...]` will continue to work until the next major release.* + +## Result Type + +GJSON supports the json types `string`, `number`, `bool`, and `null`. +Arrays and Objects are returned as their raw json types. + +The `Result` type holds one of these: + +``` +bool, for JSON booleans +float64, for JSON numbers +string, for JSON string literals +nil, for JSON null +``` + +To directly access the value: + +```go +result.Type // can be String, Number, True, False, Null, or JSON +result.Str // holds the string +result.Num // holds the float64 number +result.Raw // holds the raw json +result.Index // index of raw value in original json, zero means index unknown +result.Indexes // indexes of all the elements that match on a path containing the '#' query character. +``` + +There are a variety of handy functions that work on a result: + +```go +result.Exists() bool +result.Value() interface{} +result.Int() int64 +result.Uint() uint64 +result.Float() float64 +result.String() string +result.Bool() bool +result.Time() time.Time +result.Array() []gjson.Result +result.Map() map[string]gjson.Result +result.Get(path string) Result +result.ForEach(iterator func(key, value Result) bool) +result.Less(token Result, caseSensitive bool) bool +``` + +The `result.Value()` function returns an `interface{}` which requires type assertion and is one of the following Go types: + +```go +boolean >> bool +number >> float64 +string >> string +null >> nil +array >> []interface{} +object >> map[string]interface{} +``` + +The `result.Array()` function returns back an array of values. +If the result represents a non-existent value, then an empty array will be returned. +If the result is not a JSON array, the return value will be an array containing one result. + +### 64-bit integers + +The `result.Int()` and `result.Uint()` calls are capable of reading all 64 bits, allowing for large JSON integers. + +```go +result.Int() int64 // -9223372036854775808 to 9223372036854775807 +result.Uint() uint64 // 0 to 18446744073709551615 +``` + +## Modifiers and path chaining + +New in version 1.2 is support for modifier functions and path chaining. + +A modifier is a path component that performs custom processing on the +json. + +Multiple paths can be "chained" together using the pipe character. +This is useful for getting results from a modified query. + +For example, using the built-in `@reverse` modifier on the above json document, +we'll get `children` array and reverse the order: + +``` +"children|@reverse" >> ["Jack","Alex","Sara"] +"children|@reverse|0" >> "Jack" +``` + +There are currently the following built-in modifiers: + +- `@reverse`: Reverse an array or the members of an object. +- `@ugly`: Remove all whitespace from a json document. +- `@pretty`: Make the json document more human readable. +- `@this`: Returns the current element. It can be used to retrieve the root element. +- `@valid`: Ensure the json document is valid. +- `@flatten`: Flattens an array. +- `@join`: Joins multiple objects into a single object. +- `@keys`: Returns an array of keys for an object. +- `@values`: Returns an array of values for an object. +- `@tostr`: Converts json to a string. Wraps a json string. +- `@fromstr`: Converts a string from json. Unwraps a json string. +- `@group`: Groups arrays of objects. See [e4fc67c](https://github.com/tidwall/gjson/commit/e4fc67c92aeebf2089fabc7872f010e340d105db). + +### Modifier arguments + +A modifier may accept an optional argument. The argument can be a valid JSON +document or just characters. + +For example, the `@pretty` modifier takes a json object as its argument. + +``` +@pretty:{"sortKeys":true} +``` + +Which makes the json pretty and orders all of its keys. + +```json +{ + "age":37, + "children": ["Sara","Alex","Jack"], + "fav.movie": "Deer Hunter", + "friends": [ + {"age": 44, "first": "Dale", "last": "Murphy"}, + {"age": 68, "first": "Roger", "last": "Craig"}, + {"age": 47, "first": "Jane", "last": "Murphy"} + ], + "name": {"first": "Tom", "last": "Anderson"} +} +``` + +*The full list of `@pretty` options are `sortKeys`, `indent`, `prefix`, and `width`. +Please see [Pretty Options](https://github.com/tidwall/pretty#customized-output) for more information.* + +### Custom modifiers + +You can also add custom modifiers. + +For example, here we create a modifier that makes the entire json document upper +or lower case. + +```go +gjson.AddModifier("case", func(json, arg string) string { + if arg == "upper" { + return strings.ToUpper(json) + } + if arg == "lower" { + return strings.ToLower(json) + } + return json +}) +``` + +``` +"children|@case:upper" >> ["SARA","ALEX","JACK"] +"children|@case:lower|@reverse" >> ["jack","alex","sara"] +``` + +## JSON Lines + +There's support for [JSON Lines](http://jsonlines.org/) using the `..` prefix, which treats a multilined document as an array. + +For example: + +``` +{"name": "Gilbert", "age": 61} +{"name": "Alexa", "age": 34} +{"name": "May", "age": 57} +{"name": "Deloise", "age": 44} +``` + +``` +..# >> 4 +..1 >> {"name": "Alexa", "age": 34} +..3 >> {"name": "Deloise", "age": 44} +..#.name >> ["Gilbert","Alexa","May","Deloise"] +..#(name="May").age >> 57 +``` + +The `ForEachLines` function will iterate through JSON lines. + +```go +gjson.ForEachLine(json, func(line gjson.Result) bool{ + println(line.String()) + return true +}) +``` + +## Get nested array values + +Suppose you want all the last names from the following json: + +```json +{ + "programmers": [ + { + "firstName": "Janet", + "lastName": "McLaughlin", + }, { + "firstName": "Elliotte", + "lastName": "Hunter", + }, { + "firstName": "Jason", + "lastName": "Harold", + } + ] +} +``` + +You would use the path "programmers.#.lastName" like such: + +```go +result := gjson.Get(json, "programmers.#.lastName") +for _, name := range result.Array() { + println(name.String()) +} +``` + +You can also query an object inside an array: + +```go +name := gjson.Get(json, `programmers.#(lastName="Hunter").firstName`) +println(name.String()) // prints "Elliotte" +``` + +## Iterate through an object or array + +The `ForEach` function allows for quickly iterating through an object or array. +The key and value are passed to the iterator function for objects. +Only the value is passed for arrays. +Returning `false` from an iterator will stop iteration. + +```go +result := gjson.Get(json, "programmers") +result.ForEach(func(key, value gjson.Result) bool { + println(value.String()) + return true // keep iterating +}) +``` + +## Simple Parse and Get + +There's a `Parse(json)` function that will do a simple parse, and `result.Get(path)` that will search a result. + +For example, all of these will return the same result: + +```go +gjson.Parse(json).Get("name").Get("last") +gjson.Get(json, "name").Get("last") +gjson.Get(json, "name.last") +``` + +## Check for the existence of a value + +Sometimes you just want to know if a value exists. + +```go +value := gjson.Get(json, "name.last") +if !value.Exists() { + println("no last name") +} else { + println(value.String()) +} + +// Or as one step +if gjson.Get(json, "name.last").Exists() { + println("has a last name") +} +``` + +## Validate JSON + +The `Get*` and `Parse*` functions expects that the json is well-formed. Bad json will not panic, but it may return back unexpected results. + +If you are consuming JSON from an unpredictable source then you may want to validate prior to using GJSON. + +```go +if !gjson.Valid(json) { + return errors.New("invalid json") +} +value := gjson.Get(json, "name.last") +``` + +## Unmarshal to a map + +To unmarshal to a `map[string]interface{}`: + +```go +m, ok := gjson.Parse(json).Value().(map[string]interface{}) +if !ok { + // not a map +} +``` + +## Working with Bytes + +If your JSON is contained in a `[]byte` slice, there's the [GetBytes](https://godoc.org/github.com/tidwall/gjson#GetBytes) function. This is preferred over `Get(string(data), path)`. + +```go +var json []byte = ... +result := gjson.GetBytes(json, path) +``` + +If you are using the `gjson.GetBytes(json, path)` function and you want to avoid converting `result.Raw` to a `[]byte`, then you can use this pattern: + +```go +var json []byte = ... +result := gjson.GetBytes(json, path) +var raw []byte +if result.Index > 0 { + raw = json[result.Index:result.Index+len(result.Raw)] +} else { + raw = []byte(result.Raw) +} +``` + +This is a best-effort no allocation sub slice of the original json. This method utilizes the `result.Index` field, which is the position of the raw data in the original json. It's possible that the value of `result.Index` equals zero, in which case the `result.Raw` is converted to a `[]byte`. + +## Get multiple values at once + +The `GetMany` function can be used to get multiple values at the same time. + +```go +results := gjson.GetMany(json, "name.first", "name.last", "age") +``` + +The return value is a `[]Result`, which will always contain exactly the same number of items as the input paths. + +## Performance + +Benchmarks of GJSON alongside [encoding/json](https://golang.org/pkg/encoding/json/), +[ffjson](https://github.com/pquerna/ffjson), +[EasyJSON](https://github.com/mailru/easyjson), +[jsonparser](https://github.com/buger/jsonparser), +and [json-iterator](https://github.com/json-iterator/go) + +``` +BenchmarkGJSONGet-16 11644512 311 ns/op 0 B/op 0 allocs/op +BenchmarkGJSONUnmarshalMap-16 1122678 3094 ns/op 1920 B/op 26 allocs/op +BenchmarkJSONUnmarshalMap-16 516681 6810 ns/op 2944 B/op 69 allocs/op +BenchmarkJSONUnmarshalStruct-16 697053 5400 ns/op 928 B/op 13 allocs/op +BenchmarkJSONDecoder-16 330450 10217 ns/op 3845 B/op 160 allocs/op +BenchmarkFFJSONLexer-16 1424979 2585 ns/op 880 B/op 8 allocs/op +BenchmarkEasyJSONLexer-16 3000000 729 ns/op 501 B/op 5 allocs/op +BenchmarkJSONParserGet-16 3000000 366 ns/op 21 B/op 0 allocs/op +BenchmarkJSONIterator-16 3000000 869 ns/op 693 B/op 14 allocs/op +``` + +JSON document used: + +```json +{ + "widget": { + "debug": "on", + "window": { + "title": "Sample Konfabulator Widget", + "name": "main_window", + "width": 500, + "height": 500 + }, + "image": { + "src": "Images/Sun.png", + "hOffset": 250, + "vOffset": 250, + "alignment": "center" + }, + "text": { + "data": "Click Here", + "size": 36, + "style": "bold", + "vOffset": 100, + "alignment": "center", + "onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;" + } + } +} +``` + +Each operation was rotated through one of the following search paths: + +``` +widget.window.name +widget.image.hOffset +widget.text.onMouseUp +``` + +*These benchmarks were run on a MacBook Pro 16" 2.4 GHz Intel Core i9 using Go 1.17 and can be found [here](https://github.com/tidwall/gjson-benchmarks).* diff --git a/vendor/github.com/tidwall/gjson/SYNTAX.md b/vendor/github.com/tidwall/gjson/SYNTAX.md new file mode 100644 index 00000000..7a9b6a2d --- /dev/null +++ b/vendor/github.com/tidwall/gjson/SYNTAX.md @@ -0,0 +1,342 @@ +# GJSON Path Syntax + +A GJSON Path is a text string syntax that describes a search pattern for quickly retreiving values from a JSON payload. + +This document is designed to explain the structure of a GJSON Path through examples. + +- [Path structure](#path-structure) +- [Basic](#basic) +- [Wildcards](#wildcards) +- [Escape Character](#escape-character) +- [Arrays](#arrays) +- [Queries](#queries) +- [Dot vs Pipe](#dot-vs-pipe) +- [Modifiers](#modifiers) +- [Multipaths](#multipaths) +- [Literals](#literals) + +The definitive implemenation is [github.com/tidwall/gjson](https://github.com/tidwall/gjson). +Use the [GJSON Playground](https://gjson.dev) to experiment with the syntax online. + +## Path structure + +A GJSON Path is intended to be easily expressed as a series of components seperated by a `.` character. + +Along with `.` character, there are a few more that have special meaning, including `|`, `#`, `@`, `\`, `*`, `!`, and `?`. + +## Example + +Given this JSON + +```json +{ + "name": {"first": "Tom", "last": "Anderson"}, + "age":37, + "children": ["Sara","Alex","Jack"], + "fav.movie": "Deer Hunter", + "friends": [ + {"first": "Dale", "last": "Murphy", "age": 44, "nets": ["ig", "fb", "tw"]}, + {"first": "Roger", "last": "Craig", "age": 68, "nets": ["fb", "tw"]}, + {"first": "Jane", "last": "Murphy", "age": 47, "nets": ["ig", "tw"]} + ] +} +``` + +The following GJSON Paths evaluate to the accompanying values. + +### Basic + +In many cases you'll just want to retreive values by object name or array index. + +```go +name.last "Anderson" +name.first "Tom" +age 37 +children ["Sara","Alex","Jack"] +children.0 "Sara" +children.1 "Alex" +friends.1 {"first": "Roger", "last": "Craig", "age": 68} +friends.1.first "Roger" +``` + +### Wildcards + +A key may contain the special wildcard characters `*` and `?`. +The `*` will match on any zero+ characters, and `?` matches on any one character. + +```go +child*.2 "Jack" +c?ildren.0 "Sara" +``` + +### Escape character + +Special purpose characters, such as `.`, `*`, and `?` can be escaped with `\`. + +```go +fav\.movie "Deer Hunter" +``` + +You'll also need to make sure that the `\` character is correctly escaped when hardcoding a path in your source code. + +```go +// Go +val := gjson.Get(json, "fav\\.movie") // must escape the slash +val := gjson.Get(json, `fav\.movie`) // no need to escape the slash +``` + +```rust +// Rust +let val = gjson::get(json, "fav\\.movie") // must escape the slash +let val = gjson::get(json, r#"fav\.movie"#) // no need to escape the slash +``` + + +### Arrays + +The `#` character allows for digging into JSON Arrays. + +To get the length of an array you'll just use the `#` all by itself. + +```go +friends.# 3 +friends.#.age [44,68,47] +``` + +### Queries + +You can also query an array for the first match by using `#(...)`, or find all matches with `#(...)#`. +Queries support the `==`, `!=`, `<`, `<=`, `>`, `>=` comparison operators, +and the simple pattern matching `%` (like) and `!%` (not like) operators. + +```go +friends.#(last=="Murphy").first "Dale" +friends.#(last=="Murphy")#.first ["Dale","Jane"] +friends.#(age>45)#.last ["Craig","Murphy"] +friends.#(first%"D*").last "Murphy" +friends.#(first!%"D*").last "Craig" +``` + +To query for a non-object value in an array, you can forgo the string to the right of the operator. + +```go +children.#(!%"*a*") "Alex" +children.#(%"*a*")# ["Sara","Jack"] +``` + +Nested queries are allowed. + +```go +friends.#(nets.#(=="fb"))#.first >> ["Dale","Roger"] +``` + +*Please note that prior to v1.3.0, queries used the `#[...]` brackets. This was +changed in v1.3.0 as to avoid confusion with the new [multipath](#multipaths) +syntax. For backwards compatibility, `#[...]` will continue to work until the +next major release.* + +The `~` (tilde) operator will convert a value to a boolean before comparison. + +For example, using the following JSON: + +```json +{ + "vals": [ + { "a": 1, "b": true }, + { "a": 2, "b": true }, + { "a": 3, "b": false }, + { "a": 4, "b": "0" }, + { "a": 5, "b": 0 }, + { "a": 6, "b": "1" }, + { "a": 7, "b": 1 }, + { "a": 8, "b": "true" }, + { "a": 9, "b": false }, + { "a": 10, "b": null }, + { "a": 11 } + ] +} +``` + +You can now query for all true(ish) or false(ish) values: + +``` +vals.#(b==~true)#.a >> [1,2,6,7,8] +vals.#(b==~false)#.a >> [3,4,5,9,10,11] +``` + +The last value which was non-existent is treated as `false` + +### Dot vs Pipe + +The `.` is standard separator, but it's also possible to use a `|`. +In most cases they both end up returning the same results. +The cases where`|` differs from `.` is when it's used after the `#` for [Arrays](#arrays) and [Queries](#queries). + +Here are some examples + +```go +friends.0.first "Dale" +friends|0.first "Dale" +friends.0|first "Dale" +friends|0|first "Dale" +friends|# 3 +friends.# 3 +friends.#(last="Murphy")# [{"first": "Dale", "last": "Murphy", "age": 44},{"first": "Jane", "last": "Murphy", "age": 47}] +friends.#(last="Murphy")#.first ["Dale","Jane"] +friends.#(last="Murphy")#|first <non-existent> +friends.#(last="Murphy")#.0 [] +friends.#(last="Murphy")#|0 {"first": "Dale", "last": "Murphy", "age": 44} +friends.#(last="Murphy")#.# [] +friends.#(last="Murphy")#|# 2 +``` + +Let's break down a few of these. + +The path `friends.#(last="Murphy")#` all by itself results in + +```json +[{"first": "Dale", "last": "Murphy", "age": 44},{"first": "Jane", "last": "Murphy", "age": 47}] +``` + +The `.first` suffix will process the `first` path on each array element *before* returning the results. Which becomes + +```json +["Dale","Jane"] +``` + +But the `|first` suffix actually processes the `first` path *after* the previous result. +Since the previous result is an array, not an object, it's not possible to process +because `first` does not exist. + +Yet, `|0` suffix returns + +```json +{"first": "Dale", "last": "Murphy", "age": 44} +``` + +Because `0` is the first index of the previous result. + +### Modifiers + +A modifier is a path component that performs custom processing on the JSON. + +For example, using the built-in `@reverse` modifier on the above JSON payload will reverse the `children` array: + +```go +children.@reverse ["Jack","Alex","Sara"] +children.@reverse.0 "Jack" +``` + +There are currently the following built-in modifiers: + +- `@reverse`: Reverse an array or the members of an object. +- `@ugly`: Remove all whitespace from JSON. +- `@pretty`: Make the JSON more human readable. +- `@this`: Returns the current element. It can be used to retrieve the root element. +- `@valid`: Ensure the json document is valid. +- `@flatten`: Flattens an array. +- `@join`: Joins multiple objects into a single object. +- `@keys`: Returns an array of keys for an object. +- `@values`: Returns an array of values for an object. +- `@tostr`: Converts json to a string. Wraps a json string. +- `@fromstr`: Converts a string from json. Unwraps a json string. +- `@group`: Groups arrays of objects. See [e4fc67c](https://github.com/tidwall/gjson/commit/e4fc67c92aeebf2089fabc7872f010e340d105db). + +#### Modifier arguments + +A modifier may accept an optional argument. The argument can be a valid JSON payload or just characters. + +For example, the `@pretty` modifier takes a json object as its argument. + +``` +@pretty:{"sortKeys":true} +``` + +Which makes the json pretty and orders all of its keys. + +```json +{ + "age":37, + "children": ["Sara","Alex","Jack"], + "fav.movie": "Deer Hunter", + "friends": [ + {"age": 44, "first": "Dale", "last": "Murphy"}, + {"age": 68, "first": "Roger", "last": "Craig"}, + {"age": 47, "first": "Jane", "last": "Murphy"} + ], + "name": {"first": "Tom", "last": "Anderson"} +} +``` + +*The full list of `@pretty` options are `sortKeys`, `indent`, `prefix`, and `width`. +Please see [Pretty Options](https://github.com/tidwall/pretty#customized-output) for more information.* + +#### Custom modifiers + +You can also add custom modifiers. + +For example, here we create a modifier which makes the entire JSON payload upper or lower case. + +```go +gjson.AddModifier("case", func(json, arg string) string { + if arg == "upper" { + return strings.ToUpper(json) + } + if arg == "lower" { + return strings.ToLower(json) + } + return json +}) +"children.@case:upper" ["SARA","ALEX","JACK"] +"children.@case:lower.@reverse" ["jack","alex","sara"] +``` + +*Note: Custom modifiers are not yet available in the Rust version* + +### Multipaths + +Starting with v1.3.0, GJSON added the ability to join multiple paths together +to form new documents. Wrapping comma-separated paths between `[...]` or +`{...}` will result in a new array or object, respectively. + +For example, using the given multipath: + +``` +{name.first,age,"the_murphys":friends.#(last="Murphy")#.first} +``` + +Here we selected the first name, age, and the first name for friends with the +last name "Murphy". + +You'll notice that an optional key can be provided, in this case +"the_murphys", to force assign a key to a value. Otherwise, the name of the +actual field will be used, in this case "first". If a name cannot be +determined, then "_" is used. + +This results in + +```json +{"first":"Tom","age":37,"the_murphys":["Dale","Jane"]} +``` + +### Literals + +Starting with v1.12.0, GJSON added support of json literals, which provides a way for constructing static blocks of json. This is can be particularly useful when constructing a new json document using [multipaths](#multipaths). + +A json literal begins with the '!' declaration character. + +For example, using the given multipath: + +``` +{name.first,age,"company":!"Happysoft","employed":!true} +``` + +Here we selected the first name and age. Then add two new fields, "company" and "employed". + +This results in + +```json +{"first":"Tom","age":37,"company":"Happysoft","employed":true} +``` + +*See issue [#249](https://github.com/tidwall/gjson/issues/249) for additional context on JSON Literals.* diff --git a/vendor/github.com/tidwall/gjson/gjson.go b/vendor/github.com/tidwall/gjson/gjson.go new file mode 100644 index 00000000..53cbd236 --- /dev/null +++ b/vendor/github.com/tidwall/gjson/gjson.go @@ -0,0 +1,3359 @@ +// Package gjson provides searching for json strings. +package gjson + +import ( + "strconv" + "strings" + "time" + "unicode/utf16" + "unicode/utf8" + "unsafe" + + "github.com/tidwall/match" + "github.com/tidwall/pretty" +) + +// Type is Result type +type Type int + +const ( + // Null is a null json value + Null Type = iota + // False is a json false boolean + False + // Number is json number + Number + // String is a json string + String + // True is a json true boolean + True + // JSON is a raw block of JSON + JSON +) + +// String returns a string representation of the type. +func (t Type) String() string { + switch t { + default: + return "" + case Null: + return "Null" + case False: + return "False" + case Number: + return "Number" + case String: + return "String" + case True: + return "True" + case JSON: + return "JSON" + } +} + +// Result represents a json value that is returned from Get(). +type Result struct { + // Type is the json type + Type Type + // Raw is the raw json + Raw string + // Str is the json string + Str string + // Num is the json number + Num float64 + // Index of raw value in original json, zero means index unknown + Index int + // Indexes of all the elements that match on a path containing the '#' + // query character. + Indexes []int +} + +// String returns a string representation of the value. +func (t Result) String() string { + switch t.Type { + default: + return "" + case False: + return "false" + case Number: + if len(t.Raw) == 0 { + // calculated result + return strconv.FormatFloat(t.Num, 'f', -1, 64) + } + var i int + if t.Raw[0] == '-' { + i++ + } + for ; i < len(t.Raw); i++ { + if t.Raw[i] < '0' || t.Raw[i] > '9' { + return strconv.FormatFloat(t.Num, 'f', -1, 64) + } + } + return t.Raw + case String: + return t.Str + case JSON: + return t.Raw + case True: + return "true" + } +} + +// Bool returns an boolean representation. +func (t Result) Bool() bool { + switch t.Type { + default: + return false + case True: + return true + case String: + b, _ := strconv.ParseBool(strings.ToLower(t.Str)) + return b + case Number: + return t.Num != 0 + } +} + +// Int returns an integer representation. +func (t Result) Int() int64 { + switch t.Type { + default: + return 0 + case True: + return 1 + case String: + n, _ := parseInt(t.Str) + return n + case Number: + // try to directly convert the float64 to int64 + i, ok := safeInt(t.Num) + if ok { + return i + } + // now try to parse the raw string + i, ok = parseInt(t.Raw) + if ok { + return i + } + // fallback to a standard conversion + return int64(t.Num) + } +} + +// Uint returns an unsigned integer representation. +func (t Result) Uint() uint64 { + switch t.Type { + default: + return 0 + case True: + return 1 + case String: + n, _ := parseUint(t.Str) + return n + case Number: + // try to directly convert the float64 to uint64 + i, ok := safeInt(t.Num) + if ok && i >= 0 { + return uint64(i) + } + // now try to parse the raw string + u, ok := parseUint(t.Raw) + if ok { + return u + } + // fallback to a standard conversion + return uint64(t.Num) + } +} + +// Float returns an float64 representation. +func (t Result) Float() float64 { + switch t.Type { + default: + return 0 + case True: + return 1 + case String: + n, _ := strconv.ParseFloat(t.Str, 64) + return n + case Number: + return t.Num + } +} + +// Time returns a time.Time representation. +func (t Result) Time() time.Time { + res, _ := time.Parse(time.RFC3339, t.String()) + return res +} + +// Array returns back an array of values. +// If the result represents a null value or is non-existent, then an empty +// array will be returned. +// If the result is not a JSON array, the return value will be an +// array containing one result. +func (t Result) Array() []Result { + if t.Type == Null { + return []Result{} + } + if !t.IsArray() { + return []Result{t} + } + r := t.arrayOrMap('[', false) + return r.a +} + +// IsObject returns true if the result value is a JSON object. +func (t Result) IsObject() bool { + return t.Type == JSON && len(t.Raw) > 0 && t.Raw[0] == '{' +} + +// IsArray returns true if the result value is a JSON array. +func (t Result) IsArray() bool { + return t.Type == JSON && len(t.Raw) > 0 && t.Raw[0] == '[' +} + +// IsBool returns true if the result value is a JSON boolean. +func (t Result) IsBool() bool { + return t.Type == True || t.Type == False +} + +// ForEach iterates through values. +// If the result represents a non-existent value, then no values will be +// iterated. If the result is an Object, the iterator will pass the key and +// value of each item. If the result is an Array, the iterator will only pass +// the value of each item. If the result is not a JSON array or object, the +// iterator will pass back one value equal to the result. +func (t Result) ForEach(iterator func(key, value Result) bool) { + if !t.Exists() { + return + } + if t.Type != JSON { + iterator(Result{}, t) + return + } + json := t.Raw + var obj bool + var i int + var key, value Result + for ; i < len(json); i++ { + if json[i] == '{' { + i++ + key.Type = String + obj = true + break + } else if json[i] == '[' { + i++ + key.Type = Number + key.Num = -1 + break + } + if json[i] > ' ' { + return + } + } + var str string + var vesc bool + var ok bool + var idx int + for ; i < len(json); i++ { + if obj { + if json[i] != '"' { + continue + } + s := i + i, str, vesc, ok = parseString(json, i+1) + if !ok { + return + } + if vesc { + key.Str = unescape(str[1 : len(str)-1]) + } else { + key.Str = str[1 : len(str)-1] + } + key.Raw = str + key.Index = s + t.Index + } else { + key.Num += 1 + } + for ; i < len(json); i++ { + if json[i] <= ' ' || json[i] == ',' || json[i] == ':' { + continue + } + break + } + s := i + i, value, ok = parseAny(json, i, true) + if !ok { + return + } + if t.Indexes != nil { + if idx < len(t.Indexes) { + value.Index = t.Indexes[idx] + } + } else { + value.Index = s + t.Index + } + if !iterator(key, value) { + return + } + idx++ + } +} + +// Map returns back a map of values. The result should be a JSON object. +// If the result is not a JSON object, the return value will be an empty map. +func (t Result) Map() map[string]Result { + if t.Type != JSON { + return map[string]Result{} + } + r := t.arrayOrMap('{', false) + return r.o +} + +// Get searches result for the specified path. +// The result should be a JSON array or object. +func (t Result) Get(path string) Result { + r := Get(t.Raw, path) + if r.Indexes != nil { + for i := 0; i < len(r.Indexes); i++ { + r.Indexes[i] += t.Index + } + } else { + r.Index += t.Index + } + return r +} + +type arrayOrMapResult struct { + a []Result + ai []interface{} + o map[string]Result + oi map[string]interface{} + vc byte +} + +func (t Result) arrayOrMap(vc byte, valueize bool) (r arrayOrMapResult) { + var json = t.Raw + var i int + var value Result + var count int + var key Result + if vc == 0 { + for ; i < len(json); i++ { + if json[i] == '{' || json[i] == '[' { + r.vc = json[i] + i++ + break + } + if json[i] > ' ' { + goto end + } + } + } else { + for ; i < len(json); i++ { + if json[i] == vc { + i++ + break + } + if json[i] > ' ' { + goto end + } + } + r.vc = vc + } + if r.vc == '{' { + if valueize { + r.oi = make(map[string]interface{}) + } else { + r.o = make(map[string]Result) + } + } else { + if valueize { + r.ai = make([]interface{}, 0) + } else { + r.a = make([]Result, 0) + } + } + for ; i < len(json); i++ { + if json[i] <= ' ' { + continue + } + // get next value + if json[i] == ']' || json[i] == '}' { + break + } + switch json[i] { + default: + if (json[i] >= '0' && json[i] <= '9') || json[i] == '-' { + value.Type = Number + value.Raw, value.Num = tonum(json[i:]) + value.Str = "" + } else { + continue + } + case '{', '[': + value.Type = JSON + value.Raw = squash(json[i:]) + value.Str, value.Num = "", 0 + case 'n': + value.Type = Null + value.Raw = tolit(json[i:]) + value.Str, value.Num = "", 0 + case 't': + value.Type = True + value.Raw = tolit(json[i:]) + value.Str, value.Num = "", 0 + case 'f': + value.Type = False + value.Raw = tolit(json[i:]) + value.Str, value.Num = "", 0 + case '"': + value.Type = String + value.Raw, value.Str = tostr(json[i:]) + value.Num = 0 + } + value.Index = i + t.Index + + i += len(value.Raw) - 1 + + if r.vc == '{' { + if count%2 == 0 { + key = value + } else { + if valueize { + if _, ok := r.oi[key.Str]; !ok { + r.oi[key.Str] = value.Value() + } + } else { + if _, ok := r.o[key.Str]; !ok { + r.o[key.Str] = value + } + } + } + count++ + } else { + if valueize { + r.ai = append(r.ai, value.Value()) + } else { + r.a = append(r.a, value) + } + } + } +end: + if t.Indexes != nil { + if len(t.Indexes) != len(r.a) { + for i := 0; i < len(r.a); i++ { + r.a[i].Index = 0 + } + } else { + for i := 0; i < len(r.a); i++ { + r.a[i].Index = t.Indexes[i] + } + } + } + return +} + +// Parse parses the json and returns a result. +// +// This function expects that the json is well-formed, and does not validate. +// Invalid json will not panic, but it may return back unexpected results. +// If you are consuming JSON from an unpredictable source then you may want to +// use the Valid function first. +func Parse(json string) Result { + var value Result + i := 0 + for ; i < len(json); i++ { + if json[i] == '{' || json[i] == '[' { + value.Type = JSON + value.Raw = json[i:] // just take the entire raw + break + } + if json[i] <= ' ' { + continue + } + switch json[i] { + case '+', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'i', 'I', 'N': + value.Type = Number + value.Raw, value.Num = tonum(json[i:]) + case 'n': + if i+1 < len(json) && json[i+1] != 'u' { + // nan + value.Type = Number + value.Raw, value.Num = tonum(json[i:]) + } else { + // null + value.Type = Null + value.Raw = tolit(json[i:]) + } + case 't': + value.Type = True + value.Raw = tolit(json[i:]) + case 'f': + value.Type = False + value.Raw = tolit(json[i:]) + case '"': + value.Type = String + value.Raw, value.Str = tostr(json[i:]) + default: + return Result{} + } + break + } + if value.Exists() { + value.Index = i + } + return value +} + +// ParseBytes parses the json and returns a result. +// If working with bytes, this method preferred over Parse(string(data)) +func ParseBytes(json []byte) Result { + return Parse(string(json)) +} + +func squash(json string) string { + // expects that the lead character is a '[' or '{' or '(' or '"' + // squash the value, ignoring all nested arrays and objects. + var i, depth int + if json[0] != '"' { + i, depth = 1, 1 + } + for ; i < len(json); i++ { + if json[i] >= '"' && json[i] <= '}' { + switch json[i] { + case '"': + i++ + s2 := i + for ; i < len(json); i++ { + if json[i] > '\\' { + continue + } + if json[i] == '"' { + // look for an escaped slash + if json[i-1] == '\\' { + n := 0 + for j := i - 2; j > s2-1; j-- { + if json[j] != '\\' { + break + } + n++ + } + if n%2 == 0 { + continue + } + } + break + } + } + if depth == 0 { + if i >= len(json) { + return json + } + return json[:i+1] + } + case '{', '[', '(': + depth++ + case '}', ']', ')': + depth-- + if depth == 0 { + return json[:i+1] + } + } + } + } + return json +} + +func tonum(json string) (raw string, num float64) { + for i := 1; i < len(json); i++ { + // less than dash might have valid characters + if json[i] <= '-' { + if json[i] <= ' ' || json[i] == ',' { + // break on whitespace and comma + raw = json[:i] + num, _ = strconv.ParseFloat(raw, 64) + return + } + // could be a '+' or '-'. let's assume so. + } else if json[i] == ']' || json[i] == '}' { + // break on ']' or '}' + raw = json[:i] + num, _ = strconv.ParseFloat(raw, 64) + return + } + } + raw = json + num, _ = strconv.ParseFloat(raw, 64) + return +} + +func tolit(json string) (raw string) { + for i := 1; i < len(json); i++ { + if json[i] < 'a' || json[i] > 'z' { + return json[:i] + } + } + return json +} + +func tostr(json string) (raw string, str string) { + // expects that the lead character is a '"' + for i := 1; i < len(json); i++ { + if json[i] > '\\' { + continue + } + if json[i] == '"' { + return json[:i+1], json[1:i] + } + if json[i] == '\\' { + i++ + for ; i < len(json); i++ { + if json[i] > '\\' { + continue + } + if json[i] == '"' { + // look for an escaped slash + if json[i-1] == '\\' { + n := 0 + for j := i - 2; j > 0; j-- { + if json[j] != '\\' { + break + } + n++ + } + if n%2 == 0 { + continue + } + } + return json[:i+1], unescape(json[1:i]) + } + } + var ret string + if i+1 < len(json) { + ret = json[:i+1] + } else { + ret = json[:i] + } + return ret, unescape(json[1:i]) + } + } + return json, json[1:] +} + +// Exists returns true if value exists. +// +// if gjson.Get(json, "name.last").Exists(){ +// println("value exists") +// } +func (t Result) Exists() bool { + return t.Type != Null || len(t.Raw) != 0 +} + +// Value returns one of these types: +// +// bool, for JSON booleans +// float64, for JSON numbers +// Number, for JSON numbers +// string, for JSON string literals +// nil, for JSON null +// map[string]interface{}, for JSON objects +// []interface{}, for JSON arrays +// +func (t Result) Value() interface{} { + if t.Type == String { + return t.Str + } + switch t.Type { + default: + return nil + case False: + return false + case Number: + return t.Num + case JSON: + r := t.arrayOrMap(0, true) + if r.vc == '{' { + return r.oi + } else if r.vc == '[' { + return r.ai + } + return nil + case True: + return true + } +} + +func parseString(json string, i int) (int, string, bool, bool) { + var s = i + for ; i < len(json); i++ { + if json[i] > '\\' { + continue + } + if json[i] == '"' { + return i + 1, json[s-1 : i+1], false, true + } + if json[i] == '\\' { + i++ + for ; i < len(json); i++ { + if json[i] > '\\' { + continue + } + if json[i] == '"' { + // look for an escaped slash + if json[i-1] == '\\' { + n := 0 + for j := i - 2; j > 0; j-- { + if json[j] != '\\' { + break + } + n++ + } + if n%2 == 0 { + continue + } + } + return i + 1, json[s-1 : i+1], true, true + } + } + break + } + } + return i, json[s-1:], false, false +} + +func parseNumber(json string, i int) (int, string) { + var s = i + i++ + for ; i < len(json); i++ { + if json[i] <= ' ' || json[i] == ',' || json[i] == ']' || + json[i] == '}' { + return i, json[s:i] + } + } + return i, json[s:] +} + +func parseLiteral(json string, i int) (int, string) { + var s = i + i++ + for ; i < len(json); i++ { + if json[i] < 'a' || json[i] > 'z' { + return i, json[s:i] + } + } + return i, json[s:] +} + +type arrayPathResult struct { + part string + path string + pipe string + piped bool + more bool + alogok bool + arrch bool + alogkey string + query struct { + on bool + all bool + path string + op string + value string + } +} + +func parseArrayPath(path string) (r arrayPathResult) { + for i := 0; i < len(path); i++ { + if path[i] == '|' { + r.part = path[:i] + r.pipe = path[i+1:] + r.piped = true + return + } + if path[i] == '.' { + r.part = path[:i] + if !r.arrch && i < len(path)-1 && isDotPiperChar(path[i+1:]) { + r.pipe = path[i+1:] + r.piped = true + } else { + r.path = path[i+1:] + r.more = true + } + return + } + if path[i] == '#' { + r.arrch = true + if i == 0 && len(path) > 1 { + if path[1] == '.' { + r.alogok = true + r.alogkey = path[2:] + r.path = path[:1] + } else if path[1] == '[' || path[1] == '(' { + // query + r.query.on = true + qpath, op, value, _, fi, vesc, ok := + parseQuery(path[i:]) + if !ok { + // bad query, end now + break + } + if len(value) >= 2 && value[0] == '"' && + value[len(value)-1] == '"' { + value = value[1 : len(value)-1] + if vesc { + value = unescape(value) + } + } + r.query.path = qpath + r.query.op = op + r.query.value = value + + i = fi - 1 + if i+1 < len(path) && path[i+1] == '#' { + r.query.all = true + } + } + } + continue + } + } + r.part = path + r.path = "" + return +} + +// splitQuery takes a query and splits it into three parts: +// path, op, middle, and right. +// So for this query: +// #(first_name=="Murphy").last +// Becomes +// first_name # path +// =="Murphy" # middle +// .last # right +// Or, +// #(service_roles.#(=="one")).cap +// Becomes +// service_roles.#(=="one") # path +// # middle +// .cap # right +func parseQuery(query string) ( + path, op, value, remain string, i int, vesc, ok bool, +) { + if len(query) < 2 || query[0] != '#' || + (query[1] != '(' && query[1] != '[') { + return "", "", "", "", i, false, false + } + i = 2 + j := 0 // start of value part + depth := 1 + for ; i < len(query); i++ { + if depth == 1 && j == 0 { + switch query[i] { + case '!', '=', '<', '>', '%': + // start of the value part + j = i + continue + } + } + if query[i] == '\\' { + i++ + } else if query[i] == '[' || query[i] == '(' { + depth++ + } else if query[i] == ']' || query[i] == ')' { + depth-- + if depth == 0 { + break + } + } else if query[i] == '"' { + // inside selector string, balance quotes + i++ + for ; i < len(query); i++ { + if query[i] == '\\' { + vesc = true + i++ + } else if query[i] == '"' { + break + } + } + } + } + if depth > 0 { + return "", "", "", "", i, false, false + } + if j > 0 { + path = trim(query[2:j]) + value = trim(query[j:i]) + remain = query[i+1:] + // parse the compare op from the value + var opsz int + switch { + case len(value) == 1: + opsz = 1 + case value[0] == '!' && value[1] == '=': + opsz = 2 + case value[0] == '!' && value[1] == '%': + opsz = 2 + case value[0] == '<' && value[1] == '=': + opsz = 2 + case value[0] == '>' && value[1] == '=': + opsz = 2 + case value[0] == '=' && value[1] == '=': + value = value[1:] + opsz = 1 + case value[0] == '<': + opsz = 1 + case value[0] == '>': + opsz = 1 + case value[0] == '=': + opsz = 1 + case value[0] == '%': + opsz = 1 + } + op = value[:opsz] + value = trim(value[opsz:]) + } else { + path = trim(query[2:i]) + remain = query[i+1:] + } + return path, op, value, remain, i + 1, vesc, true +} + +func trim(s string) string { +left: + if len(s) > 0 && s[0] <= ' ' { + s = s[1:] + goto left + } +right: + if len(s) > 0 && s[len(s)-1] <= ' ' { + s = s[:len(s)-1] + goto right + } + return s +} + +// peek at the next byte and see if it's a '@', '[', or '{'. +func isDotPiperChar(s string) bool { + if DisableModifiers { + return false + } + c := s[0] + if c == '@' { + // check that the next component is *not* a modifier. + i := 1 + for ; i < len(s); i++ { + if s[i] == '.' || s[i] == '|' || s[i] == ':' { + break + } + } + _, ok := modifiers[s[1:i]] + return ok + } + return c == '[' || c == '{' +} + +type objectPathResult struct { + part string + path string + pipe string + piped bool + wild bool + more bool +} + +func parseObjectPath(path string) (r objectPathResult) { + for i := 0; i < len(path); i++ { + if path[i] == '|' { + r.part = path[:i] + r.pipe = path[i+1:] + r.piped = true + return + } + if path[i] == '.' { + r.part = path[:i] + if i < len(path)-1 && isDotPiperChar(path[i+1:]) { + r.pipe = path[i+1:] + r.piped = true + } else { + r.path = path[i+1:] + r.more = true + } + return + } + if path[i] == '*' || path[i] == '?' { + r.wild = true + continue + } + if path[i] == '\\' { + // go into escape mode. this is a slower path that + // strips off the escape character from the part. + epart := []byte(path[:i]) + i++ + if i < len(path) { + epart = append(epart, path[i]) + i++ + for ; i < len(path); i++ { + if path[i] == '\\' { + i++ + if i < len(path) { + epart = append(epart, path[i]) + } + continue + } else if path[i] == '.' { + r.part = string(epart) + if i < len(path)-1 && isDotPiperChar(path[i+1:]) { + r.pipe = path[i+1:] + r.piped = true + } else { + r.path = path[i+1:] + r.more = true + } + return + } else if path[i] == '|' { + r.part = string(epart) + r.pipe = path[i+1:] + r.piped = true + return + } else if path[i] == '*' || path[i] == '?' { + r.wild = true + } + epart = append(epart, path[i]) + } + } + // append the last part + r.part = string(epart) + return + } + } + r.part = path + return +} + +func parseSquash(json string, i int) (int, string) { + // expects that the lead character is a '[' or '{' or '(' + // squash the value, ignoring all nested arrays and objects. + // the first '[' or '{' or '(' has already been read + s := i + i++ + depth := 1 + for ; i < len(json); i++ { + if json[i] >= '"' && json[i] <= '}' { + switch json[i] { + case '"': + i++ + s2 := i + for ; i < len(json); i++ { + if json[i] > '\\' { + continue + } + if json[i] == '"' { + // look for an escaped slash + if json[i-1] == '\\' { + n := 0 + for j := i - 2; j > s2-1; j-- { + if json[j] != '\\' { + break + } + n++ + } + if n%2 == 0 { + continue + } + } + break + } + } + case '{', '[', '(': + depth++ + case '}', ']', ')': + depth-- + if depth == 0 { + i++ + return i, json[s:i] + } + } + } + } + return i, json[s:] +} + +func parseObject(c *parseContext, i int, path string) (int, bool) { + var pmatch, kesc, vesc, ok, hit bool + var key, val string + rp := parseObjectPath(path) + if !rp.more && rp.piped { + c.pipe = rp.pipe + c.piped = true + } + for i < len(c.json) { + for ; i < len(c.json); i++ { + if c.json[i] == '"' { + // parse_key_string + // this is slightly different from getting s string value + // because we don't need the outer quotes. + i++ + var s = i + for ; i < len(c.json); i++ { + if c.json[i] > '\\' { + continue + } + if c.json[i] == '"' { + i, key, kesc, ok = i+1, c.json[s:i], false, true + goto parse_key_string_done + } + if c.json[i] == '\\' { + i++ + for ; i < len(c.json); i++ { + if c.json[i] > '\\' { + continue + } + if c.json[i] == '"' { + // look for an escaped slash + if c.json[i-1] == '\\' { + n := 0 + for j := i - 2; j > 0; j-- { + if c.json[j] != '\\' { + break + } + n++ + } + if n%2 == 0 { + continue + } + } + i, key, kesc, ok = i+1, c.json[s:i], true, true + goto parse_key_string_done + } + } + break + } + } + key, kesc, ok = c.json[s:], false, false + parse_key_string_done: + break + } + if c.json[i] == '}' { + return i + 1, false + } + } + if !ok { + return i, false + } + if rp.wild { + if kesc { + pmatch = matchLimit(unescape(key), rp.part) + } else { + pmatch = matchLimit(key, rp.part) + } + } else { + if kesc { + pmatch = rp.part == unescape(key) + } else { + pmatch = rp.part == key + } + } + hit = pmatch && !rp.more + for ; i < len(c.json); i++ { + var num bool + switch c.json[i] { + default: + continue + case '"': + i++ + i, val, vesc, ok = parseString(c.json, i) + if !ok { + return i, false + } + if hit { + if vesc { + c.value.Str = unescape(val[1 : len(val)-1]) + } else { + c.value.Str = val[1 : len(val)-1] + } + c.value.Raw = val + c.value.Type = String + return i, true + } + case '{': + if pmatch && !hit { + i, hit = parseObject(c, i+1, rp.path) + if hit { + return i, true + } + } else { + i, val = parseSquash(c.json, i) + if hit { + c.value.Raw = val + c.value.Type = JSON + return i, true + } + } + case '[': + if pmatch && !hit { + i, hit = parseArray(c, i+1, rp.path) + if hit { + return i, true + } + } else { + i, val = parseSquash(c.json, i) + if hit { + c.value.Raw = val + c.value.Type = JSON + return i, true + } + } + case 'n': + if i+1 < len(c.json) && c.json[i+1] != 'u' { + num = true + break + } + fallthrough + case 't', 'f': + vc := c.json[i] + i, val = parseLiteral(c.json, i) + if hit { + c.value.Raw = val + switch vc { + case 't': + c.value.Type = True + case 'f': + c.value.Type = False + } + return i, true + } + case '+', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'i', 'I', 'N': + num = true + } + if num { + i, val = parseNumber(c.json, i) + if hit { + c.value.Raw = val + c.value.Type = Number + c.value.Num, _ = strconv.ParseFloat(val, 64) + return i, true + } + } + break + } + } + return i, false +} + +// matchLimit will limit the complexity of the match operation to avoid ReDos +// attacks from arbritary inputs. +// See the github.com/tidwall/match.MatchLimit function for more information. +func matchLimit(str, pattern string) bool { + matched, _ := match.MatchLimit(str, pattern, 10000) + return matched +} + +func queryMatches(rp *arrayPathResult, value Result) bool { + rpv := rp.query.value + if len(rpv) > 0 && rpv[0] == '~' { + // convert to bool + rpv = rpv[1:] + if value.Bool() { + value = Result{Type: True} + } else { + value = Result{Type: False} + } + } + if !value.Exists() { + return false + } + if rp.query.op == "" { + // the query is only looking for existence, such as: + // friends.#(name) + // which makes sure that the array "friends" has an element of + // "name" that exists + return true + } + switch value.Type { + case String: + switch rp.query.op { + case "=": + return value.Str == rpv + case "!=": + return value.Str != rpv + case "<": + return value.Str < rpv + case "<=": + return value.Str <= rpv + case ">": + return value.Str > rpv + case ">=": + return value.Str >= rpv + case "%": + return matchLimit(value.Str, rpv) + case "!%": + return !matchLimit(value.Str, rpv) + } + case Number: + rpvn, _ := strconv.ParseFloat(rpv, 64) + switch rp.query.op { + case "=": + return value.Num == rpvn + case "!=": + return value.Num != rpvn + case "<": + return value.Num < rpvn + case "<=": + return value.Num <= rpvn + case ">": + return value.Num > rpvn + case ">=": + return value.Num >= rpvn + } + case True: + switch rp.query.op { + case "=": + return rpv == "true" + case "!=": + return rpv != "true" + case ">": + return rpv == "false" + case ">=": + return true + } + case False: + switch rp.query.op { + case "=": + return rpv == "false" + case "!=": + return rpv != "false" + case "<": + return rpv == "true" + case "<=": + return true + } + } + return false +} +func parseArray(c *parseContext, i int, path string) (int, bool) { + var pmatch, vesc, ok, hit bool + var val string + var h int + var alog []int + var partidx int + var multires []byte + var queryIndexes []int + rp := parseArrayPath(path) + if !rp.arrch { + n, ok := parseUint(rp.part) + if !ok { + partidx = -1 + } else { + partidx = int(n) + } + } + if !rp.more && rp.piped { + c.pipe = rp.pipe + c.piped = true + } + + procQuery := func(qval Result) bool { + if rp.query.all { + if len(multires) == 0 { + multires = append(multires, '[') + } + } + var tmp parseContext + tmp.value = qval + fillIndex(c.json, &tmp) + parentIndex := tmp.value.Index + var res Result + if qval.Type == JSON { + res = qval.Get(rp.query.path) + } else { + if rp.query.path != "" { + return false + } + res = qval + } + if queryMatches(&rp, res) { + if rp.more { + left, right, ok := splitPossiblePipe(rp.path) + if ok { + rp.path = left + c.pipe = right + c.piped = true + } + res = qval.Get(rp.path) + } else { + res = qval + } + if rp.query.all { + raw := res.Raw + if len(raw) == 0 { + raw = res.String() + } + if raw != "" { + if len(multires) > 1 { + multires = append(multires, ',') + } + multires = append(multires, raw...) + queryIndexes = append(queryIndexes, res.Index+parentIndex) + } + } else { + c.value = res + return true + } + } + return false + } + for i < len(c.json)+1 { + if !rp.arrch { + pmatch = partidx == h + hit = pmatch && !rp.more + } + h++ + if rp.alogok { + alog = append(alog, i) + } + for ; ; i++ { + var ch byte + if i > len(c.json) { + break + } else if i == len(c.json) { + ch = ']' + } else { + ch = c.json[i] + } + var num bool + switch ch { + default: + continue + case '"': + i++ + i, val, vesc, ok = parseString(c.json, i) + if !ok { + return i, false + } + if rp.query.on { + var qval Result + if vesc { + qval.Str = unescape(val[1 : len(val)-1]) + } else { + qval.Str = val[1 : len(val)-1] + } + qval.Raw = val + qval.Type = String + if procQuery(qval) { + return i, true + } + } else if hit { + if rp.alogok { + break + } + if vesc { + c.value.Str = unescape(val[1 : len(val)-1]) + } else { + c.value.Str = val[1 : len(val)-1] + } + c.value.Raw = val + c.value.Type = String + return i, true + } + case '{': + if pmatch && !hit { + i, hit = parseObject(c, i+1, rp.path) + if hit { + if rp.alogok { + break + } + return i, true + } + } else { + i, val = parseSquash(c.json, i) + if rp.query.on { + if procQuery(Result{Raw: val, Type: JSON}) { + return i, true + } + } else if hit { + if rp.alogok { + break + } + c.value.Raw = val + c.value.Type = JSON + return i, true + } + } + case '[': + if pmatch && !hit { + i, hit = parseArray(c, i+1, rp.path) + if hit { + if rp.alogok { + break + } + return i, true + } + } else { + i, val = parseSquash(c.json, i) + if rp.query.on { + if procQuery(Result{Raw: val, Type: JSON}) { + return i, true + } + } else if hit { + if rp.alogok { + break + } + c.value.Raw = val + c.value.Type = JSON + return i, true + } + } + case 'n': + if i+1 < len(c.json) && c.json[i+1] != 'u' { + num = true + break + } + fallthrough + case 't', 'f': + vc := c.json[i] + i, val = parseLiteral(c.json, i) + if rp.query.on { + var qval Result + qval.Raw = val + switch vc { + case 't': + qval.Type = True + case 'f': + qval.Type = False + } + if procQuery(qval) { + return i, true + } + } else if hit { + if rp.alogok { + break + } + c.value.Raw = val + switch vc { + case 't': + c.value.Type = True + case 'f': + c.value.Type = False + } + return i, true + } + case '+', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'i', 'I', 'N': + num = true + case ']': + if rp.arrch && rp.part == "#" { + if rp.alogok { + left, right, ok := splitPossiblePipe(rp.alogkey) + if ok { + rp.alogkey = left + c.pipe = right + c.piped = true + } + var indexes = make([]int, 0, 64) + var jsons = make([]byte, 0, 64) + jsons = append(jsons, '[') + for j, k := 0, 0; j < len(alog); j++ { + idx := alog[j] + for idx < len(c.json) { + switch c.json[idx] { + case ' ', '\t', '\r', '\n': + idx++ + continue + } + break + } + if idx < len(c.json) && c.json[idx] != ']' { + _, res, ok := parseAny(c.json, idx, true) + if ok { + res := res.Get(rp.alogkey) + if res.Exists() { + if k > 0 { + jsons = append(jsons, ',') + } + raw := res.Raw + if len(raw) == 0 { + raw = res.String() + } + jsons = append(jsons, []byte(raw)...) + indexes = append(indexes, res.Index) + k++ + } + } + } + } + jsons = append(jsons, ']') + c.value.Type = JSON + c.value.Raw = string(jsons) + c.value.Indexes = indexes + return i + 1, true + } + if rp.alogok { + break + } + + c.value.Type = Number + c.value.Num = float64(h - 1) + c.value.Raw = strconv.Itoa(h - 1) + c.calcd = true + return i + 1, true + } + if !c.value.Exists() { + if len(multires) > 0 { + c.value = Result{ + Raw: string(append(multires, ']')), + Type: JSON, + Indexes: queryIndexes, + } + } else if rp.query.all { + c.value = Result{ + Raw: "[]", + Type: JSON, + } + } + } + return i + 1, false + } + if num { + i, val = parseNumber(c.json, i) + if rp.query.on { + var qval Result + qval.Raw = val + qval.Type = Number + qval.Num, _ = strconv.ParseFloat(val, 64) + if procQuery(qval) { + return i, true + } + } else if hit { + if rp.alogok { + break + } + c.value.Raw = val + c.value.Type = Number + c.value.Num, _ = strconv.ParseFloat(val, 64) + return i, true + } + } + break + } + } + return i, false +} + +func splitPossiblePipe(path string) (left, right string, ok bool) { + // take a quick peek for the pipe character. If found we'll split the piped + // part of the path into the c.pipe field and shorten the rp. + var possible bool + for i := 0; i < len(path); i++ { + if path[i] == '|' { + possible = true + break + } + } + if !possible { + return + } + + if len(path) > 0 && path[0] == '{' { + squashed := squash(path[1:]) + if len(squashed) < len(path)-1 { + squashed = path[:len(squashed)+1] + remain := path[len(squashed):] + if remain[0] == '|' { + return squashed, remain[1:], true + } + } + return + } + + // split the left and right side of the path with the pipe character as + // the delimiter. This is a little tricky because we'll need to basically + // parse the entire path. + for i := 0; i < len(path); i++ { + if path[i] == '\\' { + i++ + } else if path[i] == '.' { + if i == len(path)-1 { + return + } + if path[i+1] == '#' { + i += 2 + if i == len(path) { + return + } + if path[i] == '[' || path[i] == '(' { + var start, end byte + if path[i] == '[' { + start, end = '[', ']' + } else { + start, end = '(', ')' + } + // inside selector, balance brackets + i++ + depth := 1 + for ; i < len(path); i++ { + if path[i] == '\\' { + i++ + } else if path[i] == start { + depth++ + } else if path[i] == end { + depth-- + if depth == 0 { + break + } + } else if path[i] == '"' { + // inside selector string, balance quotes + i++ + for ; i < len(path); i++ { + if path[i] == '\\' { + i++ + } else if path[i] == '"' { + break + } + } + } + } + } + } + } else if path[i] == '|' { + return path[:i], path[i+1:], true + } + } + return +} + +// ForEachLine iterates through lines of JSON as specified by the JSON Lines +// format (http://jsonlines.org/). +// Each line is returned as a GJSON Result. +func ForEachLine(json string, iterator func(line Result) bool) { + var res Result + var i int + for { + i, res, _ = parseAny(json, i, true) + if !res.Exists() { + break + } + if !iterator(res) { + return + } + } +} + +type subSelector struct { + name string + path string +} + +// parseSubSelectors returns the subselectors belonging to a '[path1,path2]' or +// '{"field1":path1,"field2":path2}' type subSelection. It's expected that the +// first character in path is either '[' or '{', and has already been checked +// prior to calling this function. +func parseSubSelectors(path string) (sels []subSelector, out string, ok bool) { + modifier := 0 + depth := 1 + colon := 0 + start := 1 + i := 1 + pushSel := func() { + var sel subSelector + if colon == 0 { + sel.path = path[start:i] + } else { + sel.name = path[start:colon] + sel.path = path[colon+1 : i] + } + sels = append(sels, sel) + colon = 0 + modifier = 0 + start = i + 1 + } + for ; i < len(path); i++ { + switch path[i] { + case '\\': + i++ + case '@': + if modifier == 0 && i > 0 && (path[i-1] == '.' || path[i-1] == '|') { + modifier = i + } + case ':': + if modifier == 0 && colon == 0 && depth == 1 { + colon = i + } + case ',': + if depth == 1 { + pushSel() + } + case '"': + i++ + loop: + for ; i < len(path); i++ { + switch path[i] { + case '\\': + i++ + case '"': + break loop + } + } + case '[', '(', '{': + depth++ + case ']', ')', '}': + depth-- + if depth == 0 { + pushSel() + path = path[i+1:] + return sels, path, true + } + } + } + return +} + +// nameOfLast returns the name of the last component +func nameOfLast(path string) string { + for i := len(path) - 1; i >= 0; i-- { + if path[i] == '|' || path[i] == '.' { + if i > 0 { + if path[i-1] == '\\' { + continue + } + } + return path[i+1:] + } + } + return path +} + +func isSimpleName(component string) bool { + for i := 0; i < len(component); i++ { + if component[i] < ' ' { + return false + } + switch component[i] { + case '[', ']', '{', '}', '(', ')', '#', '|', '!': + return false + } + } + return true +} + +var hexchars = [...]byte{ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'a', 'b', 'c', 'd', 'e', 'f', +} + +func appendHex16(dst []byte, x uint16) []byte { + return append(dst, + hexchars[x>>12&0xF], hexchars[x>>8&0xF], + hexchars[x>>4&0xF], hexchars[x>>0&0xF], + ) +} + +// AppendJSONString is a convenience function that converts the provided string +// to a valid JSON string and appends it to dst. +func AppendJSONString(dst []byte, s string) []byte { + dst = append(dst, make([]byte, len(s)+2)...) + dst = append(dst[:len(dst)-len(s)-2], '"') + for i := 0; i < len(s); i++ { + if s[i] < ' ' { + dst = append(dst, '\\') + switch s[i] { + case '\n': + dst = append(dst, 'n') + case '\r': + dst = append(dst, 'r') + case '\t': + dst = append(dst, 't') + default: + dst = append(dst, 'u') + dst = appendHex16(dst, uint16(s[i])) + } + } else if s[i] == '>' || s[i] == '<' || s[i] == '&' { + dst = append(dst, '\\', 'u') + dst = appendHex16(dst, uint16(s[i])) + } else if s[i] == '\\' { + dst = append(dst, '\\', '\\') + } else if s[i] == '"' { + dst = append(dst, '\\', '"') + } else if s[i] > 127 { + // read utf8 character + r, n := utf8.DecodeRuneInString(s[i:]) + if n == 0 { + break + } + if r == utf8.RuneError && n == 1 { + dst = append(dst, `\ufffd`...) + } else if r == '\u2028' || r == '\u2029' { + dst = append(dst, `\u202`...) + dst = append(dst, hexchars[r&0xF]) + } else { + dst = append(dst, s[i:i+n]...) + } + i = i + n - 1 + } else { + dst = append(dst, s[i]) + } + } + return append(dst, '"') +} + +type parseContext struct { + json string + value Result + pipe string + piped bool + calcd bool + lines bool +} + +// Get searches json for the specified path. +// A path is in dot syntax, such as "name.last" or "age". +// When the value is found it's returned immediately. +// +// A path is a series of keys separated by a dot. +// A key may contain special wildcard characters '*' and '?'. +// To access an array value use the index as the key. +// To get the number of elements in an array or to access a child path, use +// the '#' character. +// The dot and wildcard character can be escaped with '\'. +// +// { +// "name": {"first": "Tom", "last": "Anderson"}, +// "age":37, +// "children": ["Sara","Alex","Jack"], +// "friends": [ +// {"first": "James", "last": "Murphy"}, +// {"first": "Roger", "last": "Craig"} +// ] +// } +// "name.last" >> "Anderson" +// "age" >> 37 +// "children" >> ["Sara","Alex","Jack"] +// "children.#" >> 3 +// "children.1" >> "Alex" +// "child*.2" >> "Jack" +// "c?ildren.0" >> "Sara" +// "friends.#.first" >> ["James","Roger"] +// +// This function expects that the json is well-formed, and does not validate. +// Invalid json will not panic, but it may return back unexpected results. +// If you are consuming JSON from an unpredictable source then you may want to +// use the Valid function first. +func Get(json, path string) Result { + if len(path) > 1 { + if (path[0] == '@' && !DisableModifiers) || path[0] == '!' { + // possible modifier + var ok bool + var npath string + var rjson string + if path[0] == '@' && !DisableModifiers { + npath, rjson, ok = execModifier(json, path) + } else if path[0] == '!' { + npath, rjson, ok = execStatic(json, path) + } + if ok { + path = npath + if len(path) > 0 && (path[0] == '|' || path[0] == '.') { + res := Get(rjson, path[1:]) + res.Index = 0 + res.Indexes = nil + return res + } + return Parse(rjson) + } + } + if path[0] == '[' || path[0] == '{' { + // using a subselector path + kind := path[0] + var ok bool + var subs []subSelector + subs, path, ok = parseSubSelectors(path) + if ok { + if len(path) == 0 || (path[0] == '|' || path[0] == '.') { + var b []byte + b = append(b, kind) + var i int + for _, sub := range subs { + res := Get(json, sub.path) + if res.Exists() { + if i > 0 { + b = append(b, ',') + } + if kind == '{' { + if len(sub.name) > 0 { + if sub.name[0] == '"' && Valid(sub.name) { + b = append(b, sub.name...) + } else { + b = AppendJSONString(b, sub.name) + } + } else { + last := nameOfLast(sub.path) + if isSimpleName(last) { + b = AppendJSONString(b, last) + } else { + b = AppendJSONString(b, "_") + } + } + b = append(b, ':') + } + var raw string + if len(res.Raw) == 0 { + raw = res.String() + if len(raw) == 0 { + raw = "null" + } + } else { + raw = res.Raw + } + b = append(b, raw...) + i++ + } + } + b = append(b, kind+2) + var res Result + res.Raw = string(b) + res.Type = JSON + if len(path) > 0 { + res = res.Get(path[1:]) + } + res.Index = 0 + return res + } + } + } + } + var i int + var c = &parseContext{json: json} + if len(path) >= 2 && path[0] == '.' && path[1] == '.' { + c.lines = true + parseArray(c, 0, path[2:]) + } else { + for ; i < len(c.json); i++ { + if c.json[i] == '{' { + i++ + parseObject(c, i, path) + break + } + if c.json[i] == '[' { + i++ + parseArray(c, i, path) + break + } + } + } + if c.piped { + res := c.value.Get(c.pipe) + res.Index = 0 + return res + } + fillIndex(json, c) + return c.value +} + +// GetBytes searches json for the specified path. +// If working with bytes, this method preferred over Get(string(data), path) +func GetBytes(json []byte, path string) Result { + return getBytes(json, path) +} + +// runeit returns the rune from the the \uXXXX +func runeit(json string) rune { + n, _ := strconv.ParseUint(json[:4], 16, 64) + return rune(n) +} + +// unescape unescapes a string +func unescape(json string) string { + var str = make([]byte, 0, len(json)) + for i := 0; i < len(json); i++ { + switch { + default: + str = append(str, json[i]) + case json[i] < ' ': + return string(str) + case json[i] == '\\': + i++ + if i >= len(json) { + return string(str) + } + switch json[i] { + default: + return string(str) + case '\\': + str = append(str, '\\') + case '/': + str = append(str, '/') + case 'b': + str = append(str, '\b') + case 'f': + str = append(str, '\f') + case 'n': + str = append(str, '\n') + case 'r': + str = append(str, '\r') + case 't': + str = append(str, '\t') + case '"': + str = append(str, '"') + case 'u': + if i+5 > len(json) { + return string(str) + } + r := runeit(json[i+1:]) + i += 5 + if utf16.IsSurrogate(r) { + // need another code + if len(json[i:]) >= 6 && json[i] == '\\' && + json[i+1] == 'u' { + // we expect it to be correct so just consume it + r = utf16.DecodeRune(r, runeit(json[i+2:])) + i += 6 + } + } + // provide enough space to encode the largest utf8 possible + str = append(str, 0, 0, 0, 0, 0, 0, 0, 0) + n := utf8.EncodeRune(str[len(str)-8:], r) + str = str[:len(str)-8+n] + i-- // backtrack index by one + } + } + } + return string(str) +} + +// Less return true if a token is less than another token. +// The caseSensitive paramater is used when the tokens are Strings. +// The order when comparing two different type is: +// +// Null < False < Number < String < True < JSON +// +func (t Result) Less(token Result, caseSensitive bool) bool { + if t.Type < token.Type { + return true + } + if t.Type > token.Type { + return false + } + if t.Type == String { + if caseSensitive { + return t.Str < token.Str + } + return stringLessInsensitive(t.Str, token.Str) + } + if t.Type == Number { + return t.Num < token.Num + } + return t.Raw < token.Raw +} + +func stringLessInsensitive(a, b string) bool { + for i := 0; i < len(a) && i < len(b); i++ { + if a[i] >= 'A' && a[i] <= 'Z' { + if b[i] >= 'A' && b[i] <= 'Z' { + // both are uppercase, do nothing + if a[i] < b[i] { + return true + } else if a[i] > b[i] { + return false + } + } else { + // a is uppercase, convert a to lowercase + if a[i]+32 < b[i] { + return true + } else if a[i]+32 > b[i] { + return false + } + } + } else if b[i] >= 'A' && b[i] <= 'Z' { + // b is uppercase, convert b to lowercase + if a[i] < b[i]+32 { + return true + } else if a[i] > b[i]+32 { + return false + } + } else { + // neither are uppercase + if a[i] < b[i] { + return true + } else if a[i] > b[i] { + return false + } + } + } + return len(a) < len(b) +} + +// parseAny parses the next value from a json string. +// A Result is returned when the hit param is set. +// The return values are (i int, res Result, ok bool) +func parseAny(json string, i int, hit bool) (int, Result, bool) { + var res Result + var val string + for ; i < len(json); i++ { + if json[i] == '{' || json[i] == '[' { + i, val = parseSquash(json, i) + if hit { + res.Raw = val + res.Type = JSON + } + var tmp parseContext + tmp.value = res + fillIndex(json, &tmp) + return i, tmp.value, true + } + if json[i] <= ' ' { + continue + } + var num bool + switch json[i] { + case '"': + i++ + var vesc bool + var ok bool + i, val, vesc, ok = parseString(json, i) + if !ok { + return i, res, false + } + if hit { + res.Type = String + res.Raw = val + if vesc { + res.Str = unescape(val[1 : len(val)-1]) + } else { + res.Str = val[1 : len(val)-1] + } + } + return i, res, true + case 'n': + if i+1 < len(json) && json[i+1] != 'u' { + num = true + break + } + fallthrough + case 't', 'f': + vc := json[i] + i, val = parseLiteral(json, i) + if hit { + res.Raw = val + switch vc { + case 't': + res.Type = True + case 'f': + res.Type = False + } + return i, res, true + } + case '+', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'i', 'I', 'N': + num = true + } + if num { + i, val = parseNumber(json, i) + if hit { + res.Raw = val + res.Type = Number + res.Num, _ = strconv.ParseFloat(val, 64) + } + return i, res, true + } + + } + return i, res, false +} + +// GetMany searches json for the multiple paths. +// The return value is a Result array where the number of items +// will be equal to the number of input paths. +func GetMany(json string, path ...string) []Result { + res := make([]Result, len(path)) + for i, path := range path { + res[i] = Get(json, path) + } + return res +} + +// GetManyBytes searches json for the multiple paths. +// The return value is a Result array where the number of items +// will be equal to the number of input paths. +func GetManyBytes(json []byte, path ...string) []Result { + res := make([]Result, len(path)) + for i, path := range path { + res[i] = GetBytes(json, path) + } + return res +} + +func validpayload(data []byte, i int) (outi int, ok bool) { + for ; i < len(data); i++ { + switch data[i] { + default: + i, ok = validany(data, i) + if !ok { + return i, false + } + for ; i < len(data); i++ { + switch data[i] { + default: + return i, false + case ' ', '\t', '\n', '\r': + continue + } + } + return i, true + case ' ', '\t', '\n', '\r': + continue + } + } + return i, false +} +func validany(data []byte, i int) (outi int, ok bool) { + for ; i < len(data); i++ { + switch data[i] { + default: + return i, false + case ' ', '\t', '\n', '\r': + continue + case '{': + return validobject(data, i+1) + case '[': + return validarray(data, i+1) + case '"': + return validstring(data, i+1) + case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + return validnumber(data, i+1) + case 't': + return validtrue(data, i+1) + case 'f': + return validfalse(data, i+1) + case 'n': + return validnull(data, i+1) + } + } + return i, false +} +func validobject(data []byte, i int) (outi int, ok bool) { + for ; i < len(data); i++ { + switch data[i] { + default: + return i, false + case ' ', '\t', '\n', '\r': + continue + case '}': + return i + 1, true + case '"': + key: + if i, ok = validstring(data, i+1); !ok { + return i, false + } + if i, ok = validcolon(data, i); !ok { + return i, false + } + if i, ok = validany(data, i); !ok { + return i, false + } + if i, ok = validcomma(data, i, '}'); !ok { + return i, false + } + if data[i] == '}' { + return i + 1, true + } + i++ + for ; i < len(data); i++ { + switch data[i] { + default: + return i, false + case ' ', '\t', '\n', '\r': + continue + case '"': + goto key + } + } + return i, false + } + } + return i, false +} +func validcolon(data []byte, i int) (outi int, ok bool) { + for ; i < len(data); i++ { + switch data[i] { + default: + return i, false + case ' ', '\t', '\n', '\r': + continue + case ':': + return i + 1, true + } + } + return i, false +} +func validcomma(data []byte, i int, end byte) (outi int, ok bool) { + for ; i < len(data); i++ { + switch data[i] { + default: + return i, false + case ' ', '\t', '\n', '\r': + continue + case ',': + return i, true + case end: + return i, true + } + } + return i, false +} +func validarray(data []byte, i int) (outi int, ok bool) { + for ; i < len(data); i++ { + switch data[i] { + default: + for ; i < len(data); i++ { + if i, ok = validany(data, i); !ok { + return i, false + } + if i, ok = validcomma(data, i, ']'); !ok { + return i, false + } + if data[i] == ']' { + return i + 1, true + } + } + case ' ', '\t', '\n', '\r': + continue + case ']': + return i + 1, true + } + } + return i, false +} +func validstring(data []byte, i int) (outi int, ok bool) { + for ; i < len(data); i++ { + if data[i] < ' ' { + return i, false + } else if data[i] == '\\' { + i++ + if i == len(data) { + return i, false + } + switch data[i] { + default: + return i, false + case '"', '\\', '/', 'b', 'f', 'n', 'r', 't': + case 'u': + for j := 0; j < 4; j++ { + i++ + if i >= len(data) { + return i, false + } + if !((data[i] >= '0' && data[i] <= '9') || + (data[i] >= 'a' && data[i] <= 'f') || + (data[i] >= 'A' && data[i] <= 'F')) { + return i, false + } + } + } + } else if data[i] == '"' { + return i + 1, true + } + } + return i, false +} +func validnumber(data []byte, i int) (outi int, ok bool) { + i-- + // sign + if data[i] == '-' { + i++ + if i == len(data) { + return i, false + } + if data[i] < '0' || data[i] > '9' { + return i, false + } + } + // int + if i == len(data) { + return i, false + } + if data[i] == '0' { + i++ + } else { + for ; i < len(data); i++ { + if data[i] >= '0' && data[i] <= '9' { + continue + } + break + } + } + // frac + if i == len(data) { + return i, true + } + if data[i] == '.' { + i++ + if i == len(data) { + return i, false + } + if data[i] < '0' || data[i] > '9' { + return i, false + } + i++ + for ; i < len(data); i++ { + if data[i] >= '0' && data[i] <= '9' { + continue + } + break + } + } + // exp + if i == len(data) { + return i, true + } + if data[i] == 'e' || data[i] == 'E' { + i++ + if i == len(data) { + return i, false + } + if data[i] == '+' || data[i] == '-' { + i++ + } + if i == len(data) { + return i, false + } + if data[i] < '0' || data[i] > '9' { + return i, false + } + i++ + for ; i < len(data); i++ { + if data[i] >= '0' && data[i] <= '9' { + continue + } + break + } + } + return i, true +} + +func validtrue(data []byte, i int) (outi int, ok bool) { + if i+3 <= len(data) && data[i] == 'r' && data[i+1] == 'u' && + data[i+2] == 'e' { + return i + 3, true + } + return i, false +} +func validfalse(data []byte, i int) (outi int, ok bool) { + if i+4 <= len(data) && data[i] == 'a' && data[i+1] == 'l' && + data[i+2] == 's' && data[i+3] == 'e' { + return i + 4, true + } + return i, false +} +func validnull(data []byte, i int) (outi int, ok bool) { + if i+3 <= len(data) && data[i] == 'u' && data[i+1] == 'l' && + data[i+2] == 'l' { + return i + 3, true + } + return i, false +} + +// Valid returns true if the input is valid json. +// +// if !gjson.Valid(json) { +// return errors.New("invalid json") +// } +// value := gjson.Get(json, "name.last") +// +func Valid(json string) bool { + _, ok := validpayload(stringBytes(json), 0) + return ok +} + +// ValidBytes returns true if the input is valid json. +// +// if !gjson.Valid(json) { +// return errors.New("invalid json") +// } +// value := gjson.Get(json, "name.last") +// +// If working with bytes, this method preferred over ValidBytes(string(data)) +// +func ValidBytes(json []byte) bool { + _, ok := validpayload(json, 0) + return ok +} + +func parseUint(s string) (n uint64, ok bool) { + var i int + if i == len(s) { + return 0, false + } + for ; i < len(s); i++ { + if s[i] >= '0' && s[i] <= '9' { + n = n*10 + uint64(s[i]-'0') + } else { + return 0, false + } + } + return n, true +} + +func parseInt(s string) (n int64, ok bool) { + var i int + var sign bool + if len(s) > 0 && s[0] == '-' { + sign = true + i++ + } + if i == len(s) { + return 0, false + } + for ; i < len(s); i++ { + if s[i] >= '0' && s[i] <= '9' { + n = n*10 + int64(s[i]-'0') + } else { + return 0, false + } + } + if sign { + return n * -1, true + } + return n, true +} + +// safeInt validates a given JSON number +// ensures it lies within the minimum and maximum representable JSON numbers +func safeInt(f float64) (n int64, ok bool) { + // https://tc39.es/ecma262/#sec-number.min_safe_integer + // https://tc39.es/ecma262/#sec-number.max_safe_integer + if f < -9007199254740991 || f > 9007199254740991 { + return 0, false + } + return int64(f), true +} + +// execStatic parses the path to find a static value. +// The input expects that the path already starts with a '!' +func execStatic(json, path string) (pathOut, res string, ok bool) { + name := path[1:] + if len(name) > 0 { + switch name[0] { + case '{', '[', '"', '+', '-', '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9': + _, res = parseSquash(name, 0) + pathOut = name[len(res):] + return pathOut, res, true + } + } + for i := 1; i < len(path); i++ { + if path[i] == '|' { + pathOut = path[i:] + name = path[1:i] + break + } + if path[i] == '.' { + pathOut = path[i:] + name = path[1:i] + break + } + } + switch strings.ToLower(name) { + case "true", "false", "null", "nan", "inf": + return pathOut, name, true + } + return pathOut, res, false +} + +// execModifier parses the path to find a matching modifier function. +// The input expects that the path already starts with a '@' +func execModifier(json, path string) (pathOut, res string, ok bool) { + name := path[1:] + var hasArgs bool + for i := 1; i < len(path); i++ { + if path[i] == ':' { + pathOut = path[i+1:] + name = path[1:i] + hasArgs = len(pathOut) > 0 + break + } + if path[i] == '|' { + pathOut = path[i:] + name = path[1:i] + break + } + if path[i] == '.' { + pathOut = path[i:] + name = path[1:i] + break + } + } + if fn, ok := modifiers[name]; ok { + var args string + if hasArgs { + var parsedArgs bool + switch pathOut[0] { + case '{', '[', '"': + res := Parse(pathOut) + if res.Exists() { + args = squash(pathOut) + pathOut = pathOut[len(args):] + parsedArgs = true + } + } + if !parsedArgs { + idx := strings.IndexByte(pathOut, '|') + if idx == -1 { + args = pathOut + pathOut = "" + } else { + args = pathOut[:idx] + pathOut = pathOut[idx:] + } + } + } + return pathOut, fn(json, args), true + } + return pathOut, res, false +} + +// unwrap removes the '[]' or '{}' characters around json +func unwrap(json string) string { + json = trim(json) + if len(json) >= 2 && (json[0] == '[' || json[0] == '{') { + json = json[1 : len(json)-1] + } + return json +} + +// DisableModifiers will disable the modifier syntax +var DisableModifiers = false + +var modifiers = map[string]func(json, arg string) string{ + "pretty": modPretty, + "ugly": modUgly, + "reverse": modReverse, + "this": modThis, + "flatten": modFlatten, + "join": modJoin, + "valid": modValid, + "keys": modKeys, + "values": modValues, + "tostr": modToStr, + "fromstr": modFromStr, + "group": modGroup, +} + +// AddModifier binds a custom modifier command to the GJSON syntax. +// This operation is not thread safe and should be executed prior to +// using all other gjson function. +func AddModifier(name string, fn func(json, arg string) string) { + modifiers[name] = fn +} + +// ModifierExists returns true when the specified modifier exists. +func ModifierExists(name string, fn func(json, arg string) string) bool { + _, ok := modifiers[name] + return ok +} + +// cleanWS remove any non-whitespace from string +func cleanWS(s string) string { + for i := 0; i < len(s); i++ { + switch s[i] { + case ' ', '\t', '\n', '\r': + continue + default: + var s2 []byte + for i := 0; i < len(s); i++ { + switch s[i] { + case ' ', '\t', '\n', '\r': + s2 = append(s2, s[i]) + } + } + return string(s2) + } + } + return s +} + +// @pretty modifier makes the json look nice. +func modPretty(json, arg string) string { + if len(arg) > 0 { + opts := *pretty.DefaultOptions + Parse(arg).ForEach(func(key, value Result) bool { + switch key.String() { + case "sortKeys": + opts.SortKeys = value.Bool() + case "indent": + opts.Indent = cleanWS(value.String()) + case "prefix": + opts.Prefix = cleanWS(value.String()) + case "width": + opts.Width = int(value.Int()) + } + return true + }) + return bytesString(pretty.PrettyOptions(stringBytes(json), &opts)) + } + return bytesString(pretty.Pretty(stringBytes(json))) +} + +// @this returns the current element. Can be used to retrieve the root element. +func modThis(json, arg string) string { + return json +} + +// @ugly modifier removes all whitespace. +func modUgly(json, arg string) string { + return bytesString(pretty.Ugly(stringBytes(json))) +} + +// @reverse reverses array elements or root object members. +func modReverse(json, arg string) string { + res := Parse(json) + if res.IsArray() { + var values []Result + res.ForEach(func(_, value Result) bool { + values = append(values, value) + return true + }) + out := make([]byte, 0, len(json)) + out = append(out, '[') + for i, j := len(values)-1, 0; i >= 0; i, j = i-1, j+1 { + if j > 0 { + out = append(out, ',') + } + out = append(out, values[i].Raw...) + } + out = append(out, ']') + return bytesString(out) + } + if res.IsObject() { + var keyValues []Result + res.ForEach(func(key, value Result) bool { + keyValues = append(keyValues, key, value) + return true + }) + out := make([]byte, 0, len(json)) + out = append(out, '{') + for i, j := len(keyValues)-2, 0; i >= 0; i, j = i-2, j+1 { + if j > 0 { + out = append(out, ',') + } + out = append(out, keyValues[i+0].Raw...) + out = append(out, ':') + out = append(out, keyValues[i+1].Raw...) + } + out = append(out, '}') + return bytesString(out) + } + return json +} + +// @flatten an array with child arrays. +// [1,[2],[3,4],[5,[6,7]]] -> [1,2,3,4,5,[6,7]] +// The {"deep":true} arg can be provide for deep flattening. +// [1,[2],[3,4],[5,[6,7]]] -> [1,2,3,4,5,6,7] +// The original json is returned when the json is not an array. +func modFlatten(json, arg string) string { + res := Parse(json) + if !res.IsArray() { + return json + } + var deep bool + if arg != "" { + Parse(arg).ForEach(func(key, value Result) bool { + if key.String() == "deep" { + deep = value.Bool() + } + return true + }) + } + var out []byte + out = append(out, '[') + var idx int + res.ForEach(func(_, value Result) bool { + var raw string + if value.IsArray() { + if deep { + raw = unwrap(modFlatten(value.Raw, arg)) + } else { + raw = unwrap(value.Raw) + } + } else { + raw = value.Raw + } + raw = strings.TrimSpace(raw) + if len(raw) > 0 { + if idx > 0 { + out = append(out, ',') + } + out = append(out, raw...) + idx++ + } + return true + }) + out = append(out, ']') + return bytesString(out) +} + +// @keys extracts the keys from an object. +// {"first":"Tom","last":"Smith"} -> ["first","last"] +func modKeys(json, arg string) string { + v := Parse(json) + if !v.Exists() { + return "[]" + } + obj := v.IsObject() + var out strings.Builder + out.WriteByte('[') + var i int + v.ForEach(func(key, _ Result) bool { + if i > 0 { + out.WriteByte(',') + } + if obj { + out.WriteString(key.Raw) + } else { + out.WriteString("null") + } + i++ + return true + }) + out.WriteByte(']') + return out.String() +} + +// @values extracts the values from an object. +// {"first":"Tom","last":"Smith"} -> ["Tom","Smith"] +func modValues(json, arg string) string { + v := Parse(json) + if !v.Exists() { + return "[]" + } + if v.IsArray() { + return json + } + var out strings.Builder + out.WriteByte('[') + var i int + v.ForEach(func(_, value Result) bool { + if i > 0 { + out.WriteByte(',') + } + out.WriteString(value.Raw) + i++ + return true + }) + out.WriteByte(']') + return out.String() +} + +// @join multiple objects into a single object. +// [{"first":"Tom"},{"last":"Smith"}] -> {"first","Tom","last":"Smith"} +// The arg can be "true" to specify that duplicate keys should be preserved. +// [{"first":"Tom","age":37},{"age":41}] -> {"first","Tom","age":37,"age":41} +// Without preserved keys: +// [{"first":"Tom","age":37},{"age":41}] -> {"first","Tom","age":41} +// The original json is returned when the json is not an object. +func modJoin(json, arg string) string { + res := Parse(json) + if !res.IsArray() { + return json + } + var preserve bool + if arg != "" { + Parse(arg).ForEach(func(key, value Result) bool { + if key.String() == "preserve" { + preserve = value.Bool() + } + return true + }) + } + var out []byte + out = append(out, '{') + if preserve { + // Preserve duplicate keys. + var idx int + res.ForEach(func(_, value Result) bool { + if !value.IsObject() { + return true + } + if idx > 0 { + out = append(out, ',') + } + out = append(out, unwrap(value.Raw)...) + idx++ + return true + }) + } else { + // Deduplicate keys and generate an object with stable ordering. + var keys []Result + kvals := make(map[string]Result) + res.ForEach(func(_, value Result) bool { + if !value.IsObject() { + return true + } + value.ForEach(func(key, value Result) bool { + k := key.String() + if _, ok := kvals[k]; !ok { + keys = append(keys, key) + } + kvals[k] = value + return true + }) + return true + }) + for i := 0; i < len(keys); i++ { + if i > 0 { + out = append(out, ',') + } + out = append(out, keys[i].Raw...) + out = append(out, ':') + out = append(out, kvals[keys[i].String()].Raw...) + } + } + out = append(out, '}') + return bytesString(out) +} + +// @valid ensures that the json is valid before moving on. An empty string is +// returned when the json is not valid, otherwise it returns the original json. +func modValid(json, arg string) string { + if !Valid(json) { + return "" + } + return json +} + +// @fromstr converts a string to json +// "{\"id\":1023,\"name\":\"alert\"}" -> {"id":1023,"name":"alert"} +func modFromStr(json, arg string) string { + if !Valid(json) { + return "" + } + return Parse(json).String() +} + +// @tostr converts a string to json +// {"id":1023,"name":"alert"} -> "{\"id\":1023,\"name\":\"alert\"}" +func modToStr(str, arg string) string { + return string(AppendJSONString(nil, str)) +} + +func modGroup(json, arg string) string { + res := Parse(json) + if !res.IsObject() { + return "" + } + var all [][]byte + res.ForEach(func(key, value Result) bool { + if !value.IsArray() { + return true + } + var idx int + value.ForEach(func(_, value Result) bool { + if idx == len(all) { + all = append(all, []byte{}) + } + all[idx] = append(all[idx], ("," + key.Raw + ":" + value.Raw)...) + idx++ + return true + }) + return true + }) + var data []byte + data = append(data, '[') + for i, item := range all { + if i > 0 { + data = append(data, ',') + } + data = append(data, '{') + data = append(data, item[1:]...) + data = append(data, '}') + } + data = append(data, ']') + return string(data) +} + +// stringHeader instead of reflect.StringHeader +type stringHeader struct { + data unsafe.Pointer + len int +} + +// sliceHeader instead of reflect.SliceHeader +type sliceHeader struct { + data unsafe.Pointer + len int + cap int +} + +// getBytes casts the input json bytes to a string and safely returns the +// results as uniquely allocated data. This operation is intended to minimize +// copies and allocations for the large json string->[]byte. +func getBytes(json []byte, path string) Result { + var result Result + if json != nil { + // unsafe cast to string + result = Get(*(*string)(unsafe.Pointer(&json)), path) + // safely get the string headers + rawhi := *(*stringHeader)(unsafe.Pointer(&result.Raw)) + strhi := *(*stringHeader)(unsafe.Pointer(&result.Str)) + // create byte slice headers + rawh := sliceHeader{data: rawhi.data, len: rawhi.len, cap: rawhi.len} + strh := sliceHeader{data: strhi.data, len: strhi.len, cap: rawhi.len} + if strh.data == nil { + // str is nil + if rawh.data == nil { + // raw is nil + result.Raw = "" + } else { + // raw has data, safely copy the slice header to a string + result.Raw = string(*(*[]byte)(unsafe.Pointer(&rawh))) + } + result.Str = "" + } else if rawh.data == nil { + // raw is nil + result.Raw = "" + // str has data, safely copy the slice header to a string + result.Str = string(*(*[]byte)(unsafe.Pointer(&strh))) + } else if uintptr(strh.data) >= uintptr(rawh.data) && + uintptr(strh.data)+uintptr(strh.len) <= + uintptr(rawh.data)+uintptr(rawh.len) { + // Str is a substring of Raw. + start := uintptr(strh.data) - uintptr(rawh.data) + // safely copy the raw slice header + result.Raw = string(*(*[]byte)(unsafe.Pointer(&rawh))) + // substring the raw + result.Str = result.Raw[start : start+uintptr(strh.len)] + } else { + // safely copy both the raw and str slice headers to strings + result.Raw = string(*(*[]byte)(unsafe.Pointer(&rawh))) + result.Str = string(*(*[]byte)(unsafe.Pointer(&strh))) + } + } + return result +} + +// fillIndex finds the position of Raw data and assigns it to the Index field +// of the resulting value. If the position cannot be found then Index zero is +// used instead. +func fillIndex(json string, c *parseContext) { + if len(c.value.Raw) > 0 && !c.calcd { + jhdr := *(*stringHeader)(unsafe.Pointer(&json)) + rhdr := *(*stringHeader)(unsafe.Pointer(&(c.value.Raw))) + c.value.Index = int(uintptr(rhdr.data) - uintptr(jhdr.data)) + if c.value.Index < 0 || c.value.Index >= len(json) { + c.value.Index = 0 + } + } +} + +func stringBytes(s string) []byte { + return *(*[]byte)(unsafe.Pointer(&sliceHeader{ + data: (*stringHeader)(unsafe.Pointer(&s)).data, + len: len(s), + cap: len(s), + })) +} + +func bytesString(b []byte) string { + return *(*string)(unsafe.Pointer(&b)) +} + +func revSquash(json string) string { + // reverse squash + // expects that the tail character is a ']' or '}' or ')' or '"' + // squash the value, ignoring all nested arrays and objects. + i := len(json) - 1 + var depth int + if json[i] != '"' { + depth++ + } + if json[i] == '}' || json[i] == ']' || json[i] == ')' { + i-- + } + for ; i >= 0; i-- { + switch json[i] { + case '"': + i-- + for ; i >= 0; i-- { + if json[i] == '"' { + esc := 0 + for i > 0 && json[i-1] == '\\' { + i-- + esc++ + } + if esc%2 == 1 { + continue + } + i += esc + break + } + } + if depth == 0 { + if i < 0 { + i = 0 + } + return json[i:] + } + case '}', ']', ')': + depth++ + case '{', '[', '(': + depth-- + if depth == 0 { + return json[i:] + } + } + } + return json +} + +// Paths returns the original GJSON paths for a Result where the Result came +// from a simple query path that returns an array, like: +// +// gjson.Get(json, "friends.#.first") +// +// The returned value will be in the form of a JSON array: +// +// ["friends.0.first","friends.1.first","friends.2.first"] +// +// The param 'json' must be the original JSON used when calling Get. +// +// Returns an empty string if the paths cannot be determined, which can happen +// when the Result came from a path that contained a multipath, modifier, +// or a nested query. +func (t Result) Paths(json string) []string { + if t.Indexes == nil { + return nil + } + paths := make([]string, 0, len(t.Indexes)) + t.ForEach(func(_, value Result) bool { + paths = append(paths, value.Path(json)) + return true + }) + if len(paths) != len(t.Indexes) { + return nil + } + return paths +} + +// Path returns the original GJSON path for a Result where the Result came +// from a simple path that returns a single value, like: +// +// gjson.Get(json, "friends.#(last=Murphy)") +// +// The returned value will be in the form of a JSON string: +// +// "friends.0" +// +// The param 'json' must be the original JSON used when calling Get. +// +// Returns an empty string if the paths cannot be determined, which can happen +// when the Result came from a path that contained a multipath, modifier, +// or a nested query. +func (t Result) Path(json string) string { + var path []byte + var comps []string // raw components + i := t.Index - 1 + if t.Index+len(t.Raw) > len(json) { + // JSON cannot safely contain Result. + goto fail + } + if !strings.HasPrefix(json[t.Index:], t.Raw) { + // Result is not at the JSON index as exepcted. + goto fail + } + for ; i >= 0; i-- { + if json[i] <= ' ' { + continue + } + if json[i] == ':' { + // inside of object, get the key + for ; i >= 0; i-- { + if json[i] != '"' { + continue + } + break + } + raw := revSquash(json[:i+1]) + i = i - len(raw) + comps = append(comps, raw) + // key gotten, now squash the rest + raw = revSquash(json[:i+1]) + i = i - len(raw) + i++ // increment the index for next loop step + } else if json[i] == '{' { + // Encountered an open object. The original result was probably an + // object key. + goto fail + } else if json[i] == ',' || json[i] == '[' { + // inside of an array, count the position + var arrIdx int + if json[i] == ',' { + arrIdx++ + i-- + } + for ; i >= 0; i-- { + if json[i] == ':' { + // Encountered an unexpected colon. The original result was + // probably an object key. + goto fail + } else if json[i] == ',' { + arrIdx++ + } else if json[i] == '[' { + comps = append(comps, strconv.Itoa(arrIdx)) + break + } else if json[i] == ']' || json[i] == '}' || json[i] == '"' { + raw := revSquash(json[:i+1]) + i = i - len(raw) + 1 + } + } + } + } + if len(comps) == 0 { + if DisableModifiers { + goto fail + } + return "@this" + } + for i := len(comps) - 1; i >= 0; i-- { + rcomp := Parse(comps[i]) + if !rcomp.Exists() { + goto fail + } + comp := escapeComp(rcomp.String()) + path = append(path, '.') + path = append(path, comp...) + } + if len(path) > 0 { + path = path[1:] + } + return string(path) +fail: + return "" +} + +// isSafePathKeyChar returns true if the input character is safe for not +// needing escaping. +func isSafePathKeyChar(c byte) bool { + return c <= ' ' || c > '~' || c == '_' || c == '-' || c == ':' || + (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') +} + +// escapeComp escaped a path compontent, making it safe for generating a +// path for later use. +func escapeComp(comp string) string { + for i := 0; i < len(comp); i++ { + if !isSafePathKeyChar(comp[i]) { + ncomp := []byte(comp[:i]) + for ; i < len(comp); i++ { + if !isSafePathKeyChar(comp[i]) { + ncomp = append(ncomp, '\\') + } + ncomp = append(ncomp, comp[i]) + } + return string(ncomp) + } + } + return comp +} diff --git a/vendor/github.com/tidwall/gjson/logo.png b/vendor/github.com/tidwall/gjson/logo.png Binary files differnew file mode 100644 index 00000000..17a8bbe9 --- /dev/null +++ b/vendor/github.com/tidwall/gjson/logo.png diff --git a/vendor/github.com/tidwall/match/LICENSE b/vendor/github.com/tidwall/match/LICENSE new file mode 100644 index 00000000..58f5819a --- /dev/null +++ b/vendor/github.com/tidwall/match/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2016 Josh Baker + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/tidwall/match/README.md b/vendor/github.com/tidwall/match/README.md new file mode 100644 index 00000000..5fdd4cf6 --- /dev/null +++ b/vendor/github.com/tidwall/match/README.md @@ -0,0 +1,29 @@ +# Match + +[![GoDoc](https://godoc.org/github.com/tidwall/match?status.svg)](https://godoc.org/github.com/tidwall/match) + +Match is a very simple pattern matcher where '*' matches on any +number characters and '?' matches on any one character. + +## Installing + +``` +go get -u github.com/tidwall/match +``` + +## Example + +```go +match.Match("hello", "*llo") +match.Match("jello", "?ello") +match.Match("hello", "h*o") +``` + + +## Contact + +Josh Baker [@tidwall](http://twitter.com/tidwall) + +## License + +Redcon source code is available under the MIT [License](/LICENSE). diff --git a/vendor/github.com/tidwall/match/match.go b/vendor/github.com/tidwall/match/match.go new file mode 100644 index 00000000..11da28f1 --- /dev/null +++ b/vendor/github.com/tidwall/match/match.go @@ -0,0 +1,237 @@ +// Package match provides a simple pattern matcher with unicode support. +package match + +import ( + "unicode/utf8" +) + +// Match returns true if str matches pattern. This is a very +// simple wildcard match where '*' matches on any number characters +// and '?' matches on any one character. +// +// pattern: +// { term } +// term: +// '*' matches any sequence of non-Separator characters +// '?' matches any single non-Separator character +// c matches character c (c != '*', '?', '\\') +// '\\' c matches character c +// +func Match(str, pattern string) bool { + if pattern == "*" { + return true + } + return match(str, pattern, 0, nil, -1) == rMatch +} + +// MatchLimit is the same as Match but will limit the complexity of the match +// operation. This is to avoid long running matches, specifically to avoid ReDos +// attacks from arbritary inputs. +// +// How it works: +// The underlying match routine is recursive and may call itself when it +// encounters a sandwiched wildcard pattern, such as: `user:*:name`. +// Everytime it calls itself a counter is incremented. +// The operation is stopped when counter > maxcomp*len(str). +func MatchLimit(str, pattern string, maxcomp int) (matched, stopped bool) { + if pattern == "*" { + return true, false + } + counter := 0 + r := match(str, pattern, len(str), &counter, maxcomp) + if r == rStop { + return false, true + } + return r == rMatch, false +} + +type result int + +const ( + rNoMatch result = iota + rMatch + rStop +) + +func match(str, pat string, slen int, counter *int, maxcomp int) result { + // check complexity limit + if maxcomp > -1 { + if *counter > slen*maxcomp { + return rStop + } + *counter++ + } + + for len(pat) > 0 { + var wild bool + pc, ps := rune(pat[0]), 1 + if pc > 0x7f { + pc, ps = utf8.DecodeRuneInString(pat) + } + var sc rune + var ss int + if len(str) > 0 { + sc, ss = rune(str[0]), 1 + if sc > 0x7f { + sc, ss = utf8.DecodeRuneInString(str) + } + } + switch pc { + case '?': + if ss == 0 { + return rNoMatch + } + case '*': + // Ignore repeating stars. + for len(pat) > 1 && pat[1] == '*' { + pat = pat[1:] + } + + // If this star is the last character then it must be a match. + if len(pat) == 1 { + return rMatch + } + + // Match and trim any non-wildcard suffix characters. + var ok bool + str, pat, ok = matchTrimSuffix(str, pat) + if !ok { + return rNoMatch + } + + // Check for single star again. + if len(pat) == 1 { + return rMatch + } + + // Perform recursive wildcard search. + r := match(str, pat[1:], slen, counter, maxcomp) + if r != rNoMatch { + return r + } + if len(str) == 0 { + return rNoMatch + } + wild = true + default: + if ss == 0 { + return rNoMatch + } + if pc == '\\' { + pat = pat[ps:] + pc, ps = utf8.DecodeRuneInString(pat) + if ps == 0 { + return rNoMatch + } + } + if sc != pc { + return rNoMatch + } + } + str = str[ss:] + if !wild { + pat = pat[ps:] + } + } + if len(str) == 0 { + return rMatch + } + return rNoMatch +} + +// matchTrimSuffix matches and trims any non-wildcard suffix characters. +// Returns the trimed string and pattern. +// +// This is called because the pattern contains extra data after the wildcard +// star. Here we compare any suffix characters in the pattern to the suffix of +// the target string. Basically a reverse match that stops when a wildcard +// character is reached. This is a little trickier than a forward match because +// we need to evaluate an escaped character in reverse. +// +// Any matched characters will be trimmed from both the target +// string and the pattern. +func matchTrimSuffix(str, pat string) (string, string, bool) { + // It's expected that the pattern has at least two bytes and the first byte + // is a wildcard star '*' + match := true + for len(str) > 0 && len(pat) > 1 { + pc, ps := utf8.DecodeLastRuneInString(pat) + var esc bool + for i := 0; ; i++ { + if pat[len(pat)-ps-i-1] != '\\' { + if i&1 == 1 { + esc = true + ps++ + } + break + } + } + if pc == '*' && !esc { + match = true + break + } + sc, ss := utf8.DecodeLastRuneInString(str) + if !((pc == '?' && !esc) || pc == sc) { + match = false + break + } + str = str[:len(str)-ss] + pat = pat[:len(pat)-ps] + } + return str, pat, match +} + +var maxRuneBytes = [...]byte{244, 143, 191, 191} + +// Allowable parses the pattern and determines the minimum and maximum allowable +// values that the pattern can represent. +// When the max cannot be determined, 'true' will be returned +// for infinite. +func Allowable(pattern string) (min, max string) { + if pattern == "" || pattern[0] == '*' { + return "", "" + } + + minb := make([]byte, 0, len(pattern)) + maxb := make([]byte, 0, len(pattern)) + var wild bool + for i := 0; i < len(pattern); i++ { + if pattern[i] == '*' { + wild = true + break + } + if pattern[i] == '?' { + minb = append(minb, 0) + maxb = append(maxb, maxRuneBytes[:]...) + } else { + minb = append(minb, pattern[i]) + maxb = append(maxb, pattern[i]) + } + } + if wild { + r, n := utf8.DecodeLastRune(maxb) + if r != utf8.RuneError { + if r < utf8.MaxRune { + r++ + if r > 0x7f { + b := make([]byte, 4) + nn := utf8.EncodeRune(b, r) + maxb = append(maxb[:len(maxb)-n], b[:nn]...) + } else { + maxb = append(maxb[:len(maxb)-n], byte(r)) + } + } + } + } + return string(minb), string(maxb) +} + +// IsPattern returns true if the string is a pattern. +func IsPattern(str string) bool { + for i := 0; i < len(str); i++ { + if str[i] == '*' || str[i] == '?' { + return true + } + } + return false +} diff --git a/vendor/github.com/tidwall/pretty/LICENSE b/vendor/github.com/tidwall/pretty/LICENSE new file mode 100644 index 00000000..993b83f2 --- /dev/null +++ b/vendor/github.com/tidwall/pretty/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2017 Josh Baker + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/tidwall/pretty/README.md b/vendor/github.com/tidwall/pretty/README.md new file mode 100644 index 00000000..d3be5e54 --- /dev/null +++ b/vendor/github.com/tidwall/pretty/README.md @@ -0,0 +1,122 @@ +# Pretty + +[![GoDoc](https://img.shields.io/badge/api-reference-blue.svg?style=flat-square)](https://pkg.go.dev/github.com/tidwall/pretty) + +Pretty is a Go package that provides [fast](#performance) methods for formatting JSON for human readability, or to compact JSON for smaller payloads. + +Getting Started +=============== + +## Installing + +To start using Pretty, install Go and run `go get`: + +```sh +$ go get -u github.com/tidwall/pretty +``` + +This will retrieve the library. + +## Pretty + +Using this example: + +```json +{"name": {"first":"Tom","last":"Anderson"}, "age":37, +"children": ["Sara","Alex","Jack"], +"fav.movie": "Deer Hunter", "friends": [ + {"first": "Janet", "last": "Murphy", "age": 44} + ]} +``` + +The following code: +```go +result = pretty.Pretty(example) +``` + +Will format the json to: + +```json +{ + "name": { + "first": "Tom", + "last": "Anderson" + }, + "age": 37, + "children": ["Sara", "Alex", "Jack"], + "fav.movie": "Deer Hunter", + "friends": [ + { + "first": "Janet", + "last": "Murphy", + "age": 44 + } + ] +} +``` + +## Color + +Color will colorize the json for outputing to the screen. + +```json +result = pretty.Color(json, nil) +``` + +Will add color to the result for printing to the terminal. +The second param is used for a customizing the style, and passing nil will use the default `pretty.TerminalStyle`. + +## Ugly + +The following code: +```go +result = pretty.Ugly(example) +``` + +Will format the json to: + +```json +{"name":{"first":"Tom","last":"Anderson"},"age":37,"children":["Sara","Alex","Jack"],"fav.movie":"Deer Hunter","friends":[{"first":"Janet","last":"Murphy","age":44}]}``` +``` + +## Customized output + +There's a `PrettyOptions(json, opts)` function which allows for customizing the output with the following options: + +```go +type Options struct { + // Width is an max column width for single line arrays + // Default is 80 + Width int + // Prefix is a prefix for all lines + // Default is an empty string + Prefix string + // Indent is the nested indentation + // Default is two spaces + Indent string + // SortKeys will sort the keys alphabetically + // Default is false + SortKeys bool +} +``` +## Performance + +Benchmarks of Pretty alongside the builtin `encoding/json` Indent/Compact methods. +``` +BenchmarkPretty-16 1000000 1034 ns/op 720 B/op 2 allocs/op +BenchmarkPrettySortKeys-16 586797 1983 ns/op 2848 B/op 14 allocs/op +BenchmarkUgly-16 4652365 254 ns/op 240 B/op 1 allocs/op +BenchmarkUglyInPlace-16 6481233 183 ns/op 0 B/op 0 allocs/op +BenchmarkJSONIndent-16 450654 2687 ns/op 1221 B/op 0 allocs/op +BenchmarkJSONCompact-16 685111 1699 ns/op 442 B/op 0 allocs/op +``` + +*These benchmarks were run on a MacBook Pro 2.4 GHz 8-Core Intel Core i9.* + +## Contact +Josh Baker [@tidwall](http://twitter.com/tidwall) + +## License + +Pretty source code is available under the MIT [License](/LICENSE). + diff --git a/vendor/github.com/tidwall/pretty/pretty.go b/vendor/github.com/tidwall/pretty/pretty.go new file mode 100644 index 00000000..f3f756aa --- /dev/null +++ b/vendor/github.com/tidwall/pretty/pretty.go @@ -0,0 +1,674 @@ +package pretty + +import ( + "bytes" + "encoding/json" + "sort" + "strconv" +) + +// Options is Pretty options +type Options struct { + // Width is an max column width for single line arrays + // Default is 80 + Width int + // Prefix is a prefix for all lines + // Default is an empty string + Prefix string + // Indent is the nested indentation + // Default is two spaces + Indent string + // SortKeys will sort the keys alphabetically + // Default is false + SortKeys bool +} + +// DefaultOptions is the default options for pretty formats. +var DefaultOptions = &Options{Width: 80, Prefix: "", Indent: " ", SortKeys: false} + +// Pretty converts the input json into a more human readable format where each +// element is on it's own line with clear indentation. +func Pretty(json []byte) []byte { return PrettyOptions(json, nil) } + +// PrettyOptions is like Pretty but with customized options. +func PrettyOptions(json []byte, opts *Options) []byte { + if opts == nil { + opts = DefaultOptions + } + buf := make([]byte, 0, len(json)) + if len(opts.Prefix) != 0 { + buf = append(buf, opts.Prefix...) + } + buf, _, _, _ = appendPrettyAny(buf, json, 0, true, + opts.Width, opts.Prefix, opts.Indent, opts.SortKeys, + 0, 0, -1) + if len(buf) > 0 { + buf = append(buf, '\n') + } + return buf +} + +// Ugly removes insignificant space characters from the input json byte slice +// and returns the compacted result. +func Ugly(json []byte) []byte { + buf := make([]byte, 0, len(json)) + return ugly(buf, json) +} + +// UglyInPlace removes insignificant space characters from the input json +// byte slice and returns the compacted result. This method reuses the +// input json buffer to avoid allocations. Do not use the original bytes +// slice upon return. +func UglyInPlace(json []byte) []byte { return ugly(json, json) } + +func ugly(dst, src []byte) []byte { + dst = dst[:0] + for i := 0; i < len(src); i++ { + if src[i] > ' ' { + dst = append(dst, src[i]) + if src[i] == '"' { + for i = i + 1; i < len(src); i++ { + dst = append(dst, src[i]) + if src[i] == '"' { + j := i - 1 + for ; ; j-- { + if src[j] != '\\' { + break + } + } + if (j-i)%2 != 0 { + break + } + } + } + } + } + } + return dst +} + +func isNaNOrInf(src []byte) bool { + return src[0] == 'i' || //Inf + src[0] == 'I' || // inf + src[0] == '+' || // +Inf + src[0] == 'N' || // Nan + (src[0] == 'n' && len(src) > 1 && src[1] != 'u') // nan +} + +func appendPrettyAny(buf, json []byte, i int, pretty bool, width int, prefix, indent string, sortkeys bool, tabs, nl, max int) ([]byte, int, int, bool) { + for ; i < len(json); i++ { + if json[i] <= ' ' { + continue + } + if json[i] == '"' { + return appendPrettyString(buf, json, i, nl) + } + + if (json[i] >= '0' && json[i] <= '9') || json[i] == '-' || isNaNOrInf(json[i:]) { + return appendPrettyNumber(buf, json, i, nl) + } + if json[i] == '{' { + return appendPrettyObject(buf, json, i, '{', '}', pretty, width, prefix, indent, sortkeys, tabs, nl, max) + } + if json[i] == '[' { + return appendPrettyObject(buf, json, i, '[', ']', pretty, width, prefix, indent, sortkeys, tabs, nl, max) + } + switch json[i] { + case 't': + return append(buf, 't', 'r', 'u', 'e'), i + 4, nl, true + case 'f': + return append(buf, 'f', 'a', 'l', 's', 'e'), i + 5, nl, true + case 'n': + return append(buf, 'n', 'u', 'l', 'l'), i + 4, nl, true + } + } + return buf, i, nl, true +} + +type pair struct { + kstart, kend int + vstart, vend int +} + +type byKeyVal struct { + sorted bool + json []byte + buf []byte + pairs []pair +} + +func (arr *byKeyVal) Len() int { + return len(arr.pairs) +} +func (arr *byKeyVal) Less(i, j int) bool { + if arr.isLess(i, j, byKey) { + return true + } + if arr.isLess(j, i, byKey) { + return false + } + return arr.isLess(i, j, byVal) +} +func (arr *byKeyVal) Swap(i, j int) { + arr.pairs[i], arr.pairs[j] = arr.pairs[j], arr.pairs[i] + arr.sorted = true +} + +type byKind int + +const ( + byKey byKind = 0 + byVal byKind = 1 +) + +type jtype int + +const ( + jnull jtype = iota + jfalse + jnumber + jstring + jtrue + jjson +) + +func getjtype(v []byte) jtype { + if len(v) == 0 { + return jnull + } + switch v[0] { + case '"': + return jstring + case 'f': + return jfalse + case 't': + return jtrue + case 'n': + return jnull + case '[', '{': + return jjson + default: + return jnumber + } +} + +func (arr *byKeyVal) isLess(i, j int, kind byKind) bool { + k1 := arr.json[arr.pairs[i].kstart:arr.pairs[i].kend] + k2 := arr.json[arr.pairs[j].kstart:arr.pairs[j].kend] + var v1, v2 []byte + if kind == byKey { + v1 = k1 + v2 = k2 + } else { + v1 = bytes.TrimSpace(arr.buf[arr.pairs[i].vstart:arr.pairs[i].vend]) + v2 = bytes.TrimSpace(arr.buf[arr.pairs[j].vstart:arr.pairs[j].vend]) + if len(v1) >= len(k1)+1 { + v1 = bytes.TrimSpace(v1[len(k1)+1:]) + } + if len(v2) >= len(k2)+1 { + v2 = bytes.TrimSpace(v2[len(k2)+1:]) + } + } + t1 := getjtype(v1) + t2 := getjtype(v2) + if t1 < t2 { + return true + } + if t1 > t2 { + return false + } + if t1 == jstring { + s1 := parsestr(v1) + s2 := parsestr(v2) + return string(s1) < string(s2) + } + if t1 == jnumber { + n1, _ := strconv.ParseFloat(string(v1), 64) + n2, _ := strconv.ParseFloat(string(v2), 64) + return n1 < n2 + } + return string(v1) < string(v2) + +} + +func parsestr(s []byte) []byte { + for i := 1; i < len(s); i++ { + if s[i] == '\\' { + var str string + json.Unmarshal(s, &str) + return []byte(str) + } + if s[i] == '"' { + return s[1:i] + } + } + return nil +} + +func appendPrettyObject(buf, json []byte, i int, open, close byte, pretty bool, width int, prefix, indent string, sortkeys bool, tabs, nl, max int) ([]byte, int, int, bool) { + var ok bool + if width > 0 { + if pretty && open == '[' && max == -1 { + // here we try to create a single line array + max := width - (len(buf) - nl) + if max > 3 { + s1, s2 := len(buf), i + buf, i, _, ok = appendPrettyObject(buf, json, i, '[', ']', false, width, prefix, "", sortkeys, 0, 0, max) + if ok && len(buf)-s1 <= max { + return buf, i, nl, true + } + buf = buf[:s1] + i = s2 + } + } else if max != -1 && open == '{' { + return buf, i, nl, false + } + } + buf = append(buf, open) + i++ + var pairs []pair + if open == '{' && sortkeys { + pairs = make([]pair, 0, 8) + } + var n int + for ; i < len(json); i++ { + if json[i] <= ' ' { + continue + } + if json[i] == close { + if pretty { + if open == '{' && sortkeys { + buf = sortPairs(json, buf, pairs) + } + if n > 0 { + nl = len(buf) + if buf[nl-1] == ' ' { + buf[nl-1] = '\n' + } else { + buf = append(buf, '\n') + } + } + if buf[len(buf)-1] != open { + buf = appendTabs(buf, prefix, indent, tabs) + } + } + buf = append(buf, close) + return buf, i + 1, nl, open != '{' + } + if open == '[' || json[i] == '"' { + if n > 0 { + buf = append(buf, ',') + if width != -1 && open == '[' { + buf = append(buf, ' ') + } + } + var p pair + if pretty { + nl = len(buf) + if buf[nl-1] == ' ' { + buf[nl-1] = '\n' + } else { + buf = append(buf, '\n') + } + if open == '{' && sortkeys { + p.kstart = i + p.vstart = len(buf) + } + buf = appendTabs(buf, prefix, indent, tabs+1) + } + if open == '{' { + buf, i, nl, _ = appendPrettyString(buf, json, i, nl) + if sortkeys { + p.kend = i + } + buf = append(buf, ':') + if pretty { + buf = append(buf, ' ') + } + } + buf, i, nl, ok = appendPrettyAny(buf, json, i, pretty, width, prefix, indent, sortkeys, tabs+1, nl, max) + if max != -1 && !ok { + return buf, i, nl, false + } + if pretty && open == '{' && sortkeys { + p.vend = len(buf) + if p.kstart > p.kend || p.vstart > p.vend { + // bad data. disable sorting + sortkeys = false + } else { + pairs = append(pairs, p) + } + } + i-- + n++ + } + } + return buf, i, nl, open != '{' +} +func sortPairs(json, buf []byte, pairs []pair) []byte { + if len(pairs) == 0 { + return buf + } + vstart := pairs[0].vstart + vend := pairs[len(pairs)-1].vend + arr := byKeyVal{false, json, buf, pairs} + sort.Stable(&arr) + if !arr.sorted { + return buf + } + nbuf := make([]byte, 0, vend-vstart) + for i, p := range pairs { + nbuf = append(nbuf, buf[p.vstart:p.vend]...) + if i < len(pairs)-1 { + nbuf = append(nbuf, ',') + nbuf = append(nbuf, '\n') + } + } + return append(buf[:vstart], nbuf...) +} + +func appendPrettyString(buf, json []byte, i, nl int) ([]byte, int, int, bool) { + s := i + i++ + for ; i < len(json); i++ { + if json[i] == '"' { + var sc int + for j := i - 1; j > s; j-- { + if json[j] == '\\' { + sc++ + } else { + break + } + } + if sc%2 == 1 { + continue + } + i++ + break + } + } + return append(buf, json[s:i]...), i, nl, true +} + +func appendPrettyNumber(buf, json []byte, i, nl int) ([]byte, int, int, bool) { + s := i + i++ + for ; i < len(json); i++ { + if json[i] <= ' ' || json[i] == ',' || json[i] == ':' || json[i] == ']' || json[i] == '}' { + break + } + } + return append(buf, json[s:i]...), i, nl, true +} + +func appendTabs(buf []byte, prefix, indent string, tabs int) []byte { + if len(prefix) != 0 { + buf = append(buf, prefix...) + } + if len(indent) == 2 && indent[0] == ' ' && indent[1] == ' ' { + for i := 0; i < tabs; i++ { + buf = append(buf, ' ', ' ') + } + } else { + for i := 0; i < tabs; i++ { + buf = append(buf, indent...) + } + } + return buf +} + +// Style is the color style +type Style struct { + Key, String, Number [2]string + True, False, Null [2]string + Escape [2]string + Append func(dst []byte, c byte) []byte +} + +func hexp(p byte) byte { + switch { + case p < 10: + return p + '0' + default: + return (p - 10) + 'a' + } +} + +// TerminalStyle is for terminals +var TerminalStyle *Style + +func init() { + TerminalStyle = &Style{ + Key: [2]string{"\x1B[94m", "\x1B[0m"}, + String: [2]string{"\x1B[92m", "\x1B[0m"}, + Number: [2]string{"\x1B[93m", "\x1B[0m"}, + True: [2]string{"\x1B[96m", "\x1B[0m"}, + False: [2]string{"\x1B[96m", "\x1B[0m"}, + Null: [2]string{"\x1B[91m", "\x1B[0m"}, + Escape: [2]string{"\x1B[35m", "\x1B[0m"}, + Append: func(dst []byte, c byte) []byte { + if c < ' ' && (c != '\r' && c != '\n' && c != '\t' && c != '\v') { + dst = append(dst, "\\u00"...) + dst = append(dst, hexp((c>>4)&0xF)) + return append(dst, hexp((c)&0xF)) + } + return append(dst, c) + }, + } +} + +// Color will colorize the json. The style parma is used for customizing +// the colors. Passing nil to the style param will use the default +// TerminalStyle. +func Color(src []byte, style *Style) []byte { + if style == nil { + style = TerminalStyle + } + apnd := style.Append + if apnd == nil { + apnd = func(dst []byte, c byte) []byte { + return append(dst, c) + } + } + type stackt struct { + kind byte + key bool + } + var dst []byte + var stack []stackt + for i := 0; i < len(src); i++ { + if src[i] == '"' { + key := len(stack) > 0 && stack[len(stack)-1].key + if key { + dst = append(dst, style.Key[0]...) + } else { + dst = append(dst, style.String[0]...) + } + dst = apnd(dst, '"') + esc := false + uesc := 0 + for i = i + 1; i < len(src); i++ { + if src[i] == '\\' { + if key { + dst = append(dst, style.Key[1]...) + } else { + dst = append(dst, style.String[1]...) + } + dst = append(dst, style.Escape[0]...) + dst = apnd(dst, src[i]) + esc = true + if i+1 < len(src) && src[i+1] == 'u' { + uesc = 5 + } else { + uesc = 1 + } + } else if esc { + dst = apnd(dst, src[i]) + if uesc == 1 { + esc = false + dst = append(dst, style.Escape[1]...) + if key { + dst = append(dst, style.Key[0]...) + } else { + dst = append(dst, style.String[0]...) + } + } else { + uesc-- + } + } else { + dst = apnd(dst, src[i]) + } + if src[i] == '"' { + j := i - 1 + for ; ; j-- { + if src[j] != '\\' { + break + } + } + if (j-i)%2 != 0 { + break + } + } + } + if esc { + dst = append(dst, style.Escape[1]...) + } else if key { + dst = append(dst, style.Key[1]...) + } else { + dst = append(dst, style.String[1]...) + } + } else if src[i] == '{' || src[i] == '[' { + stack = append(stack, stackt{src[i], src[i] == '{'}) + dst = apnd(dst, src[i]) + } else if (src[i] == '}' || src[i] == ']') && len(stack) > 0 { + stack = stack[:len(stack)-1] + dst = apnd(dst, src[i]) + } else if (src[i] == ':' || src[i] == ',') && len(stack) > 0 && stack[len(stack)-1].kind == '{' { + stack[len(stack)-1].key = !stack[len(stack)-1].key + dst = apnd(dst, src[i]) + } else { + var kind byte + if (src[i] >= '0' && src[i] <= '9') || src[i] == '-' || isNaNOrInf(src[i:]) { + kind = '0' + dst = append(dst, style.Number[0]...) + } else if src[i] == 't' { + kind = 't' + dst = append(dst, style.True[0]...) + } else if src[i] == 'f' { + kind = 'f' + dst = append(dst, style.False[0]...) + } else if src[i] == 'n' { + kind = 'n' + dst = append(dst, style.Null[0]...) + } else { + dst = apnd(dst, src[i]) + } + if kind != 0 { + for ; i < len(src); i++ { + if src[i] <= ' ' || src[i] == ',' || src[i] == ':' || src[i] == ']' || src[i] == '}' { + i-- + break + } + dst = apnd(dst, src[i]) + } + if kind == '0' { + dst = append(dst, style.Number[1]...) + } else if kind == 't' { + dst = append(dst, style.True[1]...) + } else if kind == 'f' { + dst = append(dst, style.False[1]...) + } else if kind == 'n' { + dst = append(dst, style.Null[1]...) + } + } + } + } + return dst +} + +// Spec strips out comments and trailing commas and convert the input to a +// valid JSON per the official spec: https://tools.ietf.org/html/rfc8259 +// +// The resulting JSON will always be the same length as the input and it will +// include all of the same line breaks at matching offsets. This is to ensure +// the result can be later processed by a external parser and that that +// parser will report messages or errors with the correct offsets. +func Spec(src []byte) []byte { + return spec(src, nil) +} + +// SpecInPlace is the same as Spec, but this method reuses the input json +// buffer to avoid allocations. Do not use the original bytes slice upon return. +func SpecInPlace(src []byte) []byte { + return spec(src, src) +} + +func spec(src, dst []byte) []byte { + dst = dst[:0] + for i := 0; i < len(src); i++ { + if src[i] == '/' { + if i < len(src)-1 { + if src[i+1] == '/' { + dst = append(dst, ' ', ' ') + i += 2 + for ; i < len(src); i++ { + if src[i] == '\n' { + dst = append(dst, '\n') + break + } else if src[i] == '\t' || src[i] == '\r' { + dst = append(dst, src[i]) + } else { + dst = append(dst, ' ') + } + } + continue + } + if src[i+1] == '*' { + dst = append(dst, ' ', ' ') + i += 2 + for ; i < len(src)-1; i++ { + if src[i] == '*' && src[i+1] == '/' { + dst = append(dst, ' ', ' ') + i++ + break + } else if src[i] == '\n' || src[i] == '\t' || + src[i] == '\r' { + dst = append(dst, src[i]) + } else { + dst = append(dst, ' ') + } + } + continue + } + } + } + dst = append(dst, src[i]) + if src[i] == '"' { + for i = i + 1; i < len(src); i++ { + dst = append(dst, src[i]) + if src[i] == '"' { + j := i - 1 + for ; ; j-- { + if src[j] != '\\' { + break + } + } + if (j-i)%2 != 0 { + break + } + } + } + } else if src[i] == '}' || src[i] == ']' { + for j := len(dst) - 2; j >= 0; j-- { + if dst[j] <= ' ' { + continue + } + if dst[j] == ',' { + dst[j] = ' ' + } + break + } + } + } + return dst +} diff --git a/vendor/github.com/tidwall/sjson/LICENSE b/vendor/github.com/tidwall/sjson/LICENSE new file mode 100644 index 00000000..89593c7c --- /dev/null +++ b/vendor/github.com/tidwall/sjson/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Josh Baker + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/vendor/github.com/tidwall/sjson/README.md b/vendor/github.com/tidwall/sjson/README.md new file mode 100644 index 00000000..4598424e --- /dev/null +++ b/vendor/github.com/tidwall/sjson/README.md @@ -0,0 +1,278 @@ +<p align="center"> +<img + src="logo.png" + width="240" height="78" border="0" alt="SJSON"> +<br> +<a href="https://godoc.org/github.com/tidwall/sjson"><img src="https://img.shields.io/badge/api-reference-blue.svg?style=flat-square" alt="GoDoc"></a> +</p> + +<p align="center">set a json value quickly</p> + +SJSON is a Go package that provides a [very fast](#performance) and simple way to set a value in a json document. +For quickly retrieving json values check out [GJSON](https://github.com/tidwall/gjson). + +For a command line interface check out [JJ](https://github.com/tidwall/jj). + +Getting Started +=============== + +Installing +---------- + +To start using SJSON, install Go and run `go get`: + +```sh +$ go get -u github.com/tidwall/sjson +``` + +This will retrieve the library. + +Set a value +----------- +Set sets the value for the specified path. +A path is in dot syntax, such as "name.last" or "age". +This function expects that the json is well-formed and validated. +Invalid json will not panic, but it may return back unexpected results. +Invalid paths may return an error. + +```go +package main + +import "github.com/tidwall/sjson" + +const json = `{"name":{"first":"Janet","last":"Prichard"},"age":47}` + +func main() { + value, _ := sjson.Set(json, "name.last", "Anderson") + println(value) +} +``` + +This will print: + +```json +{"name":{"first":"Janet","last":"Anderson"},"age":47} +``` + +Path syntax +----------- + +A path is a series of keys separated by a dot. +The dot and colon characters can be escaped with ``\``. + +```json +{ + "name": {"first": "Tom", "last": "Anderson"}, + "age":37, + "children": ["Sara","Alex","Jack"], + "fav.movie": "Deer Hunter", + "friends": [ + {"first": "James", "last": "Murphy"}, + {"first": "Roger", "last": "Craig"} + ] +} +``` +``` +"name.last" >> "Anderson" +"age" >> 37 +"children.1" >> "Alex" +"friends.1.last" >> "Craig" +``` + +The `-1` key can be used to append a value to an existing array: + +``` +"children.-1" >> appends a new value to the end of the children array +``` + +Normally number keys are used to modify arrays, but it's possible to force a numeric object key by using the colon character: + +```json +{ + "users":{ + "2313":{"name":"Sara"}, + "7839":{"name":"Andy"} + } +} +``` + +A colon path would look like: + +``` +"users.:2313.name" >> "Sara" +``` + +Supported types +--------------- + +Pretty much any type is supported: + +```go +sjson.Set(`{"key":true}`, "key", nil) +sjson.Set(`{"key":true}`, "key", false) +sjson.Set(`{"key":true}`, "key", 1) +sjson.Set(`{"key":true}`, "key", 10.5) +sjson.Set(`{"key":true}`, "key", "hello") +sjson.Set(`{"key":true}`, "key", []string{"hello", "world"}) +sjson.Set(`{"key":true}`, "key", map[string]interface{}{"hello":"world"}) +``` + +When a type is not recognized, SJSON will fallback to the `encoding/json` Marshaller. + + +Examples +-------- + +Set a value from empty document: +```go +value, _ := sjson.Set("", "name", "Tom") +println(value) + +// Output: +// {"name":"Tom"} +``` + +Set a nested value from empty document: +```go +value, _ := sjson.Set("", "name.last", "Anderson") +println(value) + +// Output: +// {"name":{"last":"Anderson"}} +``` + +Set a new value: +```go +value, _ := sjson.Set(`{"name":{"last":"Anderson"}}`, "name.first", "Sara") +println(value) + +// Output: +// {"name":{"first":"Sara","last":"Anderson"}} +``` + +Update an existing value: +```go +value, _ := sjson.Set(`{"name":{"last":"Anderson"}}`, "name.last", "Smith") +println(value) + +// Output: +// {"name":{"last":"Smith"}} +``` + +Set a new array value: +```go +value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.2", "Sara") +println(value) + +// Output: +// {"friends":["Andy","Carol","Sara"] +``` + +Append an array value by using the `-1` key in a path: +```go +value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.-1", "Sara") +println(value) + +// Output: +// {"friends":["Andy","Carol","Sara"] +``` + +Append an array value that is past the end: +```go +value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.4", "Sara") +println(value) + +// Output: +// {"friends":["Andy","Carol",null,null,"Sara"] +``` + +Delete a value: +```go +value, _ := sjson.Delete(`{"name":{"first":"Sara","last":"Anderson"}}`, "name.first") +println(value) + +// Output: +// {"name":{"last":"Anderson"}} +``` + +Delete an array value: +```go +value, _ := sjson.Delete(`{"friends":["Andy","Carol"]}`, "friends.1") +println(value) + +// Output: +// {"friends":["Andy"]} +``` + +Delete the last array value: +```go +value, _ := sjson.Delete(`{"friends":["Andy","Carol"]}`, "friends.-1") +println(value) + +// Output: +// {"friends":["Andy"]} +``` + +## Performance + +Benchmarks of SJSON alongside [encoding/json](https://golang.org/pkg/encoding/json/), +[ffjson](https://github.com/pquerna/ffjson), +[EasyJSON](https://github.com/mailru/easyjson), +and [Gabs](https://github.com/Jeffail/gabs) + +``` +Benchmark_SJSON-8 3000000 805 ns/op 1077 B/op 3 allocs/op +Benchmark_SJSON_ReplaceInPlace-8 3000000 449 ns/op 0 B/op 0 allocs/op +Benchmark_JSON_Map-8 300000 21236 ns/op 6392 B/op 150 allocs/op +Benchmark_JSON_Struct-8 300000 14691 ns/op 1789 B/op 24 allocs/op +Benchmark_Gabs-8 300000 21311 ns/op 6752 B/op 150 allocs/op +Benchmark_FFJSON-8 300000 17673 ns/op 3589 B/op 47 allocs/op +Benchmark_EasyJSON-8 1500000 3119 ns/op 1061 B/op 13 allocs/op +``` + +JSON document used: + +```json +{ + "widget": { + "debug": "on", + "window": { + "title": "Sample Konfabulator Widget", + "name": "main_window", + "width": 500, + "height": 500 + }, + "image": { + "src": "Images/Sun.png", + "hOffset": 250, + "vOffset": 250, + "alignment": "center" + }, + "text": { + "data": "Click Here", + "size": 36, + "style": "bold", + "vOffset": 100, + "alignment": "center", + "onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;" + } + } +} +``` + +Each operation was rotated though one of the following search paths: + +``` +widget.window.name +widget.image.hOffset +widget.text.onMouseUp +``` + +*These benchmarks were run on a MacBook Pro 15" 2.8 GHz Intel Core i7 using Go 1.7 and can be be found [here](https://github.com/tidwall/sjson-benchmarks)*. + +## Contact +Josh Baker [@tidwall](http://twitter.com/tidwall) + +## License + +SJSON source code is available under the MIT [License](/LICENSE). diff --git a/vendor/github.com/tidwall/sjson/logo.png b/vendor/github.com/tidwall/sjson/logo.png Binary files differnew file mode 100644 index 00000000..b5aa257b --- /dev/null +++ b/vendor/github.com/tidwall/sjson/logo.png diff --git a/vendor/github.com/tidwall/sjson/sjson.go b/vendor/github.com/tidwall/sjson/sjson.go new file mode 100644 index 00000000..a55eef3f --- /dev/null +++ b/vendor/github.com/tidwall/sjson/sjson.go @@ -0,0 +1,737 @@ +// Package sjson provides setting json values. +package sjson + +import ( + jsongo "encoding/json" + "sort" + "strconv" + "unsafe" + + "github.com/tidwall/gjson" +) + +type errorType struct { + msg string +} + +func (err *errorType) Error() string { + return err.msg +} + +// Options represents additional options for the Set and Delete functions. +type Options struct { + // Optimistic is a hint that the value likely exists which + // allows for the sjson to perform a fast-track search and replace. + Optimistic bool + // ReplaceInPlace is a hint to replace the input json rather than + // allocate a new json byte slice. When this field is specified + // the input json will not longer be valid and it should not be used + // In the case when the destination slice doesn't have enough free + // bytes to replace the data in place, a new bytes slice will be + // created under the hood. + // The Optimistic flag must be set to true and the input must be a + // byte slice in order to use this field. + ReplaceInPlace bool +} + +type pathResult struct { + part string // current key part + gpart string // gjson get part + path string // remaining path + force bool // force a string key + more bool // there is more path to parse +} + +func isSimpleChar(ch byte) bool { + switch ch { + case '|', '#', '@', '*', '?': + return false + default: + return true + } +} + +func parsePath(path string) (res pathResult, simple bool) { + var r pathResult + if len(path) > 0 && path[0] == ':' { + r.force = true + path = path[1:] + } + for i := 0; i < len(path); i++ { + if path[i] == '.' { + r.part = path[:i] + r.gpart = path[:i] + r.path = path[i+1:] + r.more = true + return r, true + } + if !isSimpleChar(path[i]) { + return r, false + } + if path[i] == '\\' { + // go into escape mode. this is a slower path that + // strips off the escape character from the part. + epart := []byte(path[:i]) + gpart := []byte(path[:i+1]) + i++ + if i < len(path) { + epart = append(epart, path[i]) + gpart = append(gpart, path[i]) + i++ + for ; i < len(path); i++ { + if path[i] == '\\' { + gpart = append(gpart, '\\') + i++ + if i < len(path) { + epart = append(epart, path[i]) + gpart = append(gpart, path[i]) + } + continue + } else if path[i] == '.' { + r.part = string(epart) + r.gpart = string(gpart) + r.path = path[i+1:] + r.more = true + return r, true + } else if !isSimpleChar(path[i]) { + return r, false + } + epart = append(epart, path[i]) + gpart = append(gpart, path[i]) + } + } + // append the last part + r.part = string(epart) + r.gpart = string(gpart) + return r, true + } + } + r.part = path + r.gpart = path + return r, true +} + +func mustMarshalString(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] < ' ' || s[i] > 0x7f || s[i] == '"' || s[i] == '\\' { + return true + } + } + return false +} + +// appendStringify makes a json string and appends to buf. +func appendStringify(buf []byte, s string) []byte { + if mustMarshalString(s) { + b, _ := jsongo.Marshal(s) + return append(buf, b...) + } + buf = append(buf, '"') + buf = append(buf, s...) + buf = append(buf, '"') + return buf +} + +// appendBuild builds a json block from a json path. +func appendBuild(buf []byte, array bool, paths []pathResult, raw string, + stringify bool) []byte { + if !array { + buf = appendStringify(buf, paths[0].part) + buf = append(buf, ':') + } + if len(paths) > 1 { + n, numeric := atoui(paths[1]) + if numeric || (!paths[1].force && paths[1].part == "-1") { + buf = append(buf, '[') + buf = appendRepeat(buf, "null,", n) + buf = appendBuild(buf, true, paths[1:], raw, stringify) + buf = append(buf, ']') + } else { + buf = append(buf, '{') + buf = appendBuild(buf, false, paths[1:], raw, stringify) + buf = append(buf, '}') + } + } else { + if stringify { + buf = appendStringify(buf, raw) + } else { + buf = append(buf, raw...) + } + } + return buf +} + +// atoui does a rip conversion of string -> unigned int. +func atoui(r pathResult) (n int, ok bool) { + if r.force { + return 0, false + } + for i := 0; i < len(r.part); i++ { + if r.part[i] < '0' || r.part[i] > '9' { + return 0, false + } + n = n*10 + int(r.part[i]-'0') + } + return n, true +} + +// appendRepeat repeats string "n" times and appends to buf. +func appendRepeat(buf []byte, s string, n int) []byte { + for i := 0; i < n; i++ { + buf = append(buf, s...) + } + return buf +} + +// trim does a rip trim +func trim(s string) string { + for len(s) > 0 { + if s[0] <= ' ' { + s = s[1:] + continue + } + break + } + for len(s) > 0 { + if s[len(s)-1] <= ' ' { + s = s[:len(s)-1] + continue + } + break + } + return s +} + +// deleteTailItem deletes the previous key or comma. +func deleteTailItem(buf []byte) ([]byte, bool) { +loop: + for i := len(buf) - 1; i >= 0; i-- { + // look for either a ',',':','[' + switch buf[i] { + case '[': + return buf, true + case ',': + return buf[:i], false + case ':': + // delete tail string + i-- + for ; i >= 0; i-- { + if buf[i] == '"' { + i-- + for ; i >= 0; i-- { + if buf[i] == '"' { + i-- + if i >= 0 && buf[i] == '\\' { + i-- + continue + } + for ; i >= 0; i-- { + // look for either a ',','{' + switch buf[i] { + case '{': + return buf[:i+1], true + case ',': + return buf[:i], false + } + } + } + } + break + } + } + break loop + } + } + return buf, false +} + +var errNoChange = &errorType{"no change"} + +func appendRawPaths(buf []byte, jstr string, paths []pathResult, raw string, + stringify, del bool) ([]byte, error) { + var err error + var res gjson.Result + var found bool + if del { + if paths[0].part == "-1" && !paths[0].force { + res = gjson.Get(jstr, "#") + if res.Int() > 0 { + res = gjson.Get(jstr, strconv.FormatInt(int64(res.Int()-1), 10)) + found = true + } + } + } + if !found { + res = gjson.Get(jstr, paths[0].gpart) + } + if res.Index > 0 { + if len(paths) > 1 { + buf = append(buf, jstr[:res.Index]...) + buf, err = appendRawPaths(buf, res.Raw, paths[1:], raw, + stringify, del) + if err != nil { + return nil, err + } + buf = append(buf, jstr[res.Index+len(res.Raw):]...) + return buf, nil + } + buf = append(buf, jstr[:res.Index]...) + var exidx int // additional forward stripping + if del { + var delNextComma bool + buf, delNextComma = deleteTailItem(buf) + if delNextComma { + i, j := res.Index+len(res.Raw), 0 + for ; i < len(jstr); i, j = i+1, j+1 { + if jstr[i] <= ' ' { + continue + } + if jstr[i] == ',' { + exidx = j + 1 + } + break + } + } + } else { + if stringify { + buf = appendStringify(buf, raw) + } else { + buf = append(buf, raw...) + } + } + buf = append(buf, jstr[res.Index+len(res.Raw)+exidx:]...) + return buf, nil + } + if del { + return nil, errNoChange + } + n, numeric := atoui(paths[0]) + isempty := true + for i := 0; i < len(jstr); i++ { + if jstr[i] > ' ' { + isempty = false + break + } + } + if isempty { + if numeric { + jstr = "[]" + } else { + jstr = "{}" + } + } + jsres := gjson.Parse(jstr) + if jsres.Type != gjson.JSON { + if numeric { + jstr = "[]" + } else { + jstr = "{}" + } + jsres = gjson.Parse(jstr) + } + var comma bool + for i := 1; i < len(jsres.Raw); i++ { + if jsres.Raw[i] <= ' ' { + continue + } + if jsres.Raw[i] == '}' || jsres.Raw[i] == ']' { + break + } + comma = true + break + } + switch jsres.Raw[0] { + default: + return nil, &errorType{"json must be an object or array"} + case '{': + end := len(jsres.Raw) - 1 + for ; end > 0; end-- { + if jsres.Raw[end] == '}' { + break + } + } + buf = append(buf, jsres.Raw[:end]...) + if comma { + buf = append(buf, ',') + } + buf = appendBuild(buf, false, paths, raw, stringify) + buf = append(buf, '}') + return buf, nil + case '[': + var appendit bool + if !numeric { + if paths[0].part == "-1" && !paths[0].force { + appendit = true + } else { + return nil, &errorType{ + "cannot set array element for non-numeric key '" + + paths[0].part + "'"} + } + } + if appendit { + njson := trim(jsres.Raw) + if njson[len(njson)-1] == ']' { + njson = njson[:len(njson)-1] + } + buf = append(buf, njson...) + if comma { + buf = append(buf, ',') + } + + buf = appendBuild(buf, true, paths, raw, stringify) + buf = append(buf, ']') + return buf, nil + } + buf = append(buf, '[') + ress := jsres.Array() + for i := 0; i < len(ress); i++ { + if i > 0 { + buf = append(buf, ',') + } + buf = append(buf, ress[i].Raw...) + } + if len(ress) == 0 { + buf = appendRepeat(buf, "null,", n-len(ress)) + } else { + buf = appendRepeat(buf, ",null", n-len(ress)) + if comma { + buf = append(buf, ',') + } + } + buf = appendBuild(buf, true, paths, raw, stringify) + buf = append(buf, ']') + return buf, nil + } +} + +func isOptimisticPath(path string) bool { + for i := 0; i < len(path); i++ { + if path[i] < '.' || path[i] > 'z' { + return false + } + if path[i] > '9' && path[i] < 'A' { + return false + } + if path[i] > 'z' { + return false + } + } + return true +} + +// Set sets a json value for the specified path. +// A path is in dot syntax, such as "name.last" or "age". +// This function expects that the json is well-formed, and does not validate. +// Invalid json will not panic, but it may return back unexpected results. +// An error is returned if the path is not valid. +// +// A path is a series of keys separated by a dot. +// +// { +// "name": {"first": "Tom", "last": "Anderson"}, +// "age":37, +// "children": ["Sara","Alex","Jack"], +// "friends": [ +// {"first": "James", "last": "Murphy"}, +// {"first": "Roger", "last": "Craig"} +// ] +// } +// "name.last" >> "Anderson" +// "age" >> 37 +// "children.1" >> "Alex" +// +func Set(json, path string, value interface{}) (string, error) { + return SetOptions(json, path, value, nil) +} + +// SetBytes sets a json value for the specified path. +// If working with bytes, this method preferred over +// Set(string(data), path, value) +func SetBytes(json []byte, path string, value interface{}) ([]byte, error) { + return SetBytesOptions(json, path, value, nil) +} + +// SetRaw sets a raw json value for the specified path. +// This function works the same as Set except that the value is set as a +// raw block of json. This allows for setting premarshalled json objects. +func SetRaw(json, path, value string) (string, error) { + return SetRawOptions(json, path, value, nil) +} + +// SetRawOptions sets a raw json value for the specified path with options. +// This furnction works the same as SetOptions except that the value is set +// as a raw block of json. This allows for setting premarshalled json objects. +func SetRawOptions(json, path, value string, opts *Options) (string, error) { + var optimistic bool + if opts != nil { + optimistic = opts.Optimistic + } + res, err := set(json, path, value, false, false, optimistic, false) + if err == errNoChange { + return json, nil + } + return string(res), err +} + +// SetRawBytes sets a raw json value for the specified path. +// If working with bytes, this method preferred over +// SetRaw(string(data), path, value) +func SetRawBytes(json []byte, path string, value []byte) ([]byte, error) { + return SetRawBytesOptions(json, path, value, nil) +} + +type dtype struct{} + +// Delete deletes a value from json for the specified path. +func Delete(json, path string) (string, error) { + return Set(json, path, dtype{}) +} + +// DeleteBytes deletes a value from json for the specified path. +func DeleteBytes(json []byte, path string) ([]byte, error) { + return SetBytes(json, path, dtype{}) +} + +type stringHeader struct { + data unsafe.Pointer + len int +} + +type sliceHeader struct { + data unsafe.Pointer + len int + cap int +} + +func set(jstr, path, raw string, + stringify, del, optimistic, inplace bool) ([]byte, error) { + if path == "" { + return []byte(jstr), &errorType{"path cannot be empty"} + } + if !del && optimistic && isOptimisticPath(path) { + res := gjson.Get(jstr, path) + if res.Exists() && res.Index > 0 { + sz := len(jstr) - len(res.Raw) + len(raw) + if stringify { + sz += 2 + } + if inplace && sz <= len(jstr) { + if !stringify || !mustMarshalString(raw) { + jsonh := *(*stringHeader)(unsafe.Pointer(&jstr)) + jsonbh := sliceHeader{ + data: jsonh.data, len: jsonh.len, cap: jsonh.len} + jbytes := *(*[]byte)(unsafe.Pointer(&jsonbh)) + if stringify { + jbytes[res.Index] = '"' + copy(jbytes[res.Index+1:], []byte(raw)) + jbytes[res.Index+1+len(raw)] = '"' + copy(jbytes[res.Index+1+len(raw)+1:], + jbytes[res.Index+len(res.Raw):]) + } else { + copy(jbytes[res.Index:], []byte(raw)) + copy(jbytes[res.Index+len(raw):], + jbytes[res.Index+len(res.Raw):]) + } + return jbytes[:sz], nil + } + return []byte(jstr), nil + } + buf := make([]byte, 0, sz) + buf = append(buf, jstr[:res.Index]...) + if stringify { + buf = appendStringify(buf, raw) + } else { + buf = append(buf, raw...) + } + buf = append(buf, jstr[res.Index+len(res.Raw):]...) + return buf, nil + } + } + var paths []pathResult + r, simple := parsePath(path) + if simple { + paths = append(paths, r) + for r.more { + r, simple = parsePath(r.path) + if !simple { + break + } + paths = append(paths, r) + } + } + if !simple { + if del { + return []byte(jstr), + &errorType{"cannot delete value from a complex path"} + } + return setComplexPath(jstr, path, raw, stringify) + } + njson, err := appendRawPaths(nil, jstr, paths, raw, stringify, del) + if err != nil { + return []byte(jstr), err + } + return njson, nil +} + +func setComplexPath(jstr, path, raw string, stringify bool) ([]byte, error) { + res := gjson.Get(jstr, path) + if !res.Exists() || !(res.Index != 0 || len(res.Indexes) != 0) { + return []byte(jstr), errNoChange + } + if res.Index != 0 { + njson := []byte(jstr[:res.Index]) + if stringify { + njson = appendStringify(njson, raw) + } else { + njson = append(njson, raw...) + } + njson = append(njson, jstr[res.Index+len(res.Raw):]...) + jstr = string(njson) + } + if len(res.Indexes) > 0 { + type val struct { + index int + res gjson.Result + } + vals := make([]val, 0, len(res.Indexes)) + res.ForEach(func(_, vres gjson.Result) bool { + vals = append(vals, val{res: vres}) + return true + }) + if len(res.Indexes) != len(vals) { + return []byte(jstr), errNoChange + } + for i := 0; i < len(res.Indexes); i++ { + vals[i].index = res.Indexes[i] + } + sort.SliceStable(vals, func(i, j int) bool { + return vals[i].index > vals[j].index + }) + for _, val := range vals { + vres := val.res + index := val.index + njson := []byte(jstr[:index]) + if stringify { + njson = appendStringify(njson, raw) + } else { + njson = append(njson, raw...) + } + njson = append(njson, jstr[index+len(vres.Raw):]...) + jstr = string(njson) + } + } + return []byte(jstr), nil +} + +// SetOptions sets a json value for the specified path with options. +// A path is in dot syntax, such as "name.last" or "age". +// This function expects that the json is well-formed, and does not validate. +// Invalid json will not panic, but it may return back unexpected results. +// An error is returned if the path is not valid. +func SetOptions(json, path string, value interface{}, + opts *Options) (string, error) { + if opts != nil { + if opts.ReplaceInPlace { + // it's not safe to replace bytes in-place for strings + // copy the Options and set options.ReplaceInPlace to false. + nopts := *opts + opts = &nopts + opts.ReplaceInPlace = false + } + } + jsonh := *(*stringHeader)(unsafe.Pointer(&json)) + jsonbh := sliceHeader{data: jsonh.data, len: jsonh.len, cap: jsonh.len} + jsonb := *(*[]byte)(unsafe.Pointer(&jsonbh)) + res, err := SetBytesOptions(jsonb, path, value, opts) + return string(res), err +} + +// SetBytesOptions sets a json value for the specified path with options. +// If working with bytes, this method preferred over +// SetOptions(string(data), path, value) +func SetBytesOptions(json []byte, path string, value interface{}, + opts *Options) ([]byte, error) { + var optimistic, inplace bool + if opts != nil { + optimistic = opts.Optimistic + inplace = opts.ReplaceInPlace + } + jstr := *(*string)(unsafe.Pointer(&json)) + var res []byte + var err error + switch v := value.(type) { + default: + b, merr := jsongo.Marshal(value) + if merr != nil { + return nil, merr + } + raw := *(*string)(unsafe.Pointer(&b)) + res, err = set(jstr, path, raw, false, false, optimistic, inplace) + case dtype: + res, err = set(jstr, path, "", false, true, optimistic, inplace) + case string: + res, err = set(jstr, path, v, true, false, optimistic, inplace) + case []byte: + raw := *(*string)(unsafe.Pointer(&v)) + res, err = set(jstr, path, raw, true, false, optimistic, inplace) + case bool: + if v { + res, err = set(jstr, path, "true", false, false, optimistic, inplace) + } else { + res, err = set(jstr, path, "false", false, false, optimistic, inplace) + } + case int8: + res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), + false, false, optimistic, inplace) + case int16: + res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), + false, false, optimistic, inplace) + case int32: + res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), + false, false, optimistic, inplace) + case int64: + res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), + false, false, optimistic, inplace) + case uint8: + res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), + false, false, optimistic, inplace) + case uint16: + res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), + false, false, optimistic, inplace) + case uint32: + res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), + false, false, optimistic, inplace) + case uint64: + res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), + false, false, optimistic, inplace) + case float32: + res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64), + false, false, optimistic, inplace) + case float64: + res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64), + false, false, optimistic, inplace) + } + if err == errNoChange { + return json, nil + } + return res, err +} + +// SetRawBytesOptions sets a raw json value for the specified path with options. +// If working with bytes, this method preferred over +// SetRawOptions(string(data), path, value, opts) +func SetRawBytesOptions(json []byte, path string, value []byte, + opts *Options) ([]byte, error) { + jstr := *(*string)(unsafe.Pointer(&json)) + vstr := *(*string)(unsafe.Pointer(&value)) + var optimistic, inplace bool + if opts != nil { + optimistic = opts.Optimistic + inplace = opts.ReplaceInPlace + } + res, err := set(jstr, path, vstr, false, false, optimistic, inplace) + if err == errNoChange { + return json, nil + } + return res, err +} diff --git a/vendor/gopkg.in/natefinch/lumberjack.v2/.travis.yml b/vendor/gopkg.in/natefinch/lumberjack.v2/.travis.yml index 65dcbc56..21166f5c 100644 --- a/vendor/gopkg.in/natefinch/lumberjack.v2/.travis.yml +++ b/vendor/gopkg.in/natefinch/lumberjack.v2/.travis.yml @@ -1,6 +1,11 @@ language: go go: - - 1.8 - - 1.7 - - 1.6
\ No newline at end of file + - tip + - 1.15.x + - 1.14.x + - 1.13.x + - 1.12.x + +env: + - GO111MODULE=on diff --git a/vendor/gopkg.in/natefinch/lumberjack.v2/chown_linux.go b/vendor/gopkg.in/natefinch/lumberjack.v2/chown_linux.go index 2758ec9c..465f5692 100644 --- a/vendor/gopkg.in/natefinch/lumberjack.v2/chown_linux.go +++ b/vendor/gopkg.in/natefinch/lumberjack.v2/chown_linux.go @@ -5,8 +5,8 @@ import ( "syscall" ) -// os_Chown is a var so we can mock it out during tests. -var os_Chown = os.Chown +// osChown is a var so we can mock it out during tests. +var osChown = os.Chown func chown(name string, info os.FileInfo) error { f, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode()) @@ -15,5 +15,5 @@ func chown(name string, info os.FileInfo) error { } f.Close() stat := info.Sys().(*syscall.Stat_t) - return os_Chown(name, int(stat.Uid), int(stat.Gid)) + return osChown(name, int(stat.Uid), int(stat.Gid)) } diff --git a/vendor/gopkg.in/natefinch/lumberjack.v2/lumberjack.go b/vendor/gopkg.in/natefinch/lumberjack.v2/lumberjack.go index 46d97c55..3447cdc0 100644 --- a/vendor/gopkg.in/natefinch/lumberjack.v2/lumberjack.go +++ b/vendor/gopkg.in/natefinch/lumberjack.v2/lumberjack.go @@ -120,7 +120,7 @@ var ( currentTime = time.Now // os_Stat exists so it can be mocked out by tests. - os_Stat = os.Stat + osStat = os.Stat // megabyte is the conversion factor between MaxSize and bytes. It is a // variable so tests can mock it out and not need to write megabytes of data @@ -206,14 +206,14 @@ func (l *Logger) rotate() error { // openNew opens a new log file for writing, moving any old log file out of the // way. This methods assumes the file has already been closed. func (l *Logger) openNew() error { - err := os.MkdirAll(l.dir(), 0744) + err := os.MkdirAll(l.dir(), 0755) if err != nil { return fmt.Errorf("can't make directories for new logfile: %s", err) } name := l.filename() - mode := os.FileMode(0644) - info, err := os_Stat(name) + mode := os.FileMode(0600) + info, err := osStat(name) if err == nil { // Copy the mode off the old logfile. mode = info.Mode() @@ -265,7 +265,7 @@ func (l *Logger) openExistingOrNew(writeLen int) error { l.mill() filename := l.filename() - info, err := os_Stat(filename) + info, err := osStat(filename) if os.IsNotExist(err) { return l.openNew() } @@ -288,7 +288,7 @@ func (l *Logger) openExistingOrNew(writeLen int) error { return nil } -// genFilename generates the name of the logfile from the current time. +// filename generates the name of the logfile from the current time. func (l *Logger) filename() string { if l.Filename != "" { return l.Filename @@ -376,7 +376,7 @@ func (l *Logger) millRunOnce() error { // millRun runs in a goroutine to manage post-rotation compression and removal // of old log files. func (l *Logger) millRun() { - for _ = range l.millCh { + for range l.millCh { // what am I going to do, log this? _ = l.millRunOnce() } @@ -472,7 +472,7 @@ func compressLogFile(src, dst string) (err error) { } defer f.Close() - fi, err := os_Stat(src) + fi, err := osStat(src) if err != nil { return fmt.Errorf("failed to stat log file: %v", err) } diff --git a/vendor/maunium.net/go/maulogger/v2/LICENSE b/vendor/maunium.net/go/maulogger/v2/LICENSE new file mode 100644 index 00000000..52d13511 --- /dev/null +++ b/vendor/maunium.net/go/maulogger/v2/LICENSE @@ -0,0 +1,374 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. + diff --git a/vendor/maunium.net/go/maulogger/v2/README.md b/vendor/maunium.net/go/maulogger/v2/README.md new file mode 100644 index 00000000..97db2f58 --- /dev/null +++ b/vendor/maunium.net/go/maulogger/v2/README.md @@ -0,0 +1,6 @@ +# maulogger +A logger in Go. Deprecated in favor of [zerolog](https://github.com/rs/zerolog). + +Utilities for migrating gracefully can be found in the maulogadapt package, +it includes both wrapping a zerolog in the maulogger interface, and wrapping a +maulogger as a zerolog output writer. diff --git a/vendor/maunium.net/go/maulogger/v2/defaults.go b/vendor/maunium.net/go/maulogger/v2/defaults.go new file mode 100644 index 00000000..571e79cc --- /dev/null +++ b/vendor/maunium.net/go/maulogger/v2/defaults.go @@ -0,0 +1,284 @@ +// mauLogger - A logger for Go programs +// Copyright (c) 2016-2021 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package maulogger + +import ( + "os" +) + +// DefaultLogger ... +var DefaultLogger = Create().(*BasicLogger) + +// SetWriter formats the given parts with fmt.Sprint and logs the result with the SetWriter level +func SetWriter(w *os.File) { + DefaultLogger.SetWriter(w) +} + +// OpenFile formats the given parts with fmt.Sprint and logs the result with the OpenFile level +func OpenFile() error { + return DefaultLogger.OpenFile() +} + +// Close formats the given parts with fmt.Sprint and logs the result with the Close level +func Close() error { + return DefaultLogger.Close() +} + +// Sub creates a Sublogger +func Sub(module string) Logger { + return DefaultLogger.Sub(module) +} + +// Raw formats the given parts with fmt.Sprint and logs the result with the Raw level +func Rawm(level Level, metadata map[string]interface{}, module, message string) { + DefaultLogger.Raw(level, metadata, module, message) +} + +func Raw(level Level, module, message string) { + DefaultLogger.Raw(level, map[string]interface{}{}, module, message) +} + +// Log formats the given parts with fmt.Sprint and logs the result with the given level +func Log(level Level, parts ...interface{}) { + DefaultLogger.DefaultSub.Log(level, parts...) +} + +// Logln formats the given parts with fmt.Sprintln and logs the result with the given level +func Logln(level Level, parts ...interface{}) { + DefaultLogger.DefaultSub.Logln(level, parts...) +} + +// Logf formats the given message and args with fmt.Sprintf and logs the result with the given level +func Logf(level Level, message string, args ...interface{}) { + DefaultLogger.DefaultSub.Logf(level, message, args...) +} + +// Logfln formats the given message and args with fmt.Sprintf, appends a newline and logs the result with the given level +func Logfln(level Level, message string, args ...interface{}) { + DefaultLogger.DefaultSub.Logfln(level, message, args...) +} + +// Debug formats the given parts with fmt.Sprint and logs the result with the Debug level +func Debug(parts ...interface{}) { + DefaultLogger.DefaultSub.Debug(parts...) +} + +// Debugln formats the given parts with fmt.Sprintln and logs the result with the Debug level +func Debugln(parts ...interface{}) { + DefaultLogger.DefaultSub.Debugln(parts...) +} + +// Debugf formats the given message and args with fmt.Sprintf and logs the result with the Debug level +func Debugf(message string, args ...interface{}) { + DefaultLogger.DefaultSub.Debugf(message, args...) +} + +// Debugfln formats the given message and args with fmt.Sprintf, appends a newline and logs the result with the Debug level +func Debugfln(message string, args ...interface{}) { + DefaultLogger.DefaultSub.Debugfln(message, args...) +} + +// Info formats the given parts with fmt.Sprint and logs the result with the Info level +func Info(parts ...interface{}) { + DefaultLogger.DefaultSub.Info(parts...) +} + +// Infoln formats the given parts with fmt.Sprintln and logs the result with the Info level +func Infoln(parts ...interface{}) { + DefaultLogger.DefaultSub.Infoln(parts...) +} + +// Infof formats the given message and args with fmt.Sprintf and logs the result with the Info level +func Infof(message string, args ...interface{}) { + DefaultLogger.DefaultSub.Infof(message, args...) +} + +// Infofln formats the given message and args with fmt.Sprintf, appends a newline and logs the result with the Info level +func Infofln(message string, args ...interface{}) { + DefaultLogger.DefaultSub.Infofln(message, args...) +} + +// Warn formats the given parts with fmt.Sprint and logs the result with the Warn level +func Warn(parts ...interface{}) { + DefaultLogger.DefaultSub.Warn(parts...) +} + +// Warnln formats the given parts with fmt.Sprintln and logs the result with the Warn level +func Warnln(parts ...interface{}) { + DefaultLogger.DefaultSub.Warnln(parts...) +} + +// Warnf formats the given message and args with fmt.Sprintf and logs the result with the Warn level +func Warnf(message string, args ...interface{}) { + DefaultLogger.DefaultSub.Warnf(message, args...) +} + +// Warnfln formats the given message and args with fmt.Sprintf, appends a newline and logs the result with the Warn level +func Warnfln(message string, args ...interface{}) { + DefaultLogger.DefaultSub.Warnfln(message, args...) +} + +// Error formats the given parts with fmt.Sprint and logs the result with the Error level +func Error(parts ...interface{}) { + DefaultLogger.DefaultSub.Error(parts...) +} + +// Errorln formats the given parts with fmt.Sprintln and logs the result with the Error level +func Errorln(parts ...interface{}) { + DefaultLogger.DefaultSub.Errorln(parts...) +} + +// Errorf formats the given message and args with fmt.Sprintf and logs the result with the Error level +func Errorf(message string, args ...interface{}) { + DefaultLogger.DefaultSub.Errorf(message, args...) +} + +// Errorfln formats the given message and args with fmt.Sprintf, appends a newline and logs the result with the Error level +func Errorfln(message string, args ...interface{}) { + DefaultLogger.DefaultSub.Errorfln(message, args...) +} + +// Fatal formats the given parts with fmt.Sprint and logs the result with the Fatal level +func Fatal(parts ...interface{}) { + DefaultLogger.DefaultSub.Fatal(parts...) +} + +// Fatalln formats the given parts with fmt.Sprintln and logs the result with the Fatal level +func Fatalln(parts ...interface{}) { + DefaultLogger.DefaultSub.Fatalln(parts...) +} + +// Fatalf formats the given message and args with fmt.Sprintf and logs the result with the Fatal level +func Fatalf(message string, args ...interface{}) { + DefaultLogger.DefaultSub.Fatalf(message, args...) +} + +// Fatalfln formats the given message and args with fmt.Sprintf, appends a newline and logs the result with the Fatal level +func Fatalfln(message string, args ...interface{}) { + DefaultLogger.DefaultSub.Fatalfln(message, args...) +} + +// Log formats the given parts with fmt.Sprint and logs the result with the given level +func (log *BasicLogger) Log(level Level, parts ...interface{}) { + log.DefaultSub.Log(level, parts...) +} + +// Logln formats the given parts with fmt.Sprintln and logs the result with the given level +func (log *BasicLogger) Logln(level Level, parts ...interface{}) { + log.DefaultSub.Logln(level, parts...) +} + +// Logf formats the given message and args with fmt.Sprintf and logs the result with the given level +func (log *BasicLogger) Logf(level Level, message string, args ...interface{}) { + log.DefaultSub.Logf(level, message, args...) +} + +// Logfln formats the given message and args with fmt.Sprintf, appends a newline and logs the result with the given level +func (log *BasicLogger) Logfln(level Level, message string, args ...interface{}) { + log.DefaultSub.Logfln(level, message, args...) +} + +// Debug formats the given parts with fmt.Sprint and logs the result with the Debug level +func (log *BasicLogger) Debug(parts ...interface{}) { + log.DefaultSub.Debug(parts...) +} + +// Debugln formats the given parts with fmt.Sprintln and logs the result with the Debug level +func (log *BasicLogger) Debugln(parts ...interface{}) { + log.DefaultSub.Debugln(parts...) +} + +// Debugf formats the given message and args with fmt.Sprintf and logs the result with the Debug level +func (log *BasicLogger) Debugf(message string, args ...interface{}) { + log.DefaultSub.Debugf(message, args...) +} + +// Debugfln formats the given message and args with fmt.Sprintf, appends a newline and logs the result with the Debug level +func (log *BasicLogger) Debugfln(message string, args ...interface{}) { + log.DefaultSub.Debugfln(message, args...) +} + +// Info formats the given parts with fmt.Sprint and logs the result with the Info level +func (log *BasicLogger) Info(parts ...interface{}) { + log.DefaultSub.Info(parts...) +} + +// Infoln formats the given parts with fmt.Sprintln and logs the result with the Info level +func (log *BasicLogger) Infoln(parts ...interface{}) { + log.DefaultSub.Infoln(parts...) +} + +// Infofln formats the given message and args with fmt.Sprintf, appends a newline and logs the result with the Info level +func (log *BasicLogger) Infofln(message string, args ...interface{}) { + log.DefaultSub.Infofln(message, args...) +} + +// Infof formats the given message and args with fmt.Sprintf and logs the result with the Info level +func (log *BasicLogger) Infof(message string, args ...interface{}) { + log.DefaultSub.Infof(message, args...) +} + +// Warn formats the given parts with fmt.Sprint and logs the result with the Warn level +func (log *BasicLogger) Warn(parts ...interface{}) { + log.DefaultSub.Warn(parts...) +} + +// Warnln formats the given parts with fmt.Sprintln and logs the result with the Warn level +func (log *BasicLogger) Warnln(parts ...interface{}) { + log.DefaultSub.Warnln(parts...) +} + +// Warnfln formats the given message and args with fmt.Sprintf, appends a newline and logs the result with the Warn level +func (log *BasicLogger) Warnfln(message string, args ...interface{}) { + log.DefaultSub.Warnfln(message, args...) +} + +// Warnf formats the given message and args with fmt.Sprintf and logs the result with the Warn level +func (log *BasicLogger) Warnf(message string, args ...interface{}) { + log.DefaultSub.Warnf(message, args...) +} + +// Error formats the given parts with fmt.Sprint and logs the result with the Error level +func (log *BasicLogger) Error(parts ...interface{}) { + log.DefaultSub.Error(parts...) +} + +// Errorln formats the given parts with fmt.Sprintln and logs the result with the Error level +func (log *BasicLogger) Errorln(parts ...interface{}) { + log.DefaultSub.Errorln(parts...) +} + +// Errorf formats the given message and args with fmt.Sprintf and logs the result with the Error level +func (log *BasicLogger) Errorf(message string, args ...interface{}) { + log.DefaultSub.Errorf(message, args...) +} + +// Errorfln formats the given message and args with fmt.Sprintf, appends a newline and logs the result with the Error level +func (log *BasicLogger) Errorfln(message string, args ...interface{}) { + log.DefaultSub.Errorfln(message, args...) +} + +// Fatal formats the given parts with fmt.Sprint and logs the result with the Fatal level +func (log *BasicLogger) Fatal(parts ...interface{}) { + log.DefaultSub.Fatal(parts...) +} + +// Fatalln formats the given parts with fmt.Sprintln and logs the result with the Fatal level +func (log *BasicLogger) Fatalln(parts ...interface{}) { + log.DefaultSub.Fatalln(parts...) +} + +// Fatalf formats the given message and args with fmt.Sprintf and logs the result with the Fatal level +func (log *BasicLogger) Fatalf(message string, args ...interface{}) { + log.DefaultSub.Fatalf(message, args...) +} + +// Fatalfln formats the given message and args with fmt.Sprintf, appends a newline and logs the result with the Fatal level +func (log *BasicLogger) Fatalfln(message string, args ...interface{}) { + log.DefaultSub.Fatalfln(message, args...) +} diff --git a/vendor/maunium.net/go/maulogger/v2/level.go b/vendor/maunium.net/go/maulogger/v2/level.go new file mode 100644 index 00000000..392dccd8 --- /dev/null +++ b/vendor/maunium.net/go/maulogger/v2/level.go @@ -0,0 +1,47 @@ +// mauLogger - A logger for Go programs +// Copyright (c) 2016-2021 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package maulogger + +import ( + "fmt" +) + +// Level is the severity level of a log entry. +type Level struct { + Name string + Severity, Color int +} + +var ( + // LevelDebug is the level for debug messages. + LevelDebug = Level{Name: "DEBUG", Color: -1, Severity: 0} + // LevelInfo is the level for basic log messages. + LevelInfo = Level{Name: "INFO", Color: 36, Severity: 10} + // LevelWarn is the level saying that something went wrong, but the program will continue operating mostly normally. + LevelWarn = Level{Name: "WARN", Color: 33, Severity: 50} + // LevelError is the level saying that something went wrong and the program may not operate as expected, but will still continue. + LevelError = Level{Name: "ERROR", Color: 31, Severity: 100} + // LevelFatal is the level saying that something went wrong and the program will not operate normally. + LevelFatal = Level{Name: "FATAL", Color: 35, Severity: 9001} +) + +// GetColor gets the ANSI escape color code for the log level. +func (lvl Level) GetColor() string { + if lvl.Color < 0 { + return "\x1b[0m" + } + return fmt.Sprintf("\x1b[%dm", lvl.Color) +} + +// GetReset gets the ANSI escape reset code. +func (lvl Level) GetReset() string { + if lvl.Color < 0 { + return "" + } + return "\x1b[0m" +} diff --git a/vendor/maunium.net/go/maulogger/v2/logger.go b/vendor/maunium.net/go/maulogger/v2/logger.go new file mode 100644 index 00000000..61cd1e1d --- /dev/null +++ b/vendor/maunium.net/go/maulogger/v2/logger.go @@ -0,0 +1,224 @@ +// mauLogger - A logger for Go programs +// Copyright (c) 2016-2021 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package maulogger + +import ( + "encoding/json" + "fmt" + "io" + "os" + "strings" + "sync" + "time" +) + +// LoggerFileFormat ... +type LoggerFileFormat func(now string, i int) string + +type BasicLogger struct { + PrintLevel int + FlushLineThreshold int + FileTimeFormat string + FileFormat LoggerFileFormat + TimeFormat string + FileMode os.FileMode + DefaultSub Logger + + JSONFile bool + JSONStdout bool + + stdoutEncoder *json.Encoder + fileEncoder *json.Encoder + + writer *os.File + writerLock sync.Mutex + StdoutLock sync.Mutex + StderrLock sync.Mutex + lines int + + metadata map[string]interface{} +} + +// Logger contains advanced logging functions +type Logger interface { + Sub(module string) Logger + Subm(module string, metadata map[string]interface{}) Logger + WithDefaultLevel(level Level) Logger + GetParent() Logger + + Writer(level Level) io.WriteCloser + + Log(level Level, parts ...interface{}) + Logln(level Level, parts ...interface{}) + Logf(level Level, message string, args ...interface{}) + Logfln(level Level, message string, args ...interface{}) + + Debug(parts ...interface{}) + Debugln(parts ...interface{}) + Debugf(message string, args ...interface{}) + Debugfln(message string, args ...interface{}) + Info(parts ...interface{}) + Infoln(parts ...interface{}) + Infof(message string, args ...interface{}) + Infofln(message string, args ...interface{}) + Warn(parts ...interface{}) + Warnln(parts ...interface{}) + Warnf(message string, args ...interface{}) + Warnfln(message string, args ...interface{}) + Error(parts ...interface{}) + Errorln(parts ...interface{}) + Errorf(message string, args ...interface{}) + Errorfln(message string, args ...interface{}) + Fatal(parts ...interface{}) + Fatalln(parts ...interface{}) + Fatalf(message string, args ...interface{}) + Fatalfln(message string, args ...interface{}) +} + +// Create a Logger +func Createm(metadata map[string]interface{}) Logger { + var log = &BasicLogger{ + PrintLevel: 10, + FileTimeFormat: "2006-01-02", + FileFormat: func(now string, i int) string { return fmt.Sprintf("%[1]s-%02[2]d.log", now, i) }, + TimeFormat: "15:04:05 02.01.2006", + FileMode: 0600, + FlushLineThreshold: 5, + lines: 0, + metadata: metadata, + } + log.DefaultSub = log.Sub("") + return log +} + +func Create() Logger { + return Createm(map[string]interface{}{}) +} + +func (log *BasicLogger) EnableJSONStdout() { + log.JSONStdout = true + log.stdoutEncoder = json.NewEncoder(os.Stdout) +} + +func (log *BasicLogger) GetParent() Logger { + return nil +} + +// SetWriter formats the given parts with fmt.Sprint and logs the result with the SetWriter level +func (log *BasicLogger) SetWriter(w *os.File) { + log.writer = w + if log.JSONFile { + log.fileEncoder = json.NewEncoder(w) + } +} + +// OpenFile formats the given parts with fmt.Sprint and logs the result with the OpenFile level +func (log *BasicLogger) OpenFile() error { + now := time.Now().Format(log.FileTimeFormat) + i := 1 + for ; ; i++ { + if _, err := os.Stat(log.FileFormat(now, i)); os.IsNotExist(err) { + break + } + } + writer, err := os.OpenFile(log.FileFormat(now, i), os.O_WRONLY|os.O_CREATE|os.O_APPEND, log.FileMode) + if err != nil { + return err + } else if writer == nil { + return os.ErrInvalid + } + log.SetWriter(writer) + return nil +} + +// Close formats the given parts with fmt.Sprint and logs the result with the Close level +func (log *BasicLogger) Close() error { + if log.writer != nil { + return log.writer.Close() + } + return nil +} + +type logLine struct { + log *BasicLogger + + Command string `json:"command"` + Time time.Time `json:"time"` + Level string `json:"level"` + Module string `json:"module"` + Message string `json:"message"` + Metadata map[string]interface{} `json:"metadata"` +} + +func (ll logLine) String() string { + if len(ll.Module) == 0 { + return fmt.Sprintf("[%s] [%s] %s", ll.Time.Format(ll.log.TimeFormat), ll.Level, ll.Message) + } else { + return fmt.Sprintf("[%s] [%s/%s] %s", ll.Time.Format(ll.log.TimeFormat), ll.Module, ll.Level, ll.Message) + } +} + +func reduceItem(m1, m2 map[string]interface{}) map[string]interface{} { + m3 := map[string]interface{}{} + + _merge := func(m map[string]interface{}) { + for ia, va := range m { + m3[ia] = va + } + } + + _merge(m1) + _merge(m2) + + return m3 +} + +// Raw formats the given parts with fmt.Sprint and logs the result with the Raw level +func (log *BasicLogger) Raw(level Level, extraMetadata map[string]interface{}, module, origMessage string) { + message := logLine{log, "log", time.Now(), level.Name, module, strings.TrimSpace(origMessage), reduceItem(log.metadata, extraMetadata)} + + if log.writer != nil { + log.writerLock.Lock() + var err error + if log.JSONFile { + err = log.fileEncoder.Encode(&message) + } else { + _, err = log.writer.WriteString(message.String()) + _, _ = log.writer.WriteString("\n") + } + log.writerLock.Unlock() + if err != nil { + log.StderrLock.Lock() + _, _ = os.Stderr.WriteString("Failed to write to log file:") + _, _ = os.Stderr.WriteString(err.Error()) + log.StderrLock.Unlock() + } + } + + if level.Severity >= log.PrintLevel { + if log.JSONStdout { + log.StdoutLock.Lock() + _ = log.stdoutEncoder.Encode(&message) + log.StdoutLock.Unlock() + } else if level.Severity >= LevelError.Severity { + log.StderrLock.Lock() + _, _ = os.Stderr.WriteString(level.GetColor()) + _, _ = os.Stderr.WriteString(message.String()) + _, _ = os.Stderr.WriteString(level.GetReset()) + _, _ = os.Stderr.WriteString("\n") + log.StderrLock.Unlock() + } else { + log.StdoutLock.Lock() + _, _ = os.Stdout.WriteString(level.GetColor()) + _, _ = os.Stdout.WriteString(message.String()) + _, _ = os.Stdout.WriteString(level.GetReset()) + _, _ = os.Stdout.WriteString("\n") + log.StdoutLock.Unlock() + } + } +} diff --git a/vendor/maunium.net/go/maulogger/v2/maulogadapt/mauzerolog.go b/vendor/maunium.net/go/maulogger/v2/maulogadapt/mauzerolog.go new file mode 100644 index 00000000..774c189e --- /dev/null +++ b/vendor/maunium.net/go/maulogger/v2/maulogadapt/mauzerolog.go @@ -0,0 +1,185 @@ +// Copyright (c) 2023 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package maulogadapt + +import ( + "fmt" + "io" + "strings" + + "github.com/rs/zerolog" + + "maunium.net/go/maulogger/v2" +) + +type MauZeroLog struct { + *zerolog.Logger + orig *zerolog.Logger + mod string +} + +func ZeroAsMau(log *zerolog.Logger) maulogger.Logger { + return MauZeroLog{log, log, ""} +} + +var _ maulogger.Logger = (*MauZeroLog)(nil) + +func (m MauZeroLog) Sub(module string) maulogger.Logger { + return m.Subm(module, map[string]interface{}{}) +} + +func (m MauZeroLog) Subm(module string, metadata map[string]interface{}) maulogger.Logger { + if m.mod != "" { + module = fmt.Sprintf("%s/%s", m.mod, module) + } + var orig zerolog.Logger + if m.orig != nil { + orig = *m.orig + } else { + orig = *m.Logger + } + if len(metadata) > 0 { + with := m.orig.With() + for key, value := range metadata { + with = with.Interface(key, value) + } + orig = with.Logger() + } + log := orig.With().Str("module", module).Logger() + return MauZeroLog{&log, &orig, module} +} + +func (m MauZeroLog) WithDefaultLevel(_ maulogger.Level) maulogger.Logger { + return m +} + +func (m MauZeroLog) GetParent() maulogger.Logger { + return nil +} + +type nopWriteCloser struct { + io.Writer +} + +func (nopWriteCloser) Close() error { return nil } + +func (m MauZeroLog) Writer(level maulogger.Level) io.WriteCloser { + return nopWriteCloser{m.Logger.With().Str(zerolog.LevelFieldName, zerolog.LevelFieldMarshalFunc(mauToZeroLevel(level))).Logger()} +} + +func mauToZeroLevel(level maulogger.Level) zerolog.Level { + switch level { + case maulogger.LevelDebug: + return zerolog.DebugLevel + case maulogger.LevelInfo: + return zerolog.InfoLevel + case maulogger.LevelWarn: + return zerolog.WarnLevel + case maulogger.LevelError: + return zerolog.ErrorLevel + case maulogger.LevelFatal: + return zerolog.FatalLevel + default: + return zerolog.TraceLevel + } +} + +func (m MauZeroLog) Log(level maulogger.Level, parts ...interface{}) { + m.Logger.WithLevel(mauToZeroLevel(level)).Msg(fmt.Sprint(parts...)) +} + +func (m MauZeroLog) Logln(level maulogger.Level, parts ...interface{}) { + m.Logger.WithLevel(mauToZeroLevel(level)).Msg(strings.TrimSuffix(fmt.Sprintln(parts...), "\n")) +} + +func (m MauZeroLog) Logf(level maulogger.Level, message string, args ...interface{}) { + m.Logger.WithLevel(mauToZeroLevel(level)).Msg(fmt.Sprintf(message, args...)) +} + +func (m MauZeroLog) Logfln(level maulogger.Level, message string, args ...interface{}) { + m.Logger.WithLevel(mauToZeroLevel(level)).Msg(fmt.Sprintf(message, args...)) +} + +func (m MauZeroLog) Debug(parts ...interface{}) { + m.Logger.Debug().Msg(fmt.Sprint(parts...)) +} + +func (m MauZeroLog) Debugln(parts ...interface{}) { + m.Logger.Debug().Msg(strings.TrimSuffix(fmt.Sprintln(parts...), "\n")) +} + +func (m MauZeroLog) Debugf(message string, args ...interface{}) { + m.Logger.Debug().Msg(fmt.Sprintf(message, args...)) +} + +func (m MauZeroLog) Debugfln(message string, args ...interface{}) { + m.Logger.Debug().Msg(fmt.Sprintf(message, args...)) +} + +func (m MauZeroLog) Info(parts ...interface{}) { + m.Logger.Info().Msg(fmt.Sprint(parts...)) +} + +func (m MauZeroLog) Infoln(parts ...interface{}) { + m.Logger.Info().Msg(strings.TrimSuffix(fmt.Sprintln(parts...), "\n")) +} + +func (m MauZeroLog) Infof(message string, args ...interface{}) { + m.Logger.Info().Msg(fmt.Sprintf(message, args...)) +} + +func (m MauZeroLog) Infofln(message string, args ...interface{}) { + m.Logger.Info().Msg(fmt.Sprintf(message, args...)) +} + +func (m MauZeroLog) Warn(parts ...interface{}) { + m.Logger.Warn().Msg(fmt.Sprint(parts...)) +} + +func (m MauZeroLog) Warnln(parts ...interface{}) { + m.Logger.Warn().Msg(strings.TrimSuffix(fmt.Sprintln(parts...), "\n")) +} + +func (m MauZeroLog) Warnf(message string, args ...interface{}) { + m.Logger.Warn().Msg(fmt.Sprintf(message, args...)) +} + +func (m MauZeroLog) Warnfln(message string, args ...interface{}) { + m.Logger.Warn().Msg(fmt.Sprintf(message, args...)) +} + +func (m MauZeroLog) Error(parts ...interface{}) { + m.Logger.Error().Msg(fmt.Sprint(parts...)) +} + +func (m MauZeroLog) Errorln(parts ...interface{}) { + m.Logger.Error().Msg(strings.TrimSuffix(fmt.Sprintln(parts...), "\n")) +} + +func (m MauZeroLog) Errorf(message string, args ...interface{}) { + m.Logger.Error().Msg(fmt.Sprintf(message, args...)) +} + +func (m MauZeroLog) Errorfln(message string, args ...interface{}) { + m.Logger.Error().Msg(fmt.Sprintf(message, args...)) +} + +func (m MauZeroLog) Fatal(parts ...interface{}) { + m.Logger.WithLevel(zerolog.FatalLevel).Msg(fmt.Sprint(parts...)) +} + +func (m MauZeroLog) Fatalln(parts ...interface{}) { + m.Logger.WithLevel(zerolog.FatalLevel).Msg(strings.TrimSuffix(fmt.Sprintln(parts...), "\n")) +} + +func (m MauZeroLog) Fatalf(message string, args ...interface{}) { + m.Logger.WithLevel(zerolog.FatalLevel).Msg(fmt.Sprintf(message, args...)) +} + +func (m MauZeroLog) Fatalfln(message string, args ...interface{}) { + m.Logger.WithLevel(zerolog.FatalLevel).Msg(fmt.Sprintf(message, args...)) +} diff --git a/vendor/maunium.net/go/maulogger/v2/maulogadapt/zeromaulog.go b/vendor/maunium.net/go/maulogger/v2/maulogadapt/zeromaulog.go new file mode 100644 index 00000000..1a275e7d --- /dev/null +++ b/vendor/maunium.net/go/maulogger/v2/maulogadapt/zeromaulog.go @@ -0,0 +1,73 @@ +// Copyright (c) 2023 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package maulogadapt + +import ( + "bytes" + + "github.com/rs/zerolog" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "maunium.net/go/maulogger/v2" +) + +// ZeroMauLog is a simple wrapper for a maulogger that can be set as the output writer for zerolog. +type ZeroMauLog struct { + maulogger.Logger +} + +func MauAsZero(log maulogger.Logger) *zerolog.Logger { + zero := zerolog.New(&ZeroMauLog{log}) + return &zero +} + +var _ zerolog.LevelWriter = (*ZeroMauLog)(nil) + +func (z *ZeroMauLog) Write(p []byte) (n int, err error) { + return 0, nil +} + +func (z *ZeroMauLog) WriteLevel(level zerolog.Level, p []byte) (n int, err error) { + var mauLevel maulogger.Level + switch level { + case zerolog.DebugLevel: + mauLevel = maulogger.LevelDebug + case zerolog.InfoLevel, zerolog.NoLevel: + mauLevel = maulogger.LevelInfo + case zerolog.WarnLevel: + mauLevel = maulogger.LevelWarn + case zerolog.ErrorLevel: + mauLevel = maulogger.LevelError + case zerolog.FatalLevel, zerolog.PanicLevel: + mauLevel = maulogger.LevelFatal + case zerolog.Disabled, zerolog.TraceLevel: + fallthrough + default: + return 0, nil + } + p = bytes.TrimSuffix(p, []byte{'\n'}) + msg := gjson.GetBytes(p, zerolog.MessageFieldName).Str + + p, err = sjson.DeleteBytes(p, zerolog.MessageFieldName) + if err != nil { + return + } + p, err = sjson.DeleteBytes(p, zerolog.LevelFieldName) + if err != nil { + return + } + p, err = sjson.DeleteBytes(p, zerolog.TimestampFieldName) + if err != nil { + return + } + if len(p) > 2 { + msg += " " + string(p) + } + z.Log(mauLevel, msg) + return len(p), nil +} diff --git a/vendor/maunium.net/go/maulogger/v2/sublogger.go b/vendor/maunium.net/go/maulogger/v2/sublogger.go new file mode 100644 index 00000000..b3326908 --- /dev/null +++ b/vendor/maunium.net/go/maulogger/v2/sublogger.go @@ -0,0 +1,216 @@ +// mauLogger - A logger for Go programs +// Copyright (c) 2016-2021 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package maulogger + +import ( + "fmt" +) + +type Sublogger struct { + topLevel *BasicLogger + parent Logger + Module string + DefaultLevel Level + metadata map[string]interface{} +} + +// Subm creates a Sublogger +func (log *BasicLogger) Subm(module string, metadata map[string]interface{}) Logger { + return &Sublogger{ + topLevel: log, + parent: log, + Module: module, + DefaultLevel: LevelInfo, + metadata: metadata, + } +} + +func (log *BasicLogger) Sub(module string) Logger { + return log.Subm(module, map[string]interface{}{}) +} + +// WithDefaultLevel creates a Sublogger with the same Module but different DefaultLevel +func (log *BasicLogger) WithDefaultLevel(lvl Level) Logger { + return log.DefaultSub.WithDefaultLevel(lvl) +} + +func (log *Sublogger) GetParent() Logger { + return log.parent +} + +// Sub creates a Sublogger +func (log *Sublogger) Subm(module string, metadata map[string]interface{}) Logger { + if len(module) > 0 { + module = fmt.Sprintf("%s/%s", log.Module, module) + } else { + module = log.Module + } + + return &Sublogger{ + topLevel: log.topLevel, + parent: log, + Module: module, + DefaultLevel: log.DefaultLevel, + metadata: metadata, + } +} + +func (log *Sublogger) Sub(module string) Logger { + return log.Subm(module, map[string]interface{}{}) +} + +// WithDefaultLevel creates a Sublogger with the same Module but different DefaultLevel +func (log *Sublogger) WithDefaultLevel(lvl Level) Logger { + return &Sublogger{ + topLevel: log.topLevel, + parent: log.parent, + Module: log.Module, + DefaultLevel: lvl, + } +} + +// SetModule changes the module name of this Sublogger +func (log *Sublogger) SetModule(mod string) { + log.Module = mod +} + +// SetDefaultLevel changes the default logging level of this Sublogger +func (log *Sublogger) SetDefaultLevel(lvl Level) { + log.DefaultLevel = lvl +} + +// SetParent changes the parent of this Sublogger +func (log *Sublogger) SetParent(parent *BasicLogger) { + log.topLevel = parent +} + +//Write ... +func (log *Sublogger) Write(p []byte) (n int, err error) { + log.topLevel.Raw(log.DefaultLevel, log.metadata, log.Module, string(p)) + return len(p), nil +} + +// Log formats the given parts with fmt.Sprint and logs the result with the given level +func (log *Sublogger) Log(level Level, parts ...interface{}) { + log.topLevel.Raw(level, log.metadata, log.Module, fmt.Sprint(parts...)) +} + +// Logln formats the given parts with fmt.Sprintln and logs the result with the given level +func (log *Sublogger) Logln(level Level, parts ...interface{}) { + log.topLevel.Raw(level, log.metadata, log.Module, fmt.Sprintln(parts...)) +} + +// Logf formats the given message and args with fmt.Sprintf and logs the result with the given level +func (log *Sublogger) Logf(level Level, message string, args ...interface{}) { + log.topLevel.Raw(level, log.metadata, log.Module, fmt.Sprintf(message, args...)) +} + +// Logfln formats the given message and args with fmt.Sprintf, appends a newline and logs the result with the given level +func (log *Sublogger) Logfln(level Level, message string, args ...interface{}) { + log.topLevel.Raw(level, log.metadata, log.Module, fmt.Sprintf(message+"\n", args...)) +} + +// Debug formats the given parts with fmt.Sprint and logs the result with the Debug level +func (log *Sublogger) Debug(parts ...interface{}) { + log.topLevel.Raw(LevelDebug, log.metadata, log.Module, fmt.Sprint(parts...)) +} + +// Debugln formats the given parts with fmt.Sprintln and logs the result with the Debug level +func (log *Sublogger) Debugln(parts ...interface{}) { + log.topLevel.Raw(LevelDebug, log.metadata, log.Module, fmt.Sprintln(parts...)) +} + +// Debugf formats the given message and args with fmt.Sprintf and logs the result with the Debug level +func (log *Sublogger) Debugf(message string, args ...interface{}) { + log.topLevel.Raw(LevelDebug, log.metadata, log.Module, fmt.Sprintf(message, args...)) +} + +// Debugfln formats the given message and args with fmt.Sprintf, appends a newline and logs the result with the Debug level +func (log *Sublogger) Debugfln(message string, args ...interface{}) { + log.topLevel.Raw(LevelDebug, log.metadata, log.Module, fmt.Sprintf(message+"\n", args...)) +} + +// Info formats the given parts with fmt.Sprint and logs the result with the Info level +func (log *Sublogger) Info(parts ...interface{}) { + log.topLevel.Raw(LevelInfo, log.metadata, log.Module, fmt.Sprint(parts...)) +} + +// Infoln formats the given parts with fmt.Sprintln and logs the result with the Info level +func (log *Sublogger) Infoln(parts ...interface{}) { + log.topLevel.Raw(LevelInfo, log.metadata, log.Module, fmt.Sprintln(parts...)) +} + +// Infof formats the given message and args with fmt.Sprintf and logs the result with the Info level +func (log *Sublogger) Infof(message string, args ...interface{}) { + log.topLevel.Raw(LevelInfo, log.metadata, log.Module, fmt.Sprintf(message, args...)) +} + +// Infofln formats the given message and args with fmt.Sprintf, appends a newline and logs the result with the Info level +func (log *Sublogger) Infofln(message string, args ...interface{}) { + log.topLevel.Raw(LevelInfo, log.metadata, log.Module, fmt.Sprintf(message+"\n", args...)) +} + +// Warn formats the given parts with fmt.Sprint and logs the result with the Warn level +func (log *Sublogger) Warn(parts ...interface{}) { + log.topLevel.Raw(LevelWarn, log.metadata, log.Module, fmt.Sprint(parts...)) +} + +// Warnln formats the given parts with fmt.Sprintln and logs the result with the Warn level +func (log *Sublogger) Warnln(parts ...interface{}) { + log.topLevel.Raw(LevelWarn, log.metadata, log.Module, fmt.Sprintln(parts...)) +} + +// Warnf formats the given message and args with fmt.Sprintf and logs the result with the Warn level +func (log *Sublogger) Warnf(message string, args ...interface{}) { + log.topLevel.Raw(LevelWarn, log.metadata, log.Module, fmt.Sprintf(message, args...)) +} + +// Warnfln formats the given message and args with fmt.Sprintf, appends a newline and logs the result with the Warn level +func (log *Sublogger) Warnfln(message string, args ...interface{}) { + log.topLevel.Raw(LevelWarn, log.metadata, log.Module, fmt.Sprintf(message+"\n", args...)) +} + +// Error formats the given parts with fmt.Sprint and logs the result with the Error level +func (log *Sublogger) Error(parts ...interface{}) { + log.topLevel.Raw(LevelError, log.metadata, log.Module, fmt.Sprint(parts...)) +} + +// Errorln formats the given parts with fmt.Sprintln and logs the result with the Error level +func (log *Sublogger) Errorln(parts ...interface{}) { + log.topLevel.Raw(LevelError, log.metadata, log.Module, fmt.Sprintln(parts...)) +} + +// Errorf formats the given message and args with fmt.Sprintf and logs the result with the Error level +func (log *Sublogger) Errorf(message string, args ...interface{}) { + log.topLevel.Raw(LevelError, log.metadata, log.Module, fmt.Sprintf(message, args...)) +} + +// Errorfln formats the given message and args with fmt.Sprintf, appends a newline and logs the result with the Error level +func (log *Sublogger) Errorfln(message string, args ...interface{}) { + log.topLevel.Raw(LevelError, log.metadata, log.Module, fmt.Sprintf(message+"\n", args...)) +} + +// Fatal formats the given parts with fmt.Sprint and logs the result with the Fatal level +func (log *Sublogger) Fatal(parts ...interface{}) { + log.topLevel.Raw(LevelFatal, log.metadata, log.Module, fmt.Sprint(parts...)) +} + +// Fatalln formats the given parts with fmt.Sprintln and logs the result with the Fatal level +func (log *Sublogger) Fatalln(parts ...interface{}) { + log.topLevel.Raw(LevelFatal, log.metadata, log.Module, fmt.Sprintln(parts...)) +} + +// Fatalf formats the given message and args with fmt.Sprintf and logs the result with the Fatal level +func (log *Sublogger) Fatalf(message string, args ...interface{}) { + log.topLevel.Raw(LevelFatal, log.metadata, log.Module, fmt.Sprintf(message, args...)) +} + +// Fatalfln formats the given message and args with fmt.Sprintf, appends a newline and logs the result with the Fatal level +func (log *Sublogger) Fatalfln(message string, args ...interface{}) { + log.topLevel.Raw(LevelFatal, log.metadata, log.Module, fmt.Sprintf(message+"\n", args...)) +} diff --git a/vendor/maunium.net/go/maulogger/v2/writer.go b/vendor/maunium.net/go/maulogger/v2/writer.go new file mode 100644 index 00000000..8852b857 --- /dev/null +++ b/vendor/maunium.net/go/maulogger/v2/writer.go @@ -0,0 +1,78 @@ +// mauLogger - A logger for Go programs +// Copyright (c) 2021 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package maulogger + +import ( + "bytes" + "io" + "sync" +) + +// LogWriter is a buffered io.Writer that writes lines to a Logger. +type LogWriter struct { + log Logger + lock sync.Mutex + level Level + buf bytes.Buffer +} + +func (log *BasicLogger) Writer(level Level) io.WriteCloser { + return &LogWriter{ + log: log, + level: level, + } +} + +func (log *Sublogger) Writer(level Level) io.WriteCloser { + return &LogWriter{ + log: log, + level: level, + } +} + +func (lw *LogWriter) writeLine(data []byte) { + if lw.buf.Len() == 0 { + if len(data) == 0 { + return + } + lw.log.Logln(lw.level, string(data)) + } else { + lw.buf.Write(data) + lw.log.Logln(lw.level, lw.buf.String()) + lw.buf.Reset() + } +} + +// Write will write lines from the given data to the buffer. If the data doesn't end with a line break, +// everything after the last line break will be buffered until the next Write or Close call. +func (lw *LogWriter) Write(data []byte) (int, error) { + lw.lock.Lock() + newline := bytes.IndexByte(data, '\n') + if newline == len(data)-1 { + lw.writeLine(data[:len(data)-1]) + } else if newline < 0 { + lw.buf.Write(data) + } else { + lines := bytes.Split(data, []byte("\n")) + for _, line := range lines[:len(lines)-1] { + lw.writeLine(line) + } + lw.buf.Write(lines[len(lines)-1]) + } + lw.lock.Unlock() + return len(data), nil +} + +// Close will flush remaining data in the buffer into the logger. +func (lw *LogWriter) Close() error { + lw.lock.Lock() + lw.log.Logln(lw.level, lw.buf.String()) + lw.buf.Reset() + lw.lock.Unlock() + return nil +} diff --git a/vendor/maunium.net/go/mautrix/.editorconfig b/vendor/maunium.net/go/mautrix/.editorconfig new file mode 100644 index 00000000..21d312a1 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = tab +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{yaml,yml}] +indent_style = space diff --git a/vendor/maunium.net/go/mautrix/.gitignore b/vendor/maunium.net/go/mautrix/.gitignore new file mode 100644 index 00000000..f37a7d0c --- /dev/null +++ b/vendor/maunium.net/go/mautrix/.gitignore @@ -0,0 +1,4 @@ +.idea/ +.vscode/ +*.db +*.log diff --git a/vendor/maunium.net/go/mautrix/.pre-commit-config.yaml b/vendor/maunium.net/go/mautrix/.pre-commit-config.yaml new file mode 100644 index 00000000..1ef386ea --- /dev/null +++ b/vendor/maunium.net/go/mautrix/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + exclude_types: [markdown] + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + + - repo: https://github.com/tekwizely/pre-commit-golang + rev: v1.0.0-rc.1 + hooks: + - id: go-imports-repo + - id: go-vet-repo-mod diff --git a/vendor/maunium.net/go/mautrix/CHANGELOG.md b/vendor/maunium.net/go/mautrix/CHANGELOG.md new file mode 100644 index 00000000..07f60013 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/CHANGELOG.md @@ -0,0 +1,612 @@ +## v0.15.0 (2023-03-16) + +### beta.3 (2023-03-15) + +* **Breaking change *(appservice)*** Removed `Load()` and `AppService.Init()` + functions. The struct should just be created with `Create()` and the relevant + fields should be filled manually. +* **Breaking change *(appservice)*** Removed public `HomeserverURL` field and + replaced it with a `SetHomeserverURL` method. +* *(appservice)* Added support for unix sockets for homeserver URL and + appservice HTTP server. +* *(client)* Changed request logging to log durations as floats instead of + strings (using zerolog's `Dur()`, so the exact output can be configured). +* *(bridge)* Changed zerolog to use nanosecond precision timestamps. +* *(crypto)* Added message index to log after encrypting/decrypting megolm + events, and when failing to decrypt due to duplicate index. +* *(sqlstatestore)* Fixed warning log for rooms that don't have encryption + enabled. + +### beta.2 (2023-03-02) + +* *(bridge)* Fixed building with `nocrypto` tag. +* *(bridge)* Fixed legacy logging config migration not disabling file writer + when `file_name_format` was empty. +* *(bridge)* Added option to require room power level to run commands. +* *(event)* Added structs for [MSC3952]: Intentional Mentions. +* *(util/variationselector)* Added `FullyQualify` method to add necessary emoji + variation selectors without adding all possible ones. + +[MSC3952]: https://github.com/matrix-org/matrix-spec-proposals/pull/3952 + +### beta.1 (2023-02-24) + +* Bumped minimum Go version to 1.19. +* **Breaking changes** + * *(all)* Switched to zerolog for logging. + * The `Client` and `Bridge` structs still include a legacy logger for + backwards compatibility. + * *(client, appservice)* Moved `SQLStateStore` from appservice module to the + top-level (client) module. + * *(client, appservice)* Removed unused `Typing` map in `SQLStateStore`. + * *(client)* Removed unused `SaveRoom` and `LoadRoom` methods in `Storer`. + * *(client, appservice)* Removed deprecated `SendVideo` and `SendImage` methods. + * *(client)* Replaced `AppServiceUserID` field with `SetAppServiceUserID` boolean. + The `UserID` field is used as the value for the query param. + * *(crypto)* Renamed `GobStore` to `MemoryStore` and removed the file saving + features. The data can still be persisted, but the persistence part must be + implemented separately. + * *(crypto)* Removed deprecated `DeviceIdentity` alias + (renamed to `id.Device` long ago). + * *(client)* Removed `Stringifable` interface as it's the same as `fmt.Stringer`. +* *(client)* Renamed `Storer` interface to `SyncStore`. A type alias exists for + backwards-compatibility. +* *(crypto/cryptohelper)* Added package for a simplified crypto interface for clients. +* *(example)* Added e2ee support to example using crypto helper. +* *(client)* Changed default syncer to stop syncing on `M_UNKNOWN_TOKEN` errors. + +## v0.14.0 (2023-02-16) + +* **Breaking change *(format)*** Refactored the HTML parser `Context` to have + more data. +* *(id)* Fixed escaping path components when forming matrix.to URLs + or `matrix:` URIs. +* *(bridge)* Bumped default timeouts for decrypting incoming messages. +* *(bridge)* Added `RawArgs` to commands to allow accessing non-split input. +* *(bridge)* Added `ReplyAdvanced` to commands to allow setting markdown + settings. +* *(event)* Added `notifications` key to `PowerLevelEventContent`. +* *(event)* Changed `SetEdit` to cut off edit fallback if the message is long. +* *(util)* Added `SyncMap` as a simple generic wrapper for a map with a mutex. +* *(util)* Added `ReturnableOnce` as a wrapper for `sync.Once` with a return + value. + +## v0.13.0 (2023-01-16) + +* **Breaking change:** Removed `IsTyping` and `SetTyping` in `appservice.StateStore` + and removed the `TypingStateStore` struct implementing those methods. +* **Breaking change:** Removed legacy fields in Beeper MSS events. +* Added knocked rooms to sync response structs. +* Added wrapper for `/timestamp_to_event` endpoint added in Matrix v1.6. +* Fixed MSC3870 uploads not failing properly after using up the max retry count. +* Fixed parsing non-positive ordered list start positions in HTML parser. + +## v0.12.4 (2022-12-16) + +* Added `SendReceipt` to support private read receipts and thread receipts in + the same function. `MarkReadWithContent` is now deprecated. +* Changed media download methods to return errors if the server returns a + non-2xx status code. +* Removed legacy `sql_store_upgrade.Upgrade` method. Using `store.DB.Upgrade()` + after `NewSQLCryptoStore(...)` is recommended instead (the bridge module does + this automatically). +* Added missing `suggested` field to `m.space.child` content struct. +* Added `device_unused_fallback_key_types` to `/sync` response and appservice + transaction structs. +* Changed `ReqSetReadMarkers` to omit empty fields. +* Changed bridge configs to force `sqlite3-fk-wal` instead of `sqlite3`. +* Updated bridge helper to close database connection when stopping. +* Fixed read receipt and account data endpoints sending `null` instead of an + empty object as the body when content isn't provided. + +## v0.12.3 (2022-11-16) + +* **Breaking change:** Added logging for row iteration in the dbutil package. + This changes the return type of `Query` methods from `*sql.Rows` to a new + `dbutil.Rows` interface. +* Added flag to disable wrapping database upgrades in a transaction (e.g. to + allow setting `PRAGMA`s for advanced table mutations on SQLite). +* Deprecated `MessageEventContent.GetReplyTo` in favor of directly using + `RelatesTo.GetReplyTo`. RelatesTo methods are nil-safe, so checking if + RelatesTo is nil is not necessary for using those methods. +* Added wrapper for space hierarchyendpoint (thanks to [@mgcm] in [#100]). +* Added bridge config option to handle transactions asynchronously. +* Added separate channels for to-device events in appservice transaction + handler to avoid blocking to-device events behind normal events. +* Added `RelatesTo.GetNonFallbackReplyTo` utility method to get the reply event + ID, unless the reply is a thread fallback. +* Added `event.TextToHTML` as an utility method to HTML-escape a string and + replace newlines with `<br/>`. +* Added check to bridge encryption helper to make sure the e2ee keys are still + on the server. Synapse is known to sometimes lose keys randomly. +* Changed bridge crypto syncer to crash on `M_UNKNOWN_TOKEN` errors instead of + retrying forever pointlessly. +* Fixed verifying signatures of fallback one-time keys. + +[@mgcm]: https://github.com/mgcm +[#100]: https://github.com/mautrix/go/pull/100 + +## v0.12.2 (2022-10-16) + +* Added utility method to redact bridge commands. +* Added thread ID field to read receipts to match Matrix v1.4 changes. +* Added automatic fetching of media repo config at bridge startup to make it + easier for bridges to check homeserver media size limits. +* Added wrapper for the `/register/available` endpoint. +* Added custom user agent to all requests mautrix-go makes. The value can be + customized by changing the `DefaultUserAgent` variable. +* Implemented [MSC3664], [MSC3862] and [MSC3873] in the push rule evaluator. +* Added workaround for potential race conditions in OTK uploads when using + appservice encryption ([MSC3202]). +* Fixed generating registrations to use `.+` instead of `[0-9]+` in the + username regex. +* Fixed panic in megolm session listing methods if the store contains withheld + key entries. +* Fixed missing header in bridge command help messages. + +[MSC3664]: https://github.com/matrix-org/matrix-spec-proposals/pull/3664 +[MSC3862]: https://github.com/matrix-org/matrix-spec-proposals/pull/3862 +[MSC3873]: https://github.com/matrix-org/matrix-spec-proposals/pull/3873 + +## v0.12.1 (2022-09-16) + +* Bumped minimum Go version to 1.18. +* Added `omitempty` for a bunch of fields in response structs to make them more + usable for server implementations. +* Added `util.RandomToken` to generate GitHub-style access tokens with checksums. +* Added utilities to call the push gateway API. +* Added `unread_notifications` and [MSC2654] `unread_count` fields to /sync + response structs. +* Implemented [MSC3870] for uploading and downloading media directly to/from an + external media storage like S3. +* Fixed dbutil database ownership checks on SQLite. +* Fixed typo in unauthorized encryption key withheld code + (`m.unauthorized` -> `m.unauthorised`). +* Fixed [MSC2409] support to have a separate field for to-device events. + +[MSC2654]: https://github.com/matrix-org/matrix-spec-proposals/pull/2654 +[MSC3870]: https://github.com/matrix-org/matrix-spec-proposals/pull/3870 + +## v0.12.0 (2022-08-16) + +* **Breaking change:** Switched `Client.UserTyping` to take a `time.Duration` + instead of raw `int64` milliseconds. +* **Breaking change:** Removed custom reply relation type and switched to using + the wire format (nesting in `m.in_reply_to`). +* Added device ID to appservice OTK count map to match updated [MSC3202]. + This is also a breaking change, but the previous incorrect behavior wasn't + implemented by anything other than mautrix-syncproxy/imessage. +* (There are probably other breaking changes too). +* Added database utility and schema upgrade framework + * Originally from mautrix-whatsapp, but usable for non-bridges too + * Includes connection wrapper to log query durations and mutate queries for + SQLite compatibility (replacing `$x` with `?x`). +* Added bridge utilities similar to mautrix-python. Currently includes: + * Crypto helper + * Startup flow + * Command handling and some standard commands + * Double puppeting things + * Generic parts of config, basic config validation + * Appservice SQL state store +* Added alternative markdown spoiler parsing extension that doesn't support + reasons, but works better otherwise. +* Added Discord underline markdown parsing extension (`_foo_` -> <u>foo</u>). +* Added support for parsing spoilers and color tags in the HTML parser. +* Added support for mutating plain text nodes in the HTML parser. +* Added room version field to the create room request struct. +* Added empty JSON object as default request body for all non-GET requests. +* Added wrapper for `/capabilities` endpoint. +* Added `omitempty` markers for lots of structs to make the structs easier to + use on the server side too. +* Added support for registering to-device event handlers via the default + Syncer's `OnEvent` and `OnEventType` methods. +* Fixed `CreateEventContent` using the wrong field name for the room version + field. +* Fixed `StopSync` not immediately cancelling the sync loop if it was sleeping + after a failed sync. +* Fixed `GetAvatarURL` always returning the current user's avatar instead of + the specified user's avatar (thanks to [@nightmared] in [#83]). +* Improved request logging and added new log when a request finishes. +* Crypto store improvements: + * Deleted devices are now kept in the database. + * Made ValidateMessageIndex atomic. +* Moved `appservice.RandomString` to the `util` package and made it use + `crypto/rand` instead of `math/rand`. +* Significantly improved cross-signing validation code. + * There are now more options for required trust levels, + e.g. you can set `SendKeysMinTrust` to `id.TrustStateCrossSignedTOFU` + to trust the first cross-signing master key seen and require all devices + to be signed by that key. + * Trust state of incoming messages is automatically resolved and stored in + `evt.Mautrix.TrustState`. This can be used to reject incoming messages from + untrusted devices. + +[@nightmared]: https://github.com/nightmared +[#83]: https://github.com/mautrix/go/pull/83 + +## v0.11.1 (2023-01-15) + +* Fixed parsing non-positive ordered list start positions in HTML parser + (backport of the same fix in v0.13.0). + +## v0.11.0 (2022-05-16) + +* Bumped minimum Go version to 1.17. +* Switched from `/r0` to `/v3` paths everywhere. + * The new `v3` paths are implemented since Synapse 1.48, Dendrite 0.6.5, and + Conduit 0.4.0. Servers older than these are no longer supported. +* Switched from blackfriday to goldmark for markdown parsing in the `format` + module and added spoiler syntax. +* Added `EncryptInPlace` and `DecryptInPlace` methods for attachment encryption. + In most cases the plain/ciphertext is not necessary after en/decryption, so + the old `Encrypt` and `Decrypt` are deprecated. +* Added wrapper for `/rooms/.../aliases`. +* Added utility for adding/removing emoji variation selectors to match + recommendations on reactions in Matrix. +* Added support for async media uploads ([MSC2246]). +* Added automatic sleep when receiving 429 error + (thanks to [@ownaginatious] in [#44]). +* Added support for parsing spec version numbers from the `/versions` endpoint. +* Removed unstable prefixed constant used for appservice login. +* Fixed URL encoding not working correctly in some cases. + +[MSC2246]: https://github.com/matrix-org/matrix-spec-proposals/pull/2246 +[@ownaginatious]: https://github.com/ownaginatious +[#44]: https://github.com/mautrix/go/pull/44 + +## v0.10.12 (2022-03-16) + +* Added option to use a different `Client` to send invites in + `IntentAPI.EnsureJoined`. +* Changed `MessageEventContent` struct to omit empty `msgtype`s in the output + JSON, as sticker events shouldn't have that field. +* Fixed deserializing the `thumbnail_file` field in `FileInfo`. +* Fixed bug that broke `SQLCryptoStore.FindDeviceByKey`. + +## v0.10.11 (2022-02-16) + +* Added automatic updating of state store from `IntentAPI` calls. +* Added option to ignore cache in `IntentAPI.EnsureJoined`. +* Added `GetURLPreview` as a wrapper for the `/preview_url` media repo endpoint. +* Moved base58 module inline to avoid pulling in btcd as a dependency. + +## v0.10.10 (2022-01-16) + +* Added event types and content structs for server ACLs and moderation policy + lists (thanks to [@qua3k] in [#59] and [#60]). +* Added optional parameter to `Client.LeaveRoom` to pass a `reason` field. + +[#59]: https://github.com/mautrix/go/pull/59 +[#60]: https://github.com/mautrix/go/pull/60 + +## v0.10.9 (2022-01-04) + +* **Breaking change:** Changed `Messages()` to take a filter as a parameter + instead of using the syncer's filter (thanks to [@qua3k] in [#55] and [#56]). + * The previous filter behavior was completely broken, as it sent a whole + filter instead of just a RoomEventFilter. + * Passing `nil` as the filter is fine and will disable filtering + (which is equivalent to what it did before with the invalid filter). +* Added `Context()` wrapper for the `/context` API (thanks to [@qua3k] in [#54]). +* Added utility for converting media files with ffmpeg. + +[#54]: https://github.com/mautrix/go/pull/54 +[#55]: https://github.com/mautrix/go/pull/55 +[#56]: https://github.com/mautrix/go/pull/56 +[@qua3k]: https://github.com/qua3k + +## v0.10.8 (2021-12-30) + +* Added `OlmSession.Describe()` to wrap `olm_session_describe`. +* Added trace logs to log olm session descriptions when encrypting/decrypting + to-device messages. +* Added space event types and content structs. +* Added support for power level content override field in `CreateRoom`. +* Fixed ordering of olm sessions which would cause an old session to be used in + some cases even after a client created a new session. + +## v0.10.7 (2021-12-16) + +* Changed `Client.RedactEvent` to allow arbitrary fields in redaction request. + +## v0.10.5 (2021-12-06) + +* Fixed websocket disconnection not clearing all pending requests. +* Added `OlmMachine.SendRoomKeyRequest` as a more direct way of sending room + key requests. +* Added automatic Olm session recreation if an incoming message fails to decrypt. +* Changed `Login` to only omit request content from logs if there's a password + or token (appservice logins don't have sensitive content). + +## v0.10.4 (2021-11-25) + +* Added `reason` field to invite and unban requests + (thanks to [@ptman] in [#48]). +* Fixed `AppService.HasWebsocket()` returning `true` even after websocket has + disconnected. + +[@ptman]: https://github.com/ptman +[#48]: https://github.com/mautrix/go/pull/48 + +## v0.10.3 (2021-11-18) + +* Added logs about incoming appservice transactions. +* Added support for message send checkpoints (as HTTP requests, similar to the + bridge state reporting system). + +## v0.10.2 (2021-11-15) + +* Added utility method for finding the first supported login flow matching any + of the given types. +* Updated registering appservice ghosts to use `inhibit_login` flag to prevent + lots of unnecessary access tokens from being created. + * If you want to log in as an appservice ghost, you should use [MSC2778]'s + appservice login (e.g. like [mautrix-whatsapp does for e2be](https://github.com/mautrix/whatsapp/blob/v0.2.1/crypto.go#L143-L149)). + +## v0.10.1 (2021-11-05) + +* Removed direct dependency on `pq` + * In order to use some more efficient queries on postgres, you must set + `crypto.PostgresArrayWrapper = pq.Array` if you want to use both postgres + and e2ee. +* Added temporary hack to ignore state events with the MSC2716 historical flag + (to be removed after [matrix-org/synapse#11265] is merged) +* Added received transaction acknowledgements for websocket appservice + transactions. +* Added automatic fallback to move `prev_content` from top level to the + standard location inside `unsigned`. + +[matrix-org/synapse#11265]: https://github.com/matrix-org/synapse/pull/11265 + +## v0.9.31 (2021-10-27) + +* Added `SetEdit` utility function for `MessageEventContent`. + +## v0.9.30 (2021-10-26) + +* Added wrapper for [MSC2716]'s `/batch_send` endpoint. +* Added `MarshalJSON` method for `Event` struct to prevent empty unsigned + structs from being included in the JSON. + +[MSC2716]: https://github.com/matrix-org/matrix-spec-proposals/pull/2716 + +## v0.9.29 (2021-09-30) + +* Added `client.State` method to get full room state. +* Added bridge info structs and event types ([MSC2346]). +* Made response handling more customizable. +* Fixed type of `AuthType` constants. + +[MSC2346]: https://github.com/matrix-org/matrix-spec-proposals/pull/2346 + +## v0.9.28 (2021-09-30) + +* Added `X-Mautrix-Process-ID` to appservice websocket headers to help debug + issues where multiple instances are connecting to the server at the same time. + +## v0.9.27 (2021-09-23) + +* Fixed Go 1.14 compatibility (broken in v0.9.25). +* Added GitHub actions CI to build, test and check formatting on Go 1.14-1.17. + +## v0.9.26 (2021-09-21) + +* Added default no-op logger to `Client` in order to prevent panic when the + application doesn't set a logger. + +## v0.9.25 (2021-09-19) + +* Disabled logging request JSON for sensitive requests like `/login`, + `/register` and other UIA endpoints. Logging can still be enabled by + setting `MAUTRIX_LOG_SENSITIVE_CONTENT` to `yes`. +* Added option to store new homeserver URL from `/login` response well-known data. +* Added option to stream big sync responses via disk to maybe reduce memory usage. +* Fixed trailing slashes in homeserver URL breaking all requests. + +## v0.9.24 (2021-09-03) + +* Added write deadline for appservice websocket connection. + +## v0.9.23 (2021-08-31) + +* Fixed storing e2ee key withheld events in the SQL store. + +## v0.9.22 (2021-08-30) + +* Updated appservice handler to cache multiple recent transaction IDs + instead of only the most recent one. + +## v0.9.21 (2021-08-25) + +* Added liveness and readiness endpoints to appservices. + * The endpoints are the same as mautrix-python: + `/_matrix/mau/live` and `/_matrix/mau/ready` + * Liveness always returns 200 and an empty JSON object by default, + but it can be turned off by setting `appservice.Live` to `false`. + * Readiness defaults to returning 500, and it can be switched to 200 + by setting `appservice.Ready` to `true`. + +## v0.9.20 (2021-08-19) + +* Added crypto store migration for converting all `VARCHAR(255)` columns + to `TEXT` in Postgres databases. + +## v0.9.19 (2021-08-17) + +* Fixed HTML parser outputting two newlines after paragraph tags. + +## v0.9.18 (2021-08-16) + +* Added new `BuildURL` method that does the same as `Client.BuildBaseURL` + but without requiring the `Client` instance. + +## v0.9.17 (2021-07-25) + +* Fixed handling OTK counts and device lists coming in through the appservice + transaction websocket. +* Updated OlmMachine to ignore OTK counts intended for other devices. + +## v0.9.15 (2021-07-16) + +* Added support for [MSC3202] and the to-device part of [MSC2409] in the + appservice package. +* Added support for sending commands through appservice websocket. +* Changed error message JSON field name in appservice error responses to + conform with standard Matrix errors (`message` -> `error`). + +[MSC3202]: https://github.com/matrix-org/matrix-spec-proposals/pull/3202 + +## v0.9.14 (2021-06-17) + +* Added default implementation of `PillConverter` in HTML parser utility. + +## v0.9.13 (2021-06-15) + +* Added support for parsing and generating encoded matrix.to URLs and `matrix:` URIs ([MSC2312](https://github.com/matrix-org/matrix-doc/pull/2312)). +* Updated HTML parser to use new URI parser for parsing user/room pills. + +## v0.9.12 (2021-05-18) + +* Added new method for sending custom data with read receipts + (not currently a part of the spec). + +## v0.9.11 (2021-05-12) + +* Improved debug log for unsupported event types. +* Added VoIP events to GuessClass. +* Added support for parsing strings in VoIP event version field. + +## v0.9.10 (2021-04-29) + +* Fixed `format.RenderMarkdown()` still allowing HTML when both `allowHTML` + and `allowMarkdown` are `false`. + +## v0.9.9 (2021-04-26) + +* Updated appservice `StartWebsocket` to return websocket close info. + +## v0.9.8 (2021-04-20) + +* Added methods for getting room tags and account data. + +## v0.9.7 (2021-04-19) + +* **Breaking change (crypto):** `SendEncryptedToDevice` now requires an event + type parameter. Previously it only allowed sending events of type + `event.ToDeviceForwardedRoomKey`. +* Added content structs for VoIP events. +* Added global mutex for Olm decryption + (previously it was only used for encryption). + +## v0.9.6 (2021-04-15) + +* Added option to retry all HTTP requests when encountering a HTTP network + error or gateway error response (502/503/504) + * Disabled by default, you need to set the `DefaultHTTPRetries` field in + the `AppService` or `Client` struct to enable. + * Can also be enabled with `FullRequest`s `MaxAttempts` field. + +## v0.9.5 (2021-04-06) + +* Reverted update of `golang.org/x/sys` which broke Go 1.14 / darwin/arm. + +## v0.9.4 (2021-04-06) + +* Switched appservices to using shared `http.Client` instance with a in-memory + cookie jar. + +## v0.9.3 (2021-03-26) + +* Made user agent headers easier to configure. +* Improved logging when receiving weird/unhandled to-device events. + +## v0.9.2 (2021-03-15) + +* Fixed type of presence state constants (thanks to [@babolivier] in [#30]). +* Implemented presence state fetching methods (thanks to [@babolivier] in [#29]). +* Added support for sending and receiving commands via appservice transaction websocket. + +[@babolivier]: https://github.com/babolivier +[#29]: https://github.com/mautrix/go/pull/29 +[#30]: https://github.com/mautrix/go/pull/30 + +## v0.9.1 (2021-03-11) + +* Fixed appservice register request hiding actual errors due to UIA error handling. + +## v0.9.0 (2021-03-04) + +* **Breaking change (manual API requests):** `MakeFullRequest` now takes a + `FullRequest` struct instead of individual parameters. `MakeRequest`'s + parameters are unchanged. +* **Breaking change (manual /sync):** `SyncRequest` now requires a `Context` + parameter. +* **Breaking change (end-to-bridge encryption):** + the `uk.half-shot.msc2778.login.application_service` constant used for + appservice login ([MSC2778]) was renamed from `AuthTypeAppservice` + to `AuthTypeHalfyAppservice`. + * The `AuthTypeAppservice` constant now contains `m.login.application_service`, + which is currently only used for registrations, but will also be used for + login once MSC2778 lands in the spec. +* Fixed appservice registration requests to include `m.login.application_service` + as the `type` (re [matrix-org/synapse#9548]). +* Added wrapper for `/logout/all`. + +[MSC2778]: https://github.com/matrix-org/matrix-spec-proposals/pull/2778 +[matrix-org/synapse#9548]: https://github.com/matrix-org/synapse/pull/9548 + +## v0.8.6 (2021-03-02) + +* Added client-side timeout to `mautrix.Client`'s `http.Client` + (defaults to 3 minutes). +* Updated maulogger to fix bug where plaintext file logs wouldn't have newlines. + +## v0.8.5 (2021-02-26) + +* Fixed potential concurrent map writes in appservice `Client` and `Intent` + methods. + +## v0.8.4 (2021-02-24) + +* Added option to output appservice logs as JSON. +* Added new methods for validating user ID localparts. + +## v0.8.3 (2021-02-21) + +* Allowed empty content URIs in parser +* Added functions for device management endpoints + (thanks to [@edwargix] in [#26]). + +[@edwargix]: https://github.com/edwargix +[#26]: https://github.com/mautrix/go/pull/26 + +## v0.8.2 (2021-02-09) + +* Fixed error when removing the user's avatar. + +## v0.8.1 (2021-02-09) + +* Added AccountDataStore to remove the need for persistent local storage other + than the access token (thanks to [@daenney] in [#23]). +* Added support for receiving appservice transactions over websocket. + See <https://github.com/mautrix/wsproxy> for the server-side implementation. +* Fixed error when removing the room avatar. + +[@daenney]: https://github.com/daenney +[#23]: https://github.com/mautrix/go/pull/23 + +## v0.8.0 (2020-12-24) + +* **Breaking change:** the `RateLimited` field in the `Registration` struct is + now a pointer, so that it can be omitted entirely. +* Merged initial SSSS/cross-signing code by [@nikofil]. Interactive verification + doesn't work, but the other things mostly do. +* Added support for authorization header auth in appservices ([MSC2832]). +* Added support for receiving ephemeral events directly ([MSC2409]). +* Fixed `SendReaction()` and other similar methods in the `Client` struct. +* Fixed crypto cgo code panicking in Go 1.15.3+. +* Fixed olm session locks sometime getting deadlocked. + +[MSC2832]: https://github.com/matrix-org/matrix-spec-proposals/pull/2832 +[MSC2409]: https://github.com/matrix-org/matrix-spec-proposals/pull/2409 +[@nikofil]: https://github.com/nikofil diff --git a/vendor/maunium.net/go/mautrix/LICENSE b/vendor/maunium.net/go/mautrix/LICENSE new file mode 100644 index 00000000..a612ad98 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/vendor/maunium.net/go/mautrix/README.md b/vendor/maunium.net/go/mautrix/README.md new file mode 100644 index 00000000..04fdc0e9 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/README.md @@ -0,0 +1,24 @@ +# mautrix-go +[![GoDoc](https://pkg.go.dev/badge/maunium.net/go/mautrix)](https://pkg.go.dev/maunium.net/go/mautrix) + +A Golang Matrix framework. Used by [gomuks](https://matrix.org/docs/projects/client/gomuks), +[go-neb](https://github.com/matrix-org/go-neb), [mautrix-whatsapp](https://github.com/mautrix/whatsapp) +and others. + +Matrix room: [`#maunium:maunium.net`](https://matrix.to/#/#maunium:maunium.net) + +This project is based on [matrix-org/gomatrix](https://github.com/matrix-org/gomatrix). +The original project is licensed under [Apache 2.0](https://github.com/matrix-org/gomatrix/blob/master/LICENSE). + +In addition to the basic client API features the original project has, this framework also has: + +* Appservice support (Intent API like mautrix-python, room state storage, etc) +* End-to-end encryption support (incl. interactive SAS verification) +* Structs for parsing event content +* Helpers for parsing and generating Matrix HTML +* Helpers for handling push rules + +This project contains modules that are licensed under Apache 2.0: + +* [maunium.net/go/mautrix/crypto/canonicaljson](crypto/canonicaljson) +* [maunium.net/go/mautrix/crypto/olm](crypto/olm) diff --git a/vendor/maunium.net/go/mautrix/appservice/appservice.go b/vendor/maunium.net/go/mautrix/appservice/appservice.go new file mode 100644 index 00000000..099e4b27 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/appservice/appservice.go @@ -0,0 +1,350 @@ +// Copyright (c) 2023 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package appservice + +import ( + "context" + "fmt" + "net" + "net/http" + "net/http/cookiejar" + "net/url" + "os" + "strings" + "sync" + "syscall" + "time" + + "github.com/gorilla/mux" + "github.com/gorilla/websocket" + "github.com/rs/zerolog" + "golang.org/x/net/publicsuffix" + "gopkg.in/yaml.v3" + "maunium.net/go/maulogger/v2/maulogadapt" + + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" +) + +// EventChannelSize is the size for the Events channel in Appservice instances. +var EventChannelSize = 64 +var OTKChannelSize = 4 + +// Create a blank appservice instance. +func Create() *AppService { + jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) + as := &AppService{ + Log: zerolog.Nop(), + clients: make(map[id.UserID]*mautrix.Client), + intents: make(map[id.UserID]*IntentAPI), + HTTPClient: &http.Client{Timeout: 180 * time.Second, Jar: jar}, + StateStore: mautrix.NewMemoryStateStore().(StateStore), + Router: mux.NewRouter(), + UserAgent: mautrix.DefaultUserAgent, + txnIDC: NewTransactionIDCache(128), + Live: true, + Ready: false, + ProcessID: getDefaultProcessID(), + + Events: make(chan *event.Event, EventChannelSize), + ToDeviceEvents: make(chan *event.Event, EventChannelSize), + OTKCounts: make(chan *mautrix.OTKCount, OTKChannelSize), + DeviceLists: make(chan *mautrix.DeviceLists, EventChannelSize), + QueryHandler: &QueryHandlerStub{}, + } + + as.Router.HandleFunc("/transactions/{txnID}", as.PutTransaction).Methods(http.MethodPut) + as.Router.HandleFunc("/rooms/{roomAlias}", as.GetRoom).Methods(http.MethodGet) + as.Router.HandleFunc("/users/{userID}", as.GetUser).Methods(http.MethodGet) + as.Router.HandleFunc("/_matrix/app/v1/transactions/{txnID}", as.PutTransaction).Methods(http.MethodPut) + as.Router.HandleFunc("/_matrix/app/v1/rooms/{roomAlias}", as.GetRoom).Methods(http.MethodGet) + as.Router.HandleFunc("/_matrix/app/v1/users/{userID}", as.GetUser).Methods(http.MethodGet) + as.Router.HandleFunc("/_matrix/app/v1/ping", as.PostPing).Methods(http.MethodPost) + as.Router.HandleFunc("/_matrix/app/unstable/fi.mau.msc2659/ping", as.PostPing).Methods(http.MethodPost) + as.Router.HandleFunc("/_matrix/mau/live", as.GetLive).Methods(http.MethodGet) + as.Router.HandleFunc("/_matrix/mau/ready", as.GetReady).Methods(http.MethodGet) + + return as +} + +// QueryHandler handles room alias and user ID queries from the homeserver. +type QueryHandler interface { + QueryAlias(alias string) bool + QueryUser(userID id.UserID) bool +} + +type QueryHandlerStub struct{} + +func (qh *QueryHandlerStub) QueryAlias(alias string) bool { + return false +} + +func (qh *QueryHandlerStub) QueryUser(userID id.UserID) bool { + return false +} + +type WebsocketHandler func(WebsocketCommand) (ok bool, data interface{}) + +type StateStore interface { + mautrix.StateStore + + IsRegistered(userID id.UserID) bool + MarkRegistered(userID id.UserID) + + GetPowerLevel(roomID id.RoomID, userID id.UserID) int + GetPowerLevelRequirement(roomID id.RoomID, eventType event.Type) int + HasPowerLevel(roomID id.RoomID, userID id.UserID, eventType event.Type) bool +} + +// AppService is the main config for all appservices. +// It also serves as the appservice instance struct. +type AppService struct { + HomeserverDomain string + hsURLForClient *url.URL + Host HostConfig + + Registration *Registration + Log zerolog.Logger + + txnIDC *TransactionIDCache + + Events chan *event.Event + ToDeviceEvents chan *event.Event + DeviceLists chan *mautrix.DeviceLists + OTKCounts chan *mautrix.OTKCount + QueryHandler QueryHandler + StateStore StateStore + + Router *mux.Router + UserAgent string + server *http.Server + HTTPClient *http.Client + botClient *mautrix.Client + botIntent *IntentAPI + + DefaultHTTPRetries int + + Live bool + Ready bool + + clients map[id.UserID]*mautrix.Client + clientsLock sync.RWMutex + intents map[id.UserID]*IntentAPI + intentsLock sync.RWMutex + + ws *websocket.Conn + wsWriteLock sync.Mutex + StopWebsocket func(error) + websocketHandlers map[string]WebsocketHandler + websocketHandlersLock sync.RWMutex + websocketRequests map[int]chan<- *WebsocketCommand + websocketRequestsLock sync.RWMutex + websocketRequestID int32 + // ProcessID is an identifier sent to the websocket proxy for debugging connections + ProcessID string + + DoublePuppetValue string + GetProfile func(userID id.UserID, roomID id.RoomID) *event.MemberEventContent +} + +const DoublePuppetKey = "fi.mau.double_puppet_source" + +func getDefaultProcessID() string { + pid := syscall.Getpid() + uid := syscall.Getuid() + hostname, _ := os.Hostname() + return fmt.Sprintf("%s-%d-%d", hostname, uid, pid) +} + +func (as *AppService) PrepareWebsocket() { + as.websocketHandlersLock.Lock() + defer as.websocketHandlersLock.Unlock() + if as.websocketHandlers == nil { + as.websocketHandlers = make(map[string]WebsocketHandler, 32) + as.websocketRequests = make(map[int]chan<- *WebsocketCommand) + } +} + +// HostConfig contains info about how to host the appservice. +type HostConfig struct { + Hostname string `yaml:"hostname"` + Port uint16 `yaml:"port"` + TLSKey string `yaml:"tls_key,omitempty"` + TLSCert string `yaml:"tls_cert,omitempty"` +} + +// Address gets the whole address of the Appservice. +func (hc *HostConfig) Address() string { + return fmt.Sprintf("%s:%d", hc.Hostname, hc.Port) +} + +func (hc *HostConfig) IsUnixSocket() bool { + return strings.HasPrefix(hc.Hostname, "/") +} + +func (hc *HostConfig) IsConfigured() bool { + return hc.IsUnixSocket() || hc.Port != 0 +} + +// Save saves this config into a file at the given path. +func (as *AppService) Save(path string) error { + data, err := yaml.Marshal(as) + if err != nil { + return err + } + return os.WriteFile(path, data, 0644) +} + +// YAML returns the config in YAML format. +func (as *AppService) YAML() (string, error) { + data, err := yaml.Marshal(as) + if err != nil { + return "", err + } + return string(data), nil +} + +func (as *AppService) BotMXID() id.UserID { + return id.NewUserID(as.Registration.SenderLocalpart, as.HomeserverDomain) +} + +func (as *AppService) makeIntent(userID id.UserID) *IntentAPI { + as.intentsLock.Lock() + defer as.intentsLock.Unlock() + + intent, ok := as.intents[userID] + if ok { + return intent + } + + localpart, homeserver, err := userID.Parse() + if err != nil || len(localpart) == 0 || homeserver != as.HomeserverDomain { + if err != nil { + as.Log.Error().Err(err). + Str("user_id", userID.String()). + Msg("Failed to parse user ID") + } else if len(localpart) == 0 { + as.Log.Error().Err(err). + Str("user_id", userID.String()). + Msg("Failed to make intent: localpart is empty") + } else if homeserver != as.HomeserverDomain { + as.Log.Error().Err(err). + Str("user_id", userID.String()). + Str("expected_homeserver", as.HomeserverDomain). + Msg("Failed to make intent: homeserver doesn't match") + } + return nil + } + intent = as.NewIntentAPI(localpart) + as.intents[userID] = intent + return intent +} + +func (as *AppService) Intent(userID id.UserID) *IntentAPI { + as.intentsLock.RLock() + intent, ok := as.intents[userID] + as.intentsLock.RUnlock() + if !ok { + return as.makeIntent(userID) + } + return intent +} + +func (as *AppService) BotIntent() *IntentAPI { + if as.botIntent == nil { + as.botIntent = as.makeIntent(as.BotMXID()) + } + return as.botIntent +} + +func (as *AppService) SetHomeserverURL(homeserverURL string) error { + parsedURL, err := url.Parse(homeserverURL) + if err != nil { + return err + } + + as.hsURLForClient = parsedURL + if as.hsURLForClient.Scheme == "unix" { + as.hsURLForClient.Scheme = "http" + as.hsURLForClient.Host = "unix" + as.hsURLForClient.Path = "" + } else if as.hsURLForClient.Scheme == "" { + as.hsURLForClient.Scheme = "https" + } + as.hsURLForClient.RawPath = parsedURL.EscapedPath() + + jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) + as.HTTPClient = &http.Client{Timeout: 180 * time.Second, Jar: jar} + if parsedURL.Scheme == "unix" { + as.HTTPClient.Transport = &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial("unix", parsedURL.Path) + }, + } + } + return nil +} + +func (as *AppService) NewMautrixClient(userID id.UserID) *mautrix.Client { + client := &mautrix.Client{ + HomeserverURL: as.hsURLForClient, + UserID: userID, + SetAppServiceUserID: true, + AccessToken: as.Registration.AppToken, + UserAgent: as.UserAgent, + StateStore: as.StateStore, + Log: as.Log.With().Str("as_user_id", userID.String()).Logger(), + Client: as.HTTPClient, + DefaultHTTPRetries: as.DefaultHTTPRetries, + } + client.Logger = maulogadapt.ZeroAsMau(&client.Log) + return client +} + +func (as *AppService) NewExternalMautrixClient(userID id.UserID, token string, homeserverURL string) (*mautrix.Client, error) { + client := as.NewMautrixClient(userID) + client.AccessToken = token + if homeserverURL != "" { + client.Client = &http.Client{Timeout: 180 * time.Second} + var err error + client.HomeserverURL, err = mautrix.ParseAndNormalizeBaseURL(homeserverURL) + if err != nil { + return nil, err + } + } + return client, nil +} + +func (as *AppService) makeClient(userID id.UserID) *mautrix.Client { + as.clientsLock.Lock() + defer as.clientsLock.Unlock() + + client, ok := as.clients[userID] + if !ok { + client = as.NewMautrixClient(userID) + as.clients[userID] = client + } + return client +} + +func (as *AppService) Client(userID id.UserID) *mautrix.Client { + as.clientsLock.RLock() + client, ok := as.clients[userID] + as.clientsLock.RUnlock() + if !ok { + return as.makeClient(userID) + } + return client +} + +func (as *AppService) BotClient() *mautrix.Client { + if as.botClient == nil { + as.botClient = as.makeClient(as.BotMXID()) + } + return as.botClient +} diff --git a/vendor/maunium.net/go/mautrix/appservice/eventprocessor.go b/vendor/maunium.net/go/mautrix/appservice/eventprocessor.go new file mode 100644 index 00000000..437d8536 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/appservice/eventprocessor.go @@ -0,0 +1,175 @@ +// Copyright (c) 2023 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package appservice + +import ( + "encoding/json" + "runtime/debug" + + "github.com/rs/zerolog" + + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" +) + +type ExecMode uint8 + +const ( + AsyncHandlers ExecMode = iota + AsyncLoop + Sync +) + +type EventHandler = func(evt *event.Event) +type OTKHandler = func(otk *mautrix.OTKCount) +type DeviceListHandler = func(lists *mautrix.DeviceLists, since string) + +type EventProcessor struct { + ExecMode ExecMode + + as *AppService + stop chan struct{} + handlers map[event.Type][]EventHandler + + otkHandlers []OTKHandler + deviceListHandlers []DeviceListHandler +} + +func NewEventProcessor(as *AppService) *EventProcessor { + return &EventProcessor{ + ExecMode: AsyncHandlers, + as: as, + stop: make(chan struct{}, 1), + handlers: make(map[event.Type][]EventHandler), + + otkHandlers: make([]OTKHandler, 0), + deviceListHandlers: make([]DeviceListHandler, 0), + } +} + +func (ep *EventProcessor) On(evtType event.Type, handler EventHandler) { + handlers, ok := ep.handlers[evtType] + if !ok { + handlers = []EventHandler{handler} + } else { + handlers = append(handlers, handler) + } + ep.handlers[evtType] = handlers +} + +func (ep *EventProcessor) PrependHandler(evtType event.Type, handler EventHandler) { + handlers, ok := ep.handlers[evtType] + if !ok { + handlers = []EventHandler{handler} + } else { + handlers = append([]EventHandler{handler}, handlers...) + } + ep.handlers[evtType] = handlers +} + +func (ep *EventProcessor) OnOTK(handler OTKHandler) { + ep.otkHandlers = append(ep.otkHandlers, handler) +} + +func (ep *EventProcessor) OnDeviceList(handler DeviceListHandler) { + ep.deviceListHandlers = append(ep.deviceListHandlers, handler) +} + +func (ep *EventProcessor) recoverFunc(data interface{}) { + if err := recover(); err != nil { + d, _ := json.Marshal(data) + ep.as.Log.Error(). + Str(zerolog.ErrorStackFieldName, string(debug.Stack())). + Interface(zerolog.ErrorFieldName, err). + Str("event_content", string(d)). + Msg("Panic in Matrix event handler") + } +} + +func (ep *EventProcessor) callHandler(handler EventHandler, evt *event.Event) { + defer ep.recoverFunc(evt) + handler(evt) +} + +func (ep *EventProcessor) callOTKHandler(handler OTKHandler, otk *mautrix.OTKCount) { + defer ep.recoverFunc(otk) + handler(otk) +} + +func (ep *EventProcessor) callDeviceListHandler(handler DeviceListHandler, dl *mautrix.DeviceLists) { + defer ep.recoverFunc(dl) + handler(dl, "") +} + +func (ep *EventProcessor) DispatchOTK(otk *mautrix.OTKCount) { + for _, handler := range ep.otkHandlers { + go ep.callOTKHandler(handler, otk) + } +} + +func (ep *EventProcessor) DispatchDeviceList(dl *mautrix.DeviceLists) { + for _, handler := range ep.deviceListHandlers { + go ep.callDeviceListHandler(handler, dl) + } +} + +func (ep *EventProcessor) Dispatch(evt *event.Event) { + handlers, ok := ep.handlers[evt.Type] + if !ok { + return + } + switch ep.ExecMode { + case AsyncHandlers: + for _, handler := range handlers { + go ep.callHandler(handler, evt) + } + case AsyncLoop: + go func() { + for _, handler := range handlers { + ep.callHandler(handler, evt) + } + }() + case Sync: + for _, handler := range handlers { + ep.callHandler(handler, evt) + } + } +} +func (ep *EventProcessor) startEvents() { + for { + select { + case evt := <-ep.as.Events: + ep.Dispatch(evt) + case <-ep.stop: + return + } + } +} + +func (ep *EventProcessor) startEncryption() { + for { + select { + case evt := <-ep.as.ToDeviceEvents: + ep.Dispatch(evt) + case otk := <-ep.as.OTKCounts: + ep.DispatchOTK(otk) + case dl := <-ep.as.DeviceLists: + ep.DispatchDeviceList(dl) + case <-ep.stop: + return + } + } +} + +func (ep *EventProcessor) Start() { + go ep.startEvents() + go ep.startEncryption() +} + +func (ep *EventProcessor) Stop() { + close(ep.stop) +} diff --git a/vendor/maunium.net/go/mautrix/appservice/http.go b/vendor/maunium.net/go/mautrix/appservice/http.go new file mode 100644 index 00000000..06ac7788 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/appservice/http.go @@ -0,0 +1,348 @@ +// Copyright (c) 2023 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package appservice + +import ( + "context" + "encoding/json" + "errors" + "io" + "net" + "net/http" + "strings" + "syscall" + "time" + + "github.com/gorilla/mux" + "github.com/rs/zerolog" + + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" +) + +// Start starts the HTTP server that listens for calls from the Matrix homeserver. +func (as *AppService) Start() { + as.server = &http.Server{ + Handler: as.Router, + } + var err error + if as.Host.IsUnixSocket() { + err = as.listenUnix() + } else { + as.server.Addr = as.Host.Address() + err = as.listenTCP() + } + if err != nil && !errors.Is(err, http.ErrServerClosed) { + as.Log.Error().Err(err).Msg("Error in HTTP listener") + } else { + as.Log.Debug().Msg("HTTP listener stopped") + } +} + +func (as *AppService) listenUnix() error { + socket := as.Host.Hostname + _ = syscall.Unlink(socket) + defer func() { + _ = syscall.Unlink(socket) + }() + listener, err := net.Listen("unix", socket) + if err != nil { + return err + } + as.Log.Info().Str("socket", socket).Msg("Starting unix socket HTTP listener") + return as.server.Serve(listener) +} + +func (as *AppService) listenTCP() error { + if len(as.Host.TLSCert) == 0 || len(as.Host.TLSKey) == 0 { + as.Log.Info().Str("address", as.server.Addr).Msg("Starting HTTP listener") + return as.server.ListenAndServe() + } else { + as.Log.Info().Str("address", as.server.Addr).Msg("Starting HTTP listener with TLS") + return as.server.ListenAndServeTLS(as.Host.TLSCert, as.Host.TLSKey) + } +} + +func (as *AppService) Stop() { + if as.server == nil { + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _ = as.server.Shutdown(ctx) + as.server = nil +} + +// CheckServerToken checks if the given request originated from the Matrix homeserver. +func (as *AppService) CheckServerToken(w http.ResponseWriter, r *http.Request) (isValid bool) { + authHeader := r.Header.Get("Authorization") + if len(authHeader) > 0 && strings.HasPrefix(authHeader, "Bearer ") { + isValid = authHeader[len("Bearer "):] == as.Registration.ServerToken + } else { + queryToken := r.URL.Query().Get("access_token") + if len(queryToken) > 0 { + isValid = queryToken == as.Registration.ServerToken + } else { + Error{ + ErrorCode: ErrUnknownToken, + HTTPStatus: http.StatusForbidden, + Message: "Missing access token", + }.Write(w) + return + } + } + if !isValid { + Error{ + ErrorCode: ErrUnknownToken, + HTTPStatus: http.StatusForbidden, + Message: "Incorrect access token", + }.Write(w) + } + return +} + +// PutTransaction handles a /transactions PUT call from the homeserver. +func (as *AppService) PutTransaction(w http.ResponseWriter, r *http.Request) { + if !as.CheckServerToken(w, r) { + return + } + + vars := mux.Vars(r) + txnID := vars["txnID"] + if len(txnID) == 0 { + Error{ + ErrorCode: ErrNoTransactionID, + HTTPStatus: http.StatusBadRequest, + Message: "Missing transaction ID", + }.Write(w) + return + } + defer r.Body.Close() + body, err := io.ReadAll(r.Body) + if err != nil || len(body) == 0 { + Error{ + ErrorCode: ErrNotJSON, + HTTPStatus: http.StatusBadRequest, + Message: "Missing request body", + }.Write(w) + return + } + log := as.Log.With().Str("transaction_id", txnID).Logger() + ctx := context.Background() + ctx = log.WithContext(ctx) + if as.txnIDC.IsProcessed(txnID) { + // Duplicate transaction ID: no-op + WriteBlankOK(w) + log.Debug().Msg("Ignoring duplicate transaction") + return + } + + var txn Transaction + err = json.Unmarshal(body, &txn) + if err != nil { + log.Error().Err(err).Msg("Failed to parse transaction content") + Error{ + ErrorCode: ErrBadJSON, + HTTPStatus: http.StatusBadRequest, + Message: "Failed to parse body JSON", + }.Write(w) + } else { + as.handleTransaction(ctx, txnID, &txn) + WriteBlankOK(w) + } +} + +func (as *AppService) handleTransaction(ctx context.Context, id string, txn *Transaction) { + log := zerolog.Ctx(ctx) + log.Debug().Object("content", txn).Msg("Starting handling of transaction") + if as.Registration.EphemeralEvents { + if txn.EphemeralEvents != nil { + as.handleEvents(ctx, txn.EphemeralEvents, event.EphemeralEventType) + } else if txn.MSC2409EphemeralEvents != nil { + as.handleEvents(ctx, txn.MSC2409EphemeralEvents, event.EphemeralEventType) + } + if txn.ToDeviceEvents != nil { + as.handleEvents(ctx, txn.ToDeviceEvents, event.ToDeviceEventType) + } else if txn.MSC2409ToDeviceEvents != nil { + as.handleEvents(ctx, txn.MSC2409ToDeviceEvents, event.ToDeviceEventType) + } + } + as.handleEvents(ctx, txn.Events, event.UnknownEventType) + if txn.DeviceLists != nil { + as.handleDeviceLists(ctx, txn.DeviceLists) + } else if txn.MSC3202DeviceLists != nil { + as.handleDeviceLists(ctx, txn.MSC3202DeviceLists) + } + if txn.DeviceOTKCount != nil { + as.handleOTKCounts(ctx, txn.DeviceOTKCount) + } else if txn.MSC3202DeviceOTKCount != nil { + as.handleOTKCounts(ctx, txn.MSC3202DeviceOTKCount) + } + as.txnIDC.MarkProcessed(id) + log.Debug().Msg("Finished dispatching events from transaction") +} + +func (as *AppService) handleOTKCounts(ctx context.Context, otks OTKCountMap) { + for userID, devices := range otks { + for deviceID, otkCounts := range devices { + otkCounts.UserID = userID + otkCounts.DeviceID = deviceID + select { + case as.OTKCounts <- &otkCounts: + default: + zerolog.Ctx(ctx).Warn(). + Str("user_id", userID.String()). + Msg("Dropped OTK count update for user because channel is full") + } + } + } +} + +func (as *AppService) handleDeviceLists(ctx context.Context, dl *mautrix.DeviceLists) { + select { + case as.DeviceLists <- dl: + default: + zerolog.Ctx(ctx).Warn().Msg("Dropped device list update because channel is full") + } +} + +func (as *AppService) handleEvents(ctx context.Context, evts []*event.Event, defaultTypeClass event.TypeClass) { + log := zerolog.Ctx(ctx) + for _, evt := range evts { + evt.Mautrix.ReceivedAt = time.Now() + if defaultTypeClass != event.UnknownEventType { + evt.Type.Class = defaultTypeClass + } else if evt.StateKey != nil { + evt.Type.Class = event.StateEventType + } else { + evt.Type.Class = event.MessageEventType + } + err := evt.Content.ParseRaw(evt.Type) + if errors.Is(err, event.ErrUnsupportedContentType) { + log.Debug().Str("event_id", evt.ID.String()).Msg("Not parsing content of unsupported event") + } else if err != nil { + log.Warn().Err(err). + Str("event_id", evt.ID.String()). + Str("event_type", evt.Type.Type). + Str("event_type_class", evt.Type.Class.Name()). + Msg("Failed to parse content of event") + } + + if evt.Type.IsState() { + // TODO remove this check after making sure the log doesn't happen + historical, ok := evt.Content.Raw["org.matrix.msc2716.historical"].(bool) + if ok && historical { + log.Warn(). + Str("event_id", evt.ID.String()). + Str("event_type", evt.Type.Type). + Str("state_key", evt.GetStateKey()). + Msg("Received historical state event") + } else { + mautrix.UpdateStateStore(as.StateStore, evt) + } + } + var ch chan *event.Event + if evt.Type.Class == event.ToDeviceEventType { + ch = as.ToDeviceEvents + } else { + ch = as.Events + } + select { + case ch <- evt: + default: + log.Warn(). + Str("event_id", evt.ID.String()). + Str("event_type", evt.Type.Type). + Str("event_type_class", evt.Type.Class.Name()). + Msg("Event channel is full") + ch <- evt + } + } +} + +// GetRoom handles a /rooms GET call from the homeserver. +func (as *AppService) GetRoom(w http.ResponseWriter, r *http.Request) { + if !as.CheckServerToken(w, r) { + return + } + + vars := mux.Vars(r) + roomAlias := vars["roomAlias"] + ok := as.QueryHandler.QueryAlias(roomAlias) + if ok { + WriteBlankOK(w) + } else { + Error{ + ErrorCode: ErrUnknown, + HTTPStatus: http.StatusNotFound, + }.Write(w) + } +} + +// GetUser handles a /users GET call from the homeserver. +func (as *AppService) GetUser(w http.ResponseWriter, r *http.Request) { + if !as.CheckServerToken(w, r) { + return + } + + vars := mux.Vars(r) + userID := id.UserID(vars["userID"]) + ok := as.QueryHandler.QueryUser(userID) + if ok { + WriteBlankOK(w) + } else { + Error{ + ErrorCode: ErrUnknown, + HTTPStatus: http.StatusNotFound, + }.Write(w) + } +} + +func (as *AppService) PostPing(w http.ResponseWriter, r *http.Request) { + if !as.CheckServerToken(w, r) { + return + } + body, err := io.ReadAll(r.Body) + if err != nil || len(body) == 0 || !json.Valid(body) { + Error{ + ErrorCode: ErrNotJSON, + HTTPStatus: http.StatusBadRequest, + Message: "Missing request body", + }.Write(w) + return + } + + var txn mautrix.ReqAppservicePing + _ = json.Unmarshal(body, &txn) + as.Log.Debug().Str("txn_id", txn.TxnID).Msg("Received ping from homeserver") + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte("{}")) +} + +func (as *AppService) GetLive(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + if as.Live { + w.WriteHeader(http.StatusOK) + } else { + w.WriteHeader(http.StatusInternalServerError) + } + w.Write([]byte("{}")) +} + +func (as *AppService) GetReady(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + if as.Ready { + w.WriteHeader(http.StatusOK) + } else { + w.WriteHeader(http.StatusInternalServerError) + } + w.Write([]byte("{}")) +} diff --git a/vendor/maunium.net/go/mautrix/appservice/intent.go b/vendor/maunium.net/go/mautrix/appservice/intent.go new file mode 100644 index 00000000..af6fea37 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/appservice/intent.go @@ -0,0 +1,419 @@ +// Copyright (c) 2020 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package appservice + +import ( + "errors" + "fmt" + "strings" + + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" +) + +type IntentAPI struct { + *mautrix.Client + bot *mautrix.Client + as *AppService + Localpart string + UserID id.UserID + + IsCustomPuppet bool +} + +func (as *AppService) NewIntentAPI(localpart string) *IntentAPI { + userID := id.NewUserID(localpart, as.HomeserverDomain) + bot := as.BotClient() + if userID == bot.UserID { + bot = nil + } + return &IntentAPI{ + Client: as.Client(userID), + bot: bot, + as: as, + Localpart: localpart, + UserID: userID, + + IsCustomPuppet: false, + } +} + +func (intent *IntentAPI) Register() error { + _, _, err := intent.Client.Register(&mautrix.ReqRegister{ + Username: intent.Localpart, + Type: mautrix.AuthTypeAppservice, + InhibitLogin: true, + }) + return err +} + +func (intent *IntentAPI) EnsureRegistered() error { + if intent.IsCustomPuppet || intent.as.StateStore.IsRegistered(intent.UserID) { + return nil + } + + err := intent.Register() + if err != nil && !errors.Is(err, mautrix.MUserInUse) { + return fmt.Errorf("failed to ensure registered: %w", err) + } + intent.as.StateStore.MarkRegistered(intent.UserID) + return nil +} + +type EnsureJoinedParams struct { + IgnoreCache bool + BotOverride *mautrix.Client +} + +func (intent *IntentAPI) EnsureJoined(roomID id.RoomID, extra ...EnsureJoinedParams) error { + var params EnsureJoinedParams + if len(extra) > 1 { + panic("invalid number of extra parameters") + } else if len(extra) == 1 { + params = extra[0] + } + if intent.as.StateStore.IsInRoom(roomID, intent.UserID) && !params.IgnoreCache { + return nil + } + + if err := intent.EnsureRegistered(); err != nil { + return fmt.Errorf("failed to ensure joined: %w", err) + } + + resp, err := intent.JoinRoomByID(roomID) + if err != nil { + bot := intent.bot + if params.BotOverride != nil { + bot = params.BotOverride + } + if !errors.Is(err, mautrix.MForbidden) || bot == nil { + return fmt.Errorf("failed to ensure joined: %w", err) + } + _, inviteErr := bot.InviteUser(roomID, &mautrix.ReqInviteUser{ + UserID: intent.UserID, + }) + if inviteErr != nil { + return fmt.Errorf("failed to invite in ensure joined: %w", inviteErr) + } + resp, err = intent.JoinRoomByID(roomID) + if err != nil { + return fmt.Errorf("failed to ensure joined after invite: %w", err) + } + } + intent.as.StateStore.SetMembership(resp.RoomID, intent.UserID, event.MembershipJoin) + return nil +} + +func (intent *IntentAPI) AddDoublePuppetValue(into interface{}) interface{} { + if !intent.IsCustomPuppet || intent.as.DoublePuppetValue == "" { + return into + } + switch val := into.(type) { + case *map[string]interface{}: + if *val == nil { + valNonPtr := make(map[string]interface{}) + *val = valNonPtr + } + (*val)[DoublePuppetKey] = intent.as.DoublePuppetValue + return val + case map[string]interface{}: + val[DoublePuppetKey] = intent.as.DoublePuppetValue + return val + case *event.Content: + if val.Raw == nil { + val.Raw = make(map[string]interface{}) + } + val.Raw[DoublePuppetKey] = intent.as.DoublePuppetValue + return val + case event.Content: + if val.Raw == nil { + val.Raw = make(map[string]interface{}) + } + val.Raw[DoublePuppetKey] = intent.as.DoublePuppetValue + return val + default: + return &event.Content{ + Raw: map[string]interface{}{ + DoublePuppetKey: intent.as.DoublePuppetValue, + }, + Parsed: val, + } + } +} + +func (intent *IntentAPI) SendMessageEvent(roomID id.RoomID, eventType event.Type, contentJSON interface{}) (*mautrix.RespSendEvent, error) { + if err := intent.EnsureJoined(roomID); err != nil { + return nil, err + } + contentJSON = intent.AddDoublePuppetValue(contentJSON) + return intent.Client.SendMessageEvent(roomID, eventType, contentJSON) +} + +func (intent *IntentAPI) SendMassagedMessageEvent(roomID id.RoomID, eventType event.Type, contentJSON interface{}, ts int64) (*mautrix.RespSendEvent, error) { + if err := intent.EnsureJoined(roomID); err != nil { + return nil, err + } + contentJSON = intent.AddDoublePuppetValue(contentJSON) + return intent.Client.SendMessageEvent(roomID, eventType, contentJSON, mautrix.ReqSendEvent{Timestamp: ts}) +} + +func (intent *IntentAPI) SendStateEvent(roomID id.RoomID, eventType event.Type, stateKey string, contentJSON interface{}) (*mautrix.RespSendEvent, error) { + if eventType != event.StateMember || stateKey != string(intent.UserID) { + if err := intent.EnsureJoined(roomID); err != nil { + return nil, err + } + } + contentJSON = intent.AddDoublePuppetValue(contentJSON) + return intent.Client.SendStateEvent(roomID, eventType, stateKey, contentJSON) +} + +func (intent *IntentAPI) SendMassagedStateEvent(roomID id.RoomID, eventType event.Type, stateKey string, contentJSON interface{}, ts int64) (*mautrix.RespSendEvent, error) { + if err := intent.EnsureJoined(roomID); err != nil { + return nil, err + } + contentJSON = intent.AddDoublePuppetValue(contentJSON) + return intent.Client.SendMassagedStateEvent(roomID, eventType, stateKey, contentJSON, ts) +} + +func (intent *IntentAPI) StateEvent(roomID id.RoomID, eventType event.Type, stateKey string, outContent interface{}) error { + if err := intent.EnsureJoined(roomID); err != nil { + return err + } + return intent.Client.StateEvent(roomID, eventType, stateKey, outContent) +} + +func (intent *IntentAPI) State(roomID id.RoomID) (mautrix.RoomStateMap, error) { + if err := intent.EnsureJoined(roomID); err != nil { + return nil, err + } + return intent.Client.State(roomID) +} + +func (intent *IntentAPI) SendCustomMembershipEvent(roomID id.RoomID, target id.UserID, membership event.Membership, reason string, extraContent ...map[string]interface{}) (*mautrix.RespSendEvent, error) { + content := &event.MemberEventContent{ + Membership: membership, + Reason: reason, + } + memberContent, ok := intent.as.StateStore.TryGetMember(roomID, target) + if !ok { + if intent.as.GetProfile != nil { + memberContent = intent.as.GetProfile(target, roomID) + ok = memberContent != nil + } + if !ok { + profile, err := intent.GetProfile(target) + if err != nil { + intent.Log.Debug().Err(err). + Str("target_user_id", target.String()). + Str("membership", string(membership)). + Msg("Failed to get profile to fill new membership event") + } else { + content.Displayname = profile.DisplayName + content.AvatarURL = profile.AvatarURL.CUString() + } + } + } + if ok && memberContent != nil { + content.Displayname = memberContent.Displayname + content.AvatarURL = memberContent.AvatarURL + } + var extra map[string]interface{} + if len(extraContent) > 0 { + extra = extraContent[0] + } + return intent.SendStateEvent(roomID, event.StateMember, target.String(), &event.Content{ + Parsed: content, + Raw: extra, + }) +} + +func (intent *IntentAPI) JoinRoomByID(roomID id.RoomID, extraContent ...map[string]interface{}) (resp *mautrix.RespJoinRoom, err error) { + if intent.IsCustomPuppet || len(extraContent) > 0 { + _, err = intent.SendCustomMembershipEvent(roomID, intent.UserID, event.MembershipJoin, "", extraContent...) + return &mautrix.RespJoinRoom{}, err + } + return intent.Client.JoinRoomByID(roomID) +} + +func (intent *IntentAPI) LeaveRoom(roomID id.RoomID, extra ...interface{}) (resp *mautrix.RespLeaveRoom, err error) { + var extraContent map[string]interface{} + leaveReq := &mautrix.ReqLeave{} + for _, item := range extra { + switch val := item.(type) { + case map[string]interface{}: + extraContent = val + case *mautrix.ReqLeave: + leaveReq = val + } + } + if intent.IsCustomPuppet || extraContent != nil { + _, err = intent.SendCustomMembershipEvent(roomID, intent.UserID, event.MembershipLeave, leaveReq.Reason, extraContent) + return &mautrix.RespLeaveRoom{}, err + } + return intent.Client.LeaveRoom(roomID, leaveReq) +} + +func (intent *IntentAPI) InviteUser(roomID id.RoomID, req *mautrix.ReqInviteUser, extraContent ...map[string]interface{}) (resp *mautrix.RespInviteUser, err error) { + if intent.IsCustomPuppet || len(extraContent) > 0 { + _, err = intent.SendCustomMembershipEvent(roomID, req.UserID, event.MembershipInvite, req.Reason, extraContent...) + return &mautrix.RespInviteUser{}, err + } + return intent.Client.InviteUser(roomID, req) +} + +func (intent *IntentAPI) KickUser(roomID id.RoomID, req *mautrix.ReqKickUser, extraContent ...map[string]interface{}) (resp *mautrix.RespKickUser, err error) { + if intent.IsCustomPuppet || len(extraContent) > 0 { + _, err = intent.SendCustomMembershipEvent(roomID, req.UserID, event.MembershipLeave, req.Reason, extraContent...) + return &mautrix.RespKickUser{}, err + } + return intent.Client.KickUser(roomID, req) +} + +func (intent *IntentAPI) BanUser(roomID id.RoomID, req *mautrix.ReqBanUser, extraContent ...map[string]interface{}) (resp *mautrix.RespBanUser, err error) { + if intent.IsCustomPuppet || len(extraContent) > 0 { + _, err = intent.SendCustomMembershipEvent(roomID, req.UserID, event.MembershipBan, req.Reason, extraContent...) + return &mautrix.RespBanUser{}, err + } + return intent.Client.BanUser(roomID, req) +} + +func (intent *IntentAPI) UnbanUser(roomID id.RoomID, req *mautrix.ReqUnbanUser, extraContent ...map[string]interface{}) (resp *mautrix.RespUnbanUser, err error) { + if intent.IsCustomPuppet || len(extraContent) > 0 { + _, err = intent.SendCustomMembershipEvent(roomID, req.UserID, event.MembershipLeave, req.Reason, extraContent...) + return &mautrix.RespUnbanUser{}, err + } + return intent.Client.UnbanUser(roomID, req) +} + +func (intent *IntentAPI) Member(roomID id.RoomID, userID id.UserID) *event.MemberEventContent { + member, ok := intent.as.StateStore.TryGetMember(roomID, userID) + if !ok { + _ = intent.StateEvent(roomID, event.StateMember, string(userID), &member) + } + return member +} + +func (intent *IntentAPI) PowerLevels(roomID id.RoomID) (pl *event.PowerLevelsEventContent, err error) { + pl = intent.as.StateStore.GetPowerLevels(roomID) + if pl == nil { + pl = &event.PowerLevelsEventContent{} + err = intent.StateEvent(roomID, event.StatePowerLevels, "", pl) + } + return +} + +func (intent *IntentAPI) SetPowerLevels(roomID id.RoomID, levels *event.PowerLevelsEventContent) (resp *mautrix.RespSendEvent, err error) { + return intent.SendStateEvent(roomID, event.StatePowerLevels, "", &levels) +} + +func (intent *IntentAPI) SetPowerLevel(roomID id.RoomID, userID id.UserID, level int) (*mautrix.RespSendEvent, error) { + pl, err := intent.PowerLevels(roomID) + if err != nil { + return nil, err + } + + if pl.GetUserLevel(userID) != level { + pl.SetUserLevel(userID, level) + return intent.SendStateEvent(roomID, event.StatePowerLevels, "", &pl) + } + return nil, nil +} + +func (intent *IntentAPI) SendText(roomID id.RoomID, text string) (*mautrix.RespSendEvent, error) { + if err := intent.EnsureJoined(roomID); err != nil { + return nil, err + } + return intent.Client.SendText(roomID, text) +} + +func (intent *IntentAPI) SendNotice(roomID id.RoomID, text string) (*mautrix.RespSendEvent, error) { + if err := intent.EnsureJoined(roomID); err != nil { + return nil, err + } + return intent.Client.SendNotice(roomID, text) +} + +func (intent *IntentAPI) RedactEvent(roomID id.RoomID, eventID id.EventID, extra ...mautrix.ReqRedact) (*mautrix.RespSendEvent, error) { + if err := intent.EnsureJoined(roomID); err != nil { + return nil, err + } + var req mautrix.ReqRedact + if len(extra) > 0 { + req = extra[0] + } + intent.AddDoublePuppetValue(&req.Extra) + return intent.Client.RedactEvent(roomID, eventID, req) +} + +func (intent *IntentAPI) SetRoomName(roomID id.RoomID, roomName string) (*mautrix.RespSendEvent, error) { + return intent.SendStateEvent(roomID, event.StateRoomName, "", map[string]interface{}{ + "name": roomName, + }) +} + +func (intent *IntentAPI) SetRoomAvatar(roomID id.RoomID, avatarURL id.ContentURI) (*mautrix.RespSendEvent, error) { + return intent.SendStateEvent(roomID, event.StateRoomAvatar, "", map[string]interface{}{ + "url": avatarURL.String(), + }) +} + +func (intent *IntentAPI) SetRoomTopic(roomID id.RoomID, topic string) (*mautrix.RespSendEvent, error) { + return intent.SendStateEvent(roomID, event.StateTopic, "", map[string]interface{}{ + "topic": topic, + }) +} + +func (intent *IntentAPI) SetDisplayName(displayName string) error { + if err := intent.EnsureRegistered(); err != nil { + return err + } + resp, err := intent.Client.GetOwnDisplayName() + if err != nil { + return fmt.Errorf("failed to check current displayname: %w", err) + } else if resp.DisplayName == displayName { + // No need to update + return nil + } + return intent.Client.SetDisplayName(displayName) +} + +func (intent *IntentAPI) SetAvatarURL(avatarURL id.ContentURI) error { + if err := intent.EnsureRegistered(); err != nil { + return err + } + resp, err := intent.Client.GetOwnAvatarURL() + if err != nil { + return fmt.Errorf("failed to check current avatar URL: %w", err) + } else if resp.FileID == avatarURL.FileID && resp.Homeserver == avatarURL.Homeserver { + // No need to update + return nil + } + return intent.Client.SetAvatarURL(avatarURL) +} + +func (intent *IntentAPI) Whoami() (*mautrix.RespWhoami, error) { + if err := intent.EnsureRegistered(); err != nil { + return nil, err + } + return intent.Client.Whoami() +} + +func (intent *IntentAPI) EnsureInvited(roomID id.RoomID, userID id.UserID) error { + if !intent.as.StateStore.IsInvited(roomID, userID) { + _, err := intent.InviteUser(roomID, &mautrix.ReqInviteUser{ + UserID: userID, + }) + if httpErr, ok := err.(mautrix.HTTPError); ok && + httpErr.RespError != nil && + (strings.Contains(httpErr.RespError.Err, "is already in the room") || strings.Contains(httpErr.RespError.Err, "is already joined to room")) { + return nil + } + return err + } + return nil +} diff --git a/vendor/maunium.net/go/mautrix/appservice/protocol.go b/vendor/maunium.net/go/mautrix/appservice/protocol.go new file mode 100644 index 00000000..7a9891ef --- /dev/null +++ b/vendor/maunium.net/go/mautrix/appservice/protocol.go @@ -0,0 +1,152 @@ +// Copyright (c) 2023 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package appservice + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/rs/zerolog" + + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" +) + +type OTKCountMap = map[id.UserID]map[id.DeviceID]mautrix.OTKCount +type FallbackKeyMap = map[id.UserID]map[id.DeviceID][]id.KeyAlgorithm + +// Transaction contains a list of events. +type Transaction struct { + Events []*event.Event `json:"events"` + EphemeralEvents []*event.Event `json:"ephemeral,omitempty"` + ToDeviceEvents []*event.Event `json:"to_device,omitempty"` + + DeviceLists *mautrix.DeviceLists `json:"device_lists,omitempty"` + DeviceOTKCount OTKCountMap `json:"device_one_time_keys_count,omitempty"` + FallbackKeys FallbackKeyMap `json:"device_unused_fallback_key_types,omitempty"` + + MSC2409EphemeralEvents []*event.Event `json:"de.sorunome.msc2409.ephemeral,omitempty"` + MSC2409ToDeviceEvents []*event.Event `json:"de.sorunome.msc2409.to_device,omitempty"` + MSC3202DeviceLists *mautrix.DeviceLists `json:"org.matrix.msc3202.device_lists,omitempty"` + MSC3202DeviceOTKCount OTKCountMap `json:"org.matrix.msc3202.device_one_time_keys_count,omitempty"` + MSC3202FallbackKeys FallbackKeyMap `json:"org.matrix.msc3202.device_unused_fallback_key_types,omitempty"` +} + +func (txn *Transaction) MarshalZerologObject(ctx *zerolog.Event) { + ctx.Int("pdu", len(txn.Events)) + if txn.EphemeralEvents != nil { + ctx.Int("edu", len(txn.EphemeralEvents)) + } else if txn.MSC2409EphemeralEvents != nil { + ctx.Int("unstable_edu", len(txn.MSC2409EphemeralEvents)) + } + if txn.ToDeviceEvents != nil { + ctx.Int("to_device", len(txn.ToDeviceEvents)) + } else if txn.MSC2409ToDeviceEvents != nil { + ctx.Int("unstable_to_device", len(txn.MSC2409ToDeviceEvents)) + } + if len(txn.DeviceOTKCount) > 0 { + ctx.Int("otk_count_users", len(txn.DeviceOTKCount)) + } else if len(txn.MSC3202DeviceOTKCount) > 0 { + ctx.Int("unstable_otk_count_users", len(txn.MSC3202DeviceOTKCount)) + } + if txn.DeviceLists != nil { + ctx.Int("device_changes", len(txn.DeviceLists.Changed)) + } else if txn.MSC3202DeviceLists != nil { + ctx.Int("unstable_device_changes", len(txn.MSC3202DeviceLists.Changed)) + } + if txn.FallbackKeys != nil { + ctx.Int("fallback_key_users", len(txn.FallbackKeys)) + } else if txn.MSC3202FallbackKeys != nil { + ctx.Int("unstable_fallback_key_users", len(txn.MSC3202FallbackKeys)) + } +} + +func (txn *Transaction) ContentString() string { + var parts []string + if len(txn.Events) > 0 { + parts = append(parts, fmt.Sprintf("%d PDUs", len(txn.Events))) + } + if len(txn.EphemeralEvents) > 0 { + parts = append(parts, fmt.Sprintf("%d EDUs", len(txn.EphemeralEvents))) + } else if len(txn.MSC2409EphemeralEvents) > 0 { + parts = append(parts, fmt.Sprintf("%d EDUs (unstable)", len(txn.MSC2409EphemeralEvents))) + } + if len(txn.ToDeviceEvents) > 0 { + parts = append(parts, fmt.Sprintf("%d to-device events", len(txn.ToDeviceEvents))) + } else if len(txn.MSC2409ToDeviceEvents) > 0 { + parts = append(parts, fmt.Sprintf("%d to-device events (unstable)", len(txn.MSC2409ToDeviceEvents))) + } + if len(txn.DeviceOTKCount) > 0 { + parts = append(parts, fmt.Sprintf("OTK counts for %d users", len(txn.DeviceOTKCount))) + } else if len(txn.MSC3202DeviceOTKCount) > 0 { + parts = append(parts, fmt.Sprintf("OTK counts for %d users (unstable)", len(txn.MSC3202DeviceOTKCount))) + } + if txn.DeviceLists != nil { + parts = append(parts, fmt.Sprintf("%d device list changes", len(txn.DeviceLists.Changed))) + } else if txn.MSC3202DeviceLists != nil { + parts = append(parts, fmt.Sprintf("%d device list changes (unstable)", len(txn.MSC3202DeviceLists.Changed))) + } + if txn.FallbackKeys != nil { + parts = append(parts, fmt.Sprintf("unused fallback key counts for %d users", len(txn.FallbackKeys))) + } else if txn.MSC3202FallbackKeys != nil { + parts = append(parts, fmt.Sprintf("unused fallback key counts for %d users (unstable)", len(txn.MSC3202FallbackKeys))) + } + return strings.Join(parts, ", ") +} + +// EventListener is a function that receives events. +type EventListener func(evt *event.Event) + +// WriteBlankOK writes a blank OK message as a reply to a HTTP request. +func WriteBlankOK(w http.ResponseWriter) { + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("{}")) +} + +// Respond responds to a HTTP request with a JSON object. +func Respond(w http.ResponseWriter, data interface{}) error { + w.Header().Add("Content-Type", "application/json") + dataStr, err := json.Marshal(data) + if err != nil { + return err + } + _, err = w.Write(dataStr) + return err +} + +// Error represents a Matrix protocol error. +type Error struct { + HTTPStatus int `json:"-"` + ErrorCode ErrorCode `json:"errcode"` + Message string `json:"error"` +} + +func (err Error) Write(w http.ResponseWriter) { + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(err.HTTPStatus) + _ = Respond(w, &err) +} + +// ErrorCode is the machine-readable code in an Error. +type ErrorCode string + +// Native ErrorCodes +const ( + ErrUnknownToken ErrorCode = "M_UNKNOWN_TOKEN" + ErrBadJSON ErrorCode = "M_BAD_JSON" + ErrNotJSON ErrorCode = "M_NOT_JSON" + ErrUnknown ErrorCode = "M_UNKNOWN" +) + +// Custom ErrorCodes +const ( + ErrNoTransactionID ErrorCode = "NET.MAUNIUM.NO_TRANSACTION_ID" +) diff --git a/vendor/maunium.net/go/mautrix/appservice/registration.go b/vendor/maunium.net/go/mautrix/appservice/registration.go new file mode 100644 index 00000000..f9c93fe4 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/appservice/registration.go @@ -0,0 +1,100 @@ +// Copyright (c) 2022 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package appservice + +import ( + "os" + "regexp" + + "gopkg.in/yaml.v3" + + "maunium.net/go/mautrix/util" +) + +// Registration contains the data in a Matrix appservice registration. +// See https://spec.matrix.org/v1.2/application-service-api/#registration +type Registration struct { + ID string `yaml:"id" json:"id"` + URL string `yaml:"url" json:"url"` + AppToken string `yaml:"as_token" json:"as_token"` + ServerToken string `yaml:"hs_token" json:"hs_token"` + SenderLocalpart string `yaml:"sender_localpart" json:"sender_localpart"` + RateLimited *bool `yaml:"rate_limited,omitempty" json:"rate_limited,omitempty"` + Namespaces Namespaces `yaml:"namespaces" json:"namespaces"` + Protocols []string `yaml:"protocols,omitempty" json:"protocols,omitempty"` + + SoruEphemeralEvents bool `yaml:"de.sorunome.msc2409.push_ephemeral,omitempty" json:"de.sorunome.msc2409.push_ephemeral,omitempty"` + EphemeralEvents bool `yaml:"push_ephemeral,omitempty" json:"push_ephemeral,omitempty"` +} + +// CreateRegistration creates a Registration with random appservice and homeserver tokens. +func CreateRegistration() *Registration { + return &Registration{ + AppToken: util.RandomString(64), + ServerToken: util.RandomString(64), + } +} + +// LoadRegistration loads a YAML file and turns it into a Registration. +func LoadRegistration(path string) (*Registration, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + reg := &Registration{} + err = yaml.Unmarshal(data, reg) + if err != nil { + return nil, err + } + return reg, nil +} + +// Save saves this Registration into a file at the given path. +func (reg *Registration) Save(path string) error { + data, err := yaml.Marshal(reg) + if err != nil { + return err + } + return os.WriteFile(path, data, 0600) +} + +// YAML returns the registration in YAML format. +func (reg *Registration) YAML() (string, error) { + data, err := yaml.Marshal(reg) + if err != nil { + return "", err + } + return string(data), nil +} + +// Namespaces contains the three areas that appservices can reserve parts of. +type Namespaces struct { + UserIDs NamespaceList `yaml:"users,omitempty" json:"users,omitempty"` + RoomAliases NamespaceList `yaml:"aliases,omitempty" json:"aliases,omitempty"` + RoomIDs NamespaceList `yaml:"rooms,omitempty" json:"rooms,omitempty"` +} + +// Namespace is a reserved namespace in any area. +type Namespace struct { + Regex string `yaml:"regex" json:"regex"` + Exclusive bool `yaml:"exclusive" json:"exclusive"` +} + +type NamespaceList []Namespace + +func (nsl *NamespaceList) Register(regex *regexp.Regexp, exclusive bool) { + ns := Namespace{ + Regex: regex.String(), + Exclusive: exclusive, + } + if nsl == nil { + *nsl = []Namespace{ns} + } else { + *nsl = append(*nsl, ns) + } +} diff --git a/vendor/maunium.net/go/mautrix/appservice/txnid.go b/vendor/maunium.net/go/mautrix/appservice/txnid.go new file mode 100644 index 00000000..213703c5 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/appservice/txnid.go @@ -0,0 +1,43 @@ +// Copyright (c) 2021 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package appservice + +import "sync" + +type TransactionIDCache struct { + array []string + arrayPtr int + hash map[string]struct{} + lock sync.RWMutex +} + +func NewTransactionIDCache(size int) *TransactionIDCache { + return &TransactionIDCache{ + array: make([]string, size), + hash: make(map[string]struct{}), + } +} + +func (txnIDC *TransactionIDCache) IsProcessed(txnID string) bool { + txnIDC.lock.RLock() + _, exists := txnIDC.hash[txnID] + txnIDC.lock.RUnlock() + return exists +} + +func (txnIDC *TransactionIDCache) MarkProcessed(txnID string) { + txnIDC.lock.Lock() + txnIDC.hash[txnID] = struct{}{} + if txnIDC.array[txnIDC.arrayPtr] != "" { + for i := 0; i < len(txnIDC.array)/8; i++ { + delete(txnIDC.hash, txnIDC.array[txnIDC.arrayPtr+i]) + txnIDC.array[txnIDC.arrayPtr+i] = "" + } + } + txnIDC.array[txnIDC.arrayPtr] = txnID + txnIDC.lock.Unlock() +} diff --git a/vendor/maunium.net/go/mautrix/appservice/websocket.go b/vendor/maunium.net/go/mautrix/appservice/websocket.go new file mode 100644 index 00000000..671222b8 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/appservice/websocket.go @@ -0,0 +1,408 @@ +// Copyright (c) 2023 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package appservice + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "path/filepath" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/gorilla/websocket" + "github.com/rs/zerolog" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +type WebsocketRequest struct { + ReqID int `json:"id,omitempty"` + Command string `json:"command"` + Data interface{} `json:"data"` + + Deadline time.Duration `json:"-"` +} + +type WebsocketCommand struct { + ReqID int `json:"id,omitempty"` + Command string `json:"command"` + Data json.RawMessage `json:"data"` + + Ctx context.Context `json:"-"` +} + +func (wsc *WebsocketCommand) MakeResponse(ok bool, data interface{}) *WebsocketRequest { + if wsc.ReqID == 0 || wsc.Command == "response" || wsc.Command == "error" { + return nil + } + cmd := "response" + if !ok { + cmd = "error" + } + if err, isError := data.(error); isError { + var errorData json.RawMessage + var jsonErr error + unwrappedErr := err + var prefixMessage string + for unwrappedErr != nil { + errorData, jsonErr = json.Marshal(unwrappedErr) + if errorData != nil && len(errorData) > 2 && jsonErr == nil { + prefixMessage = strings.Replace(err.Error(), unwrappedErr.Error(), "", 1) + prefixMessage = strings.TrimRight(prefixMessage, ": ") + break + } + unwrappedErr = errors.Unwrap(unwrappedErr) + } + if errorData != nil { + if !gjson.GetBytes(errorData, "message").Exists() { + errorData, _ = sjson.SetBytes(errorData, "message", err.Error()) + } // else: marshaled error contains a message already + } else { + errorData, _ = sjson.SetBytes(nil, "message", err.Error()) + } + if len(prefixMessage) > 0 { + errorData, _ = sjson.SetBytes(errorData, "prefix_message", prefixMessage) + } + data = errorData + } + return &WebsocketRequest{ + ReqID: wsc.ReqID, + Command: cmd, + Data: data, + } +} + +type WebsocketTransaction struct { + Status string `json:"status"` + TxnID string `json:"txn_id"` + Transaction +} + +type WebsocketTransactionResponse struct { + TxnID string `json:"txn_id"` +} + +type WebsocketMessage struct { + WebsocketTransaction + WebsocketCommand +} + +const ( + WebsocketCloseConnReplaced = 4001 + WebsocketCloseTxnNotAcknowledged = 4002 +) + +type MeowWebsocketCloseCode string + +const ( + MeowServerShuttingDown MeowWebsocketCloseCode = "server_shutting_down" + MeowConnectionReplaced MeowWebsocketCloseCode = "conn_replaced" + MeowTxnNotAcknowledged MeowWebsocketCloseCode = "transactions_not_acknowledged" +) + +var ( + ErrWebsocketManualStop = errors.New("the websocket was disconnected manually") + ErrWebsocketOverridden = errors.New("a new call to StartWebsocket overrode the previous connection") + ErrWebsocketUnknownError = errors.New("an unknown error occurred") + + ErrWebsocketNotConnected = errors.New("websocket not connected") + ErrWebsocketClosed = errors.New("websocket closed before response received") +) + +func (mwcc MeowWebsocketCloseCode) String() string { + switch mwcc { + case MeowServerShuttingDown: + return "the server is shutting down" + case MeowConnectionReplaced: + return "the connection was replaced by another client" + case MeowTxnNotAcknowledged: + return "transactions were not acknowledged" + default: + return string(mwcc) + } +} + +type CloseCommand struct { + Code int `json:"-"` + Command string `json:"command"` + Status MeowWebsocketCloseCode `json:"status"` +} + +func (cc CloseCommand) Error() string { + return fmt.Sprintf("websocket: close %d: %s", cc.Code, cc.Status.String()) +} + +func parseCloseError(err error) error { + closeError := &websocket.CloseError{} + if !errors.As(err, &closeError) { + return err + } + var closeCommand CloseCommand + closeCommand.Code = closeError.Code + closeCommand.Command = "disconnect" + if len(closeError.Text) > 0 { + jsonErr := json.Unmarshal([]byte(closeError.Text), &closeCommand) + if jsonErr != nil { + return err + } + } + if len(closeCommand.Status) == 0 { + if closeCommand.Code == WebsocketCloseConnReplaced { + closeCommand.Status = MeowConnectionReplaced + } else if closeCommand.Code == websocket.CloseServiceRestart { + closeCommand.Status = MeowServerShuttingDown + } + } + return &closeCommand +} + +func (as *AppService) HasWebsocket() bool { + return as.ws != nil +} + +func (as *AppService) SendWebsocket(cmd *WebsocketRequest) error { + ws := as.ws + if cmd == nil { + return nil + } else if ws == nil { + return ErrWebsocketNotConnected + } + as.wsWriteLock.Lock() + defer as.wsWriteLock.Unlock() + if cmd.Deadline == 0 { + cmd.Deadline = 3 * time.Minute + } + _ = ws.SetWriteDeadline(time.Now().Add(cmd.Deadline)) + return ws.WriteJSON(cmd) +} + +func (as *AppService) clearWebsocketResponseWaiters() { + as.websocketRequestsLock.Lock() + for _, waiter := range as.websocketRequests { + waiter <- &WebsocketCommand{Command: "__websocket_closed"} + } + as.websocketRequests = make(map[int]chan<- *WebsocketCommand) + as.websocketRequestsLock.Unlock() +} + +func (as *AppService) addWebsocketResponseWaiter(reqID int, waiter chan<- *WebsocketCommand) { + as.websocketRequestsLock.Lock() + as.websocketRequests[reqID] = waiter + as.websocketRequestsLock.Unlock() +} + +func (as *AppService) removeWebsocketResponseWaiter(reqID int, waiter chan<- *WebsocketCommand) { + as.websocketRequestsLock.Lock() + existingWaiter, ok := as.websocketRequests[reqID] + if ok && existingWaiter == waiter { + delete(as.websocketRequests, reqID) + } + close(waiter) + as.websocketRequestsLock.Unlock() +} + +type ErrorResponse struct { + Code string `json:"code"` + Message string `json:"message"` +} + +func (er *ErrorResponse) Error() string { + return fmt.Sprintf("%s: %s", er.Code, er.Message) +} + +func (as *AppService) RequestWebsocket(ctx context.Context, cmd *WebsocketRequest, response interface{}) error { + cmd.ReqID = int(atomic.AddInt32(&as.websocketRequestID, 1)) + respChan := make(chan *WebsocketCommand, 1) + as.addWebsocketResponseWaiter(cmd.ReqID, respChan) + defer as.removeWebsocketResponseWaiter(cmd.ReqID, respChan) + err := as.SendWebsocket(cmd) + if err != nil { + return err + } + select { + case resp := <-respChan: + if resp.Command == "__websocket_closed" { + return ErrWebsocketClosed + } else if resp.Command == "error" { + var respErr ErrorResponse + err = json.Unmarshal(resp.Data, &respErr) + if err != nil { + return fmt.Errorf("failed to parse error JSON: %w", err) + } + return &respErr + } else if response != nil { + err = json.Unmarshal(resp.Data, &response) + if err != nil { + return fmt.Errorf("failed to parse response JSON: %w", err) + } + return nil + } else { + return nil + } + case <-ctx.Done(): + return ctx.Err() + } +} + +func (as *AppService) unknownCommandHandler(cmd WebsocketCommand) (bool, interface{}) { + zerolog.Ctx(cmd.Ctx).Warn().Msg("No handler for websocket command") + return false, fmt.Errorf("unknown request type") +} + +func (as *AppService) SetWebsocketCommandHandler(cmd string, handler WebsocketHandler) { + as.websocketHandlersLock.Lock() + as.websocketHandlers[cmd] = handler + as.websocketHandlersLock.Unlock() +} + +func (as *AppService) consumeWebsocket(stopFunc func(error), ws *websocket.Conn) { + defer stopFunc(ErrWebsocketUnknownError) + ctx := context.Background() + for { + var msg WebsocketMessage + err := ws.ReadJSON(&msg) + if err != nil { + as.Log.Debug().Err(err).Msg("Error reading from websocket") + stopFunc(parseCloseError(err)) + return + } + with := as.Log.With(). + Int("req_id", msg.ReqID). + Str("ws_command", msg.Command) + if msg.TxnID != "" { + with = with.Str("transaction_id", msg.TxnID) + } + log := with.Logger() + ctx = log.WithContext(ctx) + if msg.Command == "" || msg.Command == "transaction" { + if msg.TxnID == "" || !as.txnIDC.IsProcessed(msg.TxnID) { + as.handleTransaction(ctx, msg.TxnID, &msg.Transaction) + } else { + log.Debug(). + Object("content", &msg.Transaction). + Msg("Ignoring duplicate transaction") + } + go func() { + err = as.SendWebsocket(msg.MakeResponse(true, &WebsocketTransactionResponse{TxnID: msg.TxnID})) + if err != nil { + log.Warn().Err(err).Msg("Failed to send response to websocket transaction") + } else { + log.Debug().Msg("Sent response to transaction") + } + }() + } else if msg.Command == "connect" { + log.Debug().Msg("Websocket connect confirmation received") + } else if msg.Command == "response" || msg.Command == "error" { + as.websocketRequestsLock.RLock() + respChan, ok := as.websocketRequests[msg.ReqID] + if ok { + select { + case respChan <- &msg.WebsocketCommand: + default: + log.Warn().Msg("Failed to handle response: channel didn't accept response") + } + } else { + log.Warn().Msg("Dropping response to unknown request ID") + } + as.websocketRequestsLock.RUnlock() + } else { + log.Debug().Msg("Received websocket command") + as.websocketHandlersLock.RLock() + handler, ok := as.websocketHandlers[msg.Command] + as.websocketHandlersLock.RUnlock() + if !ok { + handler = as.unknownCommandHandler + } + go func() { + okResp, data := handler(msg.WebsocketCommand) + err = as.SendWebsocket(msg.MakeResponse(okResp, data)) + if err != nil { + log.Error().Err(err).Msg("Failed to send response to websocket command") + } else if okResp { + log.Debug().Msg("Sent success response to websocket command") + } else { + log.Debug().Msg("Sent error response to websocket command") + } + }() + } + } +} + +func (as *AppService) StartWebsocket(baseURL string, onConnect func()) error { + parsed, err := url.Parse(baseURL) + if err != nil { + return fmt.Errorf("failed to parse URL: %w", err) + } + parsed.Path = filepath.Join(parsed.Path, "_matrix/client/unstable/fi.mau.as_sync") + if parsed.Scheme == "http" { + parsed.Scheme = "ws" + } else if parsed.Scheme == "https" { + parsed.Scheme = "wss" + } + ws, resp, err := websocket.DefaultDialer.Dial(parsed.String(), http.Header{ + "Authorization": []string{fmt.Sprintf("Bearer %s", as.Registration.AppToken)}, + "User-Agent": []string{as.BotClient().UserAgent}, + + "X-Mautrix-Process-ID": []string{as.ProcessID}, + "X-Mautrix-Websocket-Version": []string{"3"}, + }) + if resp != nil && resp.StatusCode >= 400 { + var errResp Error + err = json.NewDecoder(resp.Body).Decode(&errResp) + if err != nil { + return fmt.Errorf("websocket request returned HTTP %d with non-JSON body", resp.StatusCode) + } else { + return fmt.Errorf("websocket request returned %s (HTTP %d): %s", errResp.ErrorCode, resp.StatusCode, errResp.Message) + } + } else if err != nil { + return fmt.Errorf("failed to open websocket: %w", err) + } + if as.StopWebsocket != nil { + as.StopWebsocket(ErrWebsocketOverridden) + } + closeChan := make(chan error) + closeChanOnce := sync.Once{} + stopFunc := func(err error) { + closeChanOnce.Do(func() { + closeChan <- err + }) + } + as.ws = ws + as.StopWebsocket = stopFunc + as.PrepareWebsocket() + as.Log.Debug().Msg("Appservice transaction websocket opened") + + go as.consumeWebsocket(stopFunc, ws) + + if onConnect != nil { + onConnect() + } + + closeErr := <-closeChan + + if as.ws == ws { + as.clearWebsocketResponseWaiters() + as.ws = nil + } + + _ = ws.SetWriteDeadline(time.Now().Add(3 * time.Second)) + err = ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseGoingAway, "")) + if err != nil && !errors.Is(err, websocket.ErrCloseSent) { + as.Log.Warn().Err(err).Msg("Error writing close message to websocket") + } + err = ws.Close() + if err != nil { + as.Log.Warn().Err(err).Msg("Error closing websocket") + } + return closeErr +} diff --git a/vendor/maunium.net/go/mautrix/client.go b/vendor/maunium.net/go/mautrix/client.go new file mode 100644 index 00000000..2923eaea --- /dev/null +++ b/vendor/maunium.net/go/mautrix/client.go @@ -0,0 +1,2023 @@ +// Package mautrix implements the Matrix Client-Server API. +// +// Specification can be found at https://spec.matrix.org/v1.2/client-server-api/ +package mautrix + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "strconv" + "strings" + "sync/atomic" + "time" + + "github.com/rs/zerolog" + "maunium.net/go/maulogger/v2/maulogadapt" + + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" + "maunium.net/go/mautrix/pushrules" +) + +type CryptoHelper interface { + Encrypt(id.RoomID, event.Type, any) (*event.EncryptedEventContent, error) + Decrypt(*event.Event) (*event.Event, error) + WaitForSession(id.RoomID, id.SenderKey, id.SessionID, time.Duration) bool + RequestSession(id.RoomID, id.SenderKey, id.SessionID, id.UserID, id.DeviceID) + Init() error +} + +// Deprecated: switch to zerolog +type Logger interface { + Debugfln(message string, args ...interface{}) +} + +// Deprecated: switch to zerolog +type WarnLogger interface { + Logger + Warnfln(message string, args ...interface{}) +} + +// Client represents a Matrix client. +type Client struct { + HomeserverURL *url.URL // The base homeserver URL + UserID id.UserID // The user ID of the client. Used for forming HTTP paths which use the client's user ID. + DeviceID id.DeviceID // The device ID of the client. + AccessToken string // The access_token for the client. + UserAgent string // The value for the User-Agent header + 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 SyncStore // The thing which can store tokens/ids + StateStore StateStore + Crypto CryptoHelper + + Log zerolog.Logger + // Deprecated: switch to the zerolog instance in Log + Logger Logger + + RequestHook func(req *http.Request) + ResponseHook func(req *http.Request, resp *http.Response, duration time.Duration) + + SyncPresence event.Presence + + StreamSyncMinAge time.Duration + + // Number of times that mautrix will retry any HTTP request + // if the request fails entirely or returns a HTTP gateway error (502-504) + DefaultHTTPRetries int + // Set to true to disable automatically sleeping on 429 errors. + IgnoreRateLimit bool + + txnID int32 + + // Should the ?user_id= query parameter be set in requests? + // See https://spec.matrix.org/v1.6/application-service-api/#identity-assertion + SetAppServiceUserID bool + + syncingID uint32 // Identifies the current Sync. Only one Sync can be active at any given time. +} + +type ClientWellKnown struct { + Homeserver HomeserverInfo `json:"m.homeserver"` + IdentityServer IdentityServerInfo `json:"m.identity_server"` +} + +type HomeserverInfo struct { + BaseURL string `json:"base_url"` +} + +type IdentityServerInfo struct { + BaseURL string `json:"base_url"` +} + +// DiscoverClientAPI resolves the client API URL from a Matrix server name. +// Use ParseUserID to extract the server name from a user ID. +// https://spec.matrix.org/v1.2/client-server-api/#server-discovery +func DiscoverClientAPI(serverName string) (*ClientWellKnown, error) { + wellKnownURL := url.URL{ + Scheme: "https", + Host: serverName, + Path: "/.well-known/matrix/client", + } + + req, err := http.NewRequest("GET", wellKnownURL.String(), nil) + if err != nil { + return nil, err + } + + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", DefaultUserAgent+" (.well-known fetcher)") + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + return nil, nil + } + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var wellKnown ClientWellKnown + err = json.Unmarshal(data, &wellKnown) + if err != nil { + return nil, errors.New(".well-known response not JSON") + } + + return &wellKnown, nil +} + +// SetCredentials sets the user ID and access token on this client instance. +// +// Deprecated: use the StoreCredentials field in ReqLogin instead. +func (cli *Client) SetCredentials(userID id.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 = "" + cli.DeviceID = "" +} + +// 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 { + return cli.SyncWithContext(context.Background()) +} + +func (cli *Client) SyncWithContext(ctx context.Context) 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) + } + lastSuccessfulSync := time.Now().Add(-cli.StreamSyncMinAge - 1*time.Hour) + for { + streamResp := false + if cli.StreamSyncMinAge > 0 && time.Since(lastSuccessfulSync) > cli.StreamSyncMinAge { + cli.Log.Debug().Msg("Last sync is old, will stream next response") + streamResp = true + } + resSync, err := cli.FullSyncRequest(ReqSync{ + Timeout: 30000, + Since: nextBatch, + FilterID: filterID, + FullState: false, + SetPresence: cli.SyncPresence, + Context: ctx, + StreamResponse: streamResp, + }) + if err != nil { + if ctx.Err() != nil { + return ctx.Err() + } + duration, err2 := cli.Syncer.OnFailedSync(resSync, err) + if err2 != nil { + return err2 + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(duration): + continue + } + } + lastSuccessfulSync = time.Now() + + // 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 { + return atomic.AddUint32(&cli.syncingID, 1) +} + +func (cli *Client) getSyncingID() uint32 { + return atomic.LoadUint32(&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() +} + +type contextKey int + +const ( + LogBodyContextKey contextKey = iota + LogRequestIDContextKey +) + +func (cli *Client) LogRequest(req *http.Request) { + if cli.RequestHook != nil { + cli.RequestHook(req) + } + evt := zerolog.Ctx(req.Context()).Debug(). + Str("method", req.Method). + Str("url", req.URL.String()) + body := req.Context().Value(LogBodyContextKey) + if body != nil { + evt.Interface("body", body) + } + evt.Msg("Sending request") +} + +func (cli *Client) LogRequestDone(req *http.Request, resp *http.Response, handlerErr error, contentLength int, duration time.Duration) { + if cli.ResponseHook != nil { + cli.ResponseHook(req, resp, duration) + } + mime := resp.Header.Get("Content-Type") + length := resp.ContentLength + if length == -1 && contentLength > 0 { + length = int64(contentLength) + } + path := strings.TrimPrefix(req.URL.Path, cli.HomeserverURL.Path) + path = strings.TrimPrefix(path, "/_matrix/client") + evt := zerolog.Ctx(req.Context()).Debug(). + Str("method", req.Method). + Str("path", path). + Int("status_code", resp.StatusCode). + Int64("response_length", length). + Str("response_mime", mime). + Dur("duration", duration) + if handlerErr != nil { + evt.AnErr("body_parse_err", handlerErr) + } + evt.Msg("Request completed") +} + +func (cli *Client) MakeRequest(method string, httpURL string, reqBody interface{}, resBody interface{}) ([]byte, error) { + return cli.MakeFullRequest(FullRequest{Method: method, URL: httpURL, RequestJSON: reqBody, ResponseJSON: resBody}) +} + +type ClientResponseHandler = func(req *http.Request, res *http.Response, responseJSON interface{}) ([]byte, error) + +type FullRequest struct { + Method string + URL string + Headers http.Header + RequestJSON interface{} + RequestBytes []byte + RequestBody io.Reader + RequestLength int64 + ResponseJSON interface{} + Context context.Context + MaxAttempts int + SensitiveContent bool + Handler ClientResponseHandler + Logger *zerolog.Logger +} + +var requestID int32 +var logSensitiveContent = os.Getenv("MAUTRIX_LOG_SENSITIVE_CONTENT") == "yes" + +func (params *FullRequest) compileRequest() (*http.Request, error) { + var logBody any + reqBody := params.RequestBody + if params.Context == nil { + params.Context = context.Background() + } + if params.RequestJSON != nil { + jsonStr, err := json.Marshal(params.RequestJSON) + if err != nil { + return nil, HTTPError{ + Message: "failed to marshal JSON", + WrappedError: err, + } + } + if params.SensitiveContent && !logSensitiveContent { + logBody = "<sensitive content omitted>" + } else { + logBody = params.RequestJSON + } + reqBody = bytes.NewReader(jsonStr) + } else if params.RequestBytes != nil { + logBody = fmt.Sprintf("<%d bytes>", len(params.RequestBytes)) + reqBody = bytes.NewReader(params.RequestBytes) + params.RequestLength = int64(len(params.RequestBytes)) + } else if params.RequestLength > 0 && params.RequestBody != nil { + logBody = fmt.Sprintf("<%d bytes>", params.RequestLength) + } else if params.Method != http.MethodGet && params.Method != http.MethodHead { + params.RequestJSON = struct{}{} + logBody = params.RequestJSON + reqBody = bytes.NewReader([]byte("{}")) + } + reqID := atomic.AddInt32(&requestID, 1) + ctx := params.Context + logger := zerolog.Ctx(ctx) + if logger.GetLevel() == zerolog.Disabled || logger == zerolog.DefaultContextLogger { + logger = params.Logger + } + ctx = logger.With(). + Int32("req_id", reqID). + Logger().WithContext(ctx) + ctx = context.WithValue(ctx, LogBodyContextKey, logBody) + ctx = context.WithValue(ctx, LogRequestIDContextKey, int(reqID)) + req, err := http.NewRequestWithContext(ctx, params.Method, params.URL, reqBody) + if err != nil { + return nil, HTTPError{ + Message: "failed to create request", + WrappedError: err, + } + } + if params.Headers != nil { + req.Header = params.Headers + } + if params.RequestJSON != nil { + req.Header.Set("Content-Type", "application/json") + } + if params.RequestLength > 0 && params.RequestBody != nil { + req.ContentLength = params.RequestLength + } + return req, nil +} + +// MakeFullRequest 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) MakeFullRequest(params FullRequest) ([]byte, error) { + if params.MaxAttempts == 0 { + params.MaxAttempts = 1 + cli.DefaultHTTPRetries + } + if params.Logger == nil { + params.Logger = &cli.Log + } + req, err := params.compileRequest() + if err != nil { + return nil, err + } + if params.Handler == nil { + params.Handler = cli.handleNormalResponse + } + req.Header.Set("User-Agent", cli.UserAgent) + if len(cli.AccessToken) > 0 { + req.Header.Set("Authorization", "Bearer "+cli.AccessToken) + } + return cli.executeCompiledRequest(req, params.MaxAttempts-1, 4*time.Second, params.ResponseJSON, params.Handler) +} + +func (cli *Client) cliOrContextLog(ctx context.Context) *zerolog.Logger { + log := zerolog.Ctx(ctx) + if log.GetLevel() == zerolog.Disabled || log == zerolog.DefaultContextLogger { + return &cli.Log + } + return log +} + +func (cli *Client) doRetry(req *http.Request, cause error, retries int, backoff time.Duration, responseJSON interface{}, handler ClientResponseHandler) ([]byte, error) { + log := zerolog.Ctx(req.Context()) + if req.Body != nil { + if req.GetBody == nil { + log.Warn().Msg("Failed to get new body to retry request: GetBody is nil") + return nil, cause + } + var err error + req.Body, err = req.GetBody() + if err != nil { + log.Warn().Err(err).Msg("Failed to get new body to retry request") + return nil, cause + } + } + log.Warn().Err(cause). + Int("retry_in_seconds", int(backoff.Seconds())). + Msg("Request failed, retrying") + time.Sleep(backoff) + return cli.executeCompiledRequest(req, retries-1, backoff*2, responseJSON, handler) +} + +func (cli *Client) readRequestBody(req *http.Request, res *http.Response) ([]byte, error) { + contents, err := io.ReadAll(res.Body) + if err != nil { + return nil, HTTPError{ + Request: req, + Response: res, + + Message: "failed to read response body", + WrappedError: err, + } + } + return contents, nil +} + +func closeTemp(log *zerolog.Logger, file *os.File) { + _ = file.Close() + err := os.Remove(file.Name()) + if err != nil { + log.Warn().Err(err).Str("file_name", file.Name()).Msg("Failed to remove response temp file") + } +} + +func (cli *Client) streamResponse(req *http.Request, res *http.Response, responseJSON interface{}) ([]byte, error) { + log := zerolog.Ctx(req.Context()) + file, err := os.CreateTemp("", "mautrix-response-") + if err != nil { + log.Warn().Err(err).Msg("Failed to create temporary file for streaming response") + _, err = cli.handleNormalResponse(req, res, responseJSON) + return nil, err + } + defer closeTemp(log, file) + if _, err = io.Copy(file, res.Body); err != nil { + return nil, fmt.Errorf("failed to copy response to file: %w", err) + } else if _, err = file.Seek(0, 0); err != nil { + return nil, fmt.Errorf("failed to seek to beginning of response file: %w", err) + } else if err = json.NewDecoder(file).Decode(responseJSON); err != nil { + return nil, fmt.Errorf("failed to unmarshal response body: %w", err) + } else { + return nil, nil + } +} + +func (cli *Client) handleNormalResponse(req *http.Request, res *http.Response, responseJSON interface{}) ([]byte, error) { + if contents, err := cli.readRequestBody(req, res); err != nil { + return nil, err + } else if responseJSON == nil { + return contents, nil + } else if err = json.Unmarshal(contents, &responseJSON); err != nil { + return nil, HTTPError{ + Request: req, + Response: res, + + Message: "failed to unmarshal response body", + ResponseBody: string(contents), + WrappedError: err, + } + } else { + return contents, nil + } +} + +func (cli *Client) handleResponseError(req *http.Request, res *http.Response) ([]byte, error) { + contents, err := cli.readRequestBody(req, res) + if err != nil { + return contents, err + } + + respErr := &RespError{} + if _ = json.Unmarshal(contents, respErr); respErr.ErrCode == "" { + respErr = nil + } + + return contents, HTTPError{ + Request: req, + Response: res, + RespError: respErr, + } +} + +// parseBackoffFromResponse extracts the backoff time specified in the Retry-After header if present. See +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After. +func (cli *Client) parseBackoffFromResponse(req *http.Request, res *http.Response, now time.Time, fallback time.Duration) time.Duration { + retryAfterHeaderValue := res.Header.Get("Retry-After") + if retryAfterHeaderValue == "" { + return fallback + } + + if t, err := time.Parse(http.TimeFormat, retryAfterHeaderValue); err == nil { + return t.Sub(now) + } + + if seconds, err := strconv.Atoi(retryAfterHeaderValue); err == nil { + return time.Duration(seconds) * time.Second + } + + zerolog.Ctx(req.Context()).Warn(). + Str("retry_after", retryAfterHeaderValue). + Msg("Failed to parse Retry-After header value") + + return fallback +} + +func (cli *Client) shouldRetry(res *http.Response) bool { + return res.StatusCode == http.StatusBadGateway || + res.StatusCode == http.StatusServiceUnavailable || + res.StatusCode == http.StatusGatewayTimeout || + (res.StatusCode == http.StatusTooManyRequests && !cli.IgnoreRateLimit) +} + +func (cli *Client) executeCompiledRequest(req *http.Request, retries int, backoff time.Duration, responseJSON interface{}, handler ClientResponseHandler) ([]byte, error) { + cli.LogRequest(req) + startTime := time.Now() + res, err := cli.Client.Do(req) + duration := time.Now().Sub(startTime) + if res != nil { + defer res.Body.Close() + } + if err != nil { + if retries > 0 { + return cli.doRetry(req, err, retries, backoff, responseJSON, handler) + } + return nil, HTTPError{ + Request: req, + Response: res, + + Message: "request error", + WrappedError: err, + } + } + + if retries > 0 && cli.shouldRetry(res) { + if res.StatusCode == http.StatusTooManyRequests { + backoff = cli.parseBackoffFromResponse(req, res, time.Now(), backoff) + } + return cli.doRetry(req, fmt.Errorf("HTTP %d", res.StatusCode), retries, backoff, responseJSON, handler) + } + + var body []byte + if res.StatusCode < 200 || res.StatusCode >= 300 { + body, err = cli.handleResponseError(req, res) + cli.LogRequestDone(req, res, nil, len(body), duration) + } else { + body, err = handler(req, res, responseJSON) + cli.LogRequestDone(req, res, err, len(body), duration) + } + return body, err +} + +// Whoami gets the user ID of the current user. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3accountwhoami +func (cli *Client) Whoami() (resp *RespWhoami, err error) { + urlPath := cli.BuildClientURL("v3", "account", "whoami") + _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + return +} + +// CreateFilter makes an HTTP request according to https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3useruseridfilter +func (cli *Client) CreateFilter(filter *Filter) (resp *RespCreateFilter, err error) { + urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "filter") + _, err = cli.MakeRequest("POST", urlPath, filter, &resp) + return +} + +// SyncRequest makes an HTTP request according to https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3sync +func (cli *Client) SyncRequest(timeout int, since, filterID string, fullState bool, setPresence event.Presence, ctx context.Context) (resp *RespSync, err error) { + return cli.FullSyncRequest(ReqSync{ + Timeout: timeout, + Since: since, + FilterID: filterID, + FullState: fullState, + SetPresence: setPresence, + Context: ctx, + }) +} + +type ReqSync struct { + Timeout int + Since string + FilterID string + FullState bool + SetPresence event.Presence + + Context context.Context + StreamResponse bool +} + +func (req *ReqSync) BuildQuery() map[string]string { + query := map[string]string{ + "timeout": strconv.Itoa(req.Timeout), + } + if req.Since != "" { + query["since"] = req.Since + } + if req.FilterID != "" { + query["filter"] = req.FilterID + } + if req.SetPresence != "" { + query["set_presence"] = string(req.SetPresence) + } + if req.FullState { + query["full_state"] = "true" + } + return query +} + +// FullSyncRequest makes an HTTP request according to https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3sync +func (cli *Client) FullSyncRequest(req ReqSync) (resp *RespSync, err error) { + urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "sync"}, req.BuildQuery()) + fullReq := FullRequest{ + Method: http.MethodGet, + URL: urlPath, + ResponseJSON: &resp, + Context: req.Context, + // We don't want automatic retries for SyncRequest, the Sync() wrapper handles those. + MaxAttempts: 1, + } + if req.StreamResponse { + fullReq.Handler = cli.streamResponse + } + start := time.Now() + _, err = cli.MakeFullRequest(fullReq) + duration := time.Now().Sub(start) + timeout := time.Duration(req.Timeout) * time.Millisecond + buffer := 10 * time.Second + if req.Since == "" { + buffer = 1 * time.Minute + } + if err == nil && duration > timeout+buffer { + cli.cliOrContextLog(fullReq.Context).Warn(). + Str("since", req.Since). + Dur("duration", duration). + Dur("timeout", timeout). + Msg("Sync request took unusually long") + } + return +} + +// RegisterAvailable checks if a username is valid and available for registration on the server. +// +// See https://spec.matrix.org/v1.4/client-server-api/#get_matrixclientv3registeravailable for more details +// +// This will always return an error if the username isn't available, so checking the actual response struct is generally +// not necessary. It is still returned for future-proofing. For a simple availability check, just check that the returned +// error is nil. `errors.Is` can be used to find the exact reason why a username isn't available: +// +// _, err := cli.RegisterAvailable("cat") +// if errors.Is(err, mautrix.MUserInUse) { +// // Username is taken +// } else if errors.Is(err, mautrix.MInvalidUsername) { +// // Username is not valid +// } else if errors.Is(err, mautrix.MExclusive) { +// // Username is reserved for an appservice +// } else if errors.Is(err, mautrix.MLimitExceeded) { +// // Too many requests +// } else if err != nil { +// // Unknown error +// } else { +// // Username is available +// } +func (cli *Client) RegisterAvailable(username string) (resp *RespRegisterAvailable, err error) { + u := cli.BuildURLWithQuery(ClientURLPath{"v3", "register", "available"}, map[string]string{"username": username}) + _, err = cli.MakeRequest(http.MethodGet, u, nil, &resp) + if err == nil && !resp.Available { + err = fmt.Errorf(`request returned OK status without "available": true`) + } + return +} + +func (cli *Client) register(url string, req *ReqRegister) (resp *RespRegister, uiaResp *RespUserInteractive, err error) { + var bodyBytes []byte + bodyBytes, err = cli.MakeFullRequest(FullRequest{ + Method: http.MethodPost, + URL: url, + RequestJSON: req, + SensitiveContent: len(req.Password) > 0, + }) + if err != nil { + httpErr, ok := err.(HTTPError) + // if response has a 401 status, but doesn't have the errcode field, it's probably a UIA response. + if ok && httpErr.IsStatus(http.StatusUnauthorized) && httpErr.RespError == nil { + err = json.Unmarshal(bodyBytes, &uiaResp) + } + } else { + // body should be RespRegister + err = json.Unmarshal(bodyBytes, &resp) + } + return +} + +// Register makes an HTTP request according to https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3register +// +// Registers with kind=user. For kind=guest, see RegisterGuest. +func (cli *Client) Register(req *ReqRegister) (*RespRegister, *RespUserInteractive, error) { + u := cli.BuildClientURL("v3", "register") + return cli.register(u, req) +} + +// RegisterGuest makes an HTTP request according to https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3register +// 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(ClientURLPath{"v3", "register"}, query) + return cli.register(u, req) +} + +// RegisterDummy performs m.login.dummy registration according to https://spec.matrix.org/v1.2/client-server-api/#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(&mautrix.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 + } else if uia == nil { + return nil, errors.New("server did not return user-interactive auth flows") + } else if !uia.HasSingleStageFlow(AuthTypeDummy) { + return nil, errors.New("server does not support m.login.dummy") + } + req.Auth = BaseAuthData{Type: AuthTypeDummy, Session: uia.Session} + res, _, err = cli.Register(req) + if err != nil { + return nil, err + } + return res, nil +} + +// GetLoginFlows fetches the login flows that the homeserver supports using https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3login +func (cli *Client) GetLoginFlows() (resp *RespLoginFlows, err error) { + urlPath := cli.BuildClientURL("v3", "login") + _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + return +} + +// Login a user to the homeserver according to https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3login +func (cli *Client) Login(req *ReqLogin) (resp *RespLogin, err error) { + _, err = cli.MakeFullRequest(FullRequest{ + Method: http.MethodPost, + URL: cli.BuildClientURL("v3", "login"), + RequestJSON: req, + ResponseJSON: &resp, + SensitiveContent: len(req.Password) > 0 || len(req.Token) > 0, + }) + if req.StoreCredentials && err == nil { + cli.DeviceID = resp.DeviceID + cli.AccessToken = resp.AccessToken + cli.UserID = resp.UserID + + cli.Log.Debug(). + Str("user_id", cli.UserID.String()). + Str("device_id", cli.DeviceID.String()). + Msg("Stored credentials after login") + } + if req.StoreHomeserverURL && err == nil && resp.WellKnown != nil && len(resp.WellKnown.Homeserver.BaseURL) > 0 { + var urlErr error + cli.HomeserverURL, urlErr = url.Parse(resp.WellKnown.Homeserver.BaseURL) + if urlErr != nil { + cli.Log.Warn(). + Err(urlErr). + Str("homeserver_url", resp.WellKnown.Homeserver.BaseURL). + Msg("Failed to parse homeserver URL in login response") + } else { + cli.Log.Debug(). + Str("homeserver_url", cli.HomeserverURL.String()). + Msg("Updated homeserver URL after login") + } + } + return +} + +// Logout the current user. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3logout +// This does not clear the credentials from the client instance. See ClearCredentials() instead. +func (cli *Client) Logout() (resp *RespLogout, err error) { + urlPath := cli.BuildClientURL("v3", "logout") + _, err = cli.MakeRequest("POST", urlPath, nil, &resp) + return +} + +// LogoutAll logs out all the devices of the current user. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3logoutall +// This does not clear the credentials from the client instance. See ClearCredentials() instead. +func (cli *Client) LogoutAll() (resp *RespLogout, err error) { + urlPath := cli.BuildClientURL("v3", "logout", "all") + _, err = cli.MakeRequest("POST", urlPath, nil, &resp) + return +} + +// Versions returns the list of supported Matrix versions on this homeserver. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientversions +func (cli *Client) Versions() (resp *RespVersions, err error) { + urlPath := cli.BuildClientURL("versions") + _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + return +} + +// Capabilities returns capabilities on this homeserver. See https://spec.matrix.org/v1.3/client-server-api/#capabilities-negotiation +func (cli *Client) Capabilities() (resp *RespCapabilities, err error) { + urlPath := cli.BuildClientURL("v3", "capabilities") + _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + return +} + +// JoinRoom joins the client to a room ID or alias. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3joinroomidoralias +// +// 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(ClientURLPath{"v3", "join", roomIDorAlias}, map[string]string{ + "server_name": serverName, + }) + } else { + urlPath = cli.BuildClientURL("v3", "join", roomIDorAlias) + } + _, err = cli.MakeRequest("POST", urlPath, content, &resp) + if err == nil && cli.StateStore != nil { + cli.StateStore.SetMembership(resp.RoomID, cli.UserID, event.MembershipJoin) + } + return +} + +// JoinRoomByID joins the client to a room ID. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidjoin +// +// Unlike JoinRoom, this method can only be used to join rooms that the server already knows about. +// It's mostly intended for bridges and other things where it's already certain that the server is in the room. +func (cli *Client) JoinRoomByID(roomID id.RoomID) (resp *RespJoinRoom, err error) { + _, err = cli.MakeRequest("POST", cli.BuildClientURL("v3", "rooms", roomID, "join"), nil, &resp) + if err == nil && cli.StateStore != nil { + cli.StateStore.SetMembership(resp.RoomID, cli.UserID, event.MembershipJoin) + } + return +} + +func (cli *Client) GetProfile(mxid id.UserID) (resp *RespUserProfile, err error) { + urlPath := cli.BuildClientURL("v3", "profile", mxid) + _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + return +} + +// GetDisplayName returns the display name of the user with the specified MXID. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3profileuseriddisplayname +func (cli *Client) GetDisplayName(mxid id.UserID) (resp *RespUserDisplayName, err error) { + urlPath := cli.BuildClientURL("v3", "profile", mxid, "displayname") + _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + return +} + +// GetOwnDisplayName returns the user's display name. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3profileuseriddisplayname +func (cli *Client) GetOwnDisplayName() (resp *RespUserDisplayName, err error) { + return cli.GetDisplayName(cli.UserID) +} + +// SetDisplayName sets the user's profile display name. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3profileuseriddisplayname +func (cli *Client) SetDisplayName(displayName string) (err error) { + urlPath := cli.BuildClientURL("v3", "profile", cli.UserID, "displayname") + s := struct { + DisplayName string `json:"displayname"` + }{displayName} + _, err = cli.MakeRequest("PUT", urlPath, &s, nil) + return +} + +// GetAvatarURL gets the avatar URL of the user with the specified MXID. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3profileuseridavatar_url +func (cli *Client) GetAvatarURL(mxid id.UserID) (url id.ContentURI, err error) { + urlPath := cli.BuildClientURL("v3", "profile", mxid, "avatar_url") + s := struct { + AvatarURL id.ContentURI `json:"avatar_url"` + }{} + + _, err = cli.MakeRequest("GET", urlPath, nil, &s) + if err != nil { + return + } + url = s.AvatarURL + return +} + +// GetOwnAvatarURL gets the user's avatar URL. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3profileuseridavatar_url +func (cli *Client) GetOwnAvatarURL() (url id.ContentURI, err error) { + return cli.GetAvatarURL(cli.UserID) +} + +// SetAvatarURL sets the user's avatar URL. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3profileuseridavatar_url +func (cli *Client) SetAvatarURL(url id.ContentURI) (err error) { + urlPath := cli.BuildClientURL("v3", "profile", cli.UserID, "avatar_url") + s := struct { + AvatarURL string `json:"avatar_url"` + }{url.String()} + _, err = cli.MakeRequest("PUT", urlPath, &s, nil) + if err != nil { + return err + } + + return nil +} + +// GetAccountData gets the user's account data of this type. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3useruseridaccount_datatype +func (cli *Client) GetAccountData(name string, output interface{}) (err error) { + urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "account_data", name) + _, err = cli.MakeRequest("GET", urlPath, nil, output) + return +} + +// SetAccountData sets the user's account data of this type. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3useruseridaccount_datatype +func (cli *Client) SetAccountData(name string, data interface{}) (err error) { + urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "account_data", name) + _, err = cli.MakeRequest("PUT", urlPath, data, nil) + if err != nil { + return err + } + + return nil +} + +// GetRoomAccountData gets the user's account data of this type in a specific room. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3useruseridaccount_datatype +func (cli *Client) GetRoomAccountData(roomID id.RoomID, name string, output interface{}) (err error) { + urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "rooms", roomID, "account_data", name) + _, err = cli.MakeRequest("GET", urlPath, nil, output) + return +} + +// SetRoomAccountData sets the user's account data of this type in a specific room. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3useruseridroomsroomidaccount_datatype +func (cli *Client) SetRoomAccountData(roomID id.RoomID, name string, data interface{}) (err error) { + urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "rooms", roomID, "account_data", name) + _, err = cli.MakeRequest("PUT", urlPath, data, nil) + if err != nil { + return err + } + + return nil +} + +type ReqSendEvent struct { + Timestamp int64 + TransactionID string + + DontEncrypt bool + + MeowEventID id.EventID +} + +// SendMessageEvent sends a message event into a room. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid +// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal. +func (cli *Client) SendMessageEvent(roomID id.RoomID, eventType event.Type, contentJSON interface{}, extra ...ReqSendEvent) (resp *RespSendEvent, err error) { + var req ReqSendEvent + if len(extra) > 0 { + req = extra[0] + } + + var txnID string + if len(req.TransactionID) > 0 { + txnID = req.TransactionID + } else { + txnID = cli.TxnID() + } + + queryParams := map[string]string{} + if req.Timestamp > 0 { + queryParams["ts"] = strconv.FormatInt(req.Timestamp, 10) + } + if req.MeowEventID != "" { + queryParams["fi.mau.event_id"] = req.MeowEventID.String() + } + + if !req.DontEncrypt && cli.Crypto != nil && eventType != event.EventReaction && eventType != event.EventEncrypted && cli.StateStore.IsEncrypted(roomID) { + contentJSON, err = cli.Crypto.Encrypt(roomID, eventType, contentJSON) + if err != nil { + err = fmt.Errorf("failed to encrypt event: %w", err) + return + } + eventType = event.EventEncrypted + } + + urlData := ClientURLPath{"v3", "rooms", roomID, "send", eventType.String(), txnID} + urlPath := cli.BuildURLWithQuery(urlData, queryParams) + _, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp) + return +} + +// SendStateEvent sends a state event into a room. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey +// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal. +func (cli *Client) SendStateEvent(roomID id.RoomID, eventType event.Type, stateKey string, contentJSON interface{}) (resp *RespSendEvent, err error) { + urlPath := cli.BuildClientURL("v3", "rooms", roomID, "state", eventType.String(), stateKey) + _, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp) + if err == nil && cli.StateStore != nil { + cli.updateStoreWithOutgoingEvent(roomID, eventType, stateKey, contentJSON) + } + return +} + +// SendMassagedStateEvent sends a state event into a room with a custom timestamp. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey +// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal. +func (cli *Client) SendMassagedStateEvent(roomID id.RoomID, eventType event.Type, stateKey string, contentJSON interface{}, ts int64) (resp *RespSendEvent, err error) { + urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "rooms", roomID, "state", eventType.String(), stateKey}, map[string]string{ + "ts": strconv.FormatInt(ts, 10), + }) + _, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp) + if err == nil && cli.StateStore != nil { + cli.updateStoreWithOutgoingEvent(roomID, eventType, stateKey, contentJSON) + } + return +} + +// SendText sends an m.room.message event into the given room with a msgtype of m.text +// See https://spec.matrix.org/v1.2/client-server-api/#mtext +func (cli *Client) SendText(roomID id.RoomID, text string) (*RespSendEvent, error) { + return cli.SendMessageEvent(roomID, event.EventMessage, &event.MessageEventContent{ + MsgType: event.MsgText, + Body: text, + }) +} + +// SendNotice sends an m.room.message event into the given room with a msgtype of m.notice +// See https://spec.matrix.org/v1.2/client-server-api/#mnotice +func (cli *Client) SendNotice(roomID id.RoomID, text string) (*RespSendEvent, error) { + return cli.SendMessageEvent(roomID, event.EventMessage, &event.MessageEventContent{ + MsgType: event.MsgNotice, + Body: text, + }) +} + +func (cli *Client) SendReaction(roomID id.RoomID, eventID id.EventID, reaction string) (*RespSendEvent, error) { + return cli.SendMessageEvent(roomID, event.EventReaction, &event.ReactionEventContent{ + RelatesTo: event.RelatesTo{ + EventID: eventID, + Type: event.RelAnnotation, + Key: reaction, + }, + }) +} + +// RedactEvent redacts the given event. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidredacteventidtxnid +func (cli *Client) RedactEvent(roomID id.RoomID, eventID id.EventID, extra ...ReqRedact) (resp *RespSendEvent, err error) { + req := ReqRedact{} + if len(extra) > 0 { + req = extra[0] + } + if req.Extra == nil { + req.Extra = make(map[string]interface{}) + } + if len(req.Reason) > 0 { + req.Extra["reason"] = req.Reason + } + var txnID string + if len(req.TxnID) > 0 { + txnID = req.TxnID + } else { + txnID = cli.TxnID() + } + urlPath := cli.BuildClientURL("v3", "rooms", roomID, "redact", eventID, txnID) + _, err = cli.MakeRequest("PUT", urlPath, req.Extra, &resp) + return +} + +// CreateRoom creates a new Matrix room. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3createroom +// +// resp, err := cli.CreateRoom(&mautrix.ReqCreateRoom{ +// Preset: "public_chat", +// }) +// fmt.Println("Room:", resp.RoomID) +func (cli *Client) CreateRoom(req *ReqCreateRoom) (resp *RespCreateRoom, err error) { + urlPath := cli.BuildClientURL("v3", "createRoom") + _, err = cli.MakeRequest("POST", urlPath, req, &resp) + if err == nil && cli.StateStore != nil { + cli.StateStore.SetMembership(resp.RoomID, cli.UserID, event.MembershipJoin) + for _, evt := range req.InitialState { + UpdateStateStore(cli.StateStore, evt) + } + inviteMembership := event.MembershipInvite + if req.BeeperAutoJoinInvites { + inviteMembership = event.MembershipJoin + } + for _, invitee := range req.Invite { + cli.StateStore.SetMembership(resp.RoomID, invitee, inviteMembership) + } + } + return +} + +// LeaveRoom leaves the given room. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidleave +func (cli *Client) LeaveRoom(roomID id.RoomID, optionalReq ...*ReqLeave) (resp *RespLeaveRoom, err error) { + req := &ReqLeave{} + if len(optionalReq) == 1 { + req = optionalReq[0] + } else if len(optionalReq) > 1 { + panic("invalid number of arguments to LeaveRoom") + } + u := cli.BuildClientURL("v3", "rooms", roomID, "leave") + _, err = cli.MakeRequest("POST", u, req, &resp) + if err == nil && cli.StateStore != nil { + cli.StateStore.SetMembership(roomID, cli.UserID, event.MembershipLeave) + } + return +} + +// ForgetRoom forgets a room entirely. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidforget +func (cli *Client) ForgetRoom(roomID id.RoomID) (resp *RespForgetRoom, err error) { + u := cli.BuildClientURL("v3", "rooms", roomID, "forget") + _, err = cli.MakeRequest("POST", u, struct{}{}, &resp) + return +} + +// InviteUser invites a user to a room. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidinvite +func (cli *Client) InviteUser(roomID id.RoomID, req *ReqInviteUser) (resp *RespInviteUser, err error) { + u := cli.BuildClientURL("v3", "rooms", roomID, "invite") + _, err = cli.MakeRequest("POST", u, req, &resp) + if err == nil && cli.StateStore != nil { + cli.StateStore.SetMembership(roomID, req.UserID, event.MembershipInvite) + } + return +} + +// InviteUserByThirdParty invites a third-party identifier to a room. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidinvite-1 +func (cli *Client) InviteUserByThirdParty(roomID id.RoomID, req *ReqInvite3PID) (resp *RespInviteUser, err error) { + u := cli.BuildClientURL("v3", "rooms", roomID, "invite") + _, err = cli.MakeRequest("POST", u, req, &resp) + return +} + +// KickUser kicks a user from a room. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidkick +func (cli *Client) KickUser(roomID id.RoomID, req *ReqKickUser) (resp *RespKickUser, err error) { + u := cli.BuildClientURL("v3", "rooms", roomID, "kick") + _, err = cli.MakeRequest("POST", u, req, &resp) + if err == nil && cli.StateStore != nil { + cli.StateStore.SetMembership(roomID, req.UserID, event.MembershipLeave) + } + return +} + +// BanUser bans a user from a room. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidban +func (cli *Client) BanUser(roomID id.RoomID, req *ReqBanUser) (resp *RespBanUser, err error) { + u := cli.BuildClientURL("v3", "rooms", roomID, "ban") + _, err = cli.MakeRequest("POST", u, req, &resp) + if err == nil && cli.StateStore != nil { + cli.StateStore.SetMembership(roomID, req.UserID, event.MembershipBan) + } + return +} + +// UnbanUser unbans a user from a room. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidunban +func (cli *Client) UnbanUser(roomID id.RoomID, req *ReqUnbanUser) (resp *RespUnbanUser, err error) { + u := cli.BuildClientURL("v3", "rooms", roomID, "unban") + _, err = cli.MakeRequest("POST", u, req, &resp) + if err == nil && cli.StateStore != nil { + cli.StateStore.SetMembership(roomID, req.UserID, event.MembershipLeave) + } + return +} + +// UserTyping sets the typing status of the user. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidtypinguserid +func (cli *Client) UserTyping(roomID id.RoomID, typing bool, timeout time.Duration) (resp *RespTyping, err error) { + req := ReqTyping{Typing: typing, Timeout: timeout.Milliseconds()} + u := cli.BuildClientURL("v3", "rooms", roomID, "typing", cli.UserID) + _, err = cli.MakeRequest("PUT", u, req, &resp) + return +} + +// GetPresence gets the presence of the user with the specified MXID. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3presenceuseridstatus +func (cli *Client) GetPresence(userID id.UserID) (resp *RespPresence, err error) { + resp = new(RespPresence) + u := cli.BuildClientURL("v3", "presence", userID, "status") + _, err = cli.MakeRequest("GET", u, nil, resp) + return +} + +// GetOwnPresence gets the user's presence. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3presenceuseridstatus +func (cli *Client) GetOwnPresence() (resp *RespPresence, err error) { + return cli.GetPresence(cli.UserID) +} + +func (cli *Client) SetPresence(status event.Presence) (err error) { + req := ReqPresence{Presence: status} + u := cli.BuildClientURL("v3", "presence", cli.UserID, "status") + _, err = cli.MakeRequest("PUT", u, req, nil) + return +} + +func (cli *Client) updateStoreWithOutgoingEvent(roomID id.RoomID, eventType event.Type, stateKey string, contentJSON interface{}) { + if cli.StateStore == nil { + return + } + fakeEvt := &event.Event{ + StateKey: &stateKey, + Type: eventType, + RoomID: roomID, + } + var err error + fakeEvt.Content.VeryRaw, err = json.Marshal(contentJSON) + if err != nil { + cli.Log.Warn().Err(err).Msg("Failed to marshal state event content to update state store") + return + } + err = json.Unmarshal(fakeEvt.Content.VeryRaw, &fakeEvt.Content.Raw) + if err != nil { + cli.Log.Warn().Err(err).Msg("Failed to unmarshal state event content to update state store") + return + } + err = fakeEvt.Content.ParseRaw(fakeEvt.Type) + if err != nil { + cli.Log.Warn().Err(err).Msg("Failed to parse state event content to update state store") + return + } + UpdateStateStore(cli.StateStore, fakeEvt) +} + +// 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 https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomidstateeventtypestatekey +func (cli *Client) StateEvent(roomID id.RoomID, eventType event.Type, stateKey string, outContent interface{}) (err error) { + u := cli.BuildClientURL("v3", "rooms", roomID, "state", eventType.String(), stateKey) + _, err = cli.MakeRequest("GET", u, nil, outContent) + if err == nil && cli.StateStore != nil { + cli.updateStoreWithOutgoingEvent(roomID, eventType, stateKey, outContent) + } + return +} + +// parseRoomStateArray parses a JSON array as a stream and stores the events inside it in a room state map. +func parseRoomStateArray(_ *http.Request, res *http.Response, responseJSON interface{}) ([]byte, error) { + response := make(RoomStateMap) + responsePtr := responseJSON.(*map[event.Type]map[string]*event.Event) + *responsePtr = response + dec := json.NewDecoder(res.Body) + + arrayStart, err := dec.Token() + if err != nil { + return nil, err + } else if arrayStart != json.Delim('[') { + return nil, fmt.Errorf("expected array start, got %+v", arrayStart) + } + + for i := 1; dec.More(); i++ { + var evt *event.Event + err = dec.Decode(&evt) + if err != nil { + return nil, fmt.Errorf("failed to parse state array item #%d: %v", i, err) + } + evt.Type.Class = event.StateEventType + _ = evt.Content.ParseRaw(evt.Type) + subMap, ok := response[evt.Type] + if !ok { + subMap = make(map[string]*event.Event) + response[evt.Type] = subMap + } + subMap[*evt.StateKey] = evt + } + + arrayEnd, err := dec.Token() + if err != nil { + return nil, err + } else if arrayEnd != json.Delim(']') { + return nil, fmt.Errorf("expected array end, got %+v", arrayStart) + } + return nil, nil +} + +// State gets all state in a room. +// See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomidstate +func (cli *Client) State(roomID id.RoomID) (stateMap RoomStateMap, err error) { + _, err = cli.MakeFullRequest(FullRequest{ + Method: http.MethodGet, + URL: cli.BuildClientURL("v3", "rooms", roomID, "state"), + ResponseJSON: &stateMap, + Handler: parseRoomStateArray, + }) + if err == nil && cli.StateStore != nil { + for _, evts := range stateMap { + for _, evt := range evts { + UpdateStateStore(cli.StateStore, evt) + } + } + } + return +} + +// GetMediaConfig fetches the configuration of the content repository, such as upload limitations. +func (cli *Client) GetMediaConfig() (resp *RespMediaConfig, err error) { + u := cli.BuildURL(MediaURLPath{"v3", "config"}) + _, err = cli.MakeRequest("GET", u, nil, &resp) + 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.Upload(res.Body, res.Header.Get("Content-Type"), res.ContentLength) +} + +func (cli *Client) GetDownloadURL(mxcURL id.ContentURI) string { + return cli.BuildURLWithQuery(MediaURLPath{"v3", "download", mxcURL.Homeserver, mxcURL.FileID}, map[string]string{"allow_redirect": "true"}) +} + +func (cli *Client) Download(mxcURL id.ContentURI) (io.ReadCloser, error) { + return cli.DownloadContext(context.Background(), mxcURL) +} + +func (cli *Client) DownloadContext(ctx context.Context, mxcURL id.ContentURI) (io.ReadCloser, error) { + _, resp, err := cli.downloadContext(ctx, mxcURL) + return resp.Body, err +} + +func (cli *Client) downloadContext(ctx context.Context, mxcURL id.ContentURI) (*http.Request, *http.Response, error) { + ctxLog := zerolog.Ctx(ctx) + if ctxLog.GetLevel() == zerolog.Disabled || ctxLog == zerolog.DefaultContextLogger { + ctx = cli.Log.WithContext(ctx) + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, cli.GetDownloadURL(mxcURL), nil) + if err != nil { + return req, nil, err + } + req.Header.Set("User-Agent", cli.UserAgent+" (media downloader)") + cli.LogRequest(req) + if resp, err := cli.Client.Do(req); err != nil { + return req, nil, err + } else { + return req, resp, nil + } +} + +func (cli *Client) DownloadBytes(mxcURL id.ContentURI) ([]byte, error) { + return cli.DownloadBytesContext(context.Background(), mxcURL) +} + +func (cli *Client) DownloadBytesContext(ctx context.Context, mxcURL id.ContentURI) ([]byte, error) { + req, resp, err := cli.downloadContext(ctx, mxcURL) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode >= 300 || resp.StatusCode < 200 { + respErr := &RespError{} + if _ = json.NewDecoder(resp.Body).Decode(respErr); respErr.ErrCode == "" { + respErr = nil + } + return nil, HTTPError{Request: req, Response: resp, RespError: respErr} + } + return io.ReadAll(resp.Body) +} + +// UnstableCreateMXC creates a blank Matrix content URI to allow uploading the content asynchronously later. +// See https://github.com/matrix-org/matrix-spec-proposals/pull/2246 +func (cli *Client) UnstableCreateMXC() (*RespCreateMXC, error) { + u, _ := url.Parse(cli.BuildURL(MediaURLPath{"unstable", "fi.mau.msc2246", "create"})) + var m RespCreateMXC + _, err := cli.MakeFullRequest(FullRequest{ + Method: http.MethodPost, + URL: u.String(), + ResponseJSON: &m, + }) + return &m, err +} + +// UnstableUploadAsync creates a blank content URI with UnstableCreateMXC, starts uploading the data in the background +// and returns the created MXC immediately. See https://github.com/matrix-org/matrix-spec-proposals/pull/2246 for more info. +func (cli *Client) UnstableUploadAsync(req ReqUploadMedia) (*RespCreateMXC, error) { + resp, err := cli.UnstableCreateMXC() + if err != nil { + return nil, err + } + req.UnstableMXC = resp.ContentURI + req.UploadURL = resp.UploadURL + go func() { + _, err = cli.UploadMedia(req) + if err != nil { + cli.Log.Error().Str("mxc", req.UnstableMXC.String()).Err(err).Msg("Async upload of media failed") + } + }() + return resp, nil +} + +func (cli *Client) UploadBytes(data []byte, contentType string) (*RespMediaUpload, error) { + return cli.UploadBytesWithName(data, contentType, "") +} + +func (cli *Client) UploadBytesWithName(data []byte, contentType, fileName string) (*RespMediaUpload, error) { + return cli.UploadMedia(ReqUploadMedia{ + ContentBytes: data, + ContentType: contentType, + FileName: fileName, + }) +} + +// Upload uploads the given data to the content repository and returns an MXC URI. +// +// Deprecated: UploadMedia should be used instead. +func (cli *Client) Upload(content io.Reader, contentType string, contentLength int64) (*RespMediaUpload, error) { + return cli.UploadMedia(ReqUploadMedia{ + Content: content, + ContentLength: contentLength, + ContentType: contentType, + }) +} + +type ReqUploadMedia struct { + ContentBytes []byte + Content io.Reader + ContentLength int64 + ContentType string + FileName string + + // UnstableMXC specifies an existing MXC URI which doesn't have content yet to upload into. + // See https://github.com/matrix-org/matrix-spec-proposals/pull/2246 for more info. + UnstableMXC id.ContentURI + + // UploadURL specifies the URL to upload the content to (MSC3870) + // see https://github.com/matrix-org/matrix-spec-proposals/pull/3870 for more info + UploadURL string +} + +func (cli *Client) tryUploadMediaToURL(url, contentType string, content io.Reader) (*http.Response, error) { + cli.Log.Debug().Str("url", url).Msg("Uploading media to external URL") + req, err := http.NewRequest(http.MethodPut, url, content) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", contentType) + req.Header.Set("User-Agent", cli.UserAgent+" (external media uploader)") + + return http.DefaultClient.Do(req) +} + +func (cli *Client) uploadMediaToURL(data ReqUploadMedia) (*RespMediaUpload, error) { + retries := cli.DefaultHTTPRetries + if data.ContentBytes == nil { + // Can't retry with a reader + retries = 0 + } + for { + reader := data.Content + if reader == nil { + reader = bytes.NewReader(data.ContentBytes) + } else { + data.Content = nil + } + resp, err := cli.tryUploadMediaToURL(data.UploadURL, data.ContentType, reader) + if err == nil { + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + // Everything is fine + break + } + err = fmt.Errorf("HTTP %d", resp.StatusCode) + } + if retries <= 0 { + cli.Log.Warn().Str("url", data.UploadURL).Err(err).Msg("Error uploading media to external URL, not retrying") + return nil, err + } + cli.Log.Warn().Str("url", data.UploadURL).Err(err).Msg("Error uploading media to external URL, retrying") + retries-- + } + + query := map[string]string{} + if len(data.FileName) > 0 { + query["filename"] = data.FileName + } + + notifyURL := cli.BuildURLWithQuery(MediaURLPath{"unstable", "fi.mau.msc2246", "upload", data.UnstableMXC.Homeserver, data.UnstableMXC.FileID, "complete"}, query) + + var m *RespMediaUpload + _, err := cli.MakeFullRequest(FullRequest{ + Method: http.MethodPost, + URL: notifyURL, + ResponseJSON: m, + }) + if err != nil { + return nil, err + } + + return m, nil +} + +// UploadMedia uploads the given data to the content repository and returns an MXC URI. +// See https://spec.matrix.org/v1.2/client-server-api/#post_matrixmediav3upload +func (cli *Client) UploadMedia(data ReqUploadMedia) (*RespMediaUpload, error) { + if data.UploadURL != "" { + return cli.uploadMediaToURL(data) + } + u, _ := url.Parse(cli.BuildURL(MediaURLPath{"v3", "upload"})) + method := http.MethodPost + if !data.UnstableMXC.IsEmpty() { + u, _ = url.Parse(cli.BuildURL(MediaURLPath{"unstable", "fi.mau.msc2246", "upload", data.UnstableMXC.Homeserver, data.UnstableMXC.FileID})) + method = http.MethodPut + } + if len(data.FileName) > 0 { + q := u.Query() + q.Set("filename", data.FileName) + u.RawQuery = q.Encode() + } + + var headers http.Header + if len(data.ContentType) > 0 { + headers = http.Header{"Content-Type": []string{data.ContentType}} + } + + var m RespMediaUpload + _, err := cli.MakeFullRequest(FullRequest{ + Method: method, + URL: u.String(), + Headers: headers, + RequestBytes: data.ContentBytes, + RequestBody: data.Content, + RequestLength: data.ContentLength, + ResponseJSON: &m, + }) + return &m, err +} + +// GetURLPreview asks the homeserver to fetch a preview for a given URL. +// +// See https://spec.matrix.org/v1.2/client-server-api/#get_matrixmediav3preview_url +func (cli *Client) GetURLPreview(url string) (*RespPreviewURL, error) { + reqURL := cli.BuildURLWithQuery(MediaURLPath{"v3", "preview_url"}, map[string]string{ + "url": url, + }) + var output RespPreviewURL + _, err := cli.MakeRequest(http.MethodGet, reqURL, nil, &output) + return &output, err +} + +// JoinedMembers returns a map of joined room members. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomidjoined_members +// +// 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 id.RoomID) (resp *RespJoinedMembers, err error) { + u := cli.BuildClientURL("v3", "rooms", roomID, "joined_members") + _, err = cli.MakeRequest("GET", u, nil, &resp) + if err == nil && cli.StateStore != nil { + for userID, member := range resp.Joined { + cli.StateStore.SetMember(roomID, userID, &event.MemberEventContent{ + Membership: event.MembershipJoin, + AvatarURL: id.ContentURIString(member.AvatarURL), + Displayname: member.DisplayName, + }) + } + } + return +} + +func (cli *Client) Members(roomID id.RoomID, req ...ReqMembers) (resp *RespMembers, err error) { + var extra ReqMembers + if len(req) > 0 { + extra = req[0] + } + query := map[string]string{} + if len(extra.At) > 0 { + query["at"] = extra.At + } + if len(extra.Membership) > 0 { + query["membership"] = string(extra.Membership) + } + if len(extra.NotMembership) > 0 { + query["not_membership"] = string(extra.NotMembership) + } + u := cli.BuildURLWithQuery(ClientURLPath{"v3", "rooms", roomID, "members"}, query) + _, err = cli.MakeRequest("GET", u, nil, &resp) + if err == nil && cli.StateStore != nil { + for _, evt := range resp.Chunk { + UpdateStateStore(cli.StateStore, evt) + } + } + return +} + +// JoinedRooms returns a list of rooms which the client is joined to. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3joined_rooms +// +// 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.BuildClientURL("v3", "joined_rooms") + _, err = cli.MakeRequest("GET", u, nil, &resp) + return +} + +// Hierarchy returns a list of rooms that are in the room's hierarchy. See https://spec.matrix.org/v1.4/client-server-api/#get_matrixclientv1roomsroomidhierarchy +// +// The hierarchy API is provided to walk the space tree and discover the rooms with their aesthetic details. works in a depth-first manner: +// when it encounters another space as a child it recurses into that space before returning non-space children. +// +// The second function parameter specifies query parameters to limit the response. No query parameters will be added if it's nil. +func (cli *Client) Hierarchy(roomID id.RoomID, req *ReqHierarchy) (resp *RespHierarchy, err error) { + urlPath := cli.BuildURLWithQuery(ClientURLPath{"v1", "rooms", roomID, "hierarchy"}, req.Query()) + _, err = cli.MakeRequest(http.MethodGet, urlPath, 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://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomidmessages +func (cli *Client) Messages(roomID id.RoomID, from, to string, dir Direction, filter *FilterPart, limit int) (resp *RespMessages, err error) { + query := map[string]string{ + "from": from, + "dir": string(dir), + } + if filter != nil { + filterJSON, err := json.Marshal(filter) + if err != nil { + return nil, err + } + query["filter"] = string(filterJSON) + } + if to != "" { + query["to"] = to + } + if limit != 0 { + query["limit"] = strconv.Itoa(limit) + } + + urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "rooms", roomID, "messages"}, query) + _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + return +} + +// TimestampToEvent finds the ID of the event closest to the given timestamp. +// +// See https://spec.matrix.org/v1.6/client-server-api/#get_matrixclientv1roomsroomidtimestamp_to_event +func (cli *Client) TimestampToEvent(roomID id.RoomID, timestamp time.Time, dir Direction) (resp *RespTimestampToEvent, err error) { + query := map[string]string{ + "ts": strconv.FormatInt(timestamp.UnixMilli(), 10), + "dir": string(dir), + } + urlPath := cli.BuildURLWithQuery(ClientURLPath{"v1", "rooms", roomID, "timestamp_to_event"}, query) + _, err = cli.MakeRequest(http.MethodGet, urlPath, nil, &resp) + return +} + +// Context returns a number of events that happened just before and after the +// specified event. It use pagination query parameters to paginate history in +// the room. +// See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomidcontexteventid +func (cli *Client) Context(roomID id.RoomID, eventID id.EventID, filter *FilterPart, limit int) (resp *RespContext, err error) { + query := map[string]string{} + if filter != nil { + filterJSON, err := json.Marshal(filter) + if err != nil { + return nil, err + } + query["filter"] = string(filterJSON) + } + if limit != 0 { + query["limit"] = strconv.Itoa(limit) + } + + urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "rooms", roomID, "context", eventID}, query) + _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + return +} + +func (cli *Client) GetEvent(roomID id.RoomID, eventID id.EventID) (resp *event.Event, err error) { + urlPath := cli.BuildClientURL("v3", "rooms", roomID, "event", eventID) + _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + return +} + +func (cli *Client) MarkRead(roomID id.RoomID, eventID id.EventID) (err error) { + return cli.SendReceipt(roomID, eventID, event.ReceiptTypeRead, nil) +} + +// MarkReadWithContent sends a read receipt including custom data. +// +// Deprecated: Use SendReceipt instead. +func (cli *Client) MarkReadWithContent(roomID id.RoomID, eventID id.EventID, content interface{}) (err error) { + return cli.SendReceipt(roomID, eventID, event.ReceiptTypeRead, content) +} + +// SendReceipt sends a receipt, usually specifically a read receipt. +// +// Passing nil as the content is safe, the library will automatically replace it with an empty JSON object. +// To mark a message in a specific thread as read, use pass a ReqSendReceipt as the content. +func (cli *Client) SendReceipt(roomID id.RoomID, eventID id.EventID, receiptType event.ReceiptType, content interface{}) (err error) { + urlPath := cli.BuildClientURL("v3", "rooms", roomID, "receipt", receiptType, eventID) + _, err = cli.MakeRequest("POST", urlPath, content, nil) + return +} + +func (cli *Client) SetReadMarkers(roomID id.RoomID, content interface{}) (err error) { + urlPath := cli.BuildClientURL("v3", "rooms", roomID, "read_markers") + _, err = cli.MakeRequest("POST", urlPath, content, nil) + return +} + +func (cli *Client) AddTag(roomID id.RoomID, tag string, order float64) error { + var tagData event.Tag + if order == order { + tagData.Order = json.Number(strconv.FormatFloat(order, 'e', -1, 64)) + } + return cli.AddTagWithCustomData(roomID, tag, tagData) +} + +func (cli *Client) AddTagWithCustomData(roomID id.RoomID, tag string, data interface{}) (err error) { + urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "rooms", roomID, "tags", tag) + _, err = cli.MakeRequest("PUT", urlPath, data, nil) + return +} + +func (cli *Client) GetTags(roomID id.RoomID) (tags event.TagEventContent, err error) { + err = cli.GetTagsWithCustomData(roomID, &tags) + return +} + +func (cli *Client) GetTagsWithCustomData(roomID id.RoomID, resp interface{}) (err error) { + urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "rooms", roomID, "tags") + _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + return +} + +func (cli *Client) RemoveTag(roomID id.RoomID, tag string) (err error) { + urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "rooms", roomID, "tags", tag) + _, err = cli.MakeRequest("DELETE", urlPath, nil, nil) + return +} + +// Deprecated: Synapse may not handle setting m.tag directly properly, so you should use the Add/RemoveTag methods instead. +func (cli *Client) SetTags(roomID id.RoomID, tags event.Tags) (err error) { + return cli.SetRoomAccountData(roomID, "m.tag", map[string]event.Tags{ + "tags": tags, + }) +} + +// TurnServer returns turn server details and credentials for the client to use when initiating calls. +// See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3voipturnserver +func (cli *Client) TurnServer() (resp *RespTurnServer, err error) { + urlPath := cli.BuildClientURL("v3", "voip", "turnServer") + _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + return +} + +func (cli *Client) CreateAlias(alias id.RoomAlias, roomID id.RoomID) (resp *RespAliasCreate, err error) { + urlPath := cli.BuildClientURL("v3", "directory", "room", alias) + _, err = cli.MakeRequest("PUT", urlPath, &ReqAliasCreate{RoomID: roomID}, &resp) + return +} + +func (cli *Client) ResolveAlias(alias id.RoomAlias) (resp *RespAliasResolve, err error) { + urlPath := cli.BuildClientURL("v3", "directory", "room", alias) + _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + return +} + +func (cli *Client) DeleteAlias(alias id.RoomAlias) (resp *RespAliasDelete, err error) { + urlPath := cli.BuildClientURL("v3", "directory", "room", alias) + _, err = cli.MakeRequest("DELETE", urlPath, nil, &resp) + return +} + +func (cli *Client) GetAliases(roomID id.RoomID) (resp *RespAliasList, err error) { + urlPath := cli.BuildClientURL("v3", "rooms", roomID, "aliases") + _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + return +} + +func (cli *Client) UploadKeys(req *ReqUploadKeys) (resp *RespUploadKeys, err error) { + urlPath := cli.BuildClientURL("v3", "keys", "upload") + _, err = cli.MakeRequest("POST", urlPath, req, &resp) + return +} + +func (cli *Client) QueryKeys(req *ReqQueryKeys) (resp *RespQueryKeys, err error) { + urlPath := cli.BuildClientURL("v3", "keys", "query") + _, err = cli.MakeRequest("POST", urlPath, req, &resp) + return +} + +func (cli *Client) ClaimKeys(req *ReqClaimKeys) (resp *RespClaimKeys, err error) { + urlPath := cli.BuildClientURL("v3", "keys", "claim") + _, err = cli.MakeRequest("POST", urlPath, req, &resp) + return +} + +func (cli *Client) GetKeyChanges(from, to string) (resp *RespKeyChanges, err error) { + urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "keys", "changes"}, map[string]string{ + "from": from, + "to": to, + }) + _, err = cli.MakeRequest("POST", urlPath, nil, &resp) + return +} + +func (cli *Client) SendToDevice(eventType event.Type, req *ReqSendToDevice) (resp *RespSendToDevice, err error) { + urlPath := cli.BuildClientURL("v3", "sendToDevice", eventType.String(), cli.TxnID()) + _, err = cli.MakeRequest("PUT", urlPath, req, &resp) + return +} + +func (cli *Client) GetDevicesInfo() (resp *RespDevicesInfo, err error) { + urlPath := cli.BuildClientURL("v3", "devices") + _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + return +} + +func (cli *Client) GetDeviceInfo(deviceID id.DeviceID) (resp *RespDeviceInfo, err error) { + urlPath := cli.BuildClientURL("v3", "devices", deviceID) + _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + return +} + +func (cli *Client) SetDeviceInfo(deviceID id.DeviceID, req *ReqDeviceInfo) error { + urlPath := cli.BuildClientURL("v3", "devices", deviceID) + _, err := cli.MakeRequest("PUT", urlPath, req, nil) + return err +} + +func (cli *Client) DeleteDevice(deviceID id.DeviceID, req *ReqDeleteDevice) error { + urlPath := cli.BuildClientURL("v3", "devices", deviceID) + _, err := cli.MakeRequest("DELETE", urlPath, req, nil) + return err +} + +func (cli *Client) DeleteDevices(req *ReqDeleteDevices) error { + urlPath := cli.BuildClientURL("v3", "delete_devices") + _, err := cli.MakeRequest("DELETE", urlPath, req, nil) + return err +} + +type UIACallback = func(*RespUserInteractive) interface{} + +// UploadCrossSigningKeys uploads the given cross-signing keys to the server. +// Because the endpoint requires user-interactive authentication a callback must be provided that, +// given the UI auth parameters, produces the required result (or nil to end the flow). +func (cli *Client) UploadCrossSigningKeys(keys *UploadCrossSigningKeysReq, uiaCallback UIACallback) error { + content, err := cli.MakeFullRequest(FullRequest{ + Method: http.MethodPost, + URL: cli.BuildClientURL("v3", "keys", "device_signing", "upload"), + RequestJSON: keys, + SensitiveContent: keys.Auth != nil, + }) + if respErr, ok := err.(HTTPError); ok && respErr.IsStatus(http.StatusUnauthorized) { + // try again with UI auth + var uiAuthResp RespUserInteractive + if err := json.Unmarshal(content, &uiAuthResp); err != nil { + return fmt.Errorf("failed to decode UIA response: %w", err) + } + auth := uiaCallback(&uiAuthResp) + if auth != nil { + keys.Auth = auth + return cli.UploadCrossSigningKeys(keys, uiaCallback) + } + } + return err +} + +func (cli *Client) UploadSignatures(req *ReqUploadSignatures) (resp *RespUploadSignatures, err error) { + urlPath := cli.BuildClientURL("v3", "keys", "signatures", "upload") + _, err = cli.MakeRequest("POST", urlPath, req, &resp) + return +} + +// GetPushRules returns the push notification rules for the global scope. +func (cli *Client) GetPushRules() (*pushrules.PushRuleset, error) { + return cli.GetScopedPushRules("global") +} + +// GetScopedPushRules returns the push notification rules for the given scope. +func (cli *Client) GetScopedPushRules(scope string) (resp *pushrules.PushRuleset, err error) { + u, _ := url.Parse(cli.BuildClientURL("v3", "pushrules", scope)) + // client.BuildURL returns the URL without a trailing slash, but the pushrules endpoint requires the slash. + u.Path += "/" + _, err = cli.MakeRequest("GET", u.String(), nil, &resp) + return +} + +func (cli *Client) GetPushRule(scope string, kind pushrules.PushRuleType, ruleID string) (resp *pushrules.PushRule, err error) { + urlPath := cli.BuildClientURL("v3", "pushrules", scope, kind, ruleID) + _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + if resp != nil { + resp.Type = kind + } + return +} + +func (cli *Client) DeletePushRule(scope string, kind pushrules.PushRuleType, ruleID string) error { + urlPath := cli.BuildClientURL("v3", "pushrules", scope, kind, ruleID) + _, err := cli.MakeRequest("DELETE", urlPath, nil, nil) + return err +} + +func (cli *Client) PutPushRule(scope string, kind pushrules.PushRuleType, ruleID string, req *ReqPutPushRule) error { + query := make(map[string]string) + if len(req.After) > 0 { + query["after"] = req.After + } + if len(req.Before) > 0 { + query["before"] = req.Before + } + urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "pushrules", scope, kind, ruleID}, query) + _, err := cli.MakeRequest("PUT", urlPath, req, nil) + return err +} + +// BatchSend sends a batch of historical events into a room. This is only available for appservices. +// +// See https://github.com/matrix-org/matrix-doc/pull/2716 for more info. +func (cli *Client) BatchSend(roomID id.RoomID, req *ReqBatchSend) (resp *RespBatchSend, err error) { + path := ClientURLPath{"unstable", "org.matrix.msc2716", "rooms", roomID, "batch_send"} + query := map[string]string{ + "prev_event_id": req.PrevEventID.String(), + } + if req.BeeperNewMessages { + query["com.beeper.new_messages"] = "true" + } + if req.BeeperMarkReadBy != "" { + query["com.beeper.mark_read_by"] = req.BeeperMarkReadBy.String() + } + if len(req.BatchID) > 0 { + query["batch_id"] = req.BatchID.String() + } + _, err = cli.MakeRequest("POST", cli.BuildURLWithQuery(path, query), req, &resp) + return +} + +func (cli *Client) AppservicePing(id, txnID string) (resp *RespAppservicePing, err error) { + _, err = cli.MakeFullRequest(FullRequest{ + Method: http.MethodPost, + URL: cli.BuildClientURL("unstable", "fi.mau.msc2659", "appservice", id, "ping"), + RequestJSON: &ReqAppservicePing{TxnID: txnID}, + ResponseJSON: &resp, + // This endpoint intentionally returns 50x, so don't retry + MaxAttempts: 1, + }) + return +} + +func (cli *Client) BeeperMergeRooms(req *ReqBeeperMergeRoom) (resp *RespBeeperMergeRoom, err error) { + urlPath := cli.BuildClientURL("unstable", "com.beeper.chatmerging", "merge") + _, err = cli.MakeRequest(http.MethodPost, urlPath, req, &resp) + return +} + +func (cli *Client) BeeperSplitRoom(req *ReqBeeperSplitRoom) (resp *RespBeeperSplitRoom, err error) { + urlPath := cli.BuildClientURL("unstable", "com.beeper.chatmerging", "rooms", req.RoomID, "split") + _, err = cli.MakeRequest(http.MethodPost, urlPath, req, &resp) + return +} + +func (cli *Client) BeeperDeleteRoom(roomID id.RoomID) (err error) { + urlPath := cli.BuildClientURL("unstable", "com.beeper.yeet", "rooms", roomID, "delete") + _, err = cli.MakeRequest(http.MethodPost, urlPath, nil, nil) + return +} + +// TxnID returns the next transaction ID. +func (cli *Client) TxnID() string { + txnID := atomic.AddInt32(&cli.txnID, 1) + return fmt.Sprintf("mautrix-go_%d_%d", time.Now().UnixNano(), txnID) +} + +// NewClient creates a new Matrix Client ready for syncing +func NewClient(homeserverURL string, userID id.UserID, accessToken string) (*Client, error) { + hsURL, err := ParseAndNormalizeBaseURL(homeserverURL) + if err != nil { + return nil, err + } + cli := &Client{ + AccessToken: accessToken, + UserAgent: DefaultUserAgent, + HomeserverURL: hsURL, + UserID: userID, + Client: &http.Client{Timeout: 180 * time.Second}, + Syncer: NewDefaultSyncer(), + Log: zerolog.Nop(), + // 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: NewMemorySyncStore(), + } + cli.Logger = maulogadapt.ZeroAsMau(&cli.Log) + return cli, nil +} diff --git a/vendor/maunium.net/go/mautrix/crypto/attachment/attachments.go b/vendor/maunium.net/go/mautrix/crypto/attachment/attachments.go new file mode 100644 index 00000000..e516fded --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/attachment/attachments.go @@ -0,0 +1,239 @@ +// Copyright (c) 2022 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package attachment + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/sha256" + "encoding/base64" + "errors" + "hash" + "io" + + "maunium.net/go/mautrix/crypto/utils" +) + +var ( + HashMismatch = errors.New("mismatching SHA-256 digest") + UnsupportedVersion = errors.New("unsupported Matrix file encryption version") + UnsupportedAlgorithm = errors.New("unsupported JWK encryption algorithm") + InvalidKey = errors.New("failed to decode key") + InvalidInitVector = errors.New("failed to decode initialization vector") + InvalidHash = errors.New("failed to decode SHA-256 hash") + ReaderClosed = errors.New("encrypting reader was already closed") +) + +var ( + keyBase64Length = base64.RawURLEncoding.EncodedLen(utils.AESCTRKeyLength) + ivBase64Length = base64.RawStdEncoding.EncodedLen(utils.AESCTRIVLength) + hashBase64Length = base64.RawStdEncoding.EncodedLen(utils.SHAHashLength) +) + +type JSONWebKey struct { + Key string `json:"k"` + Algorithm string `json:"alg"` + Extractable bool `json:"ext"` + KeyType string `json:"kty"` + KeyOps []string `json:"key_ops"` +} + +type EncryptedFileHashes struct { + SHA256 string `json:"sha256"` +} + +type decodedKeys struct { + key [utils.AESCTRKeyLength]byte + iv [utils.AESCTRIVLength]byte + + sha256 [utils.SHAHashLength]byte +} + +type EncryptedFile struct { + Key JSONWebKey `json:"key"` + InitVector string `json:"iv"` + Hashes EncryptedFileHashes `json:"hashes"` + Version string `json:"v"` + + decoded *decodedKeys +} + +func NewEncryptedFile() *EncryptedFile { + key, iv := utils.GenAttachmentA256CTR() + return &EncryptedFile{ + Key: JSONWebKey{ + Key: base64.RawURLEncoding.EncodeToString(key[:]), + Algorithm: "A256CTR", + Extractable: true, + KeyType: "oct", + KeyOps: []string{"encrypt", "decrypt"}, + }, + InitVector: base64.RawStdEncoding.EncodeToString(iv[:]), + Version: "v2", + + decoded: &decodedKeys{key: key, iv: iv}, + } +} + +func (ef *EncryptedFile) decodeKeys(includeHash bool) error { + if ef.decoded != nil { + return nil + } else if len(ef.Key.Key) != keyBase64Length { + return InvalidKey + } else if len(ef.InitVector) != ivBase64Length { + return InvalidInitVector + } else if includeHash && len(ef.Hashes.SHA256) != hashBase64Length { + return InvalidHash + } + ef.decoded = &decodedKeys{} + _, err := base64.RawURLEncoding.Decode(ef.decoded.key[:], []byte(ef.Key.Key)) + if err != nil { + return InvalidKey + } + _, err = base64.RawStdEncoding.Decode(ef.decoded.iv[:], []byte(ef.InitVector)) + if err != nil { + return InvalidInitVector + } + if includeHash { + _, err = base64.RawStdEncoding.Decode(ef.decoded.sha256[:], []byte(ef.Hashes.SHA256)) + if err != nil { + return InvalidHash + } + } + return nil +} + +// Encrypt encrypts the given data, updates the SHA256 hash in the EncryptedFile struct and returns the ciphertext. +// +// Deprecated: this makes a copy for the ciphertext, which means 2x memory usage. EncryptInPlace is recommended. +func (ef *EncryptedFile) Encrypt(plaintext []byte) []byte { + ciphertext := make([]byte, len(plaintext)) + copy(ciphertext, plaintext) + ef.EncryptInPlace(ciphertext) + return ciphertext +} + +// EncryptInPlace encrypts the given data in-place (i.e. the provided data is overridden with the ciphertext) +// and updates the SHA256 hash in the EncryptedFile struct. +func (ef *EncryptedFile) EncryptInPlace(data []byte) { + ef.decodeKeys(false) + utils.XorA256CTR(data, ef.decoded.key, ef.decoded.iv) + checksum := sha256.Sum256(data) + ef.Hashes.SHA256 = base64.RawStdEncoding.EncodeToString(checksum[:]) +} + +type encryptingReader struct { + stream cipher.Stream + hash hash.Hash + source io.Reader + file *EncryptedFile + closed bool + + isDecrypting bool +} + +func (r *encryptingReader) Read(dst []byte) (n int, err error) { + if r.closed { + return 0, ReaderClosed + } else if r.isDecrypting && r.file.decoded == nil { + if err = r.file.PrepareForDecryption(); err != nil { + return + } + } + n, err = r.source.Read(dst) + r.stream.XORKeyStream(dst[:n], dst[:n]) + r.hash.Write(dst[:n]) + return +} + +func (r *encryptingReader) Close() (err error) { + closer, ok := r.source.(io.ReadCloser) + if ok { + err = closer.Close() + } + if r.isDecrypting { + var downloadedChecksum [utils.SHAHashLength]byte + r.hash.Sum(downloadedChecksum[:]) + if downloadedChecksum != r.file.decoded.sha256 { + return HashMismatch + } + } else { + r.file.Hashes.SHA256 = base64.RawStdEncoding.EncodeToString(r.hash.Sum(nil)) + } + r.closed = true + return +} + +// EncryptStream wraps the given io.Reader in order to encrypt the data. +// +// The Close() method of the returned io.ReadCloser must be called for the SHA256 hash +// in the EncryptedFile struct to be updated. The metadata is not valid before the hash +// is filled. +func (ef *EncryptedFile) EncryptStream(reader io.Reader) io.ReadCloser { + ef.decodeKeys(false) + block, _ := aes.NewCipher(ef.decoded.key[:]) + return &encryptingReader{ + stream: cipher.NewCTR(block, ef.decoded.iv[:]), + hash: sha256.New(), + source: reader, + file: ef, + } +} + +// Decrypt decrypts the given data and returns the plaintext. +// +// Deprecated: this makes a copy for the plaintext data, which means 2x memory usage. DecryptInPlace is recommended. +func (ef *EncryptedFile) Decrypt(ciphertext []byte) ([]byte, error) { + plaintext := make([]byte, len(ciphertext)) + copy(plaintext, ciphertext) + return plaintext, ef.DecryptInPlace(plaintext) +} + +// PrepareForDecryption checks that the version and algorithm are supported and decodes the base64 keys +// +// DecryptStream will call this with the first Read() call if this hasn't been called manually. +// +// DecryptInPlace will always call this automatically, so calling this manually is not necessary when using that function. +func (ef *EncryptedFile) PrepareForDecryption() error { + if ef.Version != "v2" { + return UnsupportedVersion + } else if ef.Key.Algorithm != "A256CTR" { + return UnsupportedAlgorithm + } else if err := ef.decodeKeys(true); err != nil { + return err + } + return nil +} + +// DecryptInPlace decrypts the given data in-place (i.e. the provided data is overridden with the plaintext). +func (ef *EncryptedFile) DecryptInPlace(data []byte) error { + if err := ef.PrepareForDecryption(); err != nil { + return err + } else if ef.decoded.sha256 != sha256.Sum256(data) { + return HashMismatch + } else { + utils.XorA256CTR(data, ef.decoded.key, ef.decoded.iv) + return nil + } +} + +// DecryptStream wraps the given io.Reader in order to decrypt the data. +// +// The first Read call will check the algorithm and decode keys, so it might return an error before actually reading anything. +// If you want to validate the file before opening the stream, call PrepareForDecryption manually and check for errors. +// +// The Close call will validate the hash and return an error if it doesn't match. +// In this case, the written data should be considered compromised and should not be used further. +func (ef *EncryptedFile) DecryptStream(reader io.Reader) io.ReadCloser { + block, _ := aes.NewCipher(ef.decoded.key[:]) + return &encryptingReader{ + stream: cipher.NewCTR(block, ef.decoded.iv[:]), + hash: sha256.New(), + source: reader, + file: ef, + } +} diff --git a/vendor/maunium.net/go/mautrix/crypto/utils/utils.go b/vendor/maunium.net/go/mautrix/crypto/utils/utils.go new file mode 100644 index 00000000..e320bca1 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/utils/utils.go @@ -0,0 +1,133 @@ +// Copyright (c) 2020 Nikos Filippakis +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package utils + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "math/rand" + "strings" + + "golang.org/x/crypto/hkdf" + "golang.org/x/crypto/pbkdf2" + + "maunium.net/go/mautrix/util/base58" +) + +const ( + // AESCTRKeyLength is the length of the AES256-CTR key used. + AESCTRKeyLength = 32 + // AESCTRIVLength is the length of the AES256-CTR IV used. + AESCTRIVLength = 16 + // HMACKeyLength is the length of the HMAC key used. + HMACKeyLength = 32 + // SHAHashLength is the length of the SHA hash used. + SHAHashLength = 32 +) + +// XorA256CTR encrypts the input with the keystream generated by the AES256-CTR algorithm with the given arguments. +func XorA256CTR(source []byte, key [AESCTRKeyLength]byte, iv [AESCTRIVLength]byte) []byte { + block, _ := aes.NewCipher(key[:]) + cipher.NewCTR(block, iv[:]).XORKeyStream(source, source) + return source +} + +// GenAttachmentA256CTR generates a new random AES256-CTR key and IV suitable for encrypting attachments. +func GenAttachmentA256CTR() (key [AESCTRKeyLength]byte, iv [AESCTRIVLength]byte) { + _, err := rand.Read(key[:]) + if err != nil { + panic(err) + } + + // The last 8 bytes of the IV act as the counter in AES-CTR, which means they're left empty here + _, err = rand.Read(iv[:8]) + if err != nil { + panic(err) + } + return +} + +// GenA256CTRIV generates a random IV for AES256-CTR with the last bit set to zero. +func GenA256CTRIV() (iv [AESCTRIVLength]byte) { + _, err := rand.Read(iv[:]) + if err != nil { + panic(err) + } + iv[8] &= 0x7F + return +} + +// DeriveKeysSHA256 derives an AES and a HMAC key from the given recovery key. +func DeriveKeysSHA256(key []byte, name string) ([AESCTRKeyLength]byte, [HMACKeyLength]byte) { + var zeroBytes [32]byte + + derivedHkdf := hkdf.New(sha256.New, key[:], zeroBytes[:], []byte(name)) + + var aesKey [AESCTRKeyLength]byte + var hmacKey [HMACKeyLength]byte + derivedHkdf.Read(aesKey[:]) + derivedHkdf.Read(hmacKey[:]) + + return aesKey, hmacKey +} + +// PBKDF2SHA512 generates a key of the given bit-length using the given passphrase, salt and iteration count. +func PBKDF2SHA512(password []byte, salt []byte, iters int, keyLenBits int) []byte { + return pbkdf2.Key(password, salt, iters, keyLenBits/8, sha512.New) +} + +// DecodeBase58RecoveryKey recovers the secret storage from a recovery key. +func DecodeBase58RecoveryKey(recoveryKey string) []byte { + noSpaces := strings.ReplaceAll(recoveryKey, " ", "") + decoded := base58.Decode(noSpaces) + if len(decoded) != AESCTRKeyLength+3 { // AESCTRKeyLength bytes key and 3 bytes prefix / parity + return nil + } + var parity byte + for _, b := range decoded[:34] { + parity ^= b + } + if parity != decoded[34] || decoded[0] != 0x8B || decoded[1] != 1 { + return nil + } + return decoded[2:34] +} + +// EncodeBase58RecoveryKey recovers the secret storage from a recovery key. +func EncodeBase58RecoveryKey(key []byte) string { + var inputBytes [35]byte + copy(inputBytes[2:34], key[:]) + inputBytes[0] = 0x8B + inputBytes[1] = 1 + + var parity byte + for _, b := range inputBytes[:34] { + parity ^= b + } + inputBytes[34] = parity + recoveryKey := base58.Encode(inputBytes[:]) + + var spacedKey string + for i, c := range recoveryKey { + if i > 0 && i%4 == 0 { + spacedKey += " " + } + spacedKey += string(c) + } + return spacedKey +} + +// HMACSHA256B64 calculates the base64 of the SHA256 hmac of the input with the given key. +func HMACSHA256B64(input []byte, hmacKey [HMACKeyLength]byte) string { + h := hmac.New(sha256.New, hmacKey[:]) + h.Write(input) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} diff --git a/vendor/maunium.net/go/mautrix/error.go b/vendor/maunium.net/go/mautrix/error.go new file mode 100644 index 00000000..9c5e8a0e --- /dev/null +++ b/vendor/maunium.net/go/mautrix/error.go @@ -0,0 +1,154 @@ +// Copyright (c) 2020 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package mautrix + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" +) + +// Common error codes from https://matrix.org/docs/spec/client_server/latest#api-standards +// +// Can be used with errors.Is() to check the response code without casting the error: +// +// err := client.Sync() +// if errors.Is(err, MUnknownToken) { +// // logout +// } +var ( + // Forbidden access, e.g. joining a room without permission, failed login. + MForbidden = RespError{ErrCode: "M_FORBIDDEN"} + // Unrecognized request, e.g. the endpoint does not exist or is not implemented. + MUnrecognized = RespError{ErrCode: "M_UNRECOGNIZED"} + // The access token specified was not recognised. + MUnknownToken = RespError{ErrCode: "M_UNKNOWN_TOKEN"} + // No access token was specified for the request. + MMissingToken = RespError{ErrCode: "M_MISSING_TOKEN"} + // Request contained valid JSON, but it was malformed in some way, e.g. missing required keys, invalid values for keys. + MBadJSON = RespError{ErrCode: "M_BAD_JSON"} + // Request did not contain valid JSON. + MNotJSON = RespError{ErrCode: "M_NOT_JSON"} + // No resource was found for this request. + MNotFound = RespError{ErrCode: "M_NOT_FOUND"} + // Too many requests have been sent in a short period of time. Wait a while then try again. + MLimitExceeded = RespError{ErrCode: "M_LIMIT_EXCEEDED"} + // The user ID associated with the request has been deactivated. + // Typically for endpoints that prove authentication, such as /login. + MUserDeactivated = RespError{ErrCode: "M_USER_DEACTIVATED"} + // Encountered when trying to register a user ID which has been taken. + MUserInUse = RespError{ErrCode: "M_USER_IN_USE"} + // Encountered when trying to register a user ID which is not valid. + MInvalidUsername = RespError{ErrCode: "M_INVALID_USERNAME"} + // Sent when the room alias given to the createRoom API is already in use. + MRoomInUse = RespError{ErrCode: "M_ROOM_IN_USE"} + // The state change requested cannot be performed, such as attempting to unban a user who is not banned. + MBadState = RespError{ErrCode: "M_BAD_STATE"} + // The request or entity was too large. + MTooLarge = RespError{ErrCode: "M_TOO_LARGE"} + // The resource being requested is reserved by an application service, or the application service making the request has not created the resource. + MExclusive = RespError{ErrCode: "M_EXCLUSIVE"} + // The client's request to create a room used a room version that the server does not support. + MUnsupportedRoomVersion = RespError{ErrCode: "M_UNSUPPORTED_ROOM_VERSION"} + // The client attempted to join a room that has a version the server does not support. + // Inspect the room_version property of the error response for the room's version. + MIncompatibleRoomVersion = RespError{ErrCode: "M_INCOMPATIBLE_ROOM_VERSION"} + // The client specified a parameter that has the wrong value. + MInvalidParam = RespError{ErrCode: "M_INVALID_PARAM"} + + MSC2659URLNotSet = RespError{ErrCode: "FI.MAU.MSC2659_URL_NOT_SET"} + MSC2659BadStatus = RespError{ErrCode: "FI.MAU.MSC2659_BAD_STATUS"} + MSC2659ConnectionTimeout = RespError{ErrCode: "FI.MAU.MSC2659_CONNECTION_TIMEOUT"} + MSC2659ConnectionFailed = RespError{ErrCode: "FI.MAU.MSC2659_CONNECTION_FAILED"} +) + +// HTTPError An HTTP Error response, which may wrap an underlying native Go Error. +type HTTPError struct { + Request *http.Request + Response *http.Response + ResponseBody string + + WrappedError error + RespError *RespError + Message string +} + +func (e HTTPError) Is(err error) bool { + return (e.RespError != nil && errors.Is(e.RespError, err)) || (e.WrappedError != nil && errors.Is(e.WrappedError, err)) +} + +func (e HTTPError) IsStatus(code int) bool { + return e.Response != nil && e.Response.StatusCode == code +} + +func (e HTTPError) Error() string { + if e.WrappedError != nil { + return fmt.Sprintf("%s: %v", e.Message, e.WrappedError) + } else if e.RespError != nil { + return fmt.Sprintf("failed to %s %s: %s (HTTP %d): %s", e.Request.Method, e.Request.URL.Path, + e.RespError.ErrCode, e.Response.StatusCode, e.RespError.Err) + } else { + msg := fmt.Sprintf("failed to %s %s: %s", e.Request.Method, e.Request.URL.Path, e.Response.Status) + if len(e.ResponseBody) > 0 { + msg = fmt.Sprintf("%s\n%s", msg, e.ResponseBody) + } + return msg + } +} + +func (e HTTPError) Unwrap() error { + if e.WrappedError != nil { + return e.WrappedError + } else if e.RespError != nil { + return *e.RespError + } + return nil +} + +// RespError is the standard JSON error response from Homeservers. It also implements the Golang "error" interface. +// See https://spec.matrix.org/v1.2/client-server-api/#api-standards +type RespError struct { + ErrCode string + Err string + ExtraData map[string]interface{} +} + +func (e *RespError) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, &e.ExtraData) + if err != nil { + return err + } + e.ErrCode, _ = e.ExtraData["errcode"].(string) + e.Err, _ = e.ExtraData["error"].(string) + return nil +} + +func (e *RespError) MarshalJSON() ([]byte, error) { + if e.ExtraData == nil { + e.ExtraData = make(map[string]interface{}) + } + e.ExtraData["errcode"] = e.ErrCode + e.ExtraData["error"] = e.Err + return json.Marshal(&e.ExtraData) +} + +// Error returns the errcode and error message. +func (e RespError) Error() string { + return e.ErrCode + ": " + e.Err +} + +func (e RespError) Is(err error) bool { + e2, ok := err.(RespError) + if !ok { + return false + } + if e.ErrCode == "M_UNKNOWN" && e2.ErrCode == "M_UNKNOWN" { + return e.Err == e2.Err + } + return e2.ErrCode == e.ErrCode +} diff --git a/vendor/maunium.net/go/mautrix/event/accountdata.go b/vendor/maunium.net/go/mautrix/event/accountdata.go new file mode 100644 index 00000000..6637fcfe --- /dev/null +++ b/vendor/maunium.net/go/mautrix/event/accountdata.go @@ -0,0 +1,45 @@ +// Copyright (c) 2020 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package event + +import ( + "encoding/json" + + "maunium.net/go/mautrix/id" +) + +// TagEventContent represents the content of a m.tag room account data event. +// https://spec.matrix.org/v1.2/client-server-api/#mtag +type TagEventContent struct { + Tags Tags `json:"tags"` +} + +type Tags map[string]Tag + +type Tag struct { + Order json.Number `json:"order,omitempty"` +} + +// DirectChatsEventContent represents the content of a m.direct account data event. +// https://spec.matrix.org/v1.2/client-server-api/#mdirect +type DirectChatsEventContent map[id.UserID][]id.RoomID + +// FullyReadEventContent represents the content of a m.fully_read account data event. +// https://spec.matrix.org/v1.2/client-server-api/#mfully_read +type FullyReadEventContent struct { + EventID id.EventID `json:"event_id"` +} + +// IgnoredUserListEventContent represents the content of a m.ignored_user_list account data event. +// https://spec.matrix.org/v1.2/client-server-api/#mignored_user_list +type IgnoredUserListEventContent struct { + IgnoredUsers map[id.UserID]IgnoredUser `json:"ignored_users"` +} + +type IgnoredUser struct { + // This is an empty object +} diff --git a/vendor/maunium.net/go/mautrix/event/beeper.go b/vendor/maunium.net/go/mautrix/event/beeper.go new file mode 100644 index 00000000..2ee72073 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/event/beeper.go @@ -0,0 +1,51 @@ +// Copyright (c) 2022 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package event + +import ( + "maunium.net/go/mautrix/id" +) + +type MessageStatusReason string + +const ( + MessageStatusGenericError MessageStatusReason = "m.event_not_handled" + MessageStatusUnsupported MessageStatusReason = "com.beeper.unsupported_event" + MessageStatusUndecryptable MessageStatusReason = "com.beeper.undecryptable_event" + MessageStatusTooOld MessageStatusReason = "m.event_too_old" + MessageStatusNetworkError MessageStatusReason = "m.foreign_network_error" + MessageStatusNoPermission MessageStatusReason = "m.no_permission" + MessageStatusBridgeUnavailable MessageStatusReason = "m.bridge_unavailable" +) + +type MessageStatus string + +const ( + MessageStatusSuccess MessageStatus = "SUCCESS" + MessageStatusPending MessageStatus = "PENDING" + MessageStatusRetriable MessageStatus = "FAIL_RETRIABLE" + MessageStatusFail MessageStatus = "FAIL_PERMANENT" +) + +type BeeperMessageStatusEventContent struct { + Network string `json:"network"` + RelatesTo RelatesTo `json:"m.relates_to"` + Status MessageStatus `json:"status"` + Reason MessageStatusReason `json:"reason,omitempty"` + Error string `json:"error,omitempty"` + Message string `json:"message,omitempty"` + + LastRetry id.EventID `json:"last_retry,omitempty"` + + MutateEventKey string `json:"mutate_event_key,omitempty"` +} + +type BeeperRetryMetadata struct { + OriginalEventID id.EventID `json:"original_event_id"` + RetryCount int `json:"retry_count"` + // last_retry is also present, but not used by bridges +} diff --git a/vendor/maunium.net/go/mautrix/event/content.go b/vendor/maunium.net/go/mautrix/event/content.go new file mode 100644 index 00000000..5624fd59 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/event/content.go @@ -0,0 +1,506 @@ +// Copyright (c) 2021 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package event + +import ( + "encoding/gob" + "encoding/json" + "errors" + "fmt" + "reflect" +) + +// TypeMap is a mapping from event type to the content struct type. +// This is used by Content.ParseRaw() for creating the correct type of struct. +var TypeMap = map[Type]reflect.Type{ + StateMember: reflect.TypeOf(MemberEventContent{}), + StatePowerLevels: reflect.TypeOf(PowerLevelsEventContent{}), + StateCanonicalAlias: reflect.TypeOf(CanonicalAliasEventContent{}), + StateRoomName: reflect.TypeOf(RoomNameEventContent{}), + StateRoomAvatar: reflect.TypeOf(RoomAvatarEventContent{}), + StateServerACL: reflect.TypeOf(ServerACLEventContent{}), + StateTopic: reflect.TypeOf(TopicEventContent{}), + StateTombstone: reflect.TypeOf(TombstoneEventContent{}), + StateCreate: reflect.TypeOf(CreateEventContent{}), + StateJoinRules: reflect.TypeOf(JoinRulesEventContent{}), + StateHistoryVisibility: reflect.TypeOf(HistoryVisibilityEventContent{}), + StateGuestAccess: reflect.TypeOf(GuestAccessEventContent{}), + StatePinnedEvents: reflect.TypeOf(PinnedEventsEventContent{}), + StatePolicyRoom: reflect.TypeOf(ModPolicyContent{}), + StatePolicyServer: reflect.TypeOf(ModPolicyContent{}), + StatePolicyUser: reflect.TypeOf(ModPolicyContent{}), + StateEncryption: reflect.TypeOf(EncryptionEventContent{}), + StateBridge: reflect.TypeOf(BridgeEventContent{}), + StateHalfShotBridge: reflect.TypeOf(BridgeEventContent{}), + StateSpaceParent: reflect.TypeOf(SpaceParentEventContent{}), + StateSpaceChild: reflect.TypeOf(SpaceChildEventContent{}), + StateInsertionMarker: reflect.TypeOf(InsertionMarkerContent{}), + + EventMessage: reflect.TypeOf(MessageEventContent{}), + EventSticker: reflect.TypeOf(MessageEventContent{}), + EventEncrypted: reflect.TypeOf(EncryptedEventContent{}), + EventRedaction: reflect.TypeOf(RedactionEventContent{}), + EventReaction: reflect.TypeOf(ReactionEventContent{}), + + BeeperMessageStatus: reflect.TypeOf(BeeperMessageStatusEventContent{}), + + AccountDataRoomTags: reflect.TypeOf(TagEventContent{}), + AccountDataDirectChats: reflect.TypeOf(DirectChatsEventContent{}), + AccountDataFullyRead: reflect.TypeOf(FullyReadEventContent{}), + AccountDataIgnoredUserList: reflect.TypeOf(IgnoredUserListEventContent{}), + + EphemeralEventTyping: reflect.TypeOf(TypingEventContent{}), + EphemeralEventReceipt: reflect.TypeOf(ReceiptEventContent{}), + EphemeralEventPresence: reflect.TypeOf(PresenceEventContent{}), + + InRoomVerificationStart: reflect.TypeOf(VerificationStartEventContent{}), + InRoomVerificationReady: reflect.TypeOf(VerificationReadyEventContent{}), + InRoomVerificationAccept: reflect.TypeOf(VerificationAcceptEventContent{}), + InRoomVerificationKey: reflect.TypeOf(VerificationKeyEventContent{}), + InRoomVerificationMAC: reflect.TypeOf(VerificationMacEventContent{}), + InRoomVerificationCancel: reflect.TypeOf(VerificationCancelEventContent{}), + + ToDeviceRoomKey: reflect.TypeOf(RoomKeyEventContent{}), + ToDeviceForwardedRoomKey: reflect.TypeOf(ForwardedRoomKeyEventContent{}), + ToDeviceRoomKeyRequest: reflect.TypeOf(RoomKeyRequestEventContent{}), + ToDeviceEncrypted: reflect.TypeOf(EncryptedEventContent{}), + ToDeviceRoomKeyWithheld: reflect.TypeOf(RoomKeyWithheldEventContent{}), + ToDeviceDummy: reflect.TypeOf(DummyEventContent{}), + + ToDeviceVerificationStart: reflect.TypeOf(VerificationStartEventContent{}), + ToDeviceVerificationAccept: reflect.TypeOf(VerificationAcceptEventContent{}), + ToDeviceVerificationKey: reflect.TypeOf(VerificationKeyEventContent{}), + ToDeviceVerificationMAC: reflect.TypeOf(VerificationMacEventContent{}), + ToDeviceVerificationCancel: reflect.TypeOf(VerificationCancelEventContent{}), + ToDeviceVerificationRequest: reflect.TypeOf(VerificationRequestEventContent{}), + + ToDeviceOrgMatrixRoomKeyWithheld: reflect.TypeOf(RoomKeyWithheldEventContent{}), + + CallInvite: reflect.TypeOf(CallInviteEventContent{}), + CallCandidates: reflect.TypeOf(CallCandidatesEventContent{}), + CallAnswer: reflect.TypeOf(CallAnswerEventContent{}), + CallReject: reflect.TypeOf(CallRejectEventContent{}), + CallSelectAnswer: reflect.TypeOf(CallSelectAnswerEventContent{}), + CallNegotiate: reflect.TypeOf(CallNegotiateEventContent{}), + CallHangup: reflect.TypeOf(CallHangupEventContent{}), +} + +// Content stores the content of a Matrix event. +// +// By default, the raw JSON bytes are stored in VeryRaw and parsed into a map[string]interface{} in the Raw field. +// Additionally, you can call ParseRaw with the correct event type to parse the (VeryRaw) content into a nicer struct, +// which you can then access from Parsed or via the helper functions. +// +// When being marshaled into JSON, the data in Parsed will be marshaled first and then recursively merged +// with the data in Raw. Values in Raw are preferred, but nested objects will be recursed into before merging, +// rather than overriding the whole object with the one in Raw). +// If one of them is nil, the only the other is used. If both (Parsed and Raw) are nil, VeryRaw is used instead. +type Content struct { + VeryRaw json.RawMessage + Raw map[string]interface{} + Parsed interface{} +} + +type Relatable interface { + GetRelatesTo() *RelatesTo + OptionalGetRelatesTo() *RelatesTo + SetRelatesTo(rel *RelatesTo) +} + +func (content *Content) UnmarshalJSON(data []byte) error { + content.VeryRaw = data + err := json.Unmarshal(data, &content.Raw) + return err +} + +func (content *Content) MarshalJSON() ([]byte, error) { + if content.Raw == nil { + if content.Parsed == nil { + if content.VeryRaw == nil { + return []byte("{}"), nil + } + return content.VeryRaw, nil + } + return json.Marshal(content.Parsed) + } else if content.Parsed != nil { + // TODO this whole thing is incredibly hacky + // It needs to produce JSON, where: + // * content.Parsed is applied after content.Raw + // * MarshalJSON() is respected inside content.Parsed + // * Custom field inside nested objects of content.Raw are preserved, + // even if content.Parsed contains the higher-level objects. + // * content.Raw is not modified + + unparsed, err := json.Marshal(content.Parsed) + if err != nil { + return nil, err + } + + var rawParsed map[string]interface{} + err = json.Unmarshal(unparsed, &rawParsed) + if err != nil { + return nil, err + } + + output := make(map[string]interface{}) + for key, value := range content.Raw { + output[key] = value + } + + mergeMaps(output, rawParsed) + return json.Marshal(output) + } + return json.Marshal(content.Raw) +} + +// Deprecated: use errors.Is directly +func IsUnsupportedContentType(err error) bool { + return errors.Is(err, ErrUnsupportedContentType) +} + +var ErrContentAlreadyParsed = errors.New("content is already parsed") +var ErrUnsupportedContentType = errors.New("unsupported event type") + +func (content *Content) ParseRaw(evtType Type) error { + if content.Parsed != nil { + return ErrContentAlreadyParsed + } + structType, ok := TypeMap[evtType] + if !ok { + return fmt.Errorf("%w %s", ErrUnsupportedContentType, evtType.Repr()) + } + content.Parsed = reflect.New(structType).Interface() + return json.Unmarshal(content.VeryRaw, &content.Parsed) +} + +func mergeMaps(into, from map[string]interface{}) { + for key, newValue := range from { + existingValue, ok := into[key] + if !ok { + into[key] = newValue + continue + } + existingValueMap, okEx := existingValue.(map[string]interface{}) + newValueMap, okNew := newValue.(map[string]interface{}) + if okEx && okNew { + mergeMaps(existingValueMap, newValueMap) + } else { + into[key] = newValue + } + } +} + +func init() { + gob.Register(&MemberEventContent{}) + gob.Register(&PowerLevelsEventContent{}) + gob.Register(&CanonicalAliasEventContent{}) + gob.Register(&EncryptionEventContent{}) + gob.Register(&BridgeEventContent{}) + gob.Register(&SpaceChildEventContent{}) + gob.Register(&SpaceParentEventContent{}) + gob.Register(&RoomNameEventContent{}) + gob.Register(&RoomAvatarEventContent{}) + gob.Register(&TopicEventContent{}) + gob.Register(&TombstoneEventContent{}) + gob.Register(&CreateEventContent{}) + gob.Register(&JoinRulesEventContent{}) + gob.Register(&HistoryVisibilityEventContent{}) + gob.Register(&GuestAccessEventContent{}) + gob.Register(&PinnedEventsEventContent{}) + gob.Register(&MessageEventContent{}) + gob.Register(&MessageEventContent{}) + gob.Register(&EncryptedEventContent{}) + gob.Register(&RedactionEventContent{}) + gob.Register(&ReactionEventContent{}) + gob.Register(&TagEventContent{}) + gob.Register(&DirectChatsEventContent{}) + gob.Register(&FullyReadEventContent{}) + gob.Register(&IgnoredUserListEventContent{}) + gob.Register(&TypingEventContent{}) + gob.Register(&ReceiptEventContent{}) + gob.Register(&PresenceEventContent{}) + gob.Register(&RoomKeyEventContent{}) + gob.Register(&ForwardedRoomKeyEventContent{}) + gob.Register(&RoomKeyRequestEventContent{}) + gob.Register(&RoomKeyWithheldEventContent{}) +} + +// Helper cast functions below + +func (content *Content) AsMember() *MemberEventContent { + casted, ok := content.Parsed.(*MemberEventContent) + if !ok { + return &MemberEventContent{} + } + return casted +} +func (content *Content) AsPowerLevels() *PowerLevelsEventContent { + casted, ok := content.Parsed.(*PowerLevelsEventContent) + if !ok { + return &PowerLevelsEventContent{} + } + return casted +} +func (content *Content) AsCanonicalAlias() *CanonicalAliasEventContent { + casted, ok := content.Parsed.(*CanonicalAliasEventContent) + if !ok { + return &CanonicalAliasEventContent{} + } + return casted +} +func (content *Content) AsRoomName() *RoomNameEventContent { + casted, ok := content.Parsed.(*RoomNameEventContent) + if !ok { + return &RoomNameEventContent{} + } + return casted +} +func (content *Content) AsRoomAvatar() *RoomAvatarEventContent { + casted, ok := content.Parsed.(*RoomAvatarEventContent) + if !ok { + return &RoomAvatarEventContent{} + } + return casted +} +func (content *Content) AsTopic() *TopicEventContent { + casted, ok := content.Parsed.(*TopicEventContent) + if !ok { + return &TopicEventContent{} + } + return casted +} +func (content *Content) AsTombstone() *TombstoneEventContent { + casted, ok := content.Parsed.(*TombstoneEventContent) + if !ok { + return &TombstoneEventContent{} + } + return casted +} +func (content *Content) AsCreate() *CreateEventContent { + casted, ok := content.Parsed.(*CreateEventContent) + if !ok { + return &CreateEventContent{} + } + return casted +} +func (content *Content) AsJoinRules() *JoinRulesEventContent { + casted, ok := content.Parsed.(*JoinRulesEventContent) + if !ok { + return &JoinRulesEventContent{} + } + return casted +} +func (content *Content) AsHistoryVisibility() *HistoryVisibilityEventContent { + casted, ok := content.Parsed.(*HistoryVisibilityEventContent) + if !ok { + return &HistoryVisibilityEventContent{} + } + return casted +} +func (content *Content) AsGuestAccess() *GuestAccessEventContent { + casted, ok := content.Parsed.(*GuestAccessEventContent) + if !ok { + return &GuestAccessEventContent{} + } + return casted +} +func (content *Content) AsPinnedEvents() *PinnedEventsEventContent { + casted, ok := content.Parsed.(*PinnedEventsEventContent) + if !ok { + return &PinnedEventsEventContent{} + } + return casted +} +func (content *Content) AsEncryption() *EncryptionEventContent { + casted, ok := content.Parsed.(*EncryptionEventContent) + if !ok { + return &EncryptionEventContent{} + } + return casted +} +func (content *Content) AsBridge() *BridgeEventContent { + casted, ok := content.Parsed.(*BridgeEventContent) + if !ok { + return &BridgeEventContent{} + } + return casted +} +func (content *Content) AsSpaceChild() *SpaceChildEventContent { + casted, ok := content.Parsed.(*SpaceChildEventContent) + if !ok { + return &SpaceChildEventContent{} + } + return casted +} +func (content *Content) AsSpaceParent() *SpaceParentEventContent { + casted, ok := content.Parsed.(*SpaceParentEventContent) + if !ok { + return &SpaceParentEventContent{} + } + return casted +} +func (content *Content) AsMessage() *MessageEventContent { + casted, ok := content.Parsed.(*MessageEventContent) + if !ok { + return &MessageEventContent{} + } + return casted +} +func (content *Content) AsEncrypted() *EncryptedEventContent { + casted, ok := content.Parsed.(*EncryptedEventContent) + if !ok { + return &EncryptedEventContent{} + } + return casted +} +func (content *Content) AsRedaction() *RedactionEventContent { + casted, ok := content.Parsed.(*RedactionEventContent) + if !ok { + return &RedactionEventContent{} + } + return casted +} +func (content *Content) AsReaction() *ReactionEventContent { + casted, ok := content.Parsed.(*ReactionEventContent) + if !ok { + return &ReactionEventContent{} + } + return casted +} +func (content *Content) AsTag() *TagEventContent { + casted, ok := content.Parsed.(*TagEventContent) + if !ok { + return &TagEventContent{} + } + return casted +} +func (content *Content) AsDirectChats() *DirectChatsEventContent { + casted, ok := content.Parsed.(*DirectChatsEventContent) + if !ok { + return &DirectChatsEventContent{} + } + return casted +} +func (content *Content) AsFullyRead() *FullyReadEventContent { + casted, ok := content.Parsed.(*FullyReadEventContent) + if !ok { + return &FullyReadEventContent{} + } + return casted +} +func (content *Content) AsIgnoredUserList() *IgnoredUserListEventContent { + casted, ok := content.Parsed.(*IgnoredUserListEventContent) + if !ok { + return &IgnoredUserListEventContent{} + } + return casted +} +func (content *Content) AsTyping() *TypingEventContent { + casted, ok := content.Parsed.(*TypingEventContent) + if !ok { + return &TypingEventContent{} + } + return casted +} +func (content *Content) AsReceipt() *ReceiptEventContent { + casted, ok := content.Parsed.(*ReceiptEventContent) + if !ok { + return &ReceiptEventContent{} + } + return casted +} +func (content *Content) AsPresence() *PresenceEventContent { + casted, ok := content.Parsed.(*PresenceEventContent) + if !ok { + return &PresenceEventContent{} + } + return casted +} +func (content *Content) AsRoomKey() *RoomKeyEventContent { + casted, ok := content.Parsed.(*RoomKeyEventContent) + if !ok { + return &RoomKeyEventContent{} + } + return casted +} +func (content *Content) AsForwardedRoomKey() *ForwardedRoomKeyEventContent { + casted, ok := content.Parsed.(*ForwardedRoomKeyEventContent) + if !ok { + return &ForwardedRoomKeyEventContent{} + } + return casted +} +func (content *Content) AsRoomKeyRequest() *RoomKeyRequestEventContent { + casted, ok := content.Parsed.(*RoomKeyRequestEventContent) + if !ok { + return &RoomKeyRequestEventContent{} + } + return casted +} +func (content *Content) AsRoomKeyWithheld() *RoomKeyWithheldEventContent { + casted, ok := content.Parsed.(*RoomKeyWithheldEventContent) + if !ok { + return &RoomKeyWithheldEventContent{} + } + return casted +} +func (content *Content) AsCallInvite() *CallInviteEventContent { + casted, ok := content.Parsed.(*CallInviteEventContent) + if !ok { + return &CallInviteEventContent{} + } + return casted +} +func (content *Content) AsCallCandidates() *CallCandidatesEventContent { + casted, ok := content.Parsed.(*CallCandidatesEventContent) + if !ok { + return &CallCandidatesEventContent{} + } + return casted +} +func (content *Content) AsCallAnswer() *CallAnswerEventContent { + casted, ok := content.Parsed.(*CallAnswerEventContent) + if !ok { + return &CallAnswerEventContent{} + } + return casted +} +func (content *Content) AsCallReject() *CallRejectEventContent { + casted, ok := content.Parsed.(*CallRejectEventContent) + if !ok { + return &CallRejectEventContent{} + } + return casted +} +func (content *Content) AsCallSelectAnswer() *CallSelectAnswerEventContent { + casted, ok := content.Parsed.(*CallSelectAnswerEventContent) + if !ok { + return &CallSelectAnswerEventContent{} + } + return casted +} +func (content *Content) AsCallNegotiate() *CallNegotiateEventContent { + casted, ok := content.Parsed.(*CallNegotiateEventContent) + if !ok { + return &CallNegotiateEventContent{} + } + return casted +} +func (content *Content) AsCallHangup() *CallHangupEventContent { + casted, ok := content.Parsed.(*CallHangupEventContent) + if !ok { + return &CallHangupEventContent{} + } + return casted +} +func (content *Content) AsModPolicy() *ModPolicyContent { + casted, ok := content.Parsed.(*ModPolicyContent) + if !ok { + return &ModPolicyContent{} + } + return casted +} diff --git a/vendor/maunium.net/go/mautrix/event/encryption.go b/vendor/maunium.net/go/mautrix/event/encryption.go new file mode 100644 index 00000000..2506ad57 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/event/encryption.go @@ -0,0 +1,149 @@ +// Copyright (c) 2020 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package event + +import ( + "encoding/json" + + "maunium.net/go/mautrix/id" +) + +// EncryptionEventContent represents the content of a m.room.encryption state event. +// https://spec.matrix.org/v1.2/client-server-api/#mroomencryption +type EncryptionEventContent struct { + // The encryption algorithm to be used to encrypt messages sent in this room. Must be 'm.megolm.v1.aes-sha2'. + Algorithm id.Algorithm `json:"algorithm"` + // How long the session should be used before changing it. 604800000 (a week) is the recommended default. + RotationPeriodMillis int64 `json:"rotation_period_ms,omitempty"` + // How many messages should be sent before changing the session. 100 is the recommended default. + RotationPeriodMessages int `json:"rotation_period_msgs,omitempty"` +} + +// EncryptedEventContent represents the content of a m.room.encrypted message event. +// https://spec.matrix.org/v1.2/client-server-api/#mroomencrypted +// +// Note that sender_key and device_id are deprecated in Megolm events as of https://github.com/matrix-org/matrix-spec-proposals/pull/3700 +type EncryptedEventContent struct { + Algorithm id.Algorithm `json:"algorithm"` + SenderKey id.SenderKey `json:"sender_key,omitempty"` + // Deprecated: Matrix v1.3 + DeviceID id.DeviceID `json:"device_id,omitempty"` + // Only present for Megolm events + SessionID id.SessionID `json:"session_id,omitempty"` + + Ciphertext json.RawMessage `json:"ciphertext"` + + MegolmCiphertext []byte `json:"-"` + OlmCiphertext OlmCiphertexts `json:"-"` + + RelatesTo *RelatesTo `json:"m.relates_to,omitempty"` + Mentions *Mentions `json:"m.mentions,omitempty"` +} + +type OlmCiphertexts map[id.Curve25519]struct { + Body string `json:"body"` + Type id.OlmMsgType `json:"type"` +} + +type serializableEncryptedEventContent EncryptedEventContent + +func (content *EncryptedEventContent) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, (*serializableEncryptedEventContent)(content)) + if err != nil { + return err + } + switch content.Algorithm { + case id.AlgorithmOlmV1: + content.OlmCiphertext = make(OlmCiphertexts) + return json.Unmarshal(content.Ciphertext, &content.OlmCiphertext) + case id.AlgorithmMegolmV1: + if len(content.Ciphertext) == 0 || content.Ciphertext[0] != '"' || content.Ciphertext[len(content.Ciphertext)-1] != '"' { + return id.InputNotJSONString + } + content.MegolmCiphertext = content.Ciphertext[1 : len(content.Ciphertext)-1] + } + return nil +} + +func (content *EncryptedEventContent) MarshalJSON() ([]byte, error) { + var err error + switch content.Algorithm { + case id.AlgorithmOlmV1: + content.Ciphertext, err = json.Marshal(content.OlmCiphertext) + case id.AlgorithmMegolmV1: + content.Ciphertext = make([]byte, len(content.MegolmCiphertext)+2) + content.Ciphertext[0] = '"' + content.Ciphertext[len(content.Ciphertext)-1] = '"' + copy(content.Ciphertext[1:len(content.Ciphertext)-1], content.MegolmCiphertext) + } + if err != nil { + return nil, err + } + return json.Marshal((*serializableEncryptedEventContent)(content)) +} + +// RoomKeyEventContent represents the content of a m.room_key to_device event. +// https://spec.matrix.org/v1.2/client-server-api/#mroom_key +type RoomKeyEventContent struct { + Algorithm id.Algorithm `json:"algorithm"` + RoomID id.RoomID `json:"room_id"` + SessionID id.SessionID `json:"session_id"` + SessionKey string `json:"session_key"` +} + +// ForwardedRoomKeyEventContent represents the content of a m.forwarded_room_key to_device event. +// https://spec.matrix.org/v1.2/client-server-api/#mforwarded_room_key +type ForwardedRoomKeyEventContent struct { + RoomKeyEventContent + SenderKey id.SenderKey `json:"sender_key"` + SenderClaimedKey id.Ed25519 `json:"sender_claimed_ed25519_key"` + ForwardingKeyChain []string `json:"forwarding_curve25519_key_chain"` +} + +type KeyRequestAction string + +const ( + KeyRequestActionRequest = "request" + KeyRequestActionCancel = "request_cancellation" +) + +// RoomKeyRequestEventContent represents the content of a m.room_key_request to_device event. +// https://spec.matrix.org/v1.2/client-server-api/#mroom_key_request +type RoomKeyRequestEventContent struct { + Body RequestedKeyInfo `json:"body"` + Action KeyRequestAction `json:"action"` + RequestingDeviceID id.DeviceID `json:"requesting_device_id"` + RequestID string `json:"request_id"` +} + +type RequestedKeyInfo struct { + Algorithm id.Algorithm `json:"algorithm"` + RoomID id.RoomID `json:"room_id"` + SenderKey id.SenderKey `json:"sender_key"` + SessionID id.SessionID `json:"session_id"` +} + +type RoomKeyWithheldCode string + +const ( + RoomKeyWithheldBlacklisted RoomKeyWithheldCode = "m.blacklisted" + RoomKeyWithheldUnverified RoomKeyWithheldCode = "m.unverified" + RoomKeyWithheldUnauthorized RoomKeyWithheldCode = "m.unauthorised" + RoomKeyWithheldUnavailable RoomKeyWithheldCode = "m.unavailable" + RoomKeyWithheldNoOlmSession RoomKeyWithheldCode = "m.no_olm" +) + +type RoomKeyWithheldEventContent struct { + RoomID id.RoomID `json:"room_id,omitempty"` + Algorithm id.Algorithm `json:"algorithm"` + SessionID id.SessionID `json:"session_id,omitempty"` + SenderKey id.SenderKey `json:"sender_key"` + Code RoomKeyWithheldCode `json:"code"` + Reason string `json:"reason,omitempty"` +} + +type DummyEventContent struct{} diff --git a/vendor/maunium.net/go/mautrix/event/ephemeral.go b/vendor/maunium.net/go/mautrix/event/ephemeral.go new file mode 100644 index 00000000..f447404b --- /dev/null +++ b/vendor/maunium.net/go/mautrix/event/ephemeral.go @@ -0,0 +1,140 @@ +// Copyright (c) 2020 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package event + +import ( + "encoding/json" + "time" + + "maunium.net/go/mautrix/id" +) + +// TypingEventContent represents the content of a m.typing ephemeral event. +// https://spec.matrix.org/v1.2/client-server-api/#mtyping +type TypingEventContent struct { + UserIDs []id.UserID `json:"user_ids"` +} + +// ReceiptEventContent represents the content of a m.receipt ephemeral event. +// https://spec.matrix.org/v1.2/client-server-api/#mreceipt +type ReceiptEventContent map[id.EventID]Receipts + +func (rec ReceiptEventContent) Set(evtID id.EventID, receiptType ReceiptType, userID id.UserID, receipt ReadReceipt) { + rec.GetOrCreate(evtID).GetOrCreate(receiptType).Set(userID, receipt) +} + +func (rec ReceiptEventContent) GetOrCreate(evt id.EventID) Receipts { + receipts, ok := rec[evt] + if !ok { + receipts = make(Receipts) + rec[evt] = receipts + } + return receipts +} + +type ReceiptType string + +const ( + ReceiptTypeRead ReceiptType = "m.read" + ReceiptTypeReadPrivate ReceiptType = "m.read.private" +) + +type Receipts map[ReceiptType]UserReceipts + +func (rps Receipts) GetOrCreate(receiptType ReceiptType) UserReceipts { + read, ok := rps[receiptType] + if !ok { + read = make(UserReceipts) + rps[receiptType] = read + } + return read +} + +type UserReceipts map[id.UserID]ReadReceipt + +func (ur UserReceipts) Set(userID id.UserID, receipt ReadReceipt) { + ur[userID] = receipt +} + +type ThreadID = id.EventID + +const ReadReceiptThreadMain ThreadID = "main" + +type ReadReceipt struct { + Timestamp time.Time + + // Thread ID for thread-specific read receipts from MSC3771 + ThreadID ThreadID + + // Extra contains any unknown fields in the read receipt event. + // Most servers don't allow clients to set them, so this will be empty in most cases. + Extra map[string]interface{} +} + +func (rr *ReadReceipt) UnmarshalJSON(data []byte) error { + // Hacky compatibility hack against crappy clients that send double-encoded read receipts. + // TODO is this actually needed? clients can't currently set custom content in receipts 🤔 + if data[0] == '"' && data[len(data)-1] == '"' { + var strData string + err := json.Unmarshal(data, &strData) + if err != nil { + return err + } + data = []byte(strData) + } + + var parsed map[string]interface{} + err := json.Unmarshal(data, &parsed) + if err != nil { + return err + } + threadID, _ := parsed["thread_id"].(string) + ts, tsOK := parsed["ts"].(float64) + delete(parsed, "thread_id") + delete(parsed, "ts") + *rr = ReadReceipt{ + ThreadID: ThreadID(threadID), + Extra: parsed, + } + if tsOK { + rr.Timestamp = time.UnixMilli(int64(ts)) + } + return nil +} + +func (rr ReadReceipt) MarshalJSON() ([]byte, error) { + data := rr.Extra + if data == nil { + data = make(map[string]interface{}) + } + if rr.ThreadID != "" { + data["thread_id"] = rr.ThreadID + } + if !rr.Timestamp.IsZero() { + data["ts"] = rr.Timestamp.UnixMilli() + } + return json.Marshal(data) +} + +type Presence string + +const ( + PresenceOnline Presence = "online" + PresenceOffline Presence = "offline" + PresenceUnavailable Presence = "unavailable" +) + +// PresenceEventContent represents the content of a m.presence ephemeral event. +// https://spec.matrix.org/v1.2/client-server-api/#mpresence +type PresenceEventContent struct { + Presence Presence `json:"presence"` + Displayname string `json:"displayname,omitempty"` + AvatarURL id.ContentURIString `json:"avatar_url,omitempty"` + LastActiveAgo int64 `json:"last_active_ago,omitempty"` + CurrentlyActive bool `json:"currently_active,omitempty"` + StatusMessage string `json:"status_msg,omitempty"` +} diff --git a/vendor/maunium.net/go/mautrix/event/events.go b/vendor/maunium.net/go/mautrix/event/events.go new file mode 100644 index 00000000..d2b61046 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/event/events.go @@ -0,0 +1,149 @@ +// Copyright (c) 2020 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package event + +import ( + "encoding/json" + "time" + + "maunium.net/go/mautrix/id" +) + +// 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 id.UserID `json:"sender,omitempty"` // The user ID of the sender of the event + Type Type `json:"type"` // The event type + Timestamp int64 `json:"origin_server_ts,omitempty"` // The unix timestamp when this message was sent by the origin server + ID id.EventID `json:"event_id,omitempty"` // The unique ID of this event + RoomID id.RoomID `json:"room_id,omitempty"` // The room the event was sent to. May be nil (e.g. for presence) + Content Content `json:"content"` // The JSON content of the event. + Redacts id.EventID `json:"redacts,omitempty"` // The event ID that was redacted if a m.room.redaction event + Unsigned Unsigned `json:"unsigned,omitempty"` // Unsigned content set by own homeserver. + + Mautrix MautrixInfo `json:"-"` + + ToUserID id.UserID `json:"to_user_id,omitempty"` // The user ID that the to-device event was sent to. Only present in MSC2409 appservice transactions. + ToDeviceID id.DeviceID `json:"to_device_id,omitempty"` // The device ID that the to-device event was sent to. Only present in MSC2409 appservice transactions. +} + +type eventForMarshaling struct { + StateKey *string `json:"state_key,omitempty"` + Sender id.UserID `json:"sender,omitempty"` + Type Type `json:"type"` + Timestamp int64 `json:"origin_server_ts,omitempty"` + ID id.EventID `json:"event_id,omitempty"` + RoomID id.RoomID `json:"room_id,omitempty"` + Content Content `json:"content"` + Redacts id.EventID `json:"redacts,omitempty"` + Unsigned *Unsigned `json:"unsigned,omitempty"` + + PrevContent *Content `json:"prev_content,omitempty"` + ReplacesState *id.EventID `json:"replaces_state,omitempty"` + + ToUserID id.UserID `json:"to_user_id,omitempty"` + ToDeviceID id.DeviceID `json:"to_device_id,omitempty"` +} + +// UnmarshalJSON unmarshals the event, including moving prev_content from the top level to inside unsigned. +func (evt *Event) UnmarshalJSON(data []byte) error { + var efm eventForMarshaling + err := json.Unmarshal(data, &efm) + if err != nil { + return err + } + evt.StateKey = efm.StateKey + evt.Sender = efm.Sender + evt.Type = efm.Type + evt.Timestamp = efm.Timestamp + evt.ID = efm.ID + evt.RoomID = efm.RoomID + evt.Content = efm.Content + evt.Redacts = efm.Redacts + if efm.Unsigned != nil { + evt.Unsigned = *efm.Unsigned + } + if efm.PrevContent != nil && evt.Unsigned.PrevContent == nil { + evt.Unsigned.PrevContent = efm.PrevContent + } + if efm.ReplacesState != nil && *efm.ReplacesState != "" && evt.Unsigned.ReplacesState == "" { + evt.Unsigned.ReplacesState = *efm.ReplacesState + } + evt.ToUserID = efm.ToUserID + evt.ToDeviceID = efm.ToDeviceID + return nil +} + +// MarshalJSON marshals the event, including omitting the unsigned field if it's empty. +// +// This is necessary because Unsigned is not a pointer (for convenience reasons), +// and encoding/json doesn't know how to check if a non-pointer struct is empty. +// +// TODO(tulir): maybe it makes more sense to make Unsigned a pointer and make an easy and safe way to access it? +func (evt *Event) MarshalJSON() ([]byte, error) { + unsigned := &evt.Unsigned + if unsigned.IsEmpty() { + unsigned = nil + } + return json.Marshal(&eventForMarshaling{ + StateKey: evt.StateKey, + Sender: evt.Sender, + Type: evt.Type, + Timestamp: evt.Timestamp, + ID: evt.ID, + RoomID: evt.RoomID, + Content: evt.Content, + Redacts: evt.Redacts, + Unsigned: unsigned, + ToUserID: evt.ToUserID, + ToDeviceID: evt.ToDeviceID, + }) +} + +type MautrixInfo struct { + TrustState id.TrustState + ForwardedKeys bool + WasEncrypted bool + TrustSource *id.Device + + ReceivedAt time.Time + DecryptionDuration time.Duration + + CheckpointSent bool +} + +func (evt *Event) GetStateKey() string { + if evt.StateKey != nil { + return *evt.StateKey + } + return "" +} + +type StrippedState struct { + Content Content `json:"content"` + Type Type `json:"type"` + StateKey string `json:"state_key"` + Sender id.UserID `json:"sender"` +} + +type Unsigned struct { + PrevContent *Content `json:"prev_content,omitempty"` + PrevSender id.UserID `json:"prev_sender,omitempty"` + ReplacesState id.EventID `json:"replaces_state,omitempty"` + Age int64 `json:"age,omitempty"` + TransactionID string `json:"transaction_id,omitempty"` + Relations *Relations `json:"m.relations,omitempty"` + RedactedBecause *Event `json:"redacted_because,omitempty"` + InviteRoomState []StrippedState `json:"invite_room_state,omitempty"` + + BeeperHSOrder int64 `json:"com.beeper.hs.order,omitempty"` +} + +func (us *Unsigned) IsEmpty() bool { + return us.PrevContent == nil && us.PrevSender == "" && us.ReplacesState == "" && us.Age == 0 && + us.TransactionID == "" && us.RedactedBecause == nil && us.InviteRoomState == nil && us.Relations == nil +} diff --git a/vendor/maunium.net/go/mautrix/event/member.go b/vendor/maunium.net/go/mautrix/event/member.go new file mode 100644 index 00000000..ebafdcb7 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/event/member.go @@ -0,0 +1,53 @@ +// Copyright (c) 2020 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package event + +import ( + "encoding/json" + + "maunium.net/go/mautrix/id" +) + +// Membership is an enum specifying the membership state of a room member. +type Membership string + +func (ms Membership) IsInviteOrJoin() bool { + return ms == MembershipJoin || ms == MembershipInvite +} + +func (ms Membership) IsLeaveOrBan() bool { + return ms == MembershipLeave || ms == MembershipBan +} + +// The allowed membership states as specified in spec section 10.5.5. +const ( + MembershipJoin Membership = "join" + MembershipLeave Membership = "leave" + MembershipInvite Membership = "invite" + MembershipBan Membership = "ban" + MembershipKnock Membership = "knock" +) + +// MemberEventContent represents the content of a m.room.member state event. +// https://spec.matrix.org/v1.2/client-server-api/#mroommember +type MemberEventContent struct { + Membership Membership `json:"membership"` + AvatarURL id.ContentURIString `json:"avatar_url,omitempty"` + Displayname string `json:"displayname,omitempty"` + IsDirect bool `json:"is_direct,omitempty"` + ThirdPartyInvite *ThirdPartyInvite `json:"third_party_invite,omitempty"` + Reason string `json:"reason,omitempty"` +} + +type ThirdPartyInvite struct { + DisplayName string `json:"display_name"` + Signed struct { + Token string `json:"token"` + Signatures json.RawMessage `json:"signatures"` + MXID string `json:"mxid"` + } +} diff --git a/vendor/maunium.net/go/mautrix/event/message.go b/vendor/maunium.net/go/mautrix/event/message.go new file mode 100644 index 00000000..a5f5ec0e --- /dev/null +++ b/vendor/maunium.net/go/mautrix/event/message.go @@ -0,0 +1,276 @@ +// Copyright (c) 2023 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package event + +import ( + "encoding/json" + "strconv" + "strings" + + "golang.org/x/net/html" + + "maunium.net/go/mautrix/crypto/attachment" + "maunium.net/go/mautrix/id" +) + +// MessageType is the sub-type of a m.room.message event. +// https://spec.matrix.org/v1.2/client-server-api/#mroommessage-msgtypes +type MessageType string + +// Msgtypes +const ( + MsgText MessageType = "m.text" + MsgEmote MessageType = "m.emote" + MsgNotice MessageType = "m.notice" + MsgImage MessageType = "m.image" + MsgLocation MessageType = "m.location" + MsgVideo MessageType = "m.video" + MsgAudio MessageType = "m.audio" + MsgFile MessageType = "m.file" + + MsgVerificationRequest MessageType = "m.key.verification.request" +) + +// Format specifies the format of the formatted_body in m.room.message events. +// https://spec.matrix.org/v1.2/client-server-api/#mroommessage-msgtypes +type Format string + +// Message formats +const ( + FormatHTML Format = "org.matrix.custom.html" +) + +// RedactionEventContent represents the content of a m.room.redaction message event. +// +// The redacted event ID is still at the top level, but will move in a future room version. +// See https://github.com/matrix-org/matrix-doc/pull/2244 and https://github.com/matrix-org/matrix-doc/pull/2174 +// +// https://spec.matrix.org/v1.2/client-server-api/#mroomredaction +type RedactionEventContent struct { + Reason string `json:"reason,omitempty"` +} + +// ReactionEventContent represents the content of a m.reaction message event. +// This is not yet in a spec release, see https://github.com/matrix-org/matrix-doc/pull/1849 +type ReactionEventContent struct { + RelatesTo RelatesTo `json:"m.relates_to"` +} + +func (content *ReactionEventContent) GetRelatesTo() *RelatesTo { + return &content.RelatesTo +} + +func (content *ReactionEventContent) OptionalGetRelatesTo() *RelatesTo { + return &content.RelatesTo +} + +func (content *ReactionEventContent) SetRelatesTo(rel *RelatesTo) { + content.RelatesTo = *rel +} + +// MessageEventContent represents the content of a m.room.message event. +// +// It is also used to represent m.sticker events, as they are equivalent to m.room.message +// with the exception of the msgtype field. +// +// https://spec.matrix.org/v1.2/client-server-api/#mroommessage +type MessageEventContent struct { + // Base m.room.message fields + MsgType MessageType `json:"msgtype,omitempty"` + Body string `json:"body"` + + // Extra fields for text types + Format Format `json:"format,omitempty"` + FormattedBody string `json:"formatted_body,omitempty"` + + // Extra field for m.location + GeoURI string `json:"geo_uri,omitempty"` + + // Extra fields for media types + URL id.ContentURIString `json:"url,omitempty"` + Info *FileInfo `json:"info,omitempty"` + File *EncryptedFileInfo `json:"file,omitempty"` + + FileName string `json:"filename,omitempty"` + + Mentions *Mentions `json:"m.mentions,omitempty"` + UnstableMentions *Mentions `json:"org.matrix.msc3952.mentions,omitempty"` + + // Edits and relations + NewContent *MessageEventContent `json:"m.new_content,omitempty"` + RelatesTo *RelatesTo `json:"m.relates_to,omitempty"` + + // In-room verification + To id.UserID `json:"to,omitempty"` + FromDevice id.DeviceID `json:"from_device,omitempty"` + Methods []VerificationMethod `json:"methods,omitempty"` + + replyFallbackRemoved bool + + MessageSendRetry *BeeperRetryMetadata `json:"com.beeper.message_send_retry,omitempty"` +} + +func (content *MessageEventContent) GetRelatesTo() *RelatesTo { + if content.RelatesTo == nil { + content.RelatesTo = &RelatesTo{} + } + return content.RelatesTo +} + +func (content *MessageEventContent) OptionalGetRelatesTo() *RelatesTo { + return content.RelatesTo +} + +func (content *MessageEventContent) SetRelatesTo(rel *RelatesTo) { + content.RelatesTo = rel +} + +func (content *MessageEventContent) SetEdit(original id.EventID) { + newContent := *content + content.NewContent = &newContent + content.RelatesTo = (&RelatesTo{}).SetReplace(original) + if content.MsgType == MsgText || content.MsgType == MsgNotice { + content.Body = "* " + content.Body + if content.Format == FormatHTML && len(content.FormattedBody) > 0 { + content.FormattedBody = "* " + content.FormattedBody + } + // If the message is long, remove most of the useless edit fallback to avoid event size issues. + if len(content.Body) > 10000 { + content.FormattedBody = "" + content.Format = "" + content.Body = content.Body[:50] + "[edit fallback cut…]" + } + } +} + +func TextToHTML(text string) string { + return strings.ReplaceAll(html.EscapeString(text), "\n", "<br/>") +} + +func (content *MessageEventContent) EnsureHasHTML() { + if len(content.FormattedBody) == 0 || content.Format != FormatHTML { + content.FormattedBody = TextToHTML(content.Body) + content.Format = FormatHTML + } +} + +func (content *MessageEventContent) GetFile() *EncryptedFileInfo { + if content.File == nil { + content.File = &EncryptedFileInfo{} + } + return content.File +} + +func (content *MessageEventContent) GetInfo() *FileInfo { + if content.Info == nil { + content.Info = &FileInfo{} + } + return content.Info +} + +type Mentions struct { + UserIDs []id.UserID `json:"user_ids,omitempty"` + Room bool `json:"room,omitempty"` +} + +type EncryptedFileInfo struct { + attachment.EncryptedFile + URL id.ContentURIString `json:"url"` +} + +type FileInfo struct { + MimeType string `json:"mimetype,omitempty"` + ThumbnailInfo *FileInfo `json:"thumbnail_info,omitempty"` + ThumbnailURL id.ContentURIString `json:"thumbnail_url,omitempty"` + ThumbnailFile *EncryptedFileInfo `json:"thumbnail_file,omitempty"` + Width int `json:"-"` + Height int `json:"-"` + Duration int `json:"-"` + Size int `json:"-"` +} + +type serializableFileInfo struct { + MimeType string `json:"mimetype,omitempty"` + ThumbnailInfo *serializableFileInfo `json:"thumbnail_info,omitempty"` + ThumbnailURL id.ContentURIString `json:"thumbnail_url,omitempty"` + ThumbnailFile *EncryptedFileInfo `json:"thumbnail_file,omitempty"` + + Width json.Number `json:"w,omitempty"` + Height json.Number `json:"h,omitempty"` + Duration json.Number `json:"duration,omitempty"` + Size json.Number `json:"size,omitempty"` +} + +func (sfi *serializableFileInfo) CopyFrom(fileInfo *FileInfo) *serializableFileInfo { + if fileInfo == nil { + return nil + } + *sfi = serializableFileInfo{ + MimeType: fileInfo.MimeType, + ThumbnailURL: fileInfo.ThumbnailURL, + ThumbnailInfo: (&serializableFileInfo{}).CopyFrom(fileInfo.ThumbnailInfo), + ThumbnailFile: fileInfo.ThumbnailFile, + } + if fileInfo.Width > 0 { + sfi.Width = json.Number(strconv.Itoa(fileInfo.Width)) + } + if fileInfo.Height > 0 { + sfi.Height = json.Number(strconv.Itoa(fileInfo.Height)) + } + if fileInfo.Size > 0 { + sfi.Size = json.Number(strconv.Itoa(fileInfo.Size)) + + } + if fileInfo.Duration > 0 { + sfi.Duration = json.Number(strconv.Itoa(int(fileInfo.Duration))) + } + return sfi +} + +func (sfi *serializableFileInfo) CopyTo(fileInfo *FileInfo) { + *fileInfo = FileInfo{ + Width: numberToInt(sfi.Width), + Height: numberToInt(sfi.Height), + Size: numberToInt(sfi.Size), + Duration: numberToInt(sfi.Duration), + MimeType: sfi.MimeType, + ThumbnailURL: sfi.ThumbnailURL, + ThumbnailFile: sfi.ThumbnailFile, + } + if sfi.ThumbnailInfo != nil { + fileInfo.ThumbnailInfo = &FileInfo{} + sfi.ThumbnailInfo.CopyTo(fileInfo.ThumbnailInfo) + } +} + +func (fileInfo *FileInfo) UnmarshalJSON(data []byte) error { + sfi := &serializableFileInfo{} + if err := json.Unmarshal(data, sfi); err != nil { + return err + } + sfi.CopyTo(fileInfo) + return nil +} + +func (fileInfo *FileInfo) MarshalJSON() ([]byte, error) { + return json.Marshal((&serializableFileInfo{}).CopyFrom(fileInfo)) +} + +func numberToInt(val json.Number) int { + f64, _ := val.Float64() + if f64 > 0 { + return int(f64) + } + return 0 +} + +func (fileInfo *FileInfo) GetThumbnailInfo() *FileInfo { + if fileInfo.ThumbnailInfo == nil { + fileInfo.ThumbnailInfo = &FileInfo{} + } + return fileInfo.ThumbnailInfo +} diff --git a/vendor/maunium.net/go/mautrix/event/powerlevels.go b/vendor/maunium.net/go/mautrix/event/powerlevels.go new file mode 100644 index 00000000..cf623565 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/event/powerlevels.go @@ -0,0 +1,149 @@ +// Copyright (c) 2020 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package event + +import ( + "sync" + + "maunium.net/go/mautrix/id" +) + +// PowerLevelsEventContent represents the content of a m.room.power_levels state event content. +// https://spec.matrix.org/v1.5/client-server-api/#mroompower_levels +type PowerLevelsEventContent struct { + usersLock sync.RWMutex + Users map[id.UserID]int `json:"users,omitempty"` + UsersDefault int `json:"users_default,omitempty"` + + eventsLock sync.RWMutex + Events map[string]int `json:"events,omitempty"` + EventsDefault int `json:"events_default,omitempty"` + + Notifications *NotificationPowerLevels `json:"notifications,omitempty"` + + StateDefaultPtr *int `json:"state_default,omitempty"` + + InvitePtr *int `json:"invite,omitempty"` + KickPtr *int `json:"kick,omitempty"` + BanPtr *int `json:"ban,omitempty"` + RedactPtr *int `json:"redact,omitempty"` + HistoricalPtr *int `json:"historical,omitempty"` +} + +type NotificationPowerLevels struct { + RoomPtr *int `json:"room,omitempty"` +} + +func (npl *NotificationPowerLevels) Room() int { + if npl != nil && npl.RoomPtr != nil { + return *npl.RoomPtr + } + return 50 +} + +func (pl *PowerLevelsEventContent) Invite() int { + if pl.InvitePtr != nil { + return *pl.InvitePtr + } + return 50 +} + +func (pl *PowerLevelsEventContent) Kick() int { + if pl.KickPtr != nil { + return *pl.KickPtr + } + return 50 +} + +func (pl *PowerLevelsEventContent) Ban() int { + if pl.BanPtr != nil { + return *pl.BanPtr + } + return 50 +} + +func (pl *PowerLevelsEventContent) Redact() int { + if pl.RedactPtr != nil { + return *pl.RedactPtr + } + return 50 +} + +func (pl *PowerLevelsEventContent) Historical() int { + if pl.HistoricalPtr != nil { + return *pl.HistoricalPtr + } + return 100 +} + +func (pl *PowerLevelsEventContent) StateDefault() int { + if pl.StateDefaultPtr != nil { + return *pl.StateDefaultPtr + } + return 50 +} + +func (pl *PowerLevelsEventContent) GetUserLevel(userID id.UserID) int { + pl.usersLock.RLock() + defer pl.usersLock.RUnlock() + level, ok := pl.Users[userID] + if !ok { + return pl.UsersDefault + } + return level +} + +func (pl *PowerLevelsEventContent) SetUserLevel(userID id.UserID, level int) { + pl.usersLock.Lock() + defer pl.usersLock.Unlock() + if level == pl.UsersDefault { + delete(pl.Users, userID) + } else { + pl.Users[userID] = level + } +} + +func (pl *PowerLevelsEventContent) EnsureUserLevel(userID id.UserID, level int) bool { + existingLevel := pl.GetUserLevel(userID) + if existingLevel != level { + pl.SetUserLevel(userID, level) + return true + } + return false +} + +func (pl *PowerLevelsEventContent) GetEventLevel(eventType Type) int { + pl.eventsLock.RLock() + defer pl.eventsLock.RUnlock() + level, ok := pl.Events[eventType.String()] + if !ok { + if eventType.IsState() { + return pl.StateDefault() + } + return pl.EventsDefault + } + return level +} + +func (pl *PowerLevelsEventContent) SetEventLevel(eventType Type, level int) { + pl.eventsLock.Lock() + defer pl.eventsLock.Unlock() + if (eventType.IsState() && level == pl.StateDefault()) || (!eventType.IsState() && level == pl.EventsDefault) { + delete(pl.Events, eventType.String()) + } else { + pl.Events[eventType.String()] = level + } +} + +func (pl *PowerLevelsEventContent) EnsureEventLevel(eventType Type, level int) bool { + existingLevel := pl.GetEventLevel(eventType) + if existingLevel != level { + pl.SetEventLevel(eventType, level) + return true + } + return false +} diff --git a/vendor/maunium.net/go/mautrix/event/relations.go b/vendor/maunium.net/go/mautrix/event/relations.go new file mode 100644 index 00000000..ecd7a959 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/event/relations.go @@ -0,0 +1,234 @@ +// Copyright (c) 2020 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package event + +import ( + "encoding/json" + + "maunium.net/go/mautrix/id" +) + +type RelationType string + +const ( + RelReplace RelationType = "m.replace" + RelReference RelationType = "m.reference" + RelAnnotation RelationType = "m.annotation" + RelThread RelationType = "m.thread" +) + +type RelatesTo struct { + Type RelationType `json:"rel_type,omitempty"` + EventID id.EventID `json:"event_id,omitempty"` + Key string `json:"key,omitempty"` + + InReplyTo *InReplyTo `json:"m.in_reply_to,omitempty"` + IsFallingBack bool `json:"is_falling_back,omitempty"` +} + +type InReplyTo struct { + EventID id.EventID `json:"event_id,omitempty"` + + UnstableRoomID id.RoomID `json:"room_id,omitempty"` +} + +func (rel *RelatesTo) Copy() *RelatesTo { + if rel == nil { + return nil + } + cp := *rel + return &cp +} + +func (rel *RelatesTo) GetReplaceID() id.EventID { + if rel != nil && rel.Type == RelReplace { + return rel.EventID + } + return "" +} + +func (rel *RelatesTo) GetReferenceID() id.EventID { + if rel != nil && rel.Type == RelReference { + return rel.EventID + } + return "" +} + +func (rel *RelatesTo) GetThreadParent() id.EventID { + if rel != nil && rel.Type == RelThread { + return rel.EventID + } + return "" +} + +func (rel *RelatesTo) GetReplyTo() id.EventID { + if rel != nil && rel.InReplyTo != nil { + return rel.InReplyTo.EventID + } + return "" +} + +func (rel *RelatesTo) GetNonFallbackReplyTo() id.EventID { + if rel != nil && rel.InReplyTo != nil && !rel.IsFallingBack { + return rel.InReplyTo.EventID + } + return "" +} + +func (rel *RelatesTo) GetAnnotationID() id.EventID { + if rel != nil && rel.Type == RelAnnotation { + return rel.EventID + } + return "" +} + +func (rel *RelatesTo) GetAnnotationKey() string { + if rel != nil && rel.Type == RelAnnotation { + return rel.Key + } + return "" +} + +func (rel *RelatesTo) SetReplace(mxid id.EventID) *RelatesTo { + rel.Type = RelReplace + rel.EventID = mxid + return rel +} + +func (rel *RelatesTo) SetReplyTo(mxid id.EventID) *RelatesTo { + rel.InReplyTo = &InReplyTo{EventID: mxid} + rel.IsFallingBack = false + return rel +} + +func (rel *RelatesTo) SetThread(mxid, fallback id.EventID) *RelatesTo { + rel.Type = RelThread + rel.EventID = mxid + if fallback != "" && rel.GetReplyTo() == "" { + rel.SetReplyTo(fallback) + rel.IsFallingBack = true + } + return rel +} + +func (rel *RelatesTo) SetAnnotation(mxid id.EventID, key string) *RelatesTo { + rel.Type = RelAnnotation + rel.EventID = mxid + rel.Key = key + return rel +} + +type RelationChunkItem struct { + Type RelationType `json:"type"` + EventID string `json:"event_id,omitempty"` + Key string `json:"key,omitempty"` + Count int `json:"count,omitempty"` +} + +type RelationChunk struct { + Chunk []RelationChunkItem `json:"chunk"` + + Limited bool `json:"limited"` + Count int `json:"count"` +} + +type AnnotationChunk struct { + RelationChunk + Map map[string]int `json:"-"` +} + +type serializableAnnotationChunk AnnotationChunk + +func (ac *AnnotationChunk) UnmarshalJSON(data []byte) error { + if err := json.Unmarshal(data, (*serializableAnnotationChunk)(ac)); err != nil { + return err + } + ac.Map = make(map[string]int) + for _, item := range ac.Chunk { + if item.Key != "" { + ac.Map[item.Key] += item.Count + } + } + return nil +} + +func (ac *AnnotationChunk) Serialize() RelationChunk { + ac.Chunk = make([]RelationChunkItem, len(ac.Map)) + i := 0 + for key, count := range ac.Map { + ac.Chunk[i] = RelationChunkItem{ + Type: RelAnnotation, + Key: key, + Count: count, + } + i++ + } + return ac.RelationChunk +} + +type EventIDChunk struct { + RelationChunk + List []string `json:"-"` +} + +type serializableEventIDChunk EventIDChunk + +func (ec *EventIDChunk) UnmarshalJSON(data []byte) error { + if err := json.Unmarshal(data, (*serializableEventIDChunk)(ec)); err != nil { + return err + } + for _, item := range ec.Chunk { + ec.List = append(ec.List, item.EventID) + } + return nil +} + +func (ec *EventIDChunk) Serialize(typ RelationType) RelationChunk { + ec.Chunk = make([]RelationChunkItem, len(ec.List)) + for i, eventID := range ec.List { + ec.Chunk[i] = RelationChunkItem{ + Type: typ, + EventID: eventID, + } + } + return ec.RelationChunk +} + +type Relations struct { + Raw map[RelationType]RelationChunk `json:"-"` + + Annotations AnnotationChunk `json:"m.annotation,omitempty"` + References EventIDChunk `json:"m.reference,omitempty"` + Replaces EventIDChunk `json:"m.replace,omitempty"` +} + +type serializableRelations Relations + +func (relations *Relations) UnmarshalJSON(data []byte) error { + if err := json.Unmarshal(data, &relations.Raw); err != nil { + return err + } + return json.Unmarshal(data, (*serializableRelations)(relations)) +} + +func (relations *Relations) MarshalJSON() ([]byte, error) { + if relations.Raw == nil { + relations.Raw = make(map[RelationType]RelationChunk) + } + relations.Raw[RelAnnotation] = relations.Annotations.Serialize() + relations.Raw[RelReference] = relations.References.Serialize(RelReference) + relations.Raw[RelReplace] = relations.Replaces.Serialize(RelReplace) + for key, item := range relations.Raw { + if !item.Limited { + item.Count = len(item.Chunk) + } + if item.Count == 0 { + delete(relations.Raw, key) + } + } + return json.Marshal(relations.Raw) +} diff --git a/vendor/maunium.net/go/mautrix/event/reply.go b/vendor/maunium.net/go/mautrix/event/reply.go new file mode 100644 index 00000000..e1844a4a --- /dev/null +++ b/vendor/maunium.net/go/mautrix/event/reply.go @@ -0,0 +1,100 @@ +// Copyright (c) 2020 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package event + +import ( + "fmt" + "regexp" + "strings" + + "golang.org/x/net/html" + + "maunium.net/go/mautrix/id" +) + +var HTMLReplyFallbackRegex = regexp.MustCompile(`^<mx-reply>[\s\S]+?</mx-reply>`) + +func TrimReplyFallbackHTML(html string) string { + return HTMLReplyFallbackRegex.ReplaceAllString(html, "") +} + +func TrimReplyFallbackText(text string) string { + if !strings.HasPrefix(text, "> ") || !strings.Contains(text, "\n") { + return text + } + + lines := strings.Split(text, "\n") + for len(lines) > 0 && strings.HasPrefix(lines[0], "> ") { + lines = lines[1:] + } + return strings.TrimSpace(strings.Join(lines, "\n")) +} + +func (content *MessageEventContent) RemoveReplyFallback() { + if len(content.RelatesTo.GetReplyTo()) > 0 && !content.replyFallbackRemoved { + if content.Format == FormatHTML { + content.FormattedBody = TrimReplyFallbackHTML(content.FormattedBody) + } + content.Body = TrimReplyFallbackText(content.Body) + content.replyFallbackRemoved = true + } +} + +// Deprecated: RelatesTo methods are nil-safe, so RelatesTo.GetReplyTo can be used directly +func (content *MessageEventContent) GetReplyTo() id.EventID { + return content.RelatesTo.GetReplyTo() +} + +const ReplyFormat = `<mx-reply><blockquote><a href="https://matrix.to/#/%s/%s">In reply to</a> <a href="https://matrix.to/#/%s">%s</a><br>%s</blockquote></mx-reply>` + +func (evt *Event) GenerateReplyFallbackHTML() string { + parsedContent, ok := evt.Content.Parsed.(*MessageEventContent) + if !ok { + return "" + } + parsedContent.RemoveReplyFallback() + body := parsedContent.FormattedBody + if len(body) == 0 { + body = strings.ReplaceAll(html.EscapeString(parsedContent.Body), "\n", "<br/>") + } + + senderDisplayName := evt.Sender + + return fmt.Sprintf(ReplyFormat, evt.RoomID, evt.ID, evt.Sender, senderDisplayName, body) +} + +func (evt *Event) GenerateReplyFallbackText() string { + parsedContent, ok := evt.Content.Parsed.(*MessageEventContent) + if !ok { + return "" + } + parsedContent.RemoveReplyFallback() + body := parsedContent.Body + lines := strings.Split(strings.TrimSpace(body), "\n") + firstLine, lines := lines[0], lines[1:] + + senderDisplayName := evt.Sender + + var fallbackText strings.Builder + _, _ = fmt.Fprintf(&fallbackText, "> <%s> %s", senderDisplayName, firstLine) + for _, line := range lines { + _, _ = fmt.Fprintf(&fallbackText, "\n> %s", line) + } + fallbackText.WriteString("\n\n") + return fallbackText.String() +} + +func (content *MessageEventContent) SetReply(inReplyTo *Event) { + content.RelatesTo = (&RelatesTo{}).SetReplyTo(inReplyTo.ID) + + if content.MsgType == MsgText || content.MsgType == MsgNotice { + content.EnsureHasHTML() + content.FormattedBody = inReplyTo.GenerateReplyFallbackHTML() + content.FormattedBody + content.Body = inReplyTo.GenerateReplyFallbackText() + content.Body + content.replyFallbackRemoved = false + } +} diff --git a/vendor/maunium.net/go/mautrix/event/state.go b/vendor/maunium.net/go/mautrix/event/state.go new file mode 100644 index 00000000..a387f015 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/event/state.go @@ -0,0 +1,176 @@ +// Copyright (c) 2021 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package event + +import ( + "maunium.net/go/mautrix/id" +) + +// CanonicalAliasEventContent represents the content of a m.room.canonical_alias state event. +// https://spec.matrix.org/v1.2/client-server-api/#mroomcanonical_alias +type CanonicalAliasEventContent struct { + Alias id.RoomAlias `json:"alias"` + AltAliases []id.RoomAlias `json:"alt_aliases,omitempty"` +} + +// RoomNameEventContent represents the content of a m.room.name state event. +// https://spec.matrix.org/v1.2/client-server-api/#mroomname +type RoomNameEventContent struct { + Name string `json:"name"` +} + +// RoomAvatarEventContent represents the content of a m.room.avatar state event. +// https://spec.matrix.org/v1.2/client-server-api/#mroomavatar +type RoomAvatarEventContent struct { + URL id.ContentURI `json:"url"` + Info *FileInfo `json:"info,omitempty"` +} + +// ServerACLEventContent represents the content of a m.room.server_acl state event. +// https://spec.matrix.org/v1.2/client-server-api/#server-access-control-lists-acls-for-rooms +type ServerACLEventContent struct { + Allow []string `json:"allow,omitempty"` + AllowIPLiterals bool `json:"allow_ip_literals"` + Deny []string `json:"deny,omitempty"` +} + +// TopicEventContent represents the content of a m.room.topic state event. +// https://spec.matrix.org/v1.2/client-server-api/#mroomtopic +type TopicEventContent struct { + Topic string `json:"topic"` +} + +// TombstoneEventContent represents the content of a m.room.tombstone state event. +// https://spec.matrix.org/v1.2/client-server-api/#mroomtombstone +type TombstoneEventContent struct { + Body string `json:"body"` + ReplacementRoom id.RoomID `json:"replacement_room"` +} + +type Predecessor struct { + RoomID id.RoomID `json:"room_id"` + EventID id.EventID `json:"event_id"` +} + +// CreateEventContent represents the content of a m.room.create state event. +// https://spec.matrix.org/v1.2/client-server-api/#mroomcreate +type CreateEventContent struct { + Type RoomType `json:"type,omitempty"` + Creator id.UserID `json:"creator,omitempty"` + Federate bool `json:"m.federate,omitempty"` + RoomVersion string `json:"room_version,omitempty"` + Predecessor *Predecessor `json:"predecessor,omitempty"` +} + +// JoinRule specifies how open a room is to new members. +// https://spec.matrix.org/v1.2/client-server-api/#mroomjoin_rules +type JoinRule string + +const ( + JoinRulePublic JoinRule = "public" + JoinRuleKnock JoinRule = "knock" + JoinRuleInvite JoinRule = "invite" + JoinRuleRestricted JoinRule = "restricted" + JoinRulePrivate JoinRule = "private" +) + +// JoinRulesEventContent represents the content of a m.room.join_rules state event. +// https://spec.matrix.org/v1.2/client-server-api/#mroomjoin_rules +type JoinRulesEventContent struct { + JoinRule JoinRule `json:"join_rule"` + Allow []JoinRuleAllow `json:"allow,omitempty"` +} + +type JoinRuleAllowType string + +const ( + JoinRuleAllowRoomMembership JoinRuleAllowType = "m.room_membership" +) + +type JoinRuleAllow struct { + RoomID id.RoomID `json:"room_id"` + Type JoinRuleAllowType `json:"type"` +} + +// PinnedEventsEventContent represents the content of a m.room.pinned_events state event. +// https://spec.matrix.org/v1.2/client-server-api/#mroompinned_events +type PinnedEventsEventContent struct { + Pinned []id.EventID `json:"pinned"` +} + +// HistoryVisibility specifies who can see new messages. +// https://spec.matrix.org/v1.2/client-server-api/#mroomhistory_visibility +type HistoryVisibility string + +const ( + HistoryVisibilityInvited HistoryVisibility = "invited" + HistoryVisibilityJoined HistoryVisibility = "joined" + HistoryVisibilityShared HistoryVisibility = "shared" + HistoryVisibilityWorldReadable HistoryVisibility = "world_readable" +) + +// HistoryVisibilityEventContent represents the content of a m.room.history_visibility state event. +// https://spec.matrix.org/v1.2/client-server-api/#mroomhistory_visibility +type HistoryVisibilityEventContent struct { + HistoryVisibility HistoryVisibility `json:"history_visibility"` +} + +// GuestAccess specifies whether or not guest accounts can join. +// https://spec.matrix.org/v1.2/client-server-api/#mroomguest_access +type GuestAccess string + +const ( + GuestAccessCanJoin GuestAccess = "can_join" + GuestAccessForbidden GuestAccess = "forbidden" +) + +// GuestAccessEventContent represents the content of a m.room.guest_access state event. +// https://spec.matrix.org/v1.2/client-server-api/#mroomguest_access +type GuestAccessEventContent struct { + GuestAccess GuestAccess `json:"guest_access"` +} + +type BridgeInfoSection struct { + ID string `json:"id"` + DisplayName string `json:"displayname,omitempty"` + AvatarURL id.ContentURIString `json:"avatar_url,omitempty"` + ExternalURL string `json:"external_url,omitempty"` +} + +// BridgeEventContent represents the content of a m.bridge state event. +// https://github.com/matrix-org/matrix-doc/pull/2346 +type BridgeEventContent struct { + BridgeBot id.UserID `json:"bridgebot"` + Creator id.UserID `json:"creator,omitempty"` + Protocol BridgeInfoSection `json:"protocol"` + Network *BridgeInfoSection `json:"network,omitempty"` + Channel BridgeInfoSection `json:"channel"` +} + +type SpaceChildEventContent struct { + Via []string `json:"via,omitempty"` + Order string `json:"order,omitempty"` + Suggested bool `json:"suggested,omitempty"` +} + +type SpaceParentEventContent struct { + Via []string `json:"via,omitempty"` + Canonical bool `json:"canonical,omitempty"` +} + +// ModPolicyContent represents the content of a m.room.rule.user, m.room.rule.room, and m.room.rule.server state event. +// https://spec.matrix.org/v1.2/client-server-api/#moderation-policy-lists +type ModPolicyContent struct { + Entity string `json:"entity"` + Reason string `json:"reason"` + Recommendation string `json:"recommendation"` +} + +type InsertionMarkerContent struct { + InsertionID id.EventID `json:"org.matrix.msc2716.marker.insertion"` + Timestamp int64 `json:"com.beeper.timestamp,omitempty"` +} diff --git a/vendor/maunium.net/go/mautrix/event/type.go b/vendor/maunium.net/go/mautrix/event/type.go new file mode 100644 index 00000000..050b17e9 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/event/type.go @@ -0,0 +1,256 @@ +// Copyright (c) 2021 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package event + +import ( + "encoding/json" + "fmt" + "strings" +) + +type RoomType string + +const ( + RoomTypeDefault RoomType = "" + RoomTypeSpace RoomType = "m.space" +) + +type TypeClass int + +func (tc TypeClass) Name() string { + switch tc { + case MessageEventType: + return "message" + case StateEventType: + return "state" + case EphemeralEventType: + return "ephemeral" + case AccountDataEventType: + return "account data" + case ToDeviceEventType: + return "to-device" + default: + return "unknown" + } +} + +const ( + // Unknown events + UnknownEventType TypeClass = iota + // Normal message events + MessageEventType + // State events + StateEventType + // Ephemeral events + EphemeralEventType + // Account data events + AccountDataEventType + // Device-to-device events + ToDeviceEventType +) + +type Type struct { + Type string + Class TypeClass +} + +func NewEventType(name string) Type { + evtType := Type{Type: name} + evtType.Class = evtType.GuessClass() + return evtType +} + +func (et *Type) IsState() bool { + return et.Class == StateEventType +} + +func (et *Type) IsEphemeral() bool { + return et.Class == EphemeralEventType +} + +func (et *Type) IsAccountData() bool { + return et.Class == AccountDataEventType +} + +func (et *Type) IsToDevice() bool { + return et.Class == ToDeviceEventType +} + +func (et *Type) IsInRoomVerification() bool { + switch et.Type { + case InRoomVerificationStart.Type, InRoomVerificationReady.Type, InRoomVerificationAccept.Type, + InRoomVerificationKey.Type, InRoomVerificationMAC.Type, InRoomVerificationCancel.Type: + return true + default: + return false + } +} + +func (et *Type) IsCall() bool { + switch et.Type { + case CallInvite.Type, CallCandidates.Type, CallAnswer.Type, CallReject.Type, CallSelectAnswer.Type, + CallNegotiate.Type, CallHangup.Type: + return true + default: + return false + } +} + +func (et *Type) IsCustom() bool { + return !strings.HasPrefix(et.Type, "m.") +} + +func (et *Type) GuessClass() TypeClass { + switch et.Type { + case StateAliases.Type, StateCanonicalAlias.Type, StateCreate.Type, StateJoinRules.Type, StateMember.Type, + StatePowerLevels.Type, StateRoomName.Type, StateRoomAvatar.Type, StateServerACL.Type, StateTopic.Type, + StatePinnedEvents.Type, StateTombstone.Type, StateEncryption.Type, StateBridge.Type, StateHalfShotBridge.Type, + StateSpaceParent.Type, StateSpaceChild.Type, StatePolicyRoom.Type, StatePolicyServer.Type, StatePolicyUser.Type, + StateInsertionMarker.Type: + return StateEventType + case EphemeralEventReceipt.Type, EphemeralEventTyping.Type, EphemeralEventPresence.Type: + return EphemeralEventType + case AccountDataDirectChats.Type, AccountDataPushRules.Type, AccountDataRoomTags.Type, + AccountDataSecretStorageKey.Type, AccountDataSecretStorageDefaultKey.Type, + AccountDataCrossSigningMaster.Type, AccountDataCrossSigningSelf.Type, AccountDataCrossSigningUser.Type: + return AccountDataEventType + case EventRedaction.Type, EventMessage.Type, EventEncrypted.Type, EventReaction.Type, EventSticker.Type, + InRoomVerificationStart.Type, InRoomVerificationReady.Type, InRoomVerificationAccept.Type, + InRoomVerificationKey.Type, InRoomVerificationMAC.Type, InRoomVerificationCancel.Type, + CallInvite.Type, CallCandidates.Type, CallAnswer.Type, CallReject.Type, CallSelectAnswer.Type, + CallNegotiate.Type, CallHangup.Type, BeeperMessageStatus.Type: + return MessageEventType + case ToDeviceRoomKey.Type, ToDeviceRoomKeyRequest.Type, ToDeviceForwardedRoomKey.Type, ToDeviceRoomKeyWithheld.Type: + return ToDeviceEventType + default: + return UnknownEventType + } +} + +func (et *Type) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, &et.Type) + if err != nil { + return err + } + et.Class = et.GuessClass() + return nil +} + +func (et *Type) MarshalJSON() ([]byte, error) { + return json.Marshal(&et.Type) +} + +func (et Type) UnmarshalText(data []byte) error { + et.Type = string(data) + et.Class = et.GuessClass() + return nil +} + +func (et Type) MarshalText() ([]byte, error) { + return []byte(et.Type), nil +} + +func (et *Type) String() string { + return et.Type +} + +func (et *Type) Repr() string { + return fmt.Sprintf("%s (%s)", et.Type, et.Class.Name()) +} + +// State events +var ( + StateAliases = Type{"m.room.aliases", StateEventType} + StateCanonicalAlias = Type{"m.room.canonical_alias", StateEventType} + StateCreate = Type{"m.room.create", StateEventType} + StateJoinRules = Type{"m.room.join_rules", StateEventType} + StateHistoryVisibility = Type{"m.room.history_visibility", StateEventType} + StateGuestAccess = Type{"m.room.guest_access", StateEventType} + StateMember = Type{"m.room.member", StateEventType} + StatePowerLevels = Type{"m.room.power_levels", StateEventType} + StateRoomName = Type{"m.room.name", StateEventType} + StateTopic = Type{"m.room.topic", StateEventType} + StateRoomAvatar = Type{"m.room.avatar", StateEventType} + StatePinnedEvents = Type{"m.room.pinned_events", StateEventType} + StateServerACL = Type{"m.room.server_acl", StateEventType} + StateTombstone = Type{"m.room.tombstone", StateEventType} + StatePolicyRoom = Type{"m.policy.rule.room", StateEventType} + StatePolicyServer = Type{"m.policy.rule.server", StateEventType} + StatePolicyUser = Type{"m.policy.rule.user", StateEventType} + StateEncryption = Type{"m.room.encryption", StateEventType} + StateBridge = Type{"m.bridge", StateEventType} + StateHalfShotBridge = Type{"uk.half-shot.bridge", StateEventType} + StateSpaceChild = Type{"m.space.child", StateEventType} + StateSpaceParent = Type{"m.space.parent", StateEventType} + StateInsertionMarker = Type{"org.matrix.msc2716.marker", StateEventType} +) + +// Message events +var ( + EventRedaction = Type{"m.room.redaction", MessageEventType} + EventMessage = Type{"m.room.message", MessageEventType} + EventEncrypted = Type{"m.room.encrypted", MessageEventType} + EventReaction = Type{"m.reaction", MessageEventType} + EventSticker = Type{"m.sticker", MessageEventType} + + InRoomVerificationStart = Type{"m.key.verification.start", MessageEventType} + InRoomVerificationReady = Type{"m.key.verification.ready", MessageEventType} + InRoomVerificationAccept = Type{"m.key.verification.accept", MessageEventType} + InRoomVerificationKey = Type{"m.key.verification.key", MessageEventType} + InRoomVerificationMAC = Type{"m.key.verification.mac", MessageEventType} + InRoomVerificationCancel = Type{"m.key.verification.cancel", MessageEventType} + + CallInvite = Type{"m.call.invite", MessageEventType} + CallCandidates = Type{"m.call.candidates", MessageEventType} + CallAnswer = Type{"m.call.answer", MessageEventType} + CallReject = Type{"m.call.reject", MessageEventType} + CallSelectAnswer = Type{"m.call.select_answer", MessageEventType} + CallNegotiate = Type{"m.call.negotiate", MessageEventType} + CallHangup = Type{"m.call.hangup", MessageEventType} + + BeeperMessageStatus = Type{"com.beeper.message_send_status", MessageEventType} +) + +// Ephemeral events +var ( + EphemeralEventReceipt = Type{"m.receipt", EphemeralEventType} + EphemeralEventTyping = Type{"m.typing", EphemeralEventType} + EphemeralEventPresence = Type{"m.presence", EphemeralEventType} +) + +// Account data events +var ( + AccountDataDirectChats = Type{"m.direct", AccountDataEventType} + AccountDataPushRules = Type{"m.push_rules", AccountDataEventType} + AccountDataRoomTags = Type{"m.tag", AccountDataEventType} + AccountDataFullyRead = Type{"m.fully_read", AccountDataEventType} + AccountDataIgnoredUserList = Type{"m.ignored_user_list", AccountDataEventType} + + AccountDataSecretStorageDefaultKey = Type{"m.secret_storage.default_key", AccountDataEventType} + AccountDataSecretStorageKey = Type{"m.secret_storage.key", AccountDataEventType} + AccountDataCrossSigningMaster = Type{"m.cross_signing.master", AccountDataEventType} + AccountDataCrossSigningUser = Type{"m.cross_signing.user_signing", AccountDataEventType} + AccountDataCrossSigningSelf = Type{"m.cross_signing.self_signing", AccountDataEventType} +) + +// Device-to-device events +var ( + ToDeviceRoomKey = Type{"m.room_key", ToDeviceEventType} + ToDeviceRoomKeyRequest = Type{"m.room_key_request", ToDeviceEventType} + ToDeviceForwardedRoomKey = Type{"m.forwarded_room_key", ToDeviceEventType} + ToDeviceEncrypted = Type{"m.room.encrypted", ToDeviceEventType} + ToDeviceRoomKeyWithheld = Type{"m.room_key.withheld", ToDeviceEventType} + ToDeviceDummy = Type{"m.dummy", ToDeviceEventType} + ToDeviceVerificationRequest = Type{"m.key.verification.request", ToDeviceEventType} + ToDeviceVerificationStart = Type{"m.key.verification.start", ToDeviceEventType} + ToDeviceVerificationAccept = Type{"m.key.verification.accept", ToDeviceEventType} + ToDeviceVerificationKey = Type{"m.key.verification.key", ToDeviceEventType} + ToDeviceVerificationMAC = Type{"m.key.verification.mac", ToDeviceEventType} + ToDeviceVerificationCancel = Type{"m.key.verification.cancel", ToDeviceEventType} + + ToDeviceOrgMatrixRoomKeyWithheld = Type{"org.matrix.room_key.withheld", ToDeviceEventType} +) diff --git a/vendor/maunium.net/go/mautrix/event/verification.go b/vendor/maunium.net/go/mautrix/event/verification.go new file mode 100644 index 00000000..8410904d --- /dev/null +++ b/vendor/maunium.net/go/mautrix/event/verification.go @@ -0,0 +1,307 @@ +// Copyright (c) 2020 Nikos Filippakis +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package event + +import ( + "maunium.net/go/mautrix/id" +) + +type VerificationMethod string + +const VerificationMethodSAS VerificationMethod = "m.sas.v1" + +// VerificationRequestEventContent represents the content of a m.key.verification.request to_device event. +// https://spec.matrix.org/v1.2/client-server-api/#mkeyverificationrequest +type VerificationRequestEventContent struct { + // The device ID which is initiating the request. + FromDevice id.DeviceID `json:"from_device"` + // An opaque identifier for the verification request. Must be unique with respect to the devices involved. + TransactionID string `json:"transaction_id,omitempty"` + // The verification methods supported by the sender. + Methods []VerificationMethod `json:"methods"` + // The POSIX timestamp in milliseconds for when the request was made. + Timestamp int64 `json:"timestamp,omitempty"` + // The user that the event is sent to for in-room verification. + To id.UserID `json:"to,omitempty"` + // Original event ID for in-room verification. + RelatesTo *RelatesTo `json:"m.relates_to,omitempty"` +} + +func (vrec *VerificationRequestEventContent) SupportsVerificationMethod(meth VerificationMethod) bool { + for _, supportedMeth := range vrec.Methods { + if supportedMeth == meth { + return true + } + } + return false +} + +type KeyAgreementProtocol string + +const ( + KeyAgreementCurve25519 KeyAgreementProtocol = "curve25519" + KeyAgreementCurve25519HKDFSHA256 KeyAgreementProtocol = "curve25519-hkdf-sha256" +) + +type VerificationHashMethod string + +const VerificationHashSHA256 VerificationHashMethod = "sha256" + +type MACMethod string + +const HKDFHMACSHA256 MACMethod = "hkdf-hmac-sha256" + +type SASMethod string + +const ( + SASDecimal SASMethod = "decimal" + SASEmoji SASMethod = "emoji" +) + +// VerificationStartEventContent represents the content of a m.key.verification.start to_device event. +// https://spec.matrix.org/v1.2/client-server-api/#mkeyverificationstartmsasv1 +type VerificationStartEventContent struct { + // The device ID which is initiating the process. + FromDevice id.DeviceID `json:"from_device"` + // An opaque identifier for the verification process. Must be unique with respect to the devices involved. + TransactionID string `json:"transaction_id,omitempty"` + // The verification method to use. + Method VerificationMethod `json:"method"` + // The key agreement protocols the sending device understands. + KeyAgreementProtocols []KeyAgreementProtocol `json:"key_agreement_protocols"` + // The hash methods the sending device understands. + Hashes []VerificationHashMethod `json:"hashes"` + // The message authentication codes that the sending device understands. + MessageAuthenticationCodes []MACMethod `json:"message_authentication_codes"` + // The SAS methods the sending device (and the sending device's user) understands. + ShortAuthenticationString []SASMethod `json:"short_authentication_string"` + // The user that the event is sent to for in-room verification. + To id.UserID `json:"to,omitempty"` + // Original event ID for in-room verification. + RelatesTo *RelatesTo `json:"m.relates_to,omitempty"` +} + +func (vsec *VerificationStartEventContent) SupportsKeyAgreementProtocol(proto KeyAgreementProtocol) bool { + for _, supportedProto := range vsec.KeyAgreementProtocols { + if supportedProto == proto { + return true + } + } + return false +} + +func (vsec *VerificationStartEventContent) SupportsHashMethod(alg VerificationHashMethod) bool { + for _, supportedAlg := range vsec.Hashes { + if supportedAlg == alg { + return true + } + } + return false +} + +func (vsec *VerificationStartEventContent) SupportsMACMethod(meth MACMethod) bool { + for _, supportedMeth := range vsec.MessageAuthenticationCodes { + if supportedMeth == meth { + return true + } + } + return false +} + +func (vsec *VerificationStartEventContent) SupportsSASMethod(meth SASMethod) bool { + for _, supportedMeth := range vsec.ShortAuthenticationString { + if supportedMeth == meth { + return true + } + } + return false +} + +func (vsec *VerificationStartEventContent) GetRelatesTo() *RelatesTo { + if vsec.RelatesTo == nil { + vsec.RelatesTo = &RelatesTo{} + } + return vsec.RelatesTo +} + +func (vsec *VerificationStartEventContent) OptionalGetRelatesTo() *RelatesTo { + return vsec.RelatesTo +} + +func (vsec *VerificationStartEventContent) SetRelatesTo(rel *RelatesTo) { + vsec.RelatesTo = rel +} + +// VerificationReadyEventContent represents the content of a m.key.verification.ready event. +// https://spec.matrix.org/v1.2/client-server-api/#mkeyverificationready +type VerificationReadyEventContent struct { + // The device ID which accepted the process. + FromDevice id.DeviceID `json:"from_device"` + // The verification methods supported by the sender. + Methods []VerificationMethod `json:"methods"` + // Original event ID for in-room verification. + RelatesTo *RelatesTo `json:"m.relates_to,omitempty"` +} + +var _ Relatable = (*VerificationReadyEventContent)(nil) + +func (vrec *VerificationReadyEventContent) GetRelatesTo() *RelatesTo { + if vrec.RelatesTo == nil { + vrec.RelatesTo = &RelatesTo{} + } + return vrec.RelatesTo +} + +func (vrec *VerificationReadyEventContent) OptionalGetRelatesTo() *RelatesTo { + return vrec.RelatesTo +} + +func (vrec *VerificationReadyEventContent) SetRelatesTo(rel *RelatesTo) { + vrec.RelatesTo = rel +} + +// VerificationAcceptEventContent represents the content of a m.key.verification.accept to_device event. +// https://spec.matrix.org/v1.2/client-server-api/#mkeyverificationaccept +type VerificationAcceptEventContent struct { + // An opaque identifier for the verification process. Must be the same as the one used for the m.key.verification.start message. + TransactionID string `json:"transaction_id,omitempty"` + // The verification method to use. + Method VerificationMethod `json:"method"` + // The key agreement protocol the device is choosing to use, out of the options in the m.key.verification.start message. + KeyAgreementProtocol KeyAgreementProtocol `json:"key_agreement_protocol"` + // The hash method the device is choosing to use, out of the options in the m.key.verification.start message. + Hash VerificationHashMethod `json:"hash"` + // The message authentication code the device is choosing to use, out of the options in the m.key.verification.start message. + MessageAuthenticationCode MACMethod `json:"message_authentication_code"` + // The SAS methods both devices involved in the verification process understand. Must be a subset of the options in the m.key.verification.start message. + ShortAuthenticationString []SASMethod `json:"short_authentication_string"` + // The hash (encoded as unpadded base64) of the concatenation of the device's ephemeral public key (encoded as unpadded base64) and the canonical JSON representation of the m.key.verification.start message. + Commitment string `json:"commitment"` + // The user that the event is sent to for in-room verification. + To id.UserID `json:"to,omitempty"` + // Original event ID for in-room verification. + RelatesTo *RelatesTo `json:"m.relates_to,omitempty"` +} + +func (vaec *VerificationAcceptEventContent) GetRelatesTo() *RelatesTo { + if vaec.RelatesTo == nil { + vaec.RelatesTo = &RelatesTo{} + } + return vaec.RelatesTo +} + +func (vaec *VerificationAcceptEventContent) OptionalGetRelatesTo() *RelatesTo { + return vaec.RelatesTo +} + +func (vaec *VerificationAcceptEventContent) SetRelatesTo(rel *RelatesTo) { + vaec.RelatesTo = rel +} + +// VerificationKeyEventContent represents the content of a m.key.verification.key to_device event. +// https://spec.matrix.org/v1.2/client-server-api/#mkeyverificationkey +type VerificationKeyEventContent struct { + // An opaque identifier for the verification process. Must be the same as the one used for the m.key.verification.start message. + TransactionID string `json:"transaction_id,omitempty"` + // The device's ephemeral public key, encoded as unpadded base64. + Key string `json:"key"` + // The user that the event is sent to for in-room verification. + To id.UserID `json:"to,omitempty"` + // Original event ID for in-room verification. + RelatesTo *RelatesTo `json:"m.relates_to,omitempty"` +} + +func (vkec *VerificationKeyEventContent) GetRelatesTo() *RelatesTo { + if vkec.RelatesTo == nil { + vkec.RelatesTo = &RelatesTo{} + } + return vkec.RelatesTo +} + +func (vkec *VerificationKeyEventContent) OptionalGetRelatesTo() *RelatesTo { + return vkec.RelatesTo +} + +func (vkec *VerificationKeyEventContent) SetRelatesTo(rel *RelatesTo) { + vkec.RelatesTo = rel +} + +// VerificationMacEventContent represents the content of a m.key.verification.mac to_device event. +// https://spec.matrix.org/v1.2/client-server-api/#mkeyverificationmac +type VerificationMacEventContent struct { + // An opaque identifier for the verification process. Must be the same as the one used for the m.key.verification.start message. + TransactionID string `json:"transaction_id,omitempty"` + // A map of the key ID to the MAC of the key, using the algorithm in the verification process. The MAC is encoded as unpadded base64. + Mac map[id.KeyID]string `json:"mac"` + // The MAC of the comma-separated, sorted, list of key IDs given in the mac property, encoded as unpadded base64. + Keys string `json:"keys"` + // The user that the event is sent to for in-room verification. + To id.UserID `json:"to,omitempty"` + // Original event ID for in-room verification. + RelatesTo *RelatesTo `json:"m.relates_to,omitempty"` +} + +func (vmec *VerificationMacEventContent) GetRelatesTo() *RelatesTo { + if vmec.RelatesTo == nil { + vmec.RelatesTo = &RelatesTo{} + } + return vmec.RelatesTo +} + +func (vmec *VerificationMacEventContent) OptionalGetRelatesTo() *RelatesTo { + return vmec.RelatesTo +} + +func (vmec *VerificationMacEventContent) SetRelatesTo(rel *RelatesTo) { + vmec.RelatesTo = rel +} + +type VerificationCancelCode string + +const ( + VerificationCancelByUser VerificationCancelCode = "m.user" + VerificationCancelByTimeout VerificationCancelCode = "m.timeout" + VerificationCancelUnknownTransaction VerificationCancelCode = "m.unknown_transaction" + VerificationCancelUnknownMethod VerificationCancelCode = "m.unknown_method" + VerificationCancelUnexpectedMessage VerificationCancelCode = "m.unexpected_message" + VerificationCancelKeyMismatch VerificationCancelCode = "m.key_mismatch" + VerificationCancelUserMismatch VerificationCancelCode = "m.user_mismatch" + VerificationCancelInvalidMessage VerificationCancelCode = "m.invalid_message" + VerificationCancelAccepted VerificationCancelCode = "m.accepted" + VerificationCancelSASMismatch VerificationCancelCode = "m.mismatched_sas" + VerificationCancelCommitmentMismatch VerificationCancelCode = "m.mismatched_commitment" +) + +// VerificationCancelEventContent represents the content of a m.key.verification.cancel to_device event. +// https://spec.matrix.org/v1.2/client-server-api/#mkeyverificationcancel +type VerificationCancelEventContent struct { + // The opaque identifier for the verification process/request. + TransactionID string `json:"transaction_id,omitempty"` + // A human readable description of the code. The client should only rely on this string if it does not understand the code. + Reason string `json:"reason"` + // The error code for why the process/request was cancelled by the user. + Code VerificationCancelCode `json:"code"` + // The user that the event is sent to for in-room verification. + To id.UserID `json:"to,omitempty"` + // Original event ID for in-room verification. + RelatesTo *RelatesTo `json:"m.relates_to,omitempty"` +} + +func (vcec *VerificationCancelEventContent) GetRelatesTo() *RelatesTo { + if vcec.RelatesTo == nil { + vcec.RelatesTo = &RelatesTo{} + } + return vcec.RelatesTo +} + +func (vcec *VerificationCancelEventContent) OptionalGetRelatesTo() *RelatesTo { + return vcec.RelatesTo +} + +func (vcec *VerificationCancelEventContent) SetRelatesTo(rel *RelatesTo) { + vcec.RelatesTo = rel +} diff --git a/vendor/maunium.net/go/mautrix/event/voip.go b/vendor/maunium.net/go/mautrix/event/voip.go new file mode 100644 index 00000000..28f56c95 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/event/voip.go @@ -0,0 +1,116 @@ +// Copyright (c) 2021 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package event + +import ( + "encoding/json" + "fmt" + "strconv" +) + +type CallHangupReason string + +const ( + CallHangupICEFailed CallHangupReason = "ice_failed" + CallHangupInviteTimeout CallHangupReason = "invite_timeout" + CallHangupUserHangup CallHangupReason = "user_hangup" + CallHangupUserMediaFailed CallHangupReason = "user_media_failed" + CallHangupUnknownError CallHangupReason = "unknown_error" +) + +type CallDataType string + +const ( + CallDataTypeOffer CallDataType = "offer" + CallDataTypeAnswer CallDataType = "answer" +) + +type CallData struct { + SDP string `json:"sdp"` + Type CallDataType `json:"type"` +} + +type CallCandidate struct { + Candidate string `json:"candidate"` + SDPMLineIndex int `json:"sdpMLineIndex"` + SDPMID string `json:"sdpMid"` +} + +type CallVersion string + +func (cv *CallVersion) UnmarshalJSON(raw []byte) error { + var numberVersion int + err := json.Unmarshal(raw, &numberVersion) + if err != nil { + var stringVersion string + err = json.Unmarshal(raw, &stringVersion) + if err != nil { + return fmt.Errorf("failed to parse CallVersion: %w", err) + } + *cv = CallVersion(stringVersion) + } else { + *cv = CallVersion(strconv.Itoa(numberVersion)) + } + return nil +} + +func (cv *CallVersion) MarshalJSON() ([]byte, error) { + for _, char := range *cv { + if char < '0' || char > '9' { + // The version contains weird characters, return as string. + return json.Marshal(string(*cv)) + } + } + // The version consists of only ASCII digits, return as an integer. + return []byte(*cv), nil +} + +func (cv *CallVersion) Int() (int, error) { + return strconv.Atoi(string(*cv)) +} + +type BaseCallEventContent struct { + CallID string `json:"call_id"` + PartyID string `json:"party_id"` + Version CallVersion `json:"version"` +} + +type CallInviteEventContent struct { + BaseCallEventContent + Lifetime int `json:"lifetime"` + Offer CallData `json:"offer"` +} + +type CallCandidatesEventContent struct { + BaseCallEventContent + Candidates []CallCandidate `json:"candidates"` +} + +type CallRejectEventContent struct { + BaseCallEventContent +} + +type CallAnswerEventContent struct { + BaseCallEventContent + Answer CallData `json:"answer"` +} + +type CallSelectAnswerEventContent struct { + BaseCallEventContent + SelectedPartyID string `json:"selected_party_id"` +} + +type CallNegotiateEventContent struct { + BaseCallEventContent + Lifetime int `json:"lifetime"` + Description CallData `json:"description"` +} + +type CallHangupEventContent struct { + BaseCallEventContent + Reason CallHangupReason `json:"reason"` +} diff --git a/vendor/maunium.net/go/mautrix/filter.go b/vendor/maunium.net/go/mautrix/filter.go new file mode 100644 index 00000000..fd6de7a0 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/filter.go @@ -0,0 +1,93 @@ +// Copyright 2017 Jan Christian Grünhage + +package mautrix + +import ( + "errors" + + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" +) + +type EventFormat string + +const ( + EventFormatClient EventFormat = "client" + EventFormatFederation EventFormat = "federation" +) + +// Filter is used by clients to specify how the server should filter responses to e.g. sync requests +// Specified by: https://spec.matrix.org/v1.2/client-server-api/#filtering +type Filter struct { + AccountData FilterPart `json:"account_data,omitempty"` + EventFields []string `json:"event_fields,omitempty"` + EventFormat EventFormat `json:"event_format,omitempty"` + Presence FilterPart `json:"presence,omitempty"` + Room RoomFilter `json:"room,omitempty"` +} + +// RoomFilter is used to define filtering rules for room events +type RoomFilter struct { + AccountData FilterPart `json:"account_data,omitempty"` + Ephemeral FilterPart `json:"ephemeral,omitempty"` + IncludeLeave bool `json:"include_leave,omitempty"` + NotRooms []id.RoomID `json:"not_rooms,omitempty"` + Rooms []id.RoomID `json:"rooms,omitempty"` + State FilterPart `json:"state,omitempty"` + Timeline FilterPart `json:"timeline,omitempty"` +} + +// FilterPart is used to define filtering rules for specific categories of events +type FilterPart struct { + NotRooms []id.RoomID `json:"not_rooms,omitempty"` + Rooms []id.RoomID `json:"rooms,omitempty"` + Limit int `json:"limit,omitempty"` + NotSenders []id.UserID `json:"not_senders,omitempty"` + NotTypes []event.Type `json:"not_types,omitempty"` + Senders []id.UserID `json:"senders,omitempty"` + Types []event.Type `json:"types,omitempty"` + ContainsURL *bool `json:"contains_url,omitempty"` + + LazyLoadMembers bool `json:"lazy_load_members,omitempty"` + IncludeRedundantMembers bool `json:"include_redundant_members,omitempty"` +} + +// Validate checks if the filter contains valid property values +func (filter *Filter) Validate() error { + if filter.EventFormat != EventFormatClient && filter.EventFormat != EventFormatFederation { + 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/maunium.net/go/mautrix/id/contenturi.go b/vendor/maunium.net/go/mautrix/id/contenturi.go new file mode 100644 index 00000000..cfd00c3e --- /dev/null +++ b/vendor/maunium.net/go/mautrix/id/contenturi.go @@ -0,0 +1,158 @@ +// Copyright (c) 2020 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package id + +import ( + "bytes" + "database/sql/driver" + "encoding/json" + "errors" + "fmt" + "strings" +) + +var ( + InvalidContentURI = errors.New("invalid Matrix content URI") + InputNotJSONString = errors.New("input doesn't look like a JSON string") +) + +// ContentURIString is a string that's expected to be a Matrix content URI. +// It's useful for delaying the parsing of the content URI to move errors from the event content +// JSON parsing step to a later step where more appropriate errors can be produced. +type ContentURIString string + +func (uriString ContentURIString) Parse() (ContentURI, error) { + return ParseContentURI(string(uriString)) +} + +func (uriString ContentURIString) ParseOrIgnore() ContentURI { + parsed, _ := ParseContentURI(string(uriString)) + return parsed +} + +// ContentURI represents a Matrix content URI. +// https://spec.matrix.org/v1.2/client-server-api/#matrix-content-mxc-uris +type ContentURI struct { + Homeserver string + FileID string +} + +func MustParseContentURI(uri string) ContentURI { + parsed, err := ParseContentURI(uri) + if err != nil { + panic(err) + } + return parsed +} + +// ParseContentURI parses a Matrix content URI. +func ParseContentURI(uri string) (parsed ContentURI, err error) { + if len(uri) == 0 { + return + } else if !strings.HasPrefix(uri, "mxc://") { + err = InvalidContentURI + } else if index := strings.IndexRune(uri[6:], '/'); index == -1 || index == len(uri)-7 { + err = InvalidContentURI + } else { + parsed.Homeserver = uri[6 : 6+index] + parsed.FileID = uri[6+index+1:] + } + return +} + +var mxcBytes = []byte("mxc://") + +func ParseContentURIBytes(uri []byte) (parsed ContentURI, err error) { + if len(uri) == 0 { + return + } else if !bytes.HasPrefix(uri, mxcBytes) { + err = InvalidContentURI + } else if index := bytes.IndexRune(uri[6:], '/'); index == -1 || index == len(uri)-7 { + err = InvalidContentURI + } else { + parsed.Homeserver = string(uri[6 : 6+index]) + parsed.FileID = string(uri[6+index+1:]) + } + return +} + +func (uri *ContentURI) UnmarshalJSON(raw []byte) (err error) { + if string(raw) == "null" { + *uri = ContentURI{} + return nil + } else if len(raw) < 2 || raw[0] != '"' || raw[len(raw)-1] != '"' { + return InputNotJSONString + } + parsed, err := ParseContentURIBytes(raw[1 : len(raw)-1]) + if err != nil { + return err + } + *uri = parsed + return nil +} + +func (uri *ContentURI) MarshalJSON() ([]byte, error) { + if uri == nil || uri.IsEmpty() { + return []byte("null"), nil + } + return json.Marshal(uri.String()) +} + +func (uri *ContentURI) UnmarshalText(raw []byte) (err error) { + parsed, err := ParseContentURIBytes(raw) + if err != nil { + return err + } + *uri = parsed + return nil +} + +func (uri ContentURI) MarshalText() ([]byte, error) { + return []byte(uri.String()), nil +} + +func (uri *ContentURI) Scan(i interface{}) error { + var parsed ContentURI + var err error + switch value := i.(type) { + case nil: + // don't do anything, set uri to empty + case []byte: + parsed, err = ParseContentURIBytes(value) + case string: + parsed, err = ParseContentURI(value) + default: + return fmt.Errorf("invalid type %T for ContentURI.Scan", i) + } + if err != nil { + return err + } + *uri = parsed + return nil +} + +func (uri *ContentURI) Value() (driver.Value, error) { + if uri == nil { + return nil, nil + } + return uri.String(), nil +} + +func (uri ContentURI) String() string { + if uri.IsEmpty() { + return "" + } + return fmt.Sprintf("mxc://%s/%s", uri.Homeserver, uri.FileID) +} + +func (uri ContentURI) CUString() ContentURIString { + return ContentURIString(uri.String()) +} + +func (uri ContentURI) IsEmpty() bool { + return len(uri.Homeserver) == 0 || len(uri.FileID) == 0 +} diff --git a/vendor/maunium.net/go/mautrix/id/crypto.go b/vendor/maunium.net/go/mautrix/id/crypto.go new file mode 100644 index 00000000..84fcd67f --- /dev/null +++ b/vendor/maunium.net/go/mautrix/id/crypto.go @@ -0,0 +1,149 @@ +// Copyright (c) 2020 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package id + +import ( + "fmt" + "strings" +) + +// OlmMsgType is an Olm message type +type OlmMsgType int + +const ( + OlmMsgTypePreKey OlmMsgType = 0 + OlmMsgTypeMsg OlmMsgType = 1 +) + +// Algorithm is a Matrix message encryption algorithm. +// https://spec.matrix.org/v1.2/client-server-api/#messaging-algorithm-names +type Algorithm string + +const ( + AlgorithmOlmV1 Algorithm = "m.olm.v1.curve25519-aes-sha2" + AlgorithmMegolmV1 Algorithm = "m.megolm.v1.aes-sha2" +) + +type KeyAlgorithm string + +const ( + KeyAlgorithmCurve25519 KeyAlgorithm = "curve25519" + KeyAlgorithmEd25519 KeyAlgorithm = "ed25519" + KeyAlgorithmSignedCurve25519 KeyAlgorithm = "signed_curve25519" +) + +type CrossSigningUsage string + +const ( + XSUsageMaster CrossSigningUsage = "master" + XSUsageSelfSigning CrossSigningUsage = "self_signing" + XSUsageUserSigning CrossSigningUsage = "user_signing" +) + +// A SessionID is an arbitrary string that identifies an Olm or Megolm session. +type SessionID string + +func (sessionID SessionID) String() string { + return string(sessionID) +} + +// Ed25519 is the base64 representation of an Ed25519 public key +type Ed25519 string +type SigningKey = Ed25519 + +func (ed25519 Ed25519) String() string { + return string(ed25519) +} + +func (ed25519 Ed25519) Fingerprint() string { + spacedSigningKey := make([]byte, len(ed25519)+(len(ed25519)-1)/4) + var ptr = 0 + for i, chr := range ed25519 { + spacedSigningKey[ptr] = byte(chr) + ptr++ + if i%4 == 3 { + spacedSigningKey[ptr] = ' ' + ptr++ + } + } + return string(spacedSigningKey) +} + +// Curve25519 is the base64 representation of an Curve25519 public key +type Curve25519 string +type SenderKey = Curve25519 +type IdentityKey = Curve25519 + +func (curve25519 Curve25519) String() string { + return string(curve25519) +} + +// A DeviceID is an arbitrary string that references a specific device. +type DeviceID string + +func (deviceID DeviceID) String() string { + return string(deviceID) +} + +// A DeviceKeyID is a string formatted as <algorithm>:<device_id> that is used as the key in deviceid-key mappings. +type DeviceKeyID string + +func NewDeviceKeyID(algorithm KeyAlgorithm, deviceID DeviceID) DeviceKeyID { + return DeviceKeyID(fmt.Sprintf("%s:%s", algorithm, deviceID)) +} + +func (deviceKeyID DeviceKeyID) String() string { + return string(deviceKeyID) +} + +func (deviceKeyID DeviceKeyID) Parse() (Algorithm, DeviceID) { + index := strings.IndexRune(string(deviceKeyID), ':') + if index < 0 || len(deviceKeyID) <= index+1 { + return "", "" + } + return Algorithm(deviceKeyID[:index]), DeviceID(deviceKeyID[index+1:]) +} + +// A KeyID a string formatted as <keyalgorithm>:<key_id> that is used as the key in one-time-key mappings. +type KeyID string + +func NewKeyID(algorithm KeyAlgorithm, keyID string) KeyID { + return KeyID(fmt.Sprintf("%s:%s", algorithm, keyID)) +} + +func (keyID KeyID) String() string { + return string(keyID) +} + +func (keyID KeyID) Parse() (KeyAlgorithm, string) { + index := strings.IndexRune(string(keyID), ':') + if index < 0 || len(keyID) <= index+1 { + return "", "" + } + return KeyAlgorithm(keyID[:index]), string(keyID[index+1:]) +} + +// Device contains the identity details of a device and some additional info. +type Device struct { + UserID UserID + DeviceID DeviceID + IdentityKey Curve25519 + SigningKey Ed25519 + + Trust TrustState + Deleted bool + Name string +} + +func (device *Device) Fingerprint() string { + return device.SigningKey.Fingerprint() +} + +type CrossSigningKey struct { + Key Ed25519 + First Ed25519 +} diff --git a/vendor/maunium.net/go/mautrix/id/matrixuri.go b/vendor/maunium.net/go/mautrix/id/matrixuri.go new file mode 100644 index 00000000..5ec403e9 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/id/matrixuri.go @@ -0,0 +1,293 @@ +// Copyright (c) 2021 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package id + +import ( + "errors" + "fmt" + "net/url" + "strings" +) + +// Errors that can happen when parsing matrix: URIs +var ( + ErrInvalidScheme = errors.New("matrix URI scheme must be exactly 'matrix'") + ErrInvalidPartCount = errors.New("matrix URIs must have exactly 2 or 4 segments") + ErrInvalidFirstSegment = errors.New("invalid identifier in first segment of matrix URI") + ErrEmptySecondSegment = errors.New("the second segment of the matrix URI must not be empty") + ErrInvalidThirdSegment = errors.New("invalid identifier in third segment of matrix URI") + ErrEmptyFourthSegment = errors.New("the fourth segment of the matrix URI must not be empty when the third segment is present") +) + +// Errors that can happen when parsing matrix.to URLs +var ( + ErrNotMatrixTo = errors.New("that URL is not a matrix.to URL") + ErrInvalidMatrixToPartCount = errors.New("matrix.to URLs must have exactly 1 or 2 segments") + ErrEmptyMatrixToPrimaryIdentifier = errors.New("the primary identifier in the matrix.to URL is empty") + ErrInvalidMatrixToPrimaryIdentifier = errors.New("the primary identifier in the matrix.to URL has an invalid sigil") + ErrInvalidMatrixToSecondaryIdentifier = errors.New("the secondary identifier in the matrix.to URL has an invalid sigil") +) + +var ErrNotMatrixToOrMatrixURI = errors.New("that URL is not a matrix.to URL nor matrix: URI") + +// MatrixURI contains the result of parsing a matrix: URI using ParseMatrixURI +type MatrixURI struct { + Sigil1 rune + Sigil2 rune + MXID1 string + MXID2 string + Via []string + Action string +} + +// SigilToPathSegment contains a mapping from Matrix identifier sigils to matrix: URI path segments. +var SigilToPathSegment = map[rune]string{ + '$': "e", + '#': "r", + '!': "roomid", + '@': "u", +} + +func (uri *MatrixURI) getQuery() url.Values { + q := make(url.Values) + if uri.Via != nil && len(uri.Via) > 0 { + q["via"] = uri.Via + } + if len(uri.Action) > 0 { + q.Set("action", uri.Action) + } + return q +} + +// String converts the parsed matrix: URI back into the string representation. +func (uri *MatrixURI) String() string { + parts := []string{ + SigilToPathSegment[uri.Sigil1], + url.PathEscape(uri.MXID1), + } + if uri.Sigil2 != 0 { + parts = append(parts, SigilToPathSegment[uri.Sigil2], url.PathEscape(uri.MXID2)) + } + return (&url.URL{ + Scheme: "matrix", + Opaque: strings.Join(parts, "/"), + RawQuery: uri.getQuery().Encode(), + }).String() +} + +// MatrixToURL converts to parsed matrix: URI into a matrix.to URL +func (uri *MatrixURI) MatrixToURL() string { + fragment := fmt.Sprintf("#/%s", url.PathEscape(uri.PrimaryIdentifier())) + if uri.Sigil2 != 0 { + fragment = fmt.Sprintf("%s/%s", fragment, url.PathEscape(uri.SecondaryIdentifier())) + } + query := uri.getQuery().Encode() + if len(query) > 0 { + fragment = fmt.Sprintf("%s?%s", fragment, query) + } + // It would be nice to use URL{...}.String() here, but figuring out the Fragment vs RawFragment stuff is a pain + return fmt.Sprintf("https://matrix.to/%s", fragment) +} + +// PrimaryIdentifier returns the first Matrix identifier in the URI. +// Currently room IDs, room aliases and user IDs can be in the primary identifier slot. +func (uri *MatrixURI) PrimaryIdentifier() string { + return fmt.Sprintf("%c%s", uri.Sigil1, uri.MXID1) +} + +// SecondaryIdentifier returns the second Matrix identifier in the URI. +// Currently only event IDs can be in the secondary identifier slot. +func (uri *MatrixURI) SecondaryIdentifier() string { + if uri.Sigil2 == 0 { + return "" + } + return fmt.Sprintf("%c%s", uri.Sigil2, uri.MXID2) +} + +// UserID returns the user ID from the URI if the primary identifier is a user ID. +func (uri *MatrixURI) UserID() UserID { + if uri.Sigil1 == '@' { + return UserID(uri.PrimaryIdentifier()) + } + return "" +} + +// RoomID returns the room ID from the URI if the primary identifier is a room ID. +func (uri *MatrixURI) RoomID() RoomID { + if uri.Sigil1 == '!' { + return RoomID(uri.PrimaryIdentifier()) + } + return "" +} + +// RoomAlias returns the room alias from the URI if the primary identifier is a room alias. +func (uri *MatrixURI) RoomAlias() RoomAlias { + if uri.Sigil1 == '#' { + return RoomAlias(uri.PrimaryIdentifier()) + } + return "" +} + +// EventID returns the event ID from the URI if the primary identifier is a room ID or alias and the secondary identifier is an event ID. +func (uri *MatrixURI) EventID() EventID { + if (uri.Sigil1 == '!' || uri.Sigil1 == '#') && uri.Sigil2 == '$' { + return EventID(uri.SecondaryIdentifier()) + } + return "" +} + +// ParseMatrixURIOrMatrixToURL parses the given matrix.to URL or matrix: URI into a unified representation. +func ParseMatrixURIOrMatrixToURL(uri string) (*MatrixURI, error) { + parsed, err := url.Parse(uri) + if err != nil { + return nil, fmt.Errorf("failed to parse URI: %w", err) + } + if parsed.Scheme == "matrix" { + return ProcessMatrixURI(parsed) + } else if strings.HasSuffix(parsed.Hostname(), "matrix.to") { + return ProcessMatrixToURL(parsed) + } else { + return nil, ErrNotMatrixToOrMatrixURI + } +} + +// ParseMatrixURI implements the matrix: URI parsing algorithm. +// +// Currently specified in https://github.com/matrix-org/matrix-doc/blob/master/proposals/2312-matrix-uri.md#uri-parsing-algorithm +func ParseMatrixURI(uri string) (*MatrixURI, error) { + // Step 1: parse the URI according to RFC 3986 + parsed, err := url.Parse(uri) + if err != nil { + return nil, fmt.Errorf("failed to parse URI: %w", err) + } + return ProcessMatrixURI(parsed) +} + +// ProcessMatrixURI implements steps 2-7 of the matrix: URI parsing algorithm +// (i.e. everything except parsing the URI itself, which is done with url.Parse or ParseMatrixURI) +func ProcessMatrixURI(uri *url.URL) (*MatrixURI, error) { + // Step 2: check that scheme is exactly `matrix` + if uri.Scheme != "matrix" { + return nil, ErrInvalidScheme + } + + // Step 3: split the path into segments separated by / + parts := strings.Split(uri.Opaque, "/") + + // Step 4: Check that the URI contains either 2 or 4 segments + if len(parts) != 2 && len(parts) != 4 { + return nil, ErrInvalidPartCount + } + + var parsed MatrixURI + + // Step 5: Construct the top-level Matrix identifier + // a: find the sigil from the first segment + switch parts[0] { + case "u", "user": + parsed.Sigil1 = '@' + case "r", "room": + parsed.Sigil1 = '#' + case "roomid": + parsed.Sigil1 = '!' + default: + return nil, fmt.Errorf("%w: '%s'", ErrInvalidFirstSegment, parts[0]) + } + // b: find the identifier from the second segment + if len(parts[1]) == 0 { + return nil, ErrEmptySecondSegment + } + parsed.MXID1 = parts[1] + + // Step 6: if the first part is a room and the URI has 4 segments, construct a second level identifier + if (parsed.Sigil1 == '!' || parsed.Sigil1 == '#') && len(parts) == 4 { + // a: find the sigil from the third segment + switch parts[2] { + case "e", "event": + parsed.Sigil2 = '$' + default: + return nil, fmt.Errorf("%w: '%s'", ErrInvalidThirdSegment, parts[0]) + } + + // b: find the identifier from the fourth segment + if len(parts[3]) == 0 { + return nil, ErrEmptyFourthSegment + } + parsed.MXID2 = parts[3] + } + + // Step 7: parse the query and extract via and action items + via, ok := uri.Query()["via"] + if ok && len(via) > 0 { + parsed.Via = via + } + action, ok := uri.Query()["action"] + if ok && len(action) > 0 { + parsed.Action = action[len(action)-1] + } + + return &parsed, nil +} + +// ParseMatrixToURL parses a matrix.to URL into the same container as ParseMatrixURI parses matrix: URIs. +func ParseMatrixToURL(uri string) (*MatrixURI, error) { + parsed, err := url.Parse(uri) + if err != nil { + return nil, fmt.Errorf("failed to parse URL: %w", err) + } + return ProcessMatrixToURL(parsed) +} + +// ProcessMatrixToURL is the equivalent of ProcessMatrixURI for matrix.to URLs. +func ProcessMatrixToURL(uri *url.URL) (*MatrixURI, error) { + if !strings.HasSuffix(uri.Hostname(), "matrix.to") { + return nil, ErrNotMatrixTo + } + + initialSplit := strings.SplitN(uri.Fragment, "?", 2) + parts := strings.Split(initialSplit[0], "/") + if len(initialSplit) > 1 { + uri.RawQuery = initialSplit[1] + } + + if len(parts) < 2 || len(parts) > 3 { + return nil, ErrInvalidMatrixToPartCount + } + + if len(parts[1]) == 0 { + return nil, ErrEmptyMatrixToPrimaryIdentifier + } + + var parsed MatrixURI + + parsed.Sigil1 = rune(parts[1][0]) + parsed.MXID1 = parts[1][1:] + _, isKnown := SigilToPathSegment[parsed.Sigil1] + if !isKnown { + return nil, ErrInvalidMatrixToPrimaryIdentifier + } + + if len(parts) == 3 && len(parts[2]) > 0 { + parsed.Sigil2 = rune(parts[2][0]) + parsed.MXID2 = parts[2][1:] + _, isKnown = SigilToPathSegment[parsed.Sigil2] + if !isKnown { + return nil, ErrInvalidMatrixToSecondaryIdentifier + } + } + + via, ok := uri.Query()["via"] + if ok && len(via) > 0 { + parsed.Via = via + } + action, ok := uri.Query()["action"] + if ok && len(action) > 0 { + parsed.Action = action[len(action)-1] + } + + return &parsed, nil +} diff --git a/vendor/maunium.net/go/mautrix/id/opaque.go b/vendor/maunium.net/go/mautrix/id/opaque.go new file mode 100644 index 00000000..16863b95 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/id/opaque.go @@ -0,0 +1,83 @@ +// Copyright (c) 2020 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package id + +import ( + "fmt" +) + +// A RoomID is a string starting with ! that references a specific room. +// https://matrix.org/docs/spec/appendices#room-ids-and-event-ids +type RoomID string + +// A RoomAlias is a string starting with # that can be resolved into. +// https://matrix.org/docs/spec/appendices#room-aliases +type RoomAlias string + +func NewRoomAlias(localpart, server string) RoomAlias { + return RoomAlias(fmt.Sprintf("#%s:%s", localpart, server)) +} + +// An EventID is a string starting with $ that references a specific event. +// +// https://matrix.org/docs/spec/appendices#room-ids-and-event-ids +// https://matrix.org/docs/spec/rooms/v4#event-ids +type EventID string + +// A BatchID is a string identifying a batch of events being backfilled to a room. +// https://github.com/matrix-org/matrix-doc/pull/2716 +type BatchID string + +func (roomID RoomID) String() string { + return string(roomID) +} + +func (roomID RoomID) URI(via ...string) *MatrixURI { + return &MatrixURI{ + Sigil1: '!', + MXID1: string(roomID)[1:], + Via: via, + } +} + +func (roomID RoomID) EventURI(eventID EventID, via ...string) *MatrixURI { + return &MatrixURI{ + Sigil1: '!', + MXID1: string(roomID)[1:], + Sigil2: '$', + MXID2: string(eventID)[1:], + Via: via, + } +} + +func (roomAlias RoomAlias) String() string { + return string(roomAlias) +} + +func (roomAlias RoomAlias) URI() *MatrixURI { + return &MatrixURI{ + Sigil1: '#', + MXID1: string(roomAlias)[1:], + } +} + +func (roomAlias RoomAlias) EventURI(eventID EventID) *MatrixURI { + return &MatrixURI{ + Sigil1: '#', + MXID1: string(roomAlias)[1:], + Sigil2: '$', + MXID2: string(eventID)[1:], + } +} + +func (eventID EventID) String() string { + return string(eventID) +} + +func (batchID BatchID) String() string { + return string(batchID) +} diff --git a/vendor/maunium.net/go/mautrix/id/trust.go b/vendor/maunium.net/go/mautrix/id/trust.go new file mode 100644 index 00000000..04f6e36b --- /dev/null +++ b/vendor/maunium.net/go/mautrix/id/trust.go @@ -0,0 +1,87 @@ +// Copyright (c) 2022 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package id + +import ( + "fmt" + "strings" +) + +// TrustState determines how trusted a device is. +type TrustState int + +const ( + TrustStateBlacklisted TrustState = -100 + TrustStateUnset TrustState = 0 + TrustStateUnknownDevice TrustState = 10 + TrustStateForwarded TrustState = 20 + TrustStateCrossSignedUntrusted TrustState = 50 + TrustStateCrossSignedTOFU TrustState = 100 + TrustStateCrossSignedVerified TrustState = 200 + TrustStateVerified TrustState = 300 + TrustStateInvalid TrustState = (1 << 31) - 1 +) + +func (ts *TrustState) UnmarshalText(data []byte) error { + strData := string(data) + state := ParseTrustState(strData) + if state == TrustStateInvalid { + return fmt.Errorf("invalid trust state %q", strData) + } + *ts = state + return nil +} + +func (ts *TrustState) MarshalText() ([]byte, error) { + return []byte(ts.String()), nil +} + +func ParseTrustState(val string) TrustState { + switch strings.ToLower(val) { + case "blacklisted": + return TrustStateBlacklisted + case "unverified": + return TrustStateUnset + case "cross-signed-untrusted": + return TrustStateCrossSignedUntrusted + case "unknown-device": + return TrustStateUnknownDevice + case "forwarded": + return TrustStateForwarded + case "cross-signed-tofu", "cross-signed": + return TrustStateCrossSignedTOFU + case "cross-signed-verified", "cross-signed-trusted": + return TrustStateCrossSignedVerified + case "verified": + return TrustStateVerified + default: + return TrustStateInvalid + } +} + +func (ts TrustState) String() string { + switch ts { + case TrustStateBlacklisted: + return "blacklisted" + case TrustStateUnset: + return "unverified" + case TrustStateCrossSignedUntrusted: + return "cross-signed-untrusted" + case TrustStateUnknownDevice: + return "unknown-device" + case TrustStateForwarded: + return "forwarded" + case TrustStateCrossSignedTOFU: + return "cross-signed-tofu" + case TrustStateCrossSignedVerified: + return "cross-signed-verified" + case TrustStateVerified: + return "verified" + default: + return "invalid" + } +} diff --git a/vendor/maunium.net/go/mautrix/id/userid.go b/vendor/maunium.net/go/mautrix/id/userid.go new file mode 100644 index 00000000..0522b54c --- /dev/null +++ b/vendor/maunium.net/go/mautrix/id/userid.go @@ -0,0 +1,224 @@ +// Copyright (c) 2021 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package id + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + "regexp" + "strings" +) + +// UserID represents a Matrix user ID. +// https://matrix.org/docs/spec/appendices#user-identifiers +type UserID string + +const UserIDMaxLength = 255 + +func NewUserID(localpart, homeserver string) UserID { + return UserID(fmt.Sprintf("@%s:%s", localpart, homeserver)) +} + +func NewEncodedUserID(localpart, homeserver string) UserID { + return NewUserID(EncodeUserLocalpart(localpart), homeserver) +} + +var ( + ErrInvalidUserID = errors.New("is not a valid user ID") + ErrNoncompliantLocalpart = errors.New("contains characters that are not allowed") + ErrUserIDTooLong = errors.New("the given user ID is longer than 255 characters") + ErrEmptyLocalpart = errors.New("empty localparts are not allowed") +) + +// Parse parses the user ID into the localpart and server name. +// +// Note that this only enforces very basic user ID formatting requirements: user IDs start with +// a @, and contain a : after the @. If you want to enforce localpart validity, see the +// ParseAndValidate and ValidateUserLocalpart functions. +func (userID UserID) Parse() (localpart, homeserver string, err error) { + if len(userID) == 0 || userID[0] != '@' || !strings.ContainsRune(string(userID), ':') { + // This error wrapping lets you use errors.Is() nicely even though the message contains the user ID + err = fmt.Errorf("'%s' %w", userID, ErrInvalidUserID) + return + } + parts := strings.SplitN(string(userID), ":", 2) + localpart, homeserver = strings.TrimPrefix(parts[0], "@"), parts[1] + return +} + +func (userID UserID) Localpart() string { + localpart, _, _ := userID.Parse() + return localpart +} + +func (userID UserID) Homeserver() string { + _, homeserver, _ := userID.Parse() + return homeserver +} + +// URI returns the user ID as a MatrixURI struct, which can then be stringified into a matrix: URI or a matrix.to URL. +// +// This does not parse or validate the user ID. Use the ParseAndValidate method if you want to ensure the user ID is valid first. +func (userID UserID) URI() *MatrixURI { + return &MatrixURI{ + Sigil1: '@', + MXID1: string(userID)[1:], + } +} + +var ValidLocalpartRegex = regexp.MustCompile("^[0-9a-z-.=_/]+$") + +// ValidateUserLocalpart validates a Matrix user ID localpart using the grammar +// in https://matrix.org/docs/spec/appendices#user-identifier +func ValidateUserLocalpart(localpart string) error { + if len(localpart) == 0 { + return ErrEmptyLocalpart + } else if !ValidLocalpartRegex.MatchString(localpart) { + return fmt.Errorf("'%s' %w", localpart, ErrNoncompliantLocalpart) + } + return nil +} + +// ParseAndValidate parses the user ID into the localpart and server name like Parse, +// and also validates that the localpart is allowed according to the user identifiers spec. +func (userID UserID) ParseAndValidate() (localpart, homeserver string, err error) { + localpart, homeserver, err = userID.Parse() + if err == nil { + err = ValidateUserLocalpart(localpart) + } + if err == nil && len(userID) > UserIDMaxLength { + err = ErrUserIDTooLong + } + return +} + +func (userID UserID) ParseAndDecode() (localpart, homeserver string, err error) { + localpart, homeserver, err = userID.ParseAndValidate() + if err == nil { + localpart, err = DecodeUserLocalpart(localpart) + } + return +} + +func (userID UserID) String() string { + return string(userID) +} + +const lowerhex = "0123456789abcdef" + +// encode the given byte using quoted-printable encoding (e.g "=2f") +// 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 https://spec.matrix.org/v1.2/appendices/#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 https://spec.matrix.org/v1.2/appendices/#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 +} diff --git a/vendor/maunium.net/go/mautrix/pushrules/action.go b/vendor/maunium.net/go/mautrix/pushrules/action.go new file mode 100644 index 00000000..844c3eec --- /dev/null +++ b/vendor/maunium.net/go/mautrix/pushrules/action.go @@ -0,0 +1,124 @@ +// Copyright (c) 2020 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package pushrules + +import "encoding/json" + +// PushActionType is the type of a PushAction +type PushActionType string + +// The allowed push action types as specified in spec section 11.12.1.4.1. +const ( + ActionNotify PushActionType = "notify" + ActionDontNotify PushActionType = "dont_notify" + ActionCoalesce PushActionType = "coalesce" + ActionSetTweak PushActionType = "set_tweak" +) + +// PushActionTweak is the type of the tweak in SetTweak push actions. +type PushActionTweak string + +// The allowed tweak types as specified in spec section 11.12.1.4.1.1. +const ( + TweakSound PushActionTweak = "sound" + TweakHighlight PushActionTweak = "highlight" +) + +// PushActionArray is an array of PushActions. +type PushActionArray []*PushAction + +// PushActionArrayShould contains the important information parsed from a PushActionArray. +type PushActionArrayShould struct { + // Whether or not the array contained a Notify, DontNotify or Coalesce action type. + NotifySpecified bool + // Whether or not the event in question should trigger a notification. + Notify bool + // Whether or not the event in question should be highlighted. + Highlight bool + + // Whether or not the event in question should trigger a sound alert. + PlaySound bool + // The name of the sound to play if PlaySound is true. + SoundName string +} + +// Should parses this push action array and returns the relevant details wrapped in a PushActionArrayShould struct. +func (actions PushActionArray) Should() (should PushActionArrayShould) { + for _, action := range actions { + switch action.Action { + case ActionNotify, ActionCoalesce: + should.Notify = true + should.NotifySpecified = true + case ActionDontNotify: + should.Notify = false + should.NotifySpecified = true + case ActionSetTweak: + switch action.Tweak { + case TweakHighlight: + var ok bool + should.Highlight, ok = action.Value.(bool) + if !ok { + // Highlight value not specified, so assume true since the tweak is set. + should.Highlight = true + } + case TweakSound: + should.SoundName = action.Value.(string) + should.PlaySound = len(should.SoundName) > 0 + } + } + } + return +} + +// PushAction is a single action that should be triggered when receiving a message. +type PushAction struct { + Action PushActionType + Tweak PushActionTweak + Value interface{} +} + +// UnmarshalJSON parses JSON into this PushAction. +// +// - If the JSON is a single string, the value is stored in the Action field. +// - If the JSON is an object with the set_tweak field, Action will be set to +// "set_tweak", Tweak will be set to the value of the set_tweak field and +// and Value will be set to the value of the value field. +// - In any other case, the function does nothing. +func (action *PushAction) UnmarshalJSON(raw []byte) error { + var data interface{} + + err := json.Unmarshal(raw, &data) + if err != nil { + return err + } + + switch val := data.(type) { + case string: + action.Action = PushActionType(val) + case map[string]interface{}: + tweak, ok := val["set_tweak"].(string) + if ok { + action.Action = ActionSetTweak + action.Tweak = PushActionTweak(tweak) + action.Value, _ = val["value"] + } + } + return nil +} + +// MarshalJSON is the reverse of UnmarshalJSON() +func (action *PushAction) MarshalJSON() (raw []byte, err error) { + if action.Action == ActionSetTweak { + data := map[string]interface{}{ + "set_tweak": action.Tweak, + "value": action.Value, + } + return json.Marshal(&data) + } + data := string(action.Action) + return json.Marshal(&data) +} diff --git a/vendor/maunium.net/go/mautrix/pushrules/condition.go b/vendor/maunium.net/go/mautrix/pushrules/condition.go new file mode 100644 index 00000000..f809f8e7 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/pushrules/condition.go @@ -0,0 +1,266 @@ +// Copyright (c) 2022 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package pushrules + +import ( + "encoding/json" + "fmt" + "regexp" + "strconv" + "strings" + "unicode" + + "github.com/tidwall/gjson" + + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" + "maunium.net/go/mautrix/pushrules/glob" +) + +// Room is an interface with the functions that are needed for processing room-specific push conditions +type Room interface { + GetOwnDisplayname() string + GetMemberCount() int +} + +// EventfulRoom is an extension of Room to support MSC3664. +type EventfulRoom interface { + Room + GetEvent(id.EventID) *event.Event +} + +// PushCondKind is the type of a push condition. +type PushCondKind string + +// The allowed push condition kinds as specified in https://spec.matrix.org/v1.2/client-server-api/#conditions-1 +const ( + KindEventMatch PushCondKind = "event_match" + KindContainsDisplayName PushCondKind = "contains_display_name" + KindRoomMemberCount PushCondKind = "room_member_count" + + // MSC3664: https://github.com/matrix-org/matrix-spec-proposals/pull/3664 + + KindRelatedEventMatch PushCondKind = "related_event_match" + KindUnstableRelatedEventMatch PushCondKind = "im.nheko.msc3664.related_event_match" +) + +// PushCondition wraps a condition that is required for a specific PushRule to be used. +type PushCondition struct { + // The type of the condition. + Kind PushCondKind `json:"kind"` + // The dot-separated field of the event to match. Only applicable if kind is EventMatch. + Key string `json:"key,omitempty"` + // The glob-style pattern to match the field against. Only applicable if kind is EventMatch. + Pattern string `json:"pattern,omitempty"` + // The condition that needs to be fulfilled for RoomMemberCount-type conditions. + // A decimal integer optionally prefixed by ==, <, >, >= or <=. Prefix "==" is assumed if no prefix found. + MemberCountCondition string `json:"is,omitempty"` + + // The relation type for related_event_match from MSC3664 + RelType event.RelationType `json:"rel_type,omitempty"` +} + +// MemberCountFilterRegex is the regular expression to parse the MemberCountCondition of PushConditions. +var MemberCountFilterRegex = regexp.MustCompile("^(==|[<>]=?)?([0-9]+)$") + +// Match checks if this condition is fulfilled for the given event in the given room. +func (cond *PushCondition) Match(room Room, evt *event.Event) bool { + switch cond.Kind { + case KindEventMatch: + return cond.matchValue(room, evt) + case KindRelatedEventMatch, KindUnstableRelatedEventMatch: + return cond.matchRelatedEvent(room, evt) + case KindContainsDisplayName: + return cond.matchDisplayName(room, evt) + case KindRoomMemberCount: + return cond.matchMemberCount(room) + default: + return false + } +} + +func splitWithEscaping(s string, separator, escape byte) []string { + var token []byte + var tokens []string + for i := 0; i < len(s); i++ { + if s[i] == separator { + tokens = append(tokens, string(token)) + token = token[:0] + } else if s[i] == escape && i+1 < len(s) { + i++ + token = append(token, s[i]) + } else { + token = append(token, s[i]) + } + } + tokens = append(tokens, string(token)) + return tokens +} + +func hackyNestedGet(data map[string]interface{}, path []string) (interface{}, bool) { + val, ok := data[path[0]] + if len(path) == 1 { + // We don't have any more path parts, return the value regardless of whether it exists or not. + return val, ok + } else if ok { + if mapVal, ok := val.(map[string]interface{}); ok { + val, ok = hackyNestedGet(mapVal, path[1:]) + if ok { + return val, true + } + } + } + // If we don't find the key, try to combine the first two parts. + // e.g. if the key is content.m.relates_to.rel_type, we'll first try data["m"], which will fail, + // then combine m and relates_to to get data["m.relates_to"], which should succeed. + path[1] = path[0] + "." + path[1] + return hackyNestedGet(data, path[1:]) +} + +func stringifyForPushCondition(val interface{}) string { + // Implement MSC3862 to allow matching any type of field + // https://github.com/matrix-org/matrix-spec-proposals/pull/3862 + switch typedVal := val.(type) { + case string: + return typedVal + case nil: + return "null" + case float64: + // Floats aren't allowed in Matrix events, but the JSON parser always stores numbers as floats, + // so just handle that and convert to int + return strconv.FormatInt(int64(typedVal), 10) + default: + return fmt.Sprint(val) + } +} + +func (cond *PushCondition) matchValue(room Room, evt *event.Event) bool { + key, subkey, _ := strings.Cut(cond.Key, ".") + + pattern, err := glob.Compile(cond.Pattern) + if err != nil { + return false + } + + switch key { + case "type": + return pattern.MatchString(evt.Type.String()) + case "sender": + return pattern.MatchString(string(evt.Sender)) + case "room_id": + return pattern.MatchString(string(evt.RoomID)) + case "state_key": + if evt.StateKey == nil { + return false + } + return pattern.MatchString(*evt.StateKey) + case "content": + // Split the match key with escaping to implement https://github.com/matrix-org/matrix-spec-proposals/pull/3873 + splitKey := splitWithEscaping(subkey, '.', '\\') + // Then do a hacky nested get that supports combining parts for the backwards-compat part of MSC3873 + val, ok := hackyNestedGet(evt.Content.Raw, splitKey) + if !ok { + return cond.Pattern == "" + } + return pattern.MatchString(stringifyForPushCondition(val)) + default: + return false + } +} + +func (cond *PushCondition) getRelationEventID(relatesTo *event.RelatesTo) id.EventID { + if relatesTo == nil { + return "" + } + switch cond.RelType { + case "": + return relatesTo.EventID + case "m.in_reply_to": + if relatesTo.IsFallingBack || relatesTo.InReplyTo == nil { + return "" + } + return relatesTo.InReplyTo.EventID + default: + if relatesTo.Type != cond.RelType { + return "" + } + return relatesTo.EventID + } +} + +func (cond *PushCondition) matchRelatedEvent(room Room, evt *event.Event) bool { + var relatesTo *event.RelatesTo + if relatable, ok := evt.Content.Parsed.(event.Relatable); ok { + relatesTo = relatable.OptionalGetRelatesTo() + } else { + res := gjson.GetBytes(evt.Content.VeryRaw, `m\.relates_to`) + if res.Exists() && res.IsObject() { + _ = json.Unmarshal([]byte(res.Raw), &relatesTo) + } + } + if evtID := cond.getRelationEventID(relatesTo); evtID == "" { + return false + } else if eventfulRoom, ok := room.(EventfulRoom); !ok { + return false + } else if evt = eventfulRoom.GetEvent(relatesTo.EventID); evt == nil { + return false + } else { + return cond.matchValue(room, evt) + } +} + +func (cond *PushCondition) matchDisplayName(room Room, evt *event.Event) bool { + displayname := room.GetOwnDisplayname() + if len(displayname) == 0 { + return false + } + + msg, ok := evt.Content.Raw["body"].(string) + if !ok { + return false + } + + isAcceptable := func(r uint8) bool { + return unicode.IsSpace(rune(r)) || unicode.IsPunct(rune(r)) + } + length := len(displayname) + for index := strings.Index(msg, displayname); index != -1; index = strings.Index(msg, displayname) { + if (index <= 0 || isAcceptable(msg[index-1])) && (index+length >= len(msg) || isAcceptable(msg[index+length])) { + return true + } + msg = msg[index+len(displayname):] + } + return false +} + +func (cond *PushCondition) matchMemberCount(room Room) bool { + group := MemberCountFilterRegex.FindStringSubmatch(cond.MemberCountCondition) + if len(group) != 3 { + return false + } + + operator := group[1] + wantedMemberCount, _ := strconv.Atoi(group[2]) + + memberCount := room.GetMemberCount() + + switch operator { + case "==", "": + return memberCount == wantedMemberCount + case ">": + return memberCount > wantedMemberCount + case ">=": + return memberCount >= wantedMemberCount + case "<": + return memberCount < wantedMemberCount + case "<=": + return memberCount <= wantedMemberCount + default: + // Should be impossible due to regex. + return false + } +} diff --git a/vendor/maunium.net/go/mautrix/pushrules/doc.go b/vendor/maunium.net/go/mautrix/pushrules/doc.go new file mode 100644 index 00000000..19cd7745 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/pushrules/doc.go @@ -0,0 +1,2 @@ +// Package pushrules contains utilities to parse push notification rules. +package pushrules diff --git a/vendor/maunium.net/go/mautrix/pushrules/glob/LICENSE b/vendor/maunium.net/go/mautrix/pushrules/glob/LICENSE new file mode 100644 index 00000000..cb00d952 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/pushrules/glob/LICENSE @@ -0,0 +1,22 @@ +Glob is licensed under the MIT "Expat" License: + +Copyright (c) 2016: Zachary Yedidia. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/maunium.net/go/mautrix/pushrules/glob/README.md b/vendor/maunium.net/go/mautrix/pushrules/glob/README.md new file mode 100644 index 00000000..e2e6c649 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/pushrules/glob/README.md @@ -0,0 +1,28 @@ +# String globbing in Go + +[![GoDoc](https://godoc.org/github.com/zyedidia/glob?status.svg)](http://godoc.org/github.com/zyedidia/glob) + +This package adds support for globs in Go. + +It simply converts glob expressions to regexps. I try to follow the standard defined [here](http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_13). + +# Example + +```go +package main + +import "github.com/zyedidia/glob" + +func main() { + glob, err := glob.Compile("{*.go,*.c}") + if err != nil { + // Error + } + + glob.Match([]byte("test.c")) // true + glob.Match([]byte("hello.go")) // true + glob.Match([]byte("test.d")) // false +} +``` + +You can call all the same functions on a glob that you can call on a regexp. diff --git a/vendor/maunium.net/go/mautrix/pushrules/glob/glob.go b/vendor/maunium.net/go/mautrix/pushrules/glob/glob.go new file mode 100644 index 00000000..c270dbc5 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/pushrules/glob/glob.go @@ -0,0 +1,108 @@ +// Package glob provides objects for matching strings with globs +package glob + +import "regexp" + +// Glob is a wrapper of *regexp.Regexp. +// It should contain a glob expression compiled into a regular expression. +type Glob struct { + *regexp.Regexp +} + +// Compile a takes a glob expression as a string and transforms it +// into a *Glob object (which is really just a regular expression) +// Compile also returns a possible error. +func Compile(pattern string) (*Glob, error) { + r, err := globToRegex(pattern) + return &Glob{r}, err +} + +func globToRegex(glob string) (*regexp.Regexp, error) { + regex := "" + inGroup := 0 + inClass := 0 + firstIndexInClass := -1 + arr := []byte(glob) + + hasGlobCharacters := false + + for i := 0; i < len(arr); i++ { + ch := arr[i] + + switch ch { + case '\\': + i++ + if i >= len(arr) { + regex += "\\" + } else { + next := arr[i] + switch next { + case ',': + // Nothing + case 'Q', 'E': + regex += "\\\\" + default: + regex += "\\" + } + regex += string(next) + } + case '*': + if inClass == 0 { + regex += ".*" + } else { + regex += "*" + } + hasGlobCharacters = true + case '?': + if inClass == 0 { + regex += "." + } else { + regex += "?" + } + hasGlobCharacters = true + case '[': + inClass++ + firstIndexInClass = i + 1 + regex += "[" + hasGlobCharacters = true + case ']': + inClass-- + regex += "]" + case '.', '(', ')', '+', '|', '^', '$', '@', '%': + if inClass == 0 || (firstIndexInClass == i && ch == '^') { + regex += "\\" + } + regex += string(ch) + hasGlobCharacters = true + case '!': + if firstIndexInClass == i { + regex += "^" + } else { + regex += "!" + } + hasGlobCharacters = true + case '{': + inGroup++ + regex += "(" + hasGlobCharacters = true + case '}': + inGroup-- + regex += ")" + case ',': + if inGroup > 0 { + regex += "|" + hasGlobCharacters = true + } else { + regex += "," + } + default: + regex += string(ch) + } + } + + if hasGlobCharacters { + return regexp.Compile("^" + regex + "$") + } else { + return regexp.Compile(regex) + } +} diff --git a/vendor/maunium.net/go/mautrix/pushrules/pushrules.go b/vendor/maunium.net/go/mautrix/pushrules/pushrules.go new file mode 100644 index 00000000..7944299a --- /dev/null +++ b/vendor/maunium.net/go/mautrix/pushrules/pushrules.go @@ -0,0 +1,37 @@ +// Copyright (c) 2020 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package pushrules + +import ( + "encoding/gob" + "encoding/json" + "reflect" + + "maunium.net/go/mautrix/event" +) + +// EventContent represents the content of a m.push_rules account data event. +// https://spec.matrix.org/v1.2/client-server-api/#mpush_rules +type EventContent struct { + Ruleset *PushRuleset `json:"global"` +} + +func init() { + event.TypeMap[event.AccountDataPushRules] = reflect.TypeOf(EventContent{}) + gob.Register(&EventContent{}) +} + +// EventToPushRules converts a m.push_rules event to a PushRuleset by passing the data through JSON. +func EventToPushRules(evt *event.Event) (*PushRuleset, error) { + content := &EventContent{} + err := json.Unmarshal(evt.Content.VeryRaw, content) + if err != nil { + return nil, err + } + + return content.Ruleset, nil +} diff --git a/vendor/maunium.net/go/mautrix/pushrules/rule.go b/vendor/maunium.net/go/mautrix/pushrules/rule.go new file mode 100644 index 00000000..8ce2da77 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/pushrules/rule.go @@ -0,0 +1,154 @@ +// Copyright (c) 2020 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package pushrules + +import ( + "encoding/gob" + + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" + "maunium.net/go/mautrix/pushrules/glob" +) + +func init() { + gob.Register(PushRuleArray{}) + gob.Register(PushRuleMap{}) +} + +type PushRuleCollection interface { + GetActions(room Room, evt *event.Event) PushActionArray +} + +type PushRuleArray []*PushRule + +func (rules PushRuleArray) SetType(typ PushRuleType) PushRuleArray { + for _, rule := range rules { + rule.Type = typ + } + return rules +} + +func (rules PushRuleArray) GetActions(room Room, evt *event.Event) PushActionArray { + for _, rule := range rules { + if !rule.Match(room, evt) { + continue + } + return rule.Actions + } + return nil +} + +type PushRuleMap struct { + Map map[string]*PushRule + Type PushRuleType +} + +func (rules PushRuleArray) SetTypeAndMap(typ PushRuleType) PushRuleMap { + data := PushRuleMap{ + Map: make(map[string]*PushRule), + Type: typ, + } + for _, rule := range rules { + rule.Type = typ + data.Map[rule.RuleID] = rule + } + return data +} + +func (ruleMap PushRuleMap) GetActions(room Room, evt *event.Event) PushActionArray { + var rule *PushRule + var found bool + switch ruleMap.Type { + case RoomRule: + rule, found = ruleMap.Map[string(evt.RoomID)] + case SenderRule: + rule, found = ruleMap.Map[string(evt.Sender)] + } + if found && rule.Match(room, evt) { + return rule.Actions + } + return nil +} + +func (ruleMap PushRuleMap) Unmap() PushRuleArray { + array := make(PushRuleArray, len(ruleMap.Map)) + index := 0 + for _, rule := range ruleMap.Map { + array[index] = rule + index++ + } + return array +} + +type PushRuleType string + +const ( + OverrideRule PushRuleType = "override" + ContentRule PushRuleType = "content" + RoomRule PushRuleType = "room" + SenderRule PushRuleType = "sender" + UnderrideRule PushRuleType = "underride" +) + +type PushRule struct { + // The type of this rule. + Type PushRuleType `json:"-"` + // The ID of this rule. + // For room-specific rules and user-specific rules, this is the room or user ID (respectively) + // For other types of rules, this doesn't affect anything. + RuleID string `json:"rule_id"` + // The actions this rule should trigger when matched. + Actions PushActionArray `json:"actions"` + // Whether this is a default rule, or has been set explicitly. + Default bool `json:"default"` + // Whether or not this push rule is enabled. + Enabled bool `json:"enabled"` + // The conditions to match in order to trigger this rule. + // Only applicable to generic underride/override rules. + Conditions []*PushCondition `json:"conditions,omitempty"` + // Pattern for content-specific push rules + Pattern string `json:"pattern,omitempty"` +} + +func (rule *PushRule) Match(room Room, evt *event.Event) bool { + if !rule.Enabled { + return false + } + switch rule.Type { + case OverrideRule, UnderrideRule: + return rule.matchConditions(room, evt) + case ContentRule: + return rule.matchPattern(room, evt) + case RoomRule: + return id.RoomID(rule.RuleID) == evt.RoomID + case SenderRule: + return id.UserID(rule.RuleID) == evt.Sender + default: + return false + } +} + +func (rule *PushRule) matchConditions(room Room, evt *event.Event) bool { + for _, cond := range rule.Conditions { + if !cond.Match(room, evt) { + return false + } + } + return true +} + +func (rule *PushRule) matchPattern(room Room, evt *event.Event) bool { + pattern, err := glob.Compile(rule.Pattern) + if err != nil { + return false + } + msg, ok := evt.Content.Raw["body"].(string) + if !ok { + return false + } + return pattern.MatchString(msg) +} diff --git a/vendor/maunium.net/go/mautrix/pushrules/ruleset.go b/vendor/maunium.net/go/mautrix/pushrules/ruleset.go new file mode 100644 index 00000000..48ae1e35 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/pushrules/ruleset.go @@ -0,0 +1,88 @@ +// Copyright (c) 2020 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package pushrules + +import ( + "encoding/json" + + "maunium.net/go/mautrix/event" +) + +type PushRuleset struct { + Override PushRuleArray + Content PushRuleArray + Room PushRuleMap + Sender PushRuleMap + Underride PushRuleArray +} + +type rawPushRuleset struct { + Override PushRuleArray `json:"override"` + Content PushRuleArray `json:"content"` + Room PushRuleArray `json:"room"` + Sender PushRuleArray `json:"sender"` + Underride PushRuleArray `json:"underride"` +} + +// UnmarshalJSON parses JSON into this PushRuleset. +// +// For override, sender and underride push rule arrays, the type is added +// to each PushRule and the array is used as-is. +// +// For room and sender push rule arrays, the type is added to each PushRule +// and the array is converted to a map with the rule ID as the key and the +// PushRule as the value. +func (rs *PushRuleset) UnmarshalJSON(raw []byte) (err error) { + data := rawPushRuleset{} + err = json.Unmarshal(raw, &data) + if err != nil { + return + } + + rs.Override = data.Override.SetType(OverrideRule) + rs.Content = data.Content.SetType(ContentRule) + rs.Room = data.Room.SetTypeAndMap(RoomRule) + rs.Sender = data.Sender.SetTypeAndMap(SenderRule) + rs.Underride = data.Underride.SetType(UnderrideRule) + return +} + +// MarshalJSON is the reverse of UnmarshalJSON() +func (rs *PushRuleset) MarshalJSON() ([]byte, error) { + data := rawPushRuleset{ + Override: rs.Override, + Content: rs.Content, + Room: rs.Room.Unmap(), + Sender: rs.Sender.Unmap(), + Underride: rs.Underride, + } + return json.Marshal(&data) +} + +// DefaultPushActions is the value returned if none of the rule +// collections in a Ruleset match the event given to GetActions() +var DefaultPushActions = PushActionArray{&PushAction{Action: ActionDontNotify}} + +// GetActions matches the given event against all of the push rule +// collections in this push ruleset in the order of priority as +// specified in spec section 11.12.1.4. +func (rs *PushRuleset) GetActions(room Room, evt *event.Event) (match PushActionArray) { + // Add push rule collections to array in priority order + arrays := []PushRuleCollection{rs.Override, rs.Content, rs.Room, rs.Sender, rs.Underride} + // Loop until one of the push rule collections matches the room/event combo. + for _, pra := range arrays { + if pra == nil { + continue + } + if match = pra.GetActions(room, evt); match != nil { + // Match found, return it. + return + } + } + // No match found, return default actions. + return DefaultPushActions +} diff --git a/vendor/maunium.net/go/mautrix/requests.go b/vendor/maunium.net/go/mautrix/requests.go new file mode 100644 index 00000000..8c7571de --- /dev/null +++ b/vendor/maunium.net/go/mautrix/requests.go @@ -0,0 +1,437 @@ +package mautrix + +import ( + "encoding/json" + "strconv" + + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" + "maunium.net/go/mautrix/pushrules" +) + +type AuthType string + +const ( + AuthTypePassword AuthType = "m.login.password" + AuthTypeReCAPTCHA AuthType = "m.login.recaptcha" + AuthTypeOAuth2 AuthType = "m.login.oauth2" + AuthTypeSSO AuthType = "m.login.sso" + AuthTypeEmail AuthType = "m.login.email.identity" + AuthTypeMSISDN AuthType = "m.login.msisdn" + AuthTypeToken AuthType = "m.login.token" + AuthTypeDummy AuthType = "m.login.dummy" + AuthTypeAppservice AuthType = "m.login.application_service" +) + +type IdentifierType string + +const ( + IdentifierTypeUser = "m.id.user" + IdentifierTypeThirdParty = "m.id.thirdparty" + IdentifierTypePhone = "m.id.phone" +) + +type Direction rune + +const ( + DirectionForward Direction = 'f' + DirectionBackward Direction = 'b' +) + +// ReqRegister is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3register +type ReqRegister struct { + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + DeviceID id.DeviceID `json:"device_id,omitempty"` + InitialDeviceDisplayName string `json:"initial_device_display_name,omitempty"` + InhibitLogin bool `json:"inhibit_login,omitempty"` + RefreshToken bool `json:"refresh_token,omitempty"` + Auth interface{} `json:"auth,omitempty"` + + // Type for registration, only used for appservice user registrations + // https://spec.matrix.org/v1.2/application-service-api/#server-admin-style-permissions + Type AuthType `json:"type,omitempty"` +} + +type BaseAuthData struct { + Type AuthType `json:"type"` + Session string `json:"session,omitempty"` +} + +type UserIdentifier struct { + Type IdentifierType `json:"type"` + + User string `json:"user,omitempty"` + + Medium string `json:"medium,omitempty"` + Address string `json:"address,omitempty"` + + Country string `json:"country,omitempty"` + Phone string `json:"phone,omitempty"` +} + +// ReqLogin is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3login +type ReqLogin struct { + Type AuthType `json:"type"` + Identifier UserIdentifier `json:"identifier"` + Password string `json:"password,omitempty"` + Token string `json:"token,omitempty"` + DeviceID id.DeviceID `json:"device_id,omitempty"` + InitialDeviceDisplayName string `json:"initial_device_display_name,omitempty"` + + // Whether or not the returned credentials should be stored in the Client + StoreCredentials bool `json:"-"` + // Whether or not the returned .well-known data should update the homeserver URL in the Client + StoreHomeserverURL bool `json:"-"` +} + +type ReqUIAuthFallback struct { + Session string `json:"session"` + User string `json:"user"` +} + +type ReqUIAuthLogin struct { + BaseAuthData + User string `json:"user"` + Password string `json:"password"` +} + +// ReqCreateRoom is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3createroom +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 []id.UserID `json:"invite,omitempty"` + Invite3PID []ReqInvite3PID `json:"invite_3pid,omitempty"` + CreationContent map[string]interface{} `json:"creation_content,omitempty"` + InitialState []*event.Event `json:"initial_state,omitempty"` + Preset string `json:"preset,omitempty"` + IsDirect bool `json:"is_direct,omitempty"` + RoomVersion string `json:"room_version,omitempty"` + + PowerLevelOverride *event.PowerLevelsEventContent `json:"power_level_content_override,omitempty"` + + MeowRoomID id.RoomID `json:"fi.mau.room_id,omitempty"` + BeeperAutoJoinInvites bool `json:"com.beeper.auto_join_invites,omitempty"` +} + +// ReqRedact is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidredacteventidtxnid +type ReqRedact struct { + Reason string + TxnID string + Extra map[string]interface{} +} + +type ReqMembers struct { + At string `json:"at"` + Membership event.Membership `json:"membership,omitempty"` + NotMembership event.Membership `json:"not_membership,omitempty"` +} + +// ReqInvite3PID is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidinvite-1 +// It is also a JSON object used in https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3createroom +type ReqInvite3PID struct { + IDServer string `json:"id_server"` + Medium string `json:"medium"` + Address string `json:"address"` +} + +type ReqLeave struct { + Reason string `json:"reason,omitempty"` +} + +// ReqInviteUser is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidinvite +type ReqInviteUser struct { + Reason string `json:"reason,omitempty"` + UserID id.UserID `json:"user_id"` +} + +// ReqKickUser is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidkick +type ReqKickUser struct { + Reason string `json:"reason,omitempty"` + UserID id.UserID `json:"user_id"` +} + +// ReqBanUser is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidban +type ReqBanUser struct { + Reason string `json:"reason,omitempty"` + UserID id.UserID `json:"user_id"` +} + +// ReqUnbanUser is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidunban +type ReqUnbanUser struct { + Reason string `json:"reason,omitempty"` + UserID id.UserID `json:"user_id"` +} + +// ReqTyping is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidtypinguserid +type ReqTyping struct { + Typing bool `json:"typing"` + Timeout int64 `json:"timeout,omitempty"` +} + +type ReqPresence struct { + Presence event.Presence `json:"presence"` +} + +type ReqAliasCreate struct { + RoomID id.RoomID `json:"room_id"` +} + +type OneTimeKey struct { + Key id.Curve25519 `json:"key"` + Fallback bool `json:"fallback,omitempty"` + Signatures Signatures `json:"signatures,omitempty"` + Unsigned map[string]any `json:"unsigned,omitempty"` + IsSigned bool `json:"-"` + + // Raw data in the one-time key. This must be used for signature verification to ensure unrecognized fields + // aren't thrown away (because that would invalidate the signature). + RawData json.RawMessage `json:"-"` +} + +type serializableOTK OneTimeKey + +func (otk *OneTimeKey) UnmarshalJSON(data []byte) (err error) { + if len(data) > 0 && data[0] == '"' && data[len(data)-1] == '"' { + err = json.Unmarshal(data, &otk.Key) + otk.Signatures = nil + otk.Unsigned = nil + otk.IsSigned = false + } else { + err = json.Unmarshal(data, (*serializableOTK)(otk)) + otk.RawData = data + otk.IsSigned = true + } + return err +} + +func (otk *OneTimeKey) MarshalJSON() ([]byte, error) { + if !otk.IsSigned { + return json.Marshal(otk.Key) + } else { + return json.Marshal((*serializableOTK)(otk)) + } +} + +type ReqUploadKeys struct { + DeviceKeys *DeviceKeys `json:"device_keys,omitempty"` + OneTimeKeys map[id.KeyID]OneTimeKey `json:"one_time_keys"` +} + +type ReqKeysSignatures struct { + UserID id.UserID `json:"user_id"` + DeviceID id.DeviceID `json:"device_id,omitempty"` + Algorithms []id.Algorithm `json:"algorithms,omitempty"` + Usage []id.CrossSigningUsage `json:"usage,omitempty"` + Keys map[id.KeyID]string `json:"keys"` + Signatures Signatures `json:"signatures"` +} + +type ReqUploadSignatures map[id.UserID]map[string]ReqKeysSignatures + +type DeviceKeys struct { + UserID id.UserID `json:"user_id"` + DeviceID id.DeviceID `json:"device_id"` + Algorithms []id.Algorithm `json:"algorithms"` + Keys KeyMap `json:"keys"` + Signatures Signatures `json:"signatures"` + Unsigned map[string]interface{} `json:"unsigned,omitempty"` +} + +type CrossSigningKeys struct { + UserID id.UserID `json:"user_id"` + Usage []id.CrossSigningUsage `json:"usage"` + Keys map[id.KeyID]id.Ed25519 `json:"keys"` + Signatures map[id.UserID]map[id.KeyID]string `json:"signatures,omitempty"` +} + +func (csk *CrossSigningKeys) FirstKey() id.Ed25519 { + for _, key := range csk.Keys { + return key + } + return "" +} + +type UploadCrossSigningKeysReq struct { + Master CrossSigningKeys `json:"master_key"` + SelfSigning CrossSigningKeys `json:"self_signing_key"` + UserSigning CrossSigningKeys `json:"user_signing_key"` + Auth interface{} `json:"auth,omitempty"` +} + +type KeyMap map[id.DeviceKeyID]string + +func (km KeyMap) GetEd25519(deviceID id.DeviceID) id.Ed25519 { + val, ok := km[id.NewDeviceKeyID(id.KeyAlgorithmEd25519, deviceID)] + if !ok { + return "" + } + return id.Ed25519(val) +} + +func (km KeyMap) GetCurve25519(deviceID id.DeviceID) id.Curve25519 { + val, ok := km[id.NewDeviceKeyID(id.KeyAlgorithmCurve25519, deviceID)] + if !ok { + return "" + } + return id.Curve25519(val) +} + +type Signatures map[id.UserID]map[id.KeyID]string + +type ReqQueryKeys struct { + DeviceKeys DeviceKeysRequest `json:"device_keys"` + + Timeout int64 `json:"timeout,omitempty"` + Token string `json:"token,omitempty"` +} + +type DeviceKeysRequest map[id.UserID]DeviceIDList + +type DeviceIDList []id.DeviceID + +type ReqClaimKeys struct { + OneTimeKeys OneTimeKeysRequest `json:"one_time_keys"` + + Timeout int64 `json:"timeout,omitempty"` +} + +type OneTimeKeysRequest map[id.UserID]map[id.DeviceID]id.KeyAlgorithm + +type ReqSendToDevice struct { + Messages map[id.UserID]map[id.DeviceID]*event.Content `json:"messages"` +} + +// ReqDeviceInfo is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3devicesdeviceid +type ReqDeviceInfo struct { + DisplayName string `json:"display_name,omitempty"` +} + +// ReqDeleteDevice is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#delete_matrixclientv3devicesdeviceid +type ReqDeleteDevice struct { + Auth interface{} `json:"auth,omitempty"` +} + +// ReqDeleteDevices is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3delete_devices +type ReqDeleteDevices struct { + Devices []id.DeviceID `json:"devices"` + Auth interface{} `json:"auth,omitempty"` +} + +type ReqPutPushRule struct { + Before string `json:"-"` + After string `json:"-"` + + Actions []pushrules.PushActionType `json:"actions"` + Conditions []pushrules.PushCondition `json:"conditions"` + Pattern string `json:"pattern"` +} + +type ReqBatchSend struct { + PrevEventID id.EventID `json:"-"` + BatchID id.BatchID `json:"-"` + + BeeperNewMessages bool `json:"-"` + BeeperMarkReadBy id.UserID `json:"-"` + + StateEventsAtStart []*event.Event `json:"state_events_at_start"` + Events []*event.Event `json:"events"` +} + +type ReqSetReadMarkers struct { + Read id.EventID `json:"m.read,omitempty"` + ReadPrivate id.EventID `json:"m.read.private,omitempty"` + FullyRead id.EventID `json:"m.fully_read,omitempty"` + + BeeperReadExtra interface{} `json:"com.beeper.read.extra,omitempty"` + BeeperReadPrivateExtra interface{} `json:"com.beeper.read.private.extra,omitempty"` + BeeperFullyReadExtra interface{} `json:"com.beeper.fully_read.extra,omitempty"` +} + +type ReqSendReceipt struct { + ThreadID string `json:"thread_id,omitempty"` +} + +// ReqHierarchy contains the parameters for https://spec.matrix.org/v1.4/client-server-api/#get_matrixclientv1roomsroomidhierarchy +// +// As it's a GET method, there is no JSON body, so this is only query parameters. +type ReqHierarchy struct { + // A pagination token from a previous Hierarchy call. + // If specified, max_depth and suggested_only cannot be changed from the first request. + From string + // Limit for the maximum number of rooms to include per response. + // The server will apply a default value if a limit isn't provided. + Limit int + // Limit for how far to go into the space. When reached, no further child rooms will be returned. + // The server will apply a default value if a max depth isn't provided. + MaxDepth *int + // Flag to indicate whether the server should only consider suggested rooms. + // Suggested rooms are annotated in their m.space.child event contents. + SuggestedOnly bool +} + +func (req *ReqHierarchy) Query() map[string]string { + query := map[string]string{} + if req == nil { + return query + } + if req.From != "" { + query["from"] = req.From + } + if req.Limit > 0 { + query["limit"] = strconv.Itoa(req.Limit) + } + if req.MaxDepth != nil { + query["max_depth"] = strconv.Itoa(*req.MaxDepth) + } + if req.SuggestedOnly { + query["suggested_only"] = "true" + } + return query +} + +type ReqAppservicePing struct { + TxnID string `json:"transaction_id,omitempty"` +} + +type ReqBeeperMergeRoom struct { + NewRoom ReqCreateRoom `json:"create"` + Key string `json:"key"` + Rooms []id.RoomID `json:"rooms"` + User id.UserID `json:"user_id"` +} + +type BeeperSplitRoomPart struct { + UserID id.UserID `json:"user_id"` + Values []string `json:"values"` + NewRoom ReqCreateRoom `json:"create"` +} + +type ReqBeeperSplitRoom struct { + RoomID id.RoomID `json:"-"` + + Key string `json:"key"` + Parts []BeeperSplitRoomPart `json:"parts"` +} + +type ReqRoomKeysVersionCreate struct { + Algorithm string `json:"algorithm"` + AuthData json.RawMessage `json:"auth_data"` +} + +type ReqRoomKeysUpdate struct { + Rooms map[id.RoomID]ReqRoomKeysRoomUpdate `json:"rooms"` +} + +type ReqRoomKeysRoomUpdate struct { + Sessions map[id.SessionID]ReqRoomKeysSessionUpdate `json:"sessions"` +} + +type ReqRoomKeysSessionUpdate struct { + FirstMessageIndex int `json:"first_message_index"` + ForwardedCount int `json:"forwarded_count"` + IsVerified bool `json:"is_verified"` + SessionData json.RawMessage `json:"session_data"` +} diff --git a/vendor/maunium.net/go/mautrix/responses.go b/vendor/maunium.net/go/mautrix/responses.go new file mode 100644 index 00000000..ac6bbfe7 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/responses.go @@ -0,0 +1,600 @@ +package mautrix + +import ( + "bytes" + "encoding/json" + "reflect" + "strconv" + "strings" + + "github.com/tidwall/gjson" + + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" + "maunium.net/go/mautrix/util" + "maunium.net/go/mautrix/util/jsontime" +) + +// RespWhoami is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3accountwhoami +type RespWhoami struct { + UserID id.UserID `json:"user_id"` + DeviceID id.DeviceID `json:"device_id"` +} + +// RespCreateFilter is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3useruseridfilter +type RespCreateFilter struct { + FilterID string `json:"filter_id"` +} + +// RespJoinRoom is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidjoin +type RespJoinRoom struct { + RoomID id.RoomID `json:"room_id"` +} + +// RespLeaveRoom is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidleave +type RespLeaveRoom struct{} + +// RespForgetRoom is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidforget +type RespForgetRoom struct{} + +// RespInviteUser is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidinvite +type RespInviteUser struct{} + +// RespKickUser is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidkick +type RespKickUser struct{} + +// RespBanUser is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidban +type RespBanUser struct{} + +// RespUnbanUser is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidunban +type RespUnbanUser struct{} + +// RespTyping is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidtypinguserid +type RespTyping struct{} + +// RespPresence is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3presenceuseridstatus +type RespPresence struct { + Presence event.Presence `json:"presence"` + LastActiveAgo int `json:"last_active_ago"` + StatusMsg string `json:"status_msg"` + CurrentlyActive bool `json:"currently_active"` +} + +// RespJoinedRooms is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3joined_rooms +type RespJoinedRooms struct { + JoinedRooms []id.RoomID `json:"joined_rooms"` +} + +// RespJoinedMembers is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomidjoined_members +type RespJoinedMembers struct { + Joined map[id.UserID]JoinedMember `json:"joined"` +} + +type JoinedMember struct { + DisplayName string `json:"display_name,omitempty"` + AvatarURL string `json:"avatar_url,omitempty"` +} + +// RespMessages is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomidmessages +type RespMessages struct { + Start string `json:"start"` + Chunk []*event.Event `json:"chunk"` + State []*event.Event `json:"state"` + End string `json:"end,omitempty"` +} + +// RespContext is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomidcontexteventid +type RespContext struct { + End string `json:"end"` + Event *event.Event `json:"event"` + EventsAfter []*event.Event `json:"events_after"` + EventsBefore []*event.Event `json:"events_before"` + Start string `json:"start"` + State []*event.Event `json:"state"` +} + +// RespSendEvent is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid +type RespSendEvent struct { + EventID id.EventID `json:"event_id"` +} + +// RespMediaConfig is the JSON response for https://spec.matrix.org/v1.4/client-server-api/#get_matrixmediav3config +type RespMediaConfig struct { + UploadSize int64 `json:"m.upload.size,omitempty"` +} + +// RespMediaUpload is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixmediav3upload +type RespMediaUpload struct { + ContentURI id.ContentURI `json:"content_uri"` +} + +// RespCreateMXC is the JSON response for /_matrix/media/v3/create as specified in https://github.com/matrix-org/matrix-spec-proposals/pull/2246 +type RespCreateMXC struct { + ContentURI id.ContentURI `json:"content_uri"` + UnusedExpiresAt int `json:"unused_expires_at,omitempty"` + UploadURL string `json:"upload_url,omitempty"` +} + +// RespPreviewURL is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixmediav3preview_url +type RespPreviewURL struct { + CanonicalURL string `json:"og:url,omitempty"` + Title string `json:"og:title,omitempty"` + Type string `json:"og:type,omitempty"` + Description string `json:"og:description,omitempty"` + + ImageURL id.ContentURIString `json:"og:image,omitempty"` + + ImageSize int `json:"matrix:image:size,omitempty"` + ImageWidth int `json:"og:image:width,omitempty"` + ImageHeight int `json:"og:image:height,omitempty"` + ImageType string `json:"og:image:type,omitempty"` +} + +// RespUserInteractive is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#user-interactive-authentication-api +type RespUserInteractive struct { + Flows []UIAFlow `json:"flows,omitempty"` + Params map[AuthType]interface{} `json:"params,omitempty"` + Session string `json:"session,omitempty"` + Completed []string `json:"completed,omitempty"` + + ErrCode string `json:"errcode,omitempty"` + Error string `json:"error,omitempty"` +} + +type UIAFlow struct { + Stages []AuthType `json:"stages,omitempty"` +} + +// HasSingleStageFlow returns true if there exists at least 1 Flow with a single stage of stageName. +func (r RespUserInteractive) HasSingleStageFlow(stageName AuthType) 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://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3profileuseriddisplayname +type RespUserDisplayName struct { + DisplayName string `json:"displayname"` +} + +type RespUserProfile struct { + DisplayName string `json:"displayname"` + AvatarURL id.ContentURI `json:"avatar_url"` +} + +// RespRegisterAvailable is the JSON response for https://spec.matrix.org/v1.4/client-server-api/#get_matrixclientv3registeravailable +type RespRegisterAvailable struct { + Available bool `json:"available"` +} + +// RespRegister is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3register +type RespRegister struct { + AccessToken string `json:"access_token,omitempty"` + DeviceID id.DeviceID `json:"device_id,omitempty"` + UserID id.UserID `json:"user_id"` + + RefreshToken string `json:"refresh_token,omitempty"` + ExpiresInMS int64 `json:"expires_in_ms,omitempty"` + + // Deprecated: homeserver should be parsed from the user ID + HomeServer string `json:"home_server,omitempty"` +} + +type LoginFlow struct { + Type AuthType `json:"type"` +} + +// RespLoginFlows is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3login +type RespLoginFlows struct { + Flows []LoginFlow `json:"flows"` +} + +func (rlf *RespLoginFlows) FirstFlowOfType(flowTypes ...AuthType) *LoginFlow { + for _, flow := range rlf.Flows { + for _, flowType := range flowTypes { + if flow.Type == flowType { + return &flow + } + } + } + return nil +} + +func (rlf *RespLoginFlows) HasFlow(flowType ...AuthType) bool { + return rlf.FirstFlowOfType(flowType...) != nil +} + +// RespLogin is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3login +type RespLogin struct { + AccessToken string `json:"access_token"` + DeviceID id.DeviceID `json:"device_id"` + UserID id.UserID `json:"user_id"` + WellKnown *ClientWellKnown `json:"well_known,omitempty"` +} + +// RespLogout is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3logout +type RespLogout struct{} + +// RespCreateRoom is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3createroom +type RespCreateRoom struct { + RoomID id.RoomID `json:"room_id"` +} + +type RespMembers struct { + Chunk []*event.Event `json:"chunk"` +} + +type LazyLoadSummary struct { + Heroes []id.UserID `json:"m.heroes,omitempty"` + JoinedMemberCount *int `json:"m.joined_member_count,omitempty"` + InvitedMemberCount *int `json:"m.invited_member_count,omitempty"` +} + +type SyncEventsList struct { + Events []*event.Event `json:"events,omitempty"` +} + +type SyncTimeline struct { + SyncEventsList + Limited bool `json:"limited,omitempty"` + PrevBatch string `json:"prev_batch,omitempty"` +} + +// RespSync is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3sync +type RespSync struct { + NextBatch string `json:"next_batch"` + + AccountData SyncEventsList `json:"account_data"` + Presence SyncEventsList `json:"presence"` + ToDevice SyncEventsList `json:"to_device"` + + DeviceLists DeviceLists `json:"device_lists"` + DeviceOTKCount OTKCount `json:"device_one_time_keys_count"` + FallbackKeys []id.KeyAlgorithm `json:"device_unused_fallback_key_types"` + + Rooms RespSyncRooms `json:"rooms"` +} + +type RespSyncRooms struct { + Leave map[id.RoomID]*SyncLeftRoom `json:"leave,omitempty"` + Join map[id.RoomID]*SyncJoinedRoom `json:"join,omitempty"` + Invite map[id.RoomID]*SyncInvitedRoom `json:"invite,omitempty"` + Knock map[id.RoomID]*SyncKnockedRoom `json:"knock,omitempty"` +} + +type marshalableRespSync RespSync + +var syncPathsToDelete = []string{"account_data", "presence", "to_device", "device_lists", "device_one_time_keys_count", "rooms"} + +func (rs *RespSync) MarshalJSON() ([]byte, error) { + return util.MarshalAndDeleteEmpty((*marshalableRespSync)(rs), syncPathsToDelete) +} + +type DeviceLists struct { + Changed []id.UserID `json:"changed,omitempty"` + Left []id.UserID `json:"left,omitempty"` +} + +type OTKCount struct { + Curve25519 int `json:"curve25519,omitempty"` + SignedCurve25519 int `json:"signed_curve25519,omitempty"` + + // For appservice OTK counts only: the user ID in question + UserID id.UserID `json:"-"` + DeviceID id.DeviceID `json:"-"` +} + +type SyncLeftRoom struct { + Summary LazyLoadSummary `json:"summary"` + State SyncEventsList `json:"state"` + Timeline SyncTimeline `json:"timeline"` +} + +type marshalableSyncLeftRoom SyncLeftRoom + +var syncLeftRoomPathsToDelete = []string{"summary", "state", "timeline"} + +func (slr SyncLeftRoom) MarshalJSON() ([]byte, error) { + return util.MarshalAndDeleteEmpty((marshalableSyncLeftRoom)(slr), syncLeftRoomPathsToDelete) +} + +type SyncJoinedRoom struct { + Summary LazyLoadSummary `json:"summary"` + State SyncEventsList `json:"state"` + Timeline SyncTimeline `json:"timeline"` + Ephemeral SyncEventsList `json:"ephemeral"` + AccountData SyncEventsList `json:"account_data"` + + UnreadNotifications *UnreadNotificationCounts `json:"unread_notifications,omitempty"` + // https://github.com/matrix-org/matrix-spec-proposals/pull/2654 + MSC2654UnreadCount *int `json:"org.matrix.msc2654.unread_count,omitempty"` +} + +type UnreadNotificationCounts struct { + HighlightCount int `json:"highlight_count"` + NotificationCount int `json:"notification_count"` +} + +type marshalableSyncJoinedRoom SyncJoinedRoom + +var syncJoinedRoomPathsToDelete = []string{"summary", "state", "timeline", "ephemeral", "account_data"} + +func (sjr SyncJoinedRoom) MarshalJSON() ([]byte, error) { + return util.MarshalAndDeleteEmpty((marshalableSyncJoinedRoom)(sjr), syncJoinedRoomPathsToDelete) +} + +type SyncInvitedRoom struct { + Summary LazyLoadSummary `json:"summary"` + State SyncEventsList `json:"invite_state"` +} + +type marshalableSyncInvitedRoom SyncInvitedRoom + +var syncInvitedRoomPathsToDelete = []string{"summary"} + +func (sir SyncInvitedRoom) MarshalJSON() ([]byte, error) { + return util.MarshalAndDeleteEmpty((marshalableSyncInvitedRoom)(sir), syncInvitedRoomPathsToDelete) +} + +type SyncKnockedRoom struct { + State SyncEventsList `json:"knock_state"` +} + +type RespTurnServer struct { + Username string `json:"username"` + Password string `json:"password"` + TTL int `json:"ttl"` + URIs []string `json:"uris"` +} + +type RespAliasCreate struct{} +type RespAliasDelete struct{} +type RespAliasResolve struct { + RoomID id.RoomID `json:"room_id"` + Servers []string `json:"servers"` +} +type RespAliasList struct { + Aliases []id.RoomAlias `json:"aliases"` +} + +type RespUploadKeys struct { + OneTimeKeyCounts OTKCount `json:"one_time_key_counts"` +} + +type RespQueryKeys struct { + Failures map[string]interface{} `json:"failures,omitempty"` + DeviceKeys map[id.UserID]map[id.DeviceID]DeviceKeys `json:"device_keys"` + MasterKeys map[id.UserID]CrossSigningKeys `json:"master_keys"` + SelfSigningKeys map[id.UserID]CrossSigningKeys `json:"self_signing_keys"` + UserSigningKeys map[id.UserID]CrossSigningKeys `json:"user_signing_keys"` +} + +type RespClaimKeys struct { + Failures map[string]interface{} `json:"failures,omitempty"` + OneTimeKeys map[id.UserID]map[id.DeviceID]map[id.KeyID]OneTimeKey `json:"one_time_keys"` +} + +type RespUploadSignatures struct { + Failures map[string]interface{} `json:"failures,omitempty"` +} + +type RespKeyChanges struct { + Changed []id.UserID `json:"changed"` + Left []id.UserID `json:"left"` +} + +type RespSendToDevice struct{} + +// RespDevicesInfo is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3devices +type RespDevicesInfo struct { + Devices []RespDeviceInfo `json:"devices"` +} + +// RespDeviceInfo is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3devicesdeviceid +type RespDeviceInfo struct { + DeviceID id.DeviceID `json:"device_id"` + DisplayName string `json:"display_name"` + LastSeenIP string `json:"last_seen_ip"` + LastSeenTS int64 `json:"last_seen_ts"` +} + +type RespBatchSend struct { + StateEventIDs []id.EventID `json:"state_event_ids"` + EventIDs []id.EventID `json:"event_ids"` + + InsertionEventID id.EventID `json:"insertion_event_id"` + BatchEventID id.EventID `json:"batch_event_id"` + BaseInsertionEventID id.EventID `json:"base_insertion_event_id"` + + NextBatchID id.BatchID `json:"next_batch_id"` +} + +// RespCapabilities is the JSON response for https://spec.matrix.org/v1.3/client-server-api/#get_matrixclientv3capabilities +type RespCapabilities struct { + RoomVersions *CapRoomVersions `json:"m.room_versions,omitempty"` + ChangePassword *CapBooleanTrue `json:"m.change_password,omitempty"` + SetDisplayname *CapBooleanTrue `json:"m.set_displayname,omitempty"` + SetAvatarURL *CapBooleanTrue `json:"m.set_avatar_url,omitempty"` + ThreePIDChanges *CapBooleanTrue `json:"m.3pid_changes,omitempty"` + + Custom map[string]interface{} `json:"-"` +} + +type serializableRespCapabilities RespCapabilities + +func (rc *RespCapabilities) UnmarshalJSON(data []byte) error { + res := gjson.GetBytes(data, "capabilities") + if !res.Exists() || !res.IsObject() { + return nil + } + if res.Index > 0 { + data = data[res.Index : res.Index+len(res.Raw)] + } else { + data = []byte(res.Raw) + } + err := json.Unmarshal(data, (*serializableRespCapabilities)(rc)) + if err != nil { + return err + } + err = json.Unmarshal(data, &rc.Custom) + if err != nil { + return err + } + // Remove non-custom capabilities from the custom map so that they don't get overridden when serializing back + for _, field := range reflect.VisibleFields(reflect.TypeOf(rc).Elem()) { + jsonTag := strings.Split(field.Tag.Get("json"), ",")[0] + if jsonTag != "-" && jsonTag != "" { + delete(rc.Custom, jsonTag) + } + } + return nil +} + +func (rc *RespCapabilities) MarshalJSON() ([]byte, error) { + marshalableCopy := make(map[string]interface{}, len(rc.Custom)) + val := reflect.ValueOf(rc).Elem() + for _, field := range reflect.VisibleFields(val.Type()) { + jsonTag := strings.Split(field.Tag.Get("json"), ",")[0] + if jsonTag != "-" && jsonTag != "" { + fieldVal := val.FieldByIndex(field.Index) + if !fieldVal.IsNil() { + marshalableCopy[jsonTag] = fieldVal.Interface() + } + } + } + if rc.Custom != nil { + for key, value := range rc.Custom { + marshalableCopy[key] = value + } + } + var buf bytes.Buffer + buf.WriteString(`{"capabilities":`) + err := json.NewEncoder(&buf).Encode(marshalableCopy) + if err != nil { + return nil, err + } + buf.WriteByte('}') + return buf.Bytes(), nil +} + +type CapBoolean struct { + Enabled bool `json:"enabled"` +} + +type CapBooleanTrue CapBoolean + +// IsEnabled returns true if the capability is either enabled explicitly or not specified (nil) +func (cb *CapBooleanTrue) IsEnabled() bool { + // Default to true when + return cb == nil || cb.Enabled +} + +type CapBooleanFalse CapBoolean + +// IsEnabled returns true if the capability is enabled explicitly. If it's not specified, this returns false. +func (cb *CapBooleanFalse) IsEnabled() bool { + return cb != nil && cb.Enabled +} + +type CapRoomVersionStability string + +const ( + CapRoomVersionStable CapRoomVersionStability = "stable" + CapRoomVersionUnstable CapRoomVersionStability = "unstable" +) + +type CapRoomVersions struct { + Default string `json:"default"` + Available map[string]CapRoomVersionStability `json:"available"` +} + +func (vers *CapRoomVersions) IsStable(version string) bool { + if vers == nil || vers.Available == nil { + val, err := strconv.Atoi(version) + return err == nil && val > 0 + } + return vers.Available[version] == CapRoomVersionStable +} + +func (vers *CapRoomVersions) IsAvailable(version string) bool { + if vers == nil || vers.Available == nil { + return false + } + _, available := vers.Available[version] + return available +} + +// RespHierarchy is the JSON response for https://spec.matrix.org/v1.4/client-server-api/#get_matrixclientv1roomsroomidhierarchy +type RespHierarchy struct { + NextBatch string `json:"next_batch,omitempty"` + Rooms []ChildRoomsChunk `json:"rooms"` +} + +type ChildRoomsChunk struct { + AvatarURL id.ContentURI `json:"avatar_url,omitempty"` + CanonicalAlias id.RoomAlias `json:"canonical_alias,omitempty"` + ChildrenState []StrippedStateWithTime `json:"children_state"` + GuestCanJoin bool `json:"guest_can_join"` + JoinRule event.JoinRule `json:"join_rule,omitempty"` + Name string `json:"name,omitempty"` + NumJoinedMembers int `json:"num_joined_members"` + RoomID id.RoomID `json:"room_id"` + RoomType event.RoomType `json:"room_type"` + Topic string `json:"topic,omitempty"` + WorldReadble bool `json:"world_readable"` +} + +type StrippedStateWithTime struct { + event.StrippedState + Timestamp jsontime.UnixMilli `json:"origin_server_ts"` +} + +type RespAppservicePing struct { + DurationMS int64 `json:"duration"` +} + +type RespBeeperMergeRoom RespCreateRoom + +type RespBeeperSplitRoom struct { + RoomIDs map[string]id.RoomID `json:"room_ids"` +} + +type RespTimestampToEvent struct { + EventID id.EventID `json:"event_id"` + Timestamp jsontime.UnixMilli `json:"origin_server_ts"` +} + +type RespRoomKeysVersionCreate struct { + Version string `json:"version"` +} + +type RespRoomKeysVersion struct { + Algorithm string `json:"algorithm"` + AuthData json.RawMessage `json:"auth_data"` + Count int `json:"count"` + ETag string `json:"etag"` + Version string `json:"version"` +} + +type RespRoomKeys struct { + Rooms map[id.RoomID]RespRoomKeysRoom `json:"rooms"` +} + +type RespRoomKeysRoom struct { + Sessions map[id.SessionID]RespRoomKeysSession `json:"sessions"` +} + +type RespRoomKeysSession struct { + FirstMessageIndex int `json:"first_message_index"` + ForwardedCount int `json:"forwarded_count"` + IsVerified bool `json:"is_verified"` + SessionData json.RawMessage `json:"session_data"` +} + +type RespRoomKeysUpdate struct { + Count int `json:"count"` + ETag string `json:"etag"` +} diff --git a/vendor/maunium.net/go/mautrix/room.go b/vendor/maunium.net/go/mautrix/room.go new file mode 100644 index 00000000..c3ddb7e6 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/room.go @@ -0,0 +1,54 @@ +package mautrix + +import ( + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" +) + +type RoomStateMap = map[event.Type]map[string]*event.Event + +// Room represents a single Matrix room. +type Room struct { + ID id.RoomID + State RoomStateMap +} + +// 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(evt *event.Event) { + _, exists := room.State[evt.Type] + if !exists { + room.State[evt.Type] = make(map[string]*event.Event) + } + room.State[evt.Type][*evt.StateKey] = evt +} + +// GetStateEvent returns the state event for the given type/state_key combo, or nil. +func (room Room) GetStateEvent(eventType event.Type, stateKey string) *event.Event { + stateEventMap, _ := room.State[eventType] + evt, _ := stateEventMap[stateKey] + return evt +} + +// 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 id.UserID) event.Membership { + state := event.MembershipLeave + evt := room.GetStateEvent(event.StateMember, string(userID)) + if evt != nil { + membership, ok := evt.Content.Raw["membership"].(string) + if ok { + state = event.Membership(membership) + } + } + return state +} + +// NewRoom creates a new Room with the given ID +func NewRoom(roomID id.RoomID) *Room { + // Init the State map and return a pointer to the Room + return &Room{ + ID: roomID, + State: make(RoomStateMap), + } +} diff --git a/vendor/maunium.net/go/mautrix/statestore.go b/vendor/maunium.net/go/mautrix/statestore.go new file mode 100644 index 00000000..bfe38cc9 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/statestore.go @@ -0,0 +1,221 @@ +// Copyright (c) 2023 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package mautrix + +import ( + "sync" + + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" +) + +// StateStore is an interface for storing basic room state information. +type StateStore interface { + IsInRoom(roomID id.RoomID, userID id.UserID) bool + IsInvited(roomID id.RoomID, userID id.UserID) bool + IsMembership(roomID id.RoomID, userID id.UserID, allowedMemberships ...event.Membership) bool + GetMember(roomID id.RoomID, userID id.UserID) *event.MemberEventContent + TryGetMember(roomID id.RoomID, userID id.UserID) (*event.MemberEventContent, bool) + SetMembership(roomID id.RoomID, userID id.UserID, membership event.Membership) + SetMember(roomID id.RoomID, userID id.UserID, member *event.MemberEventContent) + + SetPowerLevels(roomID id.RoomID, levels *event.PowerLevelsEventContent) + GetPowerLevels(roomID id.RoomID) *event.PowerLevelsEventContent + + SetEncryptionEvent(roomID id.RoomID, content *event.EncryptionEventContent) + IsEncrypted(roomID id.RoomID) bool +} + +func UpdateStateStore(store StateStore, evt *event.Event) { + if store == nil || evt == nil || evt.StateKey == nil { + return + } + // We only care about events without a state key (power levels, encryption) or member events with state key + if evt.Type != event.StateMember && evt.GetStateKey() != "" { + return + } + switch content := evt.Content.Parsed.(type) { + case *event.MemberEventContent: + store.SetMember(evt.RoomID, id.UserID(evt.GetStateKey()), content) + case *event.PowerLevelsEventContent: + store.SetPowerLevels(evt.RoomID, content) + case *event.EncryptionEventContent: + store.SetEncryptionEvent(evt.RoomID, content) + } +} + +// StateStoreSyncHandler can be added as an event handler in the syncer to update the state store automatically. +// +// client.Syncer.(mautrix.ExtensibleSyncer).OnEvent(client.StateStoreSyncHandler) +// +// DefaultSyncer.ParseEventContent must also be true for this to work (which it is by default). +func (cli *Client) StateStoreSyncHandler(_ EventSource, evt *event.Event) { + UpdateStateStore(cli.StateStore, evt) +} + +type MemoryStateStore struct { + Registrations map[id.UserID]bool `json:"registrations"` + Members map[id.RoomID]map[id.UserID]*event.MemberEventContent `json:"memberships"` + PowerLevels map[id.RoomID]*event.PowerLevelsEventContent `json:"power_levels"` + Encryption map[id.RoomID]*event.EncryptionEventContent `json:"encryption"` + + registrationsLock sync.RWMutex + membersLock sync.RWMutex + powerLevelsLock sync.RWMutex + encryptionLock sync.RWMutex +} + +func NewMemoryStateStore() StateStore { + return &MemoryStateStore{ + Registrations: make(map[id.UserID]bool), + Members: make(map[id.RoomID]map[id.UserID]*event.MemberEventContent), + PowerLevels: make(map[id.RoomID]*event.PowerLevelsEventContent), + } +} + +func (store *MemoryStateStore) IsRegistered(userID id.UserID) bool { + store.registrationsLock.RLock() + defer store.registrationsLock.RUnlock() + registered, ok := store.Registrations[userID] + return ok && registered +} + +func (store *MemoryStateStore) MarkRegistered(userID id.UserID) { + store.registrationsLock.Lock() + defer store.registrationsLock.Unlock() + store.Registrations[userID] = true +} + +func (store *MemoryStateStore) GetRoomMembers(roomID id.RoomID) map[id.UserID]*event.MemberEventContent { + store.membersLock.RLock() + members, ok := store.Members[roomID] + store.membersLock.RUnlock() + if !ok { + members = make(map[id.UserID]*event.MemberEventContent) + store.membersLock.Lock() + store.Members[roomID] = members + store.membersLock.Unlock() + } + return members +} + +func (store *MemoryStateStore) GetMembership(roomID id.RoomID, userID id.UserID) event.Membership { + return store.GetMember(roomID, userID).Membership +} + +func (store *MemoryStateStore) GetMember(roomID id.RoomID, userID id.UserID) *event.MemberEventContent { + member, ok := store.TryGetMember(roomID, userID) + if !ok { + member = &event.MemberEventContent{Membership: event.MembershipLeave} + } + return member +} + +func (store *MemoryStateStore) TryGetMember(roomID id.RoomID, userID id.UserID) (member *event.MemberEventContent, ok bool) { + store.membersLock.RLock() + defer store.membersLock.RUnlock() + members, membersOk := store.Members[roomID] + if !membersOk { + return + } + member, ok = members[userID] + return +} + +func (store *MemoryStateStore) IsInRoom(roomID id.RoomID, userID id.UserID) bool { + return store.IsMembership(roomID, userID, "join") +} + +func (store *MemoryStateStore) IsInvited(roomID id.RoomID, userID id.UserID) bool { + return store.IsMembership(roomID, userID, "join", "invite") +} + +func (store *MemoryStateStore) IsMembership(roomID id.RoomID, userID id.UserID, allowedMemberships ...event.Membership) bool { + membership := store.GetMembership(roomID, userID) + for _, allowedMembership := range allowedMemberships { + if allowedMembership == membership { + return true + } + } + return false +} + +func (store *MemoryStateStore) SetMembership(roomID id.RoomID, userID id.UserID, membership event.Membership) { + store.membersLock.Lock() + members, ok := store.Members[roomID] + if !ok { + members = map[id.UserID]*event.MemberEventContent{ + userID: {Membership: membership}, + } + } else { + member, ok := members[userID] + if !ok { + members[userID] = &event.MemberEventContent{Membership: membership} + } else { + member.Membership = membership + members[userID] = member + } + } + store.Members[roomID] = members + store.membersLock.Unlock() +} + +func (store *MemoryStateStore) SetMember(roomID id.RoomID, userID id.UserID, member *event.MemberEventContent) { + store.membersLock.Lock() + members, ok := store.Members[roomID] + if !ok { + members = map[id.UserID]*event.MemberEventContent{ + userID: member, + } + } else { + members[userID] = member + } + store.Members[roomID] = members + store.membersLock.Unlock() +} + +func (store *MemoryStateStore) SetPowerLevels(roomID id.RoomID, levels *event.PowerLevelsEventContent) { + store.powerLevelsLock.Lock() + store.PowerLevels[roomID] = levels + store.powerLevelsLock.Unlock() +} + +func (store *MemoryStateStore) GetPowerLevels(roomID id.RoomID) (levels *event.PowerLevelsEventContent) { + store.powerLevelsLock.RLock() + levels = store.PowerLevels[roomID] + store.powerLevelsLock.RUnlock() + return +} + +func (store *MemoryStateStore) GetPowerLevel(roomID id.RoomID, userID id.UserID) int { + return store.GetPowerLevels(roomID).GetUserLevel(userID) +} + +func (store *MemoryStateStore) GetPowerLevelRequirement(roomID id.RoomID, eventType event.Type) int { + return store.GetPowerLevels(roomID).GetEventLevel(eventType) +} + +func (store *MemoryStateStore) HasPowerLevel(roomID id.RoomID, userID id.UserID, eventType event.Type) bool { + return store.GetPowerLevel(roomID, userID) >= store.GetPowerLevelRequirement(roomID, eventType) +} + +func (store *MemoryStateStore) SetEncryptionEvent(roomID id.RoomID, content *event.EncryptionEventContent) { + store.encryptionLock.Lock() + store.Encryption[roomID] = content + store.encryptionLock.Unlock() +} + +func (store *MemoryStateStore) GetEncryptionEvent(roomID id.RoomID) *event.EncryptionEventContent { + store.encryptionLock.RLock() + defer store.encryptionLock.RUnlock() + return store.Encryption[roomID] +} + +func (store *MemoryStateStore) IsEncrypted(roomID id.RoomID) bool { + cfg := store.GetEncryptionEvent(roomID) + return cfg != nil && cfg.Algorithm == id.AlgorithmMegolmV1 +} diff --git a/vendor/maunium.net/go/mautrix/sync.go b/vendor/maunium.net/go/mautrix/sync.go new file mode 100644 index 00000000..6d3a1756 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/sync.go @@ -0,0 +1,310 @@ +// Copyright (c) 2020 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package mautrix + +import ( + "errors" + "fmt" + "runtime/debug" + "time" + + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" +) + +// EventSource represents the part of the sync response that an event came from. +type EventSource int + +const ( + EventSourcePresence EventSource = 1 << iota + EventSourceJoin + EventSourceInvite + EventSourceLeave + EventSourceAccountData + EventSourceTimeline + EventSourceState + EventSourceEphemeral + EventSourceToDevice + EventSourceDecrypted +) + +const primaryTypes = EventSourcePresence | EventSourceAccountData | EventSourceToDevice | EventSourceTimeline | EventSourceState +const roomSections = EventSourceJoin | EventSourceInvite | EventSourceLeave +const roomableTypes = EventSourceAccountData | EventSourceTimeline | EventSourceState +const encryptableTypes = roomableTypes | EventSourceToDevice + +func (es EventSource) String() string { + var typeName string + switch es & primaryTypes { + case EventSourcePresence: + typeName = "presence" + case EventSourceAccountData: + typeName = "account data" + case EventSourceToDevice: + typeName = "to-device" + case EventSourceTimeline: + typeName = "timeline" + case EventSourceState: + typeName = "state" + default: + return fmt.Sprintf("unknown (%d)", es) + } + if es&roomableTypes != 0 { + switch es & roomSections { + case EventSourceJoin: + typeName = "joined room " + typeName + case EventSourceInvite: + typeName = "invited room " + typeName + case EventSourceLeave: + typeName = "left room " + typeName + default: + return fmt.Sprintf("unknown (%d)", es) + } + es &^= roomableTypes + } + if es&encryptableTypes != 0 && es&EventSourceDecrypted != 0 { + typeName += " (decrypted)" + es &^= EventSourceDecrypted + } + es &^= primaryTypes + if es != 0 { + return fmt.Sprintf("unknown (%d)", es) + } + return typeName +} + +// EventHandler handles a single event from a sync response. +type EventHandler func(source EventSource, evt *event.Event) + +// SyncHandler handles a whole sync response. If the return value is false, handling will be stopped completely. +type SyncHandler func(resp *RespSync, since string) bool + +// Syncer is an interface that must be satisfied in order to do /sync requests on a client. +type Syncer interface { + // ProcessResponse processes 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 id.UserID) *Filter +} + +type ExtensibleSyncer interface { + OnSync(callback SyncHandler) + OnEvent(callback EventHandler) + OnEventType(eventType event.Type, callback EventHandler) +} + +type DispatchableSyncer interface { + Dispatch(source EventSource, evt *event.Event) +} + +// 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 { + // syncListeners want the whole sync response, e.g. the crypto machine + syncListeners []SyncHandler + // globalListeners want all events + globalListeners []EventHandler + // listeners want a specific event type + listeners map[event.Type][]EventHandler + // ParseEventContent determines whether or not event content should be parsed before passing to handlers. + ParseEventContent bool + // ParseErrorHandler is called when event.Content.ParseRaw returns an error. + // If it returns false, the event will not be forwarded to listeners. + ParseErrorHandler func(evt *event.Event, err error) bool + // FilterJSON is used when the client starts syncing and doesn't get an existing filter ID from SyncStore's LoadFilterID. + FilterJSON *Filter +} + +var _ Syncer = (*DefaultSyncer)(nil) +var _ ExtensibleSyncer = (*DefaultSyncer)(nil) + +// NewDefaultSyncer returns an instantiated DefaultSyncer +func NewDefaultSyncer() *DefaultSyncer { + return &DefaultSyncer{ + listeners: make(map[event.Type][]EventHandler), + syncListeners: []SyncHandler{}, + globalListeners: []EventHandler{}, + ParseEventContent: true, + ParseErrorHandler: func(evt *event.Event, err error) bool { + return false + }, + } +} + +// 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) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("ProcessResponse panicked! since=%s panic=%s\n%s", since, r, debug.Stack()) + } + }() + + for _, listener := range s.syncListeners { + if !listener(res, since) { + return + } + } + + s.processSyncEvents("", res.ToDevice.Events, EventSourceToDevice) + s.processSyncEvents("", res.Presence.Events, EventSourcePresence) + s.processSyncEvents("", res.AccountData.Events, EventSourceAccountData) + + for roomID, roomData := range res.Rooms.Join { + s.processSyncEvents(roomID, roomData.State.Events, EventSourceJoin|EventSourceState) + s.processSyncEvents(roomID, roomData.Timeline.Events, EventSourceJoin|EventSourceTimeline) + s.processSyncEvents(roomID, roomData.Ephemeral.Events, EventSourceJoin|EventSourceEphemeral) + s.processSyncEvents(roomID, roomData.AccountData.Events, EventSourceJoin|EventSourceAccountData) + } + for roomID, roomData := range res.Rooms.Invite { + s.processSyncEvents(roomID, roomData.State.Events, EventSourceInvite|EventSourceState) + } + for roomID, roomData := range res.Rooms.Leave { + s.processSyncEvents(roomID, roomData.State.Events, EventSourceLeave|EventSourceState) + s.processSyncEvents(roomID, roomData.Timeline.Events, EventSourceLeave|EventSourceTimeline) + } + return +} + +func (s *DefaultSyncer) processSyncEvents(roomID id.RoomID, events []*event.Event, source EventSource) { + for _, evt := range events { + s.processSyncEvent(roomID, evt, source) + } +} + +func (s *DefaultSyncer) processSyncEvent(roomID id.RoomID, evt *event.Event, source EventSource) { + evt.RoomID = roomID + + // Ensure the type class is correct. It's safe to mutate the class since the event type is not a pointer. + // Listeners are keyed by type structs, which means only the correct class will pass. + switch { + case evt.StateKey != nil: + evt.Type.Class = event.StateEventType + case source == EventSourcePresence, source&EventSourceEphemeral != 0: + evt.Type.Class = event.EphemeralEventType + case source&EventSourceAccountData != 0: + evt.Type.Class = event.AccountDataEventType + case source == EventSourceToDevice: + evt.Type.Class = event.ToDeviceEventType + default: + evt.Type.Class = event.MessageEventType + } + + if s.ParseEventContent { + err := evt.Content.ParseRaw(evt.Type) + if err != nil && !s.ParseErrorHandler(evt, err) { + return + } + } + + s.Dispatch(source, evt) +} + +func (s *DefaultSyncer) Dispatch(source EventSource, evt *event.Event) { + for _, fn := range s.globalListeners { + fn(source, evt) + } + listeners, exists := s.listeners[evt.Type] + if exists { + for _, fn := range listeners { + fn(source, evt) + } + } +} + +// 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 event.Type, callback EventHandler) { + _, exists := s.listeners[eventType] + if !exists { + s.listeners[eventType] = []EventHandler{} + } + s.listeners[eventType] = append(s.listeners[eventType], callback) +} + +func (s *DefaultSyncer) OnSync(callback SyncHandler) { + s.syncListeners = append(s.syncListeners, callback) +} + +func (s *DefaultSyncer) OnEvent(callback EventHandler) { + s.globalListeners = append(s.globalListeners, callback) +} + +// 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) { + if errors.Is(err, MUnknownToken) { + return 0, err + } + return 10 * time.Second, nil +} + +var defaultFilter = Filter{ + Room: RoomFilter{ + Timeline: FilterPart{ + Limit: 50, + }, + }, +} + +// GetFilterJSON returns a filter with a timeline limit of 50. +func (s *DefaultSyncer) GetFilterJSON(userID id.UserID) *Filter { + if s.FilterJSON == nil { + defaultFilterCopy := defaultFilter + s.FilterJSON = &defaultFilterCopy + } + return s.FilterJSON +} + +// OldEventIgnorer is an utility struct for bots to ignore events from before the bot joined the room. +// +// Create a struct and call Register with your DefaultSyncer to register the sync handler, e.g.: +// +// (&OldEventIgnorer{UserID: cli.UserID}).Register(cli.Syncer.(mautrix.ExtensibleSyncer)) +type OldEventIgnorer struct { + UserID id.UserID +} + +func (oei *OldEventIgnorer) Register(syncer ExtensibleSyncer) { + syncer.OnSync(oei.DontProcessOldEvents) +} + +// DontProcessOldEvents returns true if a sync response should be processed. May modify the response to remove +// stuff that shouldn't be processed. +func (oei *OldEventIgnorer) DontProcessOldEvents(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-- { + evt := roomData.Timeline.Events[i] + if evt.Type == event.StateMember && evt.GetStateKey() == string(oei.UserID) { + membership, _ := evt.Content.Raw["membership"].(string) + if membership == "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 +} diff --git a/vendor/maunium.net/go/mautrix/syncstore.go b/vendor/maunium.net/go/mautrix/syncstore.go new file mode 100644 index 00000000..a3611e5e --- /dev/null +++ b/vendor/maunium.net/go/mautrix/syncstore.go @@ -0,0 +1,149 @@ +package mautrix + +import ( + "maunium.net/go/mautrix/id" +) + +// SyncStore 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 "MemorySyncStore" which just keeps data around in-memory which is lost on +// restarts. +type SyncStore interface { + SaveFilterID(userID id.UserID, filterID string) + LoadFilterID(userID id.UserID) string + SaveNextBatch(userID id.UserID, nextBatchToken string) + LoadNextBatch(userID id.UserID) string +} + +// Deprecated: renamed to SyncStore +type Storer = SyncStore + +// MemorySyncStore 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 MemorySyncStore struct { + Filters map[id.UserID]string + NextBatch map[id.UserID]string +} + +// SaveFilterID to memory. +func (s *MemorySyncStore) SaveFilterID(userID id.UserID, filterID string) { + s.Filters[userID] = filterID +} + +// LoadFilterID from memory. +func (s *MemorySyncStore) LoadFilterID(userID id.UserID) string { + return s.Filters[userID] +} + +// SaveNextBatch to memory. +func (s *MemorySyncStore) SaveNextBatch(userID id.UserID, nextBatchToken string) { + s.NextBatch[userID] = nextBatchToken +} + +// LoadNextBatch from memory. +func (s *MemorySyncStore) LoadNextBatch(userID id.UserID) string { + return s.NextBatch[userID] +} + +// NewMemorySyncStore constructs a new MemorySyncStore. +func NewMemorySyncStore() *MemorySyncStore { + return &MemorySyncStore{ + Filters: make(map[id.UserID]string), + NextBatch: make(map[id.UserID]string), + } +} + +// AccountDataStore uses account data to store the next batch token, and stores the filter ID in memory +// (as filters can be safely recreated every startup). +type AccountDataStore struct { + FilterID string + EventType string + client *Client +} + +type accountData struct { + NextBatch string `json:"next_batch"` +} + +func (s *AccountDataStore) SaveFilterID(userID id.UserID, filterID string) { + if userID.String() != s.client.UserID.String() { + panic("AccountDataStore must only be used with a single account") + } + s.FilterID = filterID +} + +func (s *AccountDataStore) LoadFilterID(userID id.UserID) string { + if userID.String() != s.client.UserID.String() { + panic("AccountDataStore must only be used with a single account") + } + return s.FilterID +} + +func (s *AccountDataStore) SaveNextBatch(userID id.UserID, nextBatchToken string) { + if userID.String() != s.client.UserID.String() { + panic("AccountDataStore must only be used with a single account") + } + + data := accountData{ + NextBatch: nextBatchToken, + } + + err := s.client.SetAccountData(s.EventType, data) + if err != nil { + s.client.Log.Warn().Err(err).Msg("Failed to save next batch token to account data") + } +} + +func (s *AccountDataStore) LoadNextBatch(userID id.UserID) string { + if userID.String() != s.client.UserID.String() { + panic("AccountDataStore must only be used with a single account") + } + + data := &accountData{} + + err := s.client.GetAccountData(s.EventType, data) + if err != nil { + s.client.Log.Warn().Err(err).Msg("Failed to load next batch token from account data") + return "" + } + + return data.NextBatch +} + +// NewAccountDataStore returns a new AccountDataStore, which stores +// the next_batch token as a custom event in account data in the +// homeserver. +// +// AccountDataStore is only appropriate for bots, not appservices. +// +// The event type should be a reversed DNS name like tld.domain.sub.internal and +// must be unique for a client. The data stored in it is considered internal +// and must not be modified through outside means. You should also add a filter +// for account data changes of this event type, to avoid ending up in a sync +// loop: +// +// filter := mautrix.Filter{ +// AccountData: mautrix.FilterPart{ +// Limit: 20, +// NotTypes: []event.Type{ +// event.NewEventType(eventType), +// }, +// }, +// } +// // If you use a custom Syncer, set the filter there, not like this +// client.Syncer.(*mautrix.DefaultSyncer).FilterJSON = &filter +// client.Store = mautrix.NewAccountDataStore("com.example.mybot.store", client) +// go func() { +// err := client.Sync() +// // don't forget to check err +// }() +func NewAccountDataStore(eventType string, client *Client) *AccountDataStore { + return &AccountDataStore{ + EventType: eventType, + client: client, + } +} diff --git a/vendor/maunium.net/go/mautrix/url.go b/vendor/maunium.net/go/mautrix/url.go new file mode 100644 index 00000000..5ea03f1d --- /dev/null +++ b/vendor/maunium.net/go/mautrix/url.go @@ -0,0 +1,106 @@ +// Copyright (c) 2022 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package mautrix + +import ( + "fmt" + "net/url" + "strconv" + "strings" +) + +func ParseAndNormalizeBaseURL(homeserverURL string) (*url.URL, error) { + hsURL, err := url.Parse(homeserverURL) + if err != nil { + return nil, err + } + if hsURL.Scheme == "" { + hsURL.Scheme = "https" + fixedURL := hsURL.String() + hsURL, err = url.Parse(fixedURL) + if err != nil { + return nil, fmt.Errorf("failed to parse fixed URL '%s': %v", fixedURL, err) + } + } + hsURL.RawPath = hsURL.EscapedPath() + return hsURL, nil +} + +// BuildURL builds a URL with the given path parts +func BuildURL(baseURL *url.URL, path ...interface{}) *url.URL { + createdURL := *baseURL + rawParts := make([]string, len(path)+1) + rawParts[0] = strings.TrimSuffix(createdURL.RawPath, "/") + parts := make([]string, len(path)+1) + parts[0] = strings.TrimSuffix(createdURL.Path, "/") + for i, part := range path { + switch casted := part.(type) { + case string: + parts[i+1] = casted + case int: + parts[i+1] = strconv.Itoa(casted) + case fmt.Stringer: + parts[i+1] = casted.String() + default: + parts[i+1] = fmt.Sprint(casted) + } + rawParts[i+1] = url.PathEscape(parts[i+1]) + } + createdURL.Path = strings.Join(parts, "/") + createdURL.RawPath = strings.Join(rawParts, "/") + return &createdURL +} + +// BuildURL builds a URL with the Client's homeserver and appservice user ID set already. +func (cli *Client) BuildURL(urlPath PrefixableURLPath) string { + return cli.BuildURLWithQuery(urlPath, nil) +} + +// BuildClientURL builds a URL with the Client's homeserver and appservice user ID set already. +// This method also automatically prepends the client API prefix (/_matrix/client). +func (cli *Client) BuildClientURL(urlPath ...interface{}) string { + return cli.BuildURLWithQuery(ClientURLPath(urlPath), nil) +} + +type PrefixableURLPath interface { + FullPath() []interface{} +} + +type BaseURLPath []interface{} + +func (bup BaseURLPath) FullPath() []interface{} { + return bup +} + +type ClientURLPath []interface{} + +func (cup ClientURLPath) FullPath() []interface{} { + return append([]interface{}{"_matrix", "client"}, []interface{}(cup)...) +} + +type MediaURLPath []interface{} + +func (mup MediaURLPath) FullPath() []interface{} { + return append([]interface{}{"_matrix", "media"}, []interface{}(mup)...) +} + +// BuildURLWithQuery builds a URL with query parameters in addition to the Client's homeserver +// and appservice user ID set already. +func (cli *Client) BuildURLWithQuery(urlPath PrefixableURLPath, urlQuery map[string]string) string { + hsURL := *BuildURL(cli.HomeserverURL, urlPath.FullPath()...) + query := hsURL.Query() + if cli.SetAppServiceUserID { + query.Set("user_id", string(cli.UserID)) + } + if urlQuery != nil { + for k, v := range urlQuery { + query.Set(k, v) + } + } + hsURL.RawQuery = query.Encode() + return hsURL.String() +} diff --git a/vendor/maunium.net/go/mautrix/util/base58/README.md b/vendor/maunium.net/go/mautrix/util/base58/README.md new file mode 100644 index 00000000..50954507 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/util/base58/README.md @@ -0,0 +1,9 @@ +base58 +========== + +This is a copy of <https://github.com/btcsuite/btcd/tree/master/btcutil/base58>. + +## License + +Package base58 is licensed under the [copyfree](http://copyfree.org) ISC +License. diff --git a/vendor/maunium.net/go/mautrix/util/base58/alphabet.go b/vendor/maunium.net/go/mautrix/util/base58/alphabet.go new file mode 100644 index 00000000..6bb39fef --- /dev/null +++ b/vendor/maunium.net/go/mautrix/util/base58/alphabet.go @@ -0,0 +1,49 @@ +// Copyright (c) 2015 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +// AUTOGENERATED by genalphabet.go; do not edit. + +package base58 + +const ( + // alphabet is the modified base58 alphabet used by Bitcoin. + alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + + alphabetIdx0 = '1' +) + +var b58 = [256]byte{ + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 0, 1, 2, 3, 4, 5, 6, + 7, 8, 255, 255, 255, 255, 255, 255, + 255, 9, 10, 11, 12, 13, 14, 15, + 16, 255, 17, 18, 19, 20, 21, 255, + 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, 32, 255, 255, 255, 255, 255, + 255, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 255, 44, 45, 46, + 47, 48, 49, 50, 51, 52, 53, 54, + 55, 56, 57, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, +} diff --git a/vendor/maunium.net/go/mautrix/util/base58/base58.go b/vendor/maunium.net/go/mautrix/util/base58/base58.go new file mode 100644 index 00000000..8ee59567 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/util/base58/base58.go @@ -0,0 +1,138 @@ +// Copyright (c) 2013-2015 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package base58 + +import ( + "math/big" +) + +//go:generate go run genalphabet.go + +var bigRadix = [...]*big.Int{ + big.NewInt(0), + big.NewInt(58), + big.NewInt(58 * 58), + big.NewInt(58 * 58 * 58), + big.NewInt(58 * 58 * 58 * 58), + big.NewInt(58 * 58 * 58 * 58 * 58), + big.NewInt(58 * 58 * 58 * 58 * 58 * 58), + big.NewInt(58 * 58 * 58 * 58 * 58 * 58 * 58), + big.NewInt(58 * 58 * 58 * 58 * 58 * 58 * 58 * 58), + big.NewInt(58 * 58 * 58 * 58 * 58 * 58 * 58 * 58 * 58), + bigRadix10, +} + +var bigRadix10 = big.NewInt(58 * 58 * 58 * 58 * 58 * 58 * 58 * 58 * 58 * 58) // 58^10 + +// Decode decodes a modified base58 string to a byte slice. +func Decode(b string) []byte { + answer := big.NewInt(0) + scratch := new(big.Int) + + // Calculating with big.Int is slow for each iteration. + // x += b58[b[i]] * j + // j *= 58 + // + // Instead we can try to do as much calculations on int64. + // We can represent a 10 digit base58 number using an int64. + // + // Hence we'll try to convert 10, base58 digits at a time. + // The rough idea is to calculate `t`, such that: + // + // t := b58[b[i+9]] * 58^9 ... + b58[b[i+1]] * 58^1 + b58[b[i]] * 58^0 + // x *= 58^10 + // x += t + // + // Of course, in addition, we'll need to handle boundary condition when `b` is not multiple of 58^10. + // In that case we'll use the bigRadix[n] lookup for the appropriate power. + for t := b; len(t) > 0; { + n := len(t) + if n > 10 { + n = 10 + } + + total := uint64(0) + for _, v := range t[:n] { + tmp := b58[v] + if tmp == 255 { + return []byte("") + } + total = total*58 + uint64(tmp) + } + + answer.Mul(answer, bigRadix[n]) + scratch.SetUint64(total) + answer.Add(answer, scratch) + + t = t[n:] + } + + tmpval := answer.Bytes() + + var numZeros int + for numZeros = 0; numZeros < len(b); numZeros++ { + if b[numZeros] != alphabetIdx0 { + break + } + } + flen := numZeros + len(tmpval) + val := make([]byte, flen) + copy(val[numZeros:], tmpval) + + return val +} + +// Encode encodes a byte slice to a modified base58 string. +func Encode(b []byte) string { + x := new(big.Int) + x.SetBytes(b) + + // maximum length of output is log58(2^(8*len(b))) == len(b) * 8 / log(58) + maxlen := int(float64(len(b))*1.365658237309761) + 1 + answer := make([]byte, 0, maxlen) + mod := new(big.Int) + for x.Sign() > 0 { + // Calculating with big.Int is slow for each iteration. + // x, mod = x / 58, x % 58 + // + // Instead we can try to do as much calculations on int64. + // x, mod = x / 58^10, x % 58^10 + // + // Which will give us mod, which is 10 digit base58 number. + // We'll loop that 10 times to convert to the answer. + + x.DivMod(x, bigRadix10, mod) + if x.Sign() == 0 { + // When x = 0, we need to ensure we don't add any extra zeros. + m := mod.Int64() + for m > 0 { + answer = append(answer, alphabet[m%58]) + m /= 58 + } + } else { + m := mod.Int64() + for i := 0; i < 10; i++ { + answer = append(answer, alphabet[m%58]) + m /= 58 + } + } + } + + // leading zero bytes + for _, i := range b { + if i != 0 { + break + } + answer = append(answer, alphabetIdx0) + } + + // reverse + alen := len(answer) + for i := 0; i < alen/2; i++ { + answer[i], answer[alen-1-i] = answer[alen-1-i], answer[i] + } + + return string(answer) +} diff --git a/vendor/maunium.net/go/mautrix/util/base58/base58check.go b/vendor/maunium.net/go/mautrix/util/base58/base58check.go new file mode 100644 index 00000000..402c3233 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/util/base58/base58check.go @@ -0,0 +1,52 @@ +// Copyright (c) 2013-2014 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package base58 + +import ( + "crypto/sha256" + "errors" +) + +// ErrChecksum indicates that the checksum of a check-encoded string does not verify against +// the checksum. +var ErrChecksum = errors.New("checksum error") + +// ErrInvalidFormat indicates that the check-encoded string has an invalid format. +var ErrInvalidFormat = errors.New("invalid format: version and/or checksum bytes missing") + +// checksum: first four bytes of sha256^2 +func checksum(input []byte) (cksum [4]byte) { + h := sha256.Sum256(input) + h2 := sha256.Sum256(h[:]) + copy(cksum[:], h2[:4]) + return +} + +// CheckEncode prepends a version byte and appends a four byte checksum. +func CheckEncode(input []byte, version byte) string { + b := make([]byte, 0, 1+len(input)+4) + b = append(b, version) + b = append(b, input...) + cksum := checksum(b) + b = append(b, cksum[:]...) + return Encode(b) +} + +// CheckDecode decodes a string that was encoded with CheckEncode and verifies the checksum. +func CheckDecode(input string) (result []byte, version byte, err error) { + decoded := Decode(input) + if len(decoded) < 5 { + return nil, 0, ErrInvalidFormat + } + version = decoded[0] + var cksum [4]byte + copy(cksum[:], decoded[len(decoded)-4:]) + if checksum(decoded[:len(decoded)-4]) != cksum { + return nil, 0, ErrChecksum + } + payload := decoded[1 : len(decoded)-4] + result = append(result, payload...) + return +} diff --git a/vendor/maunium.net/go/mautrix/util/base58/doc.go b/vendor/maunium.net/go/mautrix/util/base58/doc.go new file mode 100644 index 00000000..d657f050 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/util/base58/doc.go @@ -0,0 +1,29 @@ +// Copyright (c) 2014 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +/* +Package base58 provides an API for working with modified base58 and Base58Check +encodings. + +# Modified Base58 Encoding + +Standard base58 encoding is similar to standard base64 encoding except, as the +name implies, it uses a 58 character alphabet which results in an alphanumeric +string and allows some characters which are problematic for humans to be +excluded. Due to this, there can be various base58 alphabets. + +The modified base58 alphabet used by Bitcoin, and hence this package, omits the +0, O, I, and l characters that look the same in many fonts and are therefore +hard to humans to distinguish. + +# Base58Check Encoding Scheme + +The Base58Check encoding scheme is primarily used for Bitcoin addresses at the +time of this writing, however it can be used to generically encode arbitrary +byte arrays into human-readable strings along with a version byte that can be +used to differentiate the same payload. For Bitcoin addresses, the extra +version is used to differentiate the network of otherwise identical public keys +which helps prevent using an address intended for one network on another. +*/ +package base58 diff --git a/vendor/maunium.net/go/mautrix/util/dualerror.go b/vendor/maunium.net/go/mautrix/util/dualerror.go new file mode 100644 index 00000000..b153d432 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/util/dualerror.go @@ -0,0 +1,33 @@ +// Copyright (c) 2022 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package util + +import ( + "errors" + "fmt" +) + +type DualError struct { + High error + Low error +} + +func NewDualError(high, low error) DualError { + return DualError{high, low} +} + +func (err DualError) Is(other error) bool { + return errors.Is(other, err.High) || errors.Is(other, err.Low) +} + +func (err DualError) Unwrap() error { + return err.Low +} + +func (err DualError) Error() string { + return fmt.Sprintf("%v: %v", err.High, err.Low) +} diff --git a/vendor/maunium.net/go/mautrix/util/gjson.go b/vendor/maunium.net/go/mautrix/util/gjson.go new file mode 100644 index 00000000..05ef8277 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/util/gjson.go @@ -0,0 +1,31 @@ +// Copyright (c) 2022 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package util + +import ( + "strings" +) + +var GJSONEscaper = strings.NewReplacer( + `\`, `\\`, + ".", `\.`, + "|", `\|`, + "#", `\#`, + "@", `\@`, + "*", `\*`, + "?", `\?`) + +func GJSONPath(path ...string) string { + var result strings.Builder + for i, part := range path { + _, _ = GJSONEscaper.WriteString(&result, part) + if i < len(path)-1 { + result.WriteRune('.') + } + } + return result.String() +} diff --git a/vendor/maunium.net/go/mautrix/util/jsontime/jsontime.go b/vendor/maunium.net/go/mautrix/util/jsontime/jsontime.go new file mode 100644 index 00000000..b3282338 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/util/jsontime/jsontime.go @@ -0,0 +1,86 @@ +// Copyright (c) 2022 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package jsontime + +import ( + "encoding/json" + "time" +) + +func UM(time time.Time) UnixMilli { + return UnixMilli{Time: time} +} + +func UMInt(ts int64) UnixMilli { + return UM(time.UnixMilli(ts)) +} + +func UnixMilliNow() UnixMilli { + return UM(time.Now()) +} + +type UnixMilli struct { + time.Time +} + +func (um UnixMilli) MarshalJSON() ([]byte, error) { + if um.IsZero() { + return []byte{'0'}, nil + } + return json.Marshal(um.UnixMilli()) +} + +func (um *UnixMilli) UnmarshalJSON(data []byte) error { + var val int64 + err := json.Unmarshal(data, &val) + if err != nil { + return err + } + if val == 0 { + um.Time = time.Time{} + } else { + um.Time = time.UnixMilli(val) + } + return nil +} + +func U(time time.Time) Unix { + return Unix{Time: time} +} + +func UInt(ts int64) Unix { + return U(time.Unix(ts, 0)) +} + +func UnixNow() Unix { + return U(time.Now()) +} + +type Unix struct { + time.Time +} + +func (u Unix) MarshalJSON() ([]byte, error) { + if u.IsZero() { + return []byte{'0'}, nil + } + return json.Marshal(u.Unix()) +} + +func (u *Unix) UnmarshalJSON(data []byte) error { + var val int64 + err := json.Unmarshal(data, &val) + if err != nil { + return err + } + if val == 0 { + u.Time = time.Time{} + } else { + u.Time = time.Unix(val, 0) + } + return nil +} diff --git a/vendor/maunium.net/go/mautrix/util/marshal.go b/vendor/maunium.net/go/mautrix/util/marshal.go new file mode 100644 index 00000000..180adef2 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/util/marshal.go @@ -0,0 +1,30 @@ +package util + +import ( + "encoding/json" + "fmt" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +// MarshalAndDeleteEmpty marshals a JSON object, then uses gjson to delete empty objects at the given gjson paths. +// +// This can be used as a convenient way to create a marshaler that omits empty non-pointer structs. +// See mautrix.RespSync for example. +func MarshalAndDeleteEmpty(marshalable interface{}, paths []string) ([]byte, error) { + data, err := json.Marshal(marshalable) + if err != nil { + return nil, err + } + for _, path := range paths { + res := gjson.GetBytes(data, path) + if res.IsObject() && len(res.Raw) == 2 { + data, err = sjson.DeleteBytes(data, path) + if err != nil { + return nil, fmt.Errorf("failed to delete empty %s: %w", path, err) + } + } + } + return data, nil +} diff --git a/vendor/maunium.net/go/mautrix/util/mimetypes.go b/vendor/maunium.net/go/mautrix/util/mimetypes.go new file mode 100644 index 00000000..9877c3d3 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/util/mimetypes.go @@ -0,0 +1,50 @@ +// Copyright (c) 2022 Sumner Evans +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package util + +import ( + "mime" + "strings" +) + +// MimeExtensionSanityOverrides includes extensions for various common mimetypes. +// +// This is necessary because sometimes the OS mimetype database and Go interact in weird ways, +// which causes very obscure extensions to be first in the array for common mimetypes +// (e.g. image/jpeg -> .jpe, text/plain -> ,v). +var MimeExtensionSanityOverrides = map[string]string{ + "image/png": ".png", + "image/webp": ".webp", + "image/jpeg": ".jpg", + "image/tiff": ".tiff", + "image/heif": ".heic", + "image/heic": ".heic", + + "audio/mpeg": ".mp3", + "audio/ogg": ".ogg", + "audio/webm": ".webm", + "audio/x-caf": ".caf", + "video/mp4": ".mp4", + "video/mpeg": ".mpeg", + "video/webm": ".webm", + + "text/plain": ".txt", + "text/html": ".html", + + "application/xml": ".xml", +} + +func ExtensionFromMimetype(mimetype string) string { + ext, ok := MimeExtensionSanityOverrides[strings.Split(mimetype, ";")[0]] + if !ok { + exts, _ := mime.ExtensionsByType(mimetype) + if len(exts) > 0 { + ext = exts[0] + } + } + return ext +} diff --git a/vendor/maunium.net/go/mautrix/util/random.go b/vendor/maunium.net/go/mautrix/util/random.go new file mode 100644 index 00000000..944dd034 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/util/random.go @@ -0,0 +1,65 @@ +// Copyright (c) 2022 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package util + +import ( + "crypto/rand" + "encoding/base64" + "hash/crc32" + "strings" + "unsafe" +) + +func RandomBytes(n int) []byte { + data := make([]byte, n) + _, err := rand.Read(data) + if err != nil { + panic(err) + } + return data +} + +var letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + +// RandomString generates a random string of the given length. +func RandomString(n int) string { + if n <= 0 { + return "" + } + base64Len := n + if n%4 != 0 { + base64Len += 4 - (n % 4) + } + decodedLength := base64.RawStdEncoding.DecodedLen(base64Len) + output := make([]byte, base64Len) + base64.RawStdEncoding.Encode(output, RandomBytes(decodedLength)) + for i, char := range output { + if char == '+' || char == '/' { + _, err := rand.Read(output[i : i+1]) + if err != nil { + panic(err) + } + output[i] = letters[int(output[i])%len(letters)] + } + } + return (*(*string)(unsafe.Pointer(&output)))[:n] +} + +func base62Encode(val uint32, minWidth int) string { + var buf strings.Builder + for val > 0 { + buf.WriteByte(letters[val%62]) + val /= 62 + } + return strings.Repeat("0", minWidth-buf.Len()) + buf.String() +} + +func RandomToken(namespace string, randomLength int) string { + token := namespace + "_" + RandomString(randomLength) + checksum := base62Encode(crc32.ChecksumIEEE([]byte(token)), 6) + return token + "_" + checksum +} diff --git a/vendor/maunium.net/go/mautrix/util/returnonce.go b/vendor/maunium.net/go/mautrix/util/returnonce.go new file mode 100644 index 00000000..c85aae0a --- /dev/null +++ b/vendor/maunium.net/go/mautrix/util/returnonce.go @@ -0,0 +1,23 @@ +// Copyright (c) 2023 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package util + +import "sync" + +// ReturnableOnce is a wrapper for sync.Once that can return a value +type ReturnableOnce[Value any] struct { + once sync.Once + output Value + err error +} + +func (ronce *ReturnableOnce[Value]) Do(fn func() (Value, error)) (Value, error) { + ronce.once.Do(func() { + ronce.output, ronce.err = fn() + }) + return ronce.output, ronce.err +} diff --git a/vendor/maunium.net/go/mautrix/util/ringbuffer.go b/vendor/maunium.net/go/mautrix/util/ringbuffer.go new file mode 100644 index 00000000..ddb4c79f --- /dev/null +++ b/vendor/maunium.net/go/mautrix/util/ringbuffer.go @@ -0,0 +1,77 @@ +// Copyright (c) 2023 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package util + +import ( + "sync" +) + +type pair[Key comparable, Value any] struct { + Key Key + Value Value +} + +type RingBuffer[Key comparable, Value any] struct { + ptr int + data []pair[Key, Value] + lock sync.RWMutex +} + +func NewRingBuffer[Key comparable, Value any](size int) *RingBuffer[Key, Value] { + return &RingBuffer[Key, Value]{ + data: make([]pair[Key, Value], size), + } +} + +func (rb *RingBuffer[Key, Value]) Contains(val Key) bool { + _, ok := rb.Get(val) + return ok +} + +func (rb *RingBuffer[Key, Value]) Get(key Key) (val Value, found bool) { + rb.lock.RLock() + end := rb.ptr + for i := clamp(end-1, len(rb.data)); i != end; i = clamp(i-1, len(rb.data)) { + if rb.data[i].Key == key { + val = rb.data[i].Value + found = true + break + } + } + rb.lock.RUnlock() + return +} + +func (rb *RingBuffer[Key, Value]) Replace(key Key, val Value) bool { + rb.lock.Lock() + defer rb.lock.Unlock() + end := rb.ptr + for i := clamp(end-1, len(rb.data)); i != end; i = clamp(i-1, len(rb.data)) { + if rb.data[i].Key == key { + rb.data[i].Value = val + return true + } + } + return false +} + +func (rb *RingBuffer[Key, Value]) Push(key Key, val Value) { + rb.lock.Lock() + rb.data[rb.ptr] = pair[Key, Value]{Key: key, Value: val} + rb.ptr = (rb.ptr + 1) % len(rb.data) + rb.lock.Unlock() +} + +func clamp(index, len int) int { + if index < 0 { + return len + index + } else if index >= len { + return len - index + } else { + return index + } +} diff --git a/vendor/maunium.net/go/mautrix/util/syncmap.go b/vendor/maunium.net/go/mautrix/util/syncmap.go new file mode 100644 index 00000000..a6b20d3b --- /dev/null +++ b/vendor/maunium.net/go/mautrix/util/syncmap.go @@ -0,0 +1,94 @@ +// Copyright (c) 2023 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package util + +import "sync" + +// SyncMap is a simple map with a built-in mutex. +type SyncMap[Key comparable, Value any] struct { + data map[Key]Value + lock sync.RWMutex +} + +func NewSyncMap[Key comparable, Value any]() *SyncMap[Key, Value] { + return &SyncMap[Key, Value]{ + data: make(map[Key]Value), + } +} + +// Set stores a value in the map. +func (sm *SyncMap[Key, Value]) Set(key Key, value Value) { + sm.Swap(key, value) +} + +// Swap sets a value in the map and returns the old value. +// +// The boolean return parameter is true if the value already existed, false if not. +func (sm *SyncMap[Key, Value]) Swap(key Key, value Value) (oldValue Value, wasReplaced bool) { + sm.lock.Lock() + oldValue, wasReplaced = sm.data[key] + sm.data[key] = value + sm.lock.Unlock() + return +} + +// Delete removes a key from the map. +func (sm *SyncMap[Key, Value]) Delete(key Key) { + sm.Pop(key) +} + +// Pop removes a key from the map and returns the old value. +// +// The boolean return parameter is the same as with normal Go map access (true if the key exists, false if not). +func (sm *SyncMap[Key, Value]) Pop(key Key) (value Value, ok bool) { + sm.lock.Lock() + value, ok = sm.data[key] + delete(sm.data, key) + sm.lock.Unlock() + return +} + +// Get gets a value in the map. +// +// The boolean return parameter is the same as with normal Go map access (true if the key exists, false if not). +func (sm *SyncMap[Key, Value]) Get(key Key) (value Value, ok bool) { + sm.lock.RLock() + value, ok = sm.data[key] + sm.lock.RUnlock() + return +} + +// GetOrSet gets a value in the map if the key already exists, otherwise inserts the given value and returns it. +// +// The boolean return parameter is true if the key already exists, and false if the given value was inserted. +func (sm *SyncMap[Key, Value]) GetOrSet(key Key, value Value) (actual Value, wasGet bool) { + sm.lock.Lock() + defer sm.lock.Unlock() + actual, wasGet = sm.data[key] + if wasGet { + return + } + sm.data[key] = value + actual = value + return +} + +// Clone returns a copy of the map. +func (sm *SyncMap[Key, Value]) Clone() *SyncMap[Key, Value] { + return &SyncMap[Key, Value]{data: sm.CopyData()} +} + +// CopyData returns a copy of the data in the map as a normal (non-atomic) map. +func (sm *SyncMap[Key, Value]) CopyData() map[Key]Value { + sm.lock.RLock() + copied := make(map[Key]Value, len(sm.data)) + for key, value := range sm.data { + copied[key] = value + } + sm.lock.RUnlock() + return copied +} diff --git a/vendor/maunium.net/go/mautrix/version.go b/vendor/maunium.net/go/mautrix/version.go new file mode 100644 index 00000000..8d3c7c01 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/version.go @@ -0,0 +1,5 @@ +package mautrix + +const Version = "v0.15.0" + +var DefaultUserAgent = "mautrix-go/" + Version diff --git a/vendor/maunium.net/go/mautrix/versions.go b/vendor/maunium.net/go/mautrix/versions.go new file mode 100644 index 00000000..1eb406c0 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/versions.go @@ -0,0 +1,153 @@ +// Copyright (c) 2022 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package mautrix + +import ( + "fmt" + "regexp" + "strconv" +) + +// RespVersions is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientversions +type RespVersions struct { + Versions []SpecVersion `json:"versions"` + UnstableFeatures map[string]bool `json:"unstable_features"` +} + +func (versions *RespVersions) ContainsFunc(match func(found SpecVersion) bool) bool { + for _, found := range versions.Versions { + if match(found) { + return true + } + } + return false +} + +func (versions *RespVersions) Contains(version SpecVersion) bool { + return versions.ContainsFunc(func(found SpecVersion) bool { + return found == version + }) +} + +func (versions *RespVersions) ContainsGreaterOrEqual(version SpecVersion) bool { + return versions.ContainsFunc(func(found SpecVersion) bool { + return found.GreaterThan(version) || found == version + }) +} + +func (versions *RespVersions) GetLatest() (latest SpecVersion) { + for _, ver := range versions.Versions { + if ver.GreaterThan(latest) { + latest = ver + } + } + return +} + +type SpecVersionFormat int + +const ( + SpecVersionFormatUnknown SpecVersionFormat = iota + SpecVersionFormatR + SpecVersionFormatV +) + +var ( + SpecR000 = MustParseSpecVersion("r0.0.0") + SpecR001 = MustParseSpecVersion("r0.0.1") + SpecR010 = MustParseSpecVersion("r0.1.0") + SpecR020 = MustParseSpecVersion("r0.2.0") + SpecR030 = MustParseSpecVersion("r0.3.0") + SpecR040 = MustParseSpecVersion("r0.4.0") + SpecR050 = MustParseSpecVersion("r0.5.0") + SpecR060 = MustParseSpecVersion("r0.6.0") + SpecR061 = MustParseSpecVersion("r0.6.1") + SpecV11 = MustParseSpecVersion("v1.1") + SpecV12 = MustParseSpecVersion("v1.2") + SpecV13 = MustParseSpecVersion("v1.3") + SpecV14 = MustParseSpecVersion("v1.4") + SpecV15 = MustParseSpecVersion("v1.5") +) + +func (svf SpecVersionFormat) String() string { + switch svf { + case SpecVersionFormatR: + return "r" + case SpecVersionFormatV: + return "v" + default: + return "" + } +} + +type SpecVersion struct { + Format SpecVersionFormat + Major int + Minor int + Patch int + + Raw string +} + +var legacyVersionRegex = regexp.MustCompile(`^r(\d+)\.(\d+)\.(\d+)$`) +var modernVersionRegex = regexp.MustCompile(`^v(\d+)\.(\d+)$`) + +func MustParseSpecVersion(version string) SpecVersion { + sv, err := ParseSpecVersion(version) + if err != nil { + panic(err) + } + return sv +} + +func ParseSpecVersion(version string) (sv SpecVersion, err error) { + sv.Raw = version + if parts := modernVersionRegex.FindStringSubmatch(version); parts != nil { + sv.Major, _ = strconv.Atoi(parts[1]) + sv.Minor, _ = strconv.Atoi(parts[2]) + sv.Format = SpecVersionFormatV + } else if parts = legacyVersionRegex.FindStringSubmatch(version); parts != nil { + sv.Major, _ = strconv.Atoi(parts[1]) + sv.Minor, _ = strconv.Atoi(parts[2]) + sv.Patch, _ = strconv.Atoi(parts[3]) + sv.Format = SpecVersionFormatR + } else { + err = fmt.Errorf("version '%s' doesn't match either known syntax", version) + } + return +} + +func (sv *SpecVersion) UnmarshalText(version []byte) error { + *sv, _ = ParseSpecVersion(string(version)) + return nil +} + +func (sv *SpecVersion) MarshalText() ([]byte, error) { + return []byte(sv.String()), nil +} + +func (sv SpecVersion) String() string { + switch sv.Format { + case SpecVersionFormatR: + return fmt.Sprintf("r%d.%d.%d", sv.Major, sv.Minor, sv.Patch) + case SpecVersionFormatV: + return fmt.Sprintf("v%d.%d", sv.Major, sv.Minor) + default: + return sv.Raw + } +} + +func (sv SpecVersion) LessThan(other SpecVersion) bool { + return sv != other && !sv.GreaterThan(other) +} + +func (sv SpecVersion) GreaterThan(other SpecVersion) bool { + return sv.Format > other.Format || + (sv.Format == other.Format && sv.Major > other.Major) || + (sv.Format == other.Format && sv.Major == other.Major && sv.Minor > other.Minor) || + (sv.Format == other.Format && sv.Major == other.Major && sv.Minor == other.Minor && sv.Patch > other.Patch) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index ce2bdc1c..49e04fa4 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -104,6 +104,9 @@ github.com/google/uuid # github.com/gopackage/ddp v0.0.3 ## explicit; go 1.17 github.com/gopackage/ddp +# github.com/gorilla/mux v1.8.0 +## explicit; go 1.12 +github.com/gorilla/mux # github.com/gorilla/schema v1.2.0 ## explicit github.com/gorilla/schema @@ -211,9 +214,6 @@ github.com/matterbridge/Rocket.Chat.Go.SDK/rest # github.com/matterbridge/go-xmpp v0.0.0-20211030125215-791a06c5f1be ## explicit github.com/matterbridge/go-xmpp -# github.com/matterbridge/gomatrix v0.0.0-20220411225302-271e5088ea27 -## explicit; go 1.17 -github.com/matterbridge/gomatrix # github.com/matterbridge/gozulipbot v0.0.0-20211023205727-a19d6c1f3b75 ## explicit github.com/matterbridge/gozulipbot @@ -354,6 +354,11 @@ github.com/rivo/uniseg # github.com/rs/xid v1.5.0 ## explicit; go 1.12 github.com/rs/xid +# github.com/rs/zerolog v1.29.0 +## explicit; go 1.15 +github.com/rs/zerolog +github.com/rs/zerolog/internal/cbor +github.com/rs/zerolog/internal/json # github.com/russross/blackfriday v1.6.0 ## explicit; go 1.13 github.com/russross/blackfriday @@ -419,6 +424,18 @@ github.com/stretchr/testify/suite # github.com/subosito/gotenv v1.4.2 ## explicit; go 1.18 github.com/subosito/gotenv +# github.com/tidwall/gjson v1.14.4 +## explicit; go 1.12 +github.com/tidwall/gjson +# github.com/tidwall/match v1.1.1 +## explicit; go 1.15 +github.com/tidwall/match +# github.com/tidwall/pretty v1.2.0 +## explicit; go 1.16 +github.com/tidwall/pretty +# github.com/tidwall/sjson v1.2.5 +## explicit; go 1.14 +github.com/tidwall/sjson # github.com/tinylib/msgp v1.1.6 ## explicit; go 1.14 github.com/tinylib/msgp/msgp @@ -652,8 +669,8 @@ google.golang.org/protobuf/types/descriptorpb # gopkg.in/ini.v1 v1.67.0 ## explicit gopkg.in/ini.v1 -# gopkg.in/natefinch/lumberjack.v2 v2.0.0 -## explicit +# gopkg.in/natefinch/lumberjack.v2 v2.2.1 +## explicit; go 1.13 gopkg.in/natefinch/lumberjack.v2 # gopkg.in/yaml.v2 v2.4.0 ## explicit; go 1.15 @@ -670,6 +687,23 @@ layeh.com/gumble/gumbleutil # lukechampine.com/uint128 v1.2.0 ## explicit; go 1.12 lukechampine.com/uint128 +# maunium.net/go/maulogger/v2 v2.4.1 +## explicit; go 1.19 +maunium.net/go/maulogger/v2 +maunium.net/go/maulogger/v2/maulogadapt +# maunium.net/go/mautrix v0.15.0 +## explicit; go 1.19 +maunium.net/go/mautrix +maunium.net/go/mautrix/appservice +maunium.net/go/mautrix/crypto/attachment +maunium.net/go/mautrix/crypto/utils +maunium.net/go/mautrix/event +maunium.net/go/mautrix/id +maunium.net/go/mautrix/pushrules +maunium.net/go/mautrix/pushrules/glob +maunium.net/go/mautrix/util +maunium.net/go/mautrix/util/base58 +maunium.net/go/mautrix/util/jsontime # modernc.org/cc/v3 v3.40.0 ## explicit; go 1.17 modernc.org/cc/v3 |