2022-05-22 19:16:42 +00:00
package main
import (
"errors"
"fmt"
2023-01-28 13:43:16 +00:00
"math/rand"
2022-05-22 19:16:42 +00:00
"net/http"
2022-05-28 20:03:24 +00:00
"os"
2023-04-20 11:27:37 +00:00
"sort"
2023-01-28 13:43:16 +00:00
"strconv"
2022-05-22 19:16:42 +00:00
"strings"
"sync"
2023-01-28 13:43:16 +00:00
"sync/atomic"
2022-05-28 20:03:24 +00:00
"time"
2022-05-22 19:16:42 +00:00
2023-03-12 12:25:24 +00:00
"github.com/bwmarrin/discordgo"
2022-05-30 21:42:11 +00:00
"github.com/gorilla/websocket"
2023-02-26 19:50:16 +00:00
"github.com/rs/zerolog"
2022-05-28 20:03:24 +00:00
2022-05-22 19:16:42 +00:00
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/bridge"
"maunium.net/go/mautrix/bridge/bridgeconfig"
2022-08-15 13:43:55 +00:00
"maunium.net/go/mautrix/bridge/status"
2022-05-22 19:16:42 +00:00
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
2023-01-20 13:07:18 +00:00
"maunium.net/go/mautrix/pushrules"
2022-05-22 19:16:42 +00:00
"go.mau.fi/mautrix-discord/database"
)
var (
ErrNotConnected = errors . New ( "not connected" )
ErrNotLoggedIn = errors . New ( "not logged in" )
)
type User struct {
* database . User
sync . Mutex
bridge * DiscordBridge
2023-03-12 12:25:24 +00:00
log zerolog . Logger
2022-05-22 19:16:42 +00:00
PermissionLevel bridgeconfig . PermissionLevel
2022-05-28 20:41:34 +00:00
spaceCreateLock sync . Mutex
spaceMembershipChecked bool
dmSpaceMembershipChecked bool
2022-05-22 19:16:42 +00:00
Session * discordgo . Session
2022-05-30 12:58:03 +00:00
2023-01-13 16:04:20 +00:00
BridgeState * bridge . BridgeStateQueue
2023-01-16 16:36:23 +00:00
bridgeStateLock sync . Mutex
wasDisconnected bool
wasLoggedOut bool
2022-07-08 14:06:02 +00:00
markedOpened map [ string ] time . Time
markedOpenedLock sync . Mutex
2023-01-28 13:43:16 +00:00
2023-01-29 11:45:00 +00:00
pendingInteractions map [ string ] * WrappedCommandEvent
pendingInteractionsLock sync . Mutex
2023-01-28 13:43:16 +00:00
nextDiscordUploadID atomic . Int32
2023-04-21 23:50:14 +00:00
relationships map [ string ] * discordgo . Relationship
2022-05-30 12:58:03 +00:00
}
func ( user * User ) GetRemoteID ( ) string {
return user . DiscordID
}
func ( user * User ) GetRemoteName ( ) string {
if user . Session != nil && user . Session . State != nil && user . Session . State . User != nil {
return fmt . Sprintf ( "%s#%s" , user . Session . State . User . Username , user . Session . State . User . Discriminator )
}
return user . DiscordID
2022-05-22 19:16:42 +00:00
}
2023-02-26 19:50:16 +00:00
var discordLog zerolog . Logger
2022-05-28 20:03:24 +00:00
func init ( ) {
discordgo . Logger = func ( msgL , caller int , format string , a ... interface { } ) {
2023-02-26 19:50:16 +00:00
var level zerolog . Level
2022-05-28 20:03:24 +00:00
switch msgL {
case discordgo . LogError :
2023-02-26 19:50:16 +00:00
level = zerolog . ErrorLevel
2022-05-28 20:03:24 +00:00
case discordgo . LogWarning :
2023-02-26 19:50:16 +00:00
level = zerolog . WarnLevel
2022-05-28 20:03:24 +00:00
case discordgo . LogInformational :
2023-02-26 19:50:16 +00:00
level = zerolog . InfoLevel
2022-05-28 20:03:24 +00:00
case discordgo . LogDebug :
2023-02-26 19:50:16 +00:00
level = zerolog . DebugLevel
2022-05-28 20:03:24 +00:00
}
2023-02-26 19:50:16 +00:00
discordLog . WithLevel ( level ) . Caller ( caller + 1 ) . Msgf ( strings . TrimSpace ( format ) , a ... )
2022-05-28 20:03:24 +00:00
}
}
2022-05-22 19:16:42 +00:00
func ( user * User ) GetPermissionLevel ( ) bridgeconfig . PermissionLevel {
return user . PermissionLevel
}
func ( user * User ) GetManagementRoomID ( ) id . RoomID {
return user . ManagementRoom
}
func ( user * User ) GetMXID ( ) id . UserID {
return user . MXID
}
func ( user * User ) GetCommandState ( ) map [ string ] interface { } {
return nil
}
func ( user * User ) GetIDoublePuppet ( ) bridge . DoublePuppet {
p := user . bridge . GetPuppetByCustomMXID ( user . MXID )
if p == nil || p . CustomIntent ( ) == nil {
return nil
}
return p
}
func ( user * User ) GetIGhost ( ) bridge . Ghost {
2022-05-28 20:03:24 +00:00
if user . DiscordID == "" {
2022-05-22 19:16:42 +00:00
return nil
}
2022-05-28 20:03:24 +00:00
p := user . bridge . GetPuppetByID ( user . DiscordID )
2022-05-22 19:16:42 +00:00
if p == nil {
return nil
}
return p
}
var _ bridge . User = ( * User ) ( nil )
func ( br * DiscordBridge ) loadUser ( dbUser * database . User , mxid * id . UserID ) * User {
if dbUser == nil {
if mxid == nil {
return nil
}
dbUser = br . DB . User . New ( )
dbUser . MXID = * mxid
dbUser . Insert ( )
}
user := br . NewUser ( dbUser )
br . usersByMXID [ user . MXID ] = user
2022-05-28 20:03:24 +00:00
if user . DiscordID != "" {
br . usersByID [ user . DiscordID ] = user
2022-05-22 19:16:42 +00:00
}
if user . ManagementRoom != "" {
br . managementRoomsLock . Lock ( )
br . managementRooms [ user . ManagementRoom ] = user
br . managementRoomsLock . Unlock ( )
}
return user
}
func ( br * DiscordBridge ) GetUserByMXID ( userID id . UserID ) * User {
2022-07-08 09:57:21 +00:00
if userID == br . Bot . UserID || br . IsGhost ( userID ) {
return nil
}
2022-05-22 19:16:42 +00:00
br . usersLock . Lock ( )
defer br . usersLock . Unlock ( )
user , ok := br . usersByMXID [ userID ]
if ! ok {
return br . loadUser ( br . DB . User . GetByMXID ( userID ) , & userID )
}
return user
}
func ( br * DiscordBridge ) GetUserByID ( id string ) * User {
br . usersLock . Lock ( )
defer br . usersLock . Unlock ( )
user , ok := br . usersByID [ id ]
if ! ok {
return br . loadUser ( br . DB . User . GetByID ( id ) , nil )
}
return user
}
func ( br * DiscordBridge ) NewUser ( dbUser * database . User ) * User {
user := & User {
User : dbUser ,
bridge : br ,
2023-03-12 12:25:24 +00:00
log : br . ZLog . With ( ) . Str ( "user_id" , string ( dbUser . MXID ) ) . Logger ( ) ,
2022-05-22 19:16:42 +00:00
2022-07-08 14:06:02 +00:00
markedOpened : make ( map [ string ] time . Time ) ,
PermissionLevel : br . Config . Bridge . Permissions . Get ( dbUser . MXID ) ,
2023-01-29 11:45:00 +00:00
pendingInteractions : make ( map [ string ] * WrappedCommandEvent ) ,
2023-04-21 23:50:14 +00:00
relationships : make ( map [ string ] * discordgo . Relationship ) ,
2022-07-08 14:06:02 +00:00
}
2023-01-28 13:43:16 +00:00
user . nextDiscordUploadID . Store ( rand . Int31n ( 100 ) )
2023-02-26 19:50:16 +00:00
user . BridgeState = br . NewBridgeStateQueue ( user )
2022-05-22 19:16:42 +00:00
return user
}
2022-05-28 20:03:24 +00:00
func ( br * DiscordBridge ) getAllUsersWithToken ( ) [ ] * User {
2022-05-22 19:16:42 +00:00
br . usersLock . Lock ( )
defer br . usersLock . Unlock ( )
2022-05-28 20:03:24 +00:00
dbUsers := br . DB . User . GetAllWithToken ( )
2022-05-22 19:16:42 +00:00
users := make ( [ ] * User , len ( dbUsers ) )
for idx , dbUser := range dbUsers {
user , ok := br . usersByMXID [ dbUser . MXID ]
if ! ok {
user = br . loadUser ( dbUser , nil )
}
users [ idx ] = user
}
return users
}
func ( br * DiscordBridge ) startUsers ( ) {
2023-03-12 12:25:24 +00:00
br . ZLog . Debug ( ) . Msg ( "Starting users" )
2022-05-22 19:16:42 +00:00
2022-05-30 12:58:03 +00:00
usersWithToken := br . getAllUsersWithToken ( )
for _ , u := range usersWithToken {
2023-01-24 19:00:17 +00:00
go u . startupTryConnect ( 0 )
2022-05-22 19:16:42 +00:00
}
2022-05-30 12:58:03 +00:00
if len ( usersWithToken ) == 0 {
2022-08-15 13:43:55 +00:00
br . SendGlobalBridgeState ( status . BridgeState { StateEvent : status . StateUnconfigured } . Fill ( nil ) )
2022-05-30 12:58:03 +00:00
}
2022-05-22 19:16:42 +00:00
2023-03-12 12:25:24 +00:00
br . ZLog . Debug ( ) . Msg ( "Starting custom puppets" )
2022-05-22 19:16:42 +00:00
for _ , customPuppet := range br . GetAllPuppetsWithCustomMXID ( ) {
go func ( puppet * Puppet ) {
2023-03-12 12:25:24 +00:00
br . ZLog . Debug ( ) . Str ( "user_id" , puppet . CustomMXID . String ( ) ) . Msg ( "Starting custom puppet" )
2022-05-22 19:16:42 +00:00
if err := puppet . StartCustomMXID ( true ) ; err != nil {
2023-03-12 12:25:24 +00:00
puppet . log . Error ( ) . Err ( err ) . Msg ( "Failed to start custom puppet" )
2022-05-22 19:16:42 +00:00
}
} ( customPuppet )
}
}
2023-01-24 19:00:17 +00:00
func ( user * User ) startupTryConnect ( retryCount int ) {
user . BridgeState . Send ( status . BridgeState { StateEvent : status . StateConnecting } )
err := user . Connect ( )
if err != nil {
2023-03-12 12:25:24 +00:00
user . log . Error ( ) . Err ( err ) . Msg ( "Error connecting on startup" )
2023-01-24 19:00:17 +00:00
closeErr := & websocket . CloseError { }
if errors . As ( err , & closeErr ) && closeErr . Code == 4004 {
user . invalidAuthHandler ( nil , nil )
} else if retryCount < 6 {
user . BridgeState . Send ( status . BridgeState { StateEvent : status . StateTransientDisconnect , Error : "dc-unknown-websocket-error" , Message : err . Error ( ) } )
retryInSeconds := 2 << retryCount
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) . Int ( "retry_in_seconds" , retryInSeconds ) . Msg ( "Sleeping and retrying connection" )
2023-01-24 19:00:17 +00:00
time . Sleep ( time . Duration ( retryInSeconds ) * time . Second )
user . startupTryConnect ( retryCount + 1 )
} else {
user . BridgeState . Send ( status . BridgeState { StateEvent : status . StateUnknownError , Error : "dc-unknown-websocket-error" , Message : err . Error ( ) } )
}
}
}
2022-05-22 19:16:42 +00:00
func ( user * User ) SetManagementRoom ( roomID id . RoomID ) {
user . bridge . managementRoomsLock . Lock ( )
defer user . bridge . managementRoomsLock . Unlock ( )
existing , ok := user . bridge . managementRooms [ roomID ]
if ok {
existing . ManagementRoom = ""
existing . Update ( )
}
user . ManagementRoom = roomID
user . bridge . managementRooms [ user . ManagementRoom ] = user
user . Update ( )
}
2022-07-09 13:33:51 +00:00
func ( user * User ) getSpaceRoom ( ptr * id . RoomID , name , topic string , parent id . RoomID ) id . RoomID {
if len ( * ptr ) > 0 {
return * ptr
}
user . spaceCreateLock . Lock ( )
defer user . spaceCreateLock . Unlock ( )
if len ( * ptr ) > 0 {
return * ptr
}
2022-05-28 20:03:24 +00:00
2022-07-09 13:33:51 +00:00
initialState := [ ] * event . Event { {
Type : event . StateRoomAvatar ,
Content : event . Content {
Parsed : & event . RoomAvatarEventContent {
URL : user . bridge . Config . AppService . Bot . ParsedAvatar ,
} ,
} ,
} }
if parent != "" {
parentIDStr := parent . String ( )
initialState = append ( initialState , & event . Event {
Type : event . StateSpaceParent ,
StateKey : & parentIDStr ,
2022-05-28 20:41:34 +00:00
Content : event . Content {
2022-07-09 13:33:51 +00:00
Parsed : & event . SpaceParentEventContent {
Canonical : true ,
Via : [ ] string { user . bridge . AS . HomeserverDomain } ,
2022-05-28 20:41:34 +00:00
} ,
} ,
2022-07-09 13:33:51 +00:00
} )
}
2022-05-28 20:41:34 +00:00
2022-07-09 13:33:51 +00:00
resp , err := user . bridge . Bot . CreateRoom ( & mautrix . ReqCreateRoom {
Visibility : "private" ,
Name : name ,
Topic : topic ,
InitialState : initialState ,
CreationContent : map [ string ] interface { } {
"type" : event . RoomTypeSpace ,
} ,
PowerLevelOverride : & event . PowerLevelsEventContent {
Users : map [ id . UserID ] int {
user . bridge . Bot . UserID : 9001 ,
user . MXID : 50 ,
2022-05-28 20:03:24 +00:00
} ,
2022-07-09 13:33:51 +00:00
} ,
} )
if err != nil {
2023-03-12 12:25:24 +00:00
user . log . Error ( ) . Err ( err ) . Msg ( "Failed to auto-create space room" )
2022-07-09 13:33:51 +00:00
} else {
* ptr = resp . RoomID
user . Update ( )
user . ensureInvited ( nil , * ptr , false )
2022-05-28 20:03:24 +00:00
2022-07-09 13:33:51 +00:00
if parent != "" {
_ , err = user . bridge . Bot . SendStateEvent ( parent , event . StateSpaceChild , resp . RoomID . String ( ) , & event . SpaceChildEventContent {
Via : [ ] string { user . bridge . AS . HomeserverDomain } ,
Order : " 0000" ,
} )
if err != nil {
2023-03-12 12:25:24 +00:00
user . log . Error ( ) . Err ( err ) .
Str ( "created_space_id" , resp . RoomID . String ( ) ) .
Str ( "parent_space_id" , parent . String ( ) ) .
Msg ( "Failed to add created space room to parent space" )
2022-05-28 20:41:34 +00:00
}
2022-05-28 20:03:24 +00:00
}
}
2022-05-28 20:41:34 +00:00
return * ptr
}
func ( user * User ) GetSpaceRoom ( ) id . RoomID {
2022-07-09 13:33:51 +00:00
return user . getSpaceRoom ( & user . SpaceRoom , "Discord" , "Your Discord bridged chats" , "" )
2022-05-28 20:41:34 +00:00
}
2022-05-28 20:03:24 +00:00
2022-05-28 20:41:34 +00:00
func ( user * User ) GetDMSpaceRoom ( ) id . RoomID {
2022-07-09 13:33:51 +00:00
return user . getSpaceRoom ( & user . DMSpaceRoom , "Direct Messages" , "Your Discord direct messages" , user . GetSpaceRoom ( ) )
2022-05-28 20:03:24 +00:00
}
2022-05-22 19:16:42 +00:00
func ( user * User ) tryAutomaticDoublePuppeting ( ) {
user . Lock ( )
defer user . Unlock ( )
if ! user . bridge . Config . CanAutoDoublePuppet ( user . MXID ) {
return
}
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) . Msg ( "Checking if double puppeting needs to be enabled" )
2022-05-22 19:16:42 +00:00
2022-05-28 20:03:24 +00:00
puppet := user . bridge . GetPuppetByID ( user . DiscordID )
2022-05-22 19:16:42 +00:00
if puppet . CustomMXID != "" {
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) . Msg ( "User already has double-puppeting enabled" )
2022-05-22 19:16:42 +00:00
return
}
accessToken , err := puppet . loginWithSharedSecret ( user . MXID )
if err != nil {
2023-03-12 12:25:24 +00:00
user . log . Warn ( ) . Err ( err ) . Msg ( "Failed to login with shared secret" )
2022-05-22 19:16:42 +00:00
return
}
err = puppet . SwitchCustomMXID ( accessToken , user . MXID )
if err != nil {
2023-03-12 12:25:24 +00:00
puppet . log . Warn ( ) . Err ( err ) . Msg ( "Failed to switch to auto-logined custom puppet" )
2022-05-22 19:16:42 +00:00
return
}
2023-03-12 12:25:24 +00:00
user . log . Info ( ) . Msg ( "Successfully automatically enabled custom puppet" )
2022-05-22 19:16:42 +00:00
}
2022-07-08 14:06:02 +00:00
func ( user * User ) ViewingChannel ( portal * Portal ) bool {
2023-02-27 09:41:53 +00:00
if portal . GuildID != "" || ! user . Session . IsUser {
2022-07-08 14:06:02 +00:00
return false
}
user . markedOpenedLock . Lock ( )
defer user . markedOpenedLock . Unlock ( )
ts := user . markedOpened [ portal . Key . ChannelID ]
// TODO is there an expiry time?
if ts . IsZero ( ) {
user . markedOpened [ portal . Key . ChannelID ] = time . Now ( )
err := user . Session . MarkViewing ( portal . Key . ChannelID )
if err != nil {
2023-03-12 12:25:24 +00:00
user . log . Error ( ) . Err ( err ) .
Str ( "channel_id" , portal . Key . ChannelID ) .
Msg ( "Failed to mark user as viewing channel" )
2022-07-08 14:06:02 +00:00
}
return true
}
return false
}
2023-01-20 13:07:18 +00:00
func ( user * User ) mutePortal ( intent * appservice . IntentAPI , portal * Portal , unmute bool ) {
if len ( portal . MXID ) == 0 || ! user . bridge . Config . Bridge . MuteChannelsOnCreate {
2022-05-22 19:16:42 +00:00
return
}
2023-01-20 13:07:18 +00:00
var err error
if unmute {
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) . Str ( "room_id" , portal . MXID . String ( ) ) . Msg ( "Unmuting portal" )
2023-01-20 13:07:18 +00:00
err = intent . DeletePushRule ( "global" , pushrules . RoomRule , string ( portal . MXID ) )
} else {
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) . Str ( "room_id" , portal . MXID . String ( ) ) . Msg ( "Muting portal" )
2023-01-20 13:07:18 +00:00
err = intent . PutPushRule ( "global" , pushrules . RoomRule , string ( portal . MXID ) , & mautrix . ReqPutPushRule {
Actions : [ ] pushrules . PushActionType { pushrules . ActionDontNotify } ,
} )
}
if err != nil && ! errors . Is ( err , mautrix . MNotFound ) {
2023-03-12 12:25:24 +00:00
user . log . Warn ( ) . Err ( err ) .
Str ( "room_id" , portal . MXID . String ( ) ) .
Msg ( "Failed to update push rule through double puppet" )
2023-01-20 13:07:18 +00:00
}
}
2022-05-22 19:16:42 +00:00
2023-01-20 13:07:18 +00:00
func ( user * User ) syncChatDoublePuppetDetails ( portal * Portal , justCreated bool ) {
doublePuppetIntent := portal . bridge . GetPuppetByCustomMXID ( user . MXID ) . CustomIntent ( )
if doublePuppetIntent == nil || portal . MXID == "" {
2022-05-22 19:16:42 +00:00
return
}
2023-01-20 13:07:18 +00:00
// TODO sync mute status properly
if portal . GuildID != "" && user . bridge . Config . Bridge . MuteChannelsOnCreate {
2023-02-24 17:05:01 +00:00
user . mutePortal ( doublePuppetIntent , portal , false )
2023-01-20 13:07:18 +00:00
}
2022-05-22 19:16:42 +00:00
}
2023-01-28 13:43:16 +00:00
func ( user * User ) NextDiscordUploadID ( ) string {
val := user . nextDiscordUploadID . Add ( 2 )
return strconv . Itoa ( int ( val ) )
}
2022-05-22 19:16:42 +00:00
func ( user * User ) Login ( token string ) error {
2023-01-16 16:36:23 +00:00
user . bridgeStateLock . Lock ( )
user . wasLoggedOut = false
user . bridgeStateLock . Unlock ( )
2022-05-28 20:03:24 +00:00
user . DiscordToken = token
2023-03-10 15:39:34 +00:00
var err error
const maxRetries = 3
Loop :
for i := 0 ; i < maxRetries ; i ++ {
err = user . Connect ( )
if err == nil {
user . Update ( )
return nil
}
2023-03-12 12:25:24 +00:00
user . log . Error ( ) . Err ( err ) . Msg ( "Error connecting for login" )
2023-03-10 15:39:34 +00:00
closeErr := & websocket . CloseError { }
errors . As ( err , & closeErr )
switch closeErr . Code {
case 4004 , 4010 , 4011 , 4012 , 4013 , 4014 :
break Loop
case 4000 :
fallthrough
default :
if i < maxRetries - 1 {
time . Sleep ( time . Duration ( i + 1 ) * 2 * time . Second )
}
}
2023-02-26 23:02:13 +00:00
}
2023-03-10 15:39:34 +00:00
user . DiscordToken = ""
return err
2022-05-22 19:16:42 +00:00
}
func ( user * User ) IsLoggedIn ( ) bool {
user . Lock ( )
defer user . Unlock ( )
2022-05-28 20:03:24 +00:00
return user . DiscordToken != ""
2022-05-22 19:16:42 +00:00
}
2023-03-08 17:49:43 +00:00
func ( user * User ) Logout ( isOverwriting bool ) {
2022-05-22 19:16:42 +00:00
user . Lock ( )
defer user . Unlock ( )
2022-11-26 13:18:42 +00:00
if user . DiscordID != "" {
puppet := user . bridge . GetPuppetByID ( user . DiscordID )
if puppet . CustomMXID != "" {
err := puppet . SwitchCustomMXID ( "" , "" )
if err != nil {
2023-03-12 12:25:24 +00:00
user . log . Warn ( ) . Err ( err ) . Msg ( "Failed to disable custom puppet while logging out of Discord" )
2022-11-26 13:18:42 +00:00
}
2022-05-22 19:16:42 +00:00
}
}
2022-11-26 13:18:42 +00:00
if user . Session != nil {
if err := user . Session . Close ( ) ; err != nil {
2023-03-12 12:25:24 +00:00
user . log . Warn ( ) . Err ( err ) . Msg ( "Error closing session" )
2022-11-26 13:18:42 +00:00
}
2022-05-22 19:16:42 +00:00
}
user . Session = nil
2022-05-28 20:03:24 +00:00
user . DiscordToken = ""
2022-11-30 13:11:04 +00:00
user . ReadStateVersion = 0
2023-03-08 17:49:43 +00:00
if ! isOverwriting {
user . bridge . usersLock . Lock ( )
if user . bridge . usersByID [ user . DiscordID ] == user {
delete ( user . bridge . usersByID , user . DiscordID )
}
user . bridge . usersLock . Unlock ( )
}
user . DiscordID = ""
2022-05-22 19:16:42 +00:00
user . Update ( )
2023-03-12 12:25:24 +00:00
user . log . Info ( ) . Msg ( "User logged out" )
2022-05-22 19:16:42 +00:00
}
func ( user * User ) Connected ( ) bool {
user . Lock ( )
defer user . Unlock ( )
return user . Session != nil
}
2023-02-26 22:43:11 +00:00
const BotIntents = discordgo . IntentGuilds |
discordgo . IntentGuildMessages |
discordgo . IntentGuildMessageReactions |
discordgo . IntentGuildMessageTyping |
discordgo . IntentGuildBans |
discordgo . IntentGuildEmojis |
discordgo . IntentGuildIntegrations |
discordgo . IntentGuildInvites |
//discordgo.IntentGuildVoiceStates |
//discordgo.IntentGuildScheduledEvents |
discordgo . IntentDirectMessages |
discordgo . IntentDirectMessageTyping |
discordgo . IntentDirectMessageTyping |
// Privileged intents
discordgo . IntentMessageContent |
//discordgo.IntentGuildPresences |
discordgo . IntentGuildMembers
2022-05-22 19:16:42 +00:00
func ( user * User ) Connect ( ) error {
user . Lock ( )
defer user . Unlock ( )
2022-05-28 20:03:24 +00:00
if user . DiscordToken == "" {
2022-05-22 19:16:42 +00:00
return ErrNotLoggedIn
}
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) . Msg ( "Connecting to discord" )
2022-05-22 19:16:42 +00:00
2022-05-28 20:03:24 +00:00
session , err := discordgo . New ( user . DiscordToken )
2022-05-22 19:16:42 +00:00
if err != nil {
return err
}
2022-05-28 20:03:24 +00:00
// TODO move to config
if os . Getenv ( "DISCORD_DEBUG" ) == "1" {
session . LogLevel = discordgo . LogDebug
}
2023-02-26 22:43:11 +00:00
if ! session . IsUser {
session . Identify . Intents = BotIntents
}
2022-05-22 19:16:42 +00:00
user . Session = session
user . Session . AddHandler ( user . readyHandler )
2023-02-04 12:13:27 +00:00
user . Session . AddHandler ( user . resumeHandler )
2022-05-22 19:16:42 +00:00
user . Session . AddHandler ( user . connectedHandler )
user . Session . AddHandler ( user . disconnectedHandler )
2023-01-13 16:41:22 +00:00
user . Session . AddHandler ( user . invalidAuthHandler )
2022-05-22 19:16:42 +00:00
user . Session . AddHandler ( user . guildCreateHandler )
user . Session . AddHandler ( user . guildDeleteHandler )
user . Session . AddHandler ( user . guildUpdateHandler )
2022-07-08 12:31:03 +00:00
user . Session . AddHandler ( user . guildRoleCreateHandler )
user . Session . AddHandler ( user . guildRoleUpdateHandler )
user . Session . AddHandler ( user . guildRoleDeleteHandler )
2022-05-22 19:16:42 +00:00
user . Session . AddHandler ( user . channelCreateHandler )
user . Session . AddHandler ( user . channelDeleteHandler )
user . Session . AddHandler ( user . channelPinsUpdateHandler )
user . Session . AddHandler ( user . channelUpdateHandler )
2023-04-21 23:50:14 +00:00
user . Session . AddHandler ( user . relationshipAddHandler )
user . Session . AddHandler ( user . relationshipRemoveHandler )
user . Session . AddHandler ( user . relationshipUpdateHandler )
2022-05-22 19:16:42 +00:00
user . Session . AddHandler ( user . messageCreateHandler )
user . Session . AddHandler ( user . messageDeleteHandler )
user . Session . AddHandler ( user . messageUpdateHandler )
user . Session . AddHandler ( user . reactionAddHandler )
user . Session . AddHandler ( user . reactionRemoveHandler )
2022-07-08 08:54:09 +00:00
user . Session . AddHandler ( user . messageAckHandler )
2022-07-08 14:06:02 +00:00
user . Session . AddHandler ( user . typingStartHandler )
2022-05-22 19:16:42 +00:00
2023-01-29 11:45:00 +00:00
user . Session . AddHandler ( user . interactionSuccessHandler )
2022-05-22 19:16:42 +00:00
return user . Session . Open ( )
}
func ( user * User ) Disconnect ( ) error {
user . Lock ( )
defer user . Unlock ( )
if user . Session == nil {
return ErrNotConnected
}
2023-03-12 12:25:24 +00:00
user . log . Info ( ) . Msg ( "Disconnecting session manually" )
2022-05-22 19:16:42 +00:00
if err := user . Session . Close ( ) ; err != nil {
return err
}
user . Session = nil
return nil
}
2023-02-18 20:53:51 +00:00
func ( user * User ) getGuildBridgingMode ( guildID string ) database . GuildBridgingMode {
2022-05-22 19:16:42 +00:00
if guildID == "" {
2023-02-18 20:53:51 +00:00
return database . GuildBridgeEverything
2022-05-22 19:16:42 +00:00
}
2022-05-28 20:03:24 +00:00
guild := user . bridge . GetGuildByID ( guildID , false )
2023-02-18 20:53:51 +00:00
if guild == nil {
return database . GuildBridgeNothing
}
return guild . BridgingMode
2022-05-22 19:16:42 +00:00
}
2023-04-20 11:27:37 +00:00
type ChannelSlice [ ] * discordgo . Channel
func ( s ChannelSlice ) Len ( ) int {
return len ( s )
}
func ( s ChannelSlice ) Less ( i , j int ) bool {
if s [ i ] . Position != 0 || s [ j ] . Position != 0 {
return s [ i ] . Position < s [ j ] . Position
}
return compareMessageIDs ( s [ i ] . LastMessageID , s [ j ] . LastMessageID ) == 1
}
func ( s ChannelSlice ) Swap ( i , j int ) {
s [ i ] , s [ j ] = s [ j ] , s [ i ]
}
2022-05-28 20:03:24 +00:00
func ( user * User ) readyHandler ( _ * discordgo . Session , r * discordgo . Ready ) {
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) . Msg ( "Discord connection ready" )
2023-01-16 16:36:23 +00:00
user . bridgeStateLock . Lock ( )
user . wasLoggedOut = false
user . bridgeStateLock . Unlock ( )
2022-05-22 19:16:42 +00:00
2022-05-28 20:03:24 +00:00
if user . DiscordID != r . User . ID {
2023-03-08 17:49:43 +00:00
user . bridge . usersLock . Lock ( )
2022-05-28 20:03:24 +00:00
user . DiscordID = r . User . ID
2023-03-08 17:49:43 +00:00
if previousUser , ok := user . bridge . usersByID [ user . DiscordID ] ; ok && previousUser != user {
2023-03-12 12:25:24 +00:00
user . log . Warn ( ) .
Str ( "previous_user_id" , previousUser . MXID . String ( ) ) .
Msg ( "Another user is logged in with same Discord ID, logging them out" )
2023-03-08 17:49:43 +00:00
// TODO send notice?
previousUser . Logout ( true )
}
user . bridge . usersByID [ user . DiscordID ] = user
user . bridge . usersLock . Unlock ( )
2022-05-28 20:03:24 +00:00
user . Update ( )
}
2022-08-15 13:43:55 +00:00
user . BridgeState . Send ( status . BridgeState { StateEvent : status . StateBackfilling } )
2023-01-13 16:04:20 +00:00
user . tryAutomaticDoublePuppeting ( )
2022-05-22 19:16:42 +00:00
2023-04-21 23:50:14 +00:00
for _ , relationship := range r . Relationships {
user . relationships [ relationship . ID ] = relationship
}
2022-05-28 20:03:24 +00:00
updateTS := time . Now ( )
2022-05-28 20:41:34 +00:00
portalsInSpace := make ( map [ string ] bool )
for _ , guild := range user . GetPortals ( ) {
portalsInSpace [ guild . DiscordID ] = guild . InSpace
2022-05-28 20:03:24 +00:00
}
2022-05-22 19:16:42 +00:00
for _ , guild := range r . Guilds {
2022-05-28 20:41:34 +00:00
user . handleGuild ( guild , updateTS , portalsInSpace [ guild . ID ] )
2022-05-28 20:03:24 +00:00
}
2023-04-20 11:27:37 +00:00
// The private channel list doesn't seem to be sorted by default, so sort it by message IDs (highest=newest first)
sort . Sort ( ChannelSlice ( r . PrivateChannels ) )
2022-05-28 20:03:24 +00:00
for i , ch := range r . PrivateChannels {
portal := user . GetPortalByMeta ( ch )
2022-07-08 19:43:52 +00:00
user . handlePrivateChannel ( portal , ch , updateTS , i < user . bridge . Config . Bridge . PrivateChannelCreateLimit , portalsInSpace [ portal . Key . ChannelID ] )
2022-05-28 20:41:34 +00:00
}
2022-08-19 20:10:03 +00:00
user . PrunePortalList ( updateTS )
2022-07-08 09:57:21 +00:00
2023-02-26 22:43:11 +00:00
if r . ReadState != nil && r . ReadState . Version > user . ReadStateVersion {
2022-07-08 09:57:21 +00:00
// TODO can we figure out which read states are actually new?
for _ , entry := range r . ReadState . Entries {
user . messageAckHandler ( nil , & discordgo . MessageAck {
MessageID : string ( entry . LastMessageID ) ,
ChannelID : entry . ID ,
} )
}
user . ReadStateVersion = r . ReadState . Version
user . Update ( )
}
2023-02-04 12:13:27 +00:00
go user . subscribeGuilds ( 2 * time . Second )
2022-08-15 13:43:55 +00:00
user . BridgeState . Send ( status . BridgeState { StateEvent : status . StateConnected } )
2022-05-28 20:41:34 +00:00
}
2023-02-04 12:13:27 +00:00
func ( user * User ) subscribeGuilds ( delay time . Duration ) {
2023-02-26 23:12:06 +00:00
if ! user . Session . IsUser {
return
}
2023-02-04 12:13:27 +00:00
for _ , guildMeta := range user . Session . State . Guilds {
guild := user . bridge . GetGuildByID ( guildMeta . ID , false )
if guild != nil && guild . MXID != "" {
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) . Str ( "guild_id" , guild . ID ) . Msg ( "Subscribing to guild" )
2023-02-04 12:13:27 +00:00
dat := discordgo . GuildSubscribeData {
GuildID : guild . ID ,
Typing : true ,
Activities : true ,
Threads : true ,
}
err := user . Session . SubscribeGuild ( dat )
if err != nil {
2023-03-12 12:25:24 +00:00
user . log . Warn ( ) . Err ( err ) . Str ( "guild_id" , guild . ID ) . Msg ( "Failed to subscribe to guild" )
2023-02-04 12:13:27 +00:00
}
time . Sleep ( delay )
}
}
}
func ( user * User ) resumeHandler ( _ * discordgo . Session , r * discordgo . Resumed ) {
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) . Msg ( "Discord connection resumed" )
2023-02-04 12:13:27 +00:00
user . subscribeGuilds ( 0 * time . Second )
}
2022-07-09 14:03:32 +00:00
func ( user * User ) addPrivateChannelToSpace ( portal * Portal ) bool {
if portal . MXID == "" {
return false
}
_ , err := user . bridge . Bot . SendStateEvent ( user . GetDMSpaceRoom ( ) , event . StateSpaceChild , portal . MXID . String ( ) , & event . SpaceChildEventContent {
Via : [ ] string { user . bridge . AS . HomeserverDomain } ,
} )
if err != nil {
2023-03-12 12:25:24 +00:00
user . log . Error ( ) . Err ( err ) .
Str ( "room_id" , portal . MXID . String ( ) ) .
Msg ( "Failed to add DMM room to user DM space" )
2022-07-09 14:03:32 +00:00
return false
} else {
return true
}
}
2023-04-21 23:50:14 +00:00
func ( user * User ) relationshipAddHandler ( _ * discordgo . Session , r * discordgo . RelationshipAdd ) {
user . log . Debug ( ) . Interface ( "relationship" , r . Relationship ) . Msg ( "Relationship added" )
user . relationships [ r . ID ] = r . Relationship
user . handleRelationshipChange ( r . ID , r . Nickname )
}
func ( user * User ) relationshipUpdateHandler ( _ * discordgo . Session , r * discordgo . RelationshipUpdate ) {
user . log . Debug ( ) . Interface ( "relationship" , r . Relationship ) . Msg ( "Relationship update" )
user . relationships [ r . ID ] = r . Relationship
user . handleRelationshipChange ( r . ID , r . Nickname )
}
func ( user * User ) relationshipRemoveHandler ( _ * discordgo . Session , r * discordgo . RelationshipRemove ) {
user . log . Debug ( ) . Str ( "other_user_id" , r . ID ) . Msg ( "Relationship removed" )
delete ( user . relationships , r . ID )
user . handleRelationshipChange ( r . ID , "" )
}
func ( user * User ) handleRelationshipChange ( userID , nickname string ) {
puppet := user . bridge . GetPuppetByID ( userID )
portal := user . FindPrivateChatWith ( userID )
if portal == nil || puppet == nil {
return
}
updated := portal . FriendNick == ( nickname != "" )
portal . FriendNick = nickname != ""
if nickname != "" {
updated = portal . UpdateNameDirect ( nickname , true )
} else if portal . Name != puppet . Name {
if portal . shouldSetDMRoomMetadata ( ) {
updated = portal . UpdateNameDirect ( puppet . Name , false )
} else if portal . NameSet {
_ , err := portal . MainIntent ( ) . SendStateEvent ( portal . MXID , event . StateRoomName , "" , map [ string ] any { } )
if err != nil {
portal . zlog . Warn ( ) . Err ( err ) . Msg ( "Failed to clear room name after friend nickname was removed" )
} else {
portal . zlog . Debug ( ) . Msg ( "Cleared room name after friend nickname was removed" )
portal . NameSet = false
portal . Update ( )
updated = true
}
}
}
if ! updated {
portal . Update ( )
}
}
2022-05-28 20:41:34 +00:00
func ( user * User ) handlePrivateChannel ( portal * Portal , meta * discordgo . Channel , timestamp time . Time , create , isInSpace bool ) {
if create && portal . MXID == "" {
err := portal . CreateMatrixRoom ( user , meta )
if err != nil {
2023-03-12 12:25:24 +00:00
user . log . Error ( ) . Err ( err ) .
Str ( "channel_id" , portal . Key . ChannelID ) .
Msg ( "Failed to create portal for private channel in create handler" )
2022-05-28 20:41:34 +00:00
}
} else {
portal . UpdateInfo ( user , meta )
2023-04-16 12:06:02 +00:00
portal . ForwardBackfillMissed ( user , meta )
2022-05-28 20:41:34 +00:00
}
user . MarkInPortal ( database . UserPortal {
2022-07-09 14:03:32 +00:00
DiscordID : portal . Key . ChannelID ,
2022-05-28 20:41:34 +00:00
Type : database . UserPortalTypeDM ,
Timestamp : timestamp ,
2022-07-09 14:03:32 +00:00
InSpace : isInSpace || user . addPrivateChannelToSpace ( portal ) ,
2022-05-28 20:41:34 +00:00
} )
2022-05-28 20:03:24 +00:00
}
2022-05-22 19:16:42 +00:00
2022-07-09 14:03:32 +00:00
func ( user * User ) addGuildToSpace ( guild * Guild , isInSpace bool , timestamp time . Time ) bool {
2022-08-19 20:10:03 +00:00
if len ( guild . MXID ) > 0 && ! isInSpace {
2022-05-28 21:22:00 +00:00
_ , err := user . bridge . Bot . SendStateEvent ( user . GetSpaceRoom ( ) , event . StateSpaceChild , guild . MXID . String ( ) , & event . SpaceChildEventContent {
Via : [ ] string { user . bridge . AS . HomeserverDomain } ,
} )
if err != nil {
2023-03-12 12:25:24 +00:00
user . log . Error ( ) . Err ( err ) .
Str ( "guild_space_id" , guild . MXID . String ( ) ) .
Msg ( "Failed to add guild space to user space" )
2022-05-28 21:22:00 +00:00
} else {
2022-07-09 14:03:32 +00:00
isInSpace = true
2022-05-28 21:22:00 +00:00
}
}
2022-07-09 14:03:32 +00:00
user . MarkInPortal ( database . UserPortal {
DiscordID : guild . ID ,
Type : database . UserPortalTypeGuild ,
Timestamp : timestamp ,
InSpace : isInSpace ,
} )
return isInSpace
2022-05-28 21:22:00 +00:00
}
2022-07-08 12:31:03 +00:00
func ( user * User ) discordRoleToDB ( guildID string , role * discordgo . Role , dbRole * database . Role ) ( * database . Role , bool ) {
var changed bool
if dbRole == nil {
dbRole = user . bridge . DB . Role . New ( )
dbRole . ID = role . ID
dbRole . GuildID = guildID
changed = true
} else {
changed = dbRole . Name != role . Name ||
dbRole . Icon != role . Icon ||
dbRole . Mentionable != role . Mentionable ||
dbRole . Managed != role . Managed ||
dbRole . Hoist != role . Hoist ||
dbRole . Color != role . Color ||
dbRole . Position != role . Position ||
dbRole . Permissions != role . Permissions
}
dbRole . Role = * role
return dbRole , changed
}
func ( user * User ) handleGuildRoles ( guildID string , newRoles [ ] * discordgo . Role ) {
existingRoles := user . bridge . DB . Role . GetAll ( guildID )
existingRoleMap := make ( map [ string ] * database . Role , len ( existingRoles ) )
for _ , role := range existingRoles {
existingRoleMap [ role . ID ] = role
}
txn , err := user . bridge . DB . Begin ( )
if err != nil {
2023-03-12 12:25:24 +00:00
user . log . Error ( ) . Err ( err ) . Msg ( "Failed to start transaction for guild role sync" )
2022-07-08 12:31:03 +00:00
panic ( err )
}
for _ , role := range newRoles {
dbRole , changed := user . discordRoleToDB ( guildID , role , existingRoleMap [ role . ID ] )
delete ( existingRoleMap , role . ID )
if changed {
dbRole . Upsert ( txn )
}
}
for _ , removeRole := range existingRoleMap {
removeRole . Delete ( txn )
}
err = txn . Commit ( )
if err != nil {
2023-03-12 12:25:24 +00:00
user . log . Error ( ) . Err ( err ) . Msg ( "Failed to commit guild role sync transaction" )
2022-07-08 12:31:03 +00:00
rollbackErr := txn . Rollback ( )
if rollbackErr != nil {
2023-03-12 12:25:24 +00:00
user . log . Error ( ) . Err ( rollbackErr ) . Msg ( "Failed to rollback errored guild role sync transaction" )
2022-07-08 12:31:03 +00:00
}
panic ( err )
}
}
func ( user * User ) guildRoleCreateHandler ( _ * discordgo . Session , r * discordgo . GuildRoleCreate ) {
dbRole , _ := user . discordRoleToDB ( r . GuildID , r . Role , nil )
dbRole . Upsert ( nil )
}
func ( user * User ) guildRoleUpdateHandler ( _ * discordgo . Session , r * discordgo . GuildRoleUpdate ) {
dbRole , _ := user . discordRoleToDB ( r . GuildID , r . Role , nil )
dbRole . Upsert ( nil )
}
func ( user * User ) guildRoleDeleteHandler ( _ * discordgo . Session , r * discordgo . GuildRoleDelete ) {
user . bridge . DB . Role . DeleteByID ( r . GuildID , r . RoleID )
}
2022-05-28 20:03:24 +00:00
func ( user * User ) handleGuild ( meta * discordgo . Guild , timestamp time . Time , isInSpace bool ) {
guild := user . bridge . GetGuildByID ( meta . ID , true )
guild . UpdateInfo ( user , meta )
if len ( meta . Channels ) > 0 {
for _ , ch := range meta . Channels {
portal := user . GetPortalByMeta ( ch )
2023-02-18 20:53:51 +00:00
if guild . BridgingMode >= database . GuildBridgeEverything && portal . MXID == "" && user . channelIsBridgeable ( ch ) {
2022-05-28 20:03:24 +00:00
err := portal . CreateMatrixRoom ( user , ch )
if err != nil {
2023-03-12 12:25:24 +00:00
user . log . Error ( ) . Err ( err ) .
Str ( "guild_id" , guild . ID ) .
Str ( "channel_id" , ch . ID ) .
Msg ( "Failed to create portal for guild channel in guild handler" )
2022-05-28 20:03:24 +00:00
}
} else {
portal . UpdateInfo ( user , ch )
2023-04-16 20:11:44 +00:00
if user . bridge . Config . Bridge . Backfill . MaxGuildMembers < 0 || meta . MemberCount < user . bridge . Config . Bridge . Backfill . MaxGuildMembers {
portal . ForwardBackfillMissed ( user , ch )
}
2022-05-22 19:16:42 +00:00
}
}
}
2022-07-08 12:31:03 +00:00
if len ( meta . Roles ) > 0 {
user . handleGuildRoles ( meta . ID , meta . Roles )
}
2022-07-09 14:03:32 +00:00
user . addGuildToSpace ( guild , isInSpace , timestamp )
2022-05-22 19:16:42 +00:00
}
2023-01-13 16:41:22 +00:00
func ( user * User ) connectedHandler ( _ * discordgo . Session , _ * discordgo . Connect ) {
2023-01-16 16:36:23 +00:00
user . bridgeStateLock . Lock ( )
defer user . bridgeStateLock . Unlock ( )
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) . Msg ( "Connected to Discord" )
2023-01-16 16:36:23 +00:00
if user . wasDisconnected {
user . wasDisconnected = false
2022-08-15 13:43:55 +00:00
user . BridgeState . Send ( status . BridgeState { StateEvent : status . StateConnected } )
2022-05-30 14:11:51 +00:00
}
2022-05-22 19:16:42 +00:00
}
2023-01-13 16:41:22 +00:00
func ( user * User ) disconnectedHandler ( _ * discordgo . Session , _ * discordgo . Disconnect ) {
2023-01-16 16:36:23 +00:00
user . bridgeStateLock . Lock ( )
defer user . bridgeStateLock . Unlock ( )
if user . wasLoggedOut {
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) . Msg ( "Disconnected from Discord (not updating bridge state as user was just logged out)" )
2023-01-13 17:25:38 +00:00
return
}
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) . Msg ( "Disconnected from Discord" )
2023-01-16 16:36:23 +00:00
user . wasDisconnected = true
2023-01-13 17:00:15 +00:00
user . BridgeState . Send ( status . BridgeState { StateEvent : status . StateTransientDisconnect , Error : "dc-transient-disconnect" , Message : "Temporarily disconnected from Discord, trying to reconnect" } )
2023-01-13 16:41:22 +00:00
}
func ( user * User ) invalidAuthHandler ( _ * discordgo . Session , _ * discordgo . InvalidAuth ) {
2023-01-16 16:36:23 +00:00
user . bridgeStateLock . Lock ( )
defer user . bridgeStateLock . Unlock ( )
2023-03-12 12:25:24 +00:00
user . log . Info ( ) . Msg ( "Got logged out from Discord due to invalid token" )
2023-01-16 16:36:23 +00:00
user . wasLoggedOut = true
2023-01-13 17:00:15 +00:00
user . BridgeState . Send ( status . BridgeState { StateEvent : status . StateBadCredentials , Error : "dc-websocket-disconnect-4004" , Message : "Discord access token is no longer valid, please log in again" } )
2023-03-08 17:49:43 +00:00
go user . Logout ( false )
2022-05-22 19:16:42 +00:00
}
2022-05-28 20:03:24 +00:00
func ( user * User ) guildCreateHandler ( _ * discordgo . Session , g * discordgo . GuildCreate ) {
2023-03-12 12:25:24 +00:00
user . log . Info ( ) .
Str ( "guild_id" , g . ID ) .
Str ( "name" , g . Name ) .
Bool ( "unavailable" , g . Unavailable ) .
Msg ( "Got guild create event" )
2022-05-28 20:03:24 +00:00
user . handleGuild ( g . Guild , time . Now ( ) , false )
}
2022-05-22 19:16:42 +00:00
2022-05-28 20:03:24 +00:00
func ( user * User ) guildDeleteHandler ( _ * discordgo . Session , g * discordgo . GuildDelete ) {
2023-03-12 12:25:24 +00:00
user . log . Info ( ) . Str ( "guild_id" , g . ID ) . Msg ( "Got guild delete event" )
2022-05-28 20:41:34 +00:00
user . MarkNotInPortal ( g . ID )
2022-05-28 20:03:24 +00:00
guild := user . bridge . GetGuildByID ( g . ID , false )
if guild == nil || guild . MXID == "" {
2022-05-22 19:16:42 +00:00
return
}
2023-01-27 19:06:29 +00:00
if user . bridge . Config . Bridge . DeleteGuildOnLeave && ! user . PortalHasOtherUsers ( g . ID ) {
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) . Str ( "guild_id" , g . ID ) . Msg ( "No other users in guild, cleaning up all portals" )
2023-01-27 19:06:29 +00:00
err := user . unbridgeGuild ( g . ID )
if err != nil {
2023-03-12 12:25:24 +00:00
user . log . Warn ( ) . Err ( err ) . Msg ( "Failed to unbridge guild that was deleted" )
2023-01-27 19:06:29 +00:00
}
}
2022-05-22 19:16:42 +00:00
}
2022-05-28 20:03:24 +00:00
func ( user * User ) guildUpdateHandler ( _ * discordgo . Session , g * discordgo . GuildUpdate ) {
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) . Str ( "guild_id" , g . ID ) . Msg ( "Got guild update event" )
2022-05-28 20:03:24 +00:00
user . handleGuild ( g . Guild , time . Now ( ) , user . IsInSpace ( g . ID ) )
2022-05-22 19:16:42 +00:00
}
2022-05-28 20:03:24 +00:00
func ( user * User ) channelCreateHandler ( _ * discordgo . Session , c * discordgo . ChannelCreate ) {
2023-02-18 20:53:51 +00:00
if user . getGuildBridgingMode ( c . GuildID ) < database . GuildBridgeEverything {
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) .
Str ( "guild_id" , c . GuildID ) . Str ( "channel_id" , c . ID ) .
Msg ( "Ignoring channel create event in unbridged guild" )
2022-05-28 20:03:24 +00:00
return
2022-05-22 19:16:42 +00:00
}
2023-03-12 12:25:24 +00:00
user . log . Info ( ) .
Str ( "guild_id" , c . GuildID ) . Str ( "channel_id" , c . ID ) .
Msg ( "Got channel create event" )
2022-05-28 20:03:24 +00:00
portal := user . GetPortalByMeta ( c . Channel )
2022-05-22 19:16:42 +00:00
if portal . MXID != "" {
return
}
2022-05-28 20:41:34 +00:00
if c . GuildID == "" {
user . handlePrivateChannel ( portal , c . Channel , time . Now ( ) , true , user . IsInSpace ( portal . Key . String ( ) ) )
2023-01-11 16:24:08 +00:00
} else if user . channelIsBridgeable ( c . Channel ) {
2022-05-28 20:41:34 +00:00
err := portal . CreateMatrixRoom ( user , c . Channel )
if err != nil {
2023-03-12 12:25:24 +00:00
user . log . Error ( ) . Err ( err ) .
Str ( "guild_id" , c . GuildID ) . Str ( "channel_id" , c . ID ) .
Msg ( "Error creating Matrix room after channel create event" )
2022-05-28 20:41:34 +00:00
}
2023-01-11 16:24:08 +00:00
} else {
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) .
Str ( "guild_id" , c . GuildID ) . Str ( "channel_id" , c . ID ) .
Msg ( "Got channel create event, but it's not bridgeable, ignoring" )
2022-05-22 19:16:42 +00:00
}
}
2022-05-28 20:03:24 +00:00
func ( user * User ) channelDeleteHandler ( _ * discordgo . Session , c * discordgo . ChannelDelete ) {
2022-07-09 13:51:43 +00:00
portal := user . GetExistingPortalByID ( c . ID )
if portal == nil {
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) .
Str ( "guild_id" , c . GuildID ) . Str ( "channel_id" , c . ID ) .
Msg ( "Ignoring channel delete event of unknown channel" )
2022-07-09 13:51:43 +00:00
return
}
2023-03-12 12:25:24 +00:00
user . log . Info ( ) .
Str ( "guild_id" , c . GuildID ) . Str ( "channel_id" , c . ID ) .
Msg ( "Got channel delete event, cleaning up portal" )
2022-07-09 13:51:43 +00:00
portal . Delete ( )
portal . cleanup ( ! user . bridge . Config . Bridge . DeletePortalOnChannelDelete )
2022-07-09 14:03:32 +00:00
if c . GuildID == "" {
user . MarkNotInPortal ( portal . Key . ChannelID )
}
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) .
Str ( "guild_id" , c . GuildID ) . Str ( "channel_id" , c . ID ) .
Msg ( "Completed cleaning up channel" )
2022-05-22 19:16:42 +00:00
}
2022-05-28 20:03:24 +00:00
func ( user * User ) channelPinsUpdateHandler ( _ * discordgo . Session , c * discordgo . ChannelPinsUpdate ) {
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) . Msg ( "channel pins update" )
2022-05-22 19:16:42 +00:00
}
2022-05-28 20:03:24 +00:00
func ( user * User ) channelUpdateHandler ( _ * discordgo . Session , c * discordgo . ChannelUpdate ) {
portal := user . GetPortalByMeta ( c . Channel )
2022-05-28 20:41:34 +00:00
if c . GuildID == "" {
user . handlePrivateChannel ( portal , c . Channel , time . Now ( ) , true , user . IsInSpace ( portal . Key . String ( ) ) )
} else {
portal . UpdateInfo ( user , c . Channel )
}
2022-05-22 19:16:42 +00:00
}
2023-02-27 09:41:53 +00:00
func ( user * User ) findPortal ( channelID string ) ( * Portal , * Thread ) {
portal := user . GetExistingPortalByID ( channelID )
if portal != nil {
return portal , nil
}
thread := user . bridge . GetThreadByID ( channelID , nil )
if thread != nil && thread . Parent != nil {
return thread . Parent , thread
}
if ! user . Session . IsUser {
channel , _ := user . Session . State . Channel ( channelID )
if channel == nil {
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) . Str ( "channel_id" , channelID ) . Msg ( "Fetching info of unknown channel to handle message" )
2023-02-27 09:41:53 +00:00
var err error
channel , err = user . Session . Channel ( channelID )
if err != nil {
2023-03-12 12:25:24 +00:00
user . log . Warn ( ) . Err ( err ) . Str ( "channel_id" , channelID ) . Msg ( "Failed to get info of unknown channel" )
2023-02-27 09:41:53 +00:00
} else {
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) . Str ( "channel_id" , channelID ) . Msg ( "Got info for channel to handle message" )
2023-02-27 09:41:53 +00:00
_ = user . Session . State . ChannelAdd ( channel )
}
}
if channel != nil && user . channelIsBridgeable ( channel ) {
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) . Str ( "channel_id" , channelID ) . Msg ( "Creating portal and updating info to handle message" )
2023-02-27 09:41:53 +00:00
portal = user . GetPortalByMeta ( channel )
if channel . GuildID == "" {
user . handlePrivateChannel ( portal , channel , time . Now ( ) , false , false )
} else {
2023-03-12 12:25:24 +00:00
user . log . Warn ( ) .
Str ( "channel_id" , channel . ID ) . Str ( "guild_id" , channel . GuildID ) .
Msg ( "Unexpected unknown guild channel" )
2023-02-27 09:41:53 +00:00
}
return portal , nil
}
}
return nil , nil
}
2022-05-28 20:03:24 +00:00
func ( user * User ) pushPortalMessage ( msg interface { } , typeName , channelID , guildID string ) {
2023-02-18 20:53:51 +00:00
if user . getGuildBridgingMode ( guildID ) <= database . GuildBridgeNothing {
// If guild bridging mode is nothing, don't even check if the portal exists
2022-05-22 19:16:42 +00:00
return
}
2023-02-27 09:41:53 +00:00
portal , thread := user . findPortal ( channelID )
2022-05-28 20:03:24 +00:00
if portal == nil {
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) .
Str ( "discord_event" , typeName ) .
Str ( "guild_id" , guildID ) .
Str ( "channel_id" , channelID ) .
Msg ( "Dropping event in unknown channel" )
2023-02-27 09:41:53 +00:00
return
2022-05-22 19:16:42 +00:00
}
2023-02-18 20:53:51 +00:00
if mode := user . getGuildBridgingMode ( portal . GuildID ) ; mode <= database . GuildBridgeNothing || ( portal . MXID == "" && mode <= database . GuildBridgeIfPortalExists ) {
2023-02-04 11:45:50 +00:00
return
}
2022-05-22 19:16:42 +00:00
2022-05-28 20:03:24 +00:00
portal . discordMessages <- portalDiscordMessage {
msg : msg ,
user : user ,
thread : thread ,
2022-05-22 19:16:42 +00:00
}
}
2022-05-28 20:03:24 +00:00
func ( user * User ) messageCreateHandler ( _ * discordgo . Session , m * discordgo . MessageCreate ) {
user . pushPortalMessage ( m , "message create" , m . ChannelID , m . GuildID )
2022-05-22 19:16:42 +00:00
}
2022-05-28 20:03:24 +00:00
func ( user * User ) messageDeleteHandler ( _ * discordgo . Session , m * discordgo . MessageDelete ) {
user . pushPortalMessage ( m , "message delete" , m . ChannelID , m . GuildID )
2022-05-22 19:16:42 +00:00
}
2022-05-28 20:03:24 +00:00
func ( user * User ) messageUpdateHandler ( _ * discordgo . Session , m * discordgo . MessageUpdate ) {
user . pushPortalMessage ( m , "message update" , m . ChannelID , m . GuildID )
}
2022-05-22 19:16:42 +00:00
2022-05-28 20:03:24 +00:00
func ( user * User ) reactionAddHandler ( _ * discordgo . Session , m * discordgo . MessageReactionAdd ) {
user . pushPortalMessage ( m , "reaction add" , m . ChannelID , m . GuildID )
}
2022-05-22 19:16:42 +00:00
2022-05-28 20:03:24 +00:00
func ( user * User ) reactionRemoveHandler ( _ * discordgo . Session , m * discordgo . MessageReactionRemove ) {
user . pushPortalMessage ( m , "reaction remove" , m . ChannelID , m . GuildID )
2022-05-22 19:16:42 +00:00
}
2022-07-08 08:54:09 +00:00
type CustomReadReceipt struct {
Timestamp int64 ` json:"ts,omitempty" `
DoublePuppetSource string ` json:"fi.mau.double_puppet_source,omitempty" `
}
type CustomReadMarkers struct {
mautrix . ReqSetReadMarkers
ReadExtra CustomReadReceipt ` json:"com.beeper.read.extra" `
FullyReadExtra CustomReadReceipt ` json:"com.beeper.fully_read.extra" `
}
func ( user * User ) makeReadMarkerContent ( eventID id . EventID ) * CustomReadMarkers {
var extra CustomReadReceipt
extra . DoublePuppetSource = user . bridge . Name
return & CustomReadMarkers {
ReqSetReadMarkers : mautrix . ReqSetReadMarkers {
Read : eventID ,
FullyRead : eventID ,
} ,
ReadExtra : extra ,
FullyReadExtra : extra ,
}
}
func ( user * User ) messageAckHandler ( _ * discordgo . Session , m * discordgo . MessageAck ) {
portal := user . GetExistingPortalByID ( m . ChannelID )
if portal == nil || portal . MXID == "" {
return
}
dp := user . GetIDoublePuppet ( )
if dp == nil {
return
}
msg := user . bridge . DB . Message . GetLastByDiscordID ( portal . Key , m . MessageID )
if msg == nil {
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) .
Str ( "channel_id" , m . ChannelID ) . Str ( "message_id" , m . MessageID ) .
Msg ( "Dropping message ack event for unknown message" )
2022-07-08 08:54:09 +00:00
return
}
err := dp . CustomIntent ( ) . SetReadMarkers ( portal . MXID , user . makeReadMarkerContent ( msg . MXID ) )
if err != nil {
2023-03-12 12:25:24 +00:00
user . log . Error ( ) . Err ( err ) .
Str ( "event_id" , msg . MXID . String ( ) ) . Str ( "message_id" , msg . DiscordID ) .
Msg ( "Failed to mark event as read" )
2022-07-08 08:54:09 +00:00
} else {
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) .
Str ( "event_id" , msg . MXID . String ( ) ) . Str ( "message_id" , msg . DiscordID ) .
Msg ( "Marked event as read after Discord message ack" )
2022-07-08 09:57:21 +00:00
if user . ReadStateVersion < m . Version {
user . ReadStateVersion = m . Version
// TODO maybe don't update every time?
user . Update ( )
}
2022-07-08 08:54:09 +00:00
}
}
2022-07-08 14:06:02 +00:00
func ( user * User ) typingStartHandler ( _ * discordgo . Session , t * discordgo . TypingStart ) {
portal := user . GetExistingPortalByID ( t . ChannelID )
if portal == nil || portal . MXID == "" {
return
}
2023-02-04 12:16:58 +00:00
portal . handleDiscordTyping ( t )
2022-07-08 14:06:02 +00:00
}
2023-01-29 11:45:00 +00:00
func ( user * User ) interactionSuccessHandler ( _ * discordgo . Session , s * discordgo . InteractionSuccess ) {
user . pendingInteractionsLock . Lock ( )
defer user . pendingInteractionsLock . Unlock ( )
ce , ok := user . pendingInteractions [ s . Nonce ]
if ! ok {
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) . Str ( "nonce" , s . Nonce ) . Str ( "id" , s . ID ) . Msg ( "Got interaction success for unknown interaction" )
2023-01-29 11:45:00 +00:00
} else {
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) . Str ( "nonce" , s . Nonce ) . Str ( "id" , s . ID ) . Msg ( "Got interaction success for pending interaction" )
2023-01-29 11:45:00 +00:00
ce . React ( "✅" )
delete ( user . pendingInteractions , s . Nonce )
}
}
2022-05-22 19:16:42 +00:00
func ( user * User ) ensureInvited ( intent * appservice . IntentAPI , roomID id . RoomID , isDirect bool ) bool {
2022-05-28 20:03:24 +00:00
if intent == nil {
intent = user . bridge . Bot
}
2022-05-22 19:16:42 +00:00
ret := false
inviteContent := event . Content {
Parsed : & event . MemberEventContent {
Membership : event . MembershipInvite ,
IsDirect : isDirect ,
} ,
Raw : map [ string ] interface { } { } ,
}
customPuppet := user . bridge . GetPuppetByCustomMXID ( user . MXID )
if customPuppet != nil && customPuppet . CustomIntent ( ) != nil {
inviteContent . Raw [ "fi.mau.will_auto_accept" ] = true
}
_ , err := intent . SendStateEvent ( roomID , event . StateMember , user . MXID . String ( ) , & inviteContent )
var httpErr mautrix . HTTPError
if err != nil && errors . As ( err , & httpErr ) && httpErr . RespError != nil && strings . Contains ( httpErr . RespError . Err , "is already in the room" ) {
user . bridge . StateStore . SetMembership ( roomID , user . MXID , event . MembershipJoin )
ret = true
} else if err != nil {
2023-03-12 12:25:24 +00:00
user . log . Error ( ) . Err ( err ) . Str ( "room_id" , roomID . String ( ) ) . Msg ( "Failed to invite user to room" )
2022-05-22 19:16:42 +00:00
} else {
ret = true
}
if customPuppet != nil && customPuppet . CustomIntent ( ) != nil {
err = customPuppet . CustomIntent ( ) . EnsureJoined ( roomID , appservice . EnsureJoinedParams { IgnoreCache : true } )
if err != nil {
2023-03-12 12:25:24 +00:00
user . log . Warn ( ) . Err ( err ) . Str ( "room_id" , roomID . String ( ) ) . Msg ( "Failed to auto-join room" )
2022-05-22 19:16:42 +00:00
ret = false
} else {
ret = true
}
}
return ret
}
func ( user * User ) getDirectChats ( ) map [ id . UserID ] [ ] id . RoomID {
chats := map [ id . UserID ] [ ] id . RoomID { }
2022-05-28 20:03:24 +00:00
privateChats := user . bridge . DB . Portal . FindPrivateChatsOf ( user . DiscordID )
2022-05-22 19:16:42 +00:00
for _ , portal := range privateChats {
if portal . MXID != "" {
puppetMXID := user . bridge . FormatPuppetMXID ( portal . Key . Receiver )
chats [ puppetMXID ] = [ ] id . RoomID { portal . MXID }
}
}
return chats
}
func ( user * User ) updateDirectChats ( chats map [ id . UserID ] [ ] id . RoomID ) {
if ! user . bridge . Config . Bridge . SyncDirectChatList {
return
}
puppet := user . bridge . GetPuppetByMXID ( user . MXID )
if puppet == nil {
return
}
intent := puppet . CustomIntent ( )
if intent == nil {
return
}
method := http . MethodPatch
if chats == nil {
chats = user . getDirectChats ( )
method = http . MethodPut
}
2023-03-12 12:25:24 +00:00
user . log . Debug ( ) . Msg ( "Updating m.direct list on homeserver" )
2022-05-22 19:16:42 +00:00
var err error
2022-09-13 11:44:31 +00:00
if user . bridge . Config . Homeserver . Software == bridgeconfig . SoftwareAsmux {
2022-05-22 19:16:42 +00:00
urlPath := intent . BuildURL ( mautrix . ClientURLPath { "unstable" , "com.beeper.asmux" , "dms" } )
_ , err = intent . MakeFullRequest ( mautrix . FullRequest {
Method : method ,
URL : urlPath ,
Headers : http . Header { "X-Asmux-Auth" : { user . bridge . AS . Registration . AppToken } } ,
RequestJSON : chats ,
} )
} else {
existingChats := map [ id . UserID ] [ ] id . RoomID { }
err = intent . GetAccountData ( event . AccountDataDirectChats . Type , & existingChats )
if err != nil {
2023-03-12 12:25:24 +00:00
user . log . Warn ( ) . Err ( err ) . Msg ( "Failed to get m.direct event to update it" )
2022-05-22 19:16:42 +00:00
return
}
for userID , rooms := range existingChats {
if _ , ok := user . bridge . ParsePuppetMXID ( userID ) ; ! ok {
// This is not a ghost user, include it in the new list
chats [ userID ] = rooms
} else if _ , ok := chats [ userID ] ; ! ok && method == http . MethodPatch {
// This is a ghost user, but we're not replacing the whole list, so include it too
chats [ userID ] = rooms
}
}
err = intent . SetAccountData ( event . AccountDataDirectChats . Type , & chats )
}
if err != nil {
2023-03-12 12:25:24 +00:00
user . log . Warn ( ) . Err ( err ) . Msg ( "Failed to update m.direct event" )
2022-05-22 19:16:42 +00:00
}
}
func ( user * User ) bridgeGuild ( guildID string , everything bool ) error {
2022-05-28 20:03:24 +00:00
guild := user . bridge . GetGuildByID ( guildID , false )
if guild == nil {
return errors . New ( "guild not found" )
2022-05-22 19:16:42 +00:00
}
2022-05-28 20:03:24 +00:00
meta , _ := user . Session . State . Guild ( guildID )
err := guild . CreateMatrixRoom ( user , meta )
if err != nil {
return err
}
2023-03-12 12:25:24 +00:00
log := user . log . With ( ) . Str ( "guild_id" , guild . ID ) . Logger ( )
2022-07-09 14:03:32 +00:00
user . addGuildToSpace ( guild , false , time . Now ( ) )
2022-05-28 20:03:24 +00:00
for _ , ch := range meta . Channels {
portal := user . GetPortalByMeta ( ch )
2023-01-11 16:24:08 +00:00
if ( everything && user . channelIsBridgeable ( ch ) ) || ch . Type == discordgo . ChannelTypeGuildCategory {
2022-05-28 20:03:24 +00:00
err = portal . CreateMatrixRoom ( user , ch )
if err != nil {
2023-03-12 12:25:24 +00:00
log . Error ( ) . Err ( err ) . Str ( "channel_id" , ch . ID ) .
Msg ( "Failed to create room for guild channel while bridging guild" )
2022-05-22 19:16:42 +00:00
}
}
}
2023-02-18 20:53:51 +00:00
if everything {
guild . BridgingMode = database . GuildBridgeEverything
}
2023-01-28 12:03:02 +00:00
guild . Update ( )
2022-05-22 19:16:42 +00:00
2023-02-26 23:12:06 +00:00
if user . Session . IsUser {
2023-03-12 12:25:24 +00:00
log . Debug ( ) . Msg ( "Subscribing to guild after bridging" )
2023-02-26 23:12:06 +00:00
err = user . Session . SubscribeGuild ( discordgo . GuildSubscribeData {
GuildID : guild . ID ,
Typing : true ,
Activities : true ,
Threads : true ,
} )
if err != nil {
2023-03-12 12:25:24 +00:00
log . Warn ( ) . Err ( err ) . Msg ( "Failed to subscribe to guild" )
2023-02-26 23:12:06 +00:00
}
2023-02-04 12:49:10 +00:00
}
2022-05-22 19:16:42 +00:00
return nil
}
func ( user * User ) unbridgeGuild ( guildID string ) error {
2023-01-27 19:06:29 +00:00
if user . PermissionLevel < bridgeconfig . PermissionLevelAdmin && user . PortalHasOtherUsers ( guildID ) {
return errors . New ( "only bridge admins can unbridge guilds with other users" )
2023-01-13 15:01:23 +00:00
}
guild := user . bridge . GetGuildByID ( guildID , false )
if guild == nil {
return errors . New ( "guild not found" )
}
guild . roomCreateLock . Lock ( )
defer guild . roomCreateLock . Unlock ( )
2023-02-18 20:53:51 +00:00
if guild . BridgingMode == database . GuildBridgeNothing && guild . MXID == "" {
2023-01-13 15:01:23 +00:00
return errors . New ( "that guild is not bridged" )
}
2023-02-18 20:53:51 +00:00
guild . BridgingMode = database . GuildBridgeNothing
2023-01-13 15:01:23 +00:00
guild . Update ( )
for _ , portal := range user . bridge . GetAllPortalsInGuild ( guild . ID ) {
portal . cleanup ( false )
portal . RemoveMXID ( )
}
guild . cleanup ( )
guild . RemoveMXID ( )
return nil
2022-05-22 19:16:42 +00:00
}