~f4814n/frost

7a47e8cf53fc52461378a813264d27a34cff2d19 — Fabian Geiselhart 11 months ago 54bdde1
Split matrix library
40 files changed, 51 insertions(+), 2789 deletions(-)

M README.md
M cmd/frost/list_view.go
A cmd/frost/log.go
M cmd/frost/main.go
M cmd/frost/merge_view.go
M cmd/frost/platform_js.go
M cmd/frost/platform_linux.go
M cmd/frost/room_view.go
M cmd/frost/util.go
M flake.nix
M go.mod
M go.sum
D matrix/.build.yml
D matrix/.golangci.toml
D matrix/api/client.go
D matrix/api/endpoints.go
D matrix/backend.go
D matrix/backend/memory/backend.go
D matrix/backend/memory/backend_test.go
D matrix/backend/sqlite/backend.go
D matrix/backend/sqlite/backend_test.go
D matrix/backend/test.go
D matrix/client.go
D matrix/doc.go
D matrix/errors.go
D matrix/event.go
D matrix/examples/echo/main.go
D matrix/examples/history/main.go
D matrix/examples/login/main.go
D matrix/examples/read_room/main.go
D matrix/examples/simple_notify/main.go
D matrix/examples/util.go
D matrix/history.go
D matrix/homeserver.go
D matrix/membershipstate_string.go
D matrix/mxid.go
D matrix/notifier.go
D matrix/room.go
D matrix/user.go
D matrix/util.go
M README.md => README.md +1 -1
@@ 3,7 3,7 @@
[![builds.sr.ht status](https://builds.sr.ht/~f4814n/frost.svg)](https://builds.sr.ht/~f4814n/frost?)

Frost is a (pre-alpha) matrix client written in Golang using the [gio](https://gioui.org)
library and my [matrix library](https://git.sr.ht/~f4814n/frost/matrix).
library and my [matrix library](https://git.sr.ht/~f4814n/matrix).


There is no bugtracker or mailing list because I do not think anyone is interested

M cmd/frost/list_view.go => cmd/frost/list_view.go +1 -1
@@ 15,7 15,7 @@ import (
	"gioui.org/unit"
	"gioui.org/widget"
	"gioui.org/widget/material"
	"git.sr.ht/~f4814n/frost/matrix"
	"git.sr.ht/~f4814n/matrix"
)

type ListView struct {

A cmd/frost/log.go => cmd/frost/log.go +14 -0
@@ 0,0 1,14 @@
package main

import (
	"github.com/sirupsen/logrus"
	"git.sr.ht/~f4814n/matrix"
)

type MatrixLogger struct {
	*logrus.Entry
}

func (m MatrixLogger) WithField(key string, value interface{}) matrix.Logger {
	return MatrixLogger{m.Entry.WithField(key, value)}
}

M cmd/frost/main.go => cmd/frost/main.go +3 -6
@@ 11,7 11,7 @@ import (
	"gioui.org/op"
	"gioui.org/unit"
	"gioui.org/widget/material"
	"git.sr.ht/~f4814n/frost/matrix"
	"git.sr.ht/~f4814n/matrix"
	"github.com/sirupsen/logrus"
)



@@ 54,6 54,7 @@ func newApp() *App {
		cli: matrix.NewClient(matrix.ClientOpts{
			HTTPClient: http.DefaultClient,
			Backend: platform.NewBackend(),
			Logger: MatrixLogger{logrus.WithField("component", "matrix")},
		}),
		window: app.NewWindow(
			app.Size(unit.Dp(400), unit.Dp(800)),


@@ 172,11 173,7 @@ func (a *App) sync() {
	go func() {
		c := make(chan matrix.Event, 100)
		a.cli.Notify(c)
		for event := range c {
			logrus.WithFields(logrus.Fields{
				"component": "matrix",
				"event": event,
			}).Trace("Received event")
		for range c {
			a.rx <- InvalidationEvent{}
		}
	}()

M cmd/frost/merge_view.go => cmd/frost/merge_view.go +2 -2
@@ 5,7 5,7 @@ import (

	"gioui.org/layout"
	"gioui.org/unit"
	"git.sr.ht/~f4814n/frost/matrix"
	"git.sr.ht/~f4814n/matrix"
	"github.com/sirupsen/logrus"
)



@@ 137,7 137,7 @@ func (o *MergeView) Layout(gtx Gtx) Dims {
					gtx.Constraints.Max.X = rightsize
					return o.right.Layout(gtx)
				}
				return NoView{}.Layout(gtx)
				return Dims{}
			}),
		)
	}

M cmd/frost/platform_js.go => cmd/frost/platform_js.go +2 -2
@@ 1,9 1,9 @@
package main

import (
	"git.sr.ht/~f4814n/frost/matrix"
	"git.sr.ht/~f4814n/matrix"
	"github.com/sirupsen/logrus"
	backend "git.sr.ht/~f4814n/frost/matrix/backend/memory"
	backend "git.sr.ht/~f4814n/matrix/backend/memory"
)

type Platform struct {

M cmd/frost/platform_linux.go => cmd/frost/platform_linux.go +2 -2
@@ 4,8 4,8 @@ import (
	"os"
	"fmt"
	"encoding/json"
	"git.sr.ht/~f4814n/frost/matrix"
	backend "git.sr.ht/~f4814n/frost/matrix/backend/sqlite"
	"git.sr.ht/~f4814n/matrix"
	backend "git.sr.ht/~f4814n/matrix/backend/sqlite"
)

var (

M cmd/frost/room_view.go => cmd/frost/room_view.go +1 -1
@@ 11,7 11,7 @@ import (
	"gioui.org/unit"
	"gioui.org/widget"
	"gioui.org/widget/material"
	"git.sr.ht/~f4814n/frost/matrix"
	"git.sr.ht/~f4814n/matrix"
)

type SentRoomEvent struct {

M cmd/frost/util.go => cmd/frost/util.go +1 -1
@@ 12,7 12,7 @@ import (
	"gioui.org/op/paint"
	"gioui.org/unit"
	"gioui.org/widget"
	"git.sr.ht/~f4814n/frost/matrix"
	"git.sr.ht/~f4814n/matrix"
	"github.com/sirupsen/logrus"
)


M flake.nix => flake.nix +1 -1
@@ 18,7 18,7 @@
            nixfmt
            go
            goimports
            gopls
            # gopls
            golangci-lint
            delve
            pkgconfig

M go.mod => go.mod +6 -18
@@ 1,23 1,11 @@
module git.sr.ht/~f4814n/frost

go 1.15
go 1.14

require (
	gioui.org v0.0.0-20201005075949-29740ba59343
	gioui.org/cmd v0.0.0-20201005075949-29740ba59343
	github.com/jmoiron/sqlx v1.2.0
	github.com/kr/text v0.2.0 // indirect
	github.com/mailru/easyjson v0.7.1 // indirect
	github.com/mattn/go-sqlite3 v1.14.3
	github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
	github.com/sirupsen/logrus v1.6.0
	github.com/stretchr/testify v1.6.1 // indirect
	golang.org/x/exp v0.0.0-20200917184745-18d7dbdd5567
	golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 // indirect
	golang.org/x/text v0.3.3 // indirect
	golang.org/x/tools v0.0.0-20200825202427-b303f430e36d // indirect
	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
	google.golang.org/appengine v1.6.6 // indirect
	gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
	gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
	gioui.org v0.0.0-20201018162216-7a4b48f67b54
	gioui.org/cmd v0.0.0-20201020094634-d5bdf0756a5a
	git.sr.ht/~f4814n/matrix v0.0.0-20201020131141-0d3825f68665
	github.com/sirupsen/logrus v1.7.0
	golang.org/x/exp v0.0.0-20201008143054-e3b2a7f2fdc7
)

M go.sum => go.sum +17 -50
@@ 1,16 1,16 @@
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
gioui.org v0.0.0-20200726090130-3b95e2918359/go.mod h1:jiUwifN9cRl/zmco43aAqh0aV+s9GbhG13KcD+gEpkU=
gioui.org v0.0.0-20201005075949-29740ba59343 h1:+BLxwayMrPRWI6gXFLaEMHJzSql/RXmSBUa4FHRenmQ=
gioui.org v0.0.0-20201005075949-29740ba59343/go.mod h1:Y+uS7hHMvku1Q+ooaoq6fYD5B2LGoT8JtFgvmYmRzTw=
gioui.org/cmd v0.0.0-20201005075949-29740ba59343 h1:R3WxtNv4CQ4vhb6dqSfNtTdrZjnNlXfprG0/fNR5d8I=
gioui.org/cmd v0.0.0-20201005075949-29740ba59343/go.mod h1:dlmJnCEkOpRaChYxRmJZ5S4jk6y7DCfWnec39xGbUYk=
gioui.org v0.0.0-20201018162216-7a4b48f67b54 h1:mM+tSH6C2GBkaErKviXhGL22qiGNgUMutvOYMyAsBK8=
gioui.org v0.0.0-20201018162216-7a4b48f67b54/go.mod h1:Y+uS7hHMvku1Q+ooaoq6fYD5B2LGoT8JtFgvmYmRzTw=
gioui.org/cmd v0.0.0-20201020094634-d5bdf0756a5a h1:BPgJqeQSxuX6CzFQ2EGDj4Kr2ZzwiqwdLyAYLXgxR+k=
gioui.org/cmd v0.0.0-20201020094634-d5bdf0756a5a/go.mod h1:dlmJnCEkOpRaChYxRmJZ5S4jk6y7DCfWnec39xGbUYk=
git.sr.ht/~f4814n/matrix v0.0.0-20201020131141-0d3825f68665 h1:sKKmeV65ABVg8wQR/c9nrKPEXg6/ReLfXrmDpkbLKyM=
git.sr.ht/~f4814n/matrix v0.0.0-20201020131141-0d3825f68665/go.mod h1:LinuYhN8Rh6XQ9Cy+XM1hTTZT6zDs6ONBTwXAoWCjI4=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4 h1:QD3KxSJ59L2lxG6MXBjNHxiQO2RmxTQ3XcK+wO44WOg=
github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g=
github.com/chromedp/chromedp v0.5.2 h1:W8xBXQuUnd2dZK0SN/lyVwsQM7KgW+kY5HGnntms194=
github.com/chromedp/chromedp v0.5.2/go.mod h1:rsTo/xRo23KZZwFmWk2Ui79rBaVRRATCjLzNQlOFSiA=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=


@@ 28,40 28,26 @@ github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08 h1:V0an7KRw92wmJysvFvtqtKMAPmvS5O0jtB0nYo6t+gs=
github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08/go.mod h1:dFWs1zEqDjFtnBXsd1vPOZaLsESovai349994nHx3e0=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/mailru/easyjson v0.7.1 h1:mdxE1MF9o53iCb2Ghj1VfWvh7ZOwHpnVG/xwXrV90U8=
github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.3 h1:j7a/xn1U6TKA/PHHxqZuzh64CdtRc7rU9M+AvkOl5bA=
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI=
github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE=
golang.org/x/exp v0.0.0-20200917184745-18d7dbdd5567 h1:HfuaahSgKLEovs8K+Sm4QyZnRoDdGeGx+PrVlOVRtIk=
golang.org/x/exp v0.0.0-20200917184745-18d7dbdd5567/go.mod h1:1phAWC201xIgDyaFpmDeZkgf70Q4Pd/CNqfRtVPtxNw=
golang.org/x/exp v0.0.0-20201008143054-e3b2a7f2fdc7 h1:2/QncOxxpPAdiH+E00abYw/SaQG353gltz79Nl1zrYE=
golang.org/x/exp v0.0.0-20201008143054-e3b2a7f2fdc7/go.mod h1:1phAWC201xIgDyaFpmDeZkgf70Q4Pd/CNqfRtVPtxNw=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200618115811-c13761719519 h1:1e2ufUJNM3lCHEY5jIgac/7UTjd6cgJNdatjPdFWf34=


@@ 69,51 55,32 @@ golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+o
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449 h1:xUIPaMhvROX9dhPvRCenIJtU78+lbEenGbgqB5hfHCQ=
golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 h1:1/DFK4b7JH8DmkqhUk48onnSfrPzImPoVxuomtbT2nk=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa h1:5E4dL8+NgFOgjwbTKz+OOEGGhP+ectTmF842l6KjupQ=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d h1:W07d4xkoAUSNOkOzdzXCdFGxT7o2rW4q8M34tB2i//k=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=

D matrix/.build.yml => matrix/.build.yml +0 -20
@@ 1,20 0,0 @@
image: alpine/edge
packages:
  - go
sources:
  - https://git.sr.ht/~f4814n/matrix
secrets:
   - 1d0e497b-270b-4a32-b9ff-f7886599c4e6
tasks:
  - build: |
      cd matrix
      go build ./...

  - test: |
      cd matrix
      go test ./...

  - annotate: |
      cd matrix
      go run git.sr.ht/~sircmpwn/annotatego -Tv ./... > annotations.json
      ../upload-annotations annotations.json f4814n matrix

D matrix/.golangci.toml => matrix/.golangci.toml +0 -2
@@ 1,2 0,0 @@
[linters]
enable = [ "gocyclo", "gofmt", "goimports", "golint", "gosec", "misspell", "stylecheck" ]

D matrix/api/client.go => matrix/api/client.go +0 -92
@@ 1,92 0,0 @@
package api

import (
	"bytes"
	"encoding/json"
	"net/http"
	"net/url"
)

const (
	prefix = "/_matrix/client/r0/"
)

type Auth func(*http.Request)

type Client struct {
	homeserver string
	cli        *http.Client
}

func NewClient(client *http.Client, homeserver string) *Client {
	return &Client{
		homeserver: homeserver,
		cli:        client,
	}
}

type MatrixError struct {
	Code string `json:"errcode"`
	Err  string `json:"error"`
}

func (m MatrixError) Error() string {
	return m.Code + " " + m.Err
}

func (cli *Client) makeRequest(method string, path *url.URL, auth Auth, body interface{}, resp interface{}) error {
	var buf bytes.Buffer
	err := json.NewEncoder(&buf).Encode(body)
	if err != nil {
		return err
	}

	req, err := http.NewRequest(method, path.String(), &buf)
	if err != nil {
		return err
	}

	if auth != nil {
		auth(req)
	}

	httpResp, err := cli.cli.Do(req)
	if err != nil {
		return err
	}
	defer httpResp.Body.Close()

	dec := json.NewDecoder(httpResp.Body)
	if httpResp.StatusCode != 200 {
		var merr MatrixError
		err := dec.Decode(&merr)
		if err != nil {
			return err
		}
		return merr
	}

	if resp == nil {
		return nil
	}

	return dec.Decode(resp)
}

func (cli *Client) makeURL(path string) *url.URL {
	url, err := url.Parse("https://" + cli.homeserver + prefix + path)
	if err != nil {
		panic(err)
	}

	return url
}

func (cli *Client) makeURLRaw(path string) *url.URL {
	url, err := url.Parse("https://" + cli.homeserver + path)
	if err != nil {
		panic(err)
	}

	return url
}

D matrix/api/endpoints.go => matrix/api/endpoints.go +0 -346
@@ 1,346 0,0 @@
package api

import (
	"net/url"
	"strconv"
)

// 2.1
type VersionsResp struct {
	Versions         []string        `json:"versions"`
	UnstableFeatures map[string]bool `json:"unstable_features,omitempty"`
}

func (cli *Client) Versions() (VersionsResp, error) {
	var resp VersionsResp
	err := cli.makeRequest("GET", cli.makeURLRaw("/_matrix/client/versions"), nil, nil, &resp)
	return resp, err
}

// 5.3.6
type Identifier map[string]interface{}

func NewUserIdentifer(username string) Identifier {
	return map[string]interface{}{
		"type": "m.id.user",
		"user": username,
	}
}

func New3rdpartyIdentifer(medium, address string) Identifier {
	return map[string]interface{}{
		"type":    "m.id.thirdparty",
		"medium":  medium,
		"address": address,
	}
}

func NewPhoneIdentifer(country, phone string) Identifier {
	return map[string]interface{}{
		"type":    "m.id.phone",
		"country": country,
		"phone":   phone,
	}
}

// 5.4.2
type LoginBody struct {
	Type                     string     `json:"type"`
	Identifier               Identifier `json:"identifier,omitempty"`
	User                     string     `json:"user,omitempty"`
	Medium                   string     `json:"medium,omitempty"`
	Address                  string     `json:"address,omitempty"`
	Password                 string     `json:"password,omitempty"`
	Token                    string     `json:"token,omitempty"`
	DeviceID                 string     `json:"device_id,omitempty"`
	InitialDeviceDisplayName string     `json:"initial_device_display_name,omitempty"`
}

type LoginResp struct {
	UserID      string `json:"user_id"`
	AccessToken string `json:"access_token"`
	HomeServer  string `json:"home_server"`
	DeviceID    string `json:"device_id"`
	WellKnown   struct {
		Homeserver struct {
			BaseURL string `json:"base_url"`
		} `json:"m.homeserver"`
		IdentityServer struct {
			BaseURL string `json:"base_url"`
		} `json:"m.identity_server"`
	} `json:"well_known"`
}

func (cli *Client) Login(body LoginBody) (LoginResp, error) {
	var resp LoginResp
	err := cli.makeRequest("POST", cli.makeURL("login"), nil, body, &resp)
	return resp, err
}

// 5.4.3
func (cli *Client) Logout(auth Auth) error {
	return cli.makeRequest("POST", cli.makeURL("logout"), auth, nil, nil)
}

// 5.4.4
func (cli *Client) LogoutAll(auth Auth) error {
	return cli.makeRequest("POST", cli.makeURL("logoutAll"), auth, nil, nil)
}

// 5.7.1

type WhoamiResp struct {
	UserID string `json:"user_Id"`
}

func (cli *Client) Whoami(auth Auth) (WhoamiResp, error) {
	var resp WhoamiResp
	err := cli.makeRequest("GET", cli.makeURL("account/whoami"), auth, nil, &resp)
	return resp, err
}

// 9.4.1
type SyncResp struct {
	NextBatch              string              `json:"next_batch"`
	Rooms                  SyncRespRooms       `json:"rooms,omitempty"`
	Presence               SyncRespPresence    `json:"presence,omitempty"`
	AccountData            SyncRespAccountData `json:"account_data,omitempty"`
	ToDevice               SyncRespToDevice    `json:"to_device,omitempty"`
	DeviceLists            SyncRespDeviceLists `json:"device_lists,omitempty"`
	DeviceOneTimeKeysCount map[string]int64    `json:"device_one_time_keys_count,omitempty"`
}

type SyncRespRooms struct {
	Join   map[string]SyncRespRoomsJoin   `json:"join,omitempty"`
	Invite map[string]SyncRespRoomsInvite `json:"invite,omitempty"`
	Leave  map[string]SyncRespRoomsLeave  `json:"leave,omitempty"`
}

type SyncRespRoomsJoin struct {
	Summary             SyncRespRoomSummary              `json:"summary,omitempty"`
	State               SyncRespState                    `json:"state,omitempty"`
	Timeline            SyncRespTimeline                 `json:"timeline,omitempty"`
	Ephemeral           SyncRespEphemeral                `json:"ephemeral,omitempty"`
	AccountData         SyncRespAccountData              `json:"account_data,omitempty"`
	UnreadNotifications SyncRespUnreadNotificationCounts `json:"unread_notifications,omitempty"`
}

type SyncRespRoomSummary struct {
	Heroes              []string `json:"m.heroes,omitempty"`
	JoinedMembersCount  int      `json:"m.joined_members_count,omitempty"`
	InvitedMembersCOund int      `json:"m.invited_members_count,omitempty"`
}

type SyncRespEphemeral struct {
	Events []SyncRespEvent `json:"events,omitempty"`
}

type SyncRespUnreadNotificationCounts struct {
	HighlightCount    int `json:"hightlight_count,omitempty"`
	NotificationCount int `json:"notification_count,omitempty"`
}

type SyncRespRoomsInvite struct {
	InviteState SyncRespInviteState `json:"invite_state,omitempty"`
}

type SyncRespInviteState struct {
	Events []SyncRespStrippedState `json:"events,omitempty"`
}

type SyncRespStrippedState struct {
	Content  map[string]interface{} `json:"content,omitempty"`
	StateKey string                 `json:"state_key,omitempty"`
	Type     string                 `json:"type,omitempty"`
	Sender   string                 `json:"sender,omitempty"`
}

type SyncRespRoomsLeave struct {
	State       SyncRespState       `json:"state,omitempty"`
	Timeline    SyncRespTimeline    `json:"timeline,omitempty"`
	AccountData SyncRespAccountData `json:"account_data,omitempty"`
}

type SyncRespState struct {
	Events []SyncRespRoomEvent `json:"events,omitempty"`
}

type SyncRespRoomEvent struct {
	Content        map[string]interface{} `json:"content"`
	Type           string                 `json:"type"`
	EventID        string                 `json:"event_id"`
	Sender         string                 `json:"sender"`
	OriginServerTS int64                  `json:"origin_server_ts"`
	Unsigned       SyncRespUnsignedData   `json:"unsigned_data,omitempty"`
	PrevContent    map[string]interface{} `json:"prev_content,omitempty"`
	StateKey       *string                `json:"state_key,omitempty"`
}

type SyncRespTimeline struct {
	Events    []SyncRespRoomEvent `json:"events,omitempty"`
	Limited   bool                `json:"limited,omitempty"`
	PrevBatch string              `json:"prev_batch,omitempty"`
}

type SyncRespUnsignedData struct {
	Age             int64         `json:"age,omitempty"`
	RedactedBecause SyncRespEvent `json:"redacted_because,omitempty"`
	TransactionID   string        `json:"transaction_id,omitempty"`
}

type SyncRespPresence struct {
	Events []SyncRespEvent `json:"events,omitempty"`
}

type SyncRespAccountData struct {
	Events []SyncRespEvent `json:"events,omitempty"`
}

type SyncRespEvent struct {
	Content map[string]interface{} `json:"content,omitempty"`
	Type    string                 `json:"type,omitempty"`
}

func (cli *Client) Sync(auth Auth, filter, since string, fullState bool, setPresence string, timeout int64) (SyncResp, error) {
	url := cli.makeURL("sync")
	q := url.Query()
	if since != "" {
		q.Set("since", since)
	}
	q.Set("fullState", strconv.FormatBool(fullState))
	q.Set("setPresence", setPresence)
	q.Set("timeout", strconv.FormatInt(timeout, 10))
	url.RawQuery = q.Encode()

	var resp SyncResp
	err := cli.makeRequest("GET", url, auth, nil, &resp)
	return resp, err
}

// 9.5.6

type GetRoomMessagesResp struct {
	Start string                 `json:"start,omitempty"`
	End   string                 `json:"end,omitempty"`
	Chunk []GetRoomMessagesEvent `json:"chunk,omitempty"`
	State []GetRoomMessagesEvent `json:"state,omitempty"`
}

type GetRoomMessagesEvent struct {
	Content        map[string]interface{}      `json:"content"`
	Type           string                      `json:"type"`
	EventID        string                      `json:"event_id"`
	Sender         string                      `json:"sender"`
	OriginServerTS int64                       `json:"origin_server_ts"`
	Unsigned       GetRoomMessagesUnsignedData `json:"unsigned,omitempty"`
	RoomID         string                      `json:"room_id"`
	PrevContent    map[string]interface{}      `json:"prev_content,omitempty"`
	StateKey       *string                     `json:"state_key"`
}

type GetRoomMessagesUnsignedData struct {
	Age             int64                  `json:"age,omitempty"`
	RedactedBecause map[string]interface{} `json:"redacted_because,omitempty"`
	TransactionID   string                 `json:"transaction_id,omitempty"`
}

func (cli *Client) GetRoomMessages(auth Auth, roomID string, from, to, dir string, limit int, filter string) (GetRoomMessagesResp, error) {
	url := cli.makeURL("rooms/" + roomID + "/messages")
	q := url.Query()
	q.Set("from", from)
	if to != "" {
		q.Set("to", to)
	}
	q.Set("dir", dir)
	q.Set("limit", strconv.FormatInt(int64(limit), 10))
	if filter != "" {
		q.Set("filter", filter)
	}
	url.RawQuery = q.Encode()

	var resp GetRoomMessagesResp

	err := cli.makeRequest("GET", url, auth, nil, &resp)
	return resp, err
}

// 9.6.1
type PutStateEventBody map[string]interface{}

type PutStateEventResp struct {
	EventID string `json:"event_id"`
}

func (cli *Client) PutStateEvent(auth Auth, roomID, eventType, stateKey string, body PutStateEventBody) (PutStateEventResp, error) {
	url := cli.makeURL("rooms/" + roomID + "/state/" + eventType + "/" + stateKey)
	var resp PutStateEventResp
	err := cli.makeRequest("PUT", url, auth, body, &resp)
	return resp, err
}

// 9.6.2
type PutRoomEventBody map[string]interface{}

type PutRoomEventResp struct {
	EventID string `json:"event_id"`
}

func (cli *Client) PutRoomEvent(auth Auth, roomID, eventType, txnID string, body PutRoomEventBody) (PutRoomEventResp, error) {
	url := cli.makeURL("rooms/" + roomID + "/send/" + eventType + "/" + txnID)
	var resp PutRoomEventResp
	err := cli.makeRequest("PUT", url, auth, body, &resp)
	return resp, err
}

// 10.2.2

type GetRoomAliasIDResp struct {
	RoomID  string   `json:"room_id"`
	Servers []string `json:"servers"`
}

func (cli *Client) GetRoomAliasID(alias string) (GetRoomAliasIDResp, error) {
	var resp GetRoomAliasIDResp
	err := cli.makeRequest("GET", cli.makeURL("directory/room/"+url.QueryEscape(alias)), nil, nil, &resp)
	return resp, err
}

// 10.5.1

type GetRoomVisibilityResp struct {
	Visibility string `json:"visibility"`
}

func (cli *Client) GetRoomVisibility(id string) (GetRoomVisibilityResp, error) {
	var resp GetRoomVisibilityResp
	err := cli.makeRequest("GET", cli.makeURL("directory/list/room/"+url.QueryEscape(id)), nil, nil, &resp)
	return resp, err
}

// 11.2.2

type GetUserDisplaynameResp struct {
	Displayname string `json:"displayname"`
}

func (cli *Client) GetUserDisplayname(userID string) (resp GetUserDisplaynameResp, err error) {
	err = cli.makeRequest("GET", cli.makeURL("profile/"+url.QueryEscape(userID)+"/displayname"), nil, nil, &resp)
	return
}

// 13.9.3.2
type SyncRespToDevice struct {
	Events []SyncRespToDeviceEvent `json:"events"`
}

type SyncRespToDeviceEvent struct {
	Content map[string]interface{} `json:"content"`
	Sender  string                 `json:"sender"`
	Type    string                 `json:"type"`
}

// 13.11.5.3
type SyncRespDeviceLists struct {
	Changed []string `json:"changed"`
	Left    []string `json:"left"`
}

D matrix/backend.go => matrix/backend.go +0 -66
@@ 1,66 0,0 @@
package matrix

// The Backend is responsible for storing session information
// (mxid, device ID, access token) and caching.
// A Backend is tied to exactly one session (or device in matrix
// terminology).
// A Backend implementation must be threadsafe!
type Backend interface {
	// Initialize configures the backend to store information
	// for the provided device. This should be called only
	// when a new device is created or mxid, deviceID and
	// access token have been persisted in another way.
	// Initialize panics if there is a persisted session
	// available (i.e if Open(mxid, deviceID) would return
	// nil.
	// Calling Initialize multiple times panics.
	// All structs obtained by calling methods of this Backend
	// will have their private fields set up in such a way, that
	// calling their methods will utilize the passed client.
	Initialize(cli *Client, mxid, deviceID, accessToken string)

	// Open configures the backend to load already an already
	// persisted session. This should be called if the
	// Backend implementation is able to persist information
	// between restarts. If the provided mxid-deviceID
	// combination is not known this returns an error.
	// Calling Open multiple times panics.
	Open(cli *Client, mxid, deviceID string) error

	// MxID returns the MxID of the user owing the device.
	MxID() string

	// DeviceID returns the device ID of the device.
	DeviceID() string

	// AccessToken returns the access token used by the device.
	AccessToken() string

	// AccountData gets a account data event of the specified
	// type from the given room. If room is nil the event
	// is searched in the users global account data.
	// If no event is found nil is returned.
	AccountData(room *Room, type_ string) *AccountDataEvent

	// UpdateAccountData updates the chached account data.
	UpdateAccountData(AccountDataEvent)

	// RoomState gets a StateEvent from a room.
	RoomState(room Room, type_, key string) *StateEvent

	// RoomStateList gets all state events of the specified type
	// from the room.
	RoomStateList(room Room, type_ string) []StateEvent

	// UpdateRoomState updates the cached room state.
	UpdateRoomState(event StateEvent)

	// AddLatestEvents adds events freshly received from the /sync
	// endpoint and the prevBatch token used to iterate room history
	// into the cache.
	AddLatestEvents(room Room, events []Event, prevBatch string)

	// LatestEvents returns the latest Events received from the /sync
	// endpoint and the prevBatch token from the cache.
	LatestEvents(room Room) ([]Event, string)
}

D matrix/backend/memory/backend.go => matrix/backend/memory/backend.go +0 -178
@@ 1,178 0,0 @@
// Package memory contains an in-memory implementation of the Backend interface.
package memory

import (
	"errors"
	"sync"

	"git.sr.ht/~f4814n/frost/matrix"
)

type Backend struct {
	mxid, deviceID, accessToken string
	state                       map[stateKey]matrix.StateEvent
	accountData                 map[string]map[string]matrix.AccountDataEvent
	latestEvents                map[string][]matrix.Event
	prevBatch                   map[string]string

	mut sync.RWMutex
}

type stateKey struct {
	room  string
	type_ string
	key   string
}

func New() *Backend {
	return &Backend{
		state:        make(map[stateKey]matrix.StateEvent),
		latestEvents: make(map[string][]matrix.Event),
		prevBatch:    make(map[string]string),
		accountData:  make(map[string]map[string]matrix.AccountDataEvent),
	}
}

func (b *Backend) Initialize(cli *matrix.Client, mxid, deviceID, accessToken string) {
	b.mut.Lock()
	defer b.mut.Unlock()

	if b.mxid != "" || b.accessToken != "" || b.deviceID != "" { // Already Initialized
		panic("cannot initialize backend multiple times")
	}
	b.mxid, b.deviceID, b.accessToken = mxid, deviceID, accessToken
}

func (b *Backend) Open(*matrix.Client, string, string) error {
	b.mut.RLock()
	defer b.mut.RUnlock()

	if b.mxid != "" || b.accessToken != "" || b.deviceID != "" { // Already Initialized
		panic("cannot open backend. Already initialized")
	}
	return errors.New("this backend does not implement persistent sessions")
}

func (b *Backend) MxID() string {
	b.mut.RLock()
	defer b.mut.RUnlock()

	return b.mxid
}

func (b *Backend) DeviceID() string {
	b.mut.RLock()
	defer b.mut.RUnlock()

	return b.deviceID
}

func (b *Backend) AccessToken() string {
	b.mut.RLock()
	defer b.mut.RUnlock()

	return b.accessToken
}

func (b *Backend) AccountData(room *matrix.Room, type_ string) *matrix.AccountDataEvent {
	b.mut.RLock()
	defer b.mut.RUnlock()

	var r string
	if room != nil {
		r = room.ID
	}

	if m, ok := b.accountData[r]; ok {
		if ev, ok := m[type_]; ok {
			return &ev
		}
	}

	return nil
}

func (b *Backend) UpdateAccountData(ev matrix.AccountDataEvent) {
	b.mut.Lock()
	defer b.mut.Unlock()

	var room string
	if ev.Room() != nil {
		room = ev.Room().ID
	}

	if _, ok := b.accountData[room]; !ok {
		b.accountData[room] = make(map[string]matrix.AccountDataEvent)
	}

	b.accountData[room][ev.Type] = ev
}

func (b *Backend) RoomState(room matrix.Room, type_, key string) *matrix.StateEvent {
	k := stateKey{
		room:  room.ID,
		type_: type_,
		key:   key,
	}

	b.mut.RLock()
	defer b.mut.RUnlock()

	if e, ok := b.state[k]; ok {
		return &e
	}

	return nil
}

func (b *Backend) RoomStateList(room matrix.Room, type_ string) []matrix.StateEvent {
	var res []matrix.StateEvent

	b.mut.RLock()
	defer b.mut.RUnlock()

	for key, event := range b.state {
		if key.room == room.ID && key.type_ == type_ {
			res = append(res, event)
		}
	}

	return res
}

func (b *Backend) UpdateRoomState(event matrix.StateEvent) {
	k := stateKey{
		room:  event.Room.ID,
		type_: event.Type,
		key:   event.StateKey,
	}

	b.mut.Lock()
	defer b.mut.Unlock()

	b.state[k] = event
}

func (b *Backend) AddLatestEvents(room matrix.Room, events []matrix.Event, prevBatch string) {
	b.mut.Lock()
	defer b.mut.Unlock()

	b.latestEvents[room.ID] = events
	b.prevBatch[room.ID] = prevBatch
}

func (b *Backend) LatestEvents(room matrix.Room) ([]matrix.Event, string) {
	b.mut.RLock()
	defer b.mut.RUnlock()

	var events []matrix.Event
	if ev, ok := b.latestEvents[room.ID]; ok {
		events = ev
	}

	if batch, ok := b.prevBatch[room.ID]; ok {
		return events, batch
	}

	return events, ""
}

D matrix/backend/memory/backend_test.go => matrix/backend/memory/backend_test.go +0 -12
@@ 1,12 0,0 @@
package memory

import (
	"testing"

	"git.sr.ht/~f4814n/frost/matrix/backend"
)

func Test(t *testing.T) {
	b := New()
	backend.Test(t, b)
}

D matrix/backend/sqlite/backend.go => matrix/backend/sqlite/backend.go +0 -390
@@ 1,390 0,0 @@
package sqlite

import (
	"errors"
	"context"
	"github.com/jmoiron/sqlx"
	"database/sql"
	_ "github.com/mattn/go-sqlite3"
	"git.sr.ht/~f4814n/frost/matrix"
	"encoding/json"
	"os"
	"fmt"
)

const ErrNoRowsStr = "sql: no rows in result set";

const (
	// XXX DOC
	stateEvent = "stateEvent"
	roomEvent = "roomEvent"
)

// Backend implements a matrix storage backend using sqlite
type Backend struct {
	path string
	db *sqlx.DB
	cli *matrix.Client
}

func New(path string) *Backend {
	return &Backend{path: path}
}

// Close closes the underlying database. After calling Close() the Backend will become unusable
func (b *Backend) Close() {
	b.db.Close()
}

// Initialize implements the backend interface
func (b *Backend) Initialize(cli *matrix.Client, mxid, deviceID, accessToken string) {
	// Backend already initialized
	if b.db != nil {
		panic("backend/sqlite: Backend already initialized")
	}

	// Create b.Path directory
	if _, err := os.Stat(b.path); os.IsNotExist(err) {
		os.Mkdir(b.path, os.ModePerm) // XXX Correct permissions
	}

	var dbFile string
	if b.path == ":memory:" {
		dbFile = ":memory:"
	} else {
		dbFile = fmt.Sprintf("file:%s/%s-%s.sqlite", b.path, mxid, deviceID)

		// Open would overwrite already initialized backend
		if _, err := os.Stat(dbFile); !os.IsNotExist(err) {
			panic("backend/sqlite: Combination of mixd and deviceID is already initialized")
		}
	}

	db, err := sqlx.Connect("sqlite3", dbFile)
	if err != nil {
		panic(err)
	}

	if b.path == ":memory:" {
		db.SetMaxOpenConns(1)
	}

	b.db = db
	b.cli = cli

	db.MustExec(`
		CREATE TABLE kvstore (
			key TEXT NOT NULL,
			value TEXT NOT NULL,
			UNIQUE(key)
		);

		CREATE TABLE accountData (
			roomID TEXT,
			type TEXT NOT NULL,
			eventJSON BLOB NOT NULL,
			UNIQUE(roomID,type)
		);

		CREATE TABLE roomState (
			roomID TEXT NOT NULL,
			type TEXT NOT NULL,
			key TEXT NOT NULL,
			eventJSON BLOB NOT NULL,
			UNIQUE(roomID,type,key)
		);

		CREATE TABLE historyBatches (
			roomID TEXT NOT NULL,
			prevBatch TEXT NOT NULL,
			UNIQUE(roomID)
		);

		CREATE TABLE latestEvents (
			roomID TEXT NOT NULL,
			ID TEXT NOT NULL,
			eventType TEXT NOT NULL,
			eventJSON BLOB NOT NULL,
			UNIQUE(roomID,ID)
		);
	`);

	db.MustExec(
		"INSERT INTO kvstore(key, value) VALUES ('mxid', ?), ('deviceID', ?), ('accessToken', ?)",
		mxid, deviceID, accessToken,
	)
}

func (b *Backend) Open(cli *matrix.Client, mxid, deviceID string) error {
	if b.path == ":memory:" {
		return errors.New(`Unable to open backend. This backend is configured as in-memory
			Backend. Is this really what you want?`)
	}
	dbFile := fmt.Sprintf("file:%s/%s-%s.sqlite", b.path, mxid, deviceID)
	db, err := sqlx.Connect("sqlite3", dbFile)

	if err != nil {
		return err
	}

	b.db = db
	b.cli = cli
	return nil
}

// queryKV gets the value of a key in the kvstore table. If it does not exist, it panics
func (b *Backend) queryKV(key string) (val string) {
	row := b.db.QueryRowx("SELECT value FROM kvstore WHERE key = ?", key)
	if row.Err() != nil {
		panic(row.Err())
	}

	if err := row.Scan(&val); err != nil {
		panic(err)
	}

	return
}

// MxID implements the Backend interface
func (b *Backend) MxID() string {
	return b.queryKV("mxid")
}

// DeviceID implements the Backend interface
func (b *Backend) DeviceID() string {
	return b.queryKV("deviceID")
}

// AccessToken implements the Backend interface
func (b *Backend) AccessToken() string {
	return b.queryKV("accessToken")
}

// AccountData implements the Backend interface
func (b *Backend) AccountData(room *matrix.Room, type_ string) *matrix.AccountDataEvent {
	var roomID string

	if room != nil {
		roomID = room.ID
	}

	row := b.db.QueryRowx(
		"SELECT eventJSON FROM accountData WHERE roomID = ? AND type = ?",
		&roomID, type_,
	)

	if row.Err() != nil { // XXX: Detect no result
		panic(row.Err())
	}

	var eventJSON []byte
	err := row.Scan(&eventJSON)
	if err != nil {
		if err.Error() == ErrNoRowsStr {
			return nil
		}
		panic(err)
	}

	var event matrix.AccountDataEvent
	err = json.Unmarshal(eventJSON, &event)
	if err != nil {
		panic(err)
	}

	if room != nil {
		event = event.WithRoom(*room)
	}

	return &event
}

// UpdateAccountData implements the AccountDataInterface
func (b *Backend) UpdateAccountData(event matrix.AccountDataEvent) {
	eventJSON, err := json.Marshal(event)
	if err != nil {
		panic(err)
	}

	var roomID *string
	if event.Room() != nil {
		roomID = &event.Room().ID
	}

	b.db.MustExec(`
		INSERT INTO accountData(roomID, type, eventJSON)
		VALUES (?, ?, ?)
		ON CONFLICT(roomID,type) DO UPDATE SET eventJSON = excluded.eventJSON`,
		roomID, event.Type, eventJSON,
	)
}

func (b *Backend) RoomState(room matrix.Room, type_ string, key string) *matrix.StateEvent {
	row := b.db.QueryRowx(
		"SELECT eventJSON FROM roomState WHERE roomID = ? AND type = ? AND key = ?",
		room.ID, type_, key,
	)

	if row.Err() != nil {
		panic(row.Err())
	}

	var eventJSON []byte
	err := row.Scan(&eventJSON)
	if err != nil {
		if err.Error() == ErrNoRowsStr {
			return nil
		}
		panic(fmt.Sprintf("%#v", err))
	}

	var event matrix.StateEvent
	err = json.Unmarshal(eventJSON, &event)
	if err != nil {
		panic(err)
	}

	event = event.WithClient(b.cli)
	return &event
}

func (b *Backend) RoomStateList(room matrix.Room, type_ string) []matrix.StateEvent {
	rows, err := b.db.Queryx(
		"SELECT eventJSON FROM roomState WHERE roomID = ? AND type = ?",
		room.ID, type_,
	)
	if err != nil {
		panic(err)
	}

	var events []matrix.StateEvent

	for rows.Next() {
		var (
			eventJSON []byte
			event matrix.StateEvent
		)

		err := rows.Scan(&eventJSON)
		if err != nil {
			panic(err)
		}

		err = json.Unmarshal(eventJSON, &event)
		if err != nil {
			panic(err)
		}

		events = append(events, event.WithClient(b.cli))
	}

	return events
}

func (b *Backend) UpdateRoomState(event matrix.StateEvent) {
	eventJSON, err := json.Marshal(event)
	if err != nil {
		panic(err)
	}

	b.db.MustExec(`
		INSERT INTO roomState(roomID,type,key,eventJSON)
		VALUES (?, ?, ?, ?)
		ON CONFLICT(roomID,type,key) DO UPDATE SET eventJSON=excluded.eventJSON`,
		event.Room.ID, event.Type, event.StateKey, eventJSON,
	)
}

func (b *Backend) AddLatestEvents(room matrix.Room, events []matrix.Event, prevBatch string) {
	tx := b.db.MustBegin()

	tx.MustExec(`
		INSERT INTO historyBatches(roomID, prevBatch)
		VALUES (?, ?)
		ON CONFLICT(roomID) DO UPDATE SET prevBatch=excluded.prevBatch`,
		room.ID, prevBatch)

	tx.MustExec("DELETE FROM latestEvents WHERE roomID = ?", room.ID)

	for id, event := range events {
		eventJSON, err := json.Marshal(event)
		if err != nil {
			panic(err)
		}

		var eventType string
		switch event.(type) {
		case matrix.StateEvent:
			eventType = stateEvent
		case matrix.RoomEvent:
			eventType = roomEvent
		default:
			// XXX Handle this?
			println("unexpected event type. skipping")
			continue
		}

		tx.MustExec(
			"INSERT INTO latestEvents(roomID, ID, eventType, eventJSON) VALUES (?, ?, ?, ?)",
			room.ID, id, eventType, eventJSON,
		)
	}

	if err := tx.Commit(); err != nil {
		panic(err)
	}
}

func (b *Backend) LatestEvents(room matrix.Room) (events []matrix.Event, prevBatch string) {
	tx := b.db.MustBeginTx(context.Background(), &sql.TxOptions{ReadOnly: true})
	defer tx.Commit()

	row := tx.QueryRowx("SELECT prevBatch FROM historyBatches WHERE roomID = ?", room.ID)
	if err := row.Err(); err != nil {
		if err.Error() == ErrNoRowsStr {
			return // XXX How to handle this
		}
		panic(err)
	}

	err := row.Scan(&prevBatch)
	if err != nil {
		panic(err)
	}

	rows, err := tx.Queryx(
		"SELECT eventJSON, eventType FROM latestEVENTS WHERE roomID = ? ORDER BY ID",
		room.ID,
	)

	if err != nil {
		panic(err)
	}

	for rows.Next() {
		res := make(map[string]interface{})
		rows.MapScan(res)

		var event matrix.Event
		if res["eventType"].(string) == stateEvent {
			var ev matrix.StateEvent
			err := json.Unmarshal(res["eventJSON"].([]byte), &ev)
			if err != nil {
				panic(err)
			}
			event = ev.WithClient(b.cli)
		} else if res["eventType"].(string) == roomEvent {
			var ev matrix.RoomEvent
			err := json.Unmarshal(res["eventJSON"].([]byte), &ev)
			if err != nil {
				panic(err)
			}
			event = ev.WithClient(b.cli)
		}

		events = append(events, event)
	}

	return
}

D matrix/backend/sqlite/backend_test.go => matrix/backend/sqlite/backend_test.go +0 -12
@@ 1,12 0,0 @@
package sqlite

import (
	"testing"

	"git.sr.ht/~f4814n/frost/matrix/backend"
)

func Test(t *testing.T) {
	b := &Backend{path: ":memory:"}
	backend.Test(t, b)
}

D matrix/backend/test.go => matrix/backend/test.go +0 -127
@@ 1,127 0,0 @@
package backend

import (
	"reflect"
	"testing"

	"git.sr.ht/~f4814n/frost/matrix"
)

func Test(t *testing.T, backend matrix.Backend) {
	t.Parallel()

	backend.Initialize(nil, "@test:test.org", "TESTID", "token")

	t.Run("initialization", func(t *testing.T) { testInit(t, backend) })

	t.Run("state", func(t *testing.T) { testState(t, backend) })

	t.Run("latestEvents", func(t *testing.T) { testLatestEvents(t, backend) })
}

func testInit(t *testing.T, backend matrix.Backend) {
	t.Parallel()

	func() {
		defer func() {
			if r := recover(); r == nil {
				t.Fatal("multiple initialization did not panic")
			}
		}()
		backend.Initialize(nil, "@test:test.org", "TESTID", "token")
	}()

	if backend.MxID() != "@test:test.org" {
		t.Fatal("got wrong mxid. Initalization failed.")
	}

	if backend.DeviceID() != "TESTID" {
		t.Fatal("got wrong device id. Initialization failed.")
	}

	if backend.AccessToken() != "token" {
		t.Fatal("go wrong access token. Initialization failed")
	}
}

func testState(t *testing.T, backend matrix.Backend) {
	t.Parallel()

	room := matrix.Room{ID: "!aaaaaaaaaaaaa:f4814n.de"}

	first := matrix.StateEvent{
		Content: map[string]interface{}{
			"first": "content",
		},
		Type:     "de.f4814n.testing",
		ID:       "aaaaaaaaaaaaaaa",
		StateKey: "test",
		Room:     room,
		Sender:   matrix.Member{User: matrix.User{ID: "@dev:f4814n.de"}, Room: room},
	}

	backend.UpdateRoomState(first)

	if ev := backend.RoomState(room, "de.f4814n.testing", "test"); ev == nil {
		t.Fatal("could not find state event")
	} else if ev.Content["first"] != "content" {
		t.Fatal("state event contained wrong content")
	}

	other := matrix.StateEvent{
		Content: map[string]interface{}{
			"other": "content",
		},
		Type:     "de.f4814n.testing",
		ID:       "bbbbbbbbbbbbbbb",
		StateKey: "test1",
		Room:     room,
		Sender:   matrix.Member{User: matrix.User{ID: "@dev:f4814n.de"}, Room: room},
	}
	backend.UpdateRoomState(other)

	res := backend.RoomStateList(room, "de.f4814n.testing")

	if (res[0].Content["other"] != "content" && res[1].Content["first"] != "content") &&
		(res[1].Content["other"] != "content" && res[0].Content["first"] != "content") {
		t.Fatal("got wrong list of state events")
	}
}

func testLatestEvents(t *testing.T, backend matrix.Backend) {
	t.Parallel()

	room := matrix.Room{ID: "!asdfasdfadf:f4814n.de"}

	events := []matrix.Event{
		matrix.RoomEvent{
			Content: map[string]interface{}{
				"a": []interface{}{"1", "2", "3"},
			},
			Type: "de.f4814n.testing",
		},
		matrix.StateEvent{
			Content: map[string]interface{}{
				"b": "b",
			},
			Type:     "de.f4814n.testing",
			StateKey: "abc",
		},
	}

	batch := "asdfasdfasfdsad"

	backend.AddLatestEvents(room, events, batch)

	ev, newBatch := backend.LatestEvents(room)

	if newBatch != batch {
		t.Fatal("got wrong batch ID")
	}

	if !reflect.DeepEqual(ev[0], events[0]) && reflect.DeepEqual(ev[1], events[1]) {
		t.Logf("%#v", ev)
		t.Logf("%#v", events)
		t.Fatal("got wrong events")
	}
}

D matrix/client.go => matrix/client.go +0 -453
@@ 1,453 0,0 @@
package matrix

import (
	"context"
	"errors"
	"net/http"
	"sync"

	"git.sr.ht/~f4814n/frost/matrix/api"
)

var ErrUnsupportedVersion = errors.New("the homeserver does not support API version r0.5.0. The Client might or might not work correctly")

// Client implements communication using the matrix protocol
type Client struct {
	// A event is sent to this channel when the first sync request has completed.
	// If the Sync routine is stopped and then restarted a event is again sent.
	InitialSyncDone chan struct{}

	notifierChannels []notifierChannel

	backend Backend
	http    *http.Client
	api     *api.Client
	auth    api.Auth

	mut sync.RWMutex
}

// ClientOpts are optional options passed to Client. NewClient is able to handle
// ClientOpts values which have nil fields or even are nil.
type ClientOpts struct {
	// HTTP Client to use.
	HTTPClient *http.Client

	// Backend
	Backend Backend
}

// NewClient creates a new client. This only initializes everything needed but
// does not make any attempt to use the network.
func NewClient(opts ClientOpts) *Client {
	return &Client{
		backend:          opts.Backend,
		notifierChannels: make([]notifierChannel, 0),
		InitialSyncDone:  make(chan struct{}, 1),
		http:             opts.HTTPClient,
	}
}

// AccessToken returns the access token used by the client. "" if there is no
// token
func (cli *Client) AccessToken() string {
	return cli.backend.AccessToken()
}

// DeviceID returns the device ID of the Client.
func (cli *Client) DeviceID() string {
	return cli.backend.DeviceID()
}

// MxID returns the matrix ID of the Client.
func (cli *Client) MxID() string {
	return cli.backend.MxID()
}

// Login using a matrix user id and a password (m.login.password)
func (cli *Client) Login(mxid string, password string) error {
	cli.mut.Lock() // Before Login is done the client is useless anyway.
	defer cli.mut.Unlock()

	localpart, serverName, err := SplitMxID(mxid)
	if err != nil {
		return LogicError{Err: err}
	}

	homeserver, err := ResolveHomeserverURL(serverName)
	if err != nil {
		return err
	}

	cli.api = api.NewClient(cli.http, homeserver.Host)

	if err := cli.checkVersions(); err != nil {
		return err
	}

	body := api.LoginBody{
		Type:       "m.login.password",
		Identifier: api.NewUserIdentifer(localpart),
		Password:   password,
	}

	resp, err := cli.api.Login(body)
	if err != nil {
		return makeError(err)
	}

	cli.auth = func(r *http.Request) {
		r.Header.Add("Authorization", "Bearer "+resp.AccessToken)
	}
	cli.backend.Initialize(cli, mxid, resp.DeviceID, resp.AccessToken)

	return nil
}

// LoadToken calls the Open method of the backend with the provided parameters,
// checks if the homeserver exists, configures the client to use the homeserver
// and finally calls the whoami API endpoint to check, whether the access token
// matches the mxID.
func (cli *Client) LoadToken(mxid, deviceID string) error {
	if err := cli.backend.Open(cli, mxid, deviceID); err != nil {
		return LogicError{Err: err}
	}

	_, serverName, err := SplitMxID(mxid)
	if err != nil {
		return LogicError{Err: err}
	}

	homeserver, err := ResolveHomeserverURL(serverName)
	if err != nil {
		return err
	}

	cli.api = api.NewClient(cli.http, homeserver.Host)
	if err := cli.checkVersions(); err != nil {
		return err
	}

	cli.auth = func(r *http.Request) {
		r.Header.Add("Authorization", "Bearer "+cli.backend.AccessToken())
	}

	resp, err := cli.api.Whoami(cli.auth)
	if err != nil {
		return makeError(err)
	}

	if resp.UserID != mxid {
		return LogicError{Err: errors.New("backend returned a valid access token not associated with this mxID. What is going on?")}
	}

	return nil
}

// chechVersions checks whether the server supports the API version we use
// (currently r0.5.0)
func (cli *Client) checkVersions() error {
	resp, err := cli.api.Versions()
	if err != nil {
		return makeError(err)
	}

	for _, version := range resp.Versions {
		if version == "r0.5.0" {
			return nil
		}
	}

	return LogicError{Err: ErrUnsupportedVersion}

}

// Logout invalidates the used access token
func (cli *Client) Logout() error {
	return makeError(cli.api.Logout(cli.auth))
}

// LogoutAll invalidates all access tokens of the user
func (cli *Client) LogoutAll() error {
	return makeError(cli.api.LogoutAll(cli.auth))
}

// User returns the user the client is authenticated as. Nil if the client is not
// authenticated.
func (cli *Client) User() *User {
	if cli.backend.MxID() == "" {
		return nil
	}

	return &User{
		ID:  cli.backend.MxID(),
		cli: cli,
	}
}

// SyncOpts are optional options to supply to the Sync function
type SyncOpts struct {
	// Execute this function if there if an error. If this returns nil, Sync
	// will sleep for SyncOpts.Timeout and then continue otherwise Sync()
	// will exit, and return the produced error.
	OnError func(error) error

	// Timeout in ms used for long-polling /sync. 3000 is used if this is negative.
	Timeout int64
}

func (cli *Client) sync(ctx context.Context, timeout int64, nextBatch string) (string, error) {
	resp, err := cli.api.Sync(cli.auth, "", nextBatch, nextBatch == "", "online", timeout)
	if err != nil {
		return "", makeError(err)
	}

	cli.syncResp(resp)

	return resp.NextBatch, nil
}

func (cli *Client) syncResp(resp api.SyncResp) {
	for _, ev := range resp.AccountData.Events {
		event := AccountDataEvent{
			Type:    ev.Type,
			Content: ev.Content,
		}

		cli.backend.UpdateAccountData(event)
		cli.handleEvent(event)
	}

	for id, body := range resp.Rooms.Join {
		room := Room{ID: id, cli: cli}
		for _, ev := range body.AccountData.Events {
			event := AccountDataEvent{
				Type:    ev.Type,
				Content: ev.Content,
				room:    &room,
			}

			cli.backend.UpdateAccountData(event)
			cli.handleEvent(event)
		}

		for _, ev := range body.Ephemeral.Events {
			event := EphemeralEvent{
				Type: ev.Type,
				Content: ev.Content,
				room: room,
			}

			cli.handleEvent(event)
		}

		for _, ev := range body.State.Events {
			sender := loadUserUnsafe(cli, ev.Sender)
			event := StateEvent{
				Content:        ev.Content,
				Type:           ev.Type,
				ID:             ev.EventID,
				OriginServerTS: timestampToTime(ev.OriginServerTS),
				Unsigned: RoomEventUnsigned{ // TODO RedactedBecause
					Age:           ev.Unsigned.Age,
					TransactionID: ev.Unsigned.TransactionID,
				},
				StateKey:    *ev.StateKey,
				PrevContent: ev.PrevContent,
				Sender:      Member{User: sender, Room: room},
				Room:        room,
			}

			cli.backend.UpdateRoomState(event)
			cli.handleEvent(event)
		}

		// TODO body.Summary

		batch := make([]Event, len(body.Timeline.Events))
		for i, ev := range body.Timeline.Events { // TODO respect Limited
			sender := loadUserUnsafe(cli, ev.Sender)
			var event Event

			if ev.StateKey != nil {
				event = StateEvent{
					Content:        ev.Content,
					Type:           ev.Type,
					ID:             ev.EventID,
					OriginServerTS: timestampToTime(ev.OriginServerTS),
					Unsigned: RoomEventUnsigned{ // TODO RedactedBecause
						Age:           ev.Unsigned.Age,
						TransactionID: ev.Unsigned.TransactionID,
					},
					PrevContent: ev.PrevContent,
					StateKey:    *ev.StateKey,
					Sender:      Member{User: sender, Room: room},
					Room:        room,
				}
			} else {
				event = RoomEvent{
					Content:        ev.Content,
					Type:           ev.Type,
					ID:             ev.EventID,
					OriginServerTS: timestampToTime(ev.OriginServerTS),
					Unsigned: RoomEventUnsigned{ // TODO RedactedBecause
						Age:           ev.Unsigned.Age,
						TransactionID: ev.Unsigned.TransactionID,
					},
					Sender: Member{User: sender, Room: room},
					Room:   room,
				}
			}
			batch[i] = event
		}

		// We do not want to call backend.AddLatestEvents with empty arguments
		if len(body.Timeline.Events) != 0 {
			cli.backend.AddLatestEvents(room, batch, body.Timeline.PrevBatch)
			cli.handleEvents(batch)
		}

		// TODO body.UnreadNotifications
	}

	for id, body := range resp.Rooms.Invite {
		room := loadRoomUnsafe(cli, id)

		for _, ev := range body.InviteState.Events {
			sender := loadUserUnsafe(cli, ev.Sender)
			event := StateEvent{
				Content:  ev.Content,
				Type:     ev.Type,
				StateKey: ev.StateKey,
				Sender:   Member{User: sender, Room: room},
				Room:     room,
			}

			cli.backend.UpdateRoomState(event)
			cli.handleEvent(event)
		}
	}

	for id, body := range resp.Rooms.Leave {
		room := loadRoomUnsafe(cli, id)
		for _, ev := range body.AccountData.Events {
			event := AccountDataEvent{
				Type:    ev.Type,
				Content: ev.Content,
				room:    &room,
			}

			cli.backend.UpdateAccountData(event)
			cli.handleEvent(event)
		}

		for _, ev := range body.State.Events {
			sender := loadUserUnsafe(cli, ev.Sender)
			event := StateEvent{
				Content:        ev.Content,
				Type:           ev.Type,
				ID:             ev.EventID,
				StateKey:       *ev.StateKey,
				OriginServerTS: timestampToTime(ev.OriginServerTS),
				Unsigned: RoomEventUnsigned{ // TODO RedactedBecause
					Age:           ev.Unsigned.Age,
					TransactionID: ev.Unsigned.TransactionID,
				},
				Sender: Member{User: sender, Room: room},
				Room:   room,
			}

			cli.backend.UpdateRoomState(event)
			cli.handleEvent(event)
		}
	}
}

// handleEvent reacts to a incoming event.
func (cli *Client) handleEvent(e Event) {
	cli.mut.RLock()
	defer cli.mut.RUnlock()

	for _, c := range cli.notifierChannels {
		if c.send(e) {
			c.ch <- e
		}
	}
}

func (cli *Client) handleEvents(events []Event) {
	for _, e := range events {
		cli.handleEvent(e)
	}
}

// Sync starts receiving change events. This will start long-polling /sync and supply
// all Changers with new events. This can be stopped and then restarted without
// having to discard Changers. Stop this by terminating ctx.
// The returned error is either a NetworkError, a LogicError or Context.Canceled /
// Context.DeadlineExceeded.
func (cli *Client) Sync(ctx context.Context, opts *SyncOpts) error {
	if opts == nil {
		opts = &SyncOpts{}
	}

	if opts.Timeout <= 0 {
		opts.Timeout = 3000
	}

	var (
		nextBatch string
		err       error
	)

	initialSync := true

	for {
		nextBatch, err = cli.sync(ctx, opts.Timeout, nextBatch)

		select { // First check if the context was closed. In that case we just ignore the error
		case <-ctx.Done():
			return ctx.Err()
		default:
		}

		if err != nil {
			if err := opts.OnError(err); err != nil {
				return err
			}
			continue
		}

		if initialSync {
			cli.InitialSyncDone <- struct{}{}
			initialSync = false
		}
	}
}

// Notify implements the Notify interface. This will contain all events received.
func (cli *Client) Notify(c chan<- Event) {
	cli.mut.Lock()
	defer cli.mut.Unlock()

	send := func(Event) bool {
		return true
	}

	cli.notifierChannels = append(cli.notifierChannels, notifierChannel{ch: c, send: send})
}

// Stop implements the Notifier interface.
func (cli *Client) Stop(c chan<- Event) {
	cli.mut.Lock()
	defer cli.mut.Unlock()

	n := make([]notifierChannel, 0)
	for _, x := range cli.notifierChannels {
		if x.ch != c {
			n = append(n, x)
		}
	}

	cli.notifierChannels = n
}

D matrix/doc.go => matrix/doc.go +0 -10
@@ 1,10 0,0 @@
// Package matrix implements a matrix client. This includes a lot of abstractionns on top of
// the http api. This package tries to use as few as possible HTTP requests. We
// try to accomplish this by saving as much information as possible from the /sync
// API endpoint. This means that whenever we initialize a new backend, we do a
// request with full_state=true which might take some seconds to complete. To avoid
// this you should use a persistent backend.
// This package is fully thread-safe.
// All errors returned by methods in this package are (unless otherwise noted)
// either a NetworkError or a LogicError wrapping the original error
package matrix

D matrix/errors.go => matrix/errors.go +0 -42
@@ 1,42 0,0 @@
package matrix

import "git.sr.ht/~f4814n/frost/matrix/api"

// A NetworkError is returned, if a correct HTTP transmission was not possible
// for whatever reason
type NetworkError struct {
	Err error
}

func (e NetworkError) Error() string {
	return "network error: " + e.Err.Error()
}

func (e NetworkError) Unwrap() error {
	return e.Err
}

// A LogicError is returned, if the HTTP connection was successful but the
// operation failed anyway. (For example due to insufficient authorization)
type LogicError struct {
	Err error
}

func (e LogicError) Error() string {
	return "logic error: " + e.Err.Error()
}

func (e LogicError) Unwrap() error {
	return e.Err
}

func makeError(err error) error {
	if err == nil {
		return nil
	}

	if _, ok := err.(api.MatrixError); ok {
		return LogicError{Err: err}
	}
	return NetworkError{Err: err} // TODO NetworkError vs LogicError
}

D matrix/event.go => matrix/event.go +0 -130
@@ 1,130 0,0 @@
package matrix

import "time"

// Event is the interface implemented by the types to represent all kinds of
// matrix events.
type Event interface {
	Base() BaseEvent
	// GetType() string
	// GetContent() map[string]interface{}
}

// BaseEvent is the lowest common denominator of all events.
type BaseEvent struct {
	Type string
	Content map[string]interface{}
}

// EphemeralEvent is a ephemeral matrix event.
type EphemeralEvent struct {
	Type string
	Content map[string]interface{}

	room Room
}

// Base implements the Event interface
func (e EphemeralEvent) Base() BaseEvent {
	return BaseEvent {
		Type: e.Type,
		Content: e.Content,
	}
}

func (e EphemeralEvent) Room() Room {
	return e.room
}

// RoomEvent is a matrix room event that is not a state event at the same time.
type RoomEvent struct {
	Content        map[string]interface{}
	Type           string
	ID             string
	OriginServerTS time.Time
	Unsigned       RoomEventUnsigned
	Sender         Member
	Room           Room
}

// RoomEventUnsigned is used within RoomEvent.
type RoomEventUnsigned struct {
	Age             int64
	RedactedBecause Event
	TransactionID   string
}

// Base implements the Event interface
func (e RoomEvent) Base() BaseEvent {
	return BaseEvent {
		Type: e.Type,
		Content: e.Content,
	}
}

// XXX HELPERDOC
func (e RoomEvent) WithClient(cli *Client) RoomEvent {
	e.Sender.User.cli = cli
	e.Sender.Room.cli = cli
	e.Room.cli = cli

	return e
}

// StateEvent is a matrix state event.
type StateEvent struct {
	Content        map[string]interface{}
	Type           string
	ID             string
	OriginServerTS time.Time
	Unsigned       RoomEventUnsigned
	StateKey       string
	PrevContent    map[string]interface{}
	Sender         Member
	Room           Room
}

// Base implements the Event interface
func (e StateEvent) Base() BaseEvent {
	return BaseEvent {
		Type: e.Type,
		Content: e.Content,
	}
}

// XXX HELPERDOC
func (e StateEvent) WithClient(cli *Client) StateEvent {
	e.Room.cli = cli
	e.Sender.User.cli = cli
	e.Sender.Room.cli = cli

	return e
}

// AccountDataEvent is a matrix account data event. If this event is attached to
// a Room, this Room can be obtained using the Room method.
type AccountDataEvent struct {
	Type    string
	Content map[string]interface{}

	room *Room
}

// Base implements the Event interface
func (e AccountDataEvent) Base() BaseEvent {
	return BaseEvent {
		Type: e.Type,
		Content: e.Content,
	}
}

// Room returns the room on which the event was stored.
func (e AccountDataEvent) Room() *Room {
	return e.room
}

// XXX HELPERDOC
func(e AccountDataEvent) WithRoom(room Room) AccountDataEvent {
	e.room = &room
	return e
}

D matrix/examples/echo/main.go => matrix/examples/echo/main.go +0 -60
@@ 1,60 0,0 @@
package main

import (
	"context"
	"fmt"
	"net/http"
	_ "net/http/pprof"

	"git.sr.ht/~f4814n/frost/matrix"
	membackend "git.sr.ht/~f4814n/frost/matrix/backend/memory"
	"git.sr.ht/~f4814n/frost/matrix/examples"
)

func main() {
	cli := matrix.NewClient(matrix.ClientOpts{
		Backend:    membackend.New(),
		HTTPClient: http.DefaultClient,
	})
	mxid, pw := examples.QueryCredentials()

	err := cli.Login(mxid, pw)
	if err != nil {
		panic(err)
	}
	fmt.Println("Authenticated")

	go cli.Sync(context.Background(), &matrix.SyncOpts{Timeout: 300,
		OnError: func(err error) error {
			fmt.Printf("%#v\n", err)
			return nil
		}})

	// Wait until the initial sync is done. We do not want to react on all the
	// events returned by initial sync
	<-cli.InitialSyncDone

	fmt.Println("Waiting for events")

	c := make(chan matrix.Event)
	cli.Notify(c)

	for {
		select {
		case e := <-c:
			go echo(cli, e)
		}
	}
}

// Send a echo, if the event type is "m.room.message" and we are not the sender
func echo(cli *matrix.Client, ev matrix.Event) {
	if event, ok := ev.(matrix.RoomEvent); ok {
		if event.Type == "m.room.message" && event.Sender.ID != (*cli.User()).ID {
			_, err := event.Room.SendRoomEvent("m.room.message", event.Content)
			if err != nil {
				fmt.Printf("%#v\n", err)
			}
		}
	}
}

D matrix/examples/history/main.go => matrix/examples/history/main.go +0 -54
@@ 1,54 0,0 @@
package main

import (
	"bufio"
	"context"
	"fmt"
	"net/http"
	"os"

	"git.sr.ht/~f4814n/frost/matrix"
	membackend "git.sr.ht/~f4814n/frost/matrix/backend/memory"
	"git.sr.ht/~f4814n/frost/matrix/examples"
)

func main() {
	cli := matrix.NewClient(matrix.ClientOpts{
		Backend:    membackend.New(),
		HTTPClient: http.DefaultClient,
	})
	mxid, pw := examples.QueryCredentials()

	reader := bufio.NewReader(os.Stdin)
	fmt.Print("Enter room ID/alias: ")
	roomID, _ := reader.ReadString('\n')
	roomID = roomID[:len(roomID)-1]

	err := cli.Login(mxid, pw)
	if err != nil {
		panic(err)
	}
	fmt.Println("Authenticated")

	go cli.Sync(context.Background(), &matrix.SyncOpts{Timeout: 3000, OnError: examples.ErrorHandler})

	// The initial sync must be completed so we know the prev_token of the sync request
	<-cli.InitialSyncDone

	room, err := matrix.LoadRoom(cli, roomID)
	if err != nil {
		panic(err)
	}

	history := room.History()

	for history.Next() {
		if history.Err != nil {
			panic(err)
		}

		for _, event := range history.Events {
			fmt.Printf("%#v\n", event)
		}
	}
}

D matrix/examples/login/main.go => matrix/examples/login/main.go +0 -31
@@ 1,31 0,0 @@
package main

import (
	"fmt"
	"net/http"

	"git.sr.ht/~f4814n/frost/matrix"
	membackend "git.sr.ht/~f4814n/frost/matrix/backend/memory"
	"git.sr.ht/~f4814n/frost/matrix/examples"
)

func main() {
	cli := matrix.NewClient(matrix.ClientOpts{
		Backend:    membackend.New(),
		HTTPClient: http.DefaultClient,
	})
	mxid, pw := examples.QueryCredentials()

	err := cli.Login(mxid, pw)
	if err != nil {
		panic(err)
	}

	fmt.Printf("Got access token %s and device ID %s\n", cli.AccessToken(), cli.DeviceID())

	fmt.Println("Logging out")
	err = cli.Logout()
	if err != nil {
		panic(err)
	}
}

D matrix/examples/read_room/main.go => matrix/examples/read_room/main.go +0 -79
@@ 1,79 0,0 @@
package main

import (
	"bufio"
	"context"
	"fmt"
	"net/http"
	"os"

	"git.sr.ht/~f4814n/frost/matrix"
	membackend "git.sr.ht/~f4814n/frost/matrix/backend/memory"
	"git.sr.ht/~f4814n/frost/matrix/examples"
)

func main() {
	cli := matrix.NewClient(matrix.ClientOpts{
		Backend:    membackend.New(),
		HTTPClient: http.DefaultClient,
	})

	mxid, pw := examples.QueryCredentials()

	reader := bufio.NewReader(os.Stdin)
	fmt.Print("Enter room ID/alias: ")
	roomID, _ := reader.ReadString('\n')
	roomID = roomID[:len(roomID)-1]

	err := cli.Login(mxid, pw)
	if err != nil {
		panic(err)
	}
	fmt.Println("Authenticated")

	go cli.Sync(context.Background(), &matrix.SyncOpts{Timeout: 300, OnError: errorHandler})

	<-cli.InitialSyncDone

	room, err := matrix.LoadRoom(cli, roomID)
	if err != nil {
		panic(err)
	}

	state := matrix.Member{User: *cli.User(), Room: room}.State()
	if state != matrix.Join {
		fmt.Printf("Your state is %s. This might not work as expected\n", state)
	}

	fmt.Println("Printing room log...")

	c := make(chan matrix.Event, 1)
	room.Notify(c)

	for {
		select {
		case e := <-c:
			format(e)
		}
	}
}

func errorHandler(err error) error {
	fmt.Printf("%#v\n", err)
	return nil
}

func format(e matrix.Event) {
	switch e.Base().Type {
	case "m.room.message":
		switch e.(matrix.RoomEvent).Content["msgtype"] {
		case "m.text":
			name := e.(matrix.RoomEvent).Sender.Displayname()
			fmt.Printf("%s: %s\n", name, e.(matrix.RoomEvent).Content["body"])
		default:
			fmt.Printf("Unhandled: %+v\n", e)
		}
	default:
		fmt.Printf("Unhandled: %+v\n", e)
	}
}

D matrix/examples/simple_notify/main.go => matrix/examples/simple_notify/main.go +0 -38
@@ 1,38 0,0 @@
package main

import (
	"context"
	"fmt"

	"net/http"

	"git.sr.ht/~f4814n/frost/matrix"
	membackend "git.sr.ht/~f4814n/frost/matrix/backend/memory"
	"git.sr.ht/~f4814n/frost/matrix/examples"
)

func main() {
	cli := matrix.NewClient(matrix.ClientOpts{
		Backend:    membackend.New(),
		HTTPClient: http.DefaultClient,
	})
	mxid, pw := examples.QueryCredentials()

	err := cli.Login(mxid, pw)
	if err != nil {
		panic(err)
	}

	ch := make(chan matrix.Event)
	cli.Notify(ch)

	go func() {
		for e := range ch {
			fmt.Printf("%#v\n", e)
		}
	}()

	if err := cli.Sync(context.Background(), nil); err != nil {
		panic(err)
	}
}

D matrix/examples/util.go => matrix/examples/util.go +0 -25
@@ 1,25 0,0 @@
package examples

import (
	"bufio"
	"fmt"
	"os"
)

func QueryCredentials() (string, string) {
	reader := bufio.NewReader(os.Stdin)
	fmt.Print("Enter mxid: ")
	mxid, _ := reader.ReadString('\n')
	mxid = mxid[:len(mxid)-1]

	fmt.Print("Enter password: ")
	pw, _ := reader.ReadString('\n')
	pw = pw[:len(pw)-1]

	return mxid, pw
}

func ErrorHandler(err error) error {
	fmt.Printf("%#v\n", err)
	return nil
}

D matrix/history.go => matrix/history.go +0 -84
@@ 1,84 0,0 @@
package matrix

import (
	"git.sr.ht/~f4814n/frost/matrix/api"
)

// History makes the room history accessible.
type History struct {
	// Contains the current events. Paginated using History.Next()
	Events []Event
	Err    error

	prevToken *string
	cli       *Client
	room      Room
}

// Next uses the room messages API to traverse the history in a backwards way.
// If there is a communication error, next sets History.Err to the error and
// returns true. Even though the messages API does not return the most recent event
// (if it was sent by the last sync response), History includes those events too.
func (h *History) Next() bool {
	if h.prevToken == nil {
		var (
			prevToken string
			events    []Event
		)
		events, prevToken = h.cli.backend.LatestEvents(h.room)
		h.prevToken = &prevToken
		h.Events = events
		return true
	}

	resp, err := h.cli.api.GetRoomMessages(h.cli.auth, h.room.ID, *h.prevToken, "", "b", 10, "")
	if err != nil {
		h.Err = makeError(err)
		return true
	}

	h.processResp(resp)

	return len(h.Events) != 0
}

func (h *History) processResp(resp api.GetRoomMessagesResp) {
	h.prevToken = &resp.End

	var events []Event
	for _, e := range resp.Chunk {
		var event Event
		if e.StateKey != nil {
			event = StateEvent{
				Content:        e.Content,
				Type:           e.Type,
				ID:             e.EventID,
				OriginServerTS: timestampToTime(e.OriginServerTS),
				Unsigned: RoomEventUnsigned{ // TODO RedactedBecause
					Age:           e.Unsigned.Age,
					TransactionID: e.Unsigned.TransactionID,
				},
				StateKey:    *e.StateKey,
				PrevContent: e.PrevContent,
				Sender:      Member{User: loadUserUnsafe(h.cli, e.Sender), Room: h.room},
				Room:        h.room,
			}
		} else {
			event = RoomEvent{
				Content:        e.Content,
				Type:           e.Type,
				ID:             e.EventID,
				OriginServerTS: timestampToTime(e.OriginServerTS),
				Unsigned: RoomEventUnsigned{ // TODO RedactedBecause
					Age:           e.Unsigned.Age,
					TransactionID: e.Unsigned.TransactionID,
				},
				Sender: Member{User: loadUserUnsafe(h.cli, e.Sender), Room: h.room},
				Room:   h.room,
			}
		}
		events = append(events, event)
	}

	h.Events = events
}

D matrix/homeserver.go => matrix/homeserver.go +0 -41
@@ 1,41 0,0 @@
package matrix

import (
	"encoding/json"
	"net/http"
	"net/url"
)

// ResolveHomeserverURL gets the actual URL of a homeserver based on the server
// name from a mxid. It returns a NetworkError if the http transport was not successful and a
// LogicError if the server provided a broken response (e.g malformatted JSON)
// TODO Identity server & extra information
// XXX Really use LogicError here?
func ResolveHomeserverURL(serverName string) (*url.URL, error) {
	resp, err := http.Get("https://" + serverName + "/.well-known/matrix/client")
	if err != nil {
		return nil, NetworkError{Err: err}
	}
	defer resp.Body.Close()

	if resp.StatusCode == 404 {
		return &url.URL{Scheme: "https", Host: serverName}, nil
	}

	var wellKnown struct {
		Homeserver struct {
			BaseURL string `json:"base_url"`
		} `json:"m.homeserver"`
	}

	err = json.NewDecoder(resp.Body).Decode(&wellKnown)
	if err != nil {
		return nil, LogicError{Err: err}
	}

	url, err := url.Parse(wellKnown.Homeserver.BaseURL)
	if err != nil {
		return nil, LogicError{Err: err}
	}
	return url, nil
}

D matrix/membershipstate_string.go => matrix/membershipstate_string.go +0 -28
@@ 1,28 0,0 @@
// Code generated by "stringer -type=MembershipState"; DO NOT EDIT.

package matrix

import "strconv"

func _() {
	// An "invalid array index" compiler error signifies that the constant values have changed.
	// Re-run the stringer command to generate them again.
	var x [1]struct{}
	_ = x[Invite-0]
	_ = x[Join-1]
	_ = x[Leave-2]
	_ = x[Ban-3]
	_ = x[Knock-4]
	_ = x[Unspecified-5]
}

const _MembershipState_name = "InviteJoinLeaveBanKnockUnspecified"

var _MembershipState_index = [...]uint8{0, 6, 10, 15, 18, 23, 34}

func (i MembershipState) String() string {
	if i >= MembershipState(len(_MembershipState_index)-1) {
		return "MembershipState(" + strconv.FormatInt(int64(i), 10) + ")"
	}
	return _MembershipState_name[_MembershipState_index[i]:_MembershipState_index[i+1]]
}

D matrix/mxid.go => matrix/mxid.go +0 -36
@@ 1,36 0,0 @@
package matrix

import (
	"errors"
	"strings"
)

const allowedLocalpartRunes string = "abcdefghijklmnopqrstuvwxyz0123456789._=-/"

// SplitMxID splits a mxid and return localpart, server name and validity respectively.
func SplitMxID(id string) (localpart string, serverName string, err error) {
	if id[0] != '@' {
		err = errors.New("id does not begin with @")
		return
	}

	id = id[1:]

	sp := strings.Split(string(id), ":")
	if len(sp) != 2 {
		err = errors.New("id does not contain a colon(:)")
		return
	}

	serverName = sp[1]
	localpart = sp[0]
	for _, c := range localpart {
		if !strings.Contains(allowedLocalpartRunes, string(c)) {
			err = errors.New("id contains unallowed character")
			return
		}
	}

	err = nil
	return
}

D matrix/notifier.go => matrix/notifier.go +0 -21
@@ 1,21 0,0 @@
package matrix

// A Notifier (Client, User, Member, Room) can receive updated events from a running
// Client.
type Notifier interface {
	// A copy of each event relecant to the Updater is sent here. It is up to the
	// use to ensure that the channel is empty. The event will be discarded, if
	// the channel is not empty. Calling this method with the same argument
	// twice will panic.
	Notify(chan<- Event)

	// Stop sending to the specified channel. This panics if the channel was
	// not registered before using Updater.Update() or if called multiple
	// times.
	Stop(chan<- Event)
}

type notifierChannel struct {
	send func(e Event) bool
	ch   chan<- Event
}

D matrix/room.go => matrix/room.go +0 -170
@@ 1,170 0,0 @@
package matrix

import (
	"errors"
	"time"
)

// Room represents a matrix room. A room value is always tied to a client, that
// is used to do the actual communication. Multiple room values describing the
// same room may be used simultaneously. Room is thread-safe.
type Room struct {
	// The matrix ID of the room
	ID string

	cli *Client
}

// loadRoomUnsafe just creates a room object without checking for existence.
func loadRoomUnsafe(cli *Client, id string) Room {
	return Room{
		ID:  id,
		cli: cli,
	}
}

// LoadRoom takes a id or alias as argument and calls LoadRoomID or LoadRoomAlias
// depending on the first sign of the id.
func LoadRoom(cli *Client, id string) (Room, error) {
	if id[0] == '!' {
		return LoadRoomID(cli, id)
	} else if id[0] == '#' {
		return LoadRoomAlias(cli, id)
	}

	return Room{}, LogicError{Err: errors.New("invalid room alias")}
}

// LoadRoomID loads a existent room identified by the room ID. This call is very
// fast (no HTTP Request) for rooms the user is invited to, has joined or has
// already left (for all rooms that are somehow part of the /sync endpoint).
// For other rooms this tries to look up the ID.
func LoadRoomID(cli *Client, id string) (Room, error) {
	room := Room{
		ID:  id,
		cli: cli,
	}

	// Check if we already know the room exists
	if ev := cli.backend.RoomState(room, "m.room.create", ""); ev != nil {
		return room, nil
	}

	// We have never seen the room. Query the room directory
	_, err := cli.api.GetRoomVisibility(id)
	return room, makeError(err)
}

// LoadRoomAlias loads a existent room identified by the room alias.
func LoadRoomAlias(cli *Client, alias string) (Room, error) {
	resp, err := cli.api.GetRoomAliasID(alias)
	if err != nil {
		return Room{}, makeError(err)
	}

	room := Room{
		ID:  resp.RoomID,
		cli: cli,
	}
	return room, nil
}

// GetState gets a specific event from the room state. If no such state event is
// known the return value is nil.
func (r Room) GetState(eventType, stateKey string) *StateEvent {
	return r.cli.backend.RoomState(r, eventType, stateKey)
}

// SendRoomEvent sends a room event to the room and returns the event ID and
// possibly an error.
func (r Room) SendRoomEvent(eventType string, content map[string]interface{}) (string, error) {
	r.cli.mut.RLock()
	defer r.cli.mut.RUnlock()

	resp, err := r.cli.api.PutRoomEvent(r.cli.auth, r.ID, eventType, time.Now().String(), content)
	return resp.EventID, makeError(err)
}

// SendStateEvent sends a state event to the room and returns the event ID and
// possibly an error.
func (r Room) SendStateEvent(eventType string, stateKey string, content map[string]interface{}) (string, error) {
	r.cli.mut.RLock()
	defer r.cli.mut.RUnlock()

	resp, err := r.cli.api.PutStateEvent(r.cli.auth, r.ID, eventType, stateKey, content)
	return resp.EventID, makeError(err)
}

// Members returns all Members of this room. At the moment lazy loading of room members
// is not used. So this method can not return an error.
func (r Room) Members() []Member {
	r.cli.mut.RLock()
	defer r.cli.mut.RUnlock()

	m := r.cli.backend.RoomStateList(r, "m.room.member")
	members := make([]Member, len(m))
	for _, m := range m {
		member := Member{
			Room: r,
			User: loadUserUnsafe(r.cli, m.StateKey),
		}

		members = append(members, member)
	}

	return members
}

// History makes the history beginning from the most recently received prev_batch
// (/sync endpoint) accessible.
func (r Room) History() History {
	return History{
		room: r,
		cli:  r.cli,
	}
}

// Invite a User to a Room.
func (r Room) Invite(u User) error {
	return nil
}

// Displayname returns the rooms display name. The name is calculated according to
// https://matrix.org/docs/spec/client_server/r0.5.0#calculating-the-display-name-for-a-room
func (r Room) Displayname() string {
	if state := r.cli.backend.RoomState(r, "m.room.name", ""); state != nil {
		if name, ok := state.Content["name"]; ok && name != "" {
			return name.(string)
		}
	}

	if state := r.cli.backend.RoomState(r, "m.room.canonical_alias", ""); state != nil {
		if alias, ok := state.Content["alias"]; ok && alias != "" {
			return alias.(string)
		}
	}

	return r.ID
}

// AccountData returns an AccountDataEvent of the given type if it exists
func (r Room) AccountData(type_ string) *AccountDataEvent {
	return r.cli.backend.AccountData(&r, type_)
}

// Notify implements the Notifier interface
func (r Room) Notify(c chan<- Event) {
	r.cli.mut.Lock()
	defer r.cli.mut.Unlock()

	send := func(e Event) bool {
		return inRoom(e, r)
	}

	r.cli.notifierChannels = append(r.cli.notifierChannels, notifierChannel{send: send, ch: c})
}

// Stop implements the Notifier interface
func (r Room) Stop(c chan<- Event) {
	r.cli.Stop(c)
}

D matrix/user.go => matrix/user.go +0 -132
@@ 1,132 0,0 @@
package matrix

//go:generate stringer -type=MembershipState
type MembershipState uint8

const (
	Invite MembershipState = iota
	Join
	Leave
	Ban
	Knock
	Unspecified
)

// User is matrix User.
type User struct {
	// The matrix ID of the user
	ID string

	cli *Client
}

func loadUserUnsafe(cli *Client, id string) User {
	return User{
		ID:  id,
		cli: cli,
	}
}

// LoadUser tries to find a matrix user. It returns a LogicError if the user does
// not exist.
func LoadUser(cli *Client, id string) (User, error) {
	return User{}, nil
}

// Displayname gets the display name of the user by calling the HTTP API.
func (u User) Displayname() (string, error) {
	resp, err := u.cli.api.GetUserDisplayname(u.ID)
	return resp.Displayname, makeError(err)
}

// Notify implements the Notifier interface. This can not be used to be notified about ephemeral events
// relevant for the user (e.g. m.typing events), since it is not possible to attribute all ephemeral events
// to a certain set of users.
func (u User) Notify(c chan<- Event) {
	u.cli.mut.Lock()
	defer u.cli.mut.Unlock()

	send := func(e Event) bool {
		switch e := e.(type) {
		case StateEvent:
			return e.Sender.ID == u.ID
		case RoomEvent:
			return e.Sender.ID == u.ID
		}
		return false
	}

	u.cli.notifierChannels = append(u.cli.notifierChannels, notifierChannel{send: send, ch: c})
}

// Stop implements the Notifier interface
func (u User) Stop(c chan<- Event) {
	u.cli.Stop(c)
}

// Member is a User with the additional information from a single room.
type Member struct {
	User
	Room Room
}

// Displayname gets the displayname of the user by searching m.room.member state
// events. If no name is set it returns the users MxID.
func (m Member) Displayname() string {
	state := m.Room.cli.backend.RoomState(m.Room, "m.room.member", m.User.ID)
	if state == nil {
		return m.User.ID
	}

	if s, ok := state.Content["displayname"]; ok {
		return s.(string)
	}
	return m.User.ID
}

func (m Member) State() MembershipState {
	state := m.Room.cli.backend.RoomState(m.Room, "m.room.member", m.User.ID)
	if state != nil {
		return Unspecified
	}

	switch state.Content["membership"] {
	case "invite":
		return Invite
	case "join":
		return Join
	case "knock":
		return Knock
	case "leave":
		return Leave
	case "ban":
		return Ban
	default:
		panic("unknown state key")
	}
}

// Notify implements the Notifier interface. This can not be used to be notified about ephemeral events
// relevant for the user (e.g. m.typing events), since it is not possible to attribute all ephemeral events
// to a certain set of users.
func (m Member) Notify(c chan<- Event) {
	m.cli.mut.Lock()
	defer m.cli.mut.Unlock()

	send := func(e Event) bool {
		switch e := e.(type) {
		case StateEvent:
			return e.Sender.ID == m.ID && e.Room.ID == m.Room.ID
		case RoomEvent:
			return e.Sender.ID == m.ID && e.Room.ID == m.Room.ID
		}
		return false
	}

	m.cli.notifierChannels = append(m.cli.notifierChannels, notifierChannel{send: send, ch: c})
}

// Stop implements the Notifier interface
func (m Member) Stop(c chan<- Event) {
	m.cli.Stop(c)
}

D matrix/util.go => matrix/util.go +0 -25
@@ 1,25 0,0 @@
package matrix

import (
	"time"
)

func timestampToTime(ts int64) time.Time {
	return time.Unix(0, ts*int64(1000000))
}

func inRoom(e Event, r Room) bool {
	switch e := e.(type) {
	case RoomEvent:
		return e.Room.ID == r.ID
	case StateEvent:
		return e.Room.ID == r.ID
	case AccountDataEvent:
		if e.Room() != nil {
			return e.Room().ID == r.ID
		}
	case EphemeralEvent:
		return e.Room().ID == r.ID
	}
	return false
}