commit
0d12947b1f
@ -63,6 +63,7 @@ type Config struct {
|
|||||||
RoomCacheAge int64 `yaml:"room_cache_age"`
|
RoomCacheAge int64 `yaml:"room_cache_age"`
|
||||||
|
|
||||||
NotifySound bool `yaml:"notify_sound"`
|
NotifySound bool `yaml:"notify_sound"`
|
||||||
|
SendToVerifiedOnly bool `yaml:"send_to_verified_only"`
|
||||||
|
|
||||||
Dir string `yaml:"-"`
|
Dir string `yaml:"-"`
|
||||||
DataDir string `yaml:"data_dir"`
|
DataDir string `yaml:"data_dir"`
|
||||||
@ -97,6 +98,7 @@ func NewConfig(configDir, dataDir, cacheDir, downloadDir string) *Config {
|
|||||||
RoomCacheAge: 1 * 60,
|
RoomCacheAge: 1 * 60,
|
||||||
|
|
||||||
NotifySound: true,
|
NotifySound: true,
|
||||||
|
SendToVerifiedOnly: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
2
go.mod
2
go.mod
@ -23,7 +23,7 @@ require (
|
|||||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9
|
golang.org/x/net v0.0.0-20200602114024-627f9648deb9
|
||||||
gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2
|
gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2
|
||||||
gopkg.in/yaml.v2 v2.3.0
|
gopkg.in/yaml.v2 v2.3.0
|
||||||
maunium.net/go/mautrix v0.5.5
|
maunium.net/go/mautrix v0.7.0-rc.1
|
||||||
maunium.net/go/mauview v0.1.1
|
maunium.net/go/mauview v0.1.1
|
||||||
maunium.net/go/tcell v0.2.0
|
maunium.net/go/tcell v0.2.0
|
||||||
)
|
)
|
||||||
|
14
go.sum
14
go.sum
@ -1,9 +1,11 @@
|
|||||||
|
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||||
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
|
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
|
||||||
github.com/alecthomas/chroma v0.7.3 h1:NfdAERMy+esYQs8OXk0I868/qDxxCEo7FMz1WIqMAeI=
|
github.com/alecthomas/chroma v0.7.3 h1:NfdAERMy+esYQs8OXk0I868/qDxxCEo7FMz1WIqMAeI=
|
||||||
github.com/alecthomas/chroma v0.7.3/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM=
|
github.com/alecthomas/chroma v0.7.3/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM=
|
||||||
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
|
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
|
||||||
github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
|
github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
|
||||||
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
|
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
|
||||||
|
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
|
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
|
||||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
|
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@ -28,6 +30,13 @@ github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope
|
|||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||||
|
github.com/nikofil/mautrix-go v0.5.2-0.20200725145808-be3336827da7 h1:w5IiIpetgAwalLPFkxiVbxBXJtcB9zLhoGLkyouQb4k=
|
||||||
|
github.com/nikofil/mautrix-go v0.5.2-0.20200725145808-be3336827da7/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo=
|
||||||
|
github.com/nikofil/mautrix-go v0.5.2-0.20200725165923-cae799088b7e h1:K8D+9n29FP1Utrv2joUhjcuDL1zC+Qq9rEFdV5kPgTQ=
|
||||||
|
github.com/nikofil/mautrix-go v0.5.2-0.20200725165923-cae799088b7e/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo=
|
||||||
|
github.com/nikofil/mautrix-go v0.5.2-0.20200725175335-dd3f90913c4d h1:tsCIy4CvvAeFEoBhet7zGoZXc+lr/e2YogoPaEGdMN8=
|
||||||
|
github.com/nikofil/mautrix-go v0.5.2-0.20200725175335-dd3f90913c4d/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo=
|
||||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
|
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
|
||||||
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ=
|
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ=
|
||||||
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o=
|
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o=
|
||||||
@ -70,7 +79,10 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
|||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw=
|
golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw=
|
||||||
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
|
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
|
||||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@ -100,6 +112,8 @@ maunium.net/go/mautrix v0.5.3 h1:Lu4PGZvKZwhXmdDnBdLwmiImNM3jNGwJI9Sk78P+Gys=
|
|||||||
maunium.net/go/mautrix v0.5.3/go.mod h1:LnkFnB1yjCbb8V+upoEHDGvI/F38NHSTWYCe2RRJgSY=
|
maunium.net/go/mautrix v0.5.3/go.mod h1:LnkFnB1yjCbb8V+upoEHDGvI/F38NHSTWYCe2RRJgSY=
|
||||||
maunium.net/go/mautrix v0.5.5 h1:e0Pql1FdxoNUudx2oXo1gZHMrqIh5MC72cdXEPIrYLA=
|
maunium.net/go/mautrix v0.5.5 h1:e0Pql1FdxoNUudx2oXo1gZHMrqIh5MC72cdXEPIrYLA=
|
||||||
maunium.net/go/mautrix v0.5.5/go.mod h1:FLbMANzwqlsX2Fgm7SDe+E4I3wSa4UxJRKqS5wGkCwA=
|
maunium.net/go/mautrix v0.5.5/go.mod h1:FLbMANzwqlsX2Fgm7SDe+E4I3wSa4UxJRKqS5wGkCwA=
|
||||||
|
maunium.net/go/mautrix v0.7.0-rc.1 h1:DT7bNR9q+HlFs5Oo9IqmtWPkE4WPKZdRfIWRtlqkXtM=
|
||||||
|
maunium.net/go/mautrix v0.7.0-rc.1/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo=
|
||||||
maunium.net/go/mauview v0.1.1 h1:wfTXyPx3LGAGpTskh+UbBv/QItUWnEpaneHmywoYnfY=
|
maunium.net/go/mauview v0.1.1 h1:wfTXyPx3LGAGpTskh+UbBv/QItUWnEpaneHmywoYnfY=
|
||||||
maunium.net/go/mauview v0.1.1/go.mod h1:3QBUiuLct9moP1LgDhCGIg0Ovxn38Bd2sGndnUOuj4o=
|
maunium.net/go/mauview v0.1.1/go.mod h1:3QBUiuLct9moP1LgDhCGIg0Ovxn38Bd2sGndnUOuj4o=
|
||||||
maunium.net/go/tcell v0.2.0 h1:1Q0kN3wCOGAIGu1r3QHADsjSUOPDylKREvCv3EzJpVg=
|
maunium.net/go/tcell v0.2.0 h1:1Q0kN3wCOGAIGu1r3QHADsjSUOPDylKREvCv3EzJpVg=
|
||||||
|
@ -55,7 +55,9 @@ func (c *Container) initCrypto() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to open crypto store")
|
return errors.Wrap(err, "failed to open crypto store")
|
||||||
}
|
}
|
||||||
c.crypto = crypto.NewOlmMachine(c.client, cryptoLogger{}, cryptoStore, c.config.Rooms)
|
crypt := crypto.NewOlmMachine(c.client, cryptoLogger{}, cryptoStore, c.config.Rooms)
|
||||||
|
crypt.AllowUnverifiedDevices = !c.config.SendToVerifiedOnly
|
||||||
|
c.crypto = crypt
|
||||||
err = c.crypto.Load()
|
err = c.crypto.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to create olm machine")
|
return errors.Wrap(err, "failed to create olm machine")
|
||||||
|
@ -162,6 +162,8 @@ func (c *Container) PasswordLogin(user, password string) error {
|
|||||||
},
|
},
|
||||||
Password: password,
|
Password: password,
|
||||||
InitialDeviceDisplayName: "gomuks",
|
InitialDeviceDisplayName: "gomuks",
|
||||||
|
|
||||||
|
StoreCredentials: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -171,8 +173,6 @@ func (c *Container) PasswordLogin(user, password string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) finishLogin(resp *mautrix.RespLogin) {
|
func (c *Container) finishLogin(resp *mautrix.RespLogin) {
|
||||||
c.client.SetCredentials(resp.UserID, resp.AccessToken)
|
|
||||||
c.client.DeviceID = resp.DeviceID
|
|
||||||
c.config.UserID = resp.UserID
|
c.config.UserID = resp.UserID
|
||||||
c.config.DeviceID = resp.DeviceID
|
c.config.DeviceID = resp.DeviceID
|
||||||
c.config.AccessToken = resp.AccessToken
|
c.config.AccessToken = resp.AccessToken
|
||||||
@ -218,6 +218,8 @@ func (c *Container) SingleSignOn() error {
|
|||||||
Type: "m.login.token",
|
Type: "m.login.token",
|
||||||
Token: loginToken,
|
Token: loginToken,
|
||||||
InitialDeviceDisplayName: "gomuks",
|
InitialDeviceDisplayName: "gomuks",
|
||||||
|
|
||||||
|
StoreCredentials: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondHTML(w, http.StatusForbidden, err.Error())
|
respondHTML(w, http.StatusForbidden, err.Error())
|
||||||
|
@ -73,6 +73,19 @@ func (cache *RoomCache) IsEncrypted(roomID id.RoomID) bool {
|
|||||||
return room != nil && room.Encrypted
|
return room != nil && room.Encrypted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cache *RoomCache) GetEncryptionEvent(roomID id.RoomID) *event.EncryptionEventContent {
|
||||||
|
room := cache.Get(roomID)
|
||||||
|
evt := room.GetStateEvent(event.StateEncryption, "")
|
||||||
|
if evt == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
content, ok := evt.Content.Parsed.(*event.EncryptionEventContent)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
func (cache *RoomCache) FindSharedRooms(userID id.UserID) (shared []id.RoomID) {
|
func (cache *RoomCache) FindSharedRooms(userID id.UserID) (shared []id.RoomID) {
|
||||||
// FIXME this disables unloading so TouchNode wouldn't try to double-lock
|
// FIXME this disables unloading so TouchNode wouldn't try to double-lock
|
||||||
cache.DisableUnloading()
|
cache.DisableUnloading()
|
||||||
|
@ -20,6 +20,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mattn/go-runewidth"
|
||||||
|
|
||||||
"maunium.net/go/gomuks/config"
|
"maunium.net/go/gomuks/config"
|
||||||
"maunium.net/go/gomuks/debug"
|
"maunium.net/go/gomuks/debug"
|
||||||
"maunium.net/go/gomuks/interface"
|
"maunium.net/go/gomuks/interface"
|
||||||
@ -45,6 +47,8 @@ type Command struct {
|
|||||||
OrigText string
|
OrigText string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CommandAutocomplete Command
|
||||||
|
|
||||||
func (cmd *Command) Reply(message string, args ...interface{}) {
|
func (cmd *Command) Reply(message string, args ...interface{}) {
|
||||||
cmd.Room.AddServiceMessage(fmt.Sprintf(message, args...))
|
cmd.Room.AddServiceMessage(fmt.Sprintf(message, args...))
|
||||||
cmd.UI.Render()
|
cmd.UI.Render()
|
||||||
@ -60,12 +64,15 @@ func (alias *Alias) Process(cmd *Command) *Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CommandHandler func(cmd *Command)
|
type CommandHandler func(cmd *Command)
|
||||||
|
type CommandAutocompleter func(cmd *CommandAutocomplete) (completions []string, newText string)
|
||||||
|
|
||||||
type CommandProcessor struct {
|
type CommandProcessor struct {
|
||||||
gomuksPointerContainer
|
gomuksPointerContainer
|
||||||
|
|
||||||
aliases map[string]*Alias
|
aliases map[string]*Alias
|
||||||
commands map[string]CommandHandler
|
commands map[string]CommandHandler
|
||||||
|
|
||||||
|
autocompleters map[string]CommandAutocompleter
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCommandProcessor(parent *MainView) *CommandProcessor {
|
func NewCommandProcessor(parent *MainView) *CommandProcessor {
|
||||||
@ -97,6 +104,12 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor {
|
|||||||
"dl": {"download"},
|
"dl": {"download"},
|
||||||
"o": {"open"},
|
"o": {"open"},
|
||||||
},
|
},
|
||||||
|
autocompleters: map[string]CommandAutocompleter{
|
||||||
|
"devices": autocompleteDevice,
|
||||||
|
"device": autocompleteDevice,
|
||||||
|
"verify": autocompleteDevice,
|
||||||
|
"unverify": autocompleteDevice,
|
||||||
|
},
|
||||||
commands: map[string]CommandHandler{
|
commands: map[string]CommandHandler{
|
||||||
"unknown-command": cmdUnknownCommand,
|
"unknown-command": cmdUnknownCommand,
|
||||||
|
|
||||||
@ -140,6 +153,12 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor {
|
|||||||
"trace": cmdTrace,
|
"trace": cmdTrace,
|
||||||
|
|
||||||
"fingerprint": cmdFingerprint,
|
"fingerprint": cmdFingerprint,
|
||||||
|
"devices": cmdDevices,
|
||||||
|
"verify": cmdVerify,
|
||||||
|
"device": cmdDevice,
|
||||||
|
"unverify": cmdUnverify,
|
||||||
|
"blacklist": cmdBlacklist,
|
||||||
|
"reset-session": cmdResetSession,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -169,6 +188,47 @@ func (ch *CommandProcessor) ParseCommand(roomView *RoomView, text string) *Comma
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ch *CommandProcessor) Autocomplete(roomView *RoomView, text string, cursorOffset int) ([]string, string, bool) {
|
||||||
|
var completions []string
|
||||||
|
if cmd := (*CommandAutocomplete)(ch.ParseCommand(roomView, text)); cmd == nil {
|
||||||
|
return completions, text, false
|
||||||
|
} else if handler, ok := ch.autocompleters[cmd.Command]; !ok {
|
||||||
|
return completions, text, false
|
||||||
|
} else if cursorOffset != runewidth.StringWidth(text) {
|
||||||
|
return completions, text, false
|
||||||
|
} else {
|
||||||
|
completions, newText := handler(cmd)
|
||||||
|
if newText == "" {
|
||||||
|
newText = text
|
||||||
|
}
|
||||||
|
return completions, newText, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch *CommandProcessor) AutocompleteCommand(word string) (completions []string) {
|
||||||
|
if word[0] != '/' {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
word = word[1:]
|
||||||
|
for alias := range ch.aliases {
|
||||||
|
if alias == word {
|
||||||
|
return []string{"/" + alias}
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(alias, word) {
|
||||||
|
completions = append(completions, "/"+alias)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for command := range ch.commands {
|
||||||
|
if command == word {
|
||||||
|
return []string{"/" + command}
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(command, word) {
|
||||||
|
completions = append(completions, "/"+command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (ch *CommandProcessor) HandleCommand(cmd *Command) {
|
func (ch *CommandProcessor) HandleCommand(cmd *Command) {
|
||||||
defer debug.Recover()
|
defer debug.Recover()
|
||||||
if cmd == nil {
|
if cmd == nil {
|
||||||
|
@ -440,7 +440,7 @@ func cmdHelp(cmd *Command) {
|
|||||||
/logout - Log out of Matrix.
|
/logout - Log out of Matrix.
|
||||||
/toggle <thing> - Temporary command to toggle various UI features.
|
/toggle <thing> - Temporary command to toggle various UI features.
|
||||||
|
|
||||||
Things: rooms, users, baremessages, images, typingnotif
|
Things: rooms, users, baremessages, images, typingnotif, unverified
|
||||||
|
|
||||||
# Sending special messages
|
# Sending special messages
|
||||||
/me <message> - Send an emote message.
|
/me <message> - Send an emote message.
|
||||||
@ -451,6 +451,17 @@ Things: rooms, users, baremessages, images, typingnotif
|
|||||||
/react <reaction> - React to the selected message.
|
/react <reaction> - React to the selected message.
|
||||||
/redact [reason] - Redact the selected message.
|
/redact [reason] - Redact the selected message.
|
||||||
|
|
||||||
|
# Encryption
|
||||||
|
/fingerprint - View the fingerprint of your device.
|
||||||
|
|
||||||
|
/devices <user id> - View the device list of a user.
|
||||||
|
/device <user id> <device id> - Show info about a specific device.
|
||||||
|
/unverify <user id> <device id> - Un-verify a device.
|
||||||
|
/blacklist <user id> <device id> - Blacklist a device.
|
||||||
|
/verify <user id> <device id> [fingerprint]
|
||||||
|
- Verify a device. If the fingerprint is not provided,
|
||||||
|
interactive emoji verification will be started.
|
||||||
|
|
||||||
# Rooms
|
# Rooms
|
||||||
/pm <user id> <...> - Create a private chat with the given user(s).
|
/pm <user id> <...> - Create a private chat with the given user(s).
|
||||||
/create [room name] - Create a room.
|
/create [room name] - Create a room.
|
||||||
@ -710,6 +721,7 @@ var toggleMsg = map[string]ToggleMessage{
|
|||||||
"markdown": SimpleToggleMessage("markdown input"),
|
"markdown": SimpleToggleMessage("markdown input"),
|
||||||
"downloads": SimpleToggleMessage("automatic downloads"),
|
"downloads": SimpleToggleMessage("automatic downloads"),
|
||||||
"notifications": SimpleToggleMessage("desktop notifications"),
|
"notifications": SimpleToggleMessage("desktop notifications"),
|
||||||
|
"unverified": SimpleToggleMessage("sending messages to unverified devices"),
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeUsage() string {
|
func makeUsage() string {
|
||||||
@ -750,6 +762,8 @@ func cmdToggle(cmd *Command) {
|
|||||||
val = &cmd.Config.Preferences.DisableDownloads
|
val = &cmd.Config.Preferences.DisableDownloads
|
||||||
case "notifications":
|
case "notifications":
|
||||||
val = &cmd.Config.Preferences.DisableNotifications
|
val = &cmd.Config.Preferences.DisableNotifications
|
||||||
|
case "unverified":
|
||||||
|
val = &cmd.Config.SendToVerifiedOnly
|
||||||
default:
|
default:
|
||||||
cmd.Reply("Unknown toggle %s. Use /toggle without arguments for a list of togglable things.", thing)
|
cmd.Reply("Unknown toggle %s. Use /toggle without arguments for a list of togglable things.", thing)
|
||||||
return
|
return
|
||||||
|
231
ui/crypto-commands.go
Normal file
231
ui/crypto-commands.go
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
// gomuks - A terminal Matrix client written in Go.
|
||||||
|
// Copyright (C) 2020 Tulir Asokan
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// +build cgo
|
||||||
|
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix/crypto"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
func autocompleteDeviceUserID(cmd *CommandAutocomplete) (completions []string, newText string) {
|
||||||
|
userCompletions := cmd.Room.AutocompleteUser(cmd.Args[0])
|
||||||
|
if len(userCompletions) == 1 {
|
||||||
|
newText = fmt.Sprintf("/%s %s ", cmd.OrigCommand, userCompletions[0].id)
|
||||||
|
} else {
|
||||||
|
completions = make([]string, len(userCompletions))
|
||||||
|
for i, completion := range userCompletions {
|
||||||
|
completions[i] = completion.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func autocompleteDeviceDeviceID(cmd *CommandAutocomplete) (completions []string, newText string) {
|
||||||
|
mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
|
||||||
|
devices, err := mach.CryptoStore.GetDevices(id.UserID(cmd.Args[0]))
|
||||||
|
if len(devices) == 0 || err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var completedDeviceID id.DeviceID
|
||||||
|
if len(cmd.Args) > 1 {
|
||||||
|
existingID := strings.ToUpper(cmd.Args[1])
|
||||||
|
for _, device := range devices {
|
||||||
|
deviceIDStr := string(device.DeviceID)
|
||||||
|
if deviceIDStr == existingID {
|
||||||
|
// We don't want to do any autocompletion if there's already a full device ID there.
|
||||||
|
return []string{}, ""
|
||||||
|
} else if strings.HasPrefix(strings.ToUpper(device.Name), existingID) || strings.HasPrefix(deviceIDStr, existingID) {
|
||||||
|
completedDeviceID = device.DeviceID
|
||||||
|
completions = append(completions, fmt.Sprintf("%s (%s)", device.DeviceID, device.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
completions = make([]string, len(devices))
|
||||||
|
i := 0
|
||||||
|
for _, device := range devices {
|
||||||
|
completedDeviceID = device.DeviceID
|
||||||
|
completions[i] = fmt.Sprintf("%s (%s)", device.DeviceID, device.Name)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(completions) == 1 {
|
||||||
|
newText = fmt.Sprintf("/%s %s %s ", cmd.OrigCommand, cmd.Args[0], completedDeviceID)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func autocompleteDevice(cmd *CommandAutocomplete) ([]string, string) {
|
||||||
|
if len(cmd.Args) == 0 {
|
||||||
|
return []string{}, ""
|
||||||
|
} else if len(cmd.Args) == 1 && !unicode.IsSpace(rune(cmd.RawArgs[len(cmd.RawArgs)-1])) {
|
||||||
|
return autocompleteDeviceUserID(cmd)
|
||||||
|
} else if cmd.Command != "devices" {
|
||||||
|
return autocompleteDeviceDeviceID(cmd)
|
||||||
|
}
|
||||||
|
return []string{}, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDevice(cmd *Command) *crypto.DeviceIdentity {
|
||||||
|
if len(cmd.Args) < 2 {
|
||||||
|
cmd.Reply("Usage: /%s <user id> <device id> [fingerprint]", cmd.Command)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
|
||||||
|
device, err := mach.GetOrFetchDevice(id.UserID(cmd.Args[0]), id.DeviceID(cmd.Args[1]))
|
||||||
|
if err != nil {
|
||||||
|
cmd.Reply("Failed to get device: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return device
|
||||||
|
}
|
||||||
|
|
||||||
|
func putDevice(cmd *Command, device *crypto.DeviceIdentity, action string) {
|
||||||
|
mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
|
||||||
|
err := mach.CryptoStore.PutDevice(device.UserID, device)
|
||||||
|
if err != nil {
|
||||||
|
cmd.Reply("Failed to save device: %v", err)
|
||||||
|
} else {
|
||||||
|
cmd.Reply("Successfully %s %s/%s (%s)", action, device.UserID, device.DeviceID, device.Name)
|
||||||
|
}
|
||||||
|
mach.OnDevicesChanged(device.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdDevices(cmd *Command) {
|
||||||
|
if len(cmd.Args) == 0 {
|
||||||
|
cmd.Reply("Usage: /devices <user id>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userID := id.UserID(cmd.Args[0])
|
||||||
|
mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
|
||||||
|
devices, err := mach.CryptoStore.GetDevices(userID)
|
||||||
|
if err != nil {
|
||||||
|
cmd.Reply("Failed to get device list: %v", err)
|
||||||
|
}
|
||||||
|
if len(devices) == 0 {
|
||||||
|
cmd.Reply("Fetching device list from server...")
|
||||||
|
devices = mach.LoadDevices(userID)
|
||||||
|
}
|
||||||
|
if len(devices) == 0 {
|
||||||
|
cmd.Reply("No devices found for %s", userID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var buf strings.Builder
|
||||||
|
for _, device := range devices {
|
||||||
|
_, _ = fmt.Fprintf(&buf, "%s (%s) - %s\n Fingerprint: %s\n", device.DeviceID, device.Name, device.Trust.String(), device.Fingerprint())
|
||||||
|
}
|
||||||
|
resp := buf.String()
|
||||||
|
cmd.Reply("%s", resp[:len(resp)-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdDevice(cmd *Command) {
|
||||||
|
device := getDevice(cmd)
|
||||||
|
if device == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
deviceType := "Device"
|
||||||
|
if device.Deleted {
|
||||||
|
deviceType = "Deleted device"
|
||||||
|
}
|
||||||
|
cmd.Reply("%s %s of %s\nFingerprint: %s\nIdentity key: %s\nDevice name: %s\nTrust state: %s",
|
||||||
|
deviceType, device.DeviceID, device.UserID,
|
||||||
|
device.Fingerprint(), device.IdentityKey,
|
||||||
|
device.Name, device.Trust.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdVerify(cmd *Command) {
|
||||||
|
device := getDevice(cmd)
|
||||||
|
if device == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if device.Trust == crypto.TrustStateVerified {
|
||||||
|
cmd.Reply("That device is already verified")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(cmd.Args) == 2 {
|
||||||
|
mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
|
||||||
|
mach.DefaultSASTimeout = 120 * time.Second
|
||||||
|
modal := NewVerificationModal(cmd.MainView, device, mach.DefaultSASTimeout)
|
||||||
|
cmd.MainView.ShowModal(modal)
|
||||||
|
_, err := mach.NewSimpleSASVerificationWith(device, modal)
|
||||||
|
if err != nil {
|
||||||
|
cmd.Reply("Failed to start interactive verification: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fingerprint := strings.Join(cmd.Args[2:], "")
|
||||||
|
if string(device.SigningKey) != fingerprint {
|
||||||
|
cmd.Reply("Mismatching fingerprint")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
action := "verified"
|
||||||
|
if device.Trust == crypto.TrustStateBlacklisted {
|
||||||
|
action = "unblacklisted and verified"
|
||||||
|
}
|
||||||
|
device.Trust = crypto.TrustStateVerified
|
||||||
|
putDevice(cmd, device, action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdUnverify(cmd *Command) {
|
||||||
|
device := getDevice(cmd)
|
||||||
|
if device == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if device.Trust == crypto.TrustStateUnset {
|
||||||
|
cmd.Reply("That device is already not verified")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
action := "unverified"
|
||||||
|
if device.Trust == crypto.TrustStateBlacklisted {
|
||||||
|
action = "unblacklisted"
|
||||||
|
}
|
||||||
|
device.Trust = crypto.TrustStateUnset
|
||||||
|
putDevice(cmd, device, action)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdBlacklist(cmd *Command) {
|
||||||
|
device := getDevice(cmd)
|
||||||
|
if device == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if device.Trust == crypto.TrustStateBlacklisted {
|
||||||
|
cmd.Reply("That device is already blacklisted")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
action := "blacklisted"
|
||||||
|
if device.Trust == crypto.TrustStateVerified {
|
||||||
|
action = "unverified and blacklisted"
|
||||||
|
}
|
||||||
|
device.Trust = crypto.TrustStateBlacklisted
|
||||||
|
putDevice(cmd, device, action)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdResetSession(cmd *Command) {
|
||||||
|
err := cmd.Matrix.Crypto().(*crypto.OlmMachine).CryptoStore.RemoveOutboundGroupSession(cmd.Room.Room.ID)
|
||||||
|
if err != nil {
|
||||||
|
cmd.Reply("Failed to remove outbound group session: %v", err)
|
||||||
|
} else {
|
||||||
|
cmd.Reply("Removed outbound group session for this room")
|
||||||
|
}
|
||||||
|
}
|
36
ui/no-crypto-commands.go
Normal file
36
ui/no-crypto-commands.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// gomuks - A terminal Matrix client written in Go.
|
||||||
|
// Copyright (C) 2020 Tulir Asokan
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// +build !cgo
|
||||||
|
|
||||||
|
package ui
|
||||||
|
|
||||||
|
func autocompleteDevice(cmd *CommandAutocomplete) ([]string, string) {
|
||||||
|
return []string{}, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdNoCrypto(cmd *Command) {
|
||||||
|
cmd.Reply("This gomuks was built without encryption support")
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
cmdDevices = cmdNoCrypto
|
||||||
|
cmdDevice = cmdNoCrypto
|
||||||
|
cmdVerify = cmdNoCrypto
|
||||||
|
cmdUnverify = cmdNoCrypto
|
||||||
|
cmdBlacklist = cmdNoCrypto
|
||||||
|
cmdResetSession = cmdNoCrypto
|
||||||
|
)
|
188
ui/room-view.go
188
ui/room-view.go
@ -22,15 +22,18 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"github.com/kyokomi/emoji"
|
"github.com/kyokomi/emoji"
|
||||||
"github.com/mattn/go-runewidth"
|
"github.com/mattn/go-runewidth"
|
||||||
"github.com/zyedidia/clipboard"
|
"github.com/zyedidia/clipboard"
|
||||||
|
|
||||||
"maunium.net/go/mautrix/crypto/attachment"
|
|
||||||
"maunium.net/go/mauview"
|
"maunium.net/go/mauview"
|
||||||
"maunium.net/go/tcell"
|
"maunium.net/go/tcell"
|
||||||
|
|
||||||
|
"maunium.net/go/gomuks/lib/util"
|
||||||
|
"maunium.net/go/mautrix/crypto/attachment"
|
||||||
|
|
||||||
"maunium.net/go/mautrix"
|
"maunium.net/go/mautrix"
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
@ -39,7 +42,6 @@ import (
|
|||||||
"maunium.net/go/gomuks/debug"
|
"maunium.net/go/gomuks/debug"
|
||||||
"maunium.net/go/gomuks/interface"
|
"maunium.net/go/gomuks/interface"
|
||||||
"maunium.net/go/gomuks/lib/open"
|
"maunium.net/go/gomuks/lib/open"
|
||||||
"maunium.net/go/gomuks/lib/util"
|
|
||||||
"maunium.net/go/gomuks/matrix/muksevt"
|
"maunium.net/go/gomuks/matrix/muksevt"
|
||||||
"maunium.net/go/gomuks/matrix/rooms"
|
"maunium.net/go/gomuks/matrix/rooms"
|
||||||
"maunium.net/go/gomuks/ui/messages"
|
"maunium.net/go/gomuks/ui/messages"
|
||||||
@ -420,65 +422,6 @@ func (view *RoomView) SetTyping(users []id.UserID) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type completion struct {
|
|
||||||
displayName string
|
|
||||||
id string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (view *RoomView) autocompleteUser(existingText string) (completions []completion) {
|
|
||||||
textWithoutPrefix := strings.TrimPrefix(existingText, "@")
|
|
||||||
for userID, user := range view.Room.GetMembers() {
|
|
||||||
if user.Displayname == textWithoutPrefix || string(userID) == existingText {
|
|
||||||
// Exact match, return that.
|
|
||||||
return []completion{{user.Displayname, string(userID)}}
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(user.Displayname, textWithoutPrefix) || strings.HasPrefix(string(userID), existingText) {
|
|
||||||
completions = append(completions, completion{user.Displayname, string(userID)})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (view *RoomView) autocompleteRoom(existingText string) (completions []completion) {
|
|
||||||
for _, room := range view.parent.rooms {
|
|
||||||
alias := string(room.Room.GetCanonicalAlias())
|
|
||||||
if alias == existingText {
|
|
||||||
// Exact match, return that.
|
|
||||||
return []completion{{alias, string(room.Room.ID)}}
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(alias, existingText) {
|
|
||||||
completions = append(completions, completion{alias, string(room.Room.ID)})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (view *RoomView) autocompleteEmoji(word string) (completions []string) {
|
|
||||||
if len(word) == 0 || word[0] != ':' {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var valueCompletion1 string
|
|
||||||
var manyValues bool
|
|
||||||
for name, value := range emoji.CodeMap() {
|
|
||||||
if name == word {
|
|
||||||
return []string{value}
|
|
||||||
} else if strings.HasPrefix(name, word) {
|
|
||||||
completions = append(completions, name)
|
|
||||||
if valueCompletion1 == "" {
|
|
||||||
valueCompletion1 = value
|
|
||||||
} else if valueCompletion1 != value {
|
|
||||||
manyValues = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !manyValues && len(completions) > 0 {
|
|
||||||
return []string{emoji.CodeMap()[completions[0]]}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (view *RoomView) SetEditing(evt *muksevt.Event) {
|
func (view *RoomView) SetEditing(evt *muksevt.Event) {
|
||||||
if evt == nil {
|
if evt == nil {
|
||||||
view.editing = nil
|
view.editing = nil
|
||||||
@ -584,23 +527,90 @@ func (view *RoomView) SelectPrevious() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (view *RoomView) InputTabComplete(text string, cursorOffset int) {
|
type completion struct {
|
||||||
debug.Print("Tab completing", cursorOffset, text)
|
displayName string
|
||||||
str := runewidth.Truncate(text, cursorOffset, "")
|
id string
|
||||||
word := findWordToTabComplete(str)
|
}
|
||||||
startIndex := len(str) - len(word)
|
|
||||||
|
|
||||||
var strCompletions []string
|
func (view *RoomView) AutocompleteUser(existingText string) (completions []completion) {
|
||||||
var strCompletion string
|
textWithoutPrefix := strings.TrimPrefix(existingText, "@")
|
||||||
|
for userID, user := range view.Room.GetMembers() {
|
||||||
|
if user.Displayname == textWithoutPrefix || string(userID) == existingText {
|
||||||
|
// Exact match, return that.
|
||||||
|
return []completion{{user.Displayname, string(userID)}}
|
||||||
|
}
|
||||||
|
|
||||||
completions := view.autocompleteUser(word)
|
if strings.HasPrefix(user.Displayname, textWithoutPrefix) || strings.HasPrefix(string(userID), existingText) {
|
||||||
completions = append(completions, view.autocompleteRoom(word)...)
|
completions = append(completions, completion{user.Displayname, string(userID)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *RoomView) AutocompleteRoom(existingText string) (completions []completion) {
|
||||||
|
for _, room := range view.parent.rooms {
|
||||||
|
alias := string(room.Room.GetCanonicalAlias())
|
||||||
|
if alias == existingText {
|
||||||
|
// Exact match, return that.
|
||||||
|
return []completion{{alias, string(room.Room.ID)}}
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(alias, existingText) {
|
||||||
|
completions = append(completions, completion{alias, string(room.Room.ID)})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *RoomView) AutocompleteEmoji(word string) (completions []string) {
|
||||||
|
if word[0] != ':' {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var valueCompletion1 string
|
||||||
|
var manyValues bool
|
||||||
|
for name, value := range emoji.CodeMap() {
|
||||||
|
if name == word {
|
||||||
|
return []string{value}
|
||||||
|
} else if strings.HasPrefix(name, word) {
|
||||||
|
completions = append(completions, name)
|
||||||
|
if valueCompletion1 == "" {
|
||||||
|
valueCompletion1 = value
|
||||||
|
} else if valueCompletion1 != value {
|
||||||
|
manyValues = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !manyValues && len(completions) > 0 {
|
||||||
|
return []string{emoji.CodeMap()[completions[0]]}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func findWordToTabComplete(text string) string {
|
||||||
|
output := ""
|
||||||
|
runes := []rune(text)
|
||||||
|
for i := len(runes) - 1; i >= 0; i-- {
|
||||||
|
if unicode.IsSpace(runes[i]) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
output = string(runes[i]) + output
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *RoomView) defaultAutocomplete(word string, startIndex int) (strCompletions []string, strCompletion string) {
|
||||||
|
if len(word) == 0 {
|
||||||
|
return []string{}, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
completions := view.AutocompleteUser(word)
|
||||||
|
completions = append(completions, view.AutocompleteRoom(word)...)
|
||||||
|
|
||||||
if len(completions) == 1 {
|
if len(completions) == 1 {
|
||||||
completion := completions[0]
|
completion := completions[0]
|
||||||
strCompletion = fmt.Sprintf("[%s](https://matrix.to/#/%s)", completion.displayName, completion.id)
|
strCompletion = fmt.Sprintf("[%s](https://matrix.to/#/%s)", completion.displayName, completion.id)
|
||||||
if startIndex == 0 {
|
if startIndex == 0 && completion.id[0] == '@' {
|
||||||
strCompletion = strCompletion + ": "
|
strCompletion = strCompletion + ":"
|
||||||
}
|
}
|
||||||
} else if len(completions) > 1 {
|
} else if len(completions) > 1 {
|
||||||
for _, completion := range completions {
|
for _, completion := range completions {
|
||||||
@ -608,18 +618,42 @@ func (view *RoomView) InputTabComplete(text string, cursorOffset int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
strCompletions = append(strCompletions, view.autocompleteEmoji(word)...)
|
strCompletions = append(strCompletions, view.parent.cmdProcessor.AutocompleteCommand(word)...)
|
||||||
|
strCompletions = append(strCompletions, view.AutocompleteEmoji(word)...)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *RoomView) InputTabComplete(text string, cursorOffset int) {
|
||||||
|
if len(text) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
str := runewidth.Truncate(text, cursorOffset, "")
|
||||||
|
word := findWordToTabComplete(str)
|
||||||
|
startIndex := len(str) - len(word)
|
||||||
|
|
||||||
|
var strCompletion string
|
||||||
|
|
||||||
|
strCompletions, newText, ok := view.parent.cmdProcessor.Autocomplete(view, text, cursorOffset)
|
||||||
|
if !ok {
|
||||||
|
strCompletions, strCompletion = view.defaultAutocomplete(word, startIndex)
|
||||||
|
}
|
||||||
|
|
||||||
if len(strCompletions) > 0 {
|
if len(strCompletions) > 0 {
|
||||||
strCompletion = util.LongestCommonPrefix(strCompletions)
|
strCompletion = util.LongestCommonPrefix(strCompletions)
|
||||||
sort.Sort(sort.StringSlice(strCompletions))
|
sort.Sort(sort.StringSlice(strCompletions))
|
||||||
}
|
}
|
||||||
|
if len(strCompletion) > 0 && len(strCompletions) < 2 {
|
||||||
if len(strCompletion) > 0 {
|
strCompletion += " "
|
||||||
text = str[0:startIndex] + strCompletion + text[len(str):]
|
strCompletions = []string{}
|
||||||
}
|
}
|
||||||
|
|
||||||
view.input.SetTextAndMoveCursor(text)
|
if len(strCompletion) > 0 && newText == text {
|
||||||
|
newText = str[0:startIndex] + strCompletion + text[len(str):]
|
||||||
|
}
|
||||||
|
|
||||||
|
view.input.SetTextAndMoveCursor(newText)
|
||||||
view.SetCompletions(strCompletions)
|
view.SetCompletions(strCompletions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
245
ui/verification-modal.go
Normal file
245
ui/verification-modal.go
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
// gomuks - A terminal Matrix client written in Go.
|
||||||
|
// Copyright (C) 2020 Tulir Asokan
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// +build cgo
|
||||||
|
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"maunium.net/go/mauview"
|
||||||
|
"maunium.net/go/tcell"
|
||||||
|
|
||||||
|
"maunium.net/go/gomuks/debug"
|
||||||
|
"maunium.net/go/mautrix/crypto"
|
||||||
|
"maunium.net/go/mautrix/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EmojiView struct {
|
||||||
|
mauview.SimpleEventHandler
|
||||||
|
Data crypto.SASData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EmojiView) Draw(screen mauview.Screen) {
|
||||||
|
if e.Data == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch e.Data.Type() {
|
||||||
|
case event.SASEmoji:
|
||||||
|
width := 10
|
||||||
|
for i, emoji := range e.Data.(crypto.EmojiSASData) {
|
||||||
|
x := i*width + i
|
||||||
|
y := 0
|
||||||
|
if i >= 4 {
|
||||||
|
x = (i-4)*width + i
|
||||||
|
y = 2
|
||||||
|
}
|
||||||
|
mauview.Print(screen, string(emoji.Emoji), x, y, width, mauview.AlignCenter, tcell.ColorDefault)
|
||||||
|
mauview.Print(screen, emoji.Description, x, y+1, width, mauview.AlignCenter, tcell.ColorDefault)
|
||||||
|
}
|
||||||
|
case event.SASDecimal:
|
||||||
|
maxWidth := 43
|
||||||
|
for i, number := range e.Data.(crypto.DecimalSASData) {
|
||||||
|
mauview.Print(screen, strconv.FormatUint(uint64(number), 10), 0, i, maxWidth, mauview.AlignCenter, tcell.ColorDefault)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type VerificationModal struct {
|
||||||
|
mauview.Component
|
||||||
|
|
||||||
|
device *crypto.DeviceIdentity
|
||||||
|
|
||||||
|
container *mauview.Box
|
||||||
|
|
||||||
|
waitingBar *mauview.ProgressBar
|
||||||
|
infoText *mauview.TextView
|
||||||
|
emojiText *EmojiView
|
||||||
|
inputBar *mauview.InputField
|
||||||
|
|
||||||
|
progress int
|
||||||
|
progressMax int
|
||||||
|
stopWaiting chan struct{}
|
||||||
|
confirmChan chan bool
|
||||||
|
done bool
|
||||||
|
|
||||||
|
parent *MainView
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVerificationModal(mainView *MainView, device *crypto.DeviceIdentity, timeout time.Duration) *VerificationModal {
|
||||||
|
vm := &VerificationModal{
|
||||||
|
parent: mainView,
|
||||||
|
device: device,
|
||||||
|
stopWaiting: make(chan struct{}),
|
||||||
|
confirmChan: make(chan bool),
|
||||||
|
done: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.progressMax = int(timeout.Seconds())
|
||||||
|
vm.progress = vm.progressMax
|
||||||
|
vm.waitingBar = mauview.NewProgressBar().
|
||||||
|
SetMax(vm.progressMax).
|
||||||
|
SetProgress(vm.progress).
|
||||||
|
SetIndeterminate(false)
|
||||||
|
|
||||||
|
vm.infoText = mauview.NewTextView()
|
||||||
|
vm.infoText.SetText(fmt.Sprintf("Waiting for %s\nto accept", device.UserID))
|
||||||
|
|
||||||
|
vm.emojiText = &EmojiView{}
|
||||||
|
|
||||||
|
vm.inputBar = mauview.NewInputField().
|
||||||
|
SetBackgroundColor(tcell.ColorDefault).
|
||||||
|
SetPlaceholderTextColor(tcell.ColorWhite)
|
||||||
|
|
||||||
|
flex := mauview.NewFlex().
|
||||||
|
SetDirection(mauview.FlexRow).
|
||||||
|
AddFixedComponent(vm.waitingBar, 1).
|
||||||
|
AddFixedComponent(vm.infoText, 4).
|
||||||
|
AddFixedComponent(vm.emojiText, 4).
|
||||||
|
AddFixedComponent(vm.inputBar, 1)
|
||||||
|
|
||||||
|
vm.container = mauview.NewBox(flex).
|
||||||
|
SetBorder(true).
|
||||||
|
SetTitle("Interactive verification")
|
||||||
|
|
||||||
|
vm.Component = mauview.Center(vm.container, 45, 12).SetAlwaysFocusChild(true)
|
||||||
|
|
||||||
|
go vm.decrementWaitingBar()
|
||||||
|
|
||||||
|
return vm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *VerificationModal) decrementWaitingBar() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-time.Tick(time.Second):
|
||||||
|
if vm.progress <= 0 {
|
||||||
|
vm.waitingBar.SetIndeterminate(true)
|
||||||
|
vm.parent.parent.app.SetRedrawTicker(100 * time.Millisecond)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
vm.progress--
|
||||||
|
vm.waitingBar.SetProgress(vm.progress)
|
||||||
|
vm.parent.parent.Render()
|
||||||
|
case <-vm.stopWaiting:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *VerificationModal) VerificationMethods() []crypto.VerificationMethod {
|
||||||
|
return []crypto.VerificationMethod{crypto.VerificationMethodEmoji{}, crypto.VerificationMethodDecimal{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *VerificationModal) VerifySASMatch(_ *crypto.DeviceIdentity, data crypto.SASData) bool {
|
||||||
|
var typeName string
|
||||||
|
if data.Type() == event.SASDecimal {
|
||||||
|
typeName = "numbers"
|
||||||
|
} else if data.Type() == event.SASEmoji {
|
||||||
|
typeName = "emojis"
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
vm.infoText.SetText(fmt.Sprintf(
|
||||||
|
"Check if the other device is showing the\n"+
|
||||||
|
"same %s as below, then type \"yes\" to\n"+
|
||||||
|
"accept, or \"no\" to reject", typeName))
|
||||||
|
vm.inputBar.
|
||||||
|
SetTextColor(tcell.ColorWhite).
|
||||||
|
SetBackgroundColor(tcell.ColorDarkCyan).
|
||||||
|
SetPlaceholder("Type \"yes\" or \"no\"").
|
||||||
|
Focus()
|
||||||
|
vm.emojiText.Data = data
|
||||||
|
vm.parent.parent.Render()
|
||||||
|
vm.progress = vm.progressMax
|
||||||
|
confirm := <-vm.confirmChan
|
||||||
|
vm.progress = vm.progressMax
|
||||||
|
vm.emojiText.Data = nil
|
||||||
|
vm.infoText.SetText(fmt.Sprintf("Waiting for %s\nto confirm", vm.device.UserID))
|
||||||
|
vm.parent.parent.Render()
|
||||||
|
return confirm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *VerificationModal) OnCancel(cancelledByUs bool, reason string, _ event.VerificationCancelCode) {
|
||||||
|
vm.waitingBar.SetIndeterminate(false).SetMax(100).SetProgress(100)
|
||||||
|
vm.parent.parent.app.SetRedrawTicker(1 * time.Minute)
|
||||||
|
if cancelledByUs {
|
||||||
|
vm.infoText.SetText(fmt.Sprintf("Verification failed: %s", reason))
|
||||||
|
} else {
|
||||||
|
vm.infoText.SetText(fmt.Sprintf("Verification cancelled by %s: %s", vm.device.UserID, reason))
|
||||||
|
}
|
||||||
|
vm.inputBar.SetPlaceholder("Press enter to close the dialog")
|
||||||
|
vm.stopWaiting <- struct{}{}
|
||||||
|
vm.done = true
|
||||||
|
vm.parent.parent.Render()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *VerificationModal) OnSuccess() {
|
||||||
|
vm.waitingBar.SetIndeterminate(false).SetMax(100).SetProgress(100)
|
||||||
|
vm.parent.parent.app.SetRedrawTicker(1 * time.Minute)
|
||||||
|
vm.infoText.SetText(fmt.Sprintf("Successfully verified %s (%s) of %s", vm.device.Name, vm.device.DeviceID, vm.device.UserID))
|
||||||
|
vm.inputBar.SetPlaceholder("Press enter to close the dialog")
|
||||||
|
vm.stopWaiting <- struct{}{}
|
||||||
|
vm.done = true
|
||||||
|
vm.parent.parent.Render()
|
||||||
|
if vm.parent.config.SendToVerifiedOnly {
|
||||||
|
// Hacky way to make new group sessions after verified
|
||||||
|
vm.parent.matrix.Crypto().(*crypto.OlmMachine).OnDevicesChanged(vm.device.UserID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *VerificationModal) OnKeyEvent(event mauview.KeyEvent) bool {
|
||||||
|
if vm.done {
|
||||||
|
if event.Key() == tcell.KeyEnter || event.Key() == tcell.KeyEsc {
|
||||||
|
vm.parent.HideModal()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
} else if vm.emojiText.Data == nil {
|
||||||
|
debug.Print("Ignoring pre-emoji key event")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if event.Key() == tcell.KeyEnter {
|
||||||
|
text := strings.ToLower(strings.TrimSpace(vm.inputBar.GetText()))
|
||||||
|
if text == "yes" {
|
||||||
|
debug.Print("Confirming verification")
|
||||||
|
vm.confirmChan <- true
|
||||||
|
} else if text == "no" {
|
||||||
|
debug.Print("Rejecting verification")
|
||||||
|
vm.confirmChan <- false
|
||||||
|
}
|
||||||
|
vm.inputBar.
|
||||||
|
SetPlaceholder("").
|
||||||
|
SetTextAndMoveCursor("").
|
||||||
|
SetBackgroundColor(tcell.ColorDefault).
|
||||||
|
SetTextColor(tcell.ColorDefault)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return vm.inputBar.OnKeyEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *VerificationModal) Focus() {
|
||||||
|
vm.container.Focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *VerificationModal) Blur() {
|
||||||
|
vm.container.Blur()
|
||||||
|
}
|
@ -22,15 +22,15 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
|
||||||
|
|
||||||
sync "github.com/sasha-s/go-deadlock"
|
sync "github.com/sasha-s/go-deadlock"
|
||||||
|
|
||||||
"maunium.net/go/gomuks/ui/messages"
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
"maunium.net/go/mauview"
|
"maunium.net/go/mauview"
|
||||||
"maunium.net/go/tcell"
|
"maunium.net/go/tcell"
|
||||||
|
|
||||||
|
"maunium.net/go/gomuks/ui/messages"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
"maunium.net/go/gomuks/config"
|
"maunium.net/go/gomuks/config"
|
||||||
"maunium.net/go/gomuks/debug"
|
"maunium.net/go/gomuks/debug"
|
||||||
"maunium.net/go/gomuks/interface"
|
"maunium.net/go/gomuks/interface"
|
||||||
@ -139,18 +139,6 @@ func (view *MainView) InputChanged(roomView *RoomView, text string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func findWordToTabComplete(text string) string {
|
|
||||||
output := ""
|
|
||||||
runes := []rune(text)
|
|
||||||
for i := len(runes) - 1; i >= 0; i-- {
|
|
||||||
if unicode.IsSpace(runes[i]) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
output = string(runes[i]) + output
|
|
||||||
}
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
func (view *MainView) ShowBare(roomView *RoomView) {
|
func (view *MainView) ShowBare(roomView *RoomView) {
|
||||||
if roomView == nil {
|
if roomView == nil {
|
||||||
return
|
return
|
||||||
|
Loading…
Reference in New Issue
Block a user