From 456a15ba56dfdb1ab7c9457cf3ed687bfd1a2460 Mon Sep 17 00:00:00 2001 From: Gary Kramlich Date: Thu, 30 Dec 2021 09:33:06 -0600 Subject: [PATCH] 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 --- bridge/bridge.go | 24 +++++++ bridge/matrix.go | 112 +++++++++++++++++++++++++++-- bridge/portal.go | 95 ++++++++++++++++++++++++ bridge/puppet.go | 87 ++++++++++++++++++++++ bridge/user.go | 93 ++++++++++++++++++++++++ config/bridge.go | 12 ++++ config/managementroomtext.go | 38 ++++++++++ database/database.go | 19 +++++ database/migrations/01-initial.sql | 35 +++++++-- database/portal.go | 53 ++++++++++++++ database/portalkey.go | 13 ++++ database/portalquery.go | 58 +++++++++++++++ database/puppet.go | 56 +++++++++++++++ database/puppetquery.go | 28 ++++++++ database/scannable.go | 5 ++ database/user.go | 83 +++++++++++++++++++++ database/userquery.go | 27 +++++++ go.mod | 11 +-- go.sum | 26 +++++++ 19 files changed, 859 insertions(+), 16 deletions(-) create mode 100644 bridge/portal.go create mode 100644 bridge/puppet.go create mode 100644 bridge/user.go create mode 100644 config/managementroomtext.go create mode 100644 database/portal.go create mode 100644 database/portalkey.go create mode 100644 database/portalquery.go create mode 100644 database/puppet.go create mode 100644 database/puppetquery.go create mode 100644 database/scannable.go create mode 100644 database/user.go create mode 100644 database/userquery.go diff --git a/bridge/bridge.go b/bridge/bridge.go index 46d01a1..56a4e08 100644 --- a/bridge/bridge.go +++ b/bridge/bridge.go @@ -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 diff --git a/bridge/matrix.go b/bridge/matrix.go index 607544f..43f24cc 100644 --- a/bridge/matrix.go +++ b/bridge/matrix.go @@ -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) } } diff --git a/bridge/portal.go b/bridge/portal.go new file mode 100644 index 0000000..f5f715e --- /dev/null +++ b/bridge/portal.go @@ -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) + } + } +} diff --git a/bridge/puppet.go b/bridge/puppet.go new file mode 100644 index 0000000..8d2423c --- /dev/null +++ b/bridge/puppet.go @@ -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, + ) +} diff --git a/bridge/user.go b/bridge/user.go new file mode 100644 index 0000000..6498d5e --- /dev/null +++ b/bridge/user.go @@ -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 +} diff --git a/config/bridge.go b/config/bridge.go index afe425f..c287ff2 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -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 } diff --git a/config/managementroomtext.go b/config/managementroomtext.go new file mode 100644 index 0000000..3d83804 --- /dev/null +++ b/config/managementroomtext.go @@ -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() +} diff --git a/database/database.go b/database/database.go index 7af8509..2edcc30 100644 --- a/database/database.go +++ b/database/database.go @@ -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 } diff --git a/database/migrations/01-initial.sql b/database/migrations/01-initial.sql index 05304f8..d0a325f 100644 --- a/database/migrations/01-initial.sql +++ b/database/migrations/01-initial.sql @@ -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 ); diff --git a/database/portal.go b/database/portal.go new file mode 100644 index 0000000..9e60c08 --- /dev/null +++ b/database/portal.go @@ -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) + } +} diff --git a/database/portalkey.go b/database/portalkey.go new file mode 100644 index 0000000..a4252b9 --- /dev/null +++ b/database/portalkey.go @@ -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 +} diff --git a/database/portalquery.go b/database/portalquery.go new file mode 100644 index 0000000..a1fd52f --- /dev/null +++ b/database/portalquery.go @@ -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) +} diff --git a/database/puppet.go b/database/puppet.go new file mode 100644 index 0000000..8603c7b --- /dev/null +++ b/database/puppet.go @@ -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) + } +} diff --git a/database/puppetquery.go b/database/puppetquery.go new file mode 100644 index 0000000..1b318b0 --- /dev/null +++ b/database/puppetquery.go @@ -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) +} diff --git a/database/scannable.go b/database/scannable.go new file mode 100644 index 0000000..66ad2fd --- /dev/null +++ b/database/scannable.go @@ -0,0 +1,5 @@ +package database + +type Scannable interface { + Scan(...interface{}) error +} diff --git a/database/user.go b/database/user.go new file mode 100644 index 0000000..13cc5c5 --- /dev/null +++ b/database/user.go @@ -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) + } +} diff --git a/database/userquery.go b/database/userquery.go new file mode 100644 index 0000000..2189e45 --- /dev/null +++ b/database/userquery.go @@ -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) +} diff --git a/go.mod b/go.mod index 8556e79..54812fc 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 688693a..6c66625 100644 --- a/go.sum +++ b/go.sum @@ -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=