Initial bot functionality

* The bot now properly joins the management room
* The management room is persisted in the database
* Welcome/help messages are sent in the management room
This commit is contained in:
Gary Kramlich
2021-12-30 09:33:06 -06:00
parent 78ab3d3804
commit 456a15ba56
19 changed files with 859 additions and 16 deletions

View File

@@ -3,11 +3,13 @@ package bridge
import (
"errors"
"fmt"
"sync"
"time"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/id"
"gitlab.com/beeper/discord/config"
"gitlab.com/beeper/discord/database"
@@ -28,6 +30,20 @@ type Bridge struct {
eventProcessor *appservice.EventProcessor
matrixHandler *matrixHandler
bot *appservice.IntentAPI
usersByMXID map[id.UserID]*User
usersByID map[string]*User
usersLock sync.Mutex
managementRooms map[id.RoomID]*User
managementRoomsLock sync.Mutex
portalsByMXID map[id.RoomID]*Portal
portalsByID map[database.PortalKey]*Portal
portalsLock sync.Mutex
puppets map[string]*Puppet
puppetsLock sync.Mutex
}
func New(cfg *config.Config) (*Bridge, error) {
@@ -64,6 +80,14 @@ func New(cfg *config.Config) (*Bridge, error) {
bot: bot,
config: cfg,
log: logger,
usersByMXID: make(map[id.UserID]*User),
usersByID: make(map[string]*User),
managementRooms: make(map[id.RoomID]*User),
portalsByMXID: make(map[id.RoomID]*Portal),
portalsByID: make(map[database.PortalKey]*Portal),
}
// Setup the event processors

View File

@@ -5,6 +5,7 @@ import (
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
)
@@ -60,16 +61,92 @@ func (mh *matrixHandler) ignoreEvent(evt *event.Event) bool {
}
func (mh *matrixHandler) handleMessage(evt *event.Event) {
mh.log.Debugfln("received message from %q: %q", evt.Sender, evt.Content.AsMessage())
if mh.ignoreEvent(evt) {
return
}
mh.log.Debugfln("received message from %q: %q", evt.Sender, evt.Content.AsMessage())
}
func (mh *matrixHandler) joinAndCheckMembers(evt *event.Event, intent *appservice.IntentAPI) *mautrix.RespJoinedMembers {
resp, err := intent.JoinRoomByID(evt.RoomID)
if err != nil {
mh.log.Debugfln("Failed to join room %q as %q with invite from %q: %v", evt.RoomID, intent.UserID, evt.Sender, err)
return nil
}
members, err := intent.JoinedMembers(resp.RoomID)
if err != nil {
mh.log.Debugfln("Failed to get members in room %q with invite from %q as %q: %v", resp.RoomID, evt.Sender, intent.UserID, err)
return nil
}
if len(members.Joined) < 2 {
mh.log.Debugfln("Leaving empty room %q with invite from %q as %q", resp.RoomID, evt.Sender, intent.UserID)
intent.LeaveRoom(resp.RoomID)
return nil
}
return members
}
func (mh *matrixHandler) sendNoticeWithmarkdown(roomID id.RoomID, message string) (*mautrix.RespSendEvent, error) {
intent := mh.as.BotIntent()
content := format.RenderMarkdown(message, true, false)
content.MsgType = event.MsgNotice
return intent.SendMessageEvent(roomID, event.EventMessage, content)
}
func (mh *matrixHandler) handleBotInvite(evt *event.Event) {
intent := mh.as.BotIntent()
user := mh.bridge.GetUserByMXID(evt.Sender)
if user == nil {
return
}
members := mh.joinAndCheckMembers(evt, intent)
if members == nil {
return
}
// If this is a DM and the user doesn't have a management room, make this
// the management room.
if len(members.Joined) == 2 && (user.ManagementRoom == "" || evt.Content.AsMember().IsDirect) {
user.SetManagementRoom(evt.RoomID)
intent.SendNotice(user.ManagementRoom, "This room has been registered as your bridge management/status room")
mh.log.Debugfln("%q registered as management room with %q", evt.RoomID, evt.Sender)
}
// Wait to send the welcome message until we're sure we're not in an empty
// room.
mh.sendNoticeWithmarkdown(evt.RoomID, mh.bridge.config.Bridge.ManagementRoomText.Welcome)
if evt.RoomID == user.ManagementRoom {
if user.HasSession() {
mh.sendNoticeWithmarkdown(evt.RoomID, mh.bridge.config.Bridge.ManagementRoomText.Connected)
} else {
mh.sendNoticeWithmarkdown(evt.RoomID, mh.bridge.config.Bridge.ManagementRoomText.NotConnected)
}
additionalHelp := mh.bridge.config.Bridge.ManagementRoomText.AdditionalHelp
if additionalHelp != "" {
mh.sendNoticeWithmarkdown(evt.RoomID, additionalHelp)
}
}
}
func (mh *matrixHandler) handlePuppetInvite(evt *event.Event, inviter *User, puppet *Puppet) {
mh.log.Warnln("handling puppet invite!")
}
func (mh *matrixHandler) handleMembership(evt *event.Event) {
mh.log.Debugfln("recevied invite %#v\n", evt)
// Return early if we're supposed to ignore the event.
if mh.ignoreEvent(evt) {
return
@@ -78,12 +155,37 @@ func (mh *matrixHandler) handleMembership(evt *event.Event) {
// Grab the content of the event.
content := evt.Content.AsMember()
// TODO: handle invites from ourselfs?
// Check if this is a new conversation from a matrix user to the bot
if content.Membership == event.MembershipInvite && id.UserID(evt.GetStateKey()) == mh.as.BotMXID() {
mh.handleBotInvite(evt)
return
}
// Load or create a new user.
user := mh.bridge.GetUserByMXID(evt.Sender)
if user == nil {
return
}
// Load or create a new portal.
portal := mh.bridge.GetPortalByMXID(evt.RoomID)
if portal == nil {
puppet := mh.bridge.GetPuppetByMXID(id.UserID(evt.GetStateKey()))
if content.Membership == event.MembershipInvite && puppet != nil {
mh.handlePuppetInvite(evt, user, puppet)
}
mh.log.Warnln("no existing portal for", evt.RoomID)
return
}
isSelf := id.UserID(evt.GetStateKey()) == evt.Sender
// Handle matrix invites.
if content.Membership == event.MembershipInvite && !isSelf {
//
portal.HandleMatrixInvite(user, evt)
}
}

95
bridge/portal.go Normal file
View File

@@ -0,0 +1,95 @@
package bridge
import (
"fmt"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"gitlab.com/beeper/discord/database"
)
type PortalMatrixMessage struct {
evt *event.Event
user *User
}
type Portal struct {
*database.Portal
bridge *Bridge
log log.Logger
matrixMessages chan PortalMatrixMessage
}
func (b *Bridge) loadPortal(dbPortal *database.Portal, key *database.PortalKey) *Portal {
// If we weren't given a portal we'll attempt to create it if a key was
// provided.
if dbPortal == nil {
if key == nil {
return nil
}
dbPortal = b.db.Portal.New()
dbPortal.Key = *key
dbPortal.Insert()
}
portal := b.NewPortal(dbPortal)
// No need to lock, it is assumed that our callers have already acquired
// the lock.
b.portalsByID[portal.Key] = portal
if portal.MXID != "" {
b.portalsByMXID[portal.MXID] = portal
}
return portal
}
func (b *Bridge) GetPortalByMXID(mxid id.RoomID) *Portal {
b.portalsLock.Lock()
defer b.portalsLock.Unlock()
portal, ok := b.portalsByMXID[mxid]
if !ok {
return b.loadPortal(b.db.Portal.GetByMXID(mxid), nil)
}
return portal
}
func (b *Bridge) NewPortal(dbPortal *database.Portal) *Portal {
portal := &Portal{
Portal: dbPortal,
bridge: b,
log: b.log.Sub(fmt.Sprintf("Portal/%s", dbPortal.Key)),
matrixMessages: make(chan PortalMatrixMessage, b.config.Bridge.PortalMessageBuffer),
}
go portal.messageLoop()
return portal
}
func (p *Portal) HandleMatrixInvite(sender *User, evt *event.Event) {
// Look up an existing puppet or create a new one.
puppet := p.bridge.GetPuppetByMXID(id.UserID(evt.GetStateKey()))
if puppet != nil {
p.log.Infoln("no puppet for %v", sender)
// Open a conversation on the discord side?
}
p.log.Infoln("puppet:", puppet)
}
func (p *Portal) messageLoop() {
for {
select {
case msg := <-p.matrixMessages:
p.log.Infoln("got message", msg)
}
}
}

87
bridge/puppet.go Normal file
View File

@@ -0,0 +1,87 @@
package bridge
import (
"fmt"
"regexp"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
"gitlab.com/beeper/discord/database"
)
type Puppet struct {
*database.Puppet
bridge *Bridge
log log.Logger
MXID id.UserID
}
var userIDRegex *regexp.Regexp
func (b *Bridge) NewPuppet(dbPuppet *database.Puppet) *Puppet {
return &Puppet{
Puppet: dbPuppet,
bridge: b,
log: b.log.Sub(fmt.Sprintf("Puppet/%s", dbPuppet.ID)),
MXID: b.FormatPuppetMXID(dbPuppet.ID),
}
}
func (b *Bridge) ParsePuppetMXID(mxid id.UserID) (string, bool) {
if userIDRegex == nil {
pattern := fmt.Sprintf(
"^@%s:%s$",
b.config.Bridge.FormatUsername("([0-9]+)"),
b.config.Homeserver.Domain,
)
userIDRegex = regexp.MustCompile(pattern)
}
match := userIDRegex.FindStringSubmatch(string(mxid))
if len(match) == 2 {
return match[1], true
}
return "", false
}
func (b *Bridge) GetPuppetByMXID(mxid id.UserID) *Puppet {
id, ok := b.ParsePuppetMXID(mxid)
if !ok {
return nil
}
return b.GetPuppetByID(id)
}
func (b *Bridge) GetPuppetByID(id string) *Puppet {
b.puppetsLock.Lock()
defer b.puppetsLock.Unlock()
puppet, ok := b.puppets[id]
if !ok {
dbPuppet := b.db.Puppet.Get(id)
if dbPuppet == nil {
dbPuppet = b.db.Puppet.New()
dbPuppet.ID = id
dbPuppet.Insert()
}
puppet = b.NewPuppet(dbPuppet)
b.puppets[puppet.ID] = puppet
}
return puppet
}
func (b *Bridge) FormatPuppetMXID(did string) id.UserID {
return id.NewUserID(
b.config.Bridge.FormatUsername(did),
b.config.Homeserver.Domain,
)
}

93
bridge/user.go Normal file
View File

@@ -0,0 +1,93 @@
package bridge
import (
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
"gitlab.com/beeper/discord/database"
)
type User struct {
*database.User
bridge *Bridge
log log.Logger
}
func (b *Bridge) loadUser(dbUser *database.User, mxid *id.UserID) *User {
// If we weren't passed in a user we attempt to create one if we were given
// a matrix id.
if dbUser == nil {
if mxid == nil {
return nil
}
dbUser = b.db.User.New()
dbUser.MXID = *mxid
dbUser.Insert()
}
user := b.NewUser(dbUser)
// We assume the usersLock was acquired by our caller.
b.usersByMXID[user.MXID] = user
if user.ID != "" {
b.usersByID[user.ID] = user
}
if user.ManagementRoom != "" {
// Lock the management rooms for our update
b.managementRoomsLock.Lock()
b.managementRooms[user.ManagementRoom] = user
b.managementRoomsLock.Unlock()
}
return user
}
func (b *Bridge) GetUserByMXID(userID id.UserID) *User {
// TODO: check if puppet
b.usersLock.Lock()
defer b.usersLock.Unlock()
user, ok := b.usersByMXID[userID]
if !ok {
return b.loadUser(b.db.User.GetByMXID(userID), &userID)
}
return user
}
func (b *Bridge) NewUser(dbUser *database.User) *User {
user := &User{
User: dbUser,
bridge: b,
log: b.log.Sub("User").Sub(string(dbUser.MXID)),
}
return user
}
func (u *User) SetManagementRoom(roomID id.RoomID) {
u.bridge.managementRoomsLock.Lock()
defer u.bridge.managementRoomsLock.Unlock()
existing, ok := u.bridge.managementRooms[roomID]
if ok {
// If there's a user already assigned to this management room, clear it
// out.
// I think this is due a name change or something? I dunno, leaving it
// for now.
existing.ManagementRoom = ""
existing.Update()
}
u.ManagementRoom = roomID
u.bridge.managementRooms[u.ManagementRoom] = u
u.Update()
}
func (u *User) HasSession() bool {
return u.User.Session != nil
}

View File

@@ -8,6 +8,10 @@ import (
type bridge struct {
UsernameTemplate string `yaml:"username_template"`
ManagementRoomText managementRoomText `yaml:"management_root_text"`
PortalMessageBuffer int `yaml:"portal_message_buffer"`
usernameTemplate *template.Template `yaml:"-"`
}
@@ -18,11 +22,19 @@ func (b *bridge) validate() error {
b.UsernameTemplate = "Discord_{{.}}"
}
if b.PortalMessageBuffer <= 0 {
b.PortalMessageBuffer = 128
}
b.usernameTemplate, err = template.New("username").Parse(b.UsernameTemplate)
if err != nil {
return err
}
if err := b.ManagementRoomText.validate(); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,38 @@
package config
type managementRoomText struct {
Welcome string `yaml:"welcome"`
Connected string `yaml:"welcome_connected"`
NotConnected string `yaml:"welcome_unconnected"`
AdditionalHelp string `yaml:"additional_help"`
}
func (m *managementRoomText) validate() error {
if m.Welcome == "" {
m.Welcome = "Greetings, I am a Discord bridge bot!"
}
if m.Connected == "" {
m.Connected = "Use `help` to get started."
}
if m.NotConnected == "" {
m.NotConnected = "Use `help` to get started, or `login` to login."
}
return nil
}
func (m *managementRoomText) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawManagementRoomText managementRoomText
raw := rawManagementRoomText{}
if err := unmarshal(&raw); err != nil {
return err
}
*m = managementRoomText(raw)
return m.validate()
}

View File

@@ -15,6 +15,10 @@ type Database struct {
*sql.DB
log log.Logger
dialect string
User *UserQuery
Portal *PortalQuery
Puppet *PuppetQuery
}
func New(dbType, uri string, maxOpenConns, maxIdleConns int, baseLog log.Logger) (*Database, error) {
@@ -42,5 +46,20 @@ func New(dbType, uri string, maxOpenConns, maxIdleConns int, baseLog log.Logger)
dialect: dbType,
}
db.User = &UserQuery{
db: db,
log: db.log.Sub("User"),
}
db.Portal = &PortalQuery{
db: db,
log: db.log.Sub("Portal"),
}
db.Puppet = &PuppetQuery{
db: db,
log: db.log.Sub("Puppet"),
}
return db, nil
}

View File

@@ -1,11 +1,32 @@
CREATE TABLE IF NOT EXISTS portal (
did text,
receiver text,
mxid text UNIQUE,
id TEXT,
receiver TEXT,
mxid TEXT UNIQUE,
name text NOT NULL,
topic text NOT NULL,
avatar text NOT NULL,
name TEXT NOT NULL,
topic TEXT NOT NULL,
PRIMARY KEY (did, receiver)
avatar TEXT NOT NULL,
avatar_url TEXT NOT NULL,
PRIMARY KEY (id, receiver)
);
CREATE TABLE IF NOT EXISTS puppet (
id TEXT PRIMARY KEY,
displayname TEXT,
avatar TEXT,
avatar_url TEXT,
enable_presence BOOLEAN NOT NULL DEFAULT true
);
CREATE TABLE IF NOT EXISTS user (
mxid TEXT PRIMARY KEY,
id TEXT UNIQUE,
management_room TEXT,
token TEXT
);

53
database/portal.go Normal file
View File

@@ -0,0 +1,53 @@
package database
import (
"database/sql"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
)
type Portal struct {
db *Database
log log.Logger
Key PortalKey
MXID id.RoomID
Name string
Topic string
Avatar string
AvatarURL id.ContentURI
}
func (p *Portal) Scan(row Scannable) *Portal {
var mxid, avatarURL sql.NullString
err := row.Scan(&p.Key.ID, &p.Key.Receiver, &mxid, &p.Name, &p.Topic, &p.Avatar, &avatarURL)
if err != nil {
if err != sql.ErrNoRows {
p.log.Errorln("Database scan failed:", err)
}
return nil
}
p.MXID = id.RoomID(mxid.String)
p.AvatarURL, _ = id.ParseContentURI(avatarURL.String)
return p
}
func (p *Portal) Insert() {
query := "INSERT INTO portal" +
" (id, receiver, mxid, name, topic, avatar, avatar_url)" +
" VALUES ($1, $2, $3, $4, $5, $6, $7)"
_, err := p.db.Exec(query, p.Key.ID, p.Key.Receiver, p.MXID,
p.Name, p.Topic, p.Avatar, p.AvatarURL.String())
if err != nil {
p.log.Warnfln("Failed to insert %s: %v", p.Key, err)
}
}

13
database/portalkey.go Normal file
View File

@@ -0,0 +1,13 @@
package database
type PortalKey struct {
ID string
Receiver string
}
func (key PortalKey) String() string {
if key.Receiver == key.ID {
return key.ID
}
return key.ID + "-" + key.Receiver
}

58
database/portalquery.go Normal file
View File

@@ -0,0 +1,58 @@
package database
import (
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
)
type PortalQuery struct {
db *Database
log log.Logger
}
func (pq *PortalQuery) New() *Portal {
return &Portal{
db: pq.db,
log: pq.log,
}
}
func (pq *PortalQuery) GetAll() []*Portal {
return pq.getAll("SELECT * FROM portal")
}
func (pq *PortalQuery) GetByDID(key PortalKey) *Portal {
return pq.get("SELECT * FROM portal WHERE did=$1 AND receiver=$2", key.ID, key.Receiver)
}
func (pq *PortalQuery) GetByMXID(mxid id.RoomID) *Portal {
return pq.get("SELECT * FROM portal WHERE mxid=$1", mxid)
}
func (pq *PortalQuery) GetAllByDID(did string) []*Portal {
return pq.getAll("SELECT * FROM portal WHERE did=$1", did)
}
func (pq *PortalQuery) getAll(query string, args ...interface{}) []*Portal {
rows, err := pq.db.Query(query, args...)
if err != nil || rows == nil {
return nil
}
defer rows.Close()
portals := []*Portal{}
for rows.Next() {
portals = append(portals, pq.New().Scan(rows))
}
return portals
}
func (pq *PortalQuery) get(query string, args ...interface{}) *Portal {
row := pq.db.QueryRow(query, args...)
if row == nil {
return nil
}
return pq.New().Scan(row)
}

56
database/puppet.go Normal file
View File

@@ -0,0 +1,56 @@
package database
import (
"database/sql"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
)
type Puppet struct {
db *Database
log log.Logger
ID string
DisplayName string
Avatar string
AvatarURL id.ContentURI
EnablePresence bool
}
func (p *Puppet) Scan(row Scannable) *Puppet {
var did, displayName, avatar, avatarURL sql.NullString
var enablePresence sql.NullBool
err := row.Scan(&did, &displayName, &avatar, &avatarURL, &enablePresence)
if err != nil {
if err != sql.ErrNoRows {
p.log.Errorln("Database scan failed:", err)
}
return nil
}
p.ID = did.String
p.DisplayName = displayName.String
p.Avatar = avatar.String
p.AvatarURL, _ = id.ParseContentURI(avatarURL.String)
p.EnablePresence = enablePresence.Bool
return p
}
func (p *Puppet) Insert() {
query := "INSERT INTO puppet" +
" (id, display_name, avatar, avatar_url, enable_presence)" +
" VALUES ($1, $2, $3, $4, $5)"
_, err := p.db.Exec(query, p.ID, p.DisplayName, p.Avatar,
p.AvatarURL.String(), p.EnablePresence)
if err != nil {
p.log.Warnfln("Failed to insert %s: %v", p.ID, err)
}
}

28
database/puppetquery.go Normal file
View File

@@ -0,0 +1,28 @@
package database
import (
log "maunium.net/go/maulogger/v2"
)
type PuppetQuery struct {
db *Database
log log.Logger
}
func (pq *PuppetQuery) New() *Puppet {
return &Puppet{
db: pq.db,
log: pq.log,
EnablePresence: true,
}
}
func (pq *PuppetQuery) Get(id string) *Puppet {
row := pq.db.QueryRow("SELECT id, displayname, avatar, avatar_url, enable_presence FROM puppet WHERE id=$1", id)
if row == nil {
return nil
}
return pq.New().Scan(row)
}

5
database/scannable.go Normal file
View File

@@ -0,0 +1,5 @@
package database
type Scannable interface {
Scan(...interface{}) error
}

83
database/user.go Normal file
View File

@@ -0,0 +1,83 @@
package database
import (
"database/sql"
"github.com/bwmarrin/discordgo"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
)
type User struct {
db *Database
log log.Logger
MXID id.UserID
ID string
ManagementRoom id.RoomID
Session *discordgo.Session
}
func (u *User) Scan(row Scannable) *User {
var token sql.NullString
err := row.Scan(&u.MXID, &u.ID, &u.ManagementRoom, &token)
if err != nil {
if err != sql.ErrNoRows {
u.log.Errorln("Database scan failed:", err)
}
return nil
}
if token.Valid {
session, err := discordgo.New("Bearer " + token.String)
if err != nil {
u.log.Errorln("Failed to create discord session:", err)
} else {
u.Session = session
}
}
return u
}
func (u *User) sessionNonptr() discordgo.Session {
if u.Session != nil {
return *u.Session
}
return discordgo.Session{}
}
func (u *User) Insert() {
session := u.sessionNonptr()
query := "INSERT INTO user" +
" (mxid, id, management_room, token)" +
" VALUES ($1, $2, $3, $4);"
_, err := u.db.Exec(query, u.MXID, u.ID, u.ManagementRoom,
session.Identify.Token)
if err != nil {
u.log.Warnfln("Failed to insert %s: %v", u.MXID, err)
}
}
func (u *User) Update() {
session := u.sessionNonptr()
query := "UPDATE user SET" +
" id=$1, management_room=$2, token=$3" +
" WHERE mxid=$4;"
_, err := u.db.Exec(query, u.ID, u.ManagementRoom, session.Identify.Token, u.MXID)
if err != nil {
u.log.Warnfln("Failed to update %q: %v", u.MXID, err)
}
}

27
database/userquery.go Normal file
View File

@@ -0,0 +1,27 @@
package database
import (
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
)
type UserQuery struct {
db *Database
log log.Logger
}
func (uq *UserQuery) New() *User {
return &User{
db: uq.db,
log: uq.log,
}
}
func (uq *UserQuery) GetByMXID(userID id.UserID) *User {
row := uq.db.QueryRow("SELECT mxid, id, management_room, token FROM user where mxid=$1", userID)
if row == nil {
return nil
}
return uq.New().Scan(row)
}

11
go.mod
View File

@@ -6,17 +6,20 @@ require (
github.com/alecthomas/kong v0.2.18
github.com/lib/pq v1.9.0
github.com/lopezator/migrator v0.3.0
github.com/mattn/go-sqlite3 v1.14.6
github.com/mattn/go-sqlite3 v1.14.9
gopkg.in/yaml.v2 v2.4.0
maunium.net/go/maulogger/v2 v2.3.1
maunium.net/go/mautrix v0.9.27
maunium.net/go/mautrix v0.10.8
)
require (
github.com/btcsuite/btcutil v1.0.2 // indirect
github.com/bwmarrin/discordgo v0.23.2 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 // indirect
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
)

26
go.sum
View File

@@ -11,6 +11,8 @@ github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVa
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4=
github.com/bwmarrin/discordgo v0.23.2/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -21,6 +23,7 @@ github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@@ -34,6 +37,8 @@ github.com/lopezator/migrator v0.3.0 h1:VW/rR+J8NYwPdkBxjrFdjwejpgvP59LbmANJxXuN
github.com/lopezator/migrator v0.3.0/go.mod h1:bpVAVPkWSvTw8ya2Pk7E/KiNAyDWNImgivQY79o8/8I=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
@@ -41,35 +46,52 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tidwall/gjson v1.6.8/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI=
github.com/tidwall/gjson v1.10.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.1.5/go.mod h1:VuJzsZnTowhSxWdOgsAnb886i4AjEyTkk7tNtsL7EYE=
github.com/tidwall/sjson v1.2.3/go.mod h1:5WdjKx3AQMvCJ4RG6/2UYT7dLrGvJUV1x4jdTAyGvZs=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d h1:1aflnvSoWWLI2k/dMUAl5lvU1YO4Mb4hz0gh+1rjcxU=
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM=
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -89,3 +111,7 @@ maunium.net/go/maulogger/v2 v2.3.1 h1:fwBYJne0pHvJrrIPHK+TAPfyxxbBEz46oVGez2x0OD
maunium.net/go/maulogger/v2 v2.3.1/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
maunium.net/go/mautrix v0.9.27 h1:6MV6YSCGqfw8Rb0G1PHjTOkYkTY0vcZaz6wd+U+V1Is=
maunium.net/go/mautrix v0.9.27/go.mod h1:7IzKfWvpQtN+W2Lzxc0rLvIxFM3ryKX6Ys3S/ZoWbg8=
maunium.net/go/mautrix v0.10.7 h1:QV5vbCY4g50N7r1ihdG6zEPfaPn/EVYjM5H+qfLy4RM=
maunium.net/go/mautrix v0.10.7/go.mod h1:k4Ng5oci83MEbqPL6KOjPdbU7f8v01KlMjR/zTQ+7mA=
maunium.net/go/mautrix v0.10.8 h1:h64yDl8fMPk3j/tnkb6c5itSo/LZ1QSKQ3ze5zyanUg=
maunium.net/go/mautrix v0.10.8/go.mod h1:k4Ng5oci83MEbqPL6KOjPdbU7f8v01KlMjR/zTQ+7mA=