From ba387764ca1590625d349e74eb8a8a64d1849b67 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 14 Nov 2018 00:00:35 +0200 Subject: Fix things --- Gopkg.lock | 94 ++- Gopkg.toml | 2 +- README.md | 2 +- config/config.go | 6 +- interface/matrix.go | 10 +- interface/ui.go | 12 +- matrix/doc.go | 2 +- matrix/matrix.go | 86 ++- matrix/matrix_test.go | 10 +- matrix/pushrules/condition.go | 14 +- matrix/pushrules/condition_test.go | 10 +- matrix/pushrules/pushrules.go | 8 +- matrix/pushrules/pushrules_test.go | 4 +- matrix/pushrules/rule.go | 14 +- matrix/pushrules/ruleset.go | 4 +- matrix/rooms/room.go | 48 +- matrix/rooms/room_test.go | 18 +- matrix/sync.go | 43 +- matrix/sync_test.go | 66 +- ui/commands.go | 6 +- ui/messages/base.go | 10 +- ui/messages/expandedtextmessage.go | 4 +- ui/messages/imagemessage.go | 4 +- ui/messages/parser/htmlparser.go | 9 +- ui/messages/parser/parser.go | 37 +- ui/messages/textmessage.go | 6 +- ui/room-view.go | 8 +- ui/view-login.go | 6 +- ui/view-main.go | 8 +- ui/widget/border.go | 4 +- vendor/golang.org/x/image/bmp/reader.go | 30 +- vendor/golang.org/x/image/tiff/reader.go | 2 +- vendor/golang.org/x/image/vp8/decode.go | 4 +- vendor/golang.org/x/image/webp/decode.go | 2 - vendor/golang.org/x/image/webp/doc.go | 9 + vendor/golang.org/x/image/webp/webp.go | 30 - vendor/golang.org/x/net/html/const.go | 10 +- vendor/golang.org/x/net/html/parse.go | 29 +- .../gopkg.in/russross/blackfriday.v2/.travis.yml | 27 +- vendor/gopkg.in/russross/blackfriday.v2/README.md | 20 +- vendor/gopkg.in/russross/blackfriday.v2/block.go | 101 ++- vendor/gopkg.in/russross/blackfriday.v2/go.mod | 1 + vendor/gopkg.in/russross/blackfriday.v2/html.go | 21 +- vendor/gopkg.in/russross/blackfriday.v2/inline.go | 18 +- .../gopkg.in/russross/blackfriday.v2/markdown.go | 94 +-- vendor/maunium.net/go/gomatrix/.gitignore | 24 - vendor/maunium.net/go/gomatrix/.travis.yml | 9 - vendor/maunium.net/go/gomatrix/LICENSE | 201 ------ vendor/maunium.net/go/gomatrix/README.md | 6 - vendor/maunium.net/go/gomatrix/client.go | 794 -------------------- vendor/maunium.net/go/gomatrix/events.go | 413 ----------- vendor/maunium.net/go/gomatrix/filter.go | 90 --- vendor/maunium.net/go/gomatrix/reply.go | 96 --- vendor/maunium.net/go/gomatrix/requests.go | 82 --- vendor/maunium.net/go/gomatrix/responses.go | 182 ----- vendor/maunium.net/go/gomatrix/room.go | 44 -- vendor/maunium.net/go/gomatrix/store.go | 65 -- vendor/maunium.net/go/gomatrix/sync.go | 159 ---- vendor/maunium.net/go/gomatrix/userids.go | 130 ---- vendor/maunium.net/go/maulogger/LICENSE | 21 - vendor/maunium.net/go/maulogger/README.md | 6 - vendor/maunium.net/go/maulogger/logger.go | 219 ------ vendor/maunium.net/go/mautrix/.gitignore | 2 + vendor/maunium.net/go/mautrix/LICENSE | 201 ++++++ vendor/maunium.net/go/mautrix/README.md | 4 + vendor/maunium.net/go/mautrix/client.go | 796 +++++++++++++++++++++ vendor/maunium.net/go/mautrix/events.go | 448 ++++++++++++ vendor/maunium.net/go/mautrix/filter.go | 90 +++ vendor/maunium.net/go/mautrix/reply.go | 97 +++ vendor/maunium.net/go/mautrix/requests.go | 82 +++ vendor/maunium.net/go/mautrix/responses.go | 182 +++++ vendor/maunium.net/go/mautrix/room.go | 44 ++ vendor/maunium.net/go/mautrix/store.go | 65 ++ vendor/maunium.net/go/mautrix/sync.go | 159 ++++ vendor/maunium.net/go/mautrix/userids.go | 130 ++++ vendor/maunium.net/go/tcell/README.adoc | 270 +++++++ vendor/maunium.net/go/tcell/cell.go | 7 +- vendor/maunium.net/go/tcell/console_win.go | 1 - vendor/maunium.net/go/tcell/tcell.png | Bin 5336 -> 0 bytes vendor/maunium.net/go/tcell/tcell.svg | 93 --- .../maunium.net/go/tcell/terminfo/term_termite.go | 152 ++++ vendor/maunium.net/go/tcell/tscreen.go | 6 +- vendor/maunium.net/go/tview/LICENSE.txt | 2 +- vendor/maunium.net/go/tview/README.md | 11 +- vendor/maunium.net/go/tview/ansi.go | 237 ++++++ vendor/maunium.net/go/tview/ansii.go | 237 ------ vendor/maunium.net/go/tview/application.go | 376 +++++++--- vendor/maunium.net/go/tview/borders.go | 45 ++ vendor/maunium.net/go/tview/box.go | 88 +-- vendor/maunium.net/go/tview/checkbox.go | 6 +- vendor/maunium.net/go/tview/doc.go | 31 +- vendor/maunium.net/go/tview/dropdown.go | 5 + vendor/maunium.net/go/tview/flex.go | 17 +- vendor/maunium.net/go/tview/form.go | 53 +- vendor/maunium.net/go/tview/grid.go | 16 +- vendor/maunium.net/go/tview/inputfield.go | 205 ++++-- vendor/maunium.net/go/tview/list.go | 15 +- vendor/maunium.net/go/tview/modal.go | 15 + vendor/maunium.net/go/tview/semigraphics.go | 296 ++++++++ vendor/maunium.net/go/tview/table.go | 119 ++- vendor/maunium.net/go/tview/textview.go | 266 ++++--- vendor/maunium.net/go/tview/treeview.go | 684 ++++++++++++++++++ vendor/maunium.net/go/tview/util.go | 658 ++++++++--------- 103 files changed, 5732 insertions(+), 3973 deletions(-) create mode 100644 vendor/golang.org/x/image/webp/doc.go delete mode 100644 vendor/golang.org/x/image/webp/webp.go create mode 100644 vendor/gopkg.in/russross/blackfriday.v2/go.mod delete mode 100644 vendor/maunium.net/go/gomatrix/.gitignore delete mode 100644 vendor/maunium.net/go/gomatrix/.travis.yml delete mode 100644 vendor/maunium.net/go/gomatrix/LICENSE delete mode 100644 vendor/maunium.net/go/gomatrix/README.md delete mode 100644 vendor/maunium.net/go/gomatrix/client.go delete mode 100644 vendor/maunium.net/go/gomatrix/events.go delete mode 100644 vendor/maunium.net/go/gomatrix/filter.go delete mode 100644 vendor/maunium.net/go/gomatrix/reply.go delete mode 100644 vendor/maunium.net/go/gomatrix/requests.go delete mode 100644 vendor/maunium.net/go/gomatrix/responses.go delete mode 100644 vendor/maunium.net/go/gomatrix/room.go delete mode 100644 vendor/maunium.net/go/gomatrix/store.go delete mode 100644 vendor/maunium.net/go/gomatrix/sync.go delete mode 100644 vendor/maunium.net/go/gomatrix/userids.go delete mode 100644 vendor/maunium.net/go/maulogger/LICENSE delete mode 100644 vendor/maunium.net/go/maulogger/README.md delete mode 100644 vendor/maunium.net/go/maulogger/logger.go create mode 100644 vendor/maunium.net/go/mautrix/.gitignore create mode 100644 vendor/maunium.net/go/mautrix/LICENSE create mode 100644 vendor/maunium.net/go/mautrix/README.md create mode 100644 vendor/maunium.net/go/mautrix/client.go create mode 100644 vendor/maunium.net/go/mautrix/events.go create mode 100644 vendor/maunium.net/go/mautrix/filter.go create mode 100644 vendor/maunium.net/go/mautrix/reply.go create mode 100644 vendor/maunium.net/go/mautrix/requests.go create mode 100644 vendor/maunium.net/go/mautrix/responses.go create mode 100644 vendor/maunium.net/go/mautrix/room.go create mode 100644 vendor/maunium.net/go/mautrix/store.go create mode 100644 vendor/maunium.net/go/mautrix/sync.go create mode 100644 vendor/maunium.net/go/mautrix/userids.go create mode 100644 vendor/maunium.net/go/tcell/README.adoc delete mode 100644 vendor/maunium.net/go/tcell/tcell.png delete mode 100644 vendor/maunium.net/go/tcell/tcell.svg create mode 100644 vendor/maunium.net/go/tcell/terminfo/term_termite.go create mode 100644 vendor/maunium.net/go/tview/ansi.go delete mode 100644 vendor/maunium.net/go/tview/ansii.go create mode 100644 vendor/maunium.net/go/tview/borders.go create mode 100644 vendor/maunium.net/go/tview/semigraphics.go create mode 100644 vendor/maunium.net/go/tview/treeview.go diff --git a/Gopkg.lock b/Gopkg.lock index 25c518f..5e46ec5 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,79 +2,104 @@ [[projects]] + digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec" name = "github.com/davecgh/go-spew" packages = ["spew"] + pruneopts = "UT" revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" version = "v1.1.1" [[projects]] + digest = "1:2694597a20d303502ad1b7f1cc688977c7dc34da732ccf4d27e98b552cceed5b" name = "github.com/disintegration/imaging" packages = ["."] + pruneopts = "UT" revision = "0bd5694c78c9c3d9a3cd06a706a8f3c59296a9ac" version = "v1.5.0" [[projects]] branch = "master" + digest = "1:40d0056c1b1f503c366ba441df92a82b5a2654d6f3747b1689a611eb5c9ce0a2" name = "github.com/gdamore/encoding" packages = ["."] + pruneopts = "UT" revision = "b23993cbb6353f0e6aa98d0ee318a34728f628b9" [[projects]] + digest = "1:f4bd9b6cfcbaafa94259861747781022d8f06ff3c88aafe49a815ac02609bc96" name = "github.com/kyokomi/emoji" packages = ["."] + pruneopts = "UT" revision = "2e9a9507333f3ee28f3fab88c2c3aba34455d734" version = "v1.5.1" [[projects]] + digest = "1:c65a16ac77d0b1aefc7009cabb6ac5ad05def02025f5be85f450c03f52cc6f86" name = "github.com/lucasb-eyer/go-colorful" packages = ["."] + pruneopts = "UT" revision = "345fbb3dbcdb252d9985ee899a84963c0fa24c82" version = "v1.0" [[projects]] + digest = "1:cdb899c199f907ac9fb50495ec71212c95cb5b0e0a8ee0800da0238036091033" name = "github.com/mattn/go-runewidth" packages = ["."] + pruneopts = "UT" revision = "ce7b0b5c7b45a81508558cd1dba6bb1e4ddb51bb" version = "v0.0.3" [[projects]] branch = "master" + digest = "1:0e1e5f960c58fdc677212fcc70e55042a0084d367623e51afbdb568963832f5d" name = "github.com/nu7hatch/gouuid" packages = ["."] + pruneopts = "UT" revision = "179d4d0c4d8d407a32af483c2354df1d2c91e6c3" [[projects]] + digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe" name = "github.com/pmezard/go-difflib" packages = ["difflib"] + pruneopts = "UT" revision = "792786c7400a136282c1664665ae0a8db921c6c2" version = "v1.0.0" [[projects]] + branch = "master" + digest = "1:3fd3d634f6815f19ac4b2c5e16d28ec9aa4584d0bba25d1ee6c424d813cca22a" name = "github.com/renstrom/fuzzysearch" packages = ["fuzzy"] + pruneopts = "UT" revision = "b18e754edff4833912ef4dce9eaca885bd3f0de1" - version = "v1.0.1" [[projects]] branch = "master" + digest = "1:def689e73e9252f6f7fe66834a76751a41b767e03daab299e607e7226c58a855" name = "github.com/shurcooL/sanitized_anchor_name" packages = ["."] + pruneopts = "UT" revision = "86672fcb3f950f35f2e675df2240550f2a50762f" [[projects]] + digest = "1:18752d0b95816a1b777505a97f71c7467a8445b8ffb55631a7bf779f6ba4fa83" name = "github.com/stretchr/testify" packages = ["assert"] + pruneopts = "UT" revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686" version = "v1.2.2" [[projects]] branch = "master" + digest = "1:22738a5261588086d75acb3c792f7c931d30fd32e351117e9e046331135f5297" name = "github.com/zyedidia/clipboard" packages = ["."] + pruneopts = "UT" revision = "bd31d747117d04b4e25b61f73e1ea4faeea3c56a" [[projects]] branch = "master" + digest = "1:441333ba1f7f3074035297647244e3d1ce3665e4e7c5054b3a8b138463f8ff58" name = "golang.org/x/image" packages = [ "bmp", @@ -83,79 +108,108 @@ "tiff/lzw", "vp8", "vp8l", - "webp" + "webp", ] - revision = "c73c2afc3b812cdd6385de5a50616511c4a3d458" + pruneopts = "UT" + revision = "249dc8530c0efc2766606a940c1c50b434b2f1cd" [[projects]] branch = "master" + digest = "1:1a1ecfa7b54ca3f7a0115ab5c578d7d6a5d8b605839c549e80260468c42f8be7" name = "golang.org/x/net" packages = [ "html", - "html/atom" + "html/atom", ] - revision = "8a410e7b638dca158bf9e766925842f6651ff828" + pruneopts = "UT" + revision = "88d92db4c548972d942ac2a3531a8a9a34c82ca6" [[projects]] + digest = "1:37672ad5821719e2df8509c2edd4ba5ae192463237c73c3a2d24ef8b2bc9e36f" name = "golang.org/x/text" packages = [ "encoding", "encoding/internal/identifier", "internal/gen", "transform", - "unicode/cldr" + "unicode/cldr", ] + pruneopts = "UT" revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" version = "v0.3.0" [[projects]] + digest = "1:2ee0f15eb0fb04f918db7c2dcf39745f40d69f798ef171610a730e8a56aaa4fd" name = "gopkg.in/russross/blackfriday.v2" packages = ["."] - revision = "cadec560ec52d93835bf2f15bd794700d3a2473b" - version = "v2.0.0" + pruneopts = "UT" + revision = "d3b5b032dc8e8927d31a5071b56e14c89f045135" + version = "v2.0.1" [[projects]] branch = "v1" + digest = "1:b71ccd8615521db4ea2f9381f55780a1312654f5d8431d2e58eca51c74110de2" name = "gopkg.in/toast.v1" packages = ["."] + pruneopts = "UT" revision = "0a84660828b24d25b35525c9a1f1f51267f8da91" [[projects]] + digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202" name = "gopkg.in/yaml.v2" packages = ["."] + pruneopts = "UT" revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" version = "v2.2.1" [[projects]] branch = "master" - name = "maunium.net/go/gomatrix" - packages = ["."] - revision = "d651abc3ecb4bbd85a6b17b710632e73ddbbc6aa" - -[[projects]] - name = "maunium.net/go/maulogger" + digest = "1:d23e0bbfa4e45fe847e61fdfd442b24adf9bd3d0b050a846f9ef0f3af4f43c86" + name = "maunium.net/go/mautrix" packages = ["."] - revision = "64f0aa33b6c51313e15575257db71dec44fe7988" - version = "v1.0" + pruneopts = "UT" + revision = "e8080dcf484d1db9021d2019fee132ffc9e37e3c" [[projects]] branch = "master" + digest = "1:28d6d6123ce29c9b0b5d393af0085f1454a9d93d70b4c9893efe6cff025702b9" name = "maunium.net/go/tcell" packages = [ ".", - "terminfo" + "terminfo", ] - revision = "f32e44a866ab0475655e11ede91337d7b562956d" + pruneopts = "UT" + revision = "a62189e4543d731b94ae0d3b659d1853975ea657" [[projects]] branch = "master" + digest = "1:b4c2e264221b6afa98aec4e0a097f3775cb014eebac8a6f49d33c53b237b77af" name = "maunium.net/go/tview" packages = ["."] - revision = "8b261597bbdb95dcaef03854aaa0cc192f56b1ff" + pruneopts = "UT" + revision = "dae31f32cda75339f5ec2a64411ef3ec1862ef5e" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "6c0a263ebffa1c073f4b67a895832017afeab906b0d88b31da38ae22bc75e1aa" + input-imports = [ + "github.com/disintegration/imaging", + "github.com/kyokomi/emoji", + "github.com/lucasb-eyer/go-colorful", + "github.com/mattn/go-runewidth", + "github.com/renstrom/fuzzysearch/fuzzy", + "github.com/stretchr/testify/assert", + "github.com/zyedidia/clipboard", + "golang.org/x/image/bmp", + "golang.org/x/image/tiff", + "golang.org/x/image/webp", + "golang.org/x/net/html", + "gopkg.in/russross/blackfriday.v2", + "gopkg.in/toast.v1", + "gopkg.in/yaml.v2", + "maunium.net/go/mautrix", + "maunium.net/go/tcell", + "maunium.net/go/tview", + ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 5927463..2fa3148 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -63,7 +63,7 @@ [[constraint]] branch = "master" - name = "maunium.net/go/gomatrix" + name = "maunium.net/go/mautrix" [[constraint]] branch = "master" diff --git a/README.md b/README.md index 79509d3..e39c33b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ![Chat Preview](chat-preview.png) -A terminal Matrix client written in Go using [gomatrix](https://github.com/matrix-org/gomatrix) and [tview](https://github.com/rivo/tview). +A terminal Matrix client written in Go using [mautrix](https://github.com/matrix-org/mautrix) and [tview](https://github.com/rivo/tview). Basic usage is possible, but expect bugs and missing features. diff --git a/config/config.go b/config/config.go index e4fb743..8f45d76 100644 --- a/config/config.go +++ b/config/config.go @@ -25,7 +25,7 @@ import ( "strings" "gopkg.in/yaml.v2" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/rooms" @@ -289,12 +289,12 @@ func (config *Config) PutRoom(room *rooms.Room) { room.Save(config.getRoomCachePath(room)) } -func (config *Config) SaveRoom(room *gomatrix.Room) { +func (config *Config) SaveRoom(room *mautrix.Room) { gmxRoom := config.GetRoom(room.ID) gmxRoom.Room = room gmxRoom.Save(config.getRoomCachePath(gmxRoom)) } -func (config *Config) LoadRoom(roomID string) *gomatrix.Room { +func (config *Config) LoadRoom(roomID string) *mautrix.Room { return config.GetRoom(roomID).Room } diff --git a/interface/matrix.go b/interface/matrix.go index 0538df9..01f224e 100644 --- a/interface/matrix.go +++ b/interface/matrix.go @@ -17,12 +17,12 @@ package ifc import ( - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/matrix/rooms" ) type MatrixContainer interface { - Client() *gomatrix.Client + Client() *mautrix.Client InitClient() error Initialized() bool @@ -33,14 +33,14 @@ type MatrixContainer interface { Logout() SendPreferencesToMatrix() - SendMessage(roomID string, msgtype gomatrix.MessageType, message string) (string, error) - SendMarkdownMessage(roomID string, msgtype gomatrix.MessageType, message string) (string, error) + SendMessage(roomID string, msgtype mautrix.MessageType, message string) (string, error) + SendMarkdownMessage(roomID string, msgtype mautrix.MessageType, message string) (string, error) SendTyping(roomID string, typing bool) MarkRead(roomID, eventID string) JoinRoom(roomID, server string) (*rooms.Room, error) LeaveRoom(roomID string) error - GetHistory(roomID, prevBatch string, limit int) ([]*gomatrix.Event, string, error) + GetHistory(roomID, prevBatch string, limit int) ([]*mautrix.Event, string, error) GetRoom(roomID string) *rooms.Room Download(mxcURL string) ([]byte, string, string, error) diff --git a/interface/ui.go b/interface/ui.go index 929dc65..39473f5 100644 --- a/interface/ui.go +++ b/interface/ui.go @@ -19,7 +19,7 @@ package ifc import ( "time" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/tcell" @@ -50,7 +50,7 @@ type MainView interface { UpdateTags(room *rooms.Room) SetTyping(roomID string, users []string) - ParseEvent(roomView RoomView, evt *gomatrix.Event) Message + ParseEvent(roomView RoomView, evt *mautrix.Event) Message NotifyMessage(room *rooms.Room, message Message, should pushrules.PushActionArrayShould) InitialSyncDone() @@ -73,8 +73,8 @@ type RoomView interface { SetTyping(users []string) UpdateUserList() - NewMessage(id, sender string, msgtype gomatrix.MessageType, text string, timestamp time.Time) Message - NewTempMessage(msgtype gomatrix.MessageType, text string) Message + NewMessage(id, sender string, msgtype mautrix.MessageType, text string, timestamp time.Time) Message + NewTempMessage(msgtype mautrix.MessageType, text string) Message AddMessage(message Message, direction MessageDirection) AddServiceMessage(message string) } @@ -111,8 +111,8 @@ type Message interface { SetID(id string) ID() string - SetType(msgtype gomatrix.MessageType) - Type() gomatrix.MessageType + SetType(msgtype mautrix.MessageType) + Type() mautrix.MessageType NotificationContent() string diff --git a/matrix/doc.go b/matrix/doc.go index 2cd54e2..f789895 100644 --- a/matrix/doc.go +++ b/matrix/doc.go @@ -1,2 +1,2 @@ -// Package matrix contains wrappers for gomatrix for use by the UI of gomuks. +// Package matrix contains wrappers for mautrix for use by the UI of gomuks. package matrix diff --git a/matrix/matrix.go b/matrix/matrix.go index 060becd..cf9e463 100644 --- a/matrix/matrix.go +++ b/matrix/matrix.go @@ -34,20 +34,20 @@ import ( "encoding/json" "gopkg.in/russross/blackfriday.v2" - "maunium.net/go/gomatrix" "maunium.net/go/gomuks/config" "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/lib/bfhtml" "maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/rooms" + "maunium.net/go/mautrix" ) -// Container is a wrapper for a gomatrix Client and some other stuff. +// Container is a wrapper for a mautrix Client and some other stuff. // // It is used for all Matrix calls from the UI and Matrix event handlers. type Container struct { - client *gomatrix.Client + client *mautrix.Client syncer *GomuksSyncer gmx ifc.Gomuks ui ifc.GomuksUI @@ -69,12 +69,18 @@ func NewContainer(gmx ifc.Gomuks) *Container { return c } -// Client returns the underlying gomatrix Client. -func (c *Container) Client() *gomatrix.Client { +// Client returns the underlying mautrix Client. +func (c *Container) Client() *mautrix.Client { return c.client } -// InitClient initializes the gomatrix client and connects to the homeserver specified in the config. +type mxLogger struct{} + +func (log mxLogger) Debugfln(message string, args ...interface{}) { + debug.Printf("[Matrix] "+message, args...) +} + +// InitClient initializes the mautrix client and connects to the homeserver specified in the config. func (c *Container) InitClient() error { if len(c.config.HS) == 0 { return fmt.Errorf("no homeserver in config") @@ -92,10 +98,11 @@ func (c *Container) InitClient() error { } var err error - c.client, err = gomatrix.NewClient(c.config.HS, mxid, accessToken) + c.client, err = mautrix.NewClient(c.config.HS, mxid, accessToken) if err != nil { return err } + c.client.Logger = mxLogger{} allowInsecure := len(os.Getenv("GOMUKS_ALLOW_INSECURE_CONNECTIONS")) > 0 if allowInsecure { @@ -112,14 +119,14 @@ func (c *Container) InitClient() error { return nil } -// Initialized returns whether or not the gomatrix client is initialized (see InitClient()) +// Initialized returns whether or not the mautrix client is initialized (see InitClient()) func (c *Container) Initialized() bool { return c.client != nil } // Login sends a password login request with the given username and password. func (c *Container) Login(user, password string) error { - resp, err := c.client.Login(&gomatrix.ReqLogin{ + resp, err := c.client.Login(&mautrix.ReqLogin{ Type: "m.login.password", User: user, Password: password, @@ -175,7 +182,7 @@ func (c *Container) PushRules() *pushrules.PushRuleset { return c.config.PushRules } -var AccountDataGomuksPreferences = gomatrix.NewEventType("net.maunium.gomuks.preferences") +var AccountDataGomuksPreferences = mautrix.NewEventType("net.maunium.gomuks.preferences") // OnLogin initializes the syncer and updates the room list. func (c *Container) OnLogin() { @@ -185,15 +192,16 @@ func (c *Container) OnLogin() { debug.Print("Initializing syncer") c.syncer = NewGomuksSyncer(c.config) - c.syncer.OnEventType(gomatrix.EventMessage, c.HandleMessage) - c.syncer.OnEventType(gomatrix.StateMember, c.HandleMembership) - c.syncer.OnEventType(gomatrix.EphemeralEventReceipt, c.HandleReadReceipt) - c.syncer.OnEventType(gomatrix.EphemeralEventTyping, c.HandleTyping) - c.syncer.OnEventType(gomatrix.AccountDataDirectChats, c.HandleDirectChatInfo) - c.syncer.OnEventType(gomatrix.AccountDataPushRules, c.HandlePushRules) - c.syncer.OnEventType(gomatrix.AccountDataRoomTags, c.HandleTag) + c.syncer.OnEventType(mautrix.EventMessage, c.HandleMessage) + c.syncer.OnEventType(mautrix.StateMember, c.HandleMembership) + c.syncer.OnEventType(mautrix.EphemeralEventReceipt, c.HandleReadReceipt) + c.syncer.OnEventType(mautrix.EphemeralEventTyping, c.HandleTyping) + c.syncer.OnEventType(mautrix.AccountDataDirectChats, c.HandleDirectChatInfo) + c.syncer.OnEventType(mautrix.AccountDataPushRules, c.HandlePushRules) + c.syncer.OnEventType(mautrix.AccountDataRoomTags, c.HandleTag) c.syncer.OnEventType(AccountDataGomuksPreferences, c.HandlePreferences) c.syncer.InitDoneCallback = func() { + debug.Print("Initial sync done") c.config.AuthCache.InitialSyncDone = true c.config.SaveAuthCache() c.ui.MainView().InitialSyncDone() @@ -227,7 +235,7 @@ func (c *Container) Start() { return default: if err := c.client.Sync(); err != nil { - if httpErr, ok := err.(gomatrix.HTTPError); ok && httpErr.Code == http.StatusUnauthorized { + if httpErr, ok := err.(mautrix.HTTPError); ok && httpErr.Code == http.StatusUnauthorized { debug.Print("Sync() errored with ", err, " -> logging out") c.Logout() } else { @@ -240,7 +248,7 @@ func (c *Container) Start() { } } -func (c *Container) HandlePreferences(source EventSource, evt *gomatrix.Event) { +func (c *Container) HandlePreferences(source EventSource, evt *mautrix.Event) { orig := c.config.Preferences rt, _ := json.Marshal(&evt.Content) json.Unmarshal(rt, &c.config.Preferences) @@ -259,7 +267,7 @@ func (c *Container) SendPreferencesToMatrix() { } // HandleMessage is the event handler for the m.room.message timeline event. -func (c *Container) HandleMessage(source EventSource, evt *gomatrix.Event) { +func (c *Container) HandleMessage(source EventSource, evt *mautrix.Event) { if source&EventSourceLeave != 0 { return } @@ -286,7 +294,7 @@ func (c *Container) HandleMessage(source EventSource, evt *gomatrix.Event) { } // HandleMembership is the event handler for the m.room.member state event. -func (c *Container) HandleMembership(source EventSource, evt *gomatrix.Event) { +func (c *Container) HandleMembership(source EventSource, evt *mautrix.Event) { isLeave := source&EventSourceLeave != 0 isTimeline := source&EventSourceTimeline != 0 isNonTimelineLeave := isLeave && !isTimeline @@ -302,9 +310,9 @@ func (c *Container) HandleMembership(source EventSource, evt *gomatrix.Event) { c.HandleMessage(source, evt) } -func (c *Container) processOwnMembershipChange(evt *gomatrix.Event) { +func (c *Container) processOwnMembershipChange(evt *mautrix.Event) { membership := evt.Content.Membership - prevMembership := gomatrix.MembershipLeave + prevMembership := mautrix.MembershipLeave if evt.Unsigned.PrevContent != nil { prevMembership = evt.Unsigned.PrevContent.Membership } @@ -326,7 +334,7 @@ func (c *Container) processOwnMembershipChange(evt *gomatrix.Event) { } } -func (c *Container) parseReadReceipt(evt *gomatrix.Event) (largestTimestampEvent string) { +func (c *Container) parseReadReceipt(evt *mautrix.Event) (largestTimestampEvent string) { var largestTimestamp int64 for eventID, rawContent := range evt.Content.Raw { content, ok := rawContent.(map[string]interface{}) @@ -353,7 +361,7 @@ func (c *Container) parseReadReceipt(evt *gomatrix.Event) (largestTimestampEvent return } -func (c *Container) HandleReadReceipt(source EventSource, evt *gomatrix.Event) { +func (c *Container) HandleReadReceipt(source EventSource, evt *mautrix.Event) { if source&EventSourceLeave != 0 { return } @@ -368,7 +376,7 @@ func (c *Container) HandleReadReceipt(source EventSource, evt *gomatrix.Event) { c.ui.Render() } -func (c *Container) parseDirectChatInfo(evt *gomatrix.Event) map[*rooms.Room]bool { +func (c *Container) parseDirectChatInfo(evt *mautrix.Event) map[*rooms.Room]bool { directChats := make(map[*rooms.Room]bool) for _, rawRoomIDList := range evt.Content.Raw { roomIDList, ok := rawRoomIDList.([]interface{}) @@ -391,7 +399,7 @@ func (c *Container) parseDirectChatInfo(evt *gomatrix.Event) map[*rooms.Room]boo return directChats } -func (c *Container) HandleDirectChatInfo(source EventSource, evt *gomatrix.Event) { +func (c *Container) HandleDirectChatInfo(source EventSource, evt *mautrix.Event) { directChats := c.parseDirectChatInfo(evt) for _, room := range c.config.Rooms { shouldBeDirect := directChats[room] @@ -403,7 +411,7 @@ func (c *Container) HandleDirectChatInfo(source EventSource, evt *gomatrix.Event } // HandlePushRules is the event handler for the m.push_rules account data event. -func (c *Container) HandlePushRules(source EventSource, evt *gomatrix.Event) { +func (c *Container) HandlePushRules(source EventSource, evt *mautrix.Event) { debug.Print("Received updated push rules") var err error c.config.PushRules, err = pushrules.EventToPushRules(evt) @@ -415,7 +423,7 @@ func (c *Container) HandlePushRules(source EventSource, evt *gomatrix.Event) { } // HandleTag is the event handler for the m.tag account data event. -func (c *Container) HandleTag(source EventSource, evt *gomatrix.Event) { +func (c *Container) HandleTag(source EventSource, evt *mautrix.Event) { room := c.config.GetRoom(evt.RoomID) newTags := make([]rooms.RoomTag, len(evt.Content.RoomTags)) @@ -423,7 +431,7 @@ func (c *Container) HandleTag(source EventSource, evt *gomatrix.Event) { for tag, info := range evt.Content.RoomTags { order := "0.5" if len(info.Order) > 0 { - order = info.Order + order = info.Order.String() } newTags[index] = rooms.RoomTag{ Tag: tag, @@ -438,7 +446,7 @@ func (c *Container) HandleTag(source EventSource, evt *gomatrix.Event) { } // HandleTyping is the event handler for the m.typing event. -func (c *Container) HandleTyping(source EventSource, evt *gomatrix.Event) { +func (c *Container) HandleTyping(source EventSource, evt *mautrix.Event) { c.ui.MainView().SetTyping(evt.RoomID, evt.Content.TypingUserIDs) } @@ -448,11 +456,11 @@ func (c *Container) MarkRead(roomID, eventID string) { } // SendMessage sends a message with the given text to the given room. -func (c *Container) SendMessage(roomID string, msgtype gomatrix.MessageType, text string) (string, error) { +func (c *Container) SendMessage(roomID string, msgtype mautrix.MessageType, text string) (string, error) { defer debug.Recover() c.SendTyping(roomID, false) - resp, err := c.client.SendMessageEvent(roomID, gomatrix.EventMessage, - gomatrix.Content{MsgType: msgtype, Body: text}) + resp, err := c.client.SendMessageEvent(roomID, mautrix.EventMessage, + mautrix.Content{MsgType: msgtype, Body: text}) if err != nil { return "", err } @@ -491,7 +499,7 @@ var roomRegex = regexp.MustCompile("\\[.+?]\\(https://matrix.to/#/(#.+?:[^/]+?)\ // // If the given text contains markdown formatting symbols, it will be rendered into HTML before sending. // Otherwise, it will be sent as plain text. -func (c *Container) SendMarkdownMessage(roomID string, msgtype gomatrix.MessageType, text string) (string, error) { +func (c *Container) SendMarkdownMessage(roomID string, msgtype mautrix.MessageType, text string) (string, error) { defer debug.Recover() html := c.renderMarkdown(text) @@ -504,11 +512,11 @@ func (c *Container) SendMarkdownMessage(roomID string, msgtype gomatrix.MessageT text = roomRegex.ReplaceAllString(text, "$1") c.SendTyping(roomID, false) - resp, err := c.client.SendMessageEvent(roomID, gomatrix.EventMessage, - gomatrix.Content{ + resp, err := c.client.SendMessageEvent(roomID, mautrix.EventMessage, + mautrix.Content{ MsgType: msgtype, Body: text, - Format: gomatrix.FormatHTML, + Format: mautrix.FormatHTML, FormattedBody: html, }) if err != nil { @@ -560,7 +568,7 @@ func (c *Container) LeaveRoom(roomID string) error { } // GetHistory fetches room history. -func (c *Container) GetHistory(roomID, prevBatch string, limit int) ([]*gomatrix.Event, string, error) { +func (c *Container) GetHistory(roomID, prevBatch string, limit int) ([]*mautrix.Event, string, error) { resp, err := c.client.Messages(roomID, prevBatch, "", 'b', limit) if err != nil { return nil, "", err diff --git a/matrix/matrix_test.go b/matrix/matrix_test.go index b6511ec..0d8bc3c 100644 --- a/matrix/matrix_test.go +++ b/matrix/matrix_test.go @@ -21,7 +21,7 @@ import ( "fmt" "github.com/stretchr/testify/assert" "io/ioutil" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/config" "net/http" "os" @@ -80,7 +80,7 @@ func TestContainer_SendMarkdownMessage_WithMarkdown(t *testing.T) { } func TestContainer_SendTyping(t *testing.T) { - var calls []gomatrix.ReqTyping + var calls []mautrix.ReqTyping c := Container{client: mockClient(func(req *http.Request) (*http.Response, error) { if req.Method != http.MethodPut || req.URL.Path != "/_matrix/client/r0/rooms/!foo:example.com/typing/@user:example.com" { return nil, fmt.Errorf("unexpected query: %s %s", req.Method, req.URL.Path) @@ -91,7 +91,7 @@ func TestContainer_SendTyping(t *testing.T) { return nil, err } - call := gomatrix.ReqTyping{} + call := mautrix.ReqTyping{} err = json.Unmarshal(rawBody, &call) if err != nil { return nil, err @@ -187,8 +187,8 @@ func TestContainer_GetHistory(t *testing.T) { assert.Equal(t, "456", prevBatch) } -func mockClient(fn func(*http.Request) (*http.Response, error)) *gomatrix.Client { - client, _ := gomatrix.NewClient("https://example.com", "@user:example.com", "foobar") +func mockClient(fn func(*http.Request) (*http.Response, error)) *mautrix.Client { + client, _ := mautrix.NewClient("https://example.com", "@user:example.com", "foobar") client.Client = &http.Client{Transport: MockRoundTripper{RT: fn}} return client } diff --git a/matrix/pushrules/condition.go b/matrix/pushrules/condition.go index 22d59aa..bacab56 100644 --- a/matrix/pushrules/condition.go +++ b/matrix/pushrules/condition.go @@ -21,14 +21,14 @@ import ( "strconv" "strings" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/lib/glob" ) // Room is an interface with the functions that are needed for processing room-specific push conditions type Room interface { - GetMember(mxid string) *gomatrix.Member - GetMembers() map[string]*gomatrix.Member + GetMember(mxid string) *mautrix.Member + GetMembers() map[string]*mautrix.Member GetSessionOwner() string } @@ -59,7 +59,7 @@ type PushCondition struct { 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, event *gomatrix.Event) bool { +func (cond *PushCondition) Match(room Room, event *mautrix.Event) bool { switch cond.Kind { case KindEventMatch: return cond.matchValue(room, event) @@ -72,7 +72,7 @@ func (cond *PushCondition) Match(room Room, event *gomatrix.Event) bool { } } -func (cond *PushCondition) matchValue(room Room, event *gomatrix.Event) bool { +func (cond *PushCondition) matchValue(room Room, event *mautrix.Event) bool { index := strings.IndexRune(cond.Key, '.') key := cond.Key subkey := "" @@ -106,7 +106,7 @@ func (cond *PushCondition) matchValue(room Room, event *gomatrix.Event) bool { } } -func (cond *PushCondition) matchDisplayName(room Room, event *gomatrix.Event) bool { +func (cond *PushCondition) matchDisplayName(room Room, event *mautrix.Event) bool { ownerID := room.GetSessionOwner() if ownerID == event.Sender { return false @@ -115,7 +115,7 @@ func (cond *PushCondition) matchDisplayName(room Room, event *gomatrix.Event) bo return strings.Contains(event.Content.Body, member.Displayname) } -func (cond *PushCondition) matchMemberCount(room Room, event *gomatrix.Event) bool { +func (cond *PushCondition) matchMemberCount(room Room, event *mautrix.Event) bool { group := MemberCountFilterRegex.FindStringSubmatch(cond.MemberCountCondition) if len(group) != 3 { return false diff --git a/matrix/pushrules/condition_test.go b/matrix/pushrules/condition_test.go index 7fd06ee..750f2c7 100644 --- a/matrix/pushrules/condition_test.go +++ b/matrix/pushrules/condition_test.go @@ -21,7 +21,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/rooms" ) @@ -30,7 +30,7 @@ var ( blankTestRoom *rooms.Room displaynameTestRoom pushrules.Room - countConditionTestEvent *gomatrix.Event + countConditionTestEvent *mautrix.Event displaynamePushCondition *pushrules.PushCondition ) @@ -38,7 +38,7 @@ var ( func init() { blankTestRoom = rooms.NewRoom("!fakeroom:maunium.net", "@tulir:maunium.net") - countConditionTestEvent = &gomatrix.Event{ + countConditionTestEvent = &mautrix.Event{ Sender: "@tulir:maunium.net", Type: "m.room.message", Timestamp: 1523791120, @@ -56,8 +56,8 @@ func init() { } } -func newFakeEvent(evtType string, content map[string]interface{}) *gomatrix.Event { - return &gomatrix.Event{ +func newFakeEvent(evtType string, content map[string]interface{}) *mautrix.Event { + return &mautrix.Event{ Sender: "@tulir:maunium.net", Type: evtType, Timestamp: 1523791120, diff --git a/matrix/pushrules/pushrules.go b/matrix/pushrules/pushrules.go index b383c66..643f2f2 100644 --- a/matrix/pushrules/pushrules.go +++ b/matrix/pushrules/pushrules.go @@ -4,16 +4,16 @@ import ( "encoding/json" "net/url" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" ) // GetPushRules returns the push notification rules for the global scope. -func GetPushRules(client *gomatrix.Client) (*PushRuleset, error) { +func GetPushRules(client *mautrix.Client) (*PushRuleset, error) { return GetScopedPushRules(client, "global") } // GetScopedPushRules returns the push notification rules for the given scope. -func GetScopedPushRules(client *gomatrix.Client, scope string) (resp *PushRuleset, err error) { +func GetScopedPushRules(client *mautrix.Client, scope string) (resp *PushRuleset, err error) { u, _ := url.Parse(client.BuildURL("pushrules", scope)) // client.BuildURL returns the URL without a trailing slash, but the pushrules endpoint requires the slash. u.Path += "/" @@ -26,7 +26,7 @@ type contentWithRuleset struct { } // EventToPushRules converts a m.push_rules event to a PushRuleset by passing the data through JSON. -func EventToPushRules(event *gomatrix.Event) (*PushRuleset, error) { +func EventToPushRules(event *mautrix.Event) (*PushRuleset, error) { content := &contentWithRuleset{} err := json.Unmarshal(event.Content.VeryRaw, content) if err != nil { diff --git a/matrix/pushrules/pushrules_test.go b/matrix/pushrules/pushrules_test.go index 09698ac..73fa787 100644 --- a/matrix/pushrules/pushrules_test.go +++ b/matrix/pushrules/pushrules_test.go @@ -21,7 +21,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/matrix/pushrules" ) @@ -33,7 +33,7 @@ func init() { } func TestEventToPushRules(t *testing.T) { - event := &gomatrix.Event{ + event := &mautrix.Event{ Type: "m.push_rules", Timestamp: 1523380910, Content: mapExamplePushRules, diff --git a/matrix/pushrules/rule.go b/matrix/pushrules/rule.go index 71f71e5..62318ac 100644 --- a/matrix/pushrules/rule.go +++ b/matrix/pushrules/rule.go @@ -18,7 +18,7 @@ package pushrules import ( "encoding/gob" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/lib/glob" ) @@ -28,7 +28,7 @@ func init() { } type PushRuleCollection interface { - GetActions(room Room, event *gomatrix.Event) PushActionArray + GetActions(room Room, event *mautrix.Event) PushActionArray } type PushRuleArray []*PushRule @@ -40,7 +40,7 @@ func (rules PushRuleArray) SetType(typ PushRuleType) PushRuleArray { return rules } -func (rules PushRuleArray) GetActions(room Room, event *gomatrix.Event) PushActionArray { +func (rules PushRuleArray) GetActions(room Room, event *mautrix.Event) PushActionArray { for _, rule := range rules { if !rule.Match(room, event) { continue @@ -67,7 +67,7 @@ func (rules PushRuleArray) SetTypeAndMap(typ PushRuleType) PushRuleMap { return data } -func (ruleMap PushRuleMap) GetActions(room Room, event *gomatrix.Event) PushActionArray { +func (ruleMap PushRuleMap) GetActions(room Room, event *mautrix.Event) PushActionArray { var rule *PushRule var found bool switch ruleMap.Type { @@ -122,7 +122,7 @@ type PushRule struct { Pattern string `json:"pattern,omitempty"` } -func (rule *PushRule) Match(room Room, event *gomatrix.Event) bool { +func (rule *PushRule) Match(room Room, event *mautrix.Event) bool { if !rule.Enabled { return false } @@ -140,7 +140,7 @@ func (rule *PushRule) Match(room Room, event *gomatrix.Event) bool { } } -func (rule *PushRule) matchConditions(room Room, event *gomatrix.Event) bool { +func (rule *PushRule) matchConditions(room Room, event *mautrix.Event) bool { for _, cond := range rule.Conditions { if !cond.Match(room, event) { return false @@ -149,7 +149,7 @@ func (rule *PushRule) matchConditions(room Room, event *gomatrix.Event) bool { return true } -func (rule *PushRule) matchPattern(room Room, event *gomatrix.Event) bool { +func (rule *PushRule) matchPattern(room Room, event *mautrix.Event) bool { pattern, err := glob.Compile(rule.Pattern) if err != nil { return false diff --git a/matrix/pushrules/ruleset.go b/matrix/pushrules/ruleset.go index 940025f..366702e 100644 --- a/matrix/pushrules/ruleset.go +++ b/matrix/pushrules/ruleset.go @@ -19,7 +19,7 @@ package pushrules import ( "encoding/json" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" ) type PushRuleset struct { @@ -80,7 +80,7 @@ var DefaultPushActions = make(PushActionArray, 0) // 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, event *gomatrix.Event) (match PushActionArray) { +func (rs *PushRuleset) GetActions(room Room, event *mautrix.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. diff --git a/matrix/rooms/room.go b/matrix/rooms/room.go index bd233f8..72251c4 100644 --- a/matrix/rooms/room.go +++ b/matrix/rooms/room.go @@ -23,7 +23,7 @@ import ( "time" "encoding/gob" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/debug" "os" ) @@ -58,7 +58,7 @@ type UnreadMessage struct { // Room represents a single Matrix room. type Room struct { - *gomatrix.Room + *mautrix.Room // Whether or not the user has left the room. HasLeft bool @@ -82,10 +82,10 @@ type Room struct { LastReceivedMessage time.Time // MXID -> Member cache calculated from membership events. - memberCache map[string]*gomatrix.Member + memberCache map[string]*mautrix.Member // The first non-SessionUserID member in the room. Calculated at // the same time as memberCache. - firstMemberCache *gomatrix.Member + firstMemberCache *mautrix.Member // The name of the room. Calculated from the state event name, // canonical_alias or alias or the member cache. nameCache string @@ -216,31 +216,31 @@ func (room *Room) Tags() []RoomTag { // 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 *gomatrix.Event) { +func (room *Room) UpdateState(event *mautrix.Event) { _, exists := room.State[event.Type] if !exists { - room.State[event.Type] = make(map[string]*gomatrix.Event) + room.State[event.Type] = make(map[string]*mautrix.Event) } switch event.Type { - case gomatrix.StateRoomName: + case mautrix.StateRoomName: room.nameCache = "" - case gomatrix.StateCanonicalAlias: + case mautrix.StateCanonicalAlias: if room.nameCacheSource >= CanonicalAliasRoomName { room.nameCache = "" } room.canonicalAliasCache = "" - case gomatrix.StateAliases: + case mautrix.StateAliases: if room.nameCacheSource >= AliasRoomName { room.nameCache = "" } room.aliasesCache = nil - case gomatrix.StateMember: + case mautrix.StateMember: room.memberCache = nil room.firstMemberCache = nil if room.nameCacheSource >= MemberRoomName { room.nameCache = "" } - case gomatrix.StateTopic: + case mautrix.StateTopic: room.topicCache = "" } @@ -248,7 +248,7 @@ func (room *Room) UpdateState(event *gomatrix.Event) { if event.StateKey != nil { stateKey = *event.StateKey } - if event.Type != gomatrix.StateMember { + if event.Type != mautrix.StateMember { debug.Printf("Updating state %s#%s for %s", event.Type, stateKey, room.ID) } @@ -260,14 +260,14 @@ func (room *Room) UpdateState(event *gomatrix.Event) { } // GetStateEvent returns the state event for the given type/state_key combo, or nil. -func (room *Room) GetStateEvent(eventType gomatrix.EventType, stateKey string) *gomatrix.Event { +func (room *Room) GetStateEvent(eventType mautrix.EventType, stateKey string) *mautrix.Event { stateEventMap, _ := room.State[eventType] event, _ := stateEventMap[stateKey] return event } // GetStateEvents returns the state events for the given type. -func (room *Room) GetStateEvents(eventType gomatrix.EventType) map[string]*gomatrix.Event { +func (room *Room) GetStateEvents(eventType mautrix.EventType) map[string]*mautrix.Event { stateEventMap, _ := room.State[eventType] return stateEventMap } @@ -275,7 +275,7 @@ func (room *Room) GetStateEvents(eventType gomatrix.EventType) map[string]*gomat // GetTopic returns the topic of the room. func (room *Room) GetTopic() string { if len(room.topicCache) == 0 { - topicEvt := room.GetStateEvent(gomatrix.StateTopic, "") + topicEvt := room.GetStateEvent(mautrix.StateTopic, "") if topicEvt != nil { room.topicCache = topicEvt.Content.Topic } @@ -285,7 +285,7 @@ func (room *Room) GetTopic() string { func (room *Room) GetCanonicalAlias() string { if len(room.canonicalAliasCache) == 0 { - canonicalAliasEvt := room.GetStateEvent(gomatrix.StateCanonicalAlias, "") + canonicalAliasEvt := room.GetStateEvent(mautrix.StateCanonicalAlias, "") if canonicalAliasEvt != nil { room.canonicalAliasCache = canonicalAliasEvt.Content.Alias } else { @@ -301,7 +301,7 @@ func (room *Room) GetCanonicalAlias() string { // GetAliases returns the list of aliases that point to this room. func (room *Room) GetAliases() []string { if room.aliasesCache == nil { - aliasEvents := room.GetStateEvents(gomatrix.StateAliases) + aliasEvents := room.GetStateEvents(mautrix.StateAliases) room.aliasesCache = []string{} for _, event := range aliasEvents { room.aliasesCache = append(room.aliasesCache, event.Content.Aliases...) @@ -312,7 +312,7 @@ func (room *Room) GetAliases() []string { // updateNameFromNameEvent updates the room display name to be the name set in the name event. func (room *Room) updateNameFromNameEvent() { - nameEvt := room.GetStateEvent(gomatrix.StateRoomName, "") + nameEvt := room.GetStateEvent(mautrix.StateRoomName, "") if nameEvt != nil { room.nameCache = nameEvt.Content.Name } @@ -384,9 +384,9 @@ func (room *Room) GetTitle() string { } // createMemberCache caches all member events into a easily processable MXID -> *Member map. -func (room *Room) createMemberCache() map[string]*gomatrix.Member { - cache := make(map[string]*gomatrix.Member) - events := room.GetStateEvents(gomatrix.StateMember) +func (room *Room) createMemberCache() map[string]*mautrix.Member { + cache := make(map[string]*mautrix.Member) + events := room.GetStateEvents(mautrix.StateMember) room.firstMemberCache = nil if events != nil { for userID, event := range events { @@ -407,7 +407,7 @@ func (room *Room) createMemberCache() map[string]*gomatrix.Member { // // The members are returned from the cache. // If the cache is empty, it is updated first. -func (room *Room) GetMembers() map[string]*gomatrix.Member { +func (room *Room) GetMembers() map[string]*mautrix.Member { if len(room.memberCache) == 0 || room.firstMemberCache == nil { room.createMemberCache() } @@ -416,7 +416,7 @@ func (room *Room) GetMembers() map[string]*gomatrix.Member { // GetMember returns the member with the given MXID. // If the member doesn't exist, nil is returned. -func (room *Room) GetMember(userID string) *gomatrix.Member { +func (room *Room) GetMember(userID string) *mautrix.Member { if len(room.memberCache) == 0 { room.createMemberCache() } @@ -432,7 +432,7 @@ func (room *Room) GetSessionOwner() string { // NewRoom creates a new Room with the given ID func NewRoom(roomID, owner string) *Room { return &Room{ - Room: gomatrix.NewRoom(roomID), + Room: mautrix.NewRoom(roomID), fetchHistoryLock: &sync.Mutex{}, SessionUserID: owner, } diff --git a/matrix/rooms/room_test.go b/matrix/rooms/room_test.go index 1fabdcc..db7f586 100644 --- a/matrix/rooms/room_test.go +++ b/matrix/rooms/room_test.go @@ -21,7 +21,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/matrix/rooms" ) @@ -39,7 +39,7 @@ func TestNewRoom_DefaultValues(t *testing.T) { func TestRoom_GetCanonicalAlias(t *testing.T) { room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - room.UpdateState(&gomatrix.Event{ + room.UpdateState(&mautrix.Event{ Type: "m.room.canonical_alias", Content: map[string]interface{}{ "alias": "#foo:maunium.net", @@ -50,7 +50,7 @@ func TestRoom_GetCanonicalAlias(t *testing.T) { func TestRoom_GetTopic(t *testing.T) { room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - room.UpdateState(&gomatrix.Event{ + room.UpdateState(&mautrix.Event{ Type: "m.room.topic", Content: map[string]interface{}{ "topic": "test topic", @@ -87,7 +87,7 @@ func TestRoom_GetAliases(t *testing.T) { } func addName(room *rooms.Room) { - room.UpdateState(&gomatrix.Event{ + room.UpdateState(&mautrix.Event{ Type: "m.room.name", Content: map[string]interface{}{ "name": "Test room", @@ -96,7 +96,7 @@ func addName(room *rooms.Room) { } func addCanonicalAlias(room *rooms.Room) { - room.UpdateState(&gomatrix.Event{ + room.UpdateState(&mautrix.Event{ Type: "m.room.canonical_alias", Content: map[string]interface{}{ "alias": "#foo:maunium.net", @@ -106,7 +106,7 @@ func addCanonicalAlias(room *rooms.Room) { func addAliases(room *rooms.Room) { server1 := "maunium.net" - room.UpdateState(&gomatrix.Event{ + room.UpdateState(&mautrix.Event{ Type: "m.room.aliases", StateKey: &server1, Content: map[string]interface{}{ @@ -115,7 +115,7 @@ func addAliases(room *rooms.Room) { }) server2 := "matrix.org" - room.UpdateState(&gomatrix.Event{ + room.UpdateState(&mautrix.Event{ Type: "m.room.aliases", StateKey: &server2, Content: map[string]interface{}{ @@ -126,7 +126,7 @@ func addAliases(room *rooms.Room) { func addMembers(room *rooms.Room, count int) { user1 := "@tulir:maunium.net" - room.UpdateState(&gomatrix.Event{ + room.UpdateState(&mautrix.Event{ Type: "m.room.member", StateKey: &user1, Content: map[string]interface{}{ @@ -146,7 +146,7 @@ func addMembers(room *rooms.Room, count int) { if i%5 == 0 { content["membership"] = "invite" } - room.UpdateState(&gomatrix.Event{ + room.UpdateState(&mautrix.Event{ Type: "m.room.member", StateKey: &userN, Content: content, diff --git a/matrix/sync.go b/matrix/sync.go index 6d5def7..06c7f47 100644 --- a/matrix/sync.go +++ b/matrix/sync.go @@ -14,15 +14,16 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -// Based on https://github.com/matrix-org/gomatrix/blob/master/sync.go +// Based on https://github.com/matrix-org/mautrix/blob/master/sync.go package matrix import ( "encoding/json" + "maunium.net/go/gomuks/debug" "time" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/matrix/rooms" ) @@ -44,14 +45,14 @@ const ( EventSourceEphemeral ) -type EventHandler func(source EventSource, event *gomatrix.Event) +type EventHandler func(source EventSource, event *mautrix.Event) // GomuksSyncer 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 GomuksSyncer.OnEventType for more information. type GomuksSyncer struct { Session SyncerSession - listeners map[gomatrix.EventType][]EventHandler // event type to listeners array + listeners map[mautrix.EventType][]EventHandler // event type to listeners array FirstSyncDone bool InitDoneCallback func() } @@ -60,13 +61,14 @@ type GomuksSyncer struct { func NewGomuksSyncer(session SyncerSession) *GomuksSyncer { return &GomuksSyncer{ Session: session, - listeners: make(map[gomatrix.EventType][]EventHandler), + listeners: make(map[mautrix.EventType][]EventHandler), FirstSyncDone: false, } } // ProcessResponse processes a Matrix sync response. -func (s *GomuksSyncer) ProcessResponse(res *gomatrix.RespSync, since string) (err error) { +func (s *GomuksSyncer) ProcessResponse(res *mautrix.RespSync, since string) (err error) { + debug.Print("Received sync response") s.processSyncEvents(nil, res.Presence.Events, EventSourcePresence, false) s.processSyncEvents(nil, res.AccountData.Events, EventSourceAccountData, false) @@ -106,7 +108,7 @@ func (s *GomuksSyncer) ProcessResponse(res *gomatrix.RespSync, since string) (er return } -func (s *GomuksSyncer) processSyncEvents(room *rooms.Room, events []*gomatrix.Event, source EventSource, checkStateKey bool) { +func (s *GomuksSyncer) processSyncEvents(room *rooms.Room, events []*mautrix.Event, source EventSource, checkStateKey bool) { for _, event := range events { if !checkStateKey || event.StateKey != nil { s.processSyncEvent(room, event, source) @@ -114,11 +116,11 @@ func (s *GomuksSyncer) processSyncEvents(room *rooms.Room, events []*gomatrix.Ev } } -func (s *GomuksSyncer) processSyncEvent(room *rooms.Room, event *gomatrix.Event, source EventSource) { +func (s *GomuksSyncer) processSyncEvent(room *rooms.Room, event *mautrix.Event, source EventSource) { if room != nil { event.RoomID = room.ID } - if event.Type.Class == gomatrix.StateEventType { + if event.Type.Class == mautrix.StateEventType { room.UpdateState(event) } s.notifyListeners(source, event) @@ -126,7 +128,7 @@ func (s *GomuksSyncer) processSyncEvent(room *rooms.Room, event *gomatrix.Event, // OnEventType allows callers to be notified when there are new events for the given event type. // There are no duplicate checks. -func (s *GomuksSyncer) OnEventType(eventType gomatrix.EventType, callback EventHandler) { +func (s *GomuksSyncer) OnEventType(eventType mautrix.EventType, callback EventHandler) { _, exists := s.listeners[eventType] if !exists { s.listeners[eventType] = []EventHandler{} @@ -134,7 +136,7 @@ func (s *GomuksSyncer) OnEventType(eventType gomatrix.EventType, callback EventH s.listeners[eventType] = append(s.listeners[eventType], callback) } -func (s *GomuksSyncer) notifyListeners(source EventSource, event *gomatrix.Event) { +func (s *GomuksSyncer) notifyListeners(source EventSource, event *mautrix.Event) { listeners, exists := s.listeners[event.Type] if !exists { return @@ -145,16 +147,17 @@ func (s *GomuksSyncer) notifyListeners(source EventSource, event *gomatrix.Event } // OnFailedSync always returns a 10 second wait period between failed /syncs, never a fatal error. -func (s *GomuksSyncer) OnFailedSync(res *gomatrix.RespSync, err error) (time.Duration, error) { +func (s *GomuksSyncer) OnFailedSync(res *mautrix.RespSync, err error) (time.Duration, error) { + debug.Printf("Sync failed: %v", err) return 10 * time.Second, nil } // GetFilterJSON returns a filter with a timeline limit of 50. func (s *GomuksSyncer) GetFilterJSON(userID string) json.RawMessage { - filter := &gomatrix.Filter{ - Room: gomatrix.RoomFilter{ + filter := &mautrix.Filter{ + Room: mautrix.RoomFilter{ IncludeLeave: false, - State: gomatrix.FilterPart{ + State: mautrix.FilterPart{ Types: []string{ "m.room.member", "m.room.name", @@ -163,21 +166,21 @@ func (s *GomuksSyncer) GetFilterJSON(userID string) json.RawMessage { "m.room.aliases", }, }, - Timeline: gomatrix.FilterPart{ + Timeline: mautrix.FilterPart{ Types: []string{"m.room.message", "m.room.member"}, Limit: 50, }, - Ephemeral: gomatrix.FilterPart{ + Ephemeral: mautrix.FilterPart{ Types: []string{"m.typing", "m.receipt"}, }, - AccountData: gomatrix.FilterPart{ + AccountData: mautrix.FilterPart{ Types: []string{"m.tag"}, }, }, - AccountData: gomatrix.FilterPart{ + AccountData: mautrix.FilterPart{ Types: []string{"m.push_rules", "m.direct", "net.maunium.gomuks.preferences"}, }, - Presence: gomatrix.FilterPart{ + Presence: mautrix.FilterPart{ Types: []string{}, }, } diff --git a/matrix/sync_test.go b/matrix/sync_test.go index 49bbcb0..79cb11b 100644 --- a/matrix/sync_test.go +++ b/matrix/sync_test.go @@ -20,7 +20,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/matrix" "maunium.net/go/gomuks/matrix/rooms" ) @@ -42,13 +42,13 @@ func TestGomuksSyncer_ProcessResponse(t *testing.T) { userID: "@tulir:maunium.net", rooms: map[string]*rooms.Room{ "!foo:maunium.net": { - Room: gomatrix.NewRoom("!foo:maunium.net"), + Room: mautrix.NewRoom("!foo:maunium.net"), }, "!bar:maunium.net": { - Room: gomatrix.NewRoom("!bar:maunium.net"), + Room: mautrix.NewRoom("!bar:maunium.net"), }, "!test:maunium.net": { - Room: gomatrix.NewRoom("!test:maunium.net"), + Room: mautrix.NewRoom("!test:maunium.net"), }, }, } @@ -58,7 +58,7 @@ func TestGomuksSyncer_ProcessResponse(t *testing.T) { syncer.OnEventType("m.room.message", ml.receive) syncer.GetFilterJSON("@tulir:maunium.net") - joinEvt := &gomatrix.Event{ + joinEvt := &mautrix.Event{ ID: "!join:maunium.net", Type: "m.room.member", Sender: "@tulir:maunium.net", @@ -67,7 +67,7 @@ func TestGomuksSyncer_ProcessResponse(t *testing.T) { "membership": "join", }, } - messageEvt := &gomatrix.Event{ + messageEvt := &mautrix.Event{ ID: "!msg:maunium.net", Type: "m.room.message", Content: map[string]interface{}{ @@ -75,11 +75,11 @@ func TestGomuksSyncer_ProcessResponse(t *testing.T) { "msgtype": "m.text", }, } - unhandledEvt := &gomatrix.Event{ + unhandledEvt := &mautrix.Event{ ID: "!unhandled:maunium.net", Type: "m.room.unhandled_event", } - inviteEvt := &gomatrix.Event{ + inviteEvt := &mautrix.Event{ ID: "!invite:matrix.org", Type: "m.room.member", Sender: "@you:matrix.org", @@ -88,7 +88,7 @@ func TestGomuksSyncer_ProcessResponse(t *testing.T) { "membership": "invite", }, } - leaveEvt := &gomatrix.Event{ + leaveEvt := &mautrix.Event{ ID: "!leave:matrix.org", Type: "m.room.member", Sender: "@you:matrix.org", @@ -100,27 +100,27 @@ func TestGomuksSyncer_ProcessResponse(t *testing.T) { resp := newRespSync() resp.Rooms.Join["!foo:maunium.net"] = join{ - State: events{Events: []*gomatrix.Event{joinEvt}}, - Timeline: timeline{Events: []*gomatrix.Event{messageEvt, unhandledEvt}}, + State: events{Events: []*mautrix.Event{joinEvt}}, + Timeline: timeline{Events: []*mautrix.Event{messageEvt, unhandledEvt}}, } resp.Rooms.Invite["!bar:maunium.net"] = struct { State struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` } `json:"invite_state"` }{ - State: events{Events: []*gomatrix.Event{inviteEvt}}, + State: events{Events: []*mautrix.Event{inviteEvt}}, } resp.Rooms.Leave["!test:maunium.net"] = struct { State struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` } `json:"state"` Timeline struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` Limited bool `json:"limited"` PrevBatch string `json:"prev_batch"` } `json:"timeline"` }{ - State: events{Events: []*gomatrix.Event{leaveEvt}}, + State: events{Events: []*mautrix.Event{leaveEvt}}, } syncer.ProcessResponse(resp, "since") @@ -145,28 +145,28 @@ func (mss *mockSyncerSession) GetUserID() string { } type events struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` } type timeline struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` Limited bool `json:"limited"` PrevBatch string `json:"prev_batch"` } type join struct { State struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` } `json:"state"` Timeline struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` Limited bool `json:"limited"` PrevBatch string `json:"prev_batch"` } `json:"timeline"` Ephemeral struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` } `json:"ephemeral"` AccountData struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` } `json:"account_data"` } @@ -175,42 +175,42 @@ func ptr(text string) *string { } type mockListener struct { - received []*gomatrix.Event + received []*mautrix.Event } -func (ml *mockListener) receive(source matrix.EventSource, evt *gomatrix.Event) { +func (ml *mockListener) receive(source matrix.EventSource, evt *mautrix.Event) { ml.received = append(ml.received, evt) } -func newRespSync() *gomatrix.RespSync { - resp := &gomatrix.RespSync{NextBatch: "123"} +func newRespSync() *mautrix.RespSync { + resp := &mautrix.RespSync{NextBatch: "123"} resp.Rooms.Join = make(map[string]struct { State struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` } `json:"state"` Timeline struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` Limited bool `json:"limited"` PrevBatch string `json:"prev_batch"` } `json:"timeline"` Ephemeral struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` } `json:"ephemeral"` AccountData struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` } `json:"account_data"` }) resp.Rooms.Invite = make(map[string]struct { State struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` } `json:"invite_state"` }) resp.Rooms.Leave = make(map[string]struct { State struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` } `json:"state"` Timeline struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` Limited bool `json:"limited"` PrevBatch string `json:"prev_batch"` } `json:"timeline"` diff --git a/ui/commands.go b/ui/commands.go index b80fa07..0feda29 100644 --- a/ui/commands.go +++ b/ui/commands.go @@ -19,7 +19,7 @@ package ui import ( "encoding/json" "fmt" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "strings" "unicode" @@ -133,7 +133,7 @@ func cmdSendEvent(cmd *Command) { return } roomID := cmd.Args[0] - eventType := gomatrix.NewEventType(cmd.Args[1]) + eventType := mautrix.NewEventType(cmd.Args[1]) rawContent := strings.Join(cmd.Args[2:], "") debug.Print(roomID, eventType, rawContent) @@ -162,7 +162,7 @@ func cmdSetState(cmd *Command) { } roomID := cmd.Args[0] - eventType := gomatrix.NewEventType(cmd.Args[1]) + eventType := mautrix.NewEventType(cmd.Args[1]) stateKey := cmd.Args[2] if stateKey == "-" { stateKey = "" diff --git a/ui/messages/base.go b/ui/messages/base.go index c9da389..441aca1 100644 --- a/ui/messages/base.go +++ b/ui/messages/base.go @@ -18,7 +18,7 @@ package messages import ( "encoding/gob" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "time" "maunium.net/go/gomuks/config" @@ -34,7 +34,7 @@ func init() { type BaseMessage struct { MsgID string - MsgType gomatrix.MessageType + MsgType mautrix.MessageType MsgSenderID string MsgSender string MsgSenderColor tcell.Color @@ -48,7 +48,7 @@ type BaseMessage struct { prevPrefs config.UserPreferences } -func newBaseMessage(id, sender, displayname string, msgtype gomatrix.MessageType, timestamp time.Time) BaseMessage { +func newBaseMessage(id, sender, displayname string, msgtype mautrix.MessageType, timestamp time.Time) BaseMessage { return BaseMessage{ MsgSenderID: sender, MsgSender: displayname, @@ -195,11 +195,11 @@ func (msg *BaseMessage) SetID(id string) { msg.MsgID = id } -func (msg *BaseMessage) Type() gomatrix.MessageType { +func (msg *BaseMessage) Type() mautrix.MessageType { return msg.MsgType } -func (msg *BaseMessage) SetType(msgtype gomatrix.MessageType) { +func (msg *BaseMessage) SetType(msgtype mautrix.MessageType) { msg.MsgType = msgtype } diff --git a/ui/messages/expandedtextmessage.go b/ui/messages/expandedtextmessage.go index 2e77a24..fbb373d 100644 --- a/ui/messages/expandedtextmessage.go +++ b/ui/messages/expandedtextmessage.go @@ -18,7 +18,7 @@ package messages import ( "encoding/gob" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "time" "maunium.net/go/gomuks/config" @@ -35,7 +35,7 @@ type ExpandedTextMessage struct { } // NewExpandedTextMessage creates a new ExpandedTextMessage object with the provided values and the default state. -func NewExpandedTextMessage(id, sender, displayname string, msgtype gomatrix.MessageType, text tstring.TString, timestamp time.Time) UIMessage { +func NewExpandedTextMessage(id, sender, displayname string, msgtype mautrix.MessageType, text tstring.TString, timestamp time.Time) UIMessage { return &ExpandedTextMessage{ BaseMessage: newBaseMessage(id, sender, displayname, msgtype, timestamp), MsgText: text, diff --git a/ui/messages/imagemessage.go b/ui/messages/imagemessage.go index a17c842..8ccff67 100644 --- a/ui/messages/imagemessage.go +++ b/ui/messages/imagemessage.go @@ -20,7 +20,7 @@ import ( "bytes" "encoding/gob" "fmt" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "time" "image/color" @@ -48,7 +48,7 @@ type ImageMessage struct { } // NewImageMessage creates a new ImageMessage object with the provided values and the default state. -func NewImageMessage(matrix ifc.MatrixContainer, id, sender, displayname string, msgtype gomatrix.MessageType, body, homeserver, fileID string, data []byte, timestamp time.Time) UIMessage { +func NewImageMessage(matrix ifc.MatrixContainer, id, sender, displayname string, msgtype mautrix.MessageType, body, homeserver, fileID string, data []byte, timestamp time.Time) UIMessage { return &ImageMessage{ newBaseMessage(id, sender, displayname, msgtype, timestamp), body, diff --git a/ui/messages/parser/htmlparser.go b/ui/messages/parser/htmlparser.go index bcde14d..b2deeba 100644 --- a/ui/messages/parser/htmlparser.go +++ b/ui/messages/parser/htmlparser.go @@ -24,10 +24,10 @@ import ( "github.com/lucasb-eyer/go-colorful" "golang.org/x/net/html" - "maunium.net/go/gomatrix" "maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/ui/messages/tstring" "maunium.net/go/gomuks/ui/widget" + "maunium.net/go/mautrix" "maunium.net/go/tcell" "strconv" ) @@ -69,6 +69,9 @@ func (parser *htmlParser) getAttribute(node *html.Node, attribute string) string } func digits(num int) int { + if num <= 0 { + return 0 + } return int(math.Floor(math.Log10(float64(num))) + 1) } @@ -270,14 +273,14 @@ func (parser *htmlParser) Parse(htmlData string) tstring.TString { } // ParseHTMLMessage parses a HTML-formatted Matrix event into a UIMessage. -func ParseHTMLMessage(room *rooms.Room, evt *gomatrix.Event, senderDisplayname string) tstring.TString { +func ParseHTMLMessage(room *rooms.Room, evt *mautrix.Event, senderDisplayname string) tstring.TString { htmlData := evt.Content.FormattedBody htmlData = strings.Replace(htmlData, "\t", " ", -1) parser := htmlParser{room} str := parser.Parse(htmlData) - if evt.Content.MsgType == gomatrix.MsgEmote { + if evt.Content.MsgType == mautrix.MsgEmote { str = tstring.Join([]tstring.TString{ tstring.NewTString("* "), tstring.NewColorTString(senderDisplayname, widget.GetHashColor(evt.Sender)), diff --git a/ui/messages/parser/parser.go b/ui/messages/parser/parser.go index 1d7ced3..ef6578f 100644 --- a/ui/messages/parser/parser.go +++ b/ui/messages/parser/parser.go @@ -18,24 +18,28 @@ package parser import ( "fmt" + "html" "strings" "time" - "maunium.net/go/gomatrix" "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/ui/messages" "maunium.net/go/gomuks/ui/messages/tstring" "maunium.net/go/gomuks/ui/widget" + "maunium.net/go/mautrix" "maunium.net/go/tcell" ) -func ParseEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *gomatrix.Event) messages.UIMessage { +func ParseEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event) messages.UIMessage { switch evt.Type { - case gomatrix.EventMessage: + case mautrix.EventSticker: + evt.Content.MsgType = mautrix.MsgImage + fallthrough + case mautrix.EventMessage: return ParseMessage(matrix, room, evt) - case gomatrix.StateMember: + case mautrix.StateMember: return ParseMembershipEvent(room, evt) } return nil @@ -49,16 +53,27 @@ func unixToTime(unix int64) time.Time { return timestamp } -func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *gomatrix.Event) messages.UIMessage { +func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event) messages.UIMessage { displayname := evt.Sender member := room.GetMember(evt.Sender) if member != nil { displayname = member.Displayname } + if len(evt.Content.GetReplyTo()) > 0 { + evt.Content.RemoveReplyFallback() + replyToEvt, _ := matrix.Client().GetEvent(room.ID, evt.Content.GetReplyTo()) + replyToEvt.Content.RemoveReplyFallback() + if len(replyToEvt.Content.FormattedBody) == 0 { + replyToEvt.Content.FormattedBody = html.EscapeString(replyToEvt.Content.Body) + } + evt.Content.FormattedBody = fmt.Sprintf( + "In reply to %[1]s
%[2]s

%[3]s", + replyToEvt.Sender, replyToEvt.Content.FormattedBody, evt.Content.FormattedBody) + } ts := unixToTime(evt.Timestamp) switch evt.Content.MsgType { case "m.text", "m.notice", "m.emote": - if evt.Content.Format == gomatrix.FormatHTML { + if evt.Content.Format == mautrix.FormatHTML { text := ParseHTMLMessage(room, evt, displayname) return messages.NewExpandedTextMessage(evt.ID, evt.Sender, displayname, evt.Content.MsgType, text, ts) } @@ -74,7 +89,7 @@ func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *gomatrix.Ev return nil } -func getMembershipChangeMessage(evt *gomatrix.Event, membership, prevMembership gomatrix.Membership, senderDisplayname, displayname, prevDisplayname string) (sender string, text tstring.TString) { +func getMembershipChangeMessage(evt *mautrix.Event, membership, prevMembership mautrix.Membership, senderDisplayname, displayname, prevDisplayname string) (sender string, text tstring.TString) { switch membership { case "invite": sender = "---" @@ -88,7 +103,7 @@ func getMembershipChangeMessage(evt *gomatrix.Event, membership, prevMembership case "leave": sender = "<--" if evt.Sender != *evt.StateKey { - if prevMembership == gomatrix.MembershipBan { + if prevMembership == mautrix.MembershipBan { text = tstring.NewColorTString(fmt.Sprintf("%s unbanned %s", senderDisplayname, displayname), tcell.ColorGreen) text.Colorize(len(senderDisplayname)+len(" unbanned "), len(displayname), widget.GetHashColor(*evt.StateKey)) } else { @@ -111,7 +126,7 @@ func getMembershipChangeMessage(evt *gomatrix.Event, membership, prevMembership return } -func getMembershipEventContent(room *rooms.Room, evt *gomatrix.Event) (sender string, text tstring.TString) { +func getMembershipEventContent(room *rooms.Room, evt *mautrix.Event) (sender string, text tstring.TString) { member := room.GetMember(evt.Sender) senderDisplayname := evt.Sender if member != nil { @@ -124,7 +139,7 @@ func getMembershipEventContent(room *rooms.Room, evt *gomatrix.Event) (sender st displayname = *evt.StateKey } - prevMembership := gomatrix.MembershipLeave + prevMembership := mautrix.MembershipLeave prevDisplayname := *evt.StateKey if evt.Unsigned.PrevContent != nil { prevMembership = evt.Unsigned.PrevContent.Membership @@ -146,7 +161,7 @@ func getMembershipEventContent(room *rooms.Room, evt *gomatrix.Event) (sender st return } -func ParseMembershipEvent(room *rooms.Room, evt *gomatrix.Event) messages.UIMessage { +func ParseMembershipEvent(room *rooms.Room, evt *mautrix.Event) messages.UIMessage { displayname, text := getMembershipEventContent(room, evt) if len(text) == 0 { return nil diff --git a/ui/messages/textmessage.go b/ui/messages/textmessage.go index d5ce324..8622c32 100644 --- a/ui/messages/textmessage.go +++ b/ui/messages/textmessage.go @@ -19,7 +19,7 @@ package messages import ( "encoding/gob" "fmt" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "time" "maunium.net/go/gomuks/config" @@ -38,7 +38,7 @@ type TextMessage struct { } // NewTextMessage creates a new UITextMessage object with the provided values and the default state. -func NewTextMessage(id, sender, displayname string, msgtype gomatrix.MessageType, text string, timestamp time.Time) UIMessage { +func NewTextMessage(id, sender, displayname string, msgtype mautrix.MessageType, text string, timestamp time.Time) UIMessage { return &TextMessage{ BaseMessage: newBaseMessage(id, sender, displayname, msgtype, timestamp), MsgText: text, @@ -58,7 +58,7 @@ func (msg *TextMessage) getCache() tstring.TString { return msg.cache } -func (msg *TextMessage) SetType(msgtype gomatrix.MessageType) { +func (msg *TextMessage) SetType(msgtype mautrix.MessageType) { msg.BaseMessage.SetType(msgtype) msg.cache = nil } diff --git a/ui/room-view.go b/ui/room-view.go index 4b2cd48..6eda559 100644 --- a/ui/room-view.go +++ b/ui/room-view.go @@ -18,7 +18,7 @@ package ui import ( "fmt" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "path/filepath" "sort" "strconv" @@ -347,7 +347,7 @@ func (view *RoomView) UpdateUserList() { } } -func (view *RoomView) newUIMessage(id, sender string, msgtype gomatrix.MessageType, text string, timestamp time.Time) messages.UIMessage { +func (view *RoomView) newUIMessage(id, sender string, msgtype mautrix.MessageType, text string, timestamp time.Time) messages.UIMessage { member := view.Room.GetMember(sender) displayname := sender if member != nil { @@ -357,11 +357,11 @@ func (view *RoomView) newUIMessage(id, sender string, msgtype gomatrix.MessageTy return msg } -func (view *RoomView) NewMessage(id, sender string, msgtype gomatrix.MessageType, text string, timestamp time.Time) ifc.Message { +func (view *RoomView) NewMessage(id, sender string, msgtype mautrix.MessageType, text string, timestamp time.Time) ifc.Message { return view.newUIMessage(id, sender, msgtype, text, timestamp) } -func (view *RoomView) NewTempMessage(msgtype gomatrix.MessageType, text string) ifc.Message { +func (view *RoomView) NewTempMessage(msgtype mautrix.MessageType, text string) ifc.Message { now := time.Now() id := strconv.FormatInt(now.UnixNano(), 10) sender := "" diff --git a/ui/view-login.go b/ui/view-login.go index a900870..3d42506 100644 --- a/ui/view-login.go +++ b/ui/view-login.go @@ -17,11 +17,11 @@ package ui import ( - "maunium.net/go/gomatrix" "maunium.net/go/gomuks/config" "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/ui/widget" + "maunium.net/go/mautrix" "maunium.net/go/tview" ) @@ -89,8 +89,8 @@ func (view *LoginView) Login() { debug.Print("Init error:", err) err = view.matrix.Login(mxid, password) if err != nil { - if httpErr, ok := err.(gomatrix.HTTPError); ok { - if respErr, ok := httpErr.WrappedError.(gomatrix.RespError); ok { + if httpErr, ok := err.(mautrix.HTTPError); ok { + if respErr, ok := httpErr.WrappedError.(mautrix.RespError); ok { view.Error(respErr.Err) } else { view.Error(httpErr.Message) diff --git a/ui/view-main.go b/ui/view-main.go index c5abb19..5faa804 100644 --- a/ui/view-main.go +++ b/ui/view-main.go @@ -26,7 +26,7 @@ import ( "bufio" "os" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/config" "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/interface" @@ -152,8 +152,8 @@ func (view *MainView) sendTempMessage(roomView *RoomView, tempMessage ifc.Messag eventID, err := view.matrix.SendMarkdownMessage(roomView.Room.ID, tempMessage.Type(), text) if err != nil { tempMessage.SetState(ifc.MessageStateFailed) - if httpErr, ok := err.(gomatrix.HTTPError); ok { - if respErr, ok := httpErr.WrappedError.(gomatrix.RespError); ok { + if httpErr, ok := err.(mautrix.HTTPError); ok { + if respErr, ok := httpErr.WrappedError.(mautrix.RespError); ok { // Show shorter version if available err = respErr } @@ -510,6 +510,6 @@ func (view *MainView) LoadHistory(room string) { view.parent.Render() } -func (view *MainView) ParseEvent(roomView ifc.RoomView, evt *gomatrix.Event) ifc.Message { +func (view *MainView) ParseEvent(roomView ifc.RoomView, evt *mautrix.Event) ifc.Message { return parser.ParseEvent(view.matrix, roomView.MxRoom(), evt) } diff --git a/ui/widget/border.go b/ui/widget/border.go index b3eb65d..834eedb 100644 --- a/ui/widget/border.go +++ b/ui/widget/border.go @@ -40,11 +40,11 @@ func (border *Border) Draw(screen tcell.Screen) { x, y, width, height := border.GetRect() if width == 1 { for borderY := y; borderY < y+height; borderY++ { - screen.SetContent(x, borderY, tview.GraphicsVertBar, nil, background) + screen.SetContent(x, borderY, tview.Borders.Vertical, nil, background) } } else if height == 1 { for borderX := x; borderX < x+width; borderX++ { - screen.SetContent(borderX, y, tview.GraphicsHoriBar, nil, background) + screen.SetContent(borderX, y, tview.Borders.Horizontal, nil, background) } } } diff --git a/vendor/golang.org/x/image/bmp/reader.go b/vendor/golang.org/x/image/bmp/reader.go index a0f2715..c10a022 100644 --- a/vendor/golang.org/x/image/bmp/reader.go +++ b/vendor/golang.org/x/image/bmp/reader.go @@ -137,20 +137,26 @@ func decodeConfig(r io.Reader) (config image.Config, bitsPerPixel int, topDown b // We only support those BMP images that are a BITMAPFILEHEADER // immediately followed by a BITMAPINFOHEADER. const ( - fileHeaderLen = 14 - infoHeaderLen = 40 + fileHeaderLen = 14 + infoHeaderLen = 40 + v4InfoHeaderLen = 108 + v5InfoHeaderLen = 124 ) var b [1024]byte - if _, err := io.ReadFull(r, b[:fileHeaderLen+infoHeaderLen]); err != nil { + if _, err := io.ReadFull(r, b[:fileHeaderLen+4]); err != nil { return image.Config{}, 0, false, err } if string(b[:2]) != "BM" { return image.Config{}, 0, false, errors.New("bmp: invalid format") } offset := readUint32(b[10:14]) - if readUint32(b[14:18]) != infoHeaderLen { + infoLen := readUint32(b[14:18]) + if infoLen != infoHeaderLen && infoLen != v4InfoHeaderLen && infoLen != v5InfoHeaderLen { return image.Config{}, 0, false, ErrUnsupported } + if _, err := io.ReadFull(r, b[fileHeaderLen+4:fileHeaderLen+infoLen]); err != nil { + return image.Config{}, 0, false, err + } width := int(int32(readUint32(b[18:22]))) height := int(int32(readUint32(b[22:26]))) if height < 0 { @@ -159,14 +165,22 @@ func decodeConfig(r io.Reader) (config image.Config, bitsPerPixel int, topDown b if width < 0 || height < 0 { return image.Config{}, 0, false, ErrUnsupported } - // We only support 1 plane, 8 or 24 bits per pixel and no compression. + // We only support 1 plane and 8, 24 or 32 bits per pixel and no + // compression. planes, bpp, compression := readUint16(b[26:28]), readUint16(b[28:30]), readUint32(b[30:34]) + // if compression is set to BITFIELDS, but the bitmask is set to the default bitmask + // that would be used if compression was set to 0, we can continue as if compression was 0 + if compression == 3 && infoLen > infoHeaderLen && + readUint32(b[54:58]) == 0xff0000 && readUint32(b[58:62]) == 0xff00 && + readUint32(b[62:66]) == 0xff && readUint32(b[66:70]) == 0xff000000 { + compression = 0 + } if planes != 1 || compression != 0 { return image.Config{}, 0, false, ErrUnsupported } switch bpp { case 8: - if offset != fileHeaderLen+infoHeaderLen+256*4 { + if offset != fileHeaderLen+infoLen+256*4 { return image.Config{}, 0, false, ErrUnsupported } _, err = io.ReadFull(r, b[:256*4]) @@ -181,12 +195,12 @@ func decodeConfig(r io.Reader) (config image.Config, bitsPerPixel int, topDown b } return image.Config{ColorModel: pcm, Width: width, Height: height}, 8, topDown, nil case 24: - if offset != fileHeaderLen+infoHeaderLen { + if offset != fileHeaderLen+infoLen { return image.Config{}, 0, false, ErrUnsupported } return image.Config{ColorModel: color.RGBAModel, Width: width, Height: height}, 24, topDown, nil case 32: - if offset != fileHeaderLen+infoHeaderLen { + if offset != fileHeaderLen+infoLen { return image.Config{}, 0, false, ErrUnsupported } return image.Config{ColorModel: color.RGBAModel, Width: width, Height: height}, 32, topDown, nil diff --git a/vendor/golang.org/x/image/tiff/reader.go b/vendor/golang.org/x/image/tiff/reader.go index 8a941c1..ce2ef71 100644 --- a/vendor/golang.org/x/image/tiff/reader.go +++ b/vendor/golang.org/x/image/tiff/reader.go @@ -110,7 +110,7 @@ func (d *decoder) ifdUint(p []byte) (u []uint, err error) { return u, nil } -// parseIFD decides whether the the IFD entry in p is "interesting" and +// parseIFD decides whether the IFD entry in p is "interesting" and // stows away the data in the decoder. It returns the tag number of the // entry and an error, if any. func (d *decoder) parseIFD(p []byte) (int, error) { diff --git a/vendor/golang.org/x/image/vp8/decode.go b/vendor/golang.org/x/image/vp8/decode.go index 1bb5028..2aa9fee 100644 --- a/vendor/golang.org/x/image/vp8/decode.go +++ b/vendor/golang.org/x/image/vp8/decode.go @@ -82,7 +82,7 @@ type mb struct { pred [4]uint8 // nzMask is a mask of 8 bits: 4 for the bottom or right 4x4 luma regions, // and 2 + 2 for the bottom or right 4x4 chroma regions. A 1 bit indicates - // that that region has non-zero coefficients. + // that region has non-zero coefficients. nzMask uint8 // nzY16 is a 0/1 value that is 1 if the macroblock used Y16 prediction and // had non-zero coefficients. @@ -274,7 +274,7 @@ func (d *Decoder) parseOtherPartitions() error { var partLens [maxNOP]int d.nOP = 1 << d.fp.readUint(uniformProb, 2) - // The final partition length is implied by the the remaining chunk data + // The final partition length is implied by the remaining chunk data // (d.r.n) and the other d.nOP-1 partition lengths. Those d.nOP-1 partition // lengths are stored as 24-bit uints, i.e. up to 16 MiB per partition. n := 3 * (d.nOP - 1) diff --git a/vendor/golang.org/x/image/webp/decode.go b/vendor/golang.org/x/image/webp/decode.go index 111f358..f77a4eb 100644 --- a/vendor/golang.org/x/image/webp/decode.go +++ b/vendor/golang.org/x/image/webp/decode.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build go1.6 - package webp import ( diff --git a/vendor/golang.org/x/image/webp/doc.go b/vendor/golang.org/x/image/webp/doc.go new file mode 100644 index 0000000..e321c85 --- /dev/null +++ b/vendor/golang.org/x/image/webp/doc.go @@ -0,0 +1,9 @@ +// Copyright 2016 The Go 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 webp implements a decoder for WEBP images. +// +// WEBP is defined at: +// https://developers.google.com/speed/webp/docs/riff_container +package webp // import "golang.org/x/image/webp" diff --git a/vendor/golang.org/x/image/webp/webp.go b/vendor/golang.org/x/image/webp/webp.go deleted file mode 100644 index 850cdc8..0000000 --- a/vendor/golang.org/x/image/webp/webp.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2016 The Go 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 webp implements a decoder for WEBP images. -// -// WEBP is defined at: -// https://developers.google.com/speed/webp/docs/riff_container -// -// It requires Go 1.6 or later. -package webp // import "golang.org/x/image/webp" - -// This blank Go file, other than the package clause, exists so that this -// package can be built for Go 1.5 and earlier. (The other files in this -// package are all marked "+build go1.6" for the NYCbCrA types introduced in Go -// 1.6). There is no functionality in a blank package, but some image -// manipulation programs might still underscore import this package for the -// side effect of registering the WEBP format with the standard library's -// image.RegisterFormat and image.Decode functions. For example, that program -// might contain: -// -// // Underscore imports to register some formats for image.Decode. -// import _ "image/gif" -// import _ "image/jpeg" -// import _ "image/png" -// import _ "golang.org/x/image/webp" -// -// Such a program will still compile for Go 1.5 (due to this placeholder Go -// file). It will simply not be able to recognize and decode WEBP (but still -// handle GIF, JPEG and PNG). diff --git a/vendor/golang.org/x/net/html/const.go b/vendor/golang.org/x/net/html/const.go index 5eb7c5a..a3a918f 100644 --- a/vendor/golang.org/x/net/html/const.go +++ b/vendor/golang.org/x/net/html/const.go @@ -97,8 +97,16 @@ func isSpecialElement(element *Node) bool { switch element.Namespace { case "", "html": return isSpecialElementMap[element.Data] + case "math": + switch element.Data { + case "mi", "mo", "mn", "ms", "mtext", "annotation-xml": + return true + } case "svg": - return element.Data == "foreignObject" + switch element.Data { + case "foreignObject", "desc", "title": + return true + } } return false } diff --git a/vendor/golang.org/x/net/html/parse.go b/vendor/golang.org/x/net/html/parse.go index 4b1fa42..64a5793 100644 --- a/vendor/golang.org/x/net/html/parse.go +++ b/vendor/golang.org/x/net/html/parse.go @@ -470,6 +470,10 @@ func (p *parser) resetInsertionMode() { case a.Table: p.im = inTableIM case a.Template: + // TODO: remove this divergence from the HTML5 spec. + if n.Namespace != "" { + continue + } p.im = p.templateStack.top() case a.Head: // TODO: remove this divergence from the HTML5 spec. @@ -984,6 +988,14 @@ func inBodyIM(p *parser) bool { p.acknowledgeSelfClosingTag() p.popUntil(buttonScope, a.P) p.parseImpliedToken(StartTagToken, a.Form, a.Form.String()) + if p.form == nil { + // NOTE: The 'isindex' element has been removed, + // and the 'template' element has not been designed to be + // collaborative with the index element. + // + // Ignore the token. + return true + } if action != "" { p.form.Attr = []Attribute{{Key: "action", Val: action}} } @@ -1252,12 +1264,6 @@ func (p *parser) inBodyEndTagFormatting(tagAtom a.Atom) { switch commonAncestor.DataAtom { case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr: p.fosterParent(lastNode) - case a.Template: - // TODO: remove namespace checking - if commonAncestor.Namespace == "html" { - commonAncestor = commonAncestor.LastChild - } - fallthrough default: commonAncestor.AppendChild(lastNode) } @@ -2209,6 +2215,15 @@ func (p *parser) parse() error { } // Parse returns the parse tree for the HTML from the given Reader. +// +// It implements the HTML5 parsing algorithm +// (https://html.spec.whatwg.org/multipage/syntax.html#tree-construction), +// which is very complicated. The resultant tree can contain implicitly created +// nodes that have no explicit listed in r's data, and nodes' parents can +// differ from the nesting implied by a naive processing of start and end +// s. Conversely, explicit s in r's data can be silently dropped, +// with no corresponding node in the resulting tree. +// // The input is assumed to be UTF-8 encoded. func Parse(r io.Reader) (*Node, error) { p := &parser{ @@ -2230,6 +2245,8 @@ func Parse(r io.Reader) (*Node, error) { // ParseFragment parses a fragment of HTML and returns the nodes that were // found. If the fragment is the InnerHTML for an existing element, pass that // element in context. +// +// It has the same intricacies as Parse. func ParseFragment(r io.Reader, context *Node) ([]*Node, error) { contextTag := "" if context != nil { diff --git a/vendor/gopkg.in/russross/blackfriday.v2/.travis.yml b/vendor/gopkg.in/russross/blackfriday.v2/.travis.yml index a4eb257..b0b525a 100644 --- a/vendor/gopkg.in/russross/blackfriday.v2/.travis.yml +++ b/vendor/gopkg.in/russross/blackfriday.v2/.travis.yml @@ -1,18 +1,17 @@ -# Travis CI (http://travis-ci.org/) is a continuous integration service for -# open source projects. This file configures it to run unit tests for -# blackfriday. - +sudo: false language: go - go: - - 1.5 - - 1.6 - - 1.7 - + - "1.10.x" + - "1.11.x" + - tip +matrix: + fast_finish: true + allow_failures: + - go: tip install: - - go get -d -t -v ./... - - go build -v ./... - + - # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step). script: - - go test -v ./... - - go test -run=^$ -bench=BenchmarkReference -benchmem + - go get -t -v ./... + - diff -u <(echo -n) <(gofmt -d -s .) + - go tool vet . + - go test -v ./... diff --git a/vendor/gopkg.in/russross/blackfriday.v2/README.md b/vendor/gopkg.in/russross/blackfriday.v2/README.md index 2e0db35..d5a8649 100644 --- a/vendor/gopkg.in/russross/blackfriday.v2/README.md +++ b/vendor/gopkg.in/russross/blackfriday.v2/README.md @@ -34,9 +34,15 @@ Versions -------- Currently maintained and recommended version of Blackfriday is `v2`. It's being -developed on its own branch: https://github.com/russross/blackfriday/v2. You -should install and import it via [gopkg.in][6] at -`gopkg.in/russross/blackfriday.v2`. +developed on its own branch: https://github.com/russross/blackfriday/tree/v2 and the +documentation is available at +https://godoc.org/gopkg.in/russross/blackfriday.v2. + +It is `go get`-able via via [gopkg.in][6] at `gopkg.in/russross/blackfriday.v2`, +but we highly recommend using package management tool like [dep][7] or +[Glide][8] and make use of semantic versioning. With package management you +should import `github.com/russross/blackfriday` and specify that you're using +version 2.0.0. Version 2 offers a number of improvements over v1: @@ -198,7 +204,7 @@ implements the following extensions: Cat : Fluffy animal everyone likes - + Internet : Vector of transmission for pictures of cats @@ -209,7 +215,7 @@ implements the following extensions: end of the document. A footnote looks like this: This is a footnote.[^1] - + [^1]: the footnote text. * **Autolinking**. Blackfriday can find URLs that have not been @@ -255,9 +261,11 @@ are a few of note: * [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt, but for markdown. -* [LaTeX output](https://bitbucket.org/ambrevar/blackfriday-latex): +* [LaTeX output](https://github.com/Ambrevar/Blackfriday-LaTeX): renders output as LaTeX. +* [Blackfriday-Confluence](https://github.com/kentaro-m/blackfriday-confluence): provides a [Confluence Wiki Markup](https://confluence.atlassian.com/doc/confluence-wiki-markup-251003035.html) renderer. + Todo ---- diff --git a/vendor/gopkg.in/russross/blackfriday.v2/block.go b/vendor/gopkg.in/russross/blackfriday.v2/block.go index d7da33f..b860747 100644 --- a/vendor/gopkg.in/russross/blackfriday.v2/block.go +++ b/vendor/gopkg.in/russross/blackfriday.v2/block.go @@ -17,6 +17,7 @@ import ( "bytes" "html" "regexp" + "strings" "github.com/shurcooL/sanitized_anchor_name" ) @@ -568,8 +569,8 @@ func (*Markdown) isHRule(data []byte) bool { // isFenceLine checks if there's a fence line (e.g., ``` or ``` go) at the beginning of data, // and returns the end index if so, or 0 otherwise. It also returns the marker found. -// If syntax is not nil, it gets set to the syntax specified in the fence line. -func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker string) { +// If info is not nil, it gets set to the syntax specified in the fence line. +func isFenceLine(data []byte, info *string, oldmarker string) (end int, marker string) { i, size := 0, 0 // skip up to three spaces @@ -605,9 +606,9 @@ func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker } // TODO(shurcooL): It's probably a good idea to simplify the 2 code paths here - // into one, always get the syntax, and discard it if the caller doesn't care. - if syntax != nil { - syn := 0 + // into one, always get the info string, and discard it if the caller doesn't care. + if info != nil { + infoLength := 0 i = skipChar(data, i, ' ') if i >= len(data) { @@ -617,14 +618,14 @@ func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker return 0, "" } - syntaxStart := i + infoStart := i if data[i] == '{' { i++ - syntaxStart++ + infoStart++ for i < len(data) && data[i] != '}' && data[i] != '\n' { - syn++ + infoLength++ i++ } @@ -634,31 +635,30 @@ func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker // strip all whitespace at the beginning and the end // of the {} block - for syn > 0 && isspace(data[syntaxStart]) { - syntaxStart++ - syn-- + for infoLength > 0 && isspace(data[infoStart]) { + infoStart++ + infoLength-- } - for syn > 0 && isspace(data[syntaxStart+syn-1]) { - syn-- + for infoLength > 0 && isspace(data[infoStart+infoLength-1]) { + infoLength-- } - i++ + i = skipChar(data, i, ' ') } else { - for i < len(data) && !isspace(data[i]) { - syn++ + for i < len(data) && !isverticalspace(data[i]) { + infoLength++ i++ } } - *syntax = string(data[syntaxStart : syntaxStart+syn]) + *info = strings.TrimSpace(string(data[infoStart : infoStart+infoLength])) } - i = skipChar(data, i, ' ') - if i >= len(data) || data[i] != '\n' { - if i == len(data) { - return i, marker - } + if i == len(data) { + return i, marker + } + if i > len(data) || data[i] != '\n' { return 0, "" } return i + 1, marker // Take newline into account. @@ -668,14 +668,14 @@ func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker // or 0 otherwise. It writes to out if doRender is true, otherwise it has no side effects. // If doRender is true, a final newline is mandatory to recognize the fenced code block. func (p *Markdown) fencedCodeBlock(data []byte, doRender bool) int { - var syntax string - beg, marker := isFenceLine(data, &syntax, "") + var info string + beg, marker := isFenceLine(data, &info, "") if beg == 0 || beg >= len(data) { return 0 } var work bytes.Buffer - work.Write([]byte(syntax)) + work.Write([]byte(info)) work.WriteByte('\n') for { @@ -1148,6 +1148,18 @@ func (p *Markdown) list(data []byte, flags ListType) int { return i } +// Returns true if the list item is not the same type as its parent list +func (p *Markdown) listTypeChanged(data []byte, flags *ListType) bool { + if p.dliPrefix(data) > 0 && *flags&ListTypeDefinition == 0 { + return true + } else if p.oliPrefix(data) > 0 && *flags&ListTypeOrdered == 0 { + return true + } else if p.uliPrefix(data) > 0 && (*flags&ListTypeOrdered != 0 || *flags&ListTypeDefinition != 0) { + return true + } + return false +} + // Returns true if block ends with a blank line, descending if needed // into lists and sublists. func endsWithBlankLine(block *Node) bool { @@ -1246,6 +1258,7 @@ func (p *Markdown) listItem(data []byte, flags *ListType) int { // process the following lines containsBlankLine := false sublist := 0 + codeBlockMarker := "" gatherlines: for line < len(data) { @@ -1279,6 +1292,27 @@ gatherlines: chunk := data[line+indentIndex : i] + if p.extensions&FencedCode != 0 { + // determine if in or out of codeblock + // if in codeblock, ignore normal list processing + _, marker := isFenceLine(chunk, nil, codeBlockMarker) + if marker != "" { + if codeBlockMarker == "" { + // start of codeblock + codeBlockMarker = marker + } else { + // end of codeblock. + codeBlockMarker = "" + } + } + // we are in a codeblock, write line, and continue + if codeBlockMarker != "" || marker != "" { + raw.Write(data[line+indentIndex : i]) + line = i + continue gatherlines + } + } + // evaluate how this line fits in switch { // is this a nested list item? @@ -1286,16 +1320,23 @@ gatherlines: p.oliPrefix(chunk) > 0 || p.dliPrefix(chunk) > 0: - if containsBlankLine { - *flags |= ListItemContainsBlock - } - // to be a nested list, it must be indented more - // if not, it is the next item in the same list + // if not, it is either a different kind of list + // or the next item in the same list if indent <= itemIndent { + if p.listTypeChanged(chunk, flags) { + *flags |= ListItemEndOfList + } else if containsBlankLine { + *flags |= ListItemContainsBlock + } + break gatherlines } + if containsBlankLine { + *flags |= ListItemContainsBlock + } + // is this the first item in the nested list? if sublist == 0 { sublist = raw.Len() diff --git a/vendor/gopkg.in/russross/blackfriday.v2/go.mod b/vendor/gopkg.in/russross/blackfriday.v2/go.mod new file mode 100644 index 0000000..620b74e --- /dev/null +++ b/vendor/gopkg.in/russross/blackfriday.v2/go.mod @@ -0,0 +1 @@ +module github.com/russross/blackfriday/v2 diff --git a/vendor/gopkg.in/russross/blackfriday.v2/html.go b/vendor/gopkg.in/russross/blackfriday.v2/html.go index 25fb185..284c871 100644 --- a/vendor/gopkg.in/russross/blackfriday.v2/html.go +++ b/vendor/gopkg.in/russross/blackfriday.v2/html.go @@ -35,6 +35,7 @@ const ( Safelink // Only link to trusted protocols NofollowLinks // Only link with rel="nofollow" NoreferrerLinks // Only link with rel="noreferrer" + NoopenerLinks // Only link with rel="noopener" HrefTargetBlank // Add a blank target CompletePage // Generate a complete HTML page UseXHTML // Generate XHTML output instead of HTML @@ -87,6 +88,10 @@ type HTMLRendererParameters struct { HeadingIDPrefix string // If set, add this text to the back of each Heading ID, to ensure uniqueness. HeadingIDSuffix string + // Increase heading levels: if the offset is 1,

becomes

etc. + // Negative offset is also valid. + // Resulting levels are clipped between 1 and 6. + HeadingLevelOffset int Title string // Document title (used if CompletePage is set) CSS string // Optional CSS file URL (used if CompletePage is set) @@ -282,6 +287,9 @@ func appendLinkAttrs(attrs []string, flags HTMLFlags, link []byte) []string { if flags&NoreferrerLinks != 0 { val = append(val, "noreferrer") } + if flags&NoopenerLinks != 0 { + val = append(val, "noopener") + } if flags&HrefTargetBlank != 0 { attrs = append(attrs, "target=\"_blank\"") } @@ -331,7 +339,7 @@ func (r *HTMLRenderer) tag(w io.Writer, name []byte, attrs []string) { func footnoteRef(prefix string, node *Node) []byte { urlFrag := prefix + string(slugify(node.Destination)) - anchor := fmt.Sprintf(`%d`, urlFrag, node.NoteID) + anchor := fmt.Sprintf(`%d`, urlFrag, node.NoteID) return []byte(fmt.Sprintf(`%s`, urlFrag, anchor)) } @@ -460,9 +468,10 @@ var ( ) func headingTagsFromLevel(level int) ([]byte, []byte) { - switch level { - case 1: + if level <= 1 { return h1Tag, h1CloseTag + } + switch level { case 2: return h2Tag, h2CloseTag case 3: @@ -471,9 +480,8 @@ func headingTagsFromLevel(level int) ([]byte, []byte) { return h4Tag, h4CloseTag case 5: return h5Tag, h5CloseTag - default: - return h6Tag, h6CloseTag } + return h6Tag, h6CloseTag } func (r *HTMLRenderer) outHRTag(w io.Writer) { @@ -651,7 +659,8 @@ func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkSt r.out(w, node.Literal) r.cr(w) case Heading: - openTag, closeTag := headingTagsFromLevel(node.Level) + headingLevel := r.HTMLRendererParameters.HeadingLevelOffset + node.Level + openTag, closeTag := headingTagsFromLevel(headingLevel) if entering { if node.IsTitleblock { attrs = append(attrs, `class="title"`) diff --git a/vendor/gopkg.in/russross/blackfriday.v2/inline.go b/vendor/gopkg.in/russross/blackfriday.v2/inline.go index 3d63310..4ed2907 100644 --- a/vendor/gopkg.in/russross/blackfriday.v2/inline.go +++ b/vendor/gopkg.in/russross/blackfriday.v2/inline.go @@ -23,8 +23,22 @@ var ( urlRe = `((https?|ftp):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+` anchorRe = regexp.MustCompile(`^(]+")?\s?>` + urlRe + `<\/a>)`) - // TODO: improve this regexp to catch all possible entities: - htmlEntityRe = regexp.MustCompile(`&[a-z]{2,5};`) + // https://www.w3.org/TR/html5/syntax.html#character-references + // highest unicode code point in 17 planes (2^20): 1,114,112d = + // 7 dec digits or 6 hex digits + // named entity references can be 2-31 characters with stuff like < + // at one end and ∳ at the other. There + // are also sometimes numbers at the end, although this isn't inherent + // in the specification; there are never numbers anywhere else in + // current character references, though; see ¾ and ▒, etc. + // https://www.w3.org/TR/html5/syntax.html#named-character-references + // + // entity := "&" (named group | number ref) ";" + // named group := [a-zA-Z]{2,31}[0-9]{0,2} + // number ref := "#" (dec ref | hex ref) + // dec ref := [0-9]{1,7} + // hex ref := ("x" | "X") [0-9a-fA-F]{1,6} + htmlEntityRe = regexp.MustCompile(`&([a-zA-Z]{2,31}[0-9]{0,2}|#([0-9]{1,7}|[xX][0-9a-fA-F]{1,6}));`) ) // Functions to parse text within a block diff --git a/vendor/gopkg.in/russross/blackfriday.v2/markdown.go b/vendor/gopkg.in/russross/blackfriday.v2/markdown.go index ff61cb0..58d2e45 100644 --- a/vendor/gopkg.in/russross/blackfriday.v2/markdown.go +++ b/vendor/gopkg.in/russross/blackfriday.v2/markdown.go @@ -93,46 +93,46 @@ const ( // blockTags is a set of tags that are recognized as HTML block tags. // Any of these can be included in markdown text without special escaping. var blockTags = map[string]struct{}{ - "blockquote": struct{}{}, - "del": struct{}{}, - "div": struct{}{}, - "dl": struct{}{}, - "fieldset": struct{}{}, - "form": struct{}{}, - "h1": struct{}{}, - "h2": struct{}{}, - "h3": struct{}{}, - "h4": struct{}{}, - "h5": struct{}{}, - "h6": struct{}{}, - "iframe": struct{}{}, - "ins": struct{}{}, - "math": struct{}{}, - "noscript": struct{}{}, - "ol": struct{}{}, - "pre": struct{}{}, - "p": struct{}{}, - "script": struct{}{}, - "style": struct{}{}, - "table": struct{}{}, - "ul": struct{}{}, + "blockquote": {}, + "del": {}, + "div": {}, + "dl": {}, + "fieldset": {}, + "form": {}, + "h1": {}, + "h2": {}, + "h3": {}, + "h4": {}, + "h5": {}, + "h6": {}, + "iframe": {}, + "ins": {}, + "math": {}, + "noscript": {}, + "ol": {}, + "pre": {}, + "p": {}, + "script": {}, + "style": {}, + "table": {}, + "ul": {}, // HTML5 - "address": struct{}{}, - "article": struct{}{}, - "aside": struct{}{}, - "canvas": struct{}{}, - "figcaption": struct{}{}, - "figure": struct{}{}, - "footer": struct{}{}, - "header": struct{}{}, - "hgroup": struct{}{}, - "main": struct{}{}, - "nav": struct{}{}, - "output": struct{}{}, - "progress": struct{}{}, - "section": struct{}{}, - "video": struct{}{}, + "address": {}, + "article": {}, + "aside": {}, + "canvas": {}, + "figcaption": {}, + "figure": {}, + "footer": {}, + "header": {}, + "hgroup": {}, + "main": {}, + "nav": {}, + "output": {}, + "progress": {}, + "section": {}, + "video": {}, } // Renderer is the rendering interface. This is mostly of interest if you are @@ -480,11 +480,11 @@ func (p *Markdown) parseRefsToAST() { // [^note]: This is the explanation. // // Footnotes should be placed at the end of the document in an ordered list. -// Inline footnotes such as: +// Finally, there are inline footnotes such as: // -// Inline footnotes^[Not supported.] also exist. +// Inline footnotes^[Also supported.] provide a quick inline explanation, +// but are rendered at the bottom of the document. // -// are not yet supported. // reference holds all information necessary for a reference-style links or // footnotes. @@ -813,7 +813,17 @@ func ispunct(c byte) bool { // Test if a character is a whitespace character. func isspace(c byte) bool { - return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v' + return ishorizontalspace(c) || isverticalspace(c) +} + +// Test if a character is a horizontal whitespace character. +func ishorizontalspace(c byte) bool { + return c == ' ' || c == '\t' +} + +// Test if a character is a vertical character. +func isverticalspace(c byte) bool { + return c == '\n' || c == '\r' || c == '\f' || c == '\v' } // Test if a character is letter. diff --git a/vendor/maunium.net/go/gomatrix/.gitignore b/vendor/maunium.net/go/gomatrix/.gitignore deleted file mode 100644 index daf913b..0000000 --- a/vendor/maunium.net/go/gomatrix/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Compiled Object files, Static and Dynamic libs (Shared Objects) -*.o -*.a -*.so - -# Folders -_obj -_test - -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - -*.exe -*.test -*.prof diff --git a/vendor/maunium.net/go/gomatrix/.travis.yml b/vendor/maunium.net/go/gomatrix/.travis.yml deleted file mode 100644 index fadc326..0000000 --- a/vendor/maunium.net/go/gomatrix/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: go -go: - - 1.8 -install: - - go get github.com/golang/lint/golint - - go get github.com/fzipp/gocyclo - - go get github.com/client9/misspell/... - - go get github.com/gordonklaus/ineffassign -script: ./hooks/pre-commit diff --git a/vendor/maunium.net/go/gomatrix/LICENSE b/vendor/maunium.net/go/gomatrix/LICENSE deleted file mode 100644 index 8dada3e..0000000 --- a/vendor/maunium.net/go/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/maunium.net/go/gomatrix/README.md b/vendor/maunium.net/go/gomatrix/README.md deleted file mode 100644 index ea9109a..0000000 --- a/vendor/maunium.net/go/gomatrix/README.md +++ /dev/null @@ -1,6 +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.** diff --git a/vendor/maunium.net/go/gomatrix/client.go b/vendor/maunium.net/go/gomatrix/client.go deleted file mode 100644 index 0806138..0000000 --- a/vendor/maunium.net/go/gomatrix/client.go +++ /dev/null @@ -1,794 +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" - "errors" - "fmt" - "io" - "io/ioutil" - "maunium.net/go/maulogger" - "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 - Logger maulogger.Logger - - // The ?user_id= query parameter for application services. This must be set *prior* to calling a method. If this is empty, - // no user_id parameter will be sent. - // See http://matrix.org/docs/spec/application_service/unstable.html#identity-assertion - AppServiceUserID string - - syncingMutex sync.Mutex // protects syncingID - syncingID uint32 // Identifies the current Sync. Only one Sync can be active at any given time. -} - -// HTTPError An HTTP Error response, which may wrap an underlying native Go Error. -type HTTPError struct { - WrappedError error - RespError *RespError - Message string - Code int -} - -func (e HTTPError) Error() string { - var wrappedErrMsg string - if e.WrappedError != nil { - wrappedErrMsg = e.WrappedError.Error() - } - return fmt.Sprintf("msg=%s code=%d wrapped=%s", e.Message, e.Code, wrappedErrMsg) -} - -// BuildURL builds a URL with the Client's homserver/prefix/access_token set already. -func (cli *Client) BuildURL(urlPath ...string) string { - ps := []string{cli.Prefix} - for _, p := range urlPath { - ps = append(ps, p) - } - return cli.BuildBaseURL(ps...) -} - -// BuildBaseURL builds a URL with the Client's homeserver/access_token set already. You must -// supply the prefix in the path. -func (cli *Client) BuildBaseURL(urlPath ...string) string { - // copy the URL. Purposefully ignore error as the input is from a valid URL already - hsURL, _ := url.Parse(cli.HomeserverURL.String()) - parts := []string{hsURL.Path} - parts = append(parts, urlPath...) - hsURL.Path = path.Join(parts...) - query := hsURL.Query() - if cli.AccessToken != "" { - query.Set("access_token", cli.AccessToken) - } - if cli.AppServiceUserID != "" { - query.Set("user_id", cli.AppServiceUserID) - } - hsURL.RawQuery = query.Encode() - return hsURL.String() -} - -// BuildURLWithQuery builds a URL with query parameters in addition to the Client's homeserver/prefix/access_token set already. -func (cli *Client) BuildURLWithQuery(urlPath []string, urlQuery map[string]string) string { - u, _ := url.Parse(cli.BuildURL(urlPath...)) - q := u.Query() - for k, v := range urlQuery { - q.Set(k, v) - } - u.RawQuery = q.Encode() - return u.String() -} - -// SetCredentials sets the user ID and access token on this client instance. -func (cli *Client) SetCredentials(userID, accessToken string) { - cli.AccessToken = accessToken - cli.UserID = userID -} - -// ClearCredentials removes the user ID and access token on this client instance. -func (cli *Client) ClearCredentials() { - cli.AccessToken = "" - cli.UserID = "" -} - -// Sync starts syncing with the provided Homeserver. If Sync() is called twice then the first sync will be stopped and the -// error will be nil. -// -// This function will block until a fatal /sync error occurs, so it should almost always be started as a new goroutine. -// Fatal sync errors can be caused by: -// - The failure to create a filter. -// - Client.Syncer.OnFailedSync returning an error in response to a failed sync. -// - Client.Syncer.ProcessResponse returning an error. -// If you wish to continue retrying in spite of these fatal errors, call Sync() again. -func (cli *Client) Sync() error { - // Mark the client as syncing. - // We will keep syncing until the syncing state changes. Either because - // Sync is called or StopSync is called. - syncingID := cli.incrementSyncingID() - nextBatch := cli.Store.LoadNextBatch(cli.UserID) - filterID := cli.Store.LoadFilterID(cli.UserID) - if filterID == "" { - filterJSON := cli.Syncer.GetFilterJSON(cli.UserID) - resFilter, err := cli.CreateFilter(filterJSON) - if err != nil { - return err - } - filterID = resFilter.FilterID - cli.Store.SaveFilterID(cli.UserID, filterID) - } - - for { - resSync, err := cli.SyncRequest(30000, nextBatch, filterID, false, "") - if err != nil { - duration, err2 := cli.Syncer.OnFailedSync(resSync, err) - if err2 != nil { - return err2 - } - time.Sleep(duration) - continue - } - - // Check that the syncing state hasn't changed - // Either because we've stopped syncing or another sync has been started. - // We discard the response from our sync. - if cli.getSyncingID() != syncingID { - return nil - } - - // Save the token now *before* processing it. This means it's possible - // to not process some events, but it means that we won't get constantly stuck processing - // a malformed/buggy event which keeps making us panic. - cli.Store.SaveNextBatch(cli.UserID, resSync.NextBatch) - if err = cli.Syncer.ProcessResponse(resSync, nextBatch); err != nil { - return err - } - - nextBatch = resSync.NextBatch - } -} - -func (cli *Client) incrementSyncingID() uint32 { - cli.syncingMutex.Lock() - defer cli.syncingMutex.Unlock() - cli.syncingID++ - return cli.syncingID -} - -func (cli *Client) getSyncingID() uint32 { - cli.syncingMutex.Lock() - defer cli.syncingMutex.Unlock() - return cli.syncingID -} - -// StopSync stops the ongoing sync started by Sync. -func (cli *Client) StopSync() { - // Advance the syncing state so that any running Syncs will terminate. - cli.incrementSyncingID() -} - -func (cli *Client) LogRequest(req *http.Request, body string) { - if cli.Logger == nil { - return - } - - cli.Logger.Debugfln("%s %s %s", req.Method, req.URL.Path, body) -} - -// MakeRequest makes a JSON HTTP request to the given URL. -// If "resBody" is not nil, the response body will be json.Unmarshalled into it. -// -// Returns the HTTP body as bytes on 2xx with a nil error. Returns an error if the response is not 2xx along -// with the HTTP body bytes if it got that far. This error is an HTTPError which includes the returned -// HTTP status code and possibly a RespError as the WrappedError, if the HTTP body could be decoded as a RespError. -func (cli *Client) MakeRequest(method string, httpURL string, reqBody interface{}, resBody interface{}) ([]byte, error) { - var req *http.Request - var err error - logBody := "{}" - if reqBody != nil { - var jsonStr []byte - jsonStr, err = json.Marshal(reqBody) - if err != nil { - return nil, err - } - logBody = string(jsonStr) - req, err = http.NewRequest(method, httpURL, bytes.NewBuffer(jsonStr)) - } else { - req, err = http.NewRequest(method, httpURL, nil) - } - - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/json") - cli.LogRequest(req, logBody) - res, err := cli.Client.Do(req) - if res != nil { - defer res.Body.Close() - } - if err != nil { - return nil, err - } - contents, err := ioutil.ReadAll(res.Body) - if res.StatusCode/100 != 2 { // not 2xx - var wrap error - respErr := &RespError{} - if _ = json.Unmarshal(contents, respErr); respErr.ErrCode != "" { - wrap = respErr - } else { - respErr = nil - } - - // If we failed to decode as RespError, don't just drop the HTTP body, include it in the - // HTTP error instead (e.g proxy errors which return HTML). - msg := "Failed to " + method + " JSON to " + req.URL.Path - if wrap == nil { - msg = msg + ": " + string(contents) - } - - return contents, HTTPError{ - Code: res.StatusCode, - Message: msg, - WrappedError: wrap, - RespError: respErr, - } - } - if err != nil { - return nil, err - } - - if resBody != nil { - if err = json.Unmarshal(contents, &resBody); err != nil { - return nil, err - } - } - - return contents, nil -} - -// CreateFilter makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-user-userid-filter -func (cli *Client) CreateFilter(filter json.RawMessage) (resp *RespCreateFilter, err error) { - urlPath := cli.BuildURL("user", cli.UserID, "filter") - _, err = cli.MakeRequest("POST", urlPath, &filter, &resp) - return -} - -// SyncRequest makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-sync -func (cli *Client) SyncRequest(timeout int, since, filterID string, fullState bool, setPresence string) (resp *RespSync, err error) { - query := map[string]string{ - "timeout": strconv.Itoa(timeout), - } - if since != "" { - query["since"] = since - } - if filterID != "" { - query["filter"] = filterID - } - if setPresence != "" { - query["set_presence"] = setPresence - } - if fullState { - query["full_state"] = "true" - } - urlPath := cli.BuildURLWithQuery([]string{"sync"}, query) - _, err = cli.MakeRequest("GET", urlPath, nil, &resp) - return -} - -func (cli *Client) register(u string, req *ReqRegister) (resp *RespRegister, uiaResp *RespUserInteractive, err error) { - var bodyBytes []byte - bodyBytes, err = cli.MakeRequest("POST", u, req, nil) - if err != nil { - httpErr, ok := err.(HTTPError) - if !ok { // network error - return - } - if httpErr.Code == 401 { - // body should be RespUserInteractive, if it isn't, fail with the error - err = json.Unmarshal(bodyBytes, &uiaResp) - return - } - return - } - // body should be RespRegister - err = json.Unmarshal(bodyBytes, &resp) - return -} - -// Register makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register -// -// Registers with kind=user. For kind=guest, see RegisterGuest. -func (cli *Client) Register(req *ReqRegister) (*RespRegister, *RespUserInteractive, error) { - u := cli.BuildURL("register") - return cli.register(u, req) -} - -// RegisterGuest makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register -// with kind=guest. -// -// For kind=user, see Register. -func (cli *Client) RegisterGuest(req *ReqRegister) (*RespRegister, *RespUserInteractive, error) { - query := map[string]string{ - "kind": "guest", - } - u := cli.BuildURLWithQuery([]string{"register"}, query) - return cli.register(u, req) -} - -// RegisterDummy performs m.login.dummy registration according to https://matrix.org/docs/spec/client_server/r0.2.0.html#dummy-auth -// -// Only a username and password need to be provided on the ReqRegister struct. Most local/developer homeservers will allow registration -// this way. If the homeserver does not, an error is returned. -// -// This does not set credentials on the client instance. See SetCredentials() instead. -// -// res, err := cli.RegisterDummy(&gomatrix.ReqRegister{ -// Username: "alice", -// Password: "wonderland", -// }) -// if err != nil { -// panic(err) -// } -// token := res.AccessToken -func (cli *Client) RegisterDummy(req *ReqRegister) (*RespRegister, error) { - res, uia, err := cli.Register(req) - if err != nil && uia == nil { - return nil, err - } - if uia != nil && uia.HasSingleStageFlow("m.login.dummy") { - req.Auth = struct { - Type string `json:"type"` - Session string `json:"session,omitempty"` - }{"m.login.dummy", uia.Session} - res, _, err = cli.Register(req) - if err != nil { - return nil, err - } - } - if res == nil { - return nil, fmt.Errorf("registration failed: does this server support m.login.dummy? ") - } - return res, nil -} - -// Login a user to the homeserver according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-login -// This does not set credentials on this client instance. See SetCredentials() instead. -func (cli *Client) Login(req *ReqLogin) (resp *RespLogin, err error) { - urlPath := cli.BuildURL("login") - _, err = cli.MakeRequest("POST", urlPath, req, &resp) - return -} - -// Logout the current user. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-logout -// This does not clear the credentials from the client instance. See ClearCredentials() instead. -func (cli *Client) Logout() (resp *RespLogout, err error) { - urlPath := cli.BuildURL("logout") - _, err = cli.MakeRequest("POST", urlPath, nil, &resp) - return -} - -// Versions returns the list of supported Matrix versions on this homeserver. See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-versions -func (cli *Client) Versions() (resp *RespVersions, err error) { - urlPath := cli.BuildBaseURL("_matrix", "client", "versions") - _, err = cli.MakeRequest("GET", urlPath, nil, &resp) - return -} - -// JoinRoom joins the client to a room ID or alias. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-join-roomidoralias -// -// If serverName is specified, this will be added as a query param to instruct the homeserver to join via that server. If content is specified, it will -// be JSON encoded and used as the request body. -func (cli *Client) JoinRoom(roomIDorAlias, serverName string, content interface{}) (resp *RespJoinRoom, err error) { - var urlPath string - if serverName != "" { - urlPath = cli.BuildURLWithQuery([]string{"join", roomIDorAlias}, map[string]string{ - "server_name": serverName, - }) - } else { - urlPath = cli.BuildURL("join", roomIDorAlias) - } - _, err = cli.MakeRequest("POST", urlPath, content, &resp) - return -} - -// GetDisplayName returns the display name of the user from the specified MXID. See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname -func (cli *Client) GetDisplayName(mxid string) (resp *RespUserDisplayName, err error) { - urlPath := cli.BuildURL("profile", mxid, "displayname") - _, err = cli.MakeRequest("GET", urlPath, nil, &resp) - return -} - -// GetOwnDisplayName returns the user's display name. See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname -func (cli *Client) GetOwnDisplayName() (resp *RespUserDisplayName, err error) { - urlPath := cli.BuildURL("profile", cli.UserID, "displayname") - _, err = cli.MakeRequest("GET", urlPath, nil, &resp) - return -} - -// SetDisplayName sets the user's profile display name. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-profile-userid-displayname -func (cli *Client) SetDisplayName(displayName string) (err error) { - urlPath := cli.BuildURL("profile", cli.UserID, "displayname") - s := struct { - DisplayName string `json:"displayname"` - }{displayName} - _, err = cli.MakeRequest("PUT", urlPath, &s, nil) - return -} - -// GetAvatarURL gets the user's avatar URL. See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-avatar-url -func (cli *Client) GetAvatarURL() (url string, err error) { - urlPath := cli.BuildURL("profile", cli.UserID, "avatar_url") - s := struct { - AvatarURL string `json:"avatar_url"` - }{} - - _, err = cli.MakeRequest("GET", urlPath, nil, &s) - if err != nil { - return "", err - } - - return s.AvatarURL, nil -} - -// SetAvatarURL sets the user's avatar URL. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-profile-userid-avatar-url -func (cli *Client) SetAvatarURL(url string) (err error) { - urlPath := cli.BuildURL("profile", cli.UserID, "avatar_url") - s := struct { - AvatarURL string `json:"avatar_url"` - }{url} - _, err = cli.MakeRequest("PUT", urlPath, &s, nil) - if err != nil { - return err - } - - return nil -} - -// SendMessageEvent sends a message event into a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid -// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal. -func (cli *Client) SendMessageEvent(roomID string, eventType EventType, contentJSON interface{}) (resp *RespSendEvent, err error) { - txnID := txnID() - urlPath := cli.BuildURL("rooms", roomID, "send", eventType.String(), txnID) - _, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp) - 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) SendMassagedMessageEvent(roomID string, eventType EventType, contentJSON interface{}, ts int64) (resp *RespSendEvent, err error) { - txnID := txnID() - urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "send", eventType.String(), txnID}, map[string]string{ - "ts": strconv.FormatInt(ts, 10), - }) - _, 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 string, eventType EventType, stateKey string, contentJSON interface{}) (resp *RespSendEvent, err error) { - urlPath := cli.BuildURL("rooms", roomID, "state", eventType.String(), stateKey) - _, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp) - return -} - -// SendStateEvent sends a state event into a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-state-eventtype-statekey -// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal. -func (cli *Client) SendMassagedStateEvent(roomID string, eventType EventType, stateKey string, contentJSON interface{}, ts int64) (resp *RespSendEvent, err error) { - urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "state", eventType.String(), stateKey}, map[string]string{ - "ts": strconv.FormatInt(ts, 10), - }) - _, 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, EventMessage, Content{ - MsgType: MsgText, - Body: text, - }) -} - -// SendImage sends an m.room.message event into the given room with a msgtype of m.image -// See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-image -func (cli *Client) SendImage(roomID, body, url string) (*RespSendEvent, error) { - return cli.SendMessageEvent(roomID, EventMessage, Content{ - MsgType: MsgImage, - 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, EventMessage, Content{ - MsgType: MsgVideo, - 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, EventMessage, Content{ - MsgType: MsgNotice, - 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 -} - -// 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 -} - -func (cli *Client) SetPresence(status string) (err error) { - req := ReqPresence{Presence: status} - u := cli.BuildURL("presence", cli.UserID, "status") - _, err = cli.MakeRequest("PUT", u, req, nil) - 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 string, eventType EventType, stateKey string, outContent interface{}) (err error) { - u := cli.BuildURL("rooms", roomID, "state", eventType.String(), 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.Upload(res.Body, res.Header.Get("Content-Type"), res.ContentLength) -} - -func (cli *Client) Download(mxcURL string) (io.ReadCloser, error) { - if !strings.HasPrefix(mxcURL, "mxc://") { - return nil, errors.New("invalid Matrix content URL") - } - parts := strings.Split(mxcURL[len("mxc://"):], "/") - if len(parts) != 2 { - return nil, errors.New("invalid Matrix content URL") - } - u := cli.BuildBaseURL("_matrix/media/r0/download", parts[0], parts[1]) - resp, err := cli.Client.Get(u) - if err != nil { - return nil, err - } - return resp.Body, nil -} - -func (cli *Client) DownloadBytes(mxcURL string) ([]byte, error) { - resp, err := cli.Download(mxcURL) - if err != nil { - return nil, err - } - defer resp.Close() - return ioutil.ReadAll(resp) -} - -func (cli *Client) UploadBytes(data []byte, contentType string) (*RespMediaUpload, error) { - return cli.Upload(bytes.NewReader(data), contentType, int64(len(data))) -} - -// 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) Upload(content io.Reader, contentType string, contentLength int64) (*RespMediaUpload, error) { - req, err := http.NewRequest("POST", cli.BuildBaseURL("_matrix/media/r0/upload"), content) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", contentType) - req.ContentLength = contentLength - cli.LogRequest(req, fmt.Sprintf("%d bytes", contentLength)) - res, err := cli.Client.Do(req) - if res != nil { - defer res.Body.Close() - } - if err != nil { - return nil, err - } - if res.StatusCode != 200 { - contents, err := ioutil.ReadAll(res.Body) - if err != nil { - return nil, HTTPError{ - Message: "Upload request failed - Failed to read response body: " + err.Error(), - Code: res.StatusCode, - } - } - return nil, HTTPError{ - Message: "Upload request failed: " + string(contents), - Code: res.StatusCode, - } - } - var m RespMediaUpload - if err := json.NewDecoder(res.Body).Decode(&m); err != nil { - return nil, err - } - return &m, nil -} - -// JoinedMembers returns a map of joined room members. See TODO-SPEC. https://github.com/matrix-org/synapse/pull/1680 -// -// In general, usage of this API is discouraged in favour of /sync, as calling this API can race with incoming membership changes. -// This API is primarily designed for application services which may want to efficiently look up joined members in a room. -func (cli *Client) JoinedMembers(roomID string) (resp *RespJoinedMembers, err error) { - u := cli.BuildURL("rooms", roomID, "joined_members") - _, err = cli.MakeRequest("GET", u, nil, &resp) - return -} - -// JoinedRooms returns a list of rooms which the client is joined to. See TODO-SPEC. https://github.com/matrix-org/synapse/pull/1680 -// -// In general, usage of this API is discouraged in favour of /sync, as calling this API can race with incoming membership changes. -// This API is primarily designed for application services which may want to efficiently look up joined rooms. -func (cli *Client) JoinedRooms() (resp *RespJoinedRooms, err error) { - u := cli.BuildURL("joined_rooms") - _, err = cli.MakeRequest("GET", u, nil, &resp) - return -} - -// Messages returns a list of message and state events for a room. It uses -// pagination query parameters to paginate history in the room. -// See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-messages -func (cli *Client) Messages(roomID, from, to string, dir rune, limit int) (resp *RespMessages, err error) { - query := map[string]string{ - "from": from, - "dir": string(dir), - } - if to != "" { - query["to"] = to - } - if limit != 0 { - query["limit"] = strconv.Itoa(limit) - } - - urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "messages"}, query) - _, err = cli.MakeRequest("GET", urlPath, nil, &resp) - return -} - -func (cli *Client) GetEvent(roomID, eventID string) (resp *Event, err error) { - urlPath := cli.BuildURL("rooms", roomID, "event", eventID) - _, err = cli.MakeRequest("GET", urlPath, nil, &resp) - return -} - -func (cli *Client) MarkRead(roomID, eventID string) (err error) { - urlPath := cli.BuildURL("rooms", roomID, "receipt", "m.read", eventID) - _, err = cli.MakeRequest("POST", urlPath, struct{}{}, nil) - 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/maunium.net/go/gomatrix/events.go b/vendor/maunium.net/go/gomatrix/events.go deleted file mode 100644 index 30166cd..0000000 --- a/vendor/maunium.net/go/gomatrix/events.go +++ /dev/null @@ -1,413 +0,0 @@ -package gomatrix - -import ( - "encoding/json" - "strings" - "sync" -) - -type EventTypeClass int - -const ( - // Normal message events - MessageEventType EventTypeClass = iota - // State events - StateEventType - // Ephemeral events - EphemeralEventType - // Account data events - AccountDataEventType - // Unknown events - UnknownEventType -) - -type EventType struct { - Type string - Class EventTypeClass -} - -func NewEventType(name string) EventType { - evtType := EventType{Type: name} - evtType.Class = evtType.GuessClass() - return evtType -} - -func (et *EventType) IsState() bool { - return et.Class == StateEventType -} - -func (et *EventType) IsEphemeral() bool { - return et.Class == EphemeralEventType -} - -func (et *EventType) IsCustom() bool { - return !strings.HasPrefix(et.Type, "m.") -} - -func (et *EventType) GuessClass() EventTypeClass { - switch et.Type { - case StateAliases.Type, StateCanonicalAlias.Type, StateCreate.Type, StateJoinRules.Type, StateMember.Type, - StatePowerLevels.Type, StateRoomName.Type, StateRoomAvatar.Type, StateTopic.Type, StatePinnedEvents.Type: - return StateEventType - case EphemeralEventReceipt.Type, EphemeralEventTyping.Type: - return EphemeralEventType - case AccountDataDirectChats.Type, AccountDataPushRules.Type, AccountDataRoomTags.Type: - return AccountDataEventType - case EventRedaction.Type, EventMessage.Type, EventSticker.Type: - return MessageEventType - default: - return UnknownEventType - } -} - -func (et *EventType) UnmarshalJSON(data []byte) error { - err := json.Unmarshal(data, &et.Type) - if err != nil { - return err - } - et.Class = et.GuessClass() - return nil -} - -func (et *EventType) MarshalJSON() ([]byte, error) { - return json.Marshal(&et.Type) -} - -func (et *EventType) String() string { - return et.Type -} - -// State events -var ( - StateAliases = EventType{"m.room.aliases", StateEventType} - StateCanonicalAlias = EventType{"m.room.canonical_alias", StateEventType} - StateCreate = EventType{"m.room.create", StateEventType} - StateJoinRules = EventType{"m.room.join_rules", StateEventType} - StateMember = EventType{"m.room.member", StateEventType} - StatePowerLevels = EventType{"m.room.power_levels", StateEventType} - StateRoomName = EventType{"m.room.name", StateEventType} - StateTopic = EventType{"m.room.topic", StateEventType} - StateRoomAvatar = EventType{"m.room.avatar", StateEventType} - StatePinnedEvents = EventType{"m.room.pinned_events", StateEventType} -) - -// Message events -var ( - EventRedaction = EventType{"m.room.redaction", MessageEventType} - EventMessage = EventType{"m.room.message", MessageEventType} - EventSticker = EventType{"m.sticker", MessageEventType} -) - -// Ephemeral events -var ( - EphemeralEventReceipt = EventType{"m.receipt", EphemeralEventType} - EphemeralEventTyping = EventType{"m.receipt", EphemeralEventType} -) - -// Account data events -var ( - AccountDataDirectChats = EventType{"m.direct", AccountDataEventType} - AccountDataPushRules = EventType{"m.push_rules", AccountDataEventType} - AccountDataRoomTags = EventType{"m.tag", AccountDataEventType} -) - -type MessageType string - -// Msgtypes -const ( - MsgText MessageType = "m.text" - MsgEmote = "m.emote" - MsgNotice = "m.notice" - MsgImage = "m.image" - MsgLocation = "m.location" - MsgVideo = "m.video" - MsgAudio = "m.audio" - MsgFile = "m.file" -) - -type Format string - -// Message formats -const ( - FormatHTML Format = "org.matrix.custom.html" -) - -// Event represents a single Matrix event. -type Event struct { - StateKey *string `json:"state_key,omitempty"` // The state key for the event. Only present on State Events. - Sender string `json:"sender"` // The user ID of the sender of the event - Type EventType `json:"type"` // The event type - Timestamp int64 `json:"origin_server_ts"` // The unix timestamp when this message was sent by the origin server - ID string `json:"event_id"` // The unique ID of this event - RoomID string `json:"room_id"` // The room the event was sent to. May be nil (e.g. for presence) - Content Content `json:"content"` // The JSON content of the event. - Redacts string `json:"redacts,omitempty"` // The event ID that was redacted if a m.room.redaction event - Unsigned Unsigned `json:"unsigned,omitempty"` // Unsigned content set by own homeserver. - - InviteRoomState []StrippedState `json:"invite_room_state"` -} - -func (evt *Event) GetStateKey() string { - if evt.StateKey != nil { - return *evt.StateKey - } - return "" -} - -type StrippedState struct { - Content Content `json:"content"` - Type EventType `json:"type"` - StateKey string `json:"state_key"` -} - -type Unsigned struct { - PrevContent *Content `json:"prev_content,omitempty"` - PrevSender string `json:"prev_sender,omitempty"` - ReplacesState string `json:"replaces_state,omitempty"` - Age int64 `json:"age,omitempty"` -} - -type Content struct { - VeryRaw json.RawMessage `json:"-"` - Raw map[string]interface{} `json:"-"` - - MsgType MessageType `json:"msgtype,omitempty"` - Body string `json:"body,omitempty"` - Format Format `json:"format,omitempty"` - FormattedBody string `json:"formatted_body,omitempty"` - - Info *FileInfo `json:"info,omitempty"` - URL string `json:"url,omitempty"` - - // Membership key for easy access in m.room.member events - Membership Membership `json:"membership,omitempty"` - - RelatesTo *RelatesTo `json:"m.relates_to,omitempty"` - - PowerLevels - Member - Aliases []string `json:"aliases,omitempty"` - CanonicalAlias - RoomName - RoomTopic - - RoomTags Tags `json:"tags,omitempty"` - TypingUserIDs []string `json:"user_ids,omitempty"` -} - -type serializableContent Content - -func (content *Content) UnmarshalJSON(data []byte) error { - content.VeryRaw = data - if err := json.Unmarshal(data, &content.Raw); err != nil { - return err - } - return json.Unmarshal(data, (*serializableContent)(content)) -} - -func (content *Content) UnmarshalPowerLevels() (pl PowerLevels, err error) { - err = json.Unmarshal(content.VeryRaw, &pl) - return -} - -func (content *Content) UnmarshalMember() (m Member, err error) { - err = json.Unmarshal(content.VeryRaw, &m) - return -} - -func (content *Content) UnmarshalCanonicalAlias() (ca CanonicalAlias, err error) { - err = json.Unmarshal(content.VeryRaw, &ca) - return -} - -func (content *Content) GetInfo() *FileInfo { - if content.Info == nil { - content.Info = &FileInfo{} - } - return content.Info -} - -type Tags map[string]struct { - Order string `json:"order"` -} - -type RoomName struct { - Name string `json:"name,omitempty"` -} - -type RoomTopic struct { - Topic string `json:"topic,omitempty"` -} - -// Membership is an enum specifying the membership state of a room member. -type Membership string - -// 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" -) - -type Member struct { - Membership Membership `json:"membership,omitempty"` - AvatarURL string `json:"avatar_url,omitempty"` - Displayname string `json:"displayname,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"` - } -} - -type CanonicalAlias struct { - Alias string `json:"alias,omitempty"` -} - -type PowerLevels struct { - usersLock sync.RWMutex `json:"-"` - Users map[string]int `json:"users,omitempty"` - UsersDefault int `json:"users_default,omitempty"` - - eventsLock sync.RWMutex `json:"-"` - Events map[string]int `json:"events,omitempty"` - EventsDefault int `json:"events_default,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"` -} - -func (pl *PowerLevels) Invite() int { - if pl.InvitePtr != nil { - return *pl.InvitePtr - } - return 50 -} - -func (pl *PowerLevels) Kick() int { - if pl.KickPtr != nil { - return *pl.KickPtr - } - return 50 -} - -func (pl *PowerLevels) Ban() int { - if pl.BanPtr != nil { - return *pl.BanPtr - } - return 50 -} - -func (pl *PowerLevels) Redact() int { - if pl.RedactPtr != nil { - return *pl.RedactPtr - } - return 50 -} - -func (pl *PowerLevels) StateDefault() int { - if pl.StateDefaultPtr != nil { - return *pl.StateDefaultPtr - } - return 50 -} - -func (pl *PowerLevels) GetUserLevel(userID string) int { - pl.usersLock.RLock() - defer pl.usersLock.RUnlock() - level, ok := pl.Users[userID] - if !ok { - return pl.UsersDefault - } - return level -} - -func (pl *PowerLevels) SetUserLevel(userID string, level int) { - pl.usersLock.Lock() - defer pl.usersLock.Unlock() - if level == pl.UsersDefault { - delete(pl.Users, userID) - } else { - pl.Users[userID] = level - } -} - -func (pl *PowerLevels) EnsureUserLevel(userID string, level int) bool { - existingLevel := pl.GetUserLevel(userID) - if existingLevel != level { - pl.SetUserLevel(userID, level) - return true - } - return false -} - -func (pl *PowerLevels) GetEventLevel(eventType EventType) 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 *PowerLevels) SetEventLevel(eventType EventType, 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 *PowerLevels) EnsureEventLevel(eventType EventType, level int) bool { - existingLevel := pl.GetEventLevel(eventType) - if existingLevel != level { - pl.SetEventLevel(eventType, level) - return true - } - return false -} - -type FileInfo struct { - MimeType string `json:"mimetype,omitempty"` - ThumbnailInfo *FileInfo `json:"thumbnail_info,omitempty"` - ThumbnailURL string `json:"thumbnail_url,omitempty"` - Height int `json:"h,omitempty"` - Width int `json:"w,omitempty"` - Duration uint `json:"duration,omitempty"` - Size int `json:"size,omitempty"` -} - -func (fileInfo *FileInfo) GetThumbnailInfo() *FileInfo { - if fileInfo.ThumbnailInfo == nil { - fileInfo.ThumbnailInfo = &FileInfo{} - } - return fileInfo.ThumbnailInfo -} - -type RelatesTo struct { - InReplyTo InReplyTo `json:"m.in_reply_to,omitempty"` -} - -type InReplyTo struct { - EventID string `json:"event_id,omitempty"` - // Not required, just for future-proofing - RoomID string `json:"room_id,omitempty"` -} diff --git a/vendor/maunium.net/go/gomatrix/filter.go b/vendor/maunium.net/go/gomatrix/filter.go deleted file mode 100644 index 2a0c37f..0000000 --- a/vendor/maunium.net/go/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/maunium.net/go/gomatrix/reply.go b/vendor/maunium.net/go/gomatrix/reply.go deleted file mode 100644 index 6985421..0000000 --- a/vendor/maunium.net/go/gomatrix/reply.go +++ /dev/null @@ -1,96 +0,0 @@ -package gomatrix - -import ( - "fmt" - "regexp" - "strings" - - "golang.org/x/net/html" -) - -var HTMLReplyFallbackRegex = regexp.MustCompile(`^[\s\S]+?`) - -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 *Content) RemoveReplyFallback() { - if len(content.GetReplyTo()) > 0 { - if content.Format == FormatHTML { - content.FormattedBody = TrimReplyFallbackHTML(content.FormattedBody) - } - content.Body = TrimReplyFallbackText(content.Body) - } -} - -func (content *Content) GetReplyTo() string { - if content.RelatesTo != nil { - return content.RelatesTo.InReplyTo.EventID - } - return "" -} - -const ReplyFormat = `
-In reply to -%s -%s -
-` - -func (evt *Event) GenerateReplyFallbackHTML() string { - body := evt.Content.FormattedBody - if len(body) == 0 { - body = html.EscapeString(evt.Content.Body) - } - - senderDisplayName := evt.Sender - - return fmt.Sprintf(ReplyFormat, evt.RoomID, evt.ID, evt.Sender, senderDisplayName, body) -} - -func (evt *Event) GenerateReplyFallbackText() string { - body := evt.Content.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 *Content) SetReply(inReplyTo *Event) { - if content.RelatesTo == nil { - content.RelatesTo = &RelatesTo{} - } - content.RelatesTo.InReplyTo = InReplyTo{ - EventID: inReplyTo.ID, - RoomID: inReplyTo.RoomID, - } - - if content.MsgType == MsgText || content.MsgType == MsgNotice { - if len(content.FormattedBody) == 0 || content.Format != FormatHTML { - content.FormattedBody = html.EscapeString(content.Body) - content.Format = FormatHTML - } - content.FormattedBody = inReplyTo.GenerateReplyFallbackHTML() + content.FormattedBody - content.Body = inReplyTo.GenerateReplyFallbackText() + content.Body - } -} diff --git a/vendor/maunium.net/go/gomatrix/requests.go b/vendor/maunium.net/go/gomatrix/requests.go deleted file mode 100644 index d8e10a6..0000000 --- a/vendor/maunium.net/go/gomatrix/requests.go +++ /dev/null @@ -1,82 +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.2.0.html#post-matrix-client-r0-login -type ReqLogin struct { - Type string `json:"type"` - Password string `json:"password,omitempty"` - Medium string `json:"medium,omitempty"` - User string `json:"user,omitempty"` - Address string `json:"address,omitempty"` - Token string `json:"token,omitempty"` - DeviceID string `json:"device_id,omitempty"` - InitialDeviceDisplayName string `json:"initial_device_display_name,omitempty"` -} - -// ReqCreateRoom is the JSON request for https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom -type ReqCreateRoom struct { - Visibility string `json:"visibility,omitempty"` - RoomAliasName string `json:"room_alias_name,omitempty"` - Name string `json:"name,omitempty"` - Topic string `json:"topic,omitempty"` - Invite []string `json:"invite,omitempty"` - Invite3PID []ReqInvite3PID `json:"invite_3pid,omitempty"` - CreationContent map[string]interface{} `json:"creation_content,omitempty"` - InitialState []*Event `json:"initial_state,omitempty"` - Preset string `json:"preset,omitempty"` - IsDirect bool `json:"is_direct,omitempty"` -} - -// ReqRedact is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-redact-eventid-txnid -type ReqRedact struct { - Reason string `json:"reason,omitempty"` -} - -// ReqInvite3PID is the JSON request for https://matrix.org/docs/spec/client_server/r0.2.0.html#id57 -// It is also a JSON object used in https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom -type ReqInvite3PID struct { - IDServer string `json:"id_server"` - Medium string `json:"medium"` - Address string `json:"address"` -} - -// ReqInviteUser is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-invite -type ReqInviteUser struct { - UserID string `json:"user_id"` -} - -// ReqKickUser is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-kick -type ReqKickUser struct { - Reason string `json:"reason,omitempty"` - UserID string `json:"user_id"` -} - -// ReqBanUser is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-ban -type ReqBanUser struct { - Reason string `json:"reason,omitempty"` - UserID string `json:"user_id"` -} - -// ReqUnbanUser is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-unban -type ReqUnbanUser struct { - UserID string `json:"user_id"` -} - -// ReqTyping is the JSON request for https://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-typing-userid -type ReqTyping struct { - Typing bool `json:"typing"` - Timeout int64 `json:"timeout,omitempty"` -} - -type ReqPresence struct { - Presence string `json:"presence"` -} \ No newline at end of file diff --git a/vendor/maunium.net/go/gomatrix/responses.go b/vendor/maunium.net/go/gomatrix/responses.go deleted file mode 100644 index 9524d62..0000000 --- a/vendor/maunium.net/go/gomatrix/responses.go +++ /dev/null @@ -1,182 +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"` -} - -// RespJoinRoom is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-join -type RespJoinRoom struct { - RoomID string `json:"room_id"` -} - -// RespLeaveRoom is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-leave -type RespLeaveRoom struct{} - -// RespForgetRoom is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-forget -type RespForgetRoom struct{} - -// RespInviteUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-invite -type RespInviteUser struct{} - -// RespKickUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-kick -type RespKickUser struct{} - -// RespBanUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-ban -type RespBanUser struct{} - -// RespUnbanUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-unban -type RespUnbanUser struct{} - -// RespTyping is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-typing-userid -type RespTyping struct{} - -// RespJoinedRooms is the JSON response for TODO-SPEC https://github.com/matrix-org/synapse/pull/1680 -type RespJoinedRooms struct { - JoinedRooms []string `json:"joined_rooms"` -} - -// RespJoinedMembers is the JSON response for TODO-SPEC https://github.com/matrix-org/synapse/pull/1680 -type RespJoinedMembers struct { - Joined map[string]struct { - DisplayName *string `json:"display_name"` - AvatarURL *string `json:"avatar_url"` - } `json:"joined"` -} - -// RespMessages is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-messages -type RespMessages struct { - Start string `json:"start"` - Chunk []*Event `json:"chunk"` - End string `json:"end"` -} - -// RespSendEvent is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid -type RespSendEvent struct { - EventID string `json:"event_id"` -} - -// RespMediaUpload is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-media-r0-upload -type RespMediaUpload struct { - ContentURI string `json:"content_uri"` -} - -// RespUserInteractive is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#user-interactive-authentication-api -type RespUserInteractive struct { - Flows []struct { - Stages []string `json:"stages"` - } `json:"flows"` - Params map[string]interface{} `json:"params"` - Session string `json:"string"` - Completed []string `json:"completed"` - ErrCode string `json:"errcode"` - Error string `json:"error"` -} - -// HasSingleStageFlow returns true if there exists at least 1 Flow with a single stage of stageName. -func (r RespUserInteractive) HasSingleStageFlow(stageName string) bool { - for _, f := range r.Flows { - if len(f.Stages) == 1 && f.Stages[0] == stageName { - return true - } - } - return false -} - -// RespUserDisplayName is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname -type RespUserDisplayName struct { - DisplayName string `json:"displayname"` -} - -// RespRegister is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register -type RespRegister struct { - AccessToken string `json:"access_token"` - DeviceID string `json:"device_id"` - HomeServer string `json:"home_server"` - RefreshToken string `json:"refresh_token"` - UserID string `json:"user_id"` -} - -// RespLogin is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-login -type RespLogin struct { - AccessToken string `json:"access_token"` - DeviceID string `json:"device_id"` - HomeServer string `json:"home_server"` - UserID string `json:"user_id"` -} - -// RespLogout is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-logout -type RespLogout struct{} - -// RespCreateRoom is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom -type RespCreateRoom struct { - RoomID string `json:"room_id"` -} - -// RespSync is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-sync -type RespSync struct { - NextBatch string `json:"next_batch"` - AccountData struct { - Events []*Event `json:"events"` - } `json:"account_data"` - Presence struct { - Events []*Event `json:"events"` - } `json:"presence"` - Rooms struct { - Leave map[string]struct { - State struct { - Events []*Event `json:"events"` - } `json:"state"` - Timeline struct { - Events []*Event `json:"events"` - Limited bool `json:"limited"` - PrevBatch string `json:"prev_batch"` - } `json:"timeline"` - } `json:"leave"` - Join map[string]struct { - State struct { - Events []*Event `json:"events"` - } `json:"state"` - Timeline struct { - Events []*Event `json:"events"` - Limited bool `json:"limited"` - PrevBatch string `json:"prev_batch"` - } `json:"timeline"` - Ephemeral struct { - Events []*Event `json:"events"` - } `json:"ephemeral"` - AccountData struct { - Events []*Event `json:"events"` - } `json:"account_data"` - } `json:"join"` - Invite map[string]struct { - State struct { - Events []*Event `json:"events"` - } `json:"invite_state"` - } `json:"invite"` - } `json:"rooms"` -} - -type RespTurnServer struct { - Username string `json:"username"` - Password string `json:"password"` - TTL int `json:"ttl"` - URIs []string `json:"uris"` -} diff --git a/vendor/maunium.net/go/gomatrix/room.go b/vendor/maunium.net/go/gomatrix/room.go deleted file mode 100644 index 80a91d8..0000000 --- a/vendor/maunium.net/go/gomatrix/room.go +++ /dev/null @@ -1,44 +0,0 @@ -package gomatrix - -// Room represents a single Matrix room. -type Room struct { - ID string - State map[EventType]map[string]*Event -} - -// UpdateState updates the room's current state with the given Event. This will clobber events based -// on the type/state_key combination. -func (room Room) UpdateState(event *Event) { - _, exists := room.State[event.Type] - if !exists { - room.State[event.Type] = make(map[string]*Event) - } - room.State[event.Type][*event.StateKey] = event -} - -// GetStateEvent returns the state event for the given type/state_key combo, or nil. -func (room Room) GetStateEvent(eventType EventType, 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) Membership { - state := MembershipLeave - event := room.GetStateEvent(StateMember, userID) - if event != nil { - state = event.Content.Membership - } - 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[EventType]map[string]*Event), - } -} diff --git a/vendor/maunium.net/go/gomatrix/store.go b/vendor/maunium.net/go/gomatrix/store.go deleted file mode 100644 index 6dc687e..0000000 --- a/vendor/maunium.net/go/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/maunium.net/go/gomatrix/sync.go b/vendor/maunium.net/go/gomatrix/sync.go deleted file mode 100644 index 09170d7..0000000 --- a/vendor/maunium.net/go/gomatrix/sync.go +++ /dev/null @@ -1,159 +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[EventType][]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[EventType][]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 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 EventType, 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 == StateMember && e.GetStateKey() == s.UserID { - if e.Content.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 -} - -// 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/maunium.net/go/gomatrix/userids.go b/vendor/maunium.net/go/gomatrix/userids.go deleted file mode 100644 index 23e7807..0000000 --- a/vendor/maunium.net/go/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/maunium.net/go/maulogger/LICENSE b/vendor/maunium.net/go/maulogger/LICENSE deleted file mode 100644 index c9739fb..0000000 --- a/vendor/maunium.net/go/maulogger/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Tulir Asokan - -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/maulogger/README.md b/vendor/maunium.net/go/maulogger/README.md deleted file mode 100644 index 68fe253..0000000 --- a/vendor/maunium.net/go/maulogger/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# maulogger -A logger in Go. - -Docs: [godoc.org/maunium.net/go/maulogger](https://godoc.org/maunium.net/go/maulogger) - -Go get: `go get maunium.net/go/maulogger` diff --git a/vendor/maunium.net/go/maulogger/logger.go b/vendor/maunium.net/go/maulogger/logger.go deleted file mode 100644 index e887237..0000000 --- a/vendor/maunium.net/go/maulogger/logger.go +++ /dev/null @@ -1,219 +0,0 @@ -package maulog - -import ( - "bufio" - "fmt" - "os" - "time" -) - -// Level is the severity level of a log entry. -type Level struct { - Name string - Severity, Color int -} - -// LogWriter writes to the log with an optional prefix -type LogWriter struct { - Level Level - Prefix string -} - -func (lw LogWriter) Write(p []byte) (n int, err error) { - log(lw.Level, fmt.Sprint(lw.Prefix, string(p))) - return len(p), nil -} - -// GetColor gets the ANSI escape color code for the log level. -func (lvl Level) GetColor() []byte { - if lvl.Color < 0 { - return []byte("") - } - return []byte(fmt.Sprintf("\x1b[%dm", lvl.Color)) -} - -// GetReset gets the ANSI escape reset code. -func (lvl Level) GetReset() []byte { - if lvl.Color < 0 { - return []byte("") - } - return []byte("\x1b[0m") -} - -var ( - // Debug is the level for debug messages. - Debug = Level{Name: "DEBUG", Color: 36, Severity: 0} - // Info is the level for basic log messages. - Info = Level{Name: "INFO", Color: -1, Severity: 10} - // Warn is the level saying that something went wrong, but the program will continue operating mostly normally. - Warn = Level{Name: "WARN", Color: 33, Severity: 50} - // Error is the level saying that something went wrong and the program may not operate as expected, but will still continue. - Error = Level{Name: "ERROR", Color: 31, Severity: 100} - // Fatal is the level saying that something went wrong and the program will not operate normally. - Fatal = Level{Name: "FATAL", Color: 35, Severity: 9001} -) - -// PrintLevel tells the first severity level at which messages should be printed to stdout -var PrintLevel = 10 - -// PrintDebug means PrintLevel = 0, kept for backwards compatibility -var PrintDebug = false - -// FileTimeformat is the time format used in log file names. -var FileTimeformat = "2006-01-02" - -// FileformatArgs is an undocumented integer. -var FileformatArgs = 3 - -// Fileformat is the format used for log file names. -var Fileformat = func(now string, i int) string { return fmt.Sprintf("%[1]s-%02[2]d.log", now, i) } - -// Timeformat is the time format used in logging. -var Timeformat = "15:04:05 02.01.2006" - -var writer *bufio.Writer -var lines int - -// InitWithWriter initializes MauLogger with the given writer. -func InitWithWriter(w *bufio.Writer) { - writer = w -} - -// Init initializes MauLogger. -func Init() { - // Find the next file name. - now := time.Now().Format(FileTimeformat) - i := 1 - for ; ; i++ { - if _, err := os.Stat(Fileformat(now, i)); os.IsNotExist(err) { - break - } - if i == 99 { - i = 1 - break - } - } - // Open the file - file, err := os.OpenFile(Fileformat(now, i), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0700) - if err != nil { - panic(err) - } - if file == nil { - panic(os.ErrInvalid) - } - // Create a writer - writer = bufio.NewWriter(file) -} - -// Debugf formats and logs a debug message. -func Debugf(message string, args ...interface{}) { - logln(Debug, fmt.Sprintf(message, args...)) -} - -// Printf formats and logs a string in the Info log level. -func Printf(message string, args ...interface{}) { - Infof(message, args...) -} - -// Infof formats and logs a string in the Info log level. -func Infof(message string, args ...interface{}) { - logln(Info, fmt.Sprintf(message, args...)) -} - -// Warnf formats and logs a string in the Warn log level. -func Warnf(message string, args ...interface{}) { - logln(Warn, fmt.Sprintf(message, args...)) -} - -// Errorf formats and logs a string in the Error log level. -func Errorf(message string, args ...interface{}) { - logln(Error, fmt.Sprintf(message, args...)) -} - -// Fatalf formats and logs a string in the Fatal log level. -func Fatalf(message string, args ...interface{}) { - logln(Fatal, fmt.Sprintf(message, args...)) -} - -// Logf formats and logs a message in the given log level. -func Logf(level Level, message string, args ...interface{}) { - logln(level, fmt.Sprintf(message, args...)) -} - -// Debugln logs a debug message. -func Debugln(args ...interface{}) { - log(Debug, fmt.Sprintln(args...)) -} - -// Println logs a string in the Info log level. -func Println(args ...interface{}) { - Infoln(args...) -} - -// Infoln logs a string in the Info log level. -func Infoln(args ...interface{}) { - log(Info, fmt.Sprintln(args...)) -} - -// Warnln logs a string in the Warn log level. -func Warnln(args ...interface{}) { - log(Warn, fmt.Sprintln(args...)) -} - -// Errorln logs a string in the Error log level. -func Errorln(args ...interface{}) { - log(Error, fmt.Sprintln(args...)) -} - -// Fatalln logs a string in the Fatal log level. -func Fatalln(args ...interface{}) { - log(Fatal, fmt.Sprintln(args...)) -} - -// Logln logs a message in the given log level. -func Logln(level Level, args ...interface{}) { - log(level, fmt.Sprintln(args...)) -} - -func logln(level Level, message string) { - log(level, fmt.Sprintln(message)) -} - -func log(level Level, message string) { - // Prefix the message with the timestamp and log level. - msg := []byte(fmt.Sprintf("[%[1]s] [%[2]s] %[3]s", time.Now().Format(Timeformat), level.Name, message)) - - if writer != nil { - // Write it to the log file. - _, err := writer.Write(msg) - if err != nil { - panic(err) - } - lines++ - // Flush the file if needed - if lines == 5 { - lines = 0 - writer.Flush() - } - } - - // Print to stdout using correct color - if level.Severity >= PrintLevel || PrintDebug { - if level.Severity >= Error.Severity { - os.Stderr.Write(level.GetColor()) - os.Stderr.Write(msg) - os.Stderr.Write(level.GetReset()) - } else { - os.Stdout.Write(level.GetColor()) - os.Stdout.Write(msg) - os.Stdout.Write(level.GetReset()) - } - } -} - -// Shutdown cleans up the logger. -func Shutdown() { - if writer != nil { - writer.Flush() - } -} diff --git a/vendor/maunium.net/go/mautrix/.gitignore b/vendor/maunium.net/go/mautrix/.gitignore new file mode 100644 index 0000000..66f8fb5 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/.gitignore @@ -0,0 +1,2 @@ +.idea/ +.vscode/ diff --git a/vendor/maunium.net/go/mautrix/LICENSE b/vendor/maunium.net/go/mautrix/LICENSE new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/vendor/maunium.net/go/mautrix/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/maunium.net/go/mautrix/README.md b/vendor/maunium.net/go/mautrix/README.md new file mode 100644 index 0000000..ca135a6 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/README.md @@ -0,0 +1,4 @@ +# mautrix-go +[![GoDoc](https://godoc.org/maunium.net/go/mautrix?status.svg)](https://godoc.org/maunium.net/go/mautrix) + +A Golang Matrix framework. diff --git a/vendor/maunium.net/go/mautrix/client.go b/vendor/maunium.net/go/mautrix/client.go new file mode 100644 index 0000000..d908b62 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/client.go @@ -0,0 +1,796 @@ +// Package mautrix implements the Matrix Client-Server API. +// +// Specification can be found at http://matrix.org/docs/spec/client_server/r0.4.0.html +package mautrix + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "path" + "strconv" + "strings" + "sync" + "time" +) + +type Logger interface { + Debugfln(message string, args ...interface{}) +} + +// Client represents a Matrix client. +type Client struct { + HomeserverURL *url.URL // The base homeserver URL + Prefix string // The API prefix eg '/_matrix/client/r0' + UserID string // The user ID of the client. Used for forming HTTP paths which use the client's user ID. + AccessToken string // The access_token for the client. + 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 + Logger Logger + + // The ?user_id= query parameter for application services. This must be set *prior* to calling a method. If this is empty, + // no user_id parameter will be sent. + // See http://matrix.org/docs/spec/application_service/unstable.html#identity-assertion + AppServiceUserID string + + syncingMutex sync.Mutex // protects syncingID + syncingID uint32 // Identifies the current Sync. Only one Sync can be active at any given time. +} + +// HTTPError An HTTP Error response, which may wrap an underlying native Go Error. +type HTTPError struct { + WrappedError error + RespError *RespError + Message string + Code int +} + +func (e HTTPError) Error() string { + var wrappedErrMsg string + if e.WrappedError != nil { + wrappedErrMsg = e.WrappedError.Error() + } + return fmt.Sprintf("msg=%s code=%d wrapped=%s", e.Message, e.Code, wrappedErrMsg) +} + +// BuildURL builds a URL with the Client's homserver/prefix/access_token set already. +func (cli *Client) BuildURL(urlPath ...string) string { + ps := []string{cli.Prefix} + for _, p := range urlPath { + ps = append(ps, p) + } + return cli.BuildBaseURL(ps...) +} + +// BuildBaseURL builds a URL with the Client's homeserver/access_token set already. You must +// supply the prefix in the path. +func (cli *Client) BuildBaseURL(urlPath ...string) string { + // copy the URL. Purposefully ignore error as the input is from a valid URL already + hsURL, _ := url.Parse(cli.HomeserverURL.String()) + parts := []string{hsURL.Path} + parts = append(parts, urlPath...) + hsURL.Path = path.Join(parts...) + query := hsURL.Query() + if cli.AccessToken != "" { + query.Set("access_token", cli.AccessToken) + } + if cli.AppServiceUserID != "" { + query.Set("user_id", cli.AppServiceUserID) + } + hsURL.RawQuery = query.Encode() + return hsURL.String() +} + +// BuildURLWithQuery builds a URL with query parameters in addition to the Client's homeserver/prefix/access_token set already. +func (cli *Client) BuildURLWithQuery(urlPath []string, urlQuery map[string]string) string { + u, _ := url.Parse(cli.BuildURL(urlPath...)) + q := u.Query() + for k, v := range urlQuery { + q.Set(k, v) + } + u.RawQuery = q.Encode() + return u.String() +} + +// SetCredentials sets the user ID and access token on this client instance. +func (cli *Client) SetCredentials(userID, accessToken string) { + cli.AccessToken = accessToken + cli.UserID = userID +} + +// ClearCredentials removes the user ID and access token on this client instance. +func (cli *Client) ClearCredentials() { + cli.AccessToken = "" + cli.UserID = "" +} + +// Sync starts syncing with the provided Homeserver. If Sync() is called twice then the first sync will be stopped and the +// error will be nil. +// +// This function will block until a fatal /sync error occurs, so it should almost always be started as a new goroutine. +// Fatal sync errors can be caused by: +// - The failure to create a filter. +// - Client.Syncer.OnFailedSync returning an error in response to a failed sync. +// - Client.Syncer.ProcessResponse returning an error. +// If you wish to continue retrying in spite of these fatal errors, call Sync() again. +func (cli *Client) Sync() error { + // Mark the client as syncing. + // We will keep syncing until the syncing state changes. Either because + // Sync is called or StopSync is called. + syncingID := cli.incrementSyncingID() + nextBatch := cli.Store.LoadNextBatch(cli.UserID) + filterID := cli.Store.LoadFilterID(cli.UserID) + if filterID == "" { + filterJSON := cli.Syncer.GetFilterJSON(cli.UserID) + resFilter, err := cli.CreateFilter(filterJSON) + if err != nil { + return err + } + filterID = resFilter.FilterID + cli.Store.SaveFilterID(cli.UserID, filterID) + } + for { + resSync, err := cli.SyncRequest(30000, nextBatch, filterID, false, "") + if err != nil { + duration, err2 := cli.Syncer.OnFailedSync(resSync, err) + if err2 != nil { + return err2 + } + time.Sleep(duration) + continue + } + + // Check that the syncing state hasn't changed + // Either because we've stopped syncing or another sync has been started. + // We discard the response from our sync. + if cli.getSyncingID() != syncingID { + return nil + } + + // Save the token now *before* processing it. This means it's possible + // to not process some events, but it means that we won't get constantly stuck processing + // a malformed/buggy event which keeps making us panic. + cli.Store.SaveNextBatch(cli.UserID, resSync.NextBatch) + if err = cli.Syncer.ProcessResponse(resSync, nextBatch); err != nil { + return err + } + + nextBatch = resSync.NextBatch + } +} + +func (cli *Client) incrementSyncingID() uint32 { + cli.syncingMutex.Lock() + defer cli.syncingMutex.Unlock() + cli.syncingID++ + return cli.syncingID +} + +func (cli *Client) getSyncingID() uint32 { + cli.syncingMutex.Lock() + defer cli.syncingMutex.Unlock() + return cli.syncingID +} + +// StopSync stops the ongoing sync started by Sync. +func (cli *Client) StopSync() { + // Advance the syncing state so that any running Syncs will terminate. + cli.incrementSyncingID() +} + +func (cli *Client) LogRequest(req *http.Request, body string) { + if cli.Logger == nil { + return + } + + cli.Logger.Debugfln("%s %s %s", req.Method, req.URL.Path, body) +} + +// MakeRequest makes a JSON HTTP request to the given URL. +// If "resBody" is not nil, the response body will be json.Unmarshalled into it. +// +// Returns the HTTP body as bytes on 2xx with a nil error. Returns an error if the response is not 2xx along +// with the HTTP body bytes if it got that far. This error is an HTTPError which includes the returned +// HTTP status code and possibly a RespError as the WrappedError, if the HTTP body could be decoded as a RespError. +func (cli *Client) MakeRequest(method string, httpURL string, reqBody interface{}, resBody interface{}) ([]byte, error) { + var req *http.Request + var err error + logBody := "{}" + if reqBody != nil { + var jsonStr []byte + jsonStr, err = json.Marshal(reqBody) + if err != nil { + return nil, err + } + logBody = string(jsonStr) + req, err = http.NewRequest(method, httpURL, bytes.NewBuffer(jsonStr)) + } else { + req, err = http.NewRequest(method, httpURL, nil) + } + + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + cli.LogRequest(req, logBody) + res, err := cli.Client.Do(req) + if res != nil { + defer res.Body.Close() + } + if err != nil { + return nil, err + } + contents, err := ioutil.ReadAll(res.Body) + if res.StatusCode/100 != 2 { // not 2xx + var wrap error + respErr := &RespError{} + if _ = json.Unmarshal(contents, respErr); respErr.ErrCode != "" { + wrap = respErr + } else { + respErr = nil + } + + // If we failed to decode as RespError, don't just drop the HTTP body, include it in the + // HTTP error instead (e.g proxy errors which return HTML). + msg := "Failed to " + method + " JSON to " + req.URL.Path + if wrap == nil { + msg = msg + ": " + string(contents) + } + + return contents, HTTPError{ + Code: res.StatusCode, + Message: msg, + WrappedError: wrap, + RespError: respErr, + } + } + if err != nil { + return nil, err + } + + if resBody != nil { + if err = json.Unmarshal(contents, &resBody); err != nil { + return nil, err + } + } + + return contents, nil +} + +// CreateFilter makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-user-userid-filter +func (cli *Client) CreateFilter(filter json.RawMessage) (resp *RespCreateFilter, err error) { + urlPath := cli.BuildURL("user", cli.UserID, "filter") + _, err = cli.MakeRequest("POST", urlPath, &filter, &resp) + return +} + +// SyncRequest makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-sync +func (cli *Client) SyncRequest(timeout int, since, filterID string, fullState bool, setPresence string) (resp *RespSync, err error) { + query := map[string]string{ + "timeout": strconv.Itoa(timeout), + } + if since != "" { + query["since"] = since + } + if filterID != "" { + query["filter"] = filterID + } + if setPresence != "" { + query["set_presence"] = setPresence + } + if fullState { + query["full_state"] = "true" + } + urlPath := cli.BuildURLWithQuery([]string{"sync"}, query) + _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + return +} + +func (cli *Client) register(u string, req *ReqRegister) (resp *RespRegister, uiaResp *RespUserInteractive, err error) { + var bodyBytes []byte + bodyBytes, err = cli.MakeRequest("POST", u, req, nil) + if err != nil { + httpErr, ok := err.(HTTPError) + if !ok { // network error + return + } + if httpErr.Code == 401 { + // body should be RespUserInteractive, if it isn't, fail with the error + err = json.Unmarshal(bodyBytes, &uiaResp) + return + } + return + } + // body should be RespRegister + err = json.Unmarshal(bodyBytes, &resp) + return +} + +// Register makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register +// +// Registers with kind=user. For kind=guest, see RegisterGuest. +func (cli *Client) Register(req *ReqRegister) (*RespRegister, *RespUserInteractive, error) { + u := cli.BuildURL("register") + return cli.register(u, req) +} + +// RegisterGuest makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register +// with kind=guest. +// +// For kind=user, see Register. +func (cli *Client) RegisterGuest(req *ReqRegister) (*RespRegister, *RespUserInteractive, error) { + query := map[string]string{ + "kind": "guest", + } + u := cli.BuildURLWithQuery([]string{"register"}, query) + return cli.register(u, req) +} + +// RegisterDummy performs m.login.dummy registration according to https://matrix.org/docs/spec/client_server/r0.2.0.html#dummy-auth +// +// Only a username and password need to be provided on the ReqRegister struct. Most local/developer homeservers will allow registration +// this way. If the homeserver does not, an error is returned. +// +// This does not set credentials on the client instance. See SetCredentials() instead. +// +// res, err := cli.RegisterDummy(&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 + } + if uia != nil && uia.HasSingleStageFlow("m.login.dummy") { + req.Auth = struct { + Type string `json:"type"` + Session string `json:"session,omitempty"` + }{"m.login.dummy", uia.Session} + res, _, err = cli.Register(req) + if err != nil { + return nil, err + } + } + if res == nil { + return nil, fmt.Errorf("registration failed: does this server support m.login.dummy? ") + } + return res, nil +} + +// Login a user to the homeserver according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-login +// This does not set credentials on this client instance. See SetCredentials() instead. +func (cli *Client) Login(req *ReqLogin) (resp *RespLogin, err error) { + urlPath := cli.BuildURL("login") + _, err = cli.MakeRequest("POST", urlPath, req, &resp) + return +} + +// Logout the current user. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-logout +// This does not clear the credentials from the client instance. See ClearCredentials() instead. +func (cli *Client) Logout() (resp *RespLogout, err error) { + urlPath := cli.BuildURL("logout") + _, err = cli.MakeRequest("POST", urlPath, nil, &resp) + return +} + +// Versions returns the list of supported Matrix versions on this homeserver. See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-versions +func (cli *Client) Versions() (resp *RespVersions, err error) { + urlPath := cli.BuildBaseURL("_matrix", "client", "versions") + _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + return +} + +// JoinRoom joins the client to a room ID or alias. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-join-roomidoralias +// +// If serverName is specified, this will be added as a query param to instruct the homeserver to join via that server. If content is specified, it will +// be JSON encoded and used as the request body. +func (cli *Client) JoinRoom(roomIDorAlias, serverName string, content interface{}) (resp *RespJoinRoom, err error) { + var urlPath string + if serverName != "" { + urlPath = cli.BuildURLWithQuery([]string{"join", roomIDorAlias}, map[string]string{ + "server_name": serverName, + }) + } else { + urlPath = cli.BuildURL("join", roomIDorAlias) + } + _, err = cli.MakeRequest("POST", urlPath, content, &resp) + return +} + +// GetDisplayName returns the display name of the user from the specified MXID. See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname +func (cli *Client) GetDisplayName(mxid string) (resp *RespUserDisplayName, err error) { + urlPath := cli.BuildURL("profile", mxid, "displayname") + _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + return +} + +// GetOwnDisplayName returns the user's display name. See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname +func (cli *Client) GetOwnDisplayName() (resp *RespUserDisplayName, err error) { + urlPath := cli.BuildURL("profile", cli.UserID, "displayname") + _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + return +} + +// SetDisplayName sets the user's profile display name. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-profile-userid-displayname +func (cli *Client) SetDisplayName(displayName string) (err error) { + urlPath := cli.BuildURL("profile", cli.UserID, "displayname") + s := struct { + DisplayName string `json:"displayname"` + }{displayName} + _, err = cli.MakeRequest("PUT", urlPath, &s, nil) + return +} + +// GetAvatarURL gets the user's avatar URL. See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-avatar-url +func (cli *Client) GetAvatarURL() (url string, err error) { + urlPath := cli.BuildURL("profile", cli.UserID, "avatar_url") + s := struct { + AvatarURL string `json:"avatar_url"` + }{} + + _, err = cli.MakeRequest("GET", urlPath, nil, &s) + if err != nil { + return "", err + } + + return s.AvatarURL, nil +} + +// SetAvatarURL sets the user's avatar URL. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-profile-userid-avatar-url +func (cli *Client) SetAvatarURL(url string) (err error) { + urlPath := cli.BuildURL("profile", cli.UserID, "avatar_url") + s := struct { + AvatarURL string `json:"avatar_url"` + }{url} + _, err = cli.MakeRequest("PUT", urlPath, &s, nil) + if err != nil { + return err + } + + return nil +} + +// SendMessageEvent sends a message event into a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid +// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal. +func (cli *Client) SendMessageEvent(roomID string, eventType EventType, contentJSON interface{}) (resp *RespSendEvent, err error) { + txnID := txnID() + urlPath := cli.BuildURL("rooms", roomID, "send", eventType.String(), txnID) + _, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp) + 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) SendMassagedMessageEvent(roomID string, eventType EventType, contentJSON interface{}, ts int64) (resp *RespSendEvent, err error) { + txnID := txnID() + urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "send", eventType.String(), txnID}, map[string]string{ + "ts": strconv.FormatInt(ts, 10), + }) + _, 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 string, eventType EventType, stateKey string, contentJSON interface{}) (resp *RespSendEvent, err error) { + urlPath := cli.BuildURL("rooms", roomID, "state", eventType.String(), stateKey) + _, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp) + return +} + +// SendStateEvent sends a state event into a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-state-eventtype-statekey +// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal. +func (cli *Client) SendMassagedStateEvent(roomID string, eventType EventType, stateKey string, contentJSON interface{}, ts int64) (resp *RespSendEvent, err error) { + urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "state", eventType.String(), stateKey}, map[string]string{ + "ts": strconv.FormatInt(ts, 10), + }) + _, 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, EventMessage, Content{ + MsgType: MsgText, + Body: text, + }) +} + +// SendImage sends an m.room.message event into the given room with a msgtype of m.image +// See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-image +func (cli *Client) SendImage(roomID, body, url string) (*RespSendEvent, error) { + return cli.SendMessageEvent(roomID, EventMessage, Content{ + MsgType: MsgImage, + 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, EventMessage, Content{ + MsgType: MsgVideo, + 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, EventMessage, Content{ + MsgType: MsgNotice, + 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 +} + +// 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(&mautrix.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 +} + +func (cli *Client) SetPresence(status string) (err error) { + req := ReqPresence{Presence: status} + u := cli.BuildURL("presence", cli.UserID, "status") + _, err = cli.MakeRequest("PUT", u, req, nil) + 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 string, eventType EventType, stateKey string, outContent interface{}) (err error) { + u := cli.BuildURL("rooms", roomID, "state", eventType.String(), 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.Upload(res.Body, res.Header.Get("Content-Type"), res.ContentLength) +} + +func (cli *Client) Download(mxcURL string) (io.ReadCloser, error) { + if !strings.HasPrefix(mxcURL, "mxc://") { + return nil, errors.New("invalid Matrix content URL") + } + parts := strings.Split(mxcURL[len("mxc://"):], "/") + if len(parts) != 2 { + return nil, errors.New("invalid Matrix content URL") + } + u := cli.BuildBaseURL("_matrix/media/r0/download", parts[0], parts[1]) + resp, err := cli.Client.Get(u) + if err != nil { + return nil, err + } + return resp.Body, nil +} + +func (cli *Client) DownloadBytes(mxcURL string) ([]byte, error) { + resp, err := cli.Download(mxcURL) + if err != nil { + return nil, err + } + defer resp.Close() + return ioutil.ReadAll(resp) +} + +func (cli *Client) UploadBytes(data []byte, contentType string) (*RespMediaUpload, error) { + return cli.Upload(bytes.NewReader(data), contentType, int64(len(data))) +} + +// 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) Upload(content io.Reader, contentType string, contentLength int64) (*RespMediaUpload, error) { + req, err := http.NewRequest("POST", cli.BuildBaseURL("_matrix/media/r0/upload"), content) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", contentType) + req.ContentLength = contentLength + cli.LogRequest(req, fmt.Sprintf("%d bytes", contentLength)) + res, err := cli.Client.Do(req) + if res != nil { + defer res.Body.Close() + } + if err != nil { + return nil, err + } + if res.StatusCode != 200 { + contents, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, HTTPError{ + Message: "Upload request failed - Failed to read response body: " + err.Error(), + Code: res.StatusCode, + } + } + return nil, HTTPError{ + Message: "Upload request failed: " + string(contents), + Code: res.StatusCode, + } + } + var m RespMediaUpload + if err := json.NewDecoder(res.Body).Decode(&m); err != nil { + return nil, err + } + return &m, nil +} + +// JoinedMembers returns a map of joined room members. See TODO-SPEC. https://github.com/matrix-org/synapse/pull/1680 +// +// In general, usage of this API is discouraged in favour of /sync, as calling this API can race with incoming membership changes. +// This API is primarily designed for application services which may want to efficiently look up joined members in a room. +func (cli *Client) JoinedMembers(roomID string) (resp *RespJoinedMembers, err error) { + u := cli.BuildURL("rooms", roomID, "joined_members") + _, err = cli.MakeRequest("GET", u, nil, &resp) + return +} + +// JoinedRooms returns a list of rooms which the client is joined to. See TODO-SPEC. https://github.com/matrix-org/synapse/pull/1680 +// +// In general, usage of this API is discouraged in favour of /sync, as calling this API can race with incoming membership changes. +// This API is primarily designed for application services which may want to efficiently look up joined rooms. +func (cli *Client) JoinedRooms() (resp *RespJoinedRooms, err error) { + u := cli.BuildURL("joined_rooms") + _, err = cli.MakeRequest("GET", u, nil, &resp) + return +} + +// Messages returns a list of message and state events for a room. It uses +// pagination query parameters to paginate history in the room. +// See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-messages +func (cli *Client) Messages(roomID, from, to string, dir rune, limit int) (resp *RespMessages, err error) { + query := map[string]string{ + "from": from, + "dir": string(dir), + } + if to != "" { + query["to"] = to + } + if limit != 0 { + query["limit"] = strconv.Itoa(limit) + } + + urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "messages"}, query) + _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + return +} + +func (cli *Client) GetEvent(roomID, eventID string) (resp *Event, err error) { + urlPath := cli.BuildURL("rooms", roomID, "event", eventID) + _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + return +} + +func (cli *Client) MarkRead(roomID, eventID string) (err error) { + urlPath := cli.BuildURL("rooms", roomID, "receipt", "m.read", eventID) + _, err = cli.MakeRequest("POST", urlPath, struct{}{}, nil) + 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/maunium.net/go/mautrix/events.go b/vendor/maunium.net/go/mautrix/events.go new file mode 100644 index 0000000..c974bf9 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/events.go @@ -0,0 +1,448 @@ +// Copyright 2018 Tulir Asokan +package mautrix + +import ( + "encoding/json" + "strings" + "sync" +) + +type EventTypeClass int + +const ( + // Normal message events + MessageEventType EventTypeClass = iota + // State events + StateEventType + // Ephemeral events + EphemeralEventType + // Account data events + AccountDataEventType + // Unknown events + UnknownEventType +) + +type EventType struct { + Type string + Class EventTypeClass +} + +func NewEventType(name string) EventType { + evtType := EventType{Type: name} + evtType.Class = evtType.GuessClass() + return evtType +} + +func (et *EventType) IsState() bool { + return et.Class == StateEventType +} + +func (et *EventType) IsEphemeral() bool { + return et.Class == EphemeralEventType +} + +func (et *EventType) IsCustom() bool { + return !strings.HasPrefix(et.Type, "m.") +} + +func (et *EventType) GuessClass() EventTypeClass { + switch et.Type { + case StateAliases.Type, StateCanonicalAlias.Type, StateCreate.Type, StateJoinRules.Type, StateMember.Type, + StatePowerLevels.Type, StateRoomName.Type, StateRoomAvatar.Type, StateTopic.Type, StatePinnedEvents.Type: + return StateEventType + case EphemeralEventReceipt.Type, EphemeralEventTyping.Type: + return EphemeralEventType + case AccountDataDirectChats.Type, AccountDataPushRules.Type, AccountDataRoomTags.Type: + return AccountDataEventType + case EventRedaction.Type, EventMessage.Type, EventSticker.Type: + return MessageEventType + default: + return UnknownEventType + } +} + +func (et *EventType) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, &et.Type) + if err != nil { + return err + } + et.Class = et.GuessClass() + return nil +} + +func (et *EventType) MarshalJSON() ([]byte, error) { + return json.Marshal(&et.Type) +} + +func (et *EventType) String() string { + return et.Type +} + +// State events +var ( + StateAliases = EventType{"m.room.aliases", StateEventType} + StateCanonicalAlias = EventType{"m.room.canonical_alias", StateEventType} + StateCreate = EventType{"m.room.create", StateEventType} + StateJoinRules = EventType{"m.room.join_rules", StateEventType} + StateMember = EventType{"m.room.member", StateEventType} + StatePowerLevels = EventType{"m.room.power_levels", StateEventType} + StateRoomName = EventType{"m.room.name", StateEventType} + StateTopic = EventType{"m.room.topic", StateEventType} + StateRoomAvatar = EventType{"m.room.avatar", StateEventType} + StatePinnedEvents = EventType{"m.room.pinned_events", StateEventType} +) + +// Message events +var ( + EventRedaction = EventType{"m.room.redaction", MessageEventType} + EventMessage = EventType{"m.room.message", MessageEventType} + EventSticker = EventType{"m.sticker", MessageEventType} +) + +// Ephemeral events +var ( + EphemeralEventReceipt = EventType{"m.receipt", EphemeralEventType} + EphemeralEventTyping = EventType{"m.typing", EphemeralEventType} +) + +// Account data events +var ( + AccountDataDirectChats = EventType{"m.direct", AccountDataEventType} + AccountDataPushRules = EventType{"m.push_rules", AccountDataEventType} + AccountDataRoomTags = EventType{"m.tag", AccountDataEventType} +) + +type MessageType string + +// Msgtypes +const ( + MsgText MessageType = "m.text" + MsgEmote = "m.emote" + MsgNotice = "m.notice" + MsgImage = "m.image" + MsgLocation = "m.location" + MsgVideo = "m.video" + MsgAudio = "m.audio" + MsgFile = "m.file" +) + +type Format string + +// Message formats +const ( + FormatHTML Format = "org.matrix.custom.html" +) + +// Event represents a single Matrix event. +type Event struct { + StateKey *string `json:"state_key,omitempty"` // The state key for the event. Only present on State Events. + Sender string `json:"sender"` // The user ID of the sender of the event + Type EventType `json:"type"` // The event type + Timestamp int64 `json:"origin_server_ts"` // The unix timestamp when this message was sent by the origin server + ID string `json:"event_id"` // The unique ID of this event + RoomID string `json:"room_id"` // The room the event was sent to. May be nil (e.g. for presence) + Content Content `json:"content"` // The JSON content of the event. + Redacts string `json:"redacts,omitempty"` // The event ID that was redacted if a m.room.redaction event + Unsigned Unsigned `json:"unsigned,omitempty"` // Unsigned content set by own homeserver. + + InviteRoomState []StrippedState `json:"invite_room_state"` +} + +func (evt *Event) GetStateKey() string { + if evt.StateKey != nil { + return *evt.StateKey + } + return "" +} + +type StrippedState struct { + Content Content `json:"content"` + Type EventType `json:"type"` + StateKey string `json:"state_key"` +} + +type Unsigned struct { + PrevContent *Content `json:"prev_content,omitempty"` + PrevSender string `json:"prev_sender,omitempty"` + ReplacesState string `json:"replaces_state,omitempty"` + Age int64 `json:"age,omitempty"` + + PassiveCommand map[string]*MatchedPassiveCommand `json:"m.passive_command,omitempty"` +} + +type MatchedPassiveCommand struct { + // Matched string `json:"matched"` + // Value string `json:"value"` + Captured [][]string `json:"captured"` + + BackCompatCommand string `json:"command"` + BackCompatArguments map[string]string `json:"arguments"` +} + +type Content struct { + VeryRaw json.RawMessage `json:"-"` + Raw map[string]interface{} `json:"-"` + + MsgType MessageType `json:"msgtype,omitempty"` + Body string `json:"body,omitempty"` + Format Format `json:"format,omitempty"` + FormattedBody string `json:"formatted_body,omitempty"` + + Info *FileInfo `json:"info,omitempty"` + URL string `json:"url,omitempty"` + + // Membership key for easy access in m.room.member events + Membership Membership `json:"membership,omitempty"` + + RelatesTo *RelatesTo `json:"m.relates_to,omitempty"` + Command *MatchedCommand `json:"m.command,omitempty"` + + PowerLevels + Member + Aliases []string `json:"aliases,omitempty"` + CanonicalAlias + RoomName + RoomTopic + + RoomTags Tags `json:"tags,omitempty"` + TypingUserIDs []string `json:"user_ids,omitempty"` +} + +type serializableContent Content + +var DisableFancyEventParsing = false + +func (content *Content) UnmarshalJSON(data []byte) error { + content.VeryRaw = data + if err := json.Unmarshal(data, &content.Raw); err != nil || DisableFancyEventParsing { + return err + } + return json.Unmarshal(data, (*serializableContent)(content)) +} + +func (content *Content) GetCommand() *MatchedCommand { + if content.Command == nil { + content.Command = &MatchedCommand{} + } + return content.Command +} + +func (content *Content) GetRelatesTo() *RelatesTo { + if content.RelatesTo == nil { + content.RelatesTo = &RelatesTo{} + } + return content.RelatesTo +} + +func (content *Content) UnmarshalPowerLevels() (pl PowerLevels, err error) { + err = json.Unmarshal(content.VeryRaw, &pl) + return +} + +func (content *Content) UnmarshalMember() (m Member, err error) { + err = json.Unmarshal(content.VeryRaw, &m) + return +} + +func (content *Content) UnmarshalCanonicalAlias() (ca CanonicalAlias, err error) { + err = json.Unmarshal(content.VeryRaw, &ca) + return +} + +func (content *Content) GetInfo() *FileInfo { + if content.Info == nil { + content.Info = &FileInfo{} + } + return content.Info +} + +type Tags map[string]struct { + Order json.Number `json:"order"` +} + +type RoomName struct { + Name string `json:"name,omitempty"` +} + +type RoomTopic struct { + Topic string `json:"topic,omitempty"` +} + +// Membership is an enum specifying the membership state of a room member. +type Membership string + +// 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" +) + +type Member struct { + Membership Membership `json:"membership,omitempty"` + AvatarURL string `json:"avatar_url,omitempty"` + Displayname string `json:"displayname,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"` + } +} + +type CanonicalAlias struct { + Alias string `json:"alias,omitempty"` +} + +type PowerLevels struct { + usersLock sync.RWMutex `json:"-"` + Users map[string]int `json:"users,omitempty"` + UsersDefault int `json:"users_default,omitempty"` + + eventsLock sync.RWMutex `json:"-"` + Events map[string]int `json:"events,omitempty"` + EventsDefault int `json:"events_default,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"` +} + +func (pl *PowerLevels) Invite() int { + if pl.InvitePtr != nil { + return *pl.InvitePtr + } + return 50 +} + +func (pl *PowerLevels) Kick() int { + if pl.KickPtr != nil { + return *pl.KickPtr + } + return 50 +} + +func (pl *PowerLevels) Ban() int { + if pl.BanPtr != nil { + return *pl.BanPtr + } + return 50 +} + +func (pl *PowerLevels) Redact() int { + if pl.RedactPtr != nil { + return *pl.RedactPtr + } + return 50 +} + +func (pl *PowerLevels) StateDefault() int { + if pl.StateDefaultPtr != nil { + return *pl.StateDefaultPtr + } + return 50 +} + +func (pl *PowerLevels) GetUserLevel(userID string) int { + pl.usersLock.RLock() + defer pl.usersLock.RUnlock() + level, ok := pl.Users[userID] + if !ok { + return pl.UsersDefault + } + return level +} + +func (pl *PowerLevels) SetUserLevel(userID string, level int) { + pl.usersLock.Lock() + defer pl.usersLock.Unlock() + if level == pl.UsersDefault { + delete(pl.Users, userID) + } else { + pl.Users[userID] = level + } +} + +func (pl *PowerLevels) EnsureUserLevel(userID string, level int) bool { + existingLevel := pl.GetUserLevel(userID) + if existingLevel != level { + pl.SetUserLevel(userID, level) + return true + } + return false +} + +func (pl *PowerLevels) GetEventLevel(eventType EventType) 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 *PowerLevels) SetEventLevel(eventType EventType, 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 *PowerLevels) EnsureEventLevel(eventType EventType, level int) bool { + existingLevel := pl.GetEventLevel(eventType) + if existingLevel != level { + pl.SetEventLevel(eventType, level) + return true + } + return false +} + +type FileInfo struct { + MimeType string `json:"mimetype,omitempty"` + ThumbnailInfo *FileInfo `json:"thumbnail_info,omitempty"` + ThumbnailURL string `json:"thumbnail_url,omitempty"` + Height int `json:"h,omitempty"` + Width int `json:"w,omitempty"` + Duration uint `json:"duration,omitempty"` + Size int `json:"size,omitempty"` +} + +func (fileInfo *FileInfo) GetThumbnailInfo() *FileInfo { + if fileInfo.ThumbnailInfo == nil { + fileInfo.ThumbnailInfo = &FileInfo{} + } + return fileInfo.ThumbnailInfo +} + +type RelatesTo struct { + InReplyTo InReplyTo `json:"m.in_reply_to,omitempty"` +} + +type InReplyTo struct { + EventID string `json:"event_id,omitempty"` + // Not required, just for future-proofing + RoomID string `json:"room_id,omitempty"` +} + +type MatchedCommand struct { + Target string `json:"target"` + Matched string `json:"matched"` + Arguments map[string]string `json:"arguments"` +} diff --git a/vendor/maunium.net/go/mautrix/filter.go b/vendor/maunium.net/go/mautrix/filter.go new file mode 100644 index 0000000..41cab2d --- /dev/null +++ b/vendor/maunium.net/go/mautrix/filter.go @@ -0,0 +1,90 @@ +// Copyright 2017 Jan Christian Grünhage +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mautrix + +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/maunium.net/go/mautrix/reply.go b/vendor/maunium.net/go/mautrix/reply.go new file mode 100644 index 0000000..5e3af92 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/reply.go @@ -0,0 +1,97 @@ +// Copyright 2018 Tulir Asokan +package mautrix + +import ( + "fmt" + "regexp" + "strings" + + "golang.org/x/net/html" +) + +var HTMLReplyFallbackRegex = regexp.MustCompile(`^[\s\S]+?`) + +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 *Content) RemoveReplyFallback() { + if len(content.GetReplyTo()) > 0 { + if content.Format == FormatHTML { + content.FormattedBody = TrimReplyFallbackHTML(content.FormattedBody) + } + content.Body = TrimReplyFallbackText(content.Body) + } +} + +func (content *Content) GetReplyTo() string { + if content.RelatesTo != nil { + return content.RelatesTo.InReplyTo.EventID + } + return "" +} + +const ReplyFormat = `
+In reply to +%s +%s +
+` + +func (evt *Event) GenerateReplyFallbackHTML() string { + body := evt.Content.FormattedBody + if len(body) == 0 { + body = html.EscapeString(evt.Content.Body) + } + + senderDisplayName := evt.Sender + + return fmt.Sprintf(ReplyFormat, evt.RoomID, evt.ID, evt.Sender, senderDisplayName, body) +} + +func (evt *Event) GenerateReplyFallbackText() string { + body := evt.Content.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 *Content) SetReply(inReplyTo *Event) { + if content.RelatesTo == nil { + content.RelatesTo = &RelatesTo{} + } + content.RelatesTo.InReplyTo = InReplyTo{ + EventID: inReplyTo.ID, + RoomID: inReplyTo.RoomID, + } + + if content.MsgType == MsgText || content.MsgType == MsgNotice { + if len(content.FormattedBody) == 0 || content.Format != FormatHTML { + content.FormattedBody = html.EscapeString(content.Body) + content.Format = FormatHTML + } + content.FormattedBody = inReplyTo.GenerateReplyFallbackHTML() + content.FormattedBody + content.Body = inReplyTo.GenerateReplyFallbackText() + content.Body + } +} diff --git a/vendor/maunium.net/go/mautrix/requests.go b/vendor/maunium.net/go/mautrix/requests.go new file mode 100644 index 0000000..b90e6fb --- /dev/null +++ b/vendor/maunium.net/go/mautrix/requests.go @@ -0,0 +1,82 @@ +package mautrix + +// ReqRegister is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register +type ReqRegister struct { + Username string `json:"username,omitempty"` + BindEmail bool `json:"bind_email,omitempty"` + Password string `json:"password,omitempty"` + DeviceID string `json:"device_id,omitempty"` + InitialDeviceDisplayName string `json:"initial_device_display_name"` + Auth interface{} `json:"auth,omitempty"` +} + +// ReqLogin is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-login +type ReqLogin struct { + Type string `json:"type"` + Password string `json:"password,omitempty"` + Medium string `json:"medium,omitempty"` + User string `json:"user,omitempty"` + Address string `json:"address,omitempty"` + Token string `json:"token,omitempty"` + DeviceID string `json:"device_id,omitempty"` + InitialDeviceDisplayName string `json:"initial_device_display_name,omitempty"` +} + +// ReqCreateRoom is the JSON request for https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom +type ReqCreateRoom struct { + Visibility string `json:"visibility,omitempty"` + RoomAliasName string `json:"room_alias_name,omitempty"` + Name string `json:"name,omitempty"` + Topic string `json:"topic,omitempty"` + Invite []string `json:"invite,omitempty"` + Invite3PID []ReqInvite3PID `json:"invite_3pid,omitempty"` + CreationContent map[string]interface{} `json:"creation_content,omitempty"` + InitialState []*Event `json:"initial_state,omitempty"` + Preset string `json:"preset,omitempty"` + IsDirect bool `json:"is_direct,omitempty"` +} + +// ReqRedact is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-redact-eventid-txnid +type ReqRedact struct { + Reason string `json:"reason,omitempty"` +} + +// ReqInvite3PID is the JSON request for https://matrix.org/docs/spec/client_server/r0.2.0.html#id57 +// It is also a JSON object used in https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom +type ReqInvite3PID struct { + IDServer string `json:"id_server"` + Medium string `json:"medium"` + Address string `json:"address"` +} + +// ReqInviteUser is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-invite +type ReqInviteUser struct { + UserID string `json:"user_id"` +} + +// ReqKickUser is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-kick +type ReqKickUser struct { + Reason string `json:"reason,omitempty"` + UserID string `json:"user_id"` +} + +// ReqBanUser is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-ban +type ReqBanUser struct { + Reason string `json:"reason,omitempty"` + UserID string `json:"user_id"` +} + +// ReqUnbanUser is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-unban +type ReqUnbanUser struct { + UserID string `json:"user_id"` +} + +// ReqTyping is the JSON request for https://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-typing-userid +type ReqTyping struct { + Typing bool `json:"typing"` + Timeout int64 `json:"timeout,omitempty"` +} + +type ReqPresence struct { + Presence string `json:"presence"` +} diff --git a/vendor/maunium.net/go/mautrix/responses.go b/vendor/maunium.net/go/mautrix/responses.go new file mode 100644 index 0000000..2adf90a --- /dev/null +++ b/vendor/maunium.net/go/mautrix/responses.go @@ -0,0 +1,182 @@ +package mautrix + +// RespError is the standard JSON error response from Homeservers. It also implements the Golang "error" interface. +// See http://matrix.org/docs/spec/client_server/r0.2.0.html#api-standards +type RespError struct { + ErrCode string `json:"errcode"` + Err string `json:"error"` +} + +// Error returns the errcode and error message. +func (e RespError) Error() string { + return e.ErrCode + ": " + e.Err +} + +// RespCreateFilter is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-user-userid-filter +type RespCreateFilter struct { + FilterID string `json:"filter_id"` +} + +// RespVersions is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-versions +type RespVersions struct { + Versions []string `json:"versions"` +} + +// RespJoinRoom is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-join +type RespJoinRoom struct { + RoomID string `json:"room_id"` +} + +// RespLeaveRoom is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-leave +type RespLeaveRoom struct{} + +// RespForgetRoom is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-forget +type RespForgetRoom struct{} + +// RespInviteUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-invite +type RespInviteUser struct{} + +// RespKickUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-kick +type RespKickUser struct{} + +// RespBanUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-ban +type RespBanUser struct{} + +// RespUnbanUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-unban +type RespUnbanUser struct{} + +// RespTyping is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-typing-userid +type RespTyping struct{} + +// RespJoinedRooms is the JSON response for TODO-SPEC https://github.com/matrix-org/synapse/pull/1680 +type RespJoinedRooms struct { + JoinedRooms []string `json:"joined_rooms"` +} + +// RespJoinedMembers is the JSON response for TODO-SPEC https://github.com/matrix-org/synapse/pull/1680 +type RespJoinedMembers struct { + Joined map[string]struct { + DisplayName *string `json:"display_name"` + AvatarURL *string `json:"avatar_url"` + } `json:"joined"` +} + +// RespMessages is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-messages +type RespMessages struct { + Start string `json:"start"` + Chunk []*Event `json:"chunk"` + End string `json:"end"` +} + +// RespSendEvent is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid +type RespSendEvent struct { + EventID string `json:"event_id"` +} + +// RespMediaUpload is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-media-r0-upload +type RespMediaUpload struct { + ContentURI string `json:"content_uri"` +} + +// RespUserInteractive is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#user-interactive-authentication-api +type RespUserInteractive struct { + Flows []struct { + Stages []string `json:"stages"` + } `json:"flows"` + Params map[string]interface{} `json:"params"` + Session string `json:"string"` + Completed []string `json:"completed"` + ErrCode string `json:"errcode"` + Error string `json:"error"` +} + +// HasSingleStageFlow returns true if there exists at least 1 Flow with a single stage of stageName. +func (r RespUserInteractive) HasSingleStageFlow(stageName string) bool { + for _, f := range r.Flows { + if len(f.Stages) == 1 && f.Stages[0] == stageName { + return true + } + } + return false +} + +// RespUserDisplayName is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname +type RespUserDisplayName struct { + DisplayName string `json:"displayname"` +} + +// RespRegister is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register +type RespRegister struct { + AccessToken string `json:"access_token"` + DeviceID string `json:"device_id"` + HomeServer string `json:"home_server"` + RefreshToken string `json:"refresh_token"` + UserID string `json:"user_id"` +} + +// RespLogin is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-login +type RespLogin struct { + AccessToken string `json:"access_token"` + DeviceID string `json:"device_id"` + HomeServer string `json:"home_server"` + UserID string `json:"user_id"` +} + +// RespLogout is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-logout +type RespLogout struct{} + +// RespCreateRoom is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom +type RespCreateRoom struct { + RoomID string `json:"room_id"` +} + +// RespSync is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-sync +type RespSync struct { + NextBatch string `json:"next_batch"` + AccountData struct { + Events []*Event `json:"events"` + } `json:"account_data"` + Presence struct { + Events []*Event `json:"events"` + } `json:"presence"` + Rooms struct { + Leave map[string]struct { + State struct { + Events []*Event `json:"events"` + } `json:"state"` + Timeline struct { + Events []*Event `json:"events"` + Limited bool `json:"limited"` + PrevBatch string `json:"prev_batch"` + } `json:"timeline"` + } `json:"leave"` + Join map[string]struct { + State struct { + Events []*Event `json:"events"` + } `json:"state"` + Timeline struct { + Events []*Event `json:"events"` + Limited bool `json:"limited"` + PrevBatch string `json:"prev_batch"` + } `json:"timeline"` + Ephemeral struct { + Events []*Event `json:"events"` + } `json:"ephemeral"` + AccountData struct { + Events []*Event `json:"events"` + } `json:"account_data"` + } `json:"join"` + Invite map[string]struct { + State struct { + Events []*Event `json:"events"` + } `json:"invite_state"` + } `json:"invite"` + } `json:"rooms"` +} + +type RespTurnServer struct { + Username string `json:"username"` + Password string `json:"password"` + TTL int `json:"ttl"` + URIs []string `json:"uris"` +} diff --git a/vendor/maunium.net/go/mautrix/room.go b/vendor/maunium.net/go/mautrix/room.go new file mode 100644 index 0000000..086e259 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/room.go @@ -0,0 +1,44 @@ +package mautrix + +// Room represents a single Matrix room. +type Room struct { + ID string + State map[EventType]map[string]*Event +} + +// UpdateState updates the room's current state with the given Event. This will clobber events based +// on the type/state_key combination. +func (room Room) UpdateState(event *Event) { + _, exists := room.State[event.Type] + if !exists { + room.State[event.Type] = make(map[string]*Event) + } + room.State[event.Type][*event.StateKey] = event +} + +// GetStateEvent returns the state event for the given type/state_key combo, or nil. +func (room Room) GetStateEvent(eventType EventType, 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) Membership { + state := MembershipLeave + event := room.GetStateEvent(StateMember, userID) + if event != nil { + state = event.Content.Membership + } + 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[EventType]map[string]*Event), + } +} diff --git a/vendor/maunium.net/go/mautrix/store.go b/vendor/maunium.net/go/mautrix/store.go new file mode 100644 index 0000000..774398e --- /dev/null +++ b/vendor/maunium.net/go/mautrix/store.go @@ -0,0 +1,65 @@ +package mautrix + +// 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/maunium.net/go/mautrix/sync.go b/vendor/maunium.net/go/mautrix/sync.go new file mode 100644 index 0000000..9589edc --- /dev/null +++ b/vendor/maunium.net/go/mautrix/sync.go @@ -0,0 +1,159 @@ +package mautrix + +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[EventType][]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[EventType][]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 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 EventType, 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 == StateMember && e.GetStateKey() == s.UserID { + if e.Content.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 +} + +// 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/maunium.net/go/mautrix/userids.go b/vendor/maunium.net/go/mautrix/userids.go new file mode 100644 index 0000000..ce6e02d --- /dev/null +++ b/vendor/maunium.net/go/mautrix/userids.go @@ -0,0 +1,130 @@ +package mautrix + +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/maunium.net/go/tcell/README.adoc b/vendor/maunium.net/go/tcell/README.adoc new file mode 100644 index 0000000..3b60057 --- /dev/null +++ b/vendor/maunium.net/go/tcell/README.adoc @@ -0,0 +1,270 @@ += tcell + + +image:https://img.shields.io/travis/gdamore/tcell.svg?label=linux[Linux Status,link="https://travis-ci.org/gdamore/tcell"] +image:https://img.shields.io/appveyor/ci/gdamore/tcell.svg?label=windows[Windows Status,link="https://ci.appveyor.com/project/gdamore/tcell"] +image:https://img.shields.io/badge/license-APACHE2-blue.svg[Apache License,link="https://github.com/gdamore/tcell/blob/master/LICENSE"] +image:https://img.shields.io/badge/gitter-join-brightgreen.svg[Gitter,link="https://gitter.im/gdamore/tcell"] +image:https://img.shields.io/badge/godoc-reference-blue.svg[GoDoc,link="https://godoc.org/github.com/gdamore/tcell"] +image:http://goreportcard.com/badge/gdamore/tcell[Go Report Card,link="http://goreportcard.com/report/gdamore/tcell"] +image:https://codecov.io/gh/gdamore/tcell/branch/master/graph/badge.svg[codecov,link="https://codecov.io/gh/gdamore/tcell"] +image:https://tidelift.com/badges/github/gdamore/tcell?style=flat[Dependencies] + +[cols="2",grid="none"] +|=== +|_Tcell_ is a _Go_ package that provides a cell based view for text terminals, like _xterm_. +It was inspired by _termbox_, but includes many additional improvements. +a|[.right] +image::logos/tcell.png[float="right"] +|=== + +## Examples + +* https://github.com/gdamore/proxima5[proxima5] - space shooter (https://youtu.be/jNxKTCmY_bQ[video]) +* https://github.com/gdamore/govisor[govisor] - service management UI (http://2.bp.blogspot.com/--OsvnfzSNow/Vf7aqMw3zXI/AAAAAAAAARo/uOMtOvw4Sbg/s1600/Screen%2BShot%2B2015-09-20%2Bat%2B9.08.41%2BAM.png[screenshot]) +* mouse demo - included mouse test (http://2.bp.blogspot.com/-fWvW5opT0es/VhIdItdKqJI/AAAAAAAAATE/7Ojc0L1SpB0/s1600/Screen%2BShot%2B2015-10-04%2Bat%2B11.47.13%2BPM.png[screenshot]) +* https://github.com/gdamore/gomatrix[gomatrix] - converted from Termbox +* https://github.com/zyedidia/micro/[micro] - lightweight text editor with syntax-highlighting and themes +* https://github.com/viktomas/godu[godu] - simple golang utility helping to discover large files/folders. +* https://github.com/rivo/tview[tview] - rich interactive widgets for terminal UIs +* https://github.com/marcusolsson/tui-go[tui-go] - UI library for terminal apps +* https://github.com/rgm3/gomandelbrot[gomandelbrot] - Mandelbrot! +* https://github.com/senorprogrammer/wtf[WTF]- Personal information dashboard for your terminal +* https://github.com/browsh-org/browsh[browsh] - A fully-modern text-based browser, rendering to TTY and browsers (https://www.youtube.com/watch?v=HZq86XfBoRo[video]) +* https://github.com/sachaos/go-life[go-life] - Conway's Game of Life. + +## Pure Go Terminfo Database + +_Tcell_ includes a full parser and expander for terminfo capability strings, +so that it can avoid hard coding escape strings for formatting. It also favors +portability, and includes support for all POSIX systems. + +The database is also flexible & extensible, and can modified by either running +a program to build the entire database, or an entry for just a single terminal. + +## More Portable + +_Tcell_ is portable to a wide variety of systems. +_Tcell_ is believed +to work with all of the systems officially supported by golang with +the exception of nacl (which lacks any kind of a terminal interface). +(Plan9 is not supported by _Tcell_, but it is experimental status only +in golang.) For all of these systems *except Solaris/illumos*, _Tcell_ +is pure Go, with no need for CGO. + +## No Async IO + +_Tcell_ is able to operate without requiring `SIGIO` signals (unlike _termbox_), +or asynchronous I/O, and can instead use standard Go file +objects and Go routines. +This means it should be safe, especially for +use with programs that use exec, or otherwise need to manipulate the +tty streams. +This model is also much closer to idiomatic Go, leading +to fewer surprises. + +## Rich Unicode & non-Unicode support + +_Tcell_ includes enhanced support for Unicode, including wide characters and +combining characters, provided your terminal can support them. +Note that +Windows terminals generally don't support the full Unicode repertoire. + +It will also convert to and from Unicode locales, so that the program +can work with UTF-8 internally, and get reasonable output in other locales. +_Tcell_ tries hard to convert to native characters on both input and output, and +on output _Tcell_ even makes use of the alternate character set to facilitate +drawing certain characters. + +## More Function Keys + +_Tcell_ also has richer support for a larger number of special keys that some terminals can send. + +## Better Color Handling + +_Tcell_ will respect your terminal's color space as specified within your terminfo +entries, so that for example attempts to emit color sequences on VT100 terminals +won't result in unintended consequences. + +In Windows mode, _Tcell_ supports 16 colors, bold, dim, and reverse, +instead of just termbox's 8 colors with reverse. (Note that there is some +conflation with bold/dim and colors.) + +_Tcell_ maps 16 colors down to 8, for terminals that need it. +(The upper 8 colors are just brighter versions of the lower 8.) + +## Better Mouse Support + +_Tcell_ supports enhanced mouse tracking mode, so your application can receive +regular mouse motion events, and wheel events, if your terminal supports it. + +## _Termbox_ Compatibility + +A compatibility layer for _termbox_ is provided in the `compat` directory. +To use it, try importing `github.com/gdamore/tcell/termbox` +instead. Most _termbox-go_ programs will probably work without further +modification. + +## Working With Unicode + +Internally Tcell uses UTF-8, just like Go. +However, Tcell understands how to +convert to and from other character sets, using the capabilities of +the `golang.org/x/text/encoding packages`. +Your application must supply +them, as the full set of the most common ones bloats the program by about 2MB. +If you're lazy, and want them all anyway, see the `encoding` sub-directory. + +## Wide & Combining Characters + +The `SetContent()` API takes a primary rune, and an optional list of combining runes. +If any of the runes is a wide (East Asian) rune occupying two cells, +then the library will skip output from the following cell, but care must be +taken in the application to avoid explicitly attempting to set content in the +next cell, otherwise the results are undefined. (Normally wide character +is displayed, and the other character is not; do not depend on that behavior.) + +Experience has shown that the vanilla Windows 8 console application does not +support any of these characters properly, but at least some options like +_ConEmu_ do support Wide characters. + +## Colors + +_Tcell_ assumes the ANSI/XTerm color model, including the 256 color map that +XTerm uses when it supports 256 colors. The terminfo guidance will be +honored, with respect to the number of colors supported. Also, only +terminals which expose ANSI style `setaf` and `setab` will support color; +if you have a color terminal that only has `setf` and `setb`, please let me +know; it wouldn't be hard to add that if there is need. + +## 24-bit Color + +_Tcell_ _supports true color_! (That is, if your terminal can support it, +_Tcell_ can accurately display 24-bit color.) + +To use 24-bit color, you need to use a terminal that supports it. Modern +xterm and similar teminal emulators can support this. As terminfo lacks any +way to describe this capability, we fabricate the capability for +terminals with names ending in `*-truecolor`. The stock distribution ships +with a database that defines `xterm-truecolor`. +To try it out, set your +`TERM` variable to `xterm-truecolor`. + +When using TrueColor, programs will display the colors that the programmer +intended, overriding any "`themes`" you may have set in your terminal +emulator. (For some cases, accurate color fidelity is more important +than respecting themes. For other cases, such as typical text apps that +only use a few colors, its more desirable to respect the themes that +the user has established.) + +If you find this undesirable, you can either use a `TERM` variable +that lacks the `TRUECOLOR` setting, or set `TCELL_TRUECOLOR=disable` in your +environment. + +## Performance + +Reasonable attempts have been made to minimize sending data to terminals, +avoiding repeated sequences or drawing the same cell on refresh updates. + +## Terminfo + +(Not relevent for Windows users.) + +The Terminfo implementation operates with two forms of database. The first +is the built-in go database, which contains a number of real database entries +that are compiled into the program directly. This should minimize calling +out to database file searches. + +The second is in the form of JSON files, that contain the same information, +which can be located either by the `$TCELLDB` environment file, `$HOME/.tcelldb`, +or is located in the Go source directory as `database.json`. + +These files (both the Go and the JSON files) can be generated using the +mkinfo.go program. If you need to regnerate the entire set for some reason, +run the mkdatabase.sh file. The generation uses the infocmp(1) program on +the system to collect the necessary information. + +The `mkinfo.go` program can also be used to generate specific database entries +for named terminals, in case your favorite terminal is missing. (If you +find that this is the case, please let me know and I'll try to add it!) + +_Tcell_ requires that the terminal support the `cup` mode of cursor addressing. +Terminals without absolute cursor addressability are not supported. +This is unlikely to be a problem; such terminals have not been mass produced +since the early 1970s. + +## Mouse Support + +Mouse support is detected via the `kmous` terminfo variable, however, +enablement/disablement and decoding mouse events is done using hard coded +sequences based on the XTerm X11 model. As of this writing all popular +terminals with mouse tracking support this model. (Full terminfo support +is not possible as terminfo sequences are not defined.) + +On Windows, the mouse works normally. + +Mouse wheel buttons on various terminals are known to work, but the support +in terminal emulators, as well as support for various buttons and +live mouse tracking, varies widely. Modern _xterm_, macOS _Terminal_, and _iTerm_ all work well. + +## Testablity + +There is a `SimulationScreen`, that can be used to simulate a real screen +for automated testing. The supplied tests do this. The simulation contains +event delivery, screen resizing support, and capabilities to inject events +and examine "`physical`" screen contents. + +## Platforms + +### POSIX (Linux, FreeBSD, macOS, Solaris, etc.) + +For mainstream systems with a suitably well defined system call interface +to tty settings, everything works using pure Go. + +For the remainder (right now means only Solaris/illumos) we use POSIX function +calls to manage termios, which implies that CGO is required on those platforms. + +### Windows + +Windows console mode applications are supported. Unfortunately _mintty_ +and other _cygwin_ style applications are not supported. + +Modern console applications like ConEmu, as well as the Windows 10 +console itself, support all the good features (resize, mouse tracking, etc.) + +I haven't figured out how to cleanly resolve the dichotomy between cygwin +style termios and the Windows Console API; it seems that perhaps nobody else +has either. If anyone has suggestions, let me know! Really, if you're +using a Windows application, you should use the native Windows console or a +fully compatible console implementation. + +### Plan9 and Native Client (Nacl) + +The nacl and plan9 platforms won't work, but compilation stubs are supplied +for folks that want to include parts of this in software targetting those +platforms. The Simulation screen works, but as Tcell doesn't know how to +allocate a real screen object on those platforms, `NewScreen()` will fail. + +If anyone has wisdom about how to improve support for either of these, +please let me know. PRs are especially welcome. + +### Commercial Support + +_Tcell_ is absolutely free, but if you want to obtain commercial, professional support, there are options. + +[cols="2",align="center",frame="none", grid="none"] +|=== +^.^| +image:logos/tidelift.png[100,100] +a| +https://tidelift.com/[Tidelift] subscriptions include support for _Tcell_, as well as many other open source packages. + +^.^| +image:logos/staysail.png[100,100] +a| +mailto:info@staysail.tech[Staysail Systems, Inc.] offers direct support, and custom development around _Tcell_ on an hourly basis. + +^.^| +image:logos/patreon.png[100,100] +a|I also welcome donations at https://www.patron.com/gedamore/[Patreon], if you just want to make a contribution. +|=== diff --git a/vendor/maunium.net/go/tcell/cell.go b/vendor/maunium.net/go/tcell/cell.go index 496f10f..957b62f 100644 --- a/vendor/maunium.net/go/tcell/cell.go +++ b/vendor/maunium.net/go/tcell/cell.go @@ -52,6 +52,10 @@ func (cb *CellBuffer) SetContent(x int, y int, i := 0 for i < len(c.currComb) { r := c.currComb[i] + if r == '\u200d' { + i += 2 + continue + } if runewidth.RuneWidth(r) != 0 { // not a combining character, yank it c.currComb = append(c.currComb[:i-1], c.currComb[i+1:]...) @@ -175,12 +179,13 @@ func (cb *CellBuffer) Resize(w, h int) { // Fill fills the entire cell buffer array with the specified character // and style. Normally choose ' ' to clear the screen. This API doesn't -// support combining characters. +// support combining characters, or characters with a width larger than one. func (cb *CellBuffer) Fill(r rune, style Style) { for i := range cb.cells { c := &cb.cells[i] c.currMain = r c.currComb = nil c.currStyle = style + c.width = 1 } } diff --git a/vendor/maunium.net/go/tcell/console_win.go b/vendor/maunium.net/go/tcell/console_win.go index 5957a17..a7507c2 100644 --- a/vendor/maunium.net/go/tcell/console_win.go +++ b/vendor/maunium.net/go/tcell/console_win.go @@ -28,7 +28,6 @@ type cScreen struct { in syscall.Handle out syscall.Handle cancelflag syscall.Handle - title syscall.Handle scandone chan struct{} evch chan Event quit chan struct{} diff --git a/vendor/maunium.net/go/tcell/tcell.png b/vendor/maunium.net/go/tcell/tcell.png deleted file mode 100644 index 24333c4..0000000 Binary files a/vendor/maunium.net/go/tcell/tcell.png and /dev/null differ diff --git a/vendor/maunium.net/go/tcell/tcell.svg b/vendor/maunium.net/go/tcell/tcell.svg deleted file mode 100644 index d8695d5..0000000 --- a/vendor/maunium.net/go/tcell/tcell.svg +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - tcell - - diff --git a/vendor/maunium.net/go/tcell/terminfo/term_termite.go b/vendor/maunium.net/go/tcell/terminfo/term_termite.go new file mode 100644 index 0000000..8e7f683 --- /dev/null +++ b/vendor/maunium.net/go/tcell/terminfo/term_termite.go @@ -0,0 +1,152 @@ +// Generated automatically. DO NOT HAND-EDIT. + +package terminfo + +func init() { + // VTE-based terminal + AddTerminfo(&Terminfo{ + Name: "xterm-termite", + Columns: 80, + Lines: 24, + Colors: 256, + Bell: "\a", + Clear: "\x1b[H\x1b[2J", + EnterCA: "\x1b[?1049h", + ExitCA: "\x1b[?1049l", + ShowCursor: "\x1b[?12l\x1b[?25h", + HideCursor: "\x1b[?25l", + AttrOff: "\x1b(B\x1b[m", + Underline: "\x1b[4m", + Bold: "\x1b[1m", + Dim: "\x1b[2m", + Reverse: "\x1b[7m", + EnterKeypad: "\x1b[?1h\x1b=", + ExitKeypad: "\x1b[?1l\x1b>", + SetFg: "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m", + SetBg: "\x1b[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m", + SetFgBg: "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;;%?%p2%{8}%<%t4%p2%d%e%p2%{16}%<%t10%p2%{8}%-%d%e48;5;%p2%d%;m", + AltChars: "++,,--..00``aaffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~", + EnterAcs: "\x1b(0", + ExitAcs: "\x1b(B", + Mouse: "\x1b[M", + MouseMode: "%?%p1%{1}%=%t%'h'%Pa%e%'l'%Pa%;\x1b[?1000%ga%c\x1b[?1002%ga%c\x1b[?1003%ga%c\x1b[?1006%ga%c", + SetCursor: "\x1b[%i%p1%d;%p2%dH", + CursorBack1: "\b", + CursorUp1: "\x1b[A", + KeyUp: "\x1bOA", + KeyDown: "\x1bOB", + KeyRight: "\x1bOC", + KeyLeft: "\x1bOD", + KeyInsert: "\x1b[2~", + KeyDelete: "\x1b[3~", + KeyBackspace: "\xff", + KeyHome: "\x1bOH", + KeyEnd: "\x1bOF", + KeyPgUp: "\x1b[5~", + KeyPgDn: "\x1b[6~", + KeyF1: "\x1bOP", + KeyF2: "\x1bOQ", + KeyF3: "\x1bOR", + KeyF4: "\x1bOS", + KeyF5: "\x1b[15~", + KeyF6: "\x1b[17~", + KeyF7: "\x1b[18~", + KeyF8: "\x1b[19~", + KeyF9: "\x1b[20~", + KeyF10: "\x1b[21~", + KeyF11: "\x1b[23~", + KeyF12: "\x1b[24~", + KeyF13: "\x1b[1;2P", + KeyF14: "\x1b[1;2Q", + KeyF15: "\x1b[1;2R", + KeyF16: "\x1b[1;2S", + KeyF17: "\x1b[15;2~", + KeyF18: "\x1b[17;2~", + KeyF19: "\x1b[18;2~", + KeyF20: "\x1b[19;2~", + KeyF21: "\x1b[20;2~", + KeyF22: "\x1b[21;2~", + KeyF23: "\x1b[23;2~", + KeyF24: "\x1b[24;2~", + KeyF25: "\x1b[1;5P", + KeyF26: "\x1b[1;5Q", + KeyF27: "\x1b[1;5R", + KeyF28: "\x1b[1;5S", + KeyF29: "\x1b[15;5~", + KeyF30: "\x1b[17;5~", + KeyF31: "\x1b[18;5~", + KeyF32: "\x1b[19;5~", + KeyF33: "\x1b[20;5~", + KeyF34: "\x1b[21;5~", + KeyF35: "\x1b[23;5~", + KeyF36: "\x1b[24;5~", + KeyF37: "\x1b[1;6P", + KeyF38: "\x1b[1;6Q", + KeyF39: "\x1b[1;6R", + KeyF40: "\x1b[1;6S", + KeyF41: "\x1b[15;6~", + KeyF42: "\x1b[17;6~", + KeyF43: "\x1b[18;6~", + KeyF44: "\x1b[19;6~", + KeyF45: "\x1b[20;6~", + KeyF46: "\x1b[21;6~", + KeyF47: "\x1b[23;6~", + KeyF48: "\x1b[24;6~", + KeyF49: "\x1b[1;3P", + KeyF50: "\x1b[1;3Q", + KeyF51: "\x1b[1;3R", + KeyF52: "\x1b[1;3S", + KeyF53: "\x1b[15;3~", + KeyF54: "\x1b[17;3~", + KeyF55: "\x1b[18;3~", + KeyF56: "\x1b[19;3~", + KeyF57: "\x1b[20;3~", + KeyF58: "\x1b[21;3~", + KeyF59: "\x1b[23;3~", + KeyF60: "\x1b[24;3~", + KeyF61: "\x1b[1;4P", + KeyF62: "\x1b[1;4Q", + KeyF63: "\x1b[1;4R", + KeyBacktab: "\x1b[Z", + KeyShfLeft: "\x1b[1;2D", + KeyShfRight: "\x1b[1;2C", + KeyShfUp: "\x1b[1;2A", + KeyShfDown: "\x1b[1;2B", + KeyCtrlLeft: "\x1b[1;5D", + KeyCtrlRight: "\x1b[1;5C", + KeyCtrlUp: "\x1b[1;5A", + KeyCtrlDown: "\x1b[1;5B", + KeyMetaLeft: "\x1b[1;9D", + KeyMetaRight: "\x1b[1;9C", + KeyMetaUp: "\x1b[1;9A", + KeyMetaDown: "\x1b[1;9B", + KeyAltLeft: "\x1b[1;3D", + KeyAltRight: "\x1b[1;3C", + KeyAltUp: "\x1b[1;3A", + KeyAltDown: "\x1b[1;3B", + KeyAltShfLeft: "\x1b[1;4D", + KeyAltShfRight: "\x1b[1;4C", + KeyAltShfUp: "\x1b[1;4A", + KeyAltShfDown: "\x1b[1;4B", + KeyMetaShfLeft: "\x1b[1;10D", + KeyMetaShfRight: "\x1b[1;10C", + KeyMetaShfUp: "\x1b[1;10A", + KeyMetaShfDown: "\x1b[1;10B", + KeyCtrlShfLeft: "\x1b[1;6D", + KeyCtrlShfRight: "\x1b[1;6C", + KeyCtrlShfUp: "\x1b[1;6A", + KeyCtrlShfDown: "\x1b[1;6B", + KeyShfHome: "\x1b[1;2H", + KeyShfEnd: "\x1b[1;2F", + KeyCtrlHome: "\x1b[1;5H", + KeyCtrlEnd: "\x1b[1;5F", + KeyAltHome: "\x1b[1;9H", + KeyAltEnd: "\x1b[1;9F", + KeyCtrlShfHome: "\x1b[1;6H", + KeyCtrlShfEnd: "\x1b[1;6F", + KeyMetaShfHome: "\x1b[1;10H", + KeyMetaShfEnd: "\x1b[1;10F", + KeyAltShfHome: "\x1b[1;4H", + KeyAltShfEnd: "\x1b[1;4F", + }) +} diff --git a/vendor/maunium.net/go/tcell/tscreen.go b/vendor/maunium.net/go/tcell/tscreen.go index d8e62b2..dd49814 100644 --- a/vendor/maunium.net/go/tcell/tscreen.go +++ b/vendor/maunium.net/go/tcell/tscreen.go @@ -444,8 +444,8 @@ func (t *tScreen) ResetTitle() { func (t *tScreen) Fini() { t.Lock() defer t.Unlock() - - ti := t.ti + + ti := t.ti t.cells.Resize(0, 0) t.TPuts(ti.ShowCursor) t.TPuts(ti.AttrOff) @@ -467,7 +467,7 @@ func (t *tScreen) Fini() { default: close(t.quit) } - + t.termioFini() } diff --git a/vendor/maunium.net/go/tview/LICENSE.txt b/vendor/maunium.net/go/tview/LICENSE.txt index 8aa2645..9d69430 100644 --- a/vendor/maunium.net/go/tview/LICENSE.txt +++ b/vendor/maunium.net/go/tview/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) [year] [fullname] +Copyright (c) 2018 Oliver Kuederle Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/vendor/maunium.net/go/tview/README.md b/vendor/maunium.net/go/tview/README.md index 3e5734e..7ce06a2 100644 --- a/vendor/maunium.net/go/tview/README.md +++ b/vendor/maunium.net/go/tview/README.md @@ -12,6 +12,7 @@ Among these components are: - __Input forms__ (include __input/password fields__, __drop-down selections__, __checkboxes__, and __buttons__) - Navigable multi-color __text views__ - Sophisticated navigable __table views__ +- Flexible __tree views__ - Selectable __lists__ - __Grid__, __Flexbox__ and __page layouts__ - Modal __message windows__ @@ -64,9 +65,17 @@ Add your issue here on GitHub. Feel free to get in touch if you have any questio (There are no corresponding tags in the project. I only keep such a history in this README.) +- v0.19 (2018-10-28) + - Added `QueueUpdate()` and `QueueEvent()` to `Application` to help with modifications to primitives from goroutines. +- v0.18 (2018-10-18) + - `InputField` elements can now be navigated freely. +- v0.17 (2018-06-20) + - Added `TreeView`. +- v0.15 (2018-05-02) + - `Flex` and `Grid` don't clear their background per default, thus allowing for custom modals. See the [Wiki](https://github.com/rivo/tview/wiki/Modal) for an example. - v0.14 (2018-04-13) - Added an `Escape()` function which keep strings like color or region tags from being recognized as such. - - Added `ANSIIWriter()` and `TranslateANSII()` which convert ANSII escape sequences to `tview` color tags. + - Added `ANSIWriter()` and `TranslateANSI()` which convert ANSI escape sequences to `tview` color tags. - v0.13 (2018-04-01) - Added background colors and text attributes to color tags. - v0.12 (2018-03-13) diff --git a/vendor/maunium.net/go/tview/ansi.go b/vendor/maunium.net/go/tview/ansi.go new file mode 100644 index 0000000..4d14c28 --- /dev/null +++ b/vendor/maunium.net/go/tview/ansi.go @@ -0,0 +1,237 @@ +package tview + +import ( + "bytes" + "fmt" + "io" + "strconv" + "strings" +) + +// The states of the ANSI escape code parser. +const ( + ansiText = iota + ansiEscape + ansiSubstring + ansiControlSequence +) + +// ansi is a io.Writer which translates ANSI escape codes into tview color +// tags. +type ansi struct { + io.Writer + + // Reusable buffers. + buffer *bytes.Buffer // The entire output text of one Write(). + csiParameter, csiIntermediate *bytes.Buffer // Partial CSI strings. + + // The current state of the parser. One of the ansi constants. + state int +} + +// ANSIWriter returns an io.Writer which translates any ANSI escape codes +// written to it into tview color tags. Other escape codes don't have an effect +// and are simply removed. The translated text is written to the provided +// writer. +func ANSIWriter(writer io.Writer) io.Writer { + return &ansi{ + Writer: writer, + buffer: new(bytes.Buffer), + csiParameter: new(bytes.Buffer), + csiIntermediate: new(bytes.Buffer), + state: ansiText, + } +} + +// Write parses the given text as a string of runes, translates ANSI escape +// codes to color tags and writes them to the output writer. +func (a *ansi) Write(text []byte) (int, error) { + defer func() { + a.buffer.Reset() + }() + + for _, r := range string(text) { + switch a.state { + + // We just entered an escape sequence. + case ansiEscape: + switch r { + case '[': // Control Sequence Introducer. + a.csiParameter.Reset() + a.csiIntermediate.Reset() + a.state = ansiControlSequence + case 'c': // Reset. + fmt.Fprint(a.buffer, "[-:-:-]") + a.state = ansiText + case 'P', ']', 'X', '^', '_': // Substrings and commands. + a.state = ansiSubstring + default: // Ignore. + a.state = ansiText + } + + // CSI Sequences. + case ansiControlSequence: + switch { + case r >= 0x30 && r <= 0x3f: // Parameter bytes. + if _, err := a.csiParameter.WriteRune(r); err != nil { + return 0, err + } + case r >= 0x20 && r <= 0x2f: // Intermediate bytes. + if _, err := a.csiIntermediate.WriteRune(r); err != nil { + return 0, err + } + case r >= 0x40 && r <= 0x7e: // Final byte. + switch r { + case 'E': // Next line. + count, _ := strconv.Atoi(a.csiParameter.String()) + if count == 0 { + count = 1 + } + fmt.Fprint(a.buffer, strings.Repeat("\n", count)) + case 'm': // Select Graphic Rendition. + var ( + background, foreground, attributes string + clearAttributes bool + ) + fields := strings.Split(a.csiParameter.String(), ";") + if len(fields) == 0 || len(fields) == 1 && fields[0] == "0" { + // Reset. + if _, err := a.buffer.WriteString("[-:-:-]"); err != nil { + return 0, err + } + break + } + lookupColor := func(colorNumber int, bright bool) string { + if colorNumber < 0 || colorNumber > 7 { + return "black" + } + if bright { + colorNumber += 8 + } + return [...]string{ + "black", + "red", + "green", + "yellow", + "blue", + "darkmagenta", + "darkcyan", + "white", + "#7f7f7f", + "#ff0000", + "#00ff00", + "#ffff00", + "#5c5cff", + "#ff00ff", + "#00ffff", + "#ffffff", + }[colorNumber] + } + for index, field := range fields { + switch field { + case "1", "01": + attributes += "b" + case "2", "02": + attributes += "d" + case "4", "04": + attributes += "u" + case "5", "05": + attributes += "l" + case "7", "07": + attributes += "7" + case "22", "24", "25", "27": + clearAttributes = true + case "30", "31", "32", "33", "34", "35", "36", "37": + colorNumber, _ := strconv.Atoi(field) + foreground = lookupColor(colorNumber-30, false) + case "40", "41", "42", "43", "44", "45", "46", "47": + colorNumber, _ := strconv.Atoi(field) + background = lookupColor(colorNumber-40, false) + case "90", "91", "92", "93", "94", "95", "96", "97": + colorNumber, _ := strconv.Atoi(field) + foreground = lookupColor(colorNumber-90, true) + case "100", "101", "102", "103", "104", "105", "106", "107": + colorNumber, _ := strconv.Atoi(field) + background = lookupColor(colorNumber-100, true) + case "38", "48": + var color string + if len(fields) > index+1 { + if fields[index+1] == "5" && len(fields) > index+2 { // 8-bit colors. + colorNumber, _ := strconv.Atoi(fields[index+2]) + if colorNumber <= 7 { + color = lookupColor(colorNumber, false) + } else if colorNumber <= 15 { + color = lookupColor(colorNumber, true) + } else if colorNumber <= 231 { + red := (colorNumber - 16) / 36 + green := ((colorNumber - 16) / 6) % 6 + blue := (colorNumber - 16) % 6 + color = fmt.Sprintf("#%02x%02x%02x", 255*red/5, 255*green/5, 255*blue/5) + } else if colorNumber <= 255 { + grey := 255 * (colorNumber - 232) / 23 + color = fmt.Sprintf("#%02x%02x%02x", grey, grey, grey) + } + } else if fields[index+1] == "2" && len(fields) > index+4 { // 24-bit colors. + red, _ := strconv.Atoi(fields[index+2]) + green, _ := strconv.Atoi(fields[index+3]) + blue, _ := strconv.Atoi(fields[index+4]) + color = fmt.Sprintf("#%02x%02x%02x", red, green, blue) + } + } + if len(color) > 0 { + if field == "38" { + foreground = color + } else { + background = color + } + } + } + } + if len(attributes) > 0 || clearAttributes { + attributes = ":" + attributes + } + if len(foreground) > 0 || len(background) > 0 || len(attributes) > 0 { + fmt.Fprintf(a.buffer, "[%s:%s%s]", foreground, background, attributes) + } + } + a.state = ansiText + default: // Undefined byte. + a.state = ansiText // Abort CSI. + } + + // We just entered a substring/command sequence. + case ansiSubstring: + if r == 27 { // Most likely the end of the substring. + a.state = ansiEscape + } // Ignore all other characters. + + // "ansiText" and all others. + default: + if r == 27 { + // This is the start of an escape sequence. + a.state = ansiEscape + } else { + // Just a regular rune. Send to buffer. + if _, err := a.buffer.WriteRune(r); err != nil { + return 0, err + } + } + } + } + + // Write buffer to target writer. + n, err := a.buffer.WriteTo(a.Writer) + if err != nil { + return int(n), err + } + return len(text), nil +} + +// TranslateANSI replaces ANSI escape sequences found in the provided string +// with tview's color tags and returns the resulting string. +func TranslateANSI(text string) string { + var buffer bytes.Buffer + writer := ANSIWriter(&buffer) + writer.Write([]byte(text)) + return buffer.String() +} diff --git a/vendor/maunium.net/go/tview/ansii.go b/vendor/maunium.net/go/tview/ansii.go deleted file mode 100644 index 0ce3d4a..0000000 --- a/vendor/maunium.net/go/tview/ansii.go +++ /dev/null @@ -1,237 +0,0 @@ -package tview - -import ( - "bytes" - "fmt" - "io" - "strconv" - "strings" -) - -// The states of the ANSII escape code parser. -const ( - ansiiText = iota - ansiiEscape - ansiiSubstring - ansiiControlSequence -) - -// ansii is a io.Writer which translates ANSII escape codes into tview color -// tags. -type ansii struct { - io.Writer - - // Reusable buffers. - buffer *bytes.Buffer // The entire output text of one Write(). - csiParameter, csiIntermediate *bytes.Buffer // Partial CSI strings. - - // The current state of the parser. One of the ansii constants. - state int -} - -// ANSIIWriter returns an io.Writer which translates any ANSII escape codes -// written to it into tview color tags. Other escape codes don't have an effect -// and are simply removed. The translated text is written to the provided -// writer. -func ANSIIWriter(writer io.Writer) io.Writer { - return &ansii{ - Writer: writer, - buffer: new(bytes.Buffer), - csiParameter: new(bytes.Buffer), - csiIntermediate: new(bytes.Buffer), - state: ansiiText, - } -} - -// Write parses the given text as a string of runes, translates ANSII escape -// codes to color tags and writes them to the output writer. -func (a *ansii) Write(text []byte) (int, error) { - defer func() { - a.buffer.Reset() - }() - - for _, r := range string(text) { - switch a.state { - - // We just entered an escape sequence. - case ansiiEscape: - switch r { - case '[': // Control Sequence Introducer. - a.csiParameter.Reset() - a.csiIntermediate.Reset() - a.state = ansiiControlSequence - case 'c': // Reset. - fmt.Fprint(a.buffer, "[-:-:-]") - a.state = ansiiText - case 'P', ']', 'X', '^', '_': // Substrings and commands. - a.state = ansiiSubstring - default: // Ignore. - a.state = ansiiText - } - - // CSI Sequences. - case ansiiControlSequence: - switch { - case r >= 0x30 && r <= 0x3f: // Parameter bytes. - if _, err := a.csiParameter.WriteRune(r); err != nil { - return 0, err - } - case r >= 0x20 && r <= 0x2f: // Intermediate bytes. - if _, err := a.csiIntermediate.WriteRune(r); err != nil { - return 0, err - } - case r >= 0x40 && r <= 0x7e: // Final byte. - switch r { - case 'E': // Next line. - count, _ := strconv.Atoi(a.csiParameter.String()) - if count == 0 { - count = 1 - } - fmt.Fprint(a.buffer, strings.Repeat("\n", count)) - case 'm': // Select Graphic Rendition. - var ( - background, foreground, attributes string - clearAttributes bool - ) - fields := strings.Split(a.csiParameter.String(), ";") - if len(fields) == 0 || len(fields) == 1 && fields[0] == "0" { - // Reset. - if _, err := a.buffer.WriteString("[-:-:-]"); err != nil { - return 0, err - } - break - } - lookupColor := func(colorNumber int, bright bool) string { - if colorNumber < 0 || colorNumber > 7 { - return "black" - } - if bright { - colorNumber += 8 - } - return [...]string{ - "black", - "red", - "green", - "yellow", - "blue", - "darkmagenta", - "darkcyan", - "white", - "#7f7f7f", - "#ff0000", - "#00ff00", - "#ffff00", - "#5c5cff", - "#ff00ff", - "#00ffff", - "#ffffff", - }[colorNumber] - } - for index, field := range fields { - switch field { - case "1", "01": - attributes += "b" - case "2", "02": - attributes += "d" - case "4", "04": - attributes += "u" - case "5", "05": - attributes += "l" - case "7", "07": - attributes += "7" - case "22", "24", "25", "27": - clearAttributes = true - case "30", "31", "32", "33", "34", "35", "36", "37": - colorNumber, _ := strconv.Atoi(field) - foreground = lookupColor(colorNumber-30, false) - case "40", "41", "42", "43", "44", "45", "46", "47": - colorNumber, _ := strconv.Atoi(field) - background = lookupColor(colorNumber-40, false) - case "90", "91", "92", "93", "94", "95", "96", "97": - colorNumber, _ := strconv.Atoi(field) - foreground = lookupColor(colorNumber-90, true) - case "100", "101", "102", "103", "104", "105", "106", "107": - colorNumber, _ := strconv.Atoi(field) - background = lookupColor(colorNumber-100, true) - case "38", "48": - var color string - if len(fields) > index+1 { - if fields[index+1] == "5" && len(fields) > index+2 { // 8-bit colors. - colorNumber, _ := strconv.Atoi(fields[index+2]) - if colorNumber <= 7 { - color = lookupColor(colorNumber, false) - } else if colorNumber <= 15 { - color = lookupColor(colorNumber, true) - } else if colorNumber <= 231 { - red := (colorNumber - 16) / 36 - green := ((colorNumber - 16) / 6) % 6 - blue := (colorNumber - 16) % 6 - color = fmt.Sprintf("%02x%02x%02x", 255*red/5, 255*green/5, 255*blue/5) - } else if colorNumber <= 255 { - grey := 255 * (colorNumber - 232) / 23 - color = fmt.Sprintf("%02x%02x%02x", grey, grey, grey) - } - } else if fields[index+1] == "2" && len(fields) > index+4 { // 24-bit colors. - red, _ := strconv.Atoi(fields[index+2]) - green, _ := strconv.Atoi(fields[index+3]) - blue, _ := strconv.Atoi(fields[index+4]) - color = fmt.Sprintf("%02x%02x%02x", red, green, blue) - } - } - if len(color) > 0 { - if field == "38" { - foreground = color - } else { - background = color - } - } - } - } - if len(attributes) > 0 || clearAttributes { - attributes = ":" + attributes - } - if len(foreground) > 0 || len(background) > 0 || len(attributes) > 0 { - fmt.Fprintf(a.buffer, "[%s:%s%s]", foreground, background, attributes) - } - } - a.state = ansiiText - default: // Undefined byte. - a.state = ansiiText // Abort CSI. - } - - // We just entered a substring/command sequence. - case ansiiSubstring: - if r == 27 { // Most likely the end of the substring. - a.state = ansiiEscape - } // Ignore all other characters. - - // "ansiiText" and all others. - default: - if r == 27 { - // This is the start of an escape sequence. - a.state = ansiiEscape - } else { - // Just a regular rune. Send to buffer. - if _, err := a.buffer.WriteRune(r); err != nil { - return 0, err - } - } - } - } - - // Write buffer to target writer. - n, err := a.buffer.WriteTo(a.Writer) - if err != nil { - return int(n), err - } - return len(text), nil -} - -// TranslateANSII replaces ANSII escape sequences found in the provided string -// with tview's color tags and returns the resulting string. -func TranslateANSII(text string) string { - var buffer bytes.Buffer - writer := ANSIIWriter(&buffer) - writer.Write([]byte(text)) - return buffer.String() -} diff --git a/vendor/maunium.net/go/tview/application.go b/vendor/maunium.net/go/tview/application.go index 7e6da3a..ae4d7f8 100644 --- a/vendor/maunium.net/go/tview/application.go +++ b/vendor/maunium.net/go/tview/application.go @@ -1,24 +1,36 @@ package tview import ( - "fmt" - "os" "sync" "maunium.net/go/tcell" ) +// The size of the event/update/redraw channels. +const queueSize = 100 + // Application represents the top node of an application. // // It is not strictly required to use this class as none of the other classes // depend on it. However, it provides useful tools to set up an application and // plays nicely with all widgets. +// +// The following command displays a primitive p on the screen until Ctrl-C is +// pressed: +// +// if err := tview.NewApplication().SetRoot(p, true).Run(); err != nil { +// panic(err) +// } type Application struct { sync.RWMutex // The application's screen. screen tcell.Screen + // Indicates whether the application's screen is currently active. This is + // false during suspended mode. + running bool + // The primitive which currently has the keyboard focus. focus Primitive @@ -48,13 +60,23 @@ type Application struct { // was drawn. afterDraw func(screen tcell.Screen) - // If this value is true, the application has entered suspended mode. - suspended bool + // Used to send screen events from separate goroutine to main event loop + events chan tcell.Event + + // Functions queued from goroutines, used to serialize updates to primitives. + updates chan func() + + // A channel which signals the end of the suspended mode. + suspendToken chan struct{} } // NewApplication creates and returns a new application. func NewApplication() *Application { - return &Application{} + return &Application{ + events: make(chan tcell.Event, queueSize), + updates: make(chan func(), queueSize), + suspendToken: make(chan struct{}, 1), + } } // SetInputCapture sets a function which captures all key events before they are @@ -97,140 +119,222 @@ func (a *Application) GetScreen() tcell.Screen { return a.screen } +// SetScreen allows you to provide your own tcell.Screen object. For most +// applications, this is not needed and you should be familiar with +// tcell.Screen when using this function. Run() will call Init() and Fini() on +// the provided screen object. +// +// This function is typically called before calling Run(). Calling it while an +// application is running will switch the application to the new screen. Fini() +// will be called on the old screen and Init() on the new screen (errors +// returned by Init() will lead to a panic). +// +// Note that calling Suspend() will invoke Fini() on your screen object and it +// will not be restored when suspended mode ends. Instead, a new default screen +// object will be created. +func (a *Application) SetScreen(screen tcell.Screen) *Application { + a.Lock() + defer a.Unlock() + if a.running { + a.screen.Fini() + } + a.screen = screen + if a.running { + if err := a.screen.Init(); err != nil { + panic(err) + } + } + return a +} + // Run starts the application and thus the event loop. This function returns // when Stop() was called. func (a *Application) Run() error { var err error a.Lock() - // Make a screen. - a.screen, err = tcell.NewScreen() - if err != nil { - a.Unlock() - return err + // Make a screen if there is none yet. + if a.screen == nil { + a.screen, err = tcell.NewScreen() + if err != nil { + a.Unlock() + return err + } } if err = a.screen.Init(); err != nil { a.Unlock() return err } a.screen.EnableMouse() + a.running = true + + // We catch panics to clean up because they mess up the terminal. + defer func() { + if p := recover(); p != nil { + if a.screen != nil { + a.screen.Fini() + } + a.running = false + panic(p) + } + }() // Draw the screen for the first time. a.Unlock() - a.Draw() + a.draw() + + // Separate loop to wait for screen events. + var wg sync.WaitGroup + wg.Add(1) + a.suspendToken <- struct{}{} // We need this to get started. + go func() { + defer wg.Done() + for range a.suspendToken { + for { + a.RLock() + screen := a.screen + a.RUnlock() + if screen == nil { + // We have no screen. We might need to stop. + break + } - // Start event loop. - for { - a.Lock() - screen := a.screen - if a.suspended { - a.suspended = false // Clear previous suspended flag. - } - a.Unlock() - if screen == nil { - break + // Wait for next event and queue it. + event := screen.PollEvent() + if event != nil { + // Regular event. Queue. + a.QueueEvent(event) + continue + } + + // A screen was finalized (event is nil). + a.RLock() + running := a.running + a.RUnlock() + if running { + // The application was stopped. End the event loop. + a.QueueEvent(nil) + return + } + + // We're in suspended mode (running is false). Pause and wait for new + // token. + break + } } + }() - // Wait for next event. - event := a.screen.PollEvent() - if event == nil { - a.Lock() - if a.suspended { - // This screen was renewed due to suspended mode. - a.suspended = false - a.Unlock() - continue // Resume. + // Start event loop. +EventLoop: + for { + select { + case event := <-a.events: + if event == nil { + break EventLoop } - a.Unlock() - // The screen was finalized. Exit the loop. - break - } + switch event := event.(type) { + case *tcell.EventKey: + a.RLock() + p := a.focus + inputCapture := a.inputCapture + a.RUnlock() + + // Intercept keys. + if inputCapture != nil { + event = inputCapture(event) + if event == nil { + continue // Don't forward event. + } + } - switch event := event.(type) { - case *tcell.EventKey: - a.RLock() - p := a.focus - a.RUnlock() - - // Intercept keys. - if a.inputCapture != nil { - event = a.inputCapture(event) - if event == nil { - break // Don't forward event. + // Ctrl-C closes the application. + if event.Key() == tcell.KeyCtrlC { + a.Stop() } - } - // Pass other key events to the currently focused primitive. - if p != nil { - if handler := p.InputHandler(); handler != nil { - handler(event, func(p Primitive) { - a.SetFocus(p) - }) - a.Draw() + // Pass other key events to the currently focused primitive. + if p != nil { + if handler := p.InputHandler(); handler != nil { + handler(event, func(p Primitive) { + a.SetFocus(p) + }) + a.draw() + } } - } - case *tcell.EventMouse: - a.RLock() - p := a.focus - a.RUnlock() - - // Intercept keys. - if a.mouseCapture != nil { - event = a.mouseCapture(event) - if event == nil { - break // Don't forward event. + + case *tcell.EventMouse: + a.RLock() + p := a.focus + a.RUnlock() + + // Intercept keys. + if a.mouseCapture != nil { + event = a.mouseCapture(event) + if event == nil { + break // Don't forward event. + } } - } - // Pass other key events to the currently focused primitive. - if p != nil { - if handler := p.MouseHandler(); handler != nil { - handler(event, func(p Primitive) { - a.SetFocus(p) - }) - //a.Draw() + // Pass other key events to the currently focused primitive. + if p != nil { + if handler := p.MouseHandler(); handler != nil { + handler(event, func(p Primitive) { + a.SetFocus(p) + }) + //a.Draw() + } } - } - case *tcell.EventPaste: - a.RLock() - p := a.focus - a.RUnlock() - - if a.pasteCapture != nil { - event = a.pasteCapture(event) - if event == nil { - break + case *tcell.EventPaste: + a.RLock() + p := a.focus + a.RUnlock() + + if a.pasteCapture != nil { + event = a.pasteCapture(event) + if event == nil { + break + } } - } - if p != nil { - if handler := p.PasteHandler(); handler != nil { - handler(event) - a.Draw() + if p != nil { + if handler := p.PasteHandler(); handler != nil { + handler(event) + a.Draw() + } } + case *tcell.EventResize: + a.RLock() + screen := a.screen + a.RUnlock() + screen.Clear() + a.draw() } - case *tcell.EventResize: - a.Lock() - screen := a.screen - a.Unlock() - screen.Clear() - a.Draw() + + // If we have updates, now is the time to execute them. + case updater := <-a.updates: + updater() } } + a.running = false + close(a.suspendToken) + wg.Wait() + return nil } // Stop stops the application, causing Run() to return. func (a *Application) Stop() { - a.RLock() - defer a.RUnlock() - if a.screen == nil { + a.Lock() + defer a.Unlock() + screen := a.screen + if screen == nil { return } - a.screen.Fini() a.screen = nil + screen.Fini() + // a.running is still true, the main loop will clean up. } // Suspend temporarily suspends the application by exiting terminal UI mode and @@ -243,29 +347,24 @@ func (a *Application) Stop() { func (a *Application) Suspend(f func()) bool { a.Lock() - if a.suspended || a.screen == nil { - // Application is already suspended. + screen := a.screen + if screen == nil { + // Screen has not yet been initialized. a.Unlock() return false } - // Enter suspended mode. - a.suspended = true + // Enter suspended mode. Make a new screen here already so our event loop can + // continue. + a.screen = nil + a.running = false + screen.Fini() a.Unlock() - a.Stop() - - // Deal with panics during suspended mode. Exit the program. - defer func() { - if p := recover(); p != nil { - fmt.Println(p) - os.Exit(1) - } - }() // Wait for "f" to return. f() - // Make a new screen and redraw. + // Initialize our new screen and draw the contents. a.Lock() var err error a.screen, err = tcell.NewScreen() @@ -278,23 +377,36 @@ func (a *Application) Suspend(f func()) bool { panic(err) } a.screen.EnableMouse() + a.running = true a.Unlock() - a.Draw() + a.draw() + a.suspendToken <- struct{}{} + // One key event will get lost, see https://github.com/gdamore/tcell/issues/194 // Continue application loop. return true } -// Draw refreshes the screen. It calls the Draw() function of the application's -// root primitive and then syncs the screen buffer. +// Draw refreshes the screen (during the next update cycle). It calls the Draw() +// function of the application's root primitive and then syncs the screen +// buffer. func (a *Application) Draw() *Application { - a.RLock() + a.QueueUpdate(func() { + a.draw() + }) + return a +} + +// draw actually does what Draw() promises to do. +func (a *Application) draw() *Application { + a.Lock() + defer a.Unlock() + screen := a.screen root := a.root fullscreen := a.rootFullscreen before := a.beforeDraw after := a.afterDraw - a.RUnlock() // Maybe we're not ready yet or not anymore. if screen == nil || root == nil { @@ -427,3 +539,35 @@ func (a *Application) GetFocus() Primitive { defer a.RUnlock() return a.focus } + +// QueueUpdate is used to synchronize access to primitives from non-main +// goroutines. The provided function will be executed as part of the event loop +// and thus will not cause race conditions with other such update functions or +// the Draw() function. +// +// Note that Draw() is not implicitly called after the execution of f as that +// may not be desirable. You can call Draw() from f if the screen should be +// refreshed after each update. Alternatively, use QueueUpdateDraw() to follow +// up with an immediate refresh of the screen. +func (a *Application) QueueUpdate(f func()) *Application { + a.updates <- f + return a +} + +// QueueUpdateDraw works like QueueUpdate() except it refreshes the screen +// immediately after executing f. +func (a *Application) QueueUpdateDraw(f func()) *Application { + a.QueueUpdate(func() { + f() + a.draw() + }) + return a +} + +// QueueEvent sends an event to the Application event loop. +// +// It is not recommended for event to be nil. +func (a *Application) QueueEvent(event tcell.Event) *Application { + a.events <- event + return a +} diff --git a/vendor/maunium.net/go/tview/borders.go b/vendor/maunium.net/go/tview/borders.go new file mode 100644 index 0000000..946c878 --- /dev/null +++ b/vendor/maunium.net/go/tview/borders.go @@ -0,0 +1,45 @@ +package tview + +// Borders defines various borders used when primitives are drawn. +// These may be changed to accommodate a different look and feel. +var Borders = struct { + Horizontal rune + Vertical rune + TopLeft rune + TopRight rune + BottomLeft rune + BottomRight rune + + LeftT rune + RightT rune + TopT rune + BottomT rune + Cross rune + + HorizontalFocus rune + VerticalFocus rune + TopLeftFocus rune + TopRightFocus rune + BottomLeftFocus rune + BottomRightFocus rune +}{ + Horizontal: BoxDrawingsLightHorizontal, + Vertical: BoxDrawingsLightVertical, + TopLeft: BoxDrawingsLightDownAndRight, + TopRight: BoxDrawingsLightDownAndLeft, + BottomLeft: BoxDrawingsLightUpAndRight, + BottomRight: BoxDrawingsLightUpAndLeft, + + LeftT: BoxDrawingsLightVerticalAndRight, + RightT: BoxDrawingsLightVerticalAndLeft, + TopT: BoxDrawingsLightDownAndHorizontal, + BottomT: BoxDrawingsLightUpAndHorizontal, + Cross: BoxDrawingsLightVerticalAndHorizontal, + + HorizontalFocus: BoxDrawingsDoubleHorizontal, + VerticalFocus: BoxDrawingsDoubleVertical, + TopLeftFocus: BoxDrawingsDoubleDownAndRight, + TopRightFocus: BoxDrawingsDoubleDownAndLeft, + BottomLeftFocus: BoxDrawingsDoubleUpAndRight, + BottomRightFocus: BoxDrawingsDoubleUpAndLeft, +} diff --git a/vendor/maunium.net/go/tview/box.go b/vendor/maunium.net/go/tview/box.go index ff3cc1e..85bf1b2 100644 --- a/vendor/maunium.net/go/tview/box.go +++ b/vendor/maunium.net/go/tview/box.go @@ -32,6 +32,9 @@ type Box struct { // The color of the border. borderColor tcell.Color + // The style attributes of the border. + borderAttributes tcell.AttrMask + // The title. Only visible if there is a border, too. title string @@ -48,10 +51,6 @@ type Box struct { // Whether or not this box has focus. hasFocus bool - // If set to true, the inner rect of this box will be within the screen at the - // last time the box was drawn. - clampToScreen bool - // An optional capture function which receives a key event and returns the // event to be forwarded to the primitive's default input handler (nil if // nothing should be forwarded). @@ -78,7 +77,6 @@ func NewBox() *Box { borderColor: Styles.BorderColor, titleColor: Styles.TitleColor, titleAlign: AlignCenter, - clampToScreen: true, } b.focus = b return b @@ -121,6 +119,7 @@ func (b *Box) SetRect(x, y, width, height int) { b.y = y b.width = width b.height = height + b.innerX = -1 // Mark inner rect as uninitialized. } // SetDrawFunc sets a callback function which is invoked after the box primitive @@ -273,6 +272,15 @@ func (b *Box) SetBorderColor(color tcell.Color) *Box { return b } +// SetBorderAttributes sets the border's style attributes. You can combine +// different attributes using bitmask operations: +// +// box.SetBorderAttributes(tcell.AttrUnderline | tcell.AttrBold) +func (b *Box) SetBorderAttributes(attr tcell.AttrMask) *Box { + b.borderAttributes = attr + return b +} + // SetTitle sets the box's title. func (b *Box) SetTitle(title string) *Box { b.title = title @@ -319,30 +327,30 @@ func (b *Box) Draw(screen tcell.Screen) { // Draw border. if b.border && b.width >= 2 && b.height >= 2 { - border := background.Foreground(b.borderColor) + border := background.Foreground(b.borderColor) | tcell.Style(b.borderAttributes) var vertical, horizontal, topLeft, topRight, bottomLeft, bottomRight rune if b.focus.HasFocus() { - vertical = GraphicsDbVertBar - horizontal = GraphicsDbHorBar - topLeft = GraphicsDbTopLeftCorner - topRight = GraphicsDbTopRightCorner - bottomLeft = GraphicsDbBottomLeftCorner - bottomRight = GraphicsDbBottomRightCorner + horizontal = Borders.HorizontalFocus + vertical = Borders.VerticalFocus + topLeft = Borders.TopLeftFocus + topRight = Borders.TopRightFocus + bottomLeft = Borders.BottomLeftFocus + bottomRight = Borders.BottomRightFocus } else { - vertical = GraphicsHoriBar - horizontal = GraphicsVertBar - topLeft = GraphicsTopLeftCorner - topRight = GraphicsTopRightCorner - bottomLeft = GraphicsBottomLeftCorner - bottomRight = GraphicsBottomRightCorner + horizontal = Borders.Horizontal + vertical = Borders.Vertical + topLeft = Borders.TopLeft + topRight = Borders.TopRight + bottomLeft = Borders.BottomLeft + bottomRight = Borders.BottomRight } for x := b.x + 1; x < b.x+b.width-1; x++ { - screen.SetContent(x, b.y, vertical, nil, border) - screen.SetContent(x, b.y+b.height-1, vertical, nil, border) + screen.SetContent(x, b.y, horizontal, nil, border) + screen.SetContent(x, b.y+b.height-1, horizontal, nil, border) } for y := b.y + 1; y < b.y+b.height-1; y++ { - screen.SetContent(b.x, y, horizontal, nil, border) - screen.SetContent(b.x+b.width-1, y, horizontal, nil, border) + screen.SetContent(b.x, y, vertical, nil, border) + screen.SetContent(b.x+b.width-1, y, vertical, nil, border) } screen.SetContent(b.x, b.y, topLeft, nil, border) screen.SetContent(b.x+b.width-1, b.y, topRight, nil, border) @@ -351,11 +359,11 @@ func (b *Box) Draw(screen tcell.Screen) { // Draw title. if b.title != "" && b.width >= 4 { - _, printed := Print(screen, b.title, b.x+1, b.y, b.width-2, b.titleAlign, b.titleColor) - if StringWidth(b.title)-printed > 0 && printed > 0 { + printed, _ := Print(screen, b.title, b.x+1, b.y, b.width-2, b.titleAlign, b.titleColor) + if len(b.title)-printed > 0 && printed > 0 { _, _, style, _ := screen.GetContent(b.x+b.width-2, b.y) fg, _, _ := style.Decompose() - Print(screen, string(GraphicsEllipsis), b.x+b.width-2, b.y, 1, AlignLeft, fg) + Print(screen, string(SemigraphicsHorizontalEllipsis), b.x+b.width-2, b.y, 1, AlignLeft, fg) } } } @@ -370,22 +378,20 @@ func (b *Box) Draw(screen tcell.Screen) { } // Clamp inner rect to screen. - if b.clampToScreen { - width, height := screen.Size() - if b.innerX < 0 { - b.innerWidth += b.innerX - b.innerX = 0 - } - if b.innerX+b.innerWidth >= width { - b.innerWidth = width - b.innerX - } - if b.innerY+b.innerHeight >= height { - b.innerHeight = height - b.innerY - } - if b.innerY < 0 { - b.innerHeight += b.innerY - b.innerY = 0 - } + width, height := screen.Size() + if b.innerX < 0 { + b.innerWidth += b.innerX + b.innerX = 0 + } + if b.innerX+b.innerWidth >= width { + b.innerWidth = width - b.innerX + } + if b.innerY+b.innerHeight >= height { + b.innerHeight = height - b.innerY + } + if b.innerY < 0 { + b.innerHeight += b.innerY + b.innerY = 0 } } diff --git a/vendor/maunium.net/go/tview/checkbox.go b/vendor/maunium.net/go/tview/checkbox.go index ae58720..48d4592 100644 --- a/vendor/maunium.net/go/tview/checkbox.go +++ b/vendor/maunium.net/go/tview/checkbox.go @@ -124,9 +124,9 @@ func (c *Checkbox) SetChangedFunc(handler func(checked bool)) *Checkbox { return c } -// SetDoneFunc sets a handler which is called when the user is done entering -// text. The callback function is provided with the key that was pressed, which -// is one of the following: +// SetDoneFunc sets a handler which is called when the user is done using the +// checkbox. The callback function is provided with the key that was pressed, +// which is one of the following: // // - KeyEscape: Abort text input. // - KeyTab: Move to the next field. diff --git a/vendor/maunium.net/go/tview/doc.go b/vendor/maunium.net/go/tview/doc.go index ccaaaf1..ddc410f 100644 --- a/vendor/maunium.net/go/tview/doc.go +++ b/vendor/maunium.net/go/tview/doc.go @@ -7,10 +7,12 @@ Widgets The package implements the following widgets: - - TextView: Scrollable windows that display multi-colored text. Text may also + - TextView: A scrollable window that display multi-colored text. Text may also be highlighted. - - Table: Scrollable display of tabular data. Table cells, rows, or columns may - also be highlighted. + - Table: A scrollable display of tabular data. Table cells, rows, or columns + may also be highlighted. + - TreeView: A scrollable display for hierarchical data. Tree nodes can be + highlighted, collapsed, expanded, and more. - List: A navigable text list with optional keyboard shortcuts. - InputField: One-line input fields to enter text. - DropDown: Drop-down selection fields. @@ -83,7 +85,7 @@ tag is as follows: [::] -Each of the three fields can be left blank and trailing fields can be ommitted. +Each of the three fields can be left blank and trailing fields can be omitted. (Empty square brackets "[]", however, are not considered color tags.) Colors that are not specified will be left unchanged. A field with just a dash ("-") means "reset to default". @@ -135,6 +137,27 @@ Unicode Support This package supports unicode characters including wide characters. +Concurrency + +Many functions in this package are not thread-safe. For many applications, this +may not be an issue: If your code makes changes in response to key events, it +will execute in the main goroutine and thus will not cause any race conditions. + +If you access your primitives from other goroutines, however, you will need to +synchronize execution. The easiest way to do this is to call +Application.QueueUpdate() or Application.QueueUpdateDraw() (see the function +documentation for details): + + go func() { + app.QueueUpdateDraw(func() { + table.SetCellSimple(0, 0, "Foo bar") + }) + }() + +One exception to this is the io.Writer interface implemented by TextView. You +can safely write to a TextView from any goroutine. See the TextView +documentation for details. + Type Hierarchy All widgets listed above contain the Box type. All of Box's functions are diff --git a/vendor/maunium.net/go/tview/dropdown.go b/vendor/maunium.net/go/tview/dropdown.go index 4e4d888..02c93bd 100644 --- a/vendor/maunium.net/go/tview/dropdown.go +++ b/vendor/maunium.net/go/tview/dropdown.go @@ -354,6 +354,7 @@ func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr // Hand control over to the list. d.open = true + optionBefore := d.currentOption d.list.SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) { // An option was selected. Close the list again. d.open = false @@ -374,6 +375,10 @@ func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr d.prefix = string(r[:len(r)-1]) } evalPrefix() + } else if event.Key() == tcell.KeyEscape { + d.open = false + d.currentOption = optionBefore + setFocus(d) } else { d.prefix = "" } diff --git a/vendor/maunium.net/go/tview/flex.go b/vendor/maunium.net/go/tview/flex.go index ad42d3a..bc1cfe1 100644 --- a/vendor/maunium.net/go/tview/flex.go +++ b/vendor/maunium.net/go/tview/flex.go @@ -28,7 +28,7 @@ type Flex struct { *Box // The items to be positioned. - items []flexItem + items []*flexItem // FlexRow or FlexColumn. direction int @@ -79,7 +79,7 @@ func (f *Flex) SetFullScreen(fullScreen bool) *Flex { // You can provide a nil value for the primitive. This will still consume screen // space but nothing will be drawn. func (f *Flex) AddItem(item Primitive, fixedSize, proportion int, focus bool) *Flex { - f.items = append(f.items, flexItem{Item: item, FixedSize: fixedSize, Proportion: proportion, Focus: focus}) + f.items = append(f.items, &flexItem{Item: item, FixedSize: fixedSize, Proportion: proportion, Focus: focus}) return f } @@ -94,6 +94,19 @@ func (f *Flex) RemoveItem(p Primitive) *Flex { return f } +// ResizeItem sets a new size for the item(s) with the given primitive. If there +// are multiple Flex items with the same primitive, they will all receive the +// same size. For details regarding the size parameters, see AddItem(). +func (f *Flex) ResizeItem(p Primitive, fixedSize, proportion int) *Flex { + for _, item := range f.items { + if item.Item == p { + item.FixedSize = fixedSize + item.Proportion = proportion + } + } + return f +} + // Draw draws this primitive onto the screen. func (f *Flex) Draw(screen tcell.Screen) { f.Box.Draw(screen) diff --git a/vendor/maunium.net/go/tview/form.go b/vendor/maunium.net/go/tview/form.go index fe0e980..e960a52 100644 --- a/vendor/maunium.net/go/tview/form.go +++ b/vendor/maunium.net/go/tview/form.go @@ -26,7 +26,7 @@ type FormItem interface { // required. GetFieldWidth() int - // SetEnteredFunc sets the handler function for when the user finished + // SetFinishedFunc sets the handler function for when the user finished // entering data into the item. The handler may receive events for the // Enter key (we're done), the Escape key (cancel input), the Tab key (move to // next field), and the Backtab key (move to previous field). @@ -218,6 +218,37 @@ func (f *Form) AddButton(label string, selected func()) *Form { return f } +// GetButton returns the button at the specified 0-based index. Note that +// buttons have been specially prepared for this form and modifying some of +// their attributes may have unintended side effects. +func (f *Form) GetButton(index int) *Button { + return f.buttons[index] +} + +// RemoveButton removes the button at the specified position, starting with 0 +// for the button that was added first. +func (f *Form) RemoveButton(index int) *Form { + f.buttons = append(f.buttons[:index], f.buttons[index+1:]...) + return f +} + +// GetButtonCount returns the number of buttons in this form. +func (f *Form) GetButtonCount() int { + return len(f.buttons) +} + +// GetButtonIndex returns the index of the button with the given label, starting +// with 0 for the button that was added first. If no such label was found, -1 +// is returned. +func (f *Form) GetButtonIndex(label string) int { + for index, button := range f.buttons { + if button.GetLabel() == label { + return index + } + } + return -1 +} + // Clear removes all input elements from the form, including the buttons if // specified. func (f *Form) Clear(includeButtons bool) *Form { @@ -251,6 +282,14 @@ func (f *Form) GetFormItem(index int) FormItem { return f.items[index] } +// RemoveFormItem removes the form element at the given position, starting with +// index 0. Elements are referenced in the order they were added. Buttons are +// not included. +func (f *Form) RemoveFormItem(index int) *Form { + f.items = append(f.items[:index], f.items[index+1:]...) + return f +} + // GetFormItemByLabel returns the first form element with the given label. If // no such element is found, nil is returned. Buttons are not searched and will // therefore not be returned. @@ -263,6 +302,18 @@ func (f *Form) GetFormItemByLabel(label string) FormItem { return nil } +// GetFormItemIndex returns the index of the first form element with the given +// label. If no such element is found, -1 is returned. Buttons are not searched +// and will therefore not be returned. +func (f *Form) GetFormItemIndex(label string) int { + for index, item := range f.items { + if item.GetLabel() == label { + return index + } + } + return -1 +} + // SetCancelFunc sets a handler which is called when the user hits the Escape // key. func (f *Form) SetCancelFunc(callback func()) *Form { diff --git a/vendor/maunium.net/go/tview/grid.go b/vendor/maunium.net/go/tview/grid.go index 77797ba..d8f6c97 100644 --- a/vendor/maunium.net/go/tview/grid.go +++ b/vendor/maunium.net/go/tview/grid.go @@ -583,11 +583,11 @@ func (g *Grid) Draw(screen tcell.Screen) { } by := item.y - 1 if by >= 0 && by < height { - PrintJoinedBorder(screen, x+bx, y+by, GraphicsHoriBar, g.bordersColor) + PrintJoinedSemigraphics(screen, x+bx, y+by, Borders.Horizontal, g.bordersColor) } by = item.y + item.h if by >= 0 && by < height { - PrintJoinedBorder(screen, x+bx, y+by, GraphicsHoriBar, g.bordersColor) + PrintJoinedSemigraphics(screen, x+bx, y+by, Borders.Horizontal, g.bordersColor) } } for by := item.y; by < item.y+item.h; by++ { // Left/right lines. @@ -596,28 +596,28 @@ func (g *Grid) Draw(screen tcell.Screen) { } bx := item.x - 1 if bx >= 0 && bx < width { - PrintJoinedBorder(screen, x+bx, y+by, GraphicsVertBar, g.bordersColor) + PrintJoinedSemigraphics(screen, x+bx, y+by, Borders.Vertical, g.bordersColor) } bx = item.x + item.w if bx >= 0 && bx < width { - PrintJoinedBorder(screen, x+bx, y+by, GraphicsVertBar, g.bordersColor) + PrintJoinedSemigraphics(screen, x+bx, y+by, Borders.Vertical, g.bordersColor) } } bx, by := item.x-1, item.y-1 // Top-left corner. if bx >= 0 && bx < width && by >= 0 && by < height { - PrintJoinedBorder(screen, x+bx, y+by, GraphicsTopLeftCorner, g.bordersColor) + PrintJoinedSemigraphics(screen, x+bx, y+by, Borders.TopLeft, g.bordersColor) } bx, by = item.x+item.w, item.y-1 // Top-right corner. if bx >= 0 && bx < width && by >= 0 && by < height { - PrintJoinedBorder(screen, x+bx, y+by, GraphicsTopRightCorner, g.bordersColor) + PrintJoinedSemigraphics(screen, x+bx, y+by, Borders.TopRight, g.bordersColor) } bx, by = item.x-1, item.y+item.h // Bottom-left corner. if bx >= 0 && bx < width && by >= 0 && by < height { - PrintJoinedBorder(screen, x+bx, y+by, GraphicsBottomLeftCorner, g.bordersColor) + PrintJoinedSemigraphics(screen, x+bx, y+by, Borders.BottomLeft, g.bordersColor) } bx, by = item.x+item.w, item.y+item.h // Bottom-right corner. if bx >= 0 && bx < width && by >= 0 && by < height { - PrintJoinedBorder(screen, x+bx, y+by, GraphicsBottomRightCorner, g.bordersColor) + PrintJoinedSemigraphics(screen, x+bx, y+by, Borders.BottomRight, g.bordersColor) } } } diff --git a/vendor/maunium.net/go/tview/inputfield.go b/vendor/maunium.net/go/tview/inputfield.go index 73224f2..ccc66e3 100644 --- a/vendor/maunium.net/go/tview/inputfield.go +++ b/vendor/maunium.net/go/tview/inputfield.go @@ -11,10 +11,23 @@ import ( ) // InputField is a one-line box (three lines if there is a title) where the -// user can enter text. +// user can enter text. Use SetAcceptanceFunc() to accept or reject input, +// SetChangedFunc() to listen for changes, and SetMaskCharacter() to hide input +// from onlookers (e.g. for password input). // -// Use SetMaskCharacter() to hide input from onlookers (e.g. for password -// input). +// The following keys can be used for navigation and editing: +// +// - Left arrow: Move left by one character. +// - Right arrow: Move right by one character. +// - Home, Ctrl-A, Alt-a: Move to the beginning of the line. +// - End, Ctrl-E, Alt-e: Move to the end of the line. +// - Alt-left, Alt-b: Move left by one word. +// - Alt-right, Alt-f: Move right by one word. +// - Backspace: Delete the character before the cursor. +// - Delete: Delete the character after the cursor. +// - Ctrl-K: Delete from the cursor to the end of the line. +// - Ctrl-W: Delete the last word before the cursor. +// - Ctrl-U: Delete the entire line. // // See https://github.com/rivo/tview/wiki/InputField for an example. type InputField struct { @@ -53,6 +66,12 @@ type InputField struct { // disables masking. maskCharacter rune + // The cursor position as a byte index into the text string. + cursorPos int + + // The number of bytes of the text string skipped ahead while drawing. + offset int + // An optional function which may reject the last character that was entered. accept func(text string, ch rune) bool @@ -83,6 +102,7 @@ func NewInputField() *InputField { // SetText sets the current text of the input field. func (i *InputField) SetText(text string) *InputField { i.text = text + i.cursorPos = len(text) if i.changed != nil { i.changed(text) } @@ -174,7 +194,7 @@ func (i *InputField) SetMaskCharacter(mask rune) *InputField { // SetAcceptanceFunc sets a handler which may reject the last character that was // entered (by returning false). // -// This package defines a number of variables Prefixed with InputField which may +// This package defines a number of variables prefixed with InputField which may // be used for common input (e.g. numbers, maximum text length). func (i *InputField) SetAcceptanceFunc(handler func(textToCheck string, lastChar rune) bool) *InputField { i.accept = handler @@ -244,54 +264,67 @@ func (i *InputField) Draw(screen tcell.Screen) { screen.SetContent(x+index, y, ' ', nil, fieldStyle) } - // Draw placeholder text. + // Text. + var cursorScreenPos int text := i.text if text == "" && i.placeholder != "" { - Print(screen, i.placeholder, x, y, fieldWidth, AlignLeft, i.placeholderTextColor) + // Draw placeholder text. + Print(screen, Escape(i.placeholder), x, y, fieldWidth, AlignLeft, i.placeholderTextColor) + i.offset = 0 } else { // Draw entered text. if i.maskCharacter > 0 { text = strings.Repeat(string(i.maskCharacter), utf8.RuneCountInString(i.text)) - } else { - text = Escape(text) } - fieldWidth-- // We need one cell for the cursor. - if fieldWidth < runewidth.StringWidth(text) { - Print(screen, text, x, y, fieldWidth, AlignRight, i.fieldTextColor) + stringWidth := runewidth.StringWidth(text) + if fieldWidth >= stringWidth { + // We have enough space for the full text. + Print(screen, Escape(text), x, y, fieldWidth, AlignLeft, i.fieldTextColor) + i.offset = 0 + iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { + if textPos >= i.cursorPos { + return true + } + cursorScreenPos += screenWidth + return false + }) } else { - Print(screen, text, x, y, fieldWidth, AlignLeft, i.fieldTextColor) + // The text doesn't fit. Where is the cursor? + if i.cursorPos < 0 { + i.cursorPos = 0 + } else if i.cursorPos > len(text) { + i.cursorPos = len(text) + } + // Shift the text so the cursor is inside the field. + var shiftLeft int + if i.offset > i.cursorPos { + i.offset = i.cursorPos + } else if subWidth := runewidth.StringWidth(text[i.offset:i.cursorPos]); subWidth > fieldWidth-1 { + shiftLeft = subWidth - fieldWidth + 1 + } + currentOffset := i.offset + iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { + if textPos >= currentOffset { + if shiftLeft > 0 { + i.offset = textPos + textWidth + shiftLeft -= screenWidth + } else { + if textPos+textWidth > i.cursorPos { + return true + } + cursorScreenPos += screenWidth + } + } + return false + }) + Print(screen, Escape(text[i.offset:]), x, y, fieldWidth, AlignLeft, i.fieldTextColor) } } // Set cursor. if i.focus.HasFocus() { - i.setCursor(screen) - } -} - -// setCursor sets the cursor position. -func (i *InputField) setCursor(screen tcell.Screen) { - x := i.x - y := i.y - rightLimit := x + i.width - if i.border { - x++ - y++ - rightLimit -= 2 - } - fieldWidth := runewidth.StringWidth(i.text) - if i.fieldWidth > 0 && fieldWidth > i.fieldWidth-1 { - fieldWidth = i.fieldWidth - 1 - } - if i.labelWidth > 0 { - x += i.labelWidth + fieldWidth - } else { - x += StringWidth(i.label) + fieldWidth - } - if x >= rightLimit { - x = rightLimit - 1 + screen.ShowCursor(x+cursorScreenPos, y) } - screen.ShowCursor(x, y) } // InputHandler returns the handler for this primitive. @@ -305,27 +338,101 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p } }() + // Movement functions. + home := func() { i.cursorPos = 0 } + end := func() { i.cursorPos = len(i.text) } + moveLeft := func() { + iterateStringReverse(i.text[:i.cursorPos], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { + i.cursorPos -= textWidth + return true + }) + } + moveRight := func() { + iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { + i.cursorPos += textWidth + return true + }) + } + moveWordLeft := func() { + i.cursorPos = len(regexp.MustCompile(`\S+\s*$`).ReplaceAllString(i.text[:i.cursorPos], "")) + } + moveWordRight := func() { + i.cursorPos = len(i.text) - len(regexp.MustCompile(`^\s*\S+\s*`).ReplaceAllString(i.text[i.cursorPos:], "")) + } + + // Add character function. Returns whether or not the rune character is + // accepted. + add := func(r rune) bool { + newText := i.text[:i.cursorPos] + string(r) + i.text[i.cursorPos:] + if i.accept != nil { + return i.accept(newText, r) + } + i.text = newText + i.cursorPos += len(string(r)) + return true + } + // Process key event. switch key := event.Key(); key { case tcell.KeyRune: // Regular character. - newText := i.text + string(event.Rune()) - if i.accept != nil { - if !i.accept(newText, event.Rune()) { + if event.Modifiers()&tcell.ModAlt > 0 { + // We accept some Alt- key combinations. + switch event.Rune() { + case 'a': // Home. + home() + case 'e': // End. + end() + case 'b': // Move word left. + moveWordLeft() + case 'f': // Move word right. + moveWordRight() + } + } else { + // Other keys are simply accepted as regular characters. + if !add(event.Rune()) { break } } - i.text = newText case tcell.KeyCtrlU: // Delete all. i.text = "" + i.cursorPos = 0 + case tcell.KeyCtrlK: // Delete until the end of the line. + i.text = i.text[:i.cursorPos] case tcell.KeyCtrlW: // Delete last word. - lastWord := regexp.MustCompile(`\s*\S+\s*$`) - i.text = lastWord.ReplaceAllString(i.text, "") - case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete last character. - if len(i.text) == 0 { - break + lastWord := regexp.MustCompile(`\S+\s*$`) + newText := lastWord.ReplaceAllString(i.text[:i.cursorPos], "") + i.text[i.cursorPos:] + i.cursorPos -= len(i.text) - len(newText) + i.text = newText + case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete character before the cursor. + iterateStringReverse(i.text[:i.cursorPos], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { + i.text = i.text[:textPos] + i.text[textPos+textWidth:] + i.cursorPos -= textWidth + return true + }) + if i.offset >= i.cursorPos { + i.offset = 0 + } + case tcell.KeyDelete: // Delete character after the cursor. + iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { + i.text = i.text[:i.cursorPos] + i.text[i.cursorPos+textWidth:] + return true + }) + case tcell.KeyLeft: + if event.Modifiers()&tcell.ModAlt > 0 { + moveWordLeft() + } else { + moveLeft() + } + case tcell.KeyRight: + if event.Modifiers()&tcell.ModAlt > 0 { + moveWordRight() + } else { + moveRight() } - runes := []rune(i.text) - i.text = string(runes[:len(runes)-1]) + case tcell.KeyHome, tcell.KeyCtrlA: + home() + case tcell.KeyEnd, tcell.KeyCtrlE: + end() case tcell.KeyEnter, tcell.KeyTab, tcell.KeyBacktab, tcell.KeyEscape: // We're done. if i.done != nil { i.done(key) diff --git a/vendor/maunium.net/go/tview/list.go b/vendor/maunium.net/go/tview/list.go index bc5be85..e8dc5dd 100644 --- a/vendor/maunium.net/go/tview/list.go +++ b/vendor/maunium.net/go/tview/list.go @@ -85,6 +85,19 @@ func (l *List) GetCurrentItem() int { return l.currentItem } +// RemoveItem removes the item with the given index (starting at 0) from the +// list. Does nothing if the index is out of range. +func (l *List) RemoveItem(index int) *List { + if index < 0 || index >= len(l.items) { + return l + } + l.items = append(l.items[:index], l.items[index+1:]...) + if l.currentItem >= len(l.items) { + l.currentItem = len(l.items) - 1 + } + return l +} + // SetMainTextColor sets the color of the items' main text. func (l *List) SetMainTextColor(color tcell.Color) *List { l.mainTextColor = color @@ -127,7 +140,7 @@ func (l *List) ShowSecondaryText(show bool) *List { // // This function is also called when the first item is added or when // SetCurrentItem() is called. -func (l *List) SetChangedFunc(handler func(int, string, string, rune)) *List { +func (l *List) SetChangedFunc(handler func(index int, mainText string, secondaryText string, shortcut rune)) *List { l.changed = handler return l } diff --git a/vendor/maunium.net/go/tview/modal.go b/vendor/maunium.net/go/tview/modal.go index f53a265..f5e92f1 100644 --- a/vendor/maunium.net/go/tview/modal.go +++ b/vendor/maunium.net/go/tview/modal.go @@ -40,6 +40,11 @@ func NewModal() *Modal { SetButtonBackgroundColor(Styles.PrimitiveBackgroundColor). SetButtonTextColor(Styles.PrimaryTextColor) m.form.SetBackgroundColor(Styles.ContrastBackgroundColor).SetBorderPadding(0, 0, 0, 0) + m.form.SetCancelFunc(func() { + if m.done != nil { + m.done(-1, "") + } + }) m.frame = NewFrame(m.form).SetBorders(0, 0, 1, 0, 0, 0) m.frame.SetBorder(true). SetBackgroundColor(Styles.ContrastBackgroundColor). @@ -81,6 +86,16 @@ func (m *Modal) AddButtons(labels []string) *Modal { m.done(i, l) } }) + button := m.form.GetButton(m.form.GetButtonCount() - 1) + button.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyDown, tcell.KeyRight: + return tcell.NewEventKey(tcell.KeyTab, 0, tcell.ModNone) + case tcell.KeyUp, tcell.KeyLeft: + return tcell.NewEventKey(tcell.KeyBacktab, 0, tcell.ModNone) + } + return event + }) }(index, label) } return m diff --git a/vendor/maunium.net/go/tview/semigraphics.go b/vendor/maunium.net/go/tview/semigraphics.go new file mode 100644 index 0000000..2455c87 --- /dev/null +++ b/vendor/maunium.net/go/tview/semigraphics.go @@ -0,0 +1,296 @@ +package tview + +import "maunium.net/go/tcell" + +// Semigraphics provides an easy way to access unicode characters for drawing. +// +// Named like the unicode characters, 'Semigraphics'-prefix used if unicode block +// isn't prefixed itself. +const ( + // Block: General Punctation U+2000-U+206F (http://unicode.org/charts/PDF/U2000.pdf) + SemigraphicsHorizontalEllipsis rune = '\u2026' // … + + // Block: Box Drawing U+2500-U+257F (http://unicode.org/charts/PDF/U2500.pdf) + BoxDrawingsLightHorizontal rune = '\u2500' // ─ + BoxDrawingsHeavyHorizontal rune = '\u2501' // ━ + BoxDrawingsLightVertical rune = '\u2502' // │ + BoxDrawingsHeavyVertical rune = '\u2503' // ┃ + BoxDrawingsLightTripleDashHorizontal rune = '\u2504' // ┄ + BoxDrawingsHeavyTripleDashHorizontal rune = '\u2505' // ┅ + BoxDrawingsLightTripleDashVertical rune = '\u2506' // ┆ + BoxDrawingsHeavyTripleDashVertical rune = '\u2507' // ┇ + BoxDrawingsLightQuadrupleDashHorizontal rune = '\u2508' // ┈ + BoxDrawingsHeavyQuadrupleDashHorizontal rune = '\u2509' // ┉ + BoxDrawingsLightQuadrupleDashVertical rune = '\u250a' // ┊ + BoxDrawingsHeavyQuadrupleDashVertical rune = '\u250b' // ┋ + BoxDrawingsLightDownAndRight rune = '\u250c' // ┌ + BoxDrawingsDownLighAndRightHeavy rune = '\u250d' // ┍ + BoxDrawingsDownHeavyAndRightLight rune = '\u250e' // ┎ + BoxDrawingsHeavyDownAndRight rune = '\u250f' // ┏ + BoxDrawingsLightDownAndLeft rune = '\u2510' // ┐ + BoxDrawingsDownLighAndLeftHeavy rune = '\u2511' // ┑ + BoxDrawingsDownHeavyAndLeftLight rune = '\u2512' // ┒ + BoxDrawingsHeavyDownAndLeft rune = '\u2513' // ┓ + BoxDrawingsLightUpAndRight rune = '\u2514' // └ + BoxDrawingsUpLightAndRightHeavy rune = '\u2515' // ┕ + BoxDrawingsUpHeavyAndRightLight rune = '\u2516' // ┖ + BoxDrawingsHeavyUpAndRight rune = '\u2517' // ┗ + BoxDrawingsLightUpAndLeft rune = '\u2518' // ┘ + BoxDrawingsUpLightAndLeftHeavy rune = '\u2519' // ┙ + BoxDrawingsUpHeavyAndLeftLight rune = '\u251a' // ┚ + BoxDrawingsHeavyUpAndLeft rune = '\u251b' // ┛ + BoxDrawingsLightVerticalAndRight rune = '\u251c' // ├ + BoxDrawingsVerticalLightAndRightHeavy rune = '\u251d' // ┝ + BoxDrawingsUpHeavyAndRightDownLight rune = '\u251e' // ┞ + BoxDrawingsDownHeacyAndRightUpLight rune = '\u251f' // ┟ + BoxDrawingsVerticalHeavyAndRightLight rune = '\u2520' // ┠ + BoxDrawingsDownLightAnbdRightUpHeavy rune = '\u2521' // ┡ + BoxDrawingsUpLightAndRightDownHeavy rune = '\u2522' // ┢ + BoxDrawingsHeavyVerticalAndRight rune = '\u2523' // ┣ + BoxDrawingsLightVerticalAndLeft rune = '\u2524' // ┤ + BoxDrawingsVerticalLightAndLeftHeavy rune = '\u2525' // ┥ + BoxDrawingsUpHeavyAndLeftDownLight rune = '\u2526' // ┦ + BoxDrawingsDownHeavyAndLeftUpLight rune = '\u2527' // ┧ + BoxDrawingsVerticalheavyAndLeftLight rune = '\u2528' // ┨ + BoxDrawingsDownLightAndLeftUpHeavy rune = '\u2529' // ┨ + BoxDrawingsUpLightAndLeftDownHeavy rune = '\u252a' // ┪ + BoxDrawingsHeavyVerticalAndLeft rune = '\u252b' // ┫ + BoxDrawingsLightDownAndHorizontal rune = '\u252c' // ┬ + BoxDrawingsLeftHeavyAndRightDownLight rune = '\u252d' // ┭ + BoxDrawingsRightHeavyAndLeftDownLight rune = '\u252e' // ┮ + BoxDrawingsDownLightAndHorizontalHeavy rune = '\u252f' // ┯ + BoxDrawingsDownHeavyAndHorizontalLight rune = '\u2530' // ┰ + BoxDrawingsRightLightAndLeftDownHeavy rune = '\u2531' // ┱ + BoxDrawingsLeftLightAndRightDownHeavy rune = '\u2532' // ┲ + BoxDrawingsHeavyDownAndHorizontal rune = '\u2533' // ┳ + BoxDrawingsLightUpAndHorizontal rune = '\u2534' // ┴ + BoxDrawingsLeftHeavyAndRightUpLight rune = '\u2535' // ┵ + BoxDrawingsRightHeavyAndLeftUpLight rune = '\u2536' // ┶ + BoxDrawingsUpLightAndHorizontalHeavy rune = '\u2537' // ┷ + BoxDrawingsUpHeavyAndHorizontalLight rune = '\u2538' // ┸ + BoxDrawingsRightLightAndLeftUpHeavy rune = '\u2539' // ┹ + BoxDrawingsLeftLightAndRightUpHeavy rune = '\u253a' // ┺ + BoxDrawingsHeavyUpAndHorizontal rune = '\u253b' // ┻ + BoxDrawingsLightVerticalAndHorizontal rune = '\u253c' // ┼ + BoxDrawingsLeftHeavyAndRightVerticalLight rune = '\u253d' // ┽ + BoxDrawingsRightHeavyAndLeftVerticalLight rune = '\u253e' // ┾ + BoxDrawingsVerticalLightAndHorizontalHeavy rune = '\u253f' // ┿ + BoxDrawingsUpHeavyAndDownHorizontalLight rune = '\u2540' // ╀ + BoxDrawingsDownHeavyAndUpHorizontalLight rune = '\u2541' // ╁ + BoxDrawingsVerticalHeavyAndHorizontalLight rune = '\u2542' // ╂ + BoxDrawingsLeftUpHeavyAndRightDownLight rune = '\u2543' // ╃ + BoxDrawingsRightUpHeavyAndLeftDownLight rune = '\u2544' // ╄ + BoxDrawingsLeftDownHeavyAndRightUpLight rune = '\u2545' // ╅ + BoxDrawingsRightDownHeavyAndLeftUpLight rune = '\u2546' // ╆ + BoxDrawingsDownLightAndUpHorizontalHeavy rune = '\u2547' // ╇ + BoxDrawingsUpLightAndDownHorizontalHeavy rune = '\u2548' // ╈ + BoxDrawingsRightLightAndLeftVerticalHeavy rune = '\u2549' // ╉ + BoxDrawingsLeftLightAndRightVerticalHeavy rune = '\u254a' // ╊ + BoxDrawingsHeavyVerticalAndHorizontal rune = '\u254b' // ╋ + BoxDrawingsLightDoubleDashHorizontal rune = '\u254c' // ╌ + BoxDrawingsHeavyDoubleDashHorizontal rune = '\u254d' // ╍ + BoxDrawingsLightDoubleDashVertical rune = '\u254e' // ╎ + BoxDrawingsHeavyDoubleDashVertical rune = '\u254f' // ╏ + BoxDrawingsDoubleHorizontal rune = '\u2550' // ═ + BoxDrawingsDoubleVertical rune = '\u2551' // ║ + BoxDrawingsDownSingleAndRightDouble rune = '\u2552' // ╒ + BoxDrawingsDownDoubleAndRightSingle rune = '\u2553' // ╓ + BoxDrawingsDoubleDownAndRight rune = '\u2554' // ╔ + BoxDrawingsDownSingleAndLeftDouble rune = '\u2555' // ╕ + BoxDrawingsDownDoubleAndLeftSingle rune = '\u2556' // ╖ + BoxDrawingsDoubleDownAndLeft rune = '\u2557' // ╗ + BoxDrawingsUpSingleAndRightDouble rune = '\u2558' // ╘ + BoxDrawingsUpDoubleAndRightSingle rune = '\u2559' // ╙ + BoxDrawingsDoubleUpAndRight rune = '\u255a' // ╚ + BoxDrawingsUpSingleAndLeftDouble rune = '\u255b' // ╛ + BoxDrawingsUpDobuleAndLeftSingle rune = '\u255c' // ╜ + BoxDrawingsDoubleUpAndLeft rune = '\u255d' // ╝ + BoxDrawingsVerticalSingleAndRightDouble rune = '\u255e' // ╞ + BoxDrawingsVerticalDoubleAndRightSingle rune = '\u255f' // ╟ + BoxDrawingsDoubleVerticalAndRight rune = '\u2560' // ╠ + BoxDrawingsVerticalSingleAndLeftDouble rune = '\u2561' // ╡ + BoxDrawingsVerticalDoubleAndLeftSingle rune = '\u2562' // ╢ + BoxDrawingsDoubleVerticalAndLeft rune = '\u2563' // ╣ + BoxDrawingsDownSingleAndHorizontalDouble rune = '\u2564' // ╤ + BoxDrawingsDownDoubleAndHorizontalSingle rune = '\u2565' // ╥ + BoxDrawingsDoubleDownAndHorizontal rune = '\u2566' // ╦ + BoxDrawingsUpSingleAndHorizontalDouble rune = '\u2567' // ╧ + BoxDrawingsUpDoubleAndHorizontalSingle rune = '\u2568' // ╨ + BoxDrawingsDoubleUpAndHorizontal rune = '\u2569' // ╩ + BoxDrawingsVerticalSingleAndHorizontalDouble rune = '\u256a' // ╪ + BoxDrawingsVerticalDoubleAndHorizontalSingle rune = '\u256b' // ╫ + BoxDrawingsDoubleVerticalAndHorizontal rune = '\u256c' // ╬ + BoxDrawingsLightArcDownAndRight rune = '\u256d' // ╭ + BoxDrawingsLightArcDownAndLeft rune = '\u256e' // ╮ + BoxDrawingsLightArcUpAndLeft rune = '\u256f' // ╯ + BoxDrawingsLightArcUpAndRight rune = '\u2570' // ╰ + BoxDrawingsLightDiagonalUpperRightToLowerLeft rune = '\u2571' // ╱ + BoxDrawingsLightDiagonalUpperLeftToLowerRight rune = '\u2572' // ╲ + BoxDrawingsLightDiagonalCross rune = '\u2573' // ╳ + BoxDrawingsLightLeft rune = '\u2574' // ╴ + BoxDrawingsLightUp rune = '\u2575' // ╵ + BoxDrawingsLightRight rune = '\u2576' // ╶ + BoxDrawingsLightDown rune = '\u2577' // ╷ + BoxDrawingsHeavyLeft rune = '\u2578' // ╸ + BoxDrawingsHeavyUp rune = '\u2579' // ╹ + BoxDrawingsHeavyRight rune = '\u257a' // ╺ + BoxDrawingsHeavyDown rune = '\u257b' // ╻ + BoxDrawingsLightLeftAndHeavyRight rune = '\u257c' // ╼ + BoxDrawingsLightUpAndHeavyDown rune = '\u257d' // ╽ + BoxDrawingsHeavyLeftAndLightRight rune = '\u257e' // ╾ + BoxDrawingsHeavyUpAndLightDown rune = '\u257f' // ╿ +) + +// SemigraphicJoints is a map for joining semigraphic (or otherwise) runes. +// So far only light lines are supported but if you want to change the border +// styling you need to provide the joints, too. +// The matching will be sorted ascending by rune value, so you don't need to +// provide all rune combinations, +// e.g. (─) + (│) = (┼) will also match (│) + (─) = (┼) +var SemigraphicJoints = map[string]rune{ + // (─) + (│) = (┼) + string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightVertical}): BoxDrawingsLightVerticalAndHorizontal, + // (─) + (┌) = (┬) + string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightDownAndRight}): BoxDrawingsLightDownAndHorizontal, + // (─) + (┐) = (┬) + string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightDownAndLeft}): BoxDrawingsLightDownAndHorizontal, + // (─) + (└) = (┴) + string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightUpAndRight}): BoxDrawingsLightUpAndHorizontal, + // (─) + (┘) = (┴) + string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightUpAndHorizontal, + // (─) + (├) = (┼) + string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndHorizontal, + // (─) + (┤) = (┼) + string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndHorizontal, + // (─) + (┬) = (┬) + string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightDownAndHorizontal, + // (─) + (┴) = (┴) + string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightUpAndHorizontal, + // (─) + (┼) = (┼) + string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, + + // (│) + (┌) = (├) + string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightDownAndRight}): BoxDrawingsLightVerticalAndRight, + // (│) + (┐) = (┤) + string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightDownAndLeft}): BoxDrawingsLightVerticalAndLeft, + // (│) + (└) = (├) + string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightUpAndRight}): BoxDrawingsLightVerticalAndRight, + // (│) + (┘) = (┤) + string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightVerticalAndLeft, + // (│) + (├) = (├) + string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndRight, + // (│) + (┤) = (┤) + string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndLeft, + // (│) + (┬) = (┼) + string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, + // (│) + (┴) = (┼) + string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, + // (│) + (┼) = (┼) + string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, + + // (┌) + (┐) = (┬) + string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightDownAndLeft}): BoxDrawingsLightDownAndHorizontal, + // (┌) + (└) = (├) + string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightUpAndRight}): BoxDrawingsLightVerticalAndRight, + // (┌) + (┘) = (┼) + string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightVerticalAndHorizontal, + // (┌) + (├) = (├) + string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndRight, + // (┌) + (┤) = (┼) + string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndHorizontal, + // (┌) + (┬) = (┬) + string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightDownAndHorizontal, + // (┌) + (┴) = (┼) + string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, + // (┌) + (┴) = (┼) + string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, + + // (┐) + (└) = (┼) + string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightUpAndRight}): BoxDrawingsLightVerticalAndHorizontal, + // (┐) + (┘) = (┤) + string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightVerticalAndLeft, + // (┐) + (├) = (┼) + string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndHorizontal, + // (┐) + (┤) = (┤) + string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndLeft, + // (┐) + (┬) = (┬) + string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightDownAndHorizontal, + // (┐) + (┴) = (┼) + string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, + // (┐) + (┼) = (┼) + string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, + + // (└) + (┘) = (┴) + string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightUpAndHorizontal, + // (└) + (├) = (├) + string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndRight, + // (└) + (┤) = (┼) + string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndHorizontal, + // (└) + (┬) = (┼) + string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, + // (└) + (┴) = (┴) + string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightUpAndHorizontal, + // (└) + (┼) = (┼) + string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, + + // (┘) + (├) = (┼) + string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndHorizontal, + // (┘) + (┤) = (┤) + string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndLeft, + // (┘) + (┬) = (┼) + string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, + // (┘) + (┴) = (┴) + string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightUpAndHorizontal, + // (┘) + (┼) = (┼) + string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, + + // (├) + (┤) = (┼) + string([]rune{BoxDrawingsLightVerticalAndRight, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndHorizontal, + // (├) + (┬) = (┼) + string([]rune{BoxDrawingsLightVerticalAndRight, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, + // (├) + (┴) = (┼) + string([]rune{BoxDrawingsLightVerticalAndRight, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, + // (├) + (┼) = (┼) + string([]rune{BoxDrawingsLightVerticalAndRight, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, + + // (┤) + (┬) = (┼) + string([]rune{BoxDrawingsLightVerticalAndLeft, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, + // (┤) + (┴) = (┼) + string([]rune{BoxDrawingsLightVerticalAndLeft, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, + // (┤) + (┼) = (┼) + string([]rune{BoxDrawingsLightVerticalAndLeft, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, + + // (┬) + (┴) = (┼) + string([]rune{BoxDrawingsLightDownAndHorizontal, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, + // (┬) + (┼) = (┼) + string([]rune{BoxDrawingsLightDownAndHorizontal, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, + + // (┴) + (┼) = (┼) + string([]rune{BoxDrawingsLightUpAndHorizontal, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, +} + +// PrintJoinedSemigraphics prints a semigraphics rune into the screen at the given +// position with the given color, joining it with any existing semigraphics +// rune. Background colors are preserved. At this point, only regular single +// line borders are supported. +func PrintJoinedSemigraphics(screen tcell.Screen, x, y int, ch rune, color tcell.Color) { + previous, _, style, _ := screen.GetContent(x, y) + style = style.Foreground(color) + + // What's the resulting rune? + var result rune + if ch == previous { + result = ch + } else { + if ch < previous { + previous, ch = ch, previous + } + result = SemigraphicJoints[string([]rune{previous, ch})] + } + if result == 0 { + result = ch + } + + // We only print something if we have something. + screen.SetContent(x, y, result, nil, style) +} diff --git a/vendor/maunium.net/go/tview/table.go b/vendor/maunium.net/go/tview/table.go index 2491ec7..6c636e7 100644 --- a/vendor/maunium.net/go/tview/table.go +++ b/vendor/maunium.net/go/tview/table.go @@ -231,6 +231,10 @@ type Table struct { // The number of visible rows the last time the table was drawn. visibleRows int + // The style of the selected rows. If this value is 0, selected rows are + // simply inverted. + selectedStyle tcell.Style + // An optional function which gets called when the user presses Enter on a // selected cell. If entire rows selected, the column value is undefined. // Likewise for entire columns. @@ -276,9 +280,21 @@ func (t *Table) SetBordersColor(color tcell.Color) *Table { return t } +// SetSelectedStyle sets a specific style for selected cells. If no such style +// is set, per default, selected cells are inverted (i.e. their foreground and +// background colors are swapped). +// +// To reset a previous setting to its default, make the following call: +// +// table.SetSelectedStyle(tcell.ColorDefault, tcell.ColorDefault, 0) +func (t *Table) SetSelectedStyle(foregroundColor, backgroundColor tcell.Color, attributes tcell.AttrMask) *Table { + t.selectedStyle = tcell.StyleDefault.Foreground(foregroundColor).Background(backgroundColor) | tcell.Style(attributes) + return t +} + // SetSeparator sets the character used to fill the space between two // neighboring cells. This is a space character ' ' per default but you may -// want to set it to GraphicsVertBar (or any other rune) if the column +// want to set it to Borders.Vertical (or any other rune) if the column // separation should be more visible. If cell borders are activated, this is // ignored. // @@ -373,7 +389,7 @@ func (t *Table) SetDoneFunc(handler func(key tcell.Key)) *Table { } // SetCell sets the content of a cell the specified position. It is ok to -// directly instantiate a TableCell object. If the cell has contain, at least +// directly instantiate a TableCell object. If the cell has content, at least // the Text and Color fields should be set. // // Note that setting cells in previously unknown rows and columns will @@ -406,7 +422,7 @@ func (t *Table) SetCellSimple(row, column int, text string) *Table { } // GetCell returns the contents of the cell at the specified position. A valid -// TableCell object is always returns but it will be uninitialized if the cell +// TableCell object is always returned but it will be uninitialized if the cell // was not previously set. func (t *Table) GetCell(row, column int) *TableCell { if row >= len(t.cells) || column >= len(t.cells[row]) { @@ -415,6 +431,31 @@ func (t *Table) GetCell(row, column int) *TableCell { return t.cells[row][column] } +// RemoveRow removes the row at the given position from the table. If there is +// no such row, this has no effect. +func (t *Table) RemoveRow(row int) *Table { + if row < 0 || row >= len(t.cells) { + return t + } + + t.cells = append(t.cells[:row], t.cells[row+1:]...) + + return t +} + +// RemoveColumn removes the column at the given position from the table. If +// there is no such column, this has no effect. +func (t *Table) RemoveColumn(column int) *Table { + for row := range t.cells { + if column < 0 || column >= len(t.cells[row]) { + continue + } + t.cells[row] = append(t.cells[row][:column], t.cells[row][column+1:]...) + } + + return t +} + // GetRowCount returns the number of rows in the table. func (t *Table) GetRowCount() int { return len(t.cells) @@ -644,7 +685,6 @@ ColumnLoop: } expWidth := toDistribute * expansion / expansionTotal widths[index] += expWidth - tableWidth += expWidth toDistribute -= expWidth expansionTotal -= expansion } @@ -668,24 +708,24 @@ ColumnLoop: // Draw borders. rowY *= 2 for pos := 0; pos < columnWidth && columnX+1+pos < width; pos++ { - drawBorder(columnX+pos+1, rowY, GraphicsHoriBar) + drawBorder(columnX+pos+1, rowY, Borders.Horizontal) } - ch := GraphicsCross + ch := Borders.Cross if columnIndex == 0 { if rowY == 0 { - ch = GraphicsTopLeftCorner + ch = Borders.TopLeft } else { - ch = GraphicsLeftT + ch = Borders.LeftT } } else if rowY == 0 { - ch = GraphicsTopT + ch = Borders.TopT } drawBorder(columnX, rowY, ch) rowY++ if rowY >= height { break // No space for the text anymore. } - drawBorder(columnX, rowY, GraphicsVertBar) + drawBorder(columnX, rowY, Borders.Vertical) } else if columnIndex > 0 { // Draw separator. drawBorder(columnX, rowY, t.separator) @@ -706,18 +746,18 @@ ColumnLoop: _, printed := printWithStyle(screen, cell.Text, x+columnX+1, y+rowY, finalWidth, cell.Align, tcell.StyleDefault.Foreground(cell.Color)|tcell.Style(cell.Attributes)) if StringWidth(cell.Text)-printed > 0 && printed > 0 { _, _, style, _ := screen.GetContent(x+columnX+1+finalWidth-1, y+rowY) - printWithStyle(screen, string(GraphicsEllipsis), x+columnX+1+finalWidth-1, y+rowY, 1, AlignLeft, style) + printWithStyle(screen, string(SemigraphicsHorizontalEllipsis), x+columnX+1+finalWidth-1, y+rowY, 1, AlignLeft, style) } } // Draw bottom border. if rowY := 2 * len(rows); t.borders && rowY < height { for pos := 0; pos < columnWidth && columnX+1+pos < width; pos++ { - drawBorder(columnX+pos+1, rowY, GraphicsHoriBar) + drawBorder(columnX+pos+1, rowY, Borders.Horizontal) } - ch := GraphicsBottomT + ch := Borders.BottomT if columnIndex == 0 { - ch = GraphicsBottomLeftCorner + ch = Borders.BottomLeft } drawBorder(columnX, rowY, ch) } @@ -730,26 +770,31 @@ ColumnLoop: for rowY := range rows { rowY *= 2 if rowY+1 < height { - drawBorder(columnX, rowY+1, GraphicsVertBar) + drawBorder(columnX, rowY+1, Borders.Vertical) } - ch := GraphicsRightT + ch := Borders.RightT if rowY == 0 { - ch = GraphicsTopRightCorner + ch = Borders.TopRight } drawBorder(columnX, rowY, ch) } if rowY := 2 * len(rows); rowY < height { - drawBorder(columnX, rowY, GraphicsBottomRightCorner) + drawBorder(columnX, rowY, Borders.BottomRight) } } // Helper function which colors the background of a box. - colorBackground := func(fromX, fromY, w, h int, backgroundColor, textColor tcell.Color, selected bool) { + // backgroundColor == tcell.ColorDefault => Don't color the background. + // textColor == tcell.ColorDefault => Don't change the text color. + // attr == 0 => Don't change attributes. + // invert == true => Ignore attr, set text to backgroundColor or t.backgroundColor; + // set background to textColor. + colorBackground := func(fromX, fromY, w, h int, backgroundColor, textColor tcell.Color, attr tcell.AttrMask, invert bool) { for by := 0; by < h && fromY+by < y+height; by++ { for bx := 0; bx < w && fromX+bx < x+width; bx++ { m, c, style, _ := screen.GetContent(fromX+bx, fromY+by) - if selected { - fg, _, _ := style.Decompose() + fg, bg, a := style.Decompose() + if invert { if fg == textColor || fg == t.bordersColor { fg = backgroundColor } @@ -758,10 +803,16 @@ ColumnLoop: } style = style.Background(textColor).Foreground(fg) } else { - if backgroundColor == tcell.ColorDefault { - continue + if backgroundColor != tcell.ColorDefault { + bg = backgroundColor } - style = style.Background(backgroundColor) + if textColor != tcell.ColorDefault { + fg = textColor + } + if attr != 0 { + a = attr + } + style = style.Background(bg).Foreground(fg) | tcell.Style(a) } screen.SetContent(fromX+bx, fromY+by, m, c, style) } @@ -770,11 +821,12 @@ ColumnLoop: // Color the cell backgrounds. To avoid undesirable artefacts, we combine // the drawing of a cell by background color, selected cells last. - cellsByBackgroundColor := make(map[tcell.Color][]*struct { + type cellInfo struct { x, y, w, h int text tcell.Color selected bool - }) + } + cellsByBackgroundColor := make(map[tcell.Color][]*cellInfo) var backgroundColors []tcell.Color for rowY, row := range rows { columnX := 0 @@ -794,11 +846,7 @@ ColumnLoop: columnSelected := t.columnsSelectable && !t.rowsSelectable && column == t.selectedColumn cellSelected := !cell.NotSelectable && (columnSelected || rowSelected || t.rowsSelectable && t.columnsSelectable && column == t.selectedColumn && row == t.selectedRow) entries, ok := cellsByBackgroundColor[cell.BackgroundColor] - cellsByBackgroundColor[cell.BackgroundColor] = append(entries, &struct { - x, y, w, h int - text tcell.Color - selected bool - }{ + cellsByBackgroundColor[cell.BackgroundColor] = append(entries, &cellInfo{ x: bx, y: by, w: bw, @@ -822,13 +870,18 @@ ColumnLoop: _, _, lj := c.Hcl() return li < lj }) + selFg, selBg, selAttr := t.selectedStyle.Decompose() for _, bgColor := range backgroundColors { entries := cellsByBackgroundColor[bgColor] for _, cell := range entries { if cell.selected { - defer colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, cell.text, true) + if t.selectedStyle != 0 { + defer colorBackground(cell.x, cell.y, cell.w, cell.h, selBg, selFg, selAttr, false) + } else { + defer colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, cell.text, 0, true) + } } else { - colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, cell.text, false) + colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, tcell.ColorDefault, 0, false) } } } diff --git a/vendor/maunium.net/go/tview/textview.go b/vendor/maunium.net/go/tview/textview.go index 44aeb1e..63d9796 100644 --- a/vendor/maunium.net/go/tview/textview.go +++ b/vendor/maunium.net/go/tview/textview.go @@ -31,7 +31,7 @@ type textViewIndex struct { // TextView is a box which displays text. It implements the io.Writer interface // so you can stream text to it. This does not trigger a redraw automatically // but if a handler is installed via SetChangedFunc(), you can cause it to be -// redrawn. +// redrawn. (See SetChangedFunc() for more details.) // // Navigation // @@ -103,6 +103,10 @@ type TextView struct { // during re-indexing. Set to -1 if there is no current highlight. fromHighlight, toHighlight int + // The screen space column of the highlight in its first line. Set to -1 if + // there is no current highlight. + posHighlight int + // A set of region IDs that are currently highlighted. highlights map[string]struct{} @@ -170,6 +174,7 @@ func NewTextView() *TextView { align: AlignLeft, wrap: true, textColor: Styles.PrimaryTextColor, + regions: false, dynamicColors: false, } } @@ -255,8 +260,20 @@ func (t *TextView) SetRegions(regions bool) *TextView { } // SetChangedFunc sets a handler function which is called when the text of the -// text view has changed. This is typically used to cause the application to -// redraw the screen. +// text view has changed. This is useful when text is written to this io.Writer +// in a separate goroutine. This does not automatically cause the screen to be +// refreshed so you may want to use the "changed" handler to redraw the screen. +// +// Note that to avoid race conditions or deadlocks, there are a few rules you +// should follow: +// +// - You can call Application.Draw() from this handler. +// - You can call TextView.HasFocus() from this handler. +// - During the execution of this handler, access to any other variables from +// this primitive or any other primitive should be queued using +// Application.QueueUpdate(). +// +// See package description for details on dealing with concurrency. func (t *TextView) SetChangedFunc(handler func()) *TextView { t.changed = handler return t @@ -270,6 +287,16 @@ func (t *TextView) SetDoneFunc(handler func(key tcell.Key)) *TextView { return t } +// ScrollTo scrolls to the specified row and column (both starting with 0). +func (t *TextView) ScrollTo(row, column int) *TextView { + if !t.scrollable { + return t + } + t.lineOffset = row + t.columnOffset = column + return t +} + // ScrollToBeginning scrolls to the top left corner of the text if the text view // is scrollable. func (t *TextView) ScrollToBeginning() *TextView { @@ -294,6 +321,12 @@ func (t *TextView) ScrollToEnd() *TextView { return t } +// GetScrollOffset returns the number of rows and columns that are skipped at +// the top left corner when the text view has been scrolled. +func (t *TextView) GetScrollOffset() (row, column int) { + return t.lineOffset, t.columnOffset +} + // Clear removes all text from the buffer. func (t *TextView) Clear() *TextView { t.buffer = nil @@ -420,13 +453,33 @@ func (t *TextView) GetRegionText(regionID string) string { return escapePattern.ReplaceAllString(buffer.String(), `[$1$2]`) } +// Focus is called when this primitive receives focus. +func (t *TextView) Focus(delegate func(p Primitive)) { + // Implemented here with locking because this is used by layout primitives. + t.Lock() + defer t.Unlock() + t.hasFocus = true +} + +// HasFocus returns whether or not this primitive has focus. +func (t *TextView) HasFocus() bool { + // Implemented here with locking because this may be used in the "changed" + // callback. + t.Lock() + defer t.Unlock() + return t.hasFocus +} + // Write lets us implement the io.Writer interface. Tab characters will be // replaced with TabSize space characters. A "\n" or "\r\n" will be interpreted // as a new line. func (t *TextView) Write(p []byte) (n int, err error) { // Notify at the end. - if t.changed != nil { - defer t.changed() + t.Lock() + changed := t.changed + t.Unlock() + if changed != nil { + defer changed() // Deadlocks may occur if we lock here. } t.Lock() @@ -492,7 +545,7 @@ func (t *TextView) reindexBuffer(width int) { return // Nothing has changed. We can still use the current index. } t.index = nil - t.fromHighlight, t.toHighlight = -1, -1 + t.fromHighlight, t.toHighlight, t.posHighlight = -1, -1, -1 // If there's no space, there's no index. if width < 1 { @@ -511,8 +564,9 @@ func (t *TextView) reindexBuffer(width int) { colorTags [][]string escapeIndices [][]int ) + strippedStr := str if t.dynamicColors { - colorTagIndices, colorTags, escapeIndices, str, _ = decomposeString(str) + colorTagIndices, colorTags, escapeIndices, strippedStr, _ = decomposeString(str) } // Find all regions in this line. Then remove them. @@ -523,14 +577,12 @@ func (t *TextView) reindexBuffer(width int) { if t.regions { regionIndices = regionPattern.FindAllStringIndex(str, -1) regions = regionPattern.FindAllStringSubmatch(str, -1) - str = regionPattern.ReplaceAllString(str, "") - if !t.dynamicColors { - // We haven't detected escape tags yet. Do it now. - escapeIndices = escapePattern.FindAllStringIndex(str, -1) - str = escapePattern.ReplaceAllString(str, "[$1$2]") - } + strippedStr = regionPattern.ReplaceAllString(strippedStr, "") } + // We don't need the original string anymore for now. + str = strippedStr + // Split the line if required. var splitLines []string if t.wrap && len(str) > 0 { @@ -574,15 +626,53 @@ func (t *TextView) reindexBuffer(width int) { // Shift original position with tags. lineLength := len(splitLine) + remainingLength := lineLength + tagEnd := originalPos + totalTagLength := 0 for { - if colorPos < len(colorTagIndices) && colorTagIndices[colorPos][0] <= originalPos+lineLength { + // Which tag comes next? + nextTag := make([][3]int, 0, 3) + if colorPos < len(colorTagIndices) { + nextTag = append(nextTag, [3]int{colorTagIndices[colorPos][0], colorTagIndices[colorPos][1], 0}) // 0 = color tag. + } + if regionPos < len(regionIndices) { + nextTag = append(nextTag, [3]int{regionIndices[regionPos][0], regionIndices[regionPos][1], 1}) // 1 = region tag. + } + if escapePos < len(escapeIndices) { + nextTag = append(nextTag, [3]int{escapeIndices[escapePos][0], escapeIndices[escapePos][1], 2}) // 2 = escape tag. + } + minPos := -1 + tagIndex := -1 + for index, pair := range nextTag { + if minPos < 0 || pair[0] < minPos { + minPos = pair[0] + tagIndex = index + } + } + + // Is the next tag in range? + if tagIndex < 0 || minPos >= tagEnd+remainingLength { + break // No. We're done with this line. + } + + // Advance. + strippedTagStart := nextTag[tagIndex][0] - originalPos - totalTagLength + tagEnd = nextTag[tagIndex][1] + tagLength := tagEnd - nextTag[tagIndex][0] + if nextTag[tagIndex][2] == 2 { + tagLength = 1 + } + totalTagLength += tagLength + remainingLength = lineLength - (tagEnd - originalPos - totalTagLength) + + // Process the tag. + switch nextTag[tagIndex][2] { + case 0: // Process color tags. - originalPos += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0] foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos]) colorPos++ - } else if regionPos < len(regionIndices) && regionIndices[regionPos][0] <= originalPos+lineLength { + case 1: // Process region tags. - originalPos += regionIndices[regionPos][1] - regionIndices[regionPos][0] regionID = regions[regionPos][1] _, highlighted = t.highlights[regionID] @@ -591,23 +681,21 @@ func (t *TextView) reindexBuffer(width int) { line := len(t.index) if t.fromHighlight < 0 { t.fromHighlight, t.toHighlight = line, line + t.posHighlight = runewidth.StringWidth(splitLine[:strippedTagStart]) } else if line > t.toHighlight { t.toHighlight = line } } regionPos++ - } else if escapePos < len(escapeIndices) && escapeIndices[escapePos][0] <= originalPos+lineLength { + case 2: // Process escape tags. - originalPos++ escapePos++ - } else { - break } } // Advance to next line. - originalPos += lineLength + originalPos += lineLength + totalTagLength // Append this line. line.NextPos = originalPos @@ -649,7 +737,7 @@ func (t *TextView) Draw(screen tcell.Screen) { t.pageSize = height // If the width has changed, we need to reindex. - if width != t.lastWidth { + if width != t.lastWidth && t.wrap { t.index = nil } t.lastWidth = width @@ -672,6 +760,16 @@ func (t *TextView) Draw(screen tcell.Screen) { // No, let's move to the start of the highlights. t.lineOffset = t.fromHighlight } + + // If the highlight is too far to the right, move it to the middle. + if t.posHighlight-t.columnOffset > 3*width/4 { + t.columnOffset = t.posHighlight - width/2 + } + + // If the highlight is off-screen on the left, move it on-screen. + if t.posHighlight-t.columnOffset < 0 { + t.columnOffset = t.posHighlight - width/4 + } } t.scrollToHighlights = false @@ -737,8 +835,9 @@ func (t *TextView) Draw(screen tcell.Screen) { colorTags [][]string escapeIndices [][]int ) + strippedText := text if t.dynamicColors { - colorTagIndices, colorTags, escapeIndices, _, _ = decomposeString(text) + colorTagIndices, colorTags, escapeIndices, strippedText, _ = decomposeString(text) } // Get regions. @@ -749,8 +848,10 @@ func (t *TextView) Draw(screen tcell.Screen) { if t.regions { regionIndices = regionPattern.FindAllStringIndex(text, -1) regions = regionPattern.FindAllStringSubmatch(text, -1) + strippedText = regionPattern.ReplaceAllString(strippedText, "") if !t.dynamicColors { escapeIndices = escapePattern.FindAllStringIndex(text, -1) + strippedText = string(escapePattern.ReplaceAllString(strippedText, "[$1$2]")) } } @@ -769,11 +870,29 @@ func (t *TextView) Draw(screen tcell.Screen) { } // Print the line. - var currentTag, currentRegion, currentEscapeTag, skipped, runeSeqWidth int - runeSequence := make([]rune, 0, 10) - flush := func() { - if len(runeSequence) == 0 { - return + var colorPos, regionPos, escapePos, tagOffset, skipped int + iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { + // Process tags. + for { + if colorPos < len(colorTags) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] { + // Get the color. + foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos]) + tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0] + colorPos++ + } else if regionPos < len(regionIndices) && textPos+tagOffset >= regionIndices[regionPos][0] && textPos+tagOffset < regionIndices[regionPos][1] { + // Get the region. + regionID = regions[regionPos][1] + tagOffset += regionIndices[regionPos][1] - regionIndices[regionPos][0] + regionPos++ + } else { + break + } + } + + // Skip the second-to-last character of an escape tag. + if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 { + tagOffset++ + escapePos++ } // Mix the existing style with the new style. @@ -803,87 +922,30 @@ func (t *TextView) Draw(screen tcell.Screen) { style = style.Background(fg).Foreground(bg) } - // Draw the character. - var comb []rune - if len(runeSequence) > 1 { - // Allocate space for the combining characters only when necessary. - comb = make([]rune, len(runeSequence)-1) - copy(comb, runeSequence[1:]) - } - for offset := 0; offset < runeSeqWidth; offset++ { - screen.SetContent(x+posX+offset, y+line-t.lineOffset, runeSequence[0], comb, style) - } - - // Advance. - posX += runeSeqWidth - runeSequence = runeSequence[:0] - runeSeqWidth = 0 - } - for pos, ch := range text { - // Get the color. - if currentTag < len(colorTags) && pos >= colorTagIndices[currentTag][0] && pos < colorTagIndices[currentTag][1] { - flush() - if pos == colorTagIndices[currentTag][1]-1 { - foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[currentTag]) - currentTag++ - } - continue - } - - // Get the region. - if currentRegion < len(regionIndices) && pos >= regionIndices[currentRegion][0] && pos < regionIndices[currentRegion][1] { - flush() - if pos == regionIndices[currentRegion][1]-1 { - regionID = regions[currentRegion][1] - currentRegion++ - } - continue - } - - // Skip the second-to-last character of an escape tag. - if currentEscapeTag < len(escapeIndices) && pos >= escapeIndices[currentEscapeTag][0] && pos < escapeIndices[currentEscapeTag][1] { - flush() - if pos == escapeIndices[currentEscapeTag][1]-1 { - currentEscapeTag++ - } else if pos == escapeIndices[currentEscapeTag][1]-2 { - continue - } - } - - // Determine the width of this rune. - chWidth := runewidth.RuneWidth(ch) - if chWidth == 0 { - // If this is not a modifier, we treat it as a space character. - if len(runeSequence) == 0 { - ch = ' ' - chWidth = 1 - } else { - runeSequence = append(runeSequence, ch) - continue - } - } - // Skip to the right. if !t.wrap && skipped < skip { - skipped += chWidth - continue + skipped += screenWidth + return false } // Stop at the right border. - if posX+runeSeqWidth+chWidth > width { - break + if posX+screenWidth > width { + return true } - // Flush the rune sequence. - flush() + // Draw the character. + for offset := screenWidth - 1; offset >= 0; offset-- { + if offset == 0 { + screen.SetContent(x+posX+offset, y+line-t.lineOffset, main, comb, style) + } else { + screen.SetContent(x+posX+offset, y+line-t.lineOffset, ' ', nil, style) + } + } - // Queue this rune. - runeSequence = append(runeSequence, ch) - runeSeqWidth += chWidth - } - if posX+runeSeqWidth <= width { - flush() - } + // Advance. + posX += screenWidth + return false + }) } // If this view is not scrollable, we'll purge the buffer of lines that have diff --git a/vendor/maunium.net/go/tview/treeview.go b/vendor/maunium.net/go/tview/treeview.go new file mode 100644 index 0000000..1b0af21 --- /dev/null +++ b/vendor/maunium.net/go/tview/treeview.go @@ -0,0 +1,684 @@ +package tview + +import ( + "maunium.net/go/tcell" +) + +// Tree navigation events. +const ( + treeNone int = iota + treeHome + treeEnd + treeUp + treeDown + treePageUp + treePageDown +) + +// TreeNode represents one node in a tree view. +type TreeNode struct { + // The reference object. + reference interface{} + + // This node's child nodes. + children []*TreeNode + + // The item's text. + text string + + // The text color. + color tcell.Color + + // Whether or not this node can be selected. + selectable bool + + // Whether or not this node's children should be displayed. + expanded bool + + // The additional horizontal indent of this node's text. + indent int + + // An optional function which is called when the user selects this node. + selected func() + + // Temporary member variables. + parent *TreeNode // The parent node (nil for the root). + level int // The hierarchy level (0 for the root, 1 for its children, and so on). + graphicsX int // The x-coordinate of the left-most graphics rune. + textX int // The x-coordinate of the first rune of the text. +} + +// NewTreeNode returns a new tree node. +func NewTreeNode(text string) *TreeNode { + return &TreeNode{ + text: text, + color: Styles.PrimaryTextColor, + indent: 2, + expanded: true, + selectable: true, + } +} + +// Walk traverses this node's subtree in depth-first, pre-order (NLR) order and +// calls the provided callback function on each traversed node (which includes +// this node) with the traversed node and its parent node (nil for this node). +// The callback returns whether traversal should continue with the traversed +// node's child nodes (true) or not recurse any deeper (false). +func (n *TreeNode) Walk(callback func(node, parent *TreeNode) bool) *TreeNode { + n.parent = nil + nodes := []*TreeNode{n} + for len(nodes) > 0 { + // Pop the top node and process it. + node := nodes[len(nodes)-1] + nodes = nodes[:len(nodes)-1] + if !callback(node, node.parent) { + // Don't add any children. + continue + } + + // Add children in reverse order. + for index := len(node.children) - 1; index >= 0; index-- { + node.children[index].parent = node + nodes = append(nodes, node.children[index]) + } + } + + return n +} + +// SetReference allows you to store a reference of any type in this node. This +// will allow you to establish a mapping between the TreeView hierarchy and your +// internal tree structure. +func (n *TreeNode) SetReference(reference interface{}) *TreeNode { + n.reference = reference + return n +} + +// GetReference returns this node's reference object. +func (n *TreeNode) GetReference() interface{} { + return n.reference +} + +// SetChildren sets this node's child nodes. +func (n *TreeNode) SetChildren(childNodes []*TreeNode) *TreeNode { + n.children = childNodes + return n +} + +// GetChildren returns this node's children. +func (n *TreeNode) GetChildren() []*TreeNode { + return n.children +} + +// ClearChildren removes all child nodes from this node. +func (n *TreeNode) ClearChildren() *TreeNode { + n.children = nil + return n +} + +// AddChild adds a new child node to this node. +func (n *TreeNode) AddChild(node *TreeNode) *TreeNode { + n.children = append(n.children, node) + return n +} + +// SetSelectable sets a flag indicating whether this node can be selected by +// the user. +func (n *TreeNode) SetSelectable(selectable bool) *TreeNode { + n.selectable = selectable + return n +} + +// SetSelectedFunc sets a function which is called when the user selects this +// node by hitting Enter when it is selected. +func (n *TreeNode) SetSelectedFunc(handler func()) *TreeNode { + n.selected = handler + return n +} + +// SetExpanded sets whether or not this node's child nodes should be displayed. +func (n *TreeNode) SetExpanded(expanded bool) *TreeNode { + n.expanded = expanded + return n +} + +// Expand makes the child nodes of this node appear. +func (n *TreeNode) Expand() *TreeNode { + n.expanded = true + return n +} + +// Collapse makes the child nodes of this node disappear. +func (n *TreeNode) Collapse() *TreeNode { + n.expanded = false + return n +} + +// ExpandAll expands this node and all descendent nodes. +func (n *TreeNode) ExpandAll() *TreeNode { + n.Walk(func(node, parent *TreeNode) bool { + node.expanded = true + return true + }) + return n +} + +// CollapseAll collapses this node and all descendent nodes. +func (n *TreeNode) CollapseAll() *TreeNode { + n.Walk(func(node, parent *TreeNode) bool { + n.expanded = false + return true + }) + return n +} + +// IsExpanded returns whether the child nodes of this node are visible. +func (n *TreeNode) IsExpanded() bool { + return n.expanded +} + +// SetText sets the node's text which is displayed. +func (n *TreeNode) SetText(text string) *TreeNode { + n.text = text + return n +} + +// SetColor sets the node's text color. +func (n *TreeNode) SetColor(color tcell.Color) *TreeNode { + n.color = color + return n +} + +// SetIndent sets an additional indentation for this node's text. A value of 0 +// keeps the text as far left as possible with a minimum of line graphics. Any +// value greater than that moves the text to the right. +func (n *TreeNode) SetIndent(indent int) *TreeNode { + n.indent = indent + return n +} + +// TreeView displays tree structures. A tree consists of nodes (TreeNode +// objects) where each node has zero or more child nodes and exactly one parent +// node (except for the root node which has no parent node). +// +// The SetRoot() function is used to specify the root of the tree. Other nodes +// are added locally to the root node or any of its descendents. See the +// TreeNode documentation for details on node attributes. (You can use +// SetReference() to store a reference to nodes of your own tree structure.) +// +// Nodes can be selected by calling SetCurrentNode(). The user can navigate the +// selection or the tree by using the following keys: +// +// - j, down arrow, right arrow: Move (the selection) down by one node. +// - k, up arrow, left arrow: Move (the selection) up by one node. +// - g, home: Move (the selection) to the top. +// - G, end: Move (the selection) to the bottom. +// - Ctrl-F, page down: Move (the selection) down by one page. +// - Ctrl-B, page up: Move (the selection) up by one page. +// +// Selected nodes can trigger the "selected" callback when the user hits Enter. +// +// The root node corresponds to level 0, its children correspond to level 1, +// their children to level 2, and so on. Per default, the first level that is +// displayed is 0, i.e. the root node. You can call SetTopLevel() to hide +// levels. +// +// If graphics are turned on (see SetGraphics()), lines indicate the tree's +// hierarchy. Alternative (or additionally), you can set different prefixes +// using SetPrefixes() for different levels, for example to display hierarchical +// bullet point lists. +// +// See https://github.com/rivo/tview/wiki/TreeView for an example. +type TreeView struct { + *Box + + // The root node. + root *TreeNode + + // The currently selected node or nil if no node is selected. + currentNode *TreeNode + + // The movement to be performed during the call to Draw(), one of the + // constants defined above. + movement int + + // The top hierarchical level shown. (0 corresponds to the root level.) + topLevel int + + // Strings drawn before the nodes, based on their level. + prefixes []string + + // Vertical scroll offset. + offsetY int + + // If set to true, all node texts will be aligned horizontally. + align bool + + // If set to true, the tree structure is drawn using lines. + graphics bool + + // The color of the lines. + graphicsColor tcell.Color + + // An optional function which is called when the user has navigated to a new + // tree node. + changed func(node *TreeNode) + + // An optional function which is called when a tree item was selected. + selected func(node *TreeNode) + + // The visible nodes, top-down, as set by process(). + nodes []*TreeNode +} + +// NewTreeView returns a new tree view. +func NewTreeView() *TreeView { + return &TreeView{ + Box: NewBox(), + graphics: true, + graphicsColor: Styles.GraphicsColor, + } +} + +// SetRoot sets the root node of the tree. +func (t *TreeView) SetRoot(root *TreeNode) *TreeView { + t.root = root + return t +} + +// GetRoot returns the root node of the tree. If no such node was previously +// set, nil is returned. +func (t *TreeView) GetRoot() *TreeNode { + return t.root +} + +// SetCurrentNode sets the currently selected node. Provide nil to clear all +// selections. Selected nodes must be visible and selectable, or else the +// selection will be changed to the top-most selectable and visible node. +// +// This function does NOT trigger the "changed" callback. +func (t *TreeView) SetCurrentNode(node *TreeNode) *TreeView { + t.currentNode = node + return t +} + +// GetCurrentNode returns the currently selected node or nil of no node is +// currently selected. +func (t *TreeView) GetCurrentNode() *TreeNode { + return t.currentNode +} + +// SetTopLevel sets the first tree level that is visible with 0 referring to the +// root, 1 to the root's child nodes, and so on. Nodes above the top level are +// not displayed. +func (t *TreeView) SetTopLevel(topLevel int) *TreeView { + t.topLevel = topLevel + return t +} + +// SetPrefixes defines the strings drawn before the nodes' texts. This is a +// slice of strings where each element corresponds to a node's hierarchy level, +// i.e. 0 for the root, 1 for the root's children, and so on (levels will +// cycle). +// +// For example, to display a hierarchical list with bullet points: +// +// treeView.SetGraphics(false). +// SetPrefixes([]string{"* ", "- ", "x "}) +func (t *TreeView) SetPrefixes(prefixes []string) *TreeView { + t.prefixes = prefixes + return t +} + +// SetAlign controls the horizontal alignment of the node texts. If set to true, +// all texts except that of top-level nodes will be placed in the same column. +// If set to false, they will indent with the hierarchy. +func (t *TreeView) SetAlign(align bool) *TreeView { + t.align = align + return t +} + +// SetGraphics sets a flag which determines whether or not line graphics are +// drawn to illustrate the tree's hierarchy. +func (t *TreeView) SetGraphics(showGraphics bool) *TreeView { + t.graphics = showGraphics + return t +} + +// SetGraphicsColor sets the colors of the lines used to draw the tree structure. +func (t *TreeView) SetGraphicsColor(color tcell.Color) *TreeView { + t.graphicsColor = color + return t +} + +// SetChangedFunc sets the function which is called when the user navigates to +// a new tree node. +func (t *TreeView) SetChangedFunc(handler func(node *TreeNode)) *TreeView { + t.changed = handler + return t +} + +// SetSelectedFunc sets the function which is called when the user selects a +// node by pressing Enter on the current selection. +func (t *TreeView) SetSelectedFunc(handler func(node *TreeNode)) *TreeView { + t.selected = handler + return t +} + +// process builds the visible tree, populates the "nodes" slice, and processes +// pending selection actions. +func (t *TreeView) process() { + _, _, _, height := t.GetInnerRect() + + // Determine visible nodes and their placement. + var graphicsOffset, maxTextX int + t.nodes = nil + selectedIndex := -1 + topLevelGraphicsX := -1 + if t.graphics { + graphicsOffset = 1 + } + t.root.Walk(func(node, parent *TreeNode) bool { + // Set node attributes. + node.parent = parent + if parent == nil { + node.level = 0 + node.graphicsX = 0 + node.textX = 0 + } else { + node.level = parent.level + 1 + node.graphicsX = parent.textX + node.textX = node.graphicsX + graphicsOffset + node.indent + } + if !t.graphics && t.align { + // Without graphics, we align nodes on the first column. + node.textX = 0 + } + if node.level == t.topLevel { + // No graphics for top level nodes. + node.graphicsX = 0 + node.textX = 0 + } + if node.textX > maxTextX { + maxTextX = node.textX + } + if node == t.currentNode && node.selectable { + selectedIndex = len(t.nodes) + } + + // Maybe we want to skip this level. + if t.topLevel == node.level && (topLevelGraphicsX < 0 || node.graphicsX < topLevelGraphicsX) { + topLevelGraphicsX = node.graphicsX + } + + // Add and recurse (if desired). + if node.level >= t.topLevel { + t.nodes = append(t.nodes, node) + } + return node.expanded + }) + + // Post-process positions. + for _, node := range t.nodes { + // If text must align, we correct the positions. + if t.align && node.level > t.topLevel { + node.textX = maxTextX + } + + // If we skipped levels, shift to the left. + if topLevelGraphicsX > 0 { + node.graphicsX -= topLevelGraphicsX + node.textX -= topLevelGraphicsX + } + } + + // Process selection. (Also trigger events if necessary.) + if selectedIndex >= 0 { + // Move the selection. + newSelectedIndex := selectedIndex + MovementSwitch: + switch t.movement { + case treeUp: + for newSelectedIndex > 0 { + newSelectedIndex-- + if t.nodes[newSelectedIndex].selectable { + break MovementSwitch + } + } + newSelectedIndex = selectedIndex + case treeDown: + for newSelectedIndex < len(t.nodes)-1 { + newSelectedIndex++ + if t.nodes[newSelectedIndex].selectable { + break MovementSwitch + } + } + newSelectedIndex = selectedIndex + case treeHome: + for newSelectedIndex = 0; newSelectedIndex < len(t.nodes); newSelectedIndex++ { + if t.nodes[newSelectedIndex].selectable { + break MovementSwitch + } + } + newSelectedIndex = selectedIndex + case treeEnd: + for newSelectedIndex = len(t.nodes) - 1; newSelectedIndex >= 0; newSelectedIndex-- { + if t.nodes[newSelectedIndex].selectable { + break MovementSwitch + } + } + newSelectedIndex = selectedIndex + case treePageUp: + if newSelectedIndex+height < len(t.nodes) { + newSelectedIndex += height + } else { + newSelectedIndex = len(t.nodes) - 1 + } + for ; newSelectedIndex < len(t.nodes); newSelectedIndex++ { + if t.nodes[newSelectedIndex].selectable { + break MovementSwitch + } + } + newSelectedIndex = selectedIndex + case treePageDown: + if newSelectedIndex >= height { + newSelectedIndex -= height + } else { + newSelectedIndex = 0 + } + for ; newSelectedIndex >= 0; newSelectedIndex-- { + if t.nodes[newSelectedIndex].selectable { + break MovementSwitch + } + } + newSelectedIndex = selectedIndex + } + t.currentNode = t.nodes[newSelectedIndex] + if newSelectedIndex != selectedIndex { + t.movement = treeNone + if t.changed != nil { + t.changed(t.currentNode) + } + } + selectedIndex = newSelectedIndex + + // Move selection into viewport. + if selectedIndex-t.offsetY >= height { + t.offsetY = selectedIndex - height + 1 + } + if selectedIndex < t.offsetY { + t.offsetY = selectedIndex + } + } else { + // If selection is not visible or selectable, select the first candidate. + if t.currentNode != nil { + for index, node := range t.nodes { + if node.selectable { + selectedIndex = index + t.currentNode = node + break + } + } + } + if selectedIndex < 0 { + t.currentNode = nil + } + } +} + +// Draw draws this primitive onto the screen. +func (t *TreeView) Draw(screen tcell.Screen) { + t.Box.Draw(screen) + if t.root == nil { + return + } + + // Build the tree if necessary. + if t.nodes == nil { + t.process() + } + defer func() { + t.nodes = nil // Rebuild during next call to Draw() + }() + + // Scroll the tree. + x, y, width, height := t.GetInnerRect() + switch t.movement { + case treeUp: + t.offsetY-- + case treeDown: + t.offsetY++ + case treeHome: + t.offsetY = 0 + case treeEnd: + t.offsetY = len(t.nodes) + case treePageUp: + t.offsetY -= height + case treePageDown: + t.offsetY += height + } + t.movement = treeNone + + // Fix invalid offsets. + if t.offsetY >= len(t.nodes)-height { + t.offsetY = len(t.nodes) - height + } + if t.offsetY < 0 { + t.offsetY = 0 + } + + // Draw the tree. + posY := y + lineStyle := tcell.StyleDefault.Background(t.backgroundColor).Foreground(t.graphicsColor) + for index, node := range t.nodes { + // Skip invisible parts. + if posY >= y+height+1 { + break + } + if index < t.offsetY { + continue + } + + // Draw the graphics. + if t.graphics { + // Draw ancestor branches. + ancestor := node.parent + for ancestor != nil && ancestor.parent != nil && ancestor.parent.level >= t.topLevel { + if ancestor.graphicsX >= width { + continue + } + + // Draw a branch if this ancestor is not a last child. + if ancestor.parent.children[len(ancestor.parent.children)-1] != ancestor { + if posY-1 >= y && ancestor.textX > ancestor.graphicsX { + PrintJoinedSemigraphics(screen, x+ancestor.graphicsX, posY-1, Borders.Vertical, t.graphicsColor) + } + if posY < y+height { + screen.SetContent(x+ancestor.graphicsX, posY, Borders.Vertical, nil, lineStyle) + } + } + ancestor = ancestor.parent + } + + if node.textX > node.graphicsX && node.graphicsX < width { + // Connect to the node above. + if posY-1 >= y && t.nodes[index-1].graphicsX <= node.graphicsX && t.nodes[index-1].textX > node.graphicsX { + PrintJoinedSemigraphics(screen, x+node.graphicsX, posY-1, Borders.TopLeft, t.graphicsColor) + } + + // Join this node. + if posY < y+height { + screen.SetContent(x+node.graphicsX, posY, Borders.BottomLeft, nil, lineStyle) + for pos := node.graphicsX + 1; pos < node.textX && pos < width; pos++ { + screen.SetContent(x+pos, posY, Borders.Horizontal, nil, lineStyle) + } + } + } + } + + // Draw the prefix and the text. + if node.textX < width && posY < y+height { + // Prefix. + var prefixWidth int + if len(t.prefixes) > 0 { + _, prefixWidth = Print(screen, t.prefixes[(node.level-t.topLevel)%len(t.prefixes)], x+node.textX, posY, width-node.textX, AlignLeft, node.color) + } + + // Text. + if node.textX+prefixWidth < width { + style := tcell.StyleDefault.Foreground(node.color) + if node == t.currentNode { + style = tcell.StyleDefault.Background(node.color).Foreground(t.backgroundColor) + } + printWithStyle(screen, node.text, x+node.textX+prefixWidth, posY, width-node.textX-prefixWidth, AlignLeft, style) + } + } + + // Advance. + posY++ + } +} + +// InputHandler returns the handler for this primitive. +func (t *TreeView) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { + return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { + // Because the tree is flattened into a list only at drawing time, we also + // postpone the (selection) movement to drawing time. + switch key := event.Key(); key { + case tcell.KeyTab, tcell.KeyDown, tcell.KeyRight: + t.movement = treeDown + case tcell.KeyBacktab, tcell.KeyUp, tcell.KeyLeft: + t.movement = treeUp + case tcell.KeyHome: + t.movement = treeHome + case tcell.KeyEnd: + t.movement = treeEnd + case tcell.KeyPgDn, tcell.KeyCtrlF: + t.movement = treePageDown + case tcell.KeyPgUp, tcell.KeyCtrlB: + t.movement = treePageUp + case tcell.KeyRune: + switch event.Rune() { + case 'g': + t.movement = treeHome + case 'G': + t.movement = treeEnd + case 'j': + t.movement = treeDown + case 'k': + t.movement = treeUp + } + case tcell.KeyEnter: + if t.currentNode != nil { + if t.selected != nil { + t.selected(t.currentNode) + } + if t.currentNode.selected != nil { + t.currentNode.selected() + } + } + } + + t.process() + }) +} diff --git a/vendor/maunium.net/go/tview/util.go b/vendor/maunium.net/go/tview/util.go index 41e52dd..e408b18 100644 --- a/vendor/maunium.net/go/tview/util.go +++ b/vendor/maunium.net/go/tview/util.go @@ -1,11 +1,9 @@ package tview import ( - "fmt" "math" "regexp" "strconv" - "strings" "unicode" "maunium.net/go/tcell" @@ -19,97 +17,13 @@ const ( AlignRight ) -// Semigraphical runes. -const ( - GraphicsHoriBar = '\u2500' - GraphicsVertBar = '\u2502' - GraphicsTopLeftCorner = '\u250c' - GraphicsTopRightCorner = '\u2510' - GraphicsBottomLeftCorner = '\u2514' - GraphicsBottomRightCorner = '\u2518' - GraphicsLeftT = '\u251c' - GraphicsRightT = '\u2524' - GraphicsTopT = '\u252c' - GraphicsBottomT = '\u2534' - GraphicsCross = '\u253c' - GraphicsDbVertBar = '\u2550' - GraphicsDbHorBar = '\u2551' - GraphicsDbTopLeftCorner = '\u2554' - GraphicsDbTopRightCorner = '\u2557' - GraphicsDbBottomRightCorner = '\u255d' - GraphicsDbBottomLeftCorner = '\u255a' - GraphicsEllipsis = '\u2026' -) - -// joints maps combinations of two graphical runes to the rune that results -// when joining the two in the same screen cell. The keys of this map are -// two-rune strings where the value of the first rune is lower than the value -// of the second rune. Identical runes are not contained. -var joints = map[string]rune{ - "\u2500\u2502": GraphicsCross, - "\u2500\u250c": GraphicsTopT, - "\u2500\u2510": GraphicsTopT, - "\u2500\u2514": GraphicsBottomT, - "\u2500\u2518": GraphicsBottomT, - "\u2500\u251c": GraphicsCross, - "\u2500\u2524": GraphicsCross, - "\u2500\u252c": GraphicsTopT, - "\u2500\u2534": GraphicsBottomT, - "\u2500\u253c": GraphicsCross, - "\u2502\u250c": GraphicsLeftT, - "\u2502\u2510": GraphicsRightT, - "\u2502\u2514": GraphicsLeftT, - "\u2502\u2518": GraphicsRightT, - "\u2502\u251c": GraphicsLeftT, - "\u2502\u2524": GraphicsRightT, - "\u2502\u252c": GraphicsCross, - "\u2502\u2534": GraphicsCross, - "\u2502\u253c": GraphicsCross, - "\u250c\u2510": GraphicsTopT, - "\u250c\u2514": GraphicsLeftT, - "\u250c\u2518": GraphicsCross, - "\u250c\u251c": GraphicsLeftT, - "\u250c\u2524": GraphicsCross, - "\u250c\u252c": GraphicsTopT, - "\u250c\u2534": GraphicsCross, - "\u250c\u253c": GraphicsCross, - "\u2510\u2514": GraphicsCross, - "\u2510\u2518": GraphicsRightT, - "\u2510\u251c": GraphicsCross, - "\u2510\u2524": GraphicsRightT, - "\u2510\u252c": GraphicsTopT, - "\u2510\u2534": GraphicsCross, - "\u2510\u253c": GraphicsCross, - "\u2514\u2518": GraphicsBottomT, - "\u2514\u251c": GraphicsLeftT, - "\u2514\u2524": GraphicsCross, - "\u2514\u252c": GraphicsCross, - "\u2514\u2534": GraphicsBottomT, - "\u2514\u253c": GraphicsCross, - "\u2518\u251c": GraphicsCross, - "\u2518\u2524": GraphicsRightT, - "\u2518\u252c": GraphicsCross, - "\u2518\u2534": GraphicsBottomT, - "\u2518\u253c": GraphicsCross, - "\u251c\u2524": GraphicsCross, - "\u251c\u252c": GraphicsCross, - "\u251c\u2534": GraphicsCross, - "\u251c\u253c": GraphicsCross, - "\u2524\u252c": GraphicsCross, - "\u2524\u2534": GraphicsCross, - "\u2524\u253c": GraphicsCross, - "\u252c\u2534": GraphicsCross, - "\u252c\u253c": GraphicsCross, - "\u2534\u253c": GraphicsCross, -} - // Common regular expressions. var ( colorPattern = regexp.MustCompile(`\[([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([lbdru]+|\-)?)?)?\]`) regionPattern = regexp.MustCompile(`\["([a-zA-Z0-9_,;: \-\.]*)"\]`) escapePattern = regexp.MustCompile(`\[([a-zA-Z0-9_,;: \-\."#]+)\[(\[*)\]`) nonEscapePattern = regexp.MustCompile(`(\[[a-zA-Z0-9_,;: \-\."#]+\[*)\]`) - boundaryPattern = regexp.MustCompile("([[:punct:]]\\s*|\\s+)") + boundaryPattern = regexp.MustCompile(`(([[:punct:]]|\n)[ \t\f\r]*|(\s+))`) spacePattern = regexp.MustCompile(`\s+`) ) @@ -204,13 +118,12 @@ func overlayStyle(background tcell.Color, defaultStyle tcell.Style, fgColor, bgC defFg, defBg, defAttr := defaultStyle.Decompose() style := defaultStyle.Background(background) - if fgColor == "-" { - style = style.Foreground(defFg) - } else if fgColor != "" { + style = style.Foreground(defFg) + if fgColor != "" { style = style.Foreground(tcell.GetColor(fgColor)) } - if bgColor == "-" { + if bgColor == "-" || bgColor == "" && defBg != tcell.ColorDefault { style = style.Background(defBg) } else if bgColor != "" { style = style.Background(tcell.GetColor(bgColor)) @@ -288,8 +201,8 @@ func decomposeString(text string) (colorIndices [][]int, colors [][]string, esca // You can change the colors and text styles mid-text by inserting a color tag. // See the package description for details. // -// Returns the number of actual runes printed (not including color tags) and the -// actual width used for the printed runes. +// Returns the number of actual bytes of the text printed (including color tags) +// and the actual width used for the printed runes. func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tcell.Color) (int, int) { return printWithStyle(screen, text, x, y, maxWidth, align, tcell.StyleDefault.Foreground(color)) } @@ -302,186 +215,160 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, } // Decompose the text. - colorIndices, colors, escapeIndices, strippedText, _ := decomposeString(text) + colorIndices, colors, escapeIndices, strippedText, strippedWidth := decomposeString(text) - // We deal with runes, not with bytes. - runes := []rune(strippedText) - - // This helper function takes positions for a substring of "runes" and returns - // a new string corresponding to this substring, making sure printing that - // substring will observe color tags. - substring := func(from, to int) string { + // We want to reduce all alignments to AlignLeft. + if align == AlignRight { + if strippedWidth <= maxWidth { + // There's enough space for the entire text. + return printWithStyle(screen, text, x+maxWidth-strippedWidth, y, maxWidth, AlignLeft, style) + } + // Trim characters off the beginning. var ( - colorPos, escapePos, runePos, startPos int + bytes, width, colorPos, escapePos, tagOffset int foregroundColor, backgroundColor, attributes string ) - if from >= len(runes) { - return "" - } - for pos := range text { - // Handle color tags. - if colorPos < len(colorIndices) && pos >= colorIndices[colorPos][0] && pos < colorIndices[colorPos][1] { - if pos == colorIndices[colorPos][1]-1 { - if runePos <= from { - foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos]) - } - colorPos++ - } - continue - } - - // Handle escape tags. - if escapePos < len(escapeIndices) && pos >= escapeIndices[escapePos][0] && pos < escapeIndices[escapePos][1] { - if pos == escapeIndices[escapePos][1]-1 { - escapePos++ - } else if pos == escapeIndices[escapePos][1]-2 { - continue - } + _, originalBackground, _ := style.Decompose() + iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { + // Update color/escape tag offset and style. + if colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] { + foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos]) + style = overlayStyle(originalBackground, style, foregroundColor, backgroundColor, attributes) + tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0] + colorPos++ } - - // Check boundaries. - if runePos == from { - startPos = pos - } else if runePos >= to { - return fmt.Sprintf(`[%s:%s:%s]%s`, foregroundColor, backgroundColor, attributes, text[startPos:pos]) + if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] { + tagOffset++ + escapePos++ } - - runePos++ - } - - return fmt.Sprintf(`[%s:%s:%s]%s`, foregroundColor, backgroundColor, attributes, text[startPos:]) - } - - // We want to reduce everything to AlignLeft. - if align == AlignRight { - width := 0 - start := len(runes) - for index := start - 1; index >= 0; index-- { - w := runewidth.RuneWidth(runes[index]) - if width+w > maxWidth { - break + if strippedWidth-screenPos < maxWidth { + // We chopped off enough. + if escapePos > 0 && textPos+tagOffset-1 >= escapeIndices[escapePos-1][0] && textPos+tagOffset-1 < escapeIndices[escapePos-1][1] { + // Unescape open escape sequences. + escapeCharPos := escapeIndices[escapePos-1][1] - 2 + text = text[:escapeCharPos] + text[escapeCharPos+1:] + } + // Print and return. + bytes, width = printWithStyle(screen, text[textPos+tagOffset:], x, y, maxWidth, AlignLeft, style) + return true } - width += w - start = index - } - for start < len(runes) && runewidth.RuneWidth(runes[start]) == 0 { - start++ - } - return printWithStyle(screen, substring(start, len(runes)), x+maxWidth-width, y, width, AlignLeft, style) + return false + }) + return bytes, width } else if align == AlignCenter { - width := runewidth.StringWidth(strippedText) - if width == maxWidth { + if strippedWidth == maxWidth { // Use the exact space. return printWithStyle(screen, text, x, y, maxWidth, AlignLeft, style) - } else if width < maxWidth { + } else if strippedWidth < maxWidth { // We have more space than we need. - half := (maxWidth - width) / 2 + half := (maxWidth - strippedWidth) / 2 return printWithStyle(screen, text, x+half, y, maxWidth-half, AlignLeft, style) } else { // Chop off runes until we have a perfect fit. var choppedLeft, choppedRight, leftIndex, rightIndex int - rightIndex = len(runes) - 1 - for rightIndex > leftIndex && width-choppedLeft-choppedRight > maxWidth { + rightIndex = len(strippedText) + for rightIndex-1 > leftIndex && strippedWidth-choppedLeft-choppedRight > maxWidth { if choppedLeft < choppedRight { - leftWidth := runewidth.RuneWidth(runes[leftIndex]) - choppedLeft += leftWidth - leftIndex++ - for leftIndex < len(runes) && leftIndex < rightIndex && runewidth.RuneWidth(runes[leftIndex]) == 0 { - leftIndex++ - } + // Iterate on the left by one character. + iterateString(strippedText[leftIndex:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { + choppedLeft += screenWidth + leftIndex += textWidth + return true + }) } else { - rightWidth := runewidth.RuneWidth(runes[rightIndex]) - choppedRight += rightWidth - rightIndex-- + // Iterate on the right by one character. + iterateStringReverse(strippedText[leftIndex:rightIndex], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { + choppedRight += screenWidth + rightIndex -= textWidth + return true + }) } } - return printWithStyle(screen, substring(leftIndex, rightIndex), x, y, maxWidth, AlignLeft, style) + + // Add tag offsets and determine start style. + var ( + colorPos, escapePos, tagOffset int + foregroundColor, backgroundColor, attributes string + ) + _, originalBackground, _ := style.Decompose() + for index := range strippedText { + // We only need the offset of the left index. + if index > leftIndex { + // We're done. + if escapePos > 0 && leftIndex+tagOffset-1 >= escapeIndices[escapePos-1][0] && leftIndex+tagOffset-1 < escapeIndices[escapePos-1][1] { + // Unescape open escape sequences. + escapeCharPos := escapeIndices[escapePos-1][1] - 2 + text = text[:escapeCharPos] + text[escapeCharPos+1:] + } + break + } + + // Update color/escape tag offset. + if colorPos < len(colorIndices) && index+tagOffset >= colorIndices[colorPos][0] && index+tagOffset < colorIndices[colorPos][1] { + if index <= leftIndex { + foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos]) + style = overlayStyle(originalBackground, style, foregroundColor, backgroundColor, attributes) + } + tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0] + colorPos++ + } + if escapePos < len(escapeIndices) && index+tagOffset >= escapeIndices[escapePos][0] && index+tagOffset < escapeIndices[escapePos][1] { + tagOffset++ + escapePos++ + } + } + return printWithStyle(screen, text[leftIndex+tagOffset:], x, y, maxWidth, AlignLeft, style) } } // Draw text. - drawn := 0 - drawnWidth := 0 var ( - colorPos, escapePos int - foregroundColor, backgroundColor, attributes string + drawn, drawnWidth, colorPos, escapePos, tagOffset int + foregroundColor, backgroundColor, attributes string ) - runeSequence := make([]rune, 0, 10) - runeSeqWidth := 0 - flush := func() { - if len(runeSequence) == 0 { - return // Nothing to flush. - } - - // Print the rune sequence. - finalX := x + drawnWidth - _, _, finalStyle, _ := screen.GetContent(finalX, y) - _, background, _ := finalStyle.Decompose() - finalStyle = overlayStyle(background, style, foregroundColor, backgroundColor, attributes) - var comb []rune - if len(runeSequence) > 1 { - // Allocate space for the combining characters only when necessary. - comb = make([]rune, len(runeSequence)-1) - copy(comb, runeSequence[1:]) - } - for offset := 0; offset < runeSeqWidth; offset++ { - // To avoid undesired effects, we place the same character in all cells. - screen.SetContent(finalX+offset, y, runeSequence[0], comb, finalStyle) + iterateString(strippedText, func(main rune, comb []rune, textPos, length, screenPos, screenWidth int) bool { + // Only continue if there is still space. + if drawnWidth+screenWidth > maxWidth { + return true } - // Advance and reset. - drawn += len(runeSequence) - drawnWidth += runeSeqWidth - runeSequence = runeSequence[:0] - runeSeqWidth = 0 - } - for pos, ch := range text { // Handle color tags. - if colorPos < len(colorIndices) && pos >= colorIndices[colorPos][0] && pos < colorIndices[colorPos][1] { - flush() - if pos == colorIndices[colorPos][1]-1 { - foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos]) - colorPos++ - } - continue + if colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] { + foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos]) + tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0] + colorPos++ } - // Handle escape tags. - if escapePos < len(escapeIndices) && pos >= escapeIndices[escapePos][0] && pos < escapeIndices[escapePos][1] { - flush() - if pos == escapeIndices[escapePos][1]-1 { + // Handle scape tags. + if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] { + if textPos+tagOffset == escapeIndices[escapePos][1]-2 { + tagOffset++ escapePos++ - } else if pos == escapeIndices[escapePos][1]-2 { - continue } } - // Check if we have enough space for this rune. - chWidth := runewidth.RuneWidth(ch) - if drawnWidth+chWidth > maxWidth { - break // No. We're done then. - } - - // Put this rune in the queue. - if chWidth == 0 { - // If this is not a modifier, we treat it as a space character. - if len(runeSequence) == 0 { - ch = ' ' - chWidth = 1 + // Print the rune sequence. + finalX := x + drawnWidth + _, _, finalStyle, _ := screen.GetContent(finalX, y) + _, background, _ := finalStyle.Decompose() + finalStyle = overlayStyle(background, style, foregroundColor, backgroundColor, attributes) + for offset := screenWidth - 1; offset >= 0; offset-- { + // To avoid undesired effects, we populate all cells. + if offset == 0 { + screen.SetContent(finalX+offset, y, main, comb, finalStyle) + } else { + screen.SetContent(finalX+offset, y, ' ', nil, finalStyle) } - } else { - // We have a character. Flush all previous runes. - flush() } - runeSequence = append(runeSequence, ch) - runeSeqWidth += chWidth - } - if drawnWidth+runeSeqWidth <= maxWidth { - flush() - } + // Advance. + drawn += length + drawnWidth += screenWidth + + return false + }) - return drawn, drawnWidth + return drawn + tagOffset + len(escapeIndices), drawnWidth } // PrintSimple prints white text to the screen at the given position. @@ -507,131 +394,86 @@ func WordWrap(text string, width int) (lines []string) { colorTagIndices, _, escapeIndices, strippedText, _ := decomposeString(text) // Find candidate breakpoints. - breakPoints := boundaryPattern.FindAllStringIndex(strippedText, -1) - - // This helper function adds a new line to the result slice. The provided - // positions are in stripped index space. - addLine := func(from, to int) { - // Shift indices back to original index space. - var colorTagIndex, escapeIndex int - for colorTagIndex < len(colorTagIndices) && to >= colorTagIndices[colorTagIndex][0] || - escapeIndex < len(escapeIndices) && to >= escapeIndices[escapeIndex][0] { - past := 0 - if colorTagIndex < len(colorTagIndices) { - tagWidth := colorTagIndices[colorTagIndex][1] - colorTagIndices[colorTagIndex][0] - if colorTagIndices[colorTagIndex][0] < from { - from += tagWidth - to += tagWidth - colorTagIndex++ - } else if colorTagIndices[colorTagIndex][0] < to { - to += tagWidth - colorTagIndex++ - } else { - past++ - } - } else { - past++ - } - if escapeIndex < len(escapeIndices) { - tagWidth := escapeIndices[escapeIndex][1] - escapeIndices[escapeIndex][0] - if escapeIndices[escapeIndex][0] < from { - from += tagWidth - to += tagWidth - escapeIndex++ - } else if escapeIndices[escapeIndex][0] < to { - to += tagWidth - escapeIndex++ - } else { - past++ - } - } else { - past++ - } - if past == 2 { - break // All other indices are beyond the requested string. + breakpoints := boundaryPattern.FindAllStringSubmatchIndex(strippedText, -1) + // Results in one entry for each candidate. Each entry is an array a of + // indices into strippedText where a[6] < 0 for newline/punctuation matches + // and a[4] < 0 for whitespace matches. + + // Process stripped text one character at a time. + var ( + colorPos, escapePos, breakpointPos, tagOffset int + lastBreakpoint, lastContinuation, currentLineStart int + lineWidth, continuationWidth int + newlineBreakpoint bool + ) + unescape := func(substr string, startIndex int) string { + // A helper function to unescape escaped tags. + for index := escapePos; index >= 0; index-- { + if index < len(escapeIndices) && startIndex > escapeIndices[index][0] && startIndex < escapeIndices[index][1]-1 { + pos := escapeIndices[index][1] - 2 - startIndex + return substr[:pos] + substr[pos+1:] } } - lines = append(lines, text[from:to]) + return substr } + iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { + // Handle colour tags. + if colorPos < len(colorTagIndices) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] { + tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0] + colorPos++ + } - // Determine final breakpoints. - var start, lastEnd, newStart, breakPoint int - for { - // What's our candidate string? - var candidate string - if breakPoint < len(breakPoints) { - candidate = text[start:breakPoints[breakPoint][1]] - } else { - candidate = text[start:] + // Handle escape tags. + if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 { + tagOffset++ + escapePos++ } - candidate = strings.TrimRightFunc(candidate, unicode.IsSpace) - - if runewidth.StringWidth(candidate) >= width { - // We're past the available width. - if lastEnd > start { - // Use the previous candidate. - addLine(start, lastEnd) - start = newStart - } else { - // We have no previous candidate. Make a hard break. - var lineWidth int - for index, ch := range text { - if index < start { - continue - } - chWidth := runewidth.RuneWidth(ch) - if lineWidth > 0 && lineWidth+chWidth >= width { - addLine(start, index) - start = index - break - } - lineWidth += chWidth - } - } - } else { - // We haven't hit the right border yet. - if breakPoint >= len(breakPoints) { - // It's the last line. We're done. - if len(candidate) > 0 { - addLine(start, len(strippedText)) - } - break - } else { - // We have a new candidate. - lastEnd = start + len(candidate) - newStart = breakPoints[breakPoint][1] - breakPoint++ - } + + // Check if a break is warranted. + afterContinuation := lastContinuation > 0 && textPos+tagOffset >= lastContinuation + noBreakpoint := lastContinuation == 0 + beyondWidth := lineWidth > 0 && lineWidth > width + if beyondWidth && noBreakpoint { + // We need a hard break without a breakpoint. + lines = append(lines, unescape(text[currentLineStart:textPos+tagOffset], currentLineStart)) + currentLineStart = textPos + tagOffset + lineWidth = continuationWidth + } else if afterContinuation && (beyondWidth || newlineBreakpoint) { + // Break at last breakpoint or at newline. + lines = append(lines, unescape(text[currentLineStart:lastBreakpoint], currentLineStart)) + currentLineStart = lastContinuation + lineWidth = continuationWidth + lastBreakpoint, lastContinuation, newlineBreakpoint = 0, 0, false } - } - return -} + // Is this a breakpoint? + if breakpointPos < len(breakpoints) && textPos == breakpoints[breakpointPos][0] { + // Yes, it is. Set up breakpoint infos depending on its type. + lastBreakpoint = breakpoints[breakpointPos][0] + tagOffset + lastContinuation = breakpoints[breakpointPos][1] + tagOffset + newlineBreakpoint = main == '\n' + if breakpoints[breakpointPos][6] < 0 && !newlineBreakpoint { + lastBreakpoint++ // Don't skip punctuation. + } + breakpointPos++ + } -// PrintJoinedBorder prints a border graphics rune into the screen at the given -// position with the given color, joining it with any existing border graphics -// rune. Background colors are preserved. At this point, only regular single -// line borders are supported. -func PrintJoinedBorder(screen tcell.Screen, x, y int, ch rune, color tcell.Color) { - previous, _, style, _ := screen.GetContent(x, y) - style = style.Foreground(color) - - // What's the resulting rune? - var result rune - if ch == previous { - result = ch - } else { - if ch < previous { - previous, ch = ch, previous + // Once we hit the continuation point, we start buffering widths. + if textPos+tagOffset < lastContinuation { + continuationWidth = 0 } - result = joints[string(previous)+string(ch)] - } - if result == 0 { - result = ch + + lineWidth += screenWidth + continuationWidth += screenWidth + return false + }) + + // Flush the rest. + if currentLineStart < len(text) { + lines = append(lines, unescape(text[currentLineStart:], currentLineStart)) } - // We only print something if we have something. - screen.SetContent(x, y, result, nil, style) + return } // Escape escapes the given text such that color and/or region tags are not @@ -643,3 +485,121 @@ func PrintJoinedBorder(screen tcell.Screen, x, y int, ch rune, color tcell.Color func Escape(text string) string { return nonEscapePattern.ReplaceAllString(text, "$1[]") } + +// iterateString iterates through the given string one printed character at a +// time. For each such character, the callback function is called with the +// Unicode code points of the character (the first rune and any combining runes +// which may be nil if there aren't any), the starting position (in bytes) +// within the original string, its length in bytes, the screen position of the +// character, and the screen width of it. The iteration stops if the callback +// returns true. This function returns true if the iteration was stopped before +// the last character. +func iterateString(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool) bool { + var ( + runes []rune + lastZeroWidthJoiner bool + startIndex int + startPos int + pos int + ) + + // Helper function which invokes the callback. + flush := func(index int) bool { + var comb []rune + if len(runes) > 1 { + comb = runes[1:] + } + return callback(runes[0], comb, startIndex, index-startIndex, startPos, pos-startPos) + } + + for index, r := range text { + if unicode.In(r, unicode.Lm, unicode.M) || r == '\u200d' { + lastZeroWidthJoiner = r == '\u200d' + } else { + // We have a rune that's not a modifier. It could be the beginning of a + // new character. + if !lastZeroWidthJoiner { + if len(runes) > 0 { + // It is. Invoke callback. + if flush(index) { + return true // We're done. + } + // Reset rune store. + runes = runes[:0] + startIndex = index + startPos = pos + } + pos += runewidth.RuneWidth(r) + } else { + lastZeroWidthJoiner = false + } + } + runes = append(runes, r) + } + + // Flush any remaining runes. + if len(runes) > 0 { + flush(len(text)) + } + + return false +} + +// iterateStringReverse iterates through the given string in reverse, starting +// from the end of the string, one printed character at a time. For each such +// character, the callback function is called with the Unicode code points of +// the character (the first rune and any combining runes which may be nil if +// there aren't any), the starting position (in bytes) within the original +// string, its length in bytes, the screen position of the character, and the +// screen width of it. The iteration stops if the callback returns true. This +// function returns true if the iteration was stopped before the last character. +func iterateStringReverse(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool) bool { + type runePos struct { + r rune + pos int // The byte position of the rune in the original string. + width int // The screen width of the rune. + mod bool // Modifier or zero-width-joiner. + } + + // We use the following: + // len(text) >= number of runes in text. + + // Put all runes into a runePos slice in reverse. + runesReverse := make([]runePos, len(text)) + index := len(text) - 1 + for pos, ch := range text { + runesReverse[index].r = ch + runesReverse[index].pos = pos + runesReverse[index].width = runewidth.RuneWidth(ch) + runesReverse[index].mod = unicode.In(ch, unicode.Lm, unicode.M) || ch == '\u200d' + index-- + } + runesReverse = runesReverse[index+1:] + + // Parse reverse runes. + var screenWidth int + buffer := make([]rune, len(text)) // We fill this up from the back so it's forward again. + bufferPos := len(text) + stringWidth := runewidth.StringWidth(text) + for index, r := range runesReverse { + // Put this rune into the buffer. + bufferPos-- + buffer[bufferPos] = r.r + + // Do we need to flush the buffer? + if r.pos == 0 || !r.mod && runesReverse[index+1].r != '\u200d' { + // Yes, invoke callback. + var comb []rune + if len(text)-bufferPos > 1 { + comb = buffer[bufferPos+1:] + } + if callback(r.r, comb, r.pos, len(text)-r.pos, stringWidth-screenWidth, r.width) { + return true + } + screenWidth += r.width + bufferPos = len(text) + } + } + + return false +} -- cgit v1.2.3