2022-05-22 19:16:42 +00:00
package main
import (
"bytes"
2022-05-30 12:58:03 +00:00
"errors"
2022-05-22 19:16:42 +00:00
"fmt"
2022-05-28 20:03:24 +00:00
"strconv"
2022-05-22 19:16:42 +00:00
"strings"
"sync"
"time"
2022-06-28 08:55:36 +00:00
"maunium.net/go/mautrix/crypto/attachment"
2022-05-27 12:59:16 +00:00
"maunium.net/go/mautrix/util/variationselector"
2022-05-22 19:16:42 +00:00
2022-05-28 20:03:24 +00:00
"github.com/bwmarrin/discordgo"
2022-05-22 19:16:42 +00:00
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/bridge"
"maunium.net/go/mautrix/bridge/bridgeconfig"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-discord/database"
)
type portalDiscordMessage struct {
msg interface { }
user * User
2022-05-28 20:03:24 +00:00
thread * Thread
2022-05-22 19:16:42 +00:00
}
type portalMatrixMessage struct {
evt * event . Event
user * User
}
type Portal struct {
* database . Portal
2022-05-28 20:03:24 +00:00
Parent * Portal
Guild * Guild
2022-05-22 19:16:42 +00:00
bridge * DiscordBridge
log log . Logger
roomCreateLock sync . Mutex
encryptLock sync . Mutex
discordMessages chan portalDiscordMessage
matrixMessages chan portalMatrixMessage
}
func ( portal * Portal ) IsEncrypted ( ) bool {
return portal . Encrypted
}
func ( portal * Portal ) MarkEncrypted ( ) {
portal . Encrypted = true
portal . Update ( )
}
func ( portal * Portal ) ReceiveMatrixEvent ( user bridge . User , evt * event . Event ) {
if user . GetPermissionLevel ( ) >= bridgeconfig . PermissionLevelUser /*|| portal.HasRelaybot()*/ {
portal . matrixMessages <- portalMatrixMessage { user : user . ( * User ) , evt : evt }
}
}
var _ bridge . Portal = ( * Portal ) ( nil )
var (
portalCreationDummyEvent = event . Type { Type : "fi.mau.dummy.portal_created" , Class : event . MessageEventType }
)
2022-05-28 20:03:24 +00:00
func ( br * DiscordBridge ) loadPortal ( dbPortal * database . Portal , key * database . PortalKey , chanType discordgo . ChannelType ) * Portal {
2022-05-22 19:16:42 +00:00
if dbPortal == nil {
2022-05-28 20:03:24 +00:00
if key == nil || chanType < 0 {
2022-05-22 19:16:42 +00:00
return nil
}
dbPortal = br . DB . Portal . New ( )
dbPortal . Key = * key
2022-05-28 20:03:24 +00:00
dbPortal . Type = chanType
2022-05-22 19:16:42 +00:00
dbPortal . Insert ( )
}
portal := br . NewPortal ( dbPortal )
br . portalsByID [ portal . Key ] = portal
if portal . MXID != "" {
br . portalsByMXID [ portal . MXID ] = portal
}
2022-05-28 20:03:24 +00:00
if portal . GuildID != "" {
portal . Guild = portal . bridge . GetGuildByID ( portal . GuildID , true )
}
if portal . ParentID != "" {
parentKey := database . NewPortalKey ( portal . ParentID , "" )
var ok bool
portal . Parent , ok = br . portalsByID [ parentKey ]
if ! ok {
portal . Parent = br . loadPortal ( br . DB . Portal . GetByID ( parentKey ) , nil , - 1 )
}
}
2022-05-22 19:16:42 +00:00
return portal
}
func ( br * DiscordBridge ) GetPortalByMXID ( mxid id . RoomID ) * Portal {
br . portalsLock . Lock ( )
defer br . portalsLock . Unlock ( )
portal , ok := br . portalsByMXID [ mxid ]
if ! ok {
2022-05-28 20:03:24 +00:00
return br . loadPortal ( br . DB . Portal . GetByMXID ( mxid ) , nil , - 1 )
2022-05-22 19:16:42 +00:00
}
return portal
}
2022-05-28 20:03:24 +00:00
func ( user * User ) GetPortalByMeta ( meta * discordgo . Channel ) * Portal {
return user . GetPortalByID ( meta . ID , meta . Type )
}
func ( user * User ) GetExistingPortalByID ( id string ) * Portal {
return user . bridge . GetExistingPortalByID ( database . NewPortalKey ( id , user . DiscordID ) )
}
func ( user * User ) GetPortalByID ( id string , chanType discordgo . ChannelType ) * Portal {
return user . bridge . GetPortalByID ( database . NewPortalKey ( id , user . DiscordID ) , chanType )
}
func ( br * DiscordBridge ) GetExistingPortalByID ( key database . PortalKey ) * Portal {
br . portalsLock . Lock ( )
defer br . portalsLock . Unlock ( )
portal , ok := br . portalsByID [ key ]
if ! ok {
portal , ok = br . portalsByID [ database . NewPortalKey ( key . ChannelID , "" ) ]
if ! ok {
return br . loadPortal ( br . DB . Portal . GetByID ( key ) , nil , - 1 )
}
}
return portal
}
func ( br * DiscordBridge ) GetPortalByID ( key database . PortalKey , chanType discordgo . ChannelType ) * Portal {
2022-05-22 19:16:42 +00:00
br . portalsLock . Lock ( )
defer br . portalsLock . Unlock ( )
2022-05-28 20:03:24 +00:00
if chanType != discordgo . ChannelTypeDM {
key . Receiver = ""
}
2022-05-22 19:16:42 +00:00
portal , ok := br . portalsByID [ key ]
if ! ok {
2022-05-28 20:03:24 +00:00
return br . loadPortal ( br . DB . Portal . GetByID ( key ) , & key , chanType )
2022-05-22 19:16:42 +00:00
}
return portal
}
func ( br * DiscordBridge ) GetAllPortals ( ) [ ] * Portal {
return br . dbPortalsToPortals ( br . DB . Portal . GetAll ( ) )
}
2022-06-27 08:48:51 +00:00
func ( br * DiscordBridge ) GetAllIPortals ( ) ( iportals [ ] bridge . Portal ) {
portals := br . GetAllPortals ( )
iportals = make ( [ ] bridge . Portal , len ( portals ) )
for i , portal := range portals {
iportals [ i ] = portal
}
return iportals
}
2022-05-23 20:18:10 +00:00
func ( br * DiscordBridge ) GetDMPortalsWith ( otherUserID string ) [ ] * Portal {
return br . dbPortalsToPortals ( br . DB . Portal . FindPrivateChatsWith ( otherUserID ) )
2022-05-22 19:16:42 +00:00
}
func ( br * DiscordBridge ) dbPortalsToPortals ( dbPortals [ ] * database . Portal ) [ ] * Portal {
br . portalsLock . Lock ( )
defer br . portalsLock . Unlock ( )
output := make ( [ ] * Portal , len ( dbPortals ) )
for index , dbPortal := range dbPortals {
if dbPortal == nil {
continue
}
portal , ok := br . portalsByID [ dbPortal . Key ]
if ! ok {
2022-05-28 20:03:24 +00:00
portal = br . loadPortal ( dbPortal , nil , - 1 )
2022-05-22 19:16:42 +00:00
}
output [ index ] = portal
}
return output
}
func ( br * DiscordBridge ) NewPortal ( dbPortal * database . Portal ) * Portal {
portal := & Portal {
Portal : dbPortal ,
bridge : br ,
log : br . Log . Sub ( fmt . Sprintf ( "Portal/%s" , dbPortal . Key ) ) ,
discordMessages : make ( chan portalDiscordMessage , br . Config . Bridge . PortalMessageBuffer ) ,
matrixMessages : make ( chan portalMatrixMessage , br . Config . Bridge . PortalMessageBuffer ) ,
}
go portal . messageLoop ( )
return portal
}
func ( portal * Portal ) messageLoop ( ) {
for {
select {
case msg := <- portal . matrixMessages :
portal . handleMatrixMessages ( msg )
case msg := <- portal . discordMessages :
portal . handleDiscordMessages ( msg )
}
}
}
func ( portal * Portal ) IsPrivateChat ( ) bool {
return portal . Type == discordgo . ChannelTypeDM
}
func ( portal * Portal ) MainIntent ( ) * appservice . IntentAPI {
2022-05-27 12:58:09 +00:00
if portal . IsPrivateChat ( ) && portal . OtherUserID != "" {
return portal . bridge . GetPuppetByID ( portal . OtherUserID ) . DefaultIntent ( )
2022-05-22 19:16:42 +00:00
}
return portal . bridge . Bot
}
2022-05-27 13:19:54 +00:00
func ( portal * Portal ) getBridgeInfo ( ) ( string , event . BridgeEventContent ) {
bridgeInfo := event . BridgeEventContent {
BridgeBot : portal . bridge . Bot . UserID ,
Creator : portal . MainIntent ( ) . UserID ,
Protocol : event . BridgeInfoSection {
2022-06-27 08:52:19 +00:00
ID : "discord" ,
2022-05-27 13:19:54 +00:00
DisplayName : "Discord" ,
AvatarURL : portal . bridge . Config . AppService . Bot . ParsedAvatar . CUString ( ) ,
ExternalURL : "https://discord.com/" ,
} ,
Channel : event . BridgeInfoSection {
ID : portal . Key . ChannelID ,
DisplayName : portal . Name ,
} ,
}
2022-05-28 20:03:24 +00:00
var bridgeInfoStateKey string
if portal . GuildID == "" {
bridgeInfoStateKey = fmt . Sprintf ( "fi.mau.discord://discord/dm/%s" , portal . Key . ChannelID )
2022-06-27 07:54:00 +00:00
bridgeInfo . Channel . ExternalURL = fmt . Sprintf ( "https://discord.com/channels/@me/%s" , portal . Key . ChannelID )
2022-05-28 20:03:24 +00:00
} else {
bridgeInfo . Network = & event . BridgeInfoSection {
ID : portal . GuildID ,
}
if portal . Guild != nil {
bridgeInfo . Network . DisplayName = portal . Guild . Name
bridgeInfo . Network . AvatarURL = portal . Guild . AvatarURL . CUString ( )
// TODO is it possible to find the URL?
}
bridgeInfoStateKey = fmt . Sprintf ( "fi.mau.discord://discord/%s/%s" , portal . GuildID , portal . Key . ChannelID )
2022-06-27 07:54:00 +00:00
bridgeInfo . Channel . ExternalURL = fmt . Sprintf ( "https://discord.com/channels/%s/%s" , portal . GuildID , portal . Key . ChannelID )
2022-05-28 20:03:24 +00:00
}
2022-05-27 13:19:54 +00:00
return bridgeInfoStateKey , bridgeInfo
}
func ( portal * Portal ) UpdateBridgeInfo ( ) {
if len ( portal . MXID ) == 0 {
portal . log . Debugln ( "Not updating bridge info: no Matrix room created" )
return
}
portal . log . Debugln ( "Updating bridge info..." )
stateKey , content := portal . getBridgeInfo ( )
_ , err := portal . MainIntent ( ) . SendStateEvent ( portal . MXID , event . StateBridge , stateKey , content )
if err != nil {
portal . log . Warnln ( "Failed to update m.bridge:" , err )
}
// TODO remove this once https://github.com/matrix-org/matrix-doc/pull/2346 is in spec
_ , err = portal . MainIntent ( ) . SendStateEvent ( portal . MXID , event . StateHalfShotBridge , stateKey , content )
if err != nil {
portal . log . Warnln ( "Failed to update uk.half-shot.bridge:" , err )
}
}
2022-06-22 13:56:13 +00:00
func ( portal * Portal ) GetEncryptionEventContent ( ) ( evt * event . EncryptionEventContent ) {
evt = & event . EncryptionEventContent { Algorithm : id . AlgorithmMegolmV1 }
if rot := portal . bridge . Config . Bridge . Encryption . Rotation ; rot . EnableCustom {
evt . RotationPeriodMillis = rot . Milliseconds
evt . RotationPeriodMessages = rot . Messages
}
return
}
2022-05-28 20:03:24 +00:00
func ( portal * Portal ) CreateMatrixRoom ( user * User , channel * discordgo . Channel ) error {
2022-05-22 19:16:42 +00:00
portal . roomCreateLock . Lock ( )
defer portal . roomCreateLock . Unlock ( )
if portal . MXID != "" {
return nil
}
2022-05-28 20:03:24 +00:00
portal . log . Infoln ( "Creating Matrix room for channel" )
2022-05-22 19:16:42 +00:00
2022-05-28 20:03:24 +00:00
channel = portal . UpdateInfo ( user , channel )
2022-05-30 17:46:21 +00:00
if channel == nil {
return fmt . Errorf ( "didn't find channel metadata" )
}
2022-05-22 19:16:42 +00:00
intent := portal . MainIntent ( )
if err := intent . EnsureRegistered ( ) ; err != nil {
return err
}
2022-05-27 13:19:54 +00:00
bridgeInfoStateKey , bridgeInfo := portal . getBridgeInfo ( )
initialState := [ ] * event . Event { {
Type : event . StateBridge ,
Content : event . Content { Parsed : bridgeInfo } ,
StateKey : & bridgeInfoStateKey ,
} , {
// TODO remove this once https://github.com/matrix-org/matrix-doc/pull/2346 is in spec
Type : event . StateHalfShotBridge ,
Content : event . Content { Parsed : bridgeInfo } ,
StateKey : & bridgeInfoStateKey ,
} }
2022-05-22 19:16:42 +00:00
2022-05-28 20:03:24 +00:00
if ! portal . AvatarURL . IsEmpty ( ) {
initialState = append ( initialState , & event . Event {
Type : event . StateRoomAvatar ,
Content : event . Content { Parsed : & event . RoomAvatarEventContent {
URL : portal . AvatarURL ,
} } ,
} )
}
2022-05-22 19:16:42 +00:00
creationContent := make ( map [ string ] interface { } )
2022-05-28 20:03:24 +00:00
if portal . Type == discordgo . ChannelTypeGuildCategory {
creationContent [ "type" ] = event . RoomTypeSpace
}
2022-05-27 12:58:09 +00:00
if ! portal . bridge . Config . Bridge . FederateRooms {
creationContent [ "m.federate" ] = false
}
2022-05-28 20:03:24 +00:00
spaceID := portal . ExpectedSpaceID ( )
if spaceID != "" {
spaceIDStr := spaceID . String ( )
initialState = append ( initialState , & event . Event {
Type : event . StateSpaceParent ,
StateKey : & spaceIDStr ,
Content : event . Content { Parsed : & event . SpaceParentEventContent {
Via : [ ] string { portal . bridge . AS . HomeserverDomain } ,
Canonical : true ,
} } ,
} )
}
2022-05-30 12:59:26 +00:00
if portal . bridge . Config . Bridge . RestrictedRooms && portal . Guild != nil && portal . Guild . MXID != "" {
2022-05-28 20:03:24 +00:00
// TODO don't do this for private channels in guilds
initialState = append ( initialState , & event . Event {
Type : event . StateJoinRules ,
Content : event . Content { Parsed : & event . JoinRulesEventContent {
JoinRule : event . JoinRuleRestricted ,
Allow : [ ] event . JoinRuleAllow { {
RoomID : spaceID ,
Type : event . JoinRuleAllowRoomMembership ,
} } ,
} } ,
} )
}
2022-05-22 19:16:42 +00:00
var invite [ ] id . UserID
if portal . bridge . Config . Bridge . Encryption . Default {
initialState = append ( initialState , & event . Event {
Type : event . StateEncryption ,
Content : event . Content {
2022-06-22 13:56:13 +00:00
Parsed : portal . GetEncryptionEventContent ( ) ,
2022-05-22 19:16:42 +00:00
} ,
} )
portal . Encrypted = true
if portal . IsPrivateChat ( ) {
invite = append ( invite , portal . bridge . Bot . UserID )
}
}
resp , err := intent . CreateRoom ( & mautrix . ReqCreateRoom {
Visibility : "private" ,
Name : portal . Name ,
Topic : portal . Topic ,
Invite : invite ,
Preset : "private_chat" ,
IsDirect : portal . IsPrivateChat ( ) ,
InitialState : initialState ,
CreationContent : creationContent ,
} )
if err != nil {
portal . log . Warnln ( "Failed to create room:" , err )
return err
}
2022-05-28 20:03:24 +00:00
portal . NameSet = true
portal . TopicSet = true
portal . AvatarSet = ! portal . AvatarURL . IsEmpty ( )
2022-05-22 19:16:42 +00:00
portal . MXID = resp . RoomID
portal . bridge . portalsLock . Lock ( )
portal . bridge . portalsByMXID [ portal . MXID ] = portal
portal . bridge . portalsLock . Unlock ( )
2022-05-28 20:03:24 +00:00
portal . Update ( )
2022-05-27 13:19:54 +00:00
portal . log . Infoln ( "Matrix room created:" , portal . MXID )
2022-05-22 19:16:42 +00:00
2022-05-28 20:03:24 +00:00
portal . updateSpace ( )
2022-05-22 19:16:42 +00:00
portal . ensureUserInvited ( user )
user . syncChatDoublePuppetDetails ( portal , true )
portal . syncParticipants ( user , channel . Recipients )
if portal . IsPrivateChat ( ) {
puppet := user . bridge . GetPuppetByID ( portal . Key . Receiver )
chats := map [ id . UserID ] [ ] id . RoomID { puppet . MXID : { portal . MXID } }
user . updateDirectChats ( chats )
}
firstEventResp , err := portal . MainIntent ( ) . SendMessageEvent ( portal . MXID , portalCreationDummyEvent , struct { } { } )
if err != nil {
portal . log . Errorln ( "Failed to send dummy event to mark portal creation:" , err )
} else {
portal . FirstEventID = firstEventResp . EventID
portal . Update ( )
}
return nil
}
func ( portal * Portal ) handleDiscordMessages ( msg portalDiscordMessage ) {
if portal . MXID == "" {
2022-05-28 20:03:24 +00:00
_ , ok := msg . msg . ( * discordgo . MessageCreate )
2022-05-22 19:16:42 +00:00
if ! ok {
portal . log . Warnln ( "Can't create Matrix room from non new message event" )
return
}
portal . log . Debugln ( "Creating Matrix room from incoming message" )
2022-05-28 20:03:24 +00:00
if err := portal . CreateMatrixRoom ( msg . user , nil ) ; err != nil {
2022-05-22 19:16:42 +00:00
portal . log . Errorln ( "Failed to create portal room:" , err )
return
}
}
2022-05-28 20:03:24 +00:00
switch convertedMsg := msg . msg . ( type ) {
2022-05-22 19:16:42 +00:00
case * discordgo . MessageCreate :
2022-05-28 20:03:24 +00:00
portal . handleDiscordMessageCreate ( msg . user , convertedMsg . Message , msg . thread )
2022-05-22 19:16:42 +00:00
case * discordgo . MessageUpdate :
2022-05-28 20:03:24 +00:00
portal . handleDiscordMessageUpdate ( msg . user , convertedMsg . Message )
2022-05-22 19:16:42 +00:00
case * discordgo . MessageDelete :
2022-05-28 20:03:24 +00:00
portal . handleDiscordMessageDelete ( msg . user , convertedMsg . Message )
2022-05-22 19:16:42 +00:00
case * discordgo . MessageReactionAdd :
2022-05-28 20:03:24 +00:00
portal . handleDiscordReaction ( msg . user , convertedMsg . MessageReaction , true , msg . thread )
2022-05-22 19:16:42 +00:00
case * discordgo . MessageReactionRemove :
2022-05-28 20:03:24 +00:00
portal . handleDiscordReaction ( msg . user , convertedMsg . MessageReaction , false , msg . thread )
2022-05-22 19:16:42 +00:00
default :
portal . log . Warnln ( "unknown message type" )
}
}
func ( portal * Portal ) ensureUserInvited ( user * User ) bool {
return user . ensureInvited ( portal . MainIntent ( ) , portal . MXID , portal . IsPrivateChat ( ) )
}
2022-06-27 07:53:49 +00:00
func ( portal * Portal ) markMessageHandled ( discordID string , editIndex int , authorID string , timestamp time . Time , threadID string , parts [ ] database . MessagePart ) {
2022-05-27 12:58:09 +00:00
msg := portal . bridge . DB . Message . New ( )
msg . Channel = portal . Key
msg . DiscordID = discordID
2022-06-27 07:53:49 +00:00
msg . EditIndex = editIndex
2022-05-27 12:58:09 +00:00
msg . SenderID = authorID
msg . Timestamp = timestamp
2022-05-28 20:03:24 +00:00
msg . ThreadID = threadID
2022-06-27 07:53:49 +00:00
msg . MassInsert ( parts )
2022-05-22 19:16:42 +00:00
}
func ( portal * Portal ) sendMediaFailedMessage ( intent * appservice . IntentAPI , bridgeErr error ) {
content := & event . MessageEventContent {
Body : fmt . Sprintf ( "Failed to bridge media: %v" , bridgeErr ) ,
MsgType : event . MsgNotice ,
}
2022-05-28 21:22:00 +00:00
_ , err := portal . sendMatrixMessage ( intent , event . EventMessage , content , nil , 0 )
2022-05-22 19:16:42 +00:00
if err != nil {
2022-05-28 21:22:00 +00:00
portal . log . Warnfln ( "Failed to send media error message to matrix: %v" , err )
2022-05-22 19:16:42 +00:00
}
}
2022-06-27 07:53:49 +00:00
func ( portal * Portal ) handleDiscordAttachment ( intent * appservice . IntentAPI , msgID string , attachment * discordgo . MessageAttachment , ts time . Time , threadRelation * event . RelatesTo , threadID string ) * database . MessagePart {
2022-05-22 19:16:42 +00:00
// var captionContent *event.MessageEventContent
// if attachment.Description != "" {
// captionContent = &event.MessageEventContent{
// Body: attachment.Description,
// MsgType: event.MsgNotice,
// }
// }
// portal.Log.Debugfln("captionContent: %#v", captionContent)
content := & event . MessageEventContent {
Body : attachment . Filename ,
Info : & event . FileInfo {
Height : attachment . Height ,
MimeType : attachment . ContentType ,
Width : attachment . Width ,
// This gets overwritten later after the file is uploaded to the homeserver
Size : attachment . Size ,
} ,
2022-05-28 20:03:24 +00:00
RelatesTo : threadRelation ,
2022-05-22 19:16:42 +00:00
}
switch strings . ToLower ( strings . Split ( attachment . ContentType , "/" ) [ 0 ] ) {
case "audio" :
content . MsgType = event . MsgAudio
case "image" :
content . MsgType = event . MsgImage
case "video" :
content . MsgType = event . MsgVideo
default :
content . MsgType = event . MsgFile
}
data , err := portal . downloadDiscordAttachment ( attachment . URL )
if err != nil {
portal . sendMediaFailedMessage ( intent , err )
2022-06-27 07:53:49 +00:00
return nil
2022-05-22 19:16:42 +00:00
}
err = portal . uploadMatrixAttachment ( intent , data , content )
if err != nil {
portal . sendMediaFailedMessage ( intent , err )
2022-06-27 07:53:49 +00:00
return nil
2022-05-22 19:16:42 +00:00
}
2022-05-28 20:03:24 +00:00
resp , err := portal . sendMatrixMessage ( intent , event . EventMessage , content , nil , ts . UnixMilli ( ) )
2022-05-22 19:16:42 +00:00
if err != nil {
portal . log . Warnfln ( "failed to send media message to matrix: %v" , err )
}
2022-05-28 20:03:24 +00:00
// Update the fallback reply event for the next attachment
if threadRelation != nil {
threadRelation . InReplyTo . EventID = resp . EventID
}
2022-06-27 07:53:49 +00:00
return & database . MessagePart {
AttachmentID : attachment . ID ,
MXID : resp . EventID ,
}
2022-05-22 19:16:42 +00:00
}
2022-05-28 20:03:24 +00:00
func ( portal * Portal ) handleDiscordMessageCreate ( user * User , msg * discordgo . Message , thread * Thread ) {
2022-05-22 19:16:42 +00:00
if portal . MXID == "" {
portal . log . Warnln ( "handle message called without a valid portal" )
return
}
// Handle room name changes
if msg . Type == discordgo . MessageTypeChannelNameChange {
2022-05-28 20:03:24 +00:00
//channel, err := user.Session.Channel(msg.ChannelID)
//if err != nil {
// portal.log.Errorf("Failed to find the channel for portal %s", portal.Key)
// return
//}
//
//name, err := portal.bridge.Config.Bridge.FormatChannelname(channel, user.Session)
//if err != nil {
// portal.log.Errorf("Failed to format name for portal %s", portal.Key)
// return
//}
//
//portal.Name = name
//portal.Update()
//
//portal.MainIntent().SetRoomName(portal.MXID, name)
2022-05-22 19:16:42 +00:00
return
}
// Handle normal message
existing := portal . bridge . DB . Message . GetByDiscordID ( portal . Key , msg . ID )
if existing != nil {
2022-05-28 20:03:24 +00:00
portal . log . Debugln ( "Dropping duplicate message" , msg . ID )
2022-05-22 19:16:42 +00:00
return
}
2022-05-28 20:03:24 +00:00
portal . log . Debugfln ( "Starting handling of %s by %s" , msg . ID , msg . Author . ID )
2022-05-22 19:16:42 +00:00
puppet := portal . bridge . GetPuppetByID ( msg . Author . ID )
2022-05-28 20:03:24 +00:00
puppet . UpdateInfo ( user , msg . Author )
2022-05-22 19:16:42 +00:00
intent := puppet . IntentFor ( portal )
2022-05-28 20:03:24 +00:00
var threadRelation * event . RelatesTo
var threadID string
if thread != nil {
threadID = thread . ID
lastEventID := thread . RootMXID
lastInThread := portal . bridge . DB . Message . GetLastInThread ( portal . Key , thread . ID )
if lastInThread != nil {
lastEventID = lastInThread . MXID
}
threadRelation = ( & event . RelatesTo { } ) . SetThread ( thread . RootMXID , lastEventID )
}
2022-06-27 07:53:49 +00:00
var parts [ ] database . MessagePart
2022-05-28 20:03:24 +00:00
ts , _ := discordgo . SnowflakeTimestamp ( msg . ID )
2022-05-22 19:16:42 +00:00
if msg . Content != "" {
2022-07-02 11:48:42 +00:00
content := portal . renderDiscordMarkdown ( msg . Content )
2022-05-29 11:13:34 +00:00
content . RelatesTo = threadRelation . Copy ( )
2022-05-22 19:16:42 +00:00
2022-05-28 20:03:24 +00:00
if msg . MessageReference != nil {
2022-05-27 12:58:09 +00:00
//key := database.PortalKey{msg.MessageReference.ChannelID, user.ID}
replyTo := portal . bridge . DB . Message . GetByDiscordID ( portal . Key , msg . MessageReference . MessageID )
2022-06-27 07:53:49 +00:00
if len ( replyTo ) > 0 {
2022-05-28 20:03:24 +00:00
if content . RelatesTo == nil {
content . RelatesTo = & event . RelatesTo { }
2022-05-22 19:16:42 +00:00
}
2022-06-27 07:53:49 +00:00
content . RelatesTo . SetReplyTo ( replyTo [ 0 ] . MXID )
2022-05-22 19:16:42 +00:00
}
}
2022-05-29 11:13:34 +00:00
resp , err := portal . sendMatrixMessage ( intent , event . EventMessage , & content , nil , ts . UnixMilli ( ) )
2022-05-22 19:16:42 +00:00
if err != nil {
portal . log . Warnfln ( "failed to send message %q to matrix: %v" , msg . ID , err )
return
}
2022-06-27 07:53:49 +00:00
parts = append ( parts , database . MessagePart { MXID : resp . EventID } )
2022-05-28 20:03:24 +00:00
// Update the fallback reply event for attachments
if threadRelation != nil {
threadRelation . InReplyTo . EventID = resp . EventID
}
2022-05-30 12:58:03 +00:00
go portal . sendDeliveryReceipt ( resp . EventID )
2022-05-22 19:16:42 +00:00
}
for _ , attachment := range msg . Attachments {
2022-06-27 07:53:49 +00:00
part := portal . handleDiscordAttachment ( intent , msg . ID , attachment , ts , threadRelation , threadID )
if part != nil {
parts = append ( parts , * part )
}
2022-05-22 19:16:42 +00:00
}
2022-06-27 07:53:49 +00:00
portal . markMessageHandled ( msg . ID , 0 , msg . Author . ID , ts , threadID , parts )
2022-05-22 19:16:42 +00:00
}
2022-05-28 20:03:24 +00:00
func ( portal * Portal ) handleDiscordMessageUpdate ( user * User , msg * discordgo . Message ) {
2022-05-22 19:16:42 +00:00
if portal . MXID == "" {
portal . log . Warnln ( "handle message called without a valid portal" )
return
}
2022-05-28 20:03:24 +00:00
existing := portal . bridge . DB . Message . GetByDiscordID ( portal . Key , msg . ID )
if existing == nil {
portal . log . Warnfln ( "Dropping update of unknown message %s" , msg . ID )
return
}
if msg . Flags == discordgo . MessageFlagsHasThread {
2022-06-27 07:53:49 +00:00
portal . bridge . GetThreadByID ( msg . ID , existing [ 0 ] )
2022-05-28 20:03:24 +00:00
portal . log . Debugfln ( "Marked %s as a thread root" , msg . ID )
2022-05-28 21:32:02 +00:00
// TODO make autojoining configurable
//err := user.Session.ThreadJoinWithLocation(msg.ID, discordgo.ThreadJoinLocationContextMenu)
//if err != nil {
// user.log.Warnfln("Error autojoining thread %s@%s: %v", msg.ChannelID, portal.Key.ChannelID, err)
//}
2022-05-28 20:03:24 +00:00
}
2022-05-22 19:16:42 +00:00
// There's a few scenarios where the author is nil but I haven't figured
// them all out yet.
if msg . Author == nil {
// If the server has to lookup opengraph previews it'll send the
// message through without the preview and then add the preview later
// via a message update. However, when it does this there is no author
// as it's just the server, so for the moment we'll ignore this to
// avoid a crash.
if len ( msg . Embeds ) > 0 {
portal . log . Debugln ( "ignoring update for opengraph attachment" )
return
}
2022-05-28 21:32:02 +00:00
//portal.log.Errorfln("author is nil: %#v", msg)
2022-05-28 20:03:24 +00:00
return
2022-05-22 19:16:42 +00:00
}
intent := portal . bridge . GetPuppetByID ( msg . Author . ID ) . IntentFor ( portal )
2022-06-27 13:13:26 +00:00
attachmentMap := map [ string ] * database . Message { }
for _ , existingPart := range existing {
if existingPart . AttachmentID != "" {
attachmentMap [ existingPart . AttachmentID ] = existingPart
}
}
2022-07-02 11:48:42 +00:00
for _ , remainingAttachment := range msg . Attachments {
if _ , found := attachmentMap [ remainingAttachment . ID ] ; found {
delete ( attachmentMap , remainingAttachment . ID )
2022-06-27 13:13:26 +00:00
}
}
2022-07-02 11:48:42 +00:00
for _ , deletedAttachment := range attachmentMap {
_ , err := intent . RedactEvent ( portal . MXID , deletedAttachment . MXID )
2022-06-27 13:13:26 +00:00
if err != nil {
2022-07-02 11:48:42 +00:00
portal . log . Warnfln ( "Failed to remove attachment %s: %v" , deletedAttachment . MXID , err )
2022-06-27 13:13:26 +00:00
}
2022-07-02 11:48:42 +00:00
deletedAttachment . Delete ( )
2022-06-27 13:13:26 +00:00
}
2022-06-27 07:53:49 +00:00
if msg . Content == "" || existing [ 0 ] . AttachmentID != "" {
2022-06-27 13:13:26 +00:00
portal . log . Debugfln ( "Dropping non-text edit to %s (message on matrix: %t, text on discord: %t)" , msg . ID , existing [ 0 ] . AttachmentID == "" , len ( msg . Content ) > 0 )
2022-05-22 19:16:42 +00:00
return
}
2022-07-02 11:48:42 +00:00
content := portal . renderDiscordMarkdown ( msg . Content )
2022-06-27 07:53:49 +00:00
content . SetEdit ( existing [ 0 ] . MXID )
2022-05-22 19:16:42 +00:00
2022-05-28 21:22:00 +00:00
var editTS int64
if msg . EditedTimestamp != nil {
editTS = msg . EditedTimestamp . UnixMilli ( )
}
// TODO figure out some way to deduplicate outgoing edits
2022-05-30 12:58:03 +00:00
resp , err := portal . sendMatrixMessage ( intent , event . EventMessage , & content , nil , editTS )
2022-05-22 19:16:42 +00:00
if err != nil {
portal . log . Warnfln ( "failed to send message %q to matrix: %v" , msg . ID , err )
return
}
2022-05-30 12:58:03 +00:00
portal . sendDeliveryReceipt ( resp . EventID )
2022-05-27 12:58:09 +00:00
//ts, _ := msg.Timestamp.Parse()
//portal.markMessageHandled(existing, msg.ID, resp.EventID, msg.Author.ID, ts)
2022-05-22 19:16:42 +00:00
}
func ( portal * Portal ) handleDiscordMessageDelete ( user * User , msg * discordgo . Message ) {
existing := portal . bridge . DB . Message . GetByDiscordID ( portal . Key , msg . ID )
2022-05-27 12:58:09 +00:00
intent := portal . MainIntent ( )
2022-06-27 07:53:49 +00:00
var lastResp id . EventID
for _ , dbMsg := range existing {
resp , err := intent . RedactEvent ( portal . MXID , dbMsg . MXID )
2022-05-22 19:16:42 +00:00
if err != nil {
2022-06-27 07:53:49 +00:00
portal . log . Warnfln ( "Failed to redact message %s: %v" , dbMsg . MXID , err )
} else if resp != nil && resp . EventID != "" {
lastResp = resp . EventID
2022-05-22 19:16:42 +00:00
}
2022-06-27 07:53:49 +00:00
dbMsg . Delete ( )
}
if lastResp != "" {
portal . sendDeliveryReceipt ( lastResp )
2022-05-22 19:16:42 +00:00
}
}
func ( portal * Portal ) syncParticipants ( source * User , participants [ ] * discordgo . User ) {
for _ , participant := range participants {
puppet := portal . bridge . GetPuppetByID ( participant . ID )
2022-05-28 20:03:24 +00:00
puppet . UpdateInfo ( source , participant )
2022-05-22 19:16:42 +00:00
user := portal . bridge . GetUserByID ( participant . ID )
if user != nil {
portal . ensureUserInvited ( user )
}
if user == nil || ! puppet . IntentFor ( portal ) . IsCustomPuppet {
if err := puppet . IntentFor ( portal ) . EnsureJoined ( portal . MXID ) ; err != nil {
portal . log . Warnfln ( "Failed to make puppet of %s join %s: %v" , participant . ID , portal . MXID , err )
}
}
}
}
2022-06-30 19:00:12 +00:00
func ( portal * Portal ) encrypt ( intent * appservice . IntentAPI , content * event . Content , eventType event . Type ) ( event . Type , error ) {
if ! portal . Encrypted || portal . bridge . Crypto == nil {
return eventType , nil
}
intent . AddDoublePuppetValue ( content )
// TODO maybe the locking should be inside mautrix-go?
portal . encryptLock . Lock ( )
err := portal . bridge . Crypto . Encrypt ( portal . MXID , eventType , content )
portal . encryptLock . Unlock ( )
if err != nil {
return eventType , fmt . Errorf ( "failed to encrypt event: %w" , err )
2022-05-22 19:16:42 +00:00
}
2022-06-30 19:00:12 +00:00
return event . EventEncrypted , nil
2022-05-22 19:16:42 +00:00
}
func ( portal * Portal ) sendMatrixMessage ( intent * appservice . IntentAPI , eventType event . Type , content * event . MessageEventContent , extraContent map [ string ] interface { } , timestamp int64 ) ( * mautrix . RespSendEvent , error ) {
wrappedContent := event . Content { Parsed : content , Raw : extraContent }
var err error
2022-06-30 19:00:12 +00:00
eventType , err = portal . encrypt ( intent , & wrappedContent , eventType )
2022-05-22 19:16:42 +00:00
if err != nil {
return nil , err
}
_ , _ = intent . UserTyping ( portal . MXID , false , 0 )
if timestamp == 0 {
return intent . SendMessageEvent ( portal . MXID , eventType , & wrappedContent )
} else {
return intent . SendMassagedMessageEvent ( portal . MXID , eventType , & wrappedContent , timestamp )
}
}
func ( portal * Portal ) handleMatrixMessages ( msg portalMatrixMessage ) {
switch msg . evt . Type {
case event . EventMessage :
portal . handleMatrixMessage ( msg . user , msg . evt )
case event . EventRedaction :
portal . handleMatrixRedaction ( msg . user , msg . evt )
case event . EventReaction :
portal . handleMatrixReaction ( msg . user , msg . evt )
default :
portal . log . Debugln ( "unknown event type" , msg . evt . Type )
}
}
2022-05-28 20:03:24 +00:00
const discordEpoch = 1420070400000
func generateNonce ( ) string {
snowflake := ( time . Now ( ) . UnixMilli ( ) - discordEpoch ) << 22
// Nonce snowflakes don't have internal IDs or increments
return strconv . FormatInt ( snowflake , 10 )
}
2022-05-28 21:22:00 +00:00
func ( portal * Portal ) getEvent ( mxid id . EventID ) ( * event . Event , error ) {
evt , err := portal . MainIntent ( ) . GetEvent ( portal . MXID , mxid )
if err != nil {
return nil , err
}
_ = evt . Content . ParseRaw ( evt . Type )
if evt . Type == event . EventEncrypted {
decryptedEvt , err := portal . bridge . Crypto . Decrypt ( evt )
if err != nil {
return nil , err
} else {
evt = decryptedEvt
}
}
return evt , nil
}
func genThreadName ( evt * event . Event ) string {
body := evt . Content . AsMessage ( ) . Body
if len ( body ) == 0 {
return "thread"
}
fields := strings . Fields ( body )
var title string
for _ , field := range fields {
if len ( title ) + len ( field ) < 40 {
title += field
title += " "
continue
}
if len ( title ) == 0 {
title = field [ : 40 ]
}
break
}
return title
}
func ( portal * Portal ) startThreadFromMatrix ( sender * User , threadRoot id . EventID ) ( string , error ) {
rootEvt , err := portal . getEvent ( threadRoot )
if err != nil {
return "" , fmt . Errorf ( "failed to get root event: %w" , err )
}
threadName := genThreadName ( rootEvt )
existingMsg := portal . bridge . DB . Message . GetByMXID ( portal . Key , threadRoot )
if existingMsg == nil {
return "" , fmt . Errorf ( "unknown root event" )
} else if existingMsg . ThreadID != "" {
return "" , fmt . Errorf ( "root event is already in a thread" )
} else {
var ch * discordgo . Channel
ch , err = sender . Session . MessageThreadStartComplex ( portal . Key . ChannelID , existingMsg . DiscordID , & discordgo . ThreadStart {
Name : threadName ,
AutoArchiveDuration : 24 * 60 ,
Type : discordgo . ChannelTypeGuildPublicThread ,
Location : "Message" ,
} )
if err != nil {
return "" , fmt . Errorf ( "error starting thread: %v" , err )
}
portal . log . Debugfln ( "Created Discord thread from %s/%s" , threadRoot , ch . ID )
portal . bridge . GetThreadByID ( existingMsg . DiscordID , existingMsg )
return ch . ID , nil
}
}
2022-06-28 08:55:36 +00:00
func ( portal * Portal ) sendErrorMessage ( msgType , message string , confirmed bool ) id . EventID {
if ! portal . bridge . Config . Bridge . MessageErrorNotices {
return ""
}
certainty := "may not have been"
if confirmed {
certainty = "was not"
}
resp , err := portal . sendMatrixMessage ( portal . MainIntent ( ) , event . EventMessage , & event . MessageEventContent {
MsgType : event . MsgNotice ,
Body : fmt . Sprintf ( "\u26a0 Your %s %s bridged: %v" , msgType , certainty , message ) ,
} , nil , 0 )
if err != nil {
portal . log . Warnfln ( "Failed to send bridging error message:" , err )
return ""
}
return resp . EventID
}
var (
errUnknownMsgType = errors . New ( "unknown msgtype" )
errUnexpectedParsedContentType = errors . New ( "unexpected parsed content type" )
errUserNotReceiver = errors . New ( "user is not portal receiver" )
errUnknownEditTarget = errors . New ( "unknown edit target" )
errUnknownRelationType = errors . New ( "unknown relation type" )
errTargetNotFound = errors . New ( "target event not found" )
errUnknownEmoji = errors . New ( "unknown emoji" )
)
func errorToStatusReason ( err error ) ( reason event . MessageStatusReason , isCertain , canRetry , sendNotice bool ) {
switch {
case errors . Is ( err , errUnknownMsgType ) ,
errors . Is ( err , errUnknownRelationType ) ,
errors . Is ( err , errUnexpectedParsedContentType ) ,
errors . Is ( err , errUnknownEmoji ) ,
errors . Is ( err , id . InvalidContentURI ) ,
errors . Is ( err , attachment . UnsupportedVersion ) ,
errors . Is ( err , attachment . UnsupportedAlgorithm ) :
return event . MessageStatusUnsupported , true , false , true
case errors . Is ( err , attachment . HashMismatch ) ,
errors . Is ( err , attachment . InvalidKey ) ,
errors . Is ( err , attachment . InvalidInitVector ) :
return event . MessageStatusUndecryptable , true , false , true
case errors . Is ( err , errUserNotReceiver ) :
return event . MessageStatusNoPermission , true , false , false
case errors . Is ( err , errUnknownEditTarget ) :
return event . MessageStatusGenericError , true , false , false
case errors . Is ( err , errTargetNotFound ) :
return event . MessageStatusGenericError , true , false , true
default :
return event . MessageStatusGenericError , false , true , true
}
}
func ( portal * Portal ) sendStatusEvent ( evtID id . EventID , err error ) {
if ! portal . bridge . Config . Bridge . MessageStatusEvents {
return
}
intent := portal . bridge . Bot
if ! portal . Encrypted {
// Bridge bot isn't present in unencrypted DMs
intent = portal . MainIntent ( )
}
stateKey , _ := portal . getBridgeInfo ( )
content := event . BeeperMessageStatusEventContent {
Network : stateKey ,
RelatesTo : event . RelatesTo {
Type : event . RelReference ,
EventID : evtID ,
} ,
Success : err == nil ,
}
if ! content . Success {
reason , isCertain , canRetry , _ := errorToStatusReason ( err )
content . Reason = reason
content . IsCertain = & isCertain
content . CanRetry = & canRetry
content . Error = err . Error ( )
}
_ , err = intent . SendMessageEvent ( portal . MXID , event . BeeperMessageStatus , & content )
if err != nil {
portal . log . Warnln ( "Failed to send message status event:" , err )
}
}
func ( portal * Portal ) sendMessageMetrics ( evt * event . Event , err error , part string ) {
var msgType string
switch evt . Type {
case event . EventMessage :
msgType = "message"
case event . EventReaction :
msgType = "reaction"
case event . EventRedaction :
msgType = "redaction"
default :
msgType = "unknown event"
}
evtDescription := evt . ID . String ( )
if evt . Type == event . EventRedaction {
evtDescription += fmt . Sprintf ( " of %s" , evt . Redacts )
}
if err != nil {
level := log . LevelError
if part == "Ignoring" {
level = log . LevelDebug
}
portal . log . Logfln ( level , "%s %s %s from %s: %v" , part , msgType , evtDescription , evt . Sender , err )
reason , isCertain , _ , sendNotice := errorToStatusReason ( err )
status := bridge . ReasonToCheckpointStatus ( reason )
portal . bridge . SendMessageCheckpoint ( evt , bridge . MsgStepRemote , err , status , 0 )
if sendNotice {
portal . sendErrorMessage ( msgType , err . Error ( ) , isCertain )
}
portal . sendStatusEvent ( evt . ID , err )
} else {
portal . log . Debugfln ( "Handled Matrix %s %s" , msgType , evtDescription )
portal . sendDeliveryReceipt ( evt . ID )
portal . bridge . SendMessageSuccessCheckpoint ( evt , bridge . MsgStepRemote , 0 )
portal . sendStatusEvent ( evt . ID , nil )
}
}
2022-05-22 19:16:42 +00:00
func ( portal * Portal ) handleMatrixMessage ( sender * User , evt * event . Event ) {
2022-05-28 20:03:24 +00:00
if portal . IsPrivateChat ( ) && sender . DiscordID != portal . Key . Receiver {
2022-06-28 08:55:36 +00:00
go portal . sendMessageMetrics ( evt , errUserNotReceiver , "Ignoring" )
2022-05-22 19:16:42 +00:00
return
}
content , ok := evt . Content . Parsed . ( * event . MessageEventContent )
if ! ok {
2022-06-28 08:55:36 +00:00
go portal . sendMessageMetrics ( evt , fmt . Errorf ( "%w %T" , errUnexpectedParsedContentType , evt . Content . Parsed ) , "Ignoring" )
2022-05-22 19:16:42 +00:00
return
}
2022-05-28 20:03:24 +00:00
channelID := portal . Key . ChannelID
var threadID string
2022-05-22 19:16:42 +00:00
2022-05-28 20:03:24 +00:00
if editMXID := content . GetRelatesTo ( ) . GetReplaceID ( ) ; editMXID != "" && content . NewContent != nil {
edits := portal . bridge . DB . Message . GetByMXID ( portal . Key , editMXID )
2022-05-27 12:58:09 +00:00
if edits != nil {
2022-05-29 15:48:11 +00:00
discordContent := portal . parseMatrixHTML ( sender , content . NewContent )
2022-06-28 08:55:36 +00:00
// TODO save edit in message table
2022-05-29 15:48:11 +00:00
_ , err := sender . Session . ChannelMessageEdit ( edits . DiscordProtoChannelID ( ) , edits . DiscordID , discordContent )
2022-06-28 08:55:36 +00:00
go portal . sendMessageMetrics ( evt , err , "Failed to edit" )
} else {
go portal . sendMessageMetrics ( evt , fmt . Errorf ( "%w %s" , errUnknownEditTarget , editMXID ) , "Ignoring" )
2022-05-22 19:16:42 +00:00
}
return
2022-05-28 20:03:24 +00:00
} else if threadRoot := content . GetRelatesTo ( ) . GetThreadParent ( ) ; threadRoot != "" {
existingThread := portal . bridge . DB . Thread . GetByMatrixRootMsg ( threadRoot )
if existingThread != nil {
threadID = existingThread . ID
} else {
2022-05-28 21:22:00 +00:00
var err error
threadID , err = portal . startThreadFromMatrix ( sender , threadRoot )
if err != nil {
portal . log . Warnfln ( "Failed to start thread from %s: %v" , threadRoot , err )
}
2022-05-28 20:03:24 +00:00
}
2022-05-22 19:16:42 +00:00
}
2022-05-28 21:22:00 +00:00
if threadID != "" {
channelID = threadID
}
2022-05-22 19:16:42 +00:00
2022-05-28 20:03:24 +00:00
var sendReq discordgo . MessageSend
2022-05-22 19:16:42 +00:00
switch content . MsgType {
case event . MsgText , event . MsgEmote , event . MsgNotice :
2022-05-28 20:03:24 +00:00
if replyToMXID := content . GetReplyTo ( ) ; replyToMXID != "" {
replyTo := portal . bridge . DB . Message . GetByMXID ( portal . Key , replyToMXID )
if replyTo != nil && replyTo . ThreadID == threadID {
sendReq . Reference = & discordgo . MessageReference {
ChannelID : channelID ,
MessageID : replyTo . DiscordID ,
2022-05-22 19:16:42 +00:00
}
}
}
2022-05-29 15:48:11 +00:00
sendReq . Content = portal . parseMatrixHTML ( sender , content )
2022-05-22 19:16:42 +00:00
case event . MsgAudio , event . MsgFile , event . MsgImage , event . MsgVideo :
2022-06-28 08:55:36 +00:00
data , err := portal . downloadMatrixAttachment ( content )
2022-05-22 19:16:42 +00:00
if err != nil {
2022-06-28 08:55:36 +00:00
go portal . sendMessageMetrics ( evt , err , "Error downloading media in" )
2022-05-22 19:16:42 +00:00
return
}
2022-05-28 20:03:24 +00:00
sendReq . Files = [ ] * discordgo . File { {
Name : content . Body ,
ContentType : content . Info . MimeType ,
Reader : bytes . NewReader ( data ) ,
} }
2022-06-28 08:55:36 +00:00
if content . FileName != "" && content . FileName != content . Body {
sendReq . Files [ 0 ] . Name = content . FileName
sendReq . Content = portal . parseMatrixHTML ( sender , content )
}
2022-05-22 19:16:42 +00:00
default :
2022-06-28 08:55:36 +00:00
go portal . sendMessageMetrics ( evt , fmt . Errorf ( "%w %q" , errUnknownMsgType , content . MsgType ) , "Ignoring" )
2022-05-22 19:16:42 +00:00
return
}
2022-05-28 20:03:24 +00:00
sendReq . Nonce = generateNonce ( )
msg , err := sender . Session . ChannelMessageSendComplex ( channelID , & sendReq )
2022-06-28 08:55:36 +00:00
go portal . sendMessageMetrics ( evt , err , "Error sending" )
2022-05-22 19:16:42 +00:00
if msg != nil {
dbMsg := portal . bridge . DB . Message . New ( )
dbMsg . Channel = portal . Key
dbMsg . DiscordID = msg . ID
2022-06-27 07:53:49 +00:00
if len ( msg . Attachments ) > 0 {
dbMsg . AttachmentID = msg . Attachments [ 0 ] . ID
}
2022-05-27 12:58:09 +00:00
dbMsg . MXID = evt . ID
2022-05-28 20:03:24 +00:00
dbMsg . SenderID = sender . DiscordID
dbMsg . Timestamp , _ = discordgo . SnowflakeTimestamp ( msg . ID )
dbMsg . ThreadID = threadID
2022-05-22 19:16:42 +00:00
dbMsg . Insert ( )
2022-05-30 12:58:03 +00:00
}
}
func ( portal * Portal ) sendDeliveryReceipt ( eventID id . EventID ) {
if portal . bridge . Config . Bridge . DeliveryReceipts {
err := portal . bridge . Bot . MarkRead ( portal . MXID , eventID )
if err != nil {
portal . log . Debugfln ( "Failed to send delivery receipt for %s: %v" , eventID , err )
}
2022-05-22 19:16:42 +00:00
}
}
func ( portal * Portal ) HandleMatrixLeave ( brSender bridge . User ) {
portal . log . Debugln ( "User left private chat portal, cleaning up and deleting..." )
2022-05-28 20:03:24 +00:00
portal . Delete ( )
2022-05-22 19:16:42 +00:00
portal . cleanup ( false )
// TODO: figure out how to close a dm from the API.
portal . cleanupIfEmpty ( )
}
func ( portal * Portal ) leave ( sender * User ) {
if portal . MXID == "" {
return
}
2022-05-28 20:03:24 +00:00
intent := portal . bridge . GetPuppetByID ( sender . DiscordID ) . IntentFor ( portal )
2022-05-22 19:16:42 +00:00
intent . LeaveRoom ( portal . MXID )
}
2022-05-28 20:03:24 +00:00
func ( portal * Portal ) Delete ( ) {
2022-05-22 19:16:42 +00:00
portal . Portal . Delete ( )
portal . bridge . portalsLock . Lock ( )
delete ( portal . bridge . portalsByID , portal . Key )
if portal . MXID != "" {
delete ( portal . bridge . portalsByMXID , portal . MXID )
}
portal . bridge . portalsLock . Unlock ( )
}
func ( portal * Portal ) cleanupIfEmpty ( ) {
2022-05-28 20:03:24 +00:00
if portal . MXID == "" {
return
}
2022-05-22 19:16:42 +00:00
users , err := portal . getMatrixUsers ( )
if err != nil {
portal . log . Errorfln ( "Failed to get Matrix user list to determine if portal needs to be cleaned up: %v" , err )
return
}
if len ( users ) == 0 {
portal . log . Infoln ( "Room seems to be empty, cleaning up..." )
2022-05-28 20:03:24 +00:00
portal . Delete ( )
2022-05-22 19:16:42 +00:00
portal . cleanup ( false )
}
}
func ( portal * Portal ) cleanup ( puppetsOnly bool ) {
2022-05-28 20:03:24 +00:00
if portal . MXID == "" {
2022-05-22 19:16:42 +00:00
return
}
if portal . IsPrivateChat ( ) {
_ , err := portal . MainIntent ( ) . LeaveRoom ( portal . MXID )
if err != nil {
portal . log . Warnln ( "Failed to leave private chat portal with main intent:" , err )
}
return
}
intent := portal . MainIntent ( )
members , err := intent . JoinedMembers ( portal . MXID )
if err != nil {
portal . log . Errorln ( "Failed to get portal members for cleanup:" , err )
return
}
for member := range members . Joined {
if member == intent . UserID {
continue
}
puppet := portal . bridge . GetPuppetByMXID ( member )
if portal != nil {
_ , err = puppet . DefaultIntent ( ) . LeaveRoom ( portal . MXID )
if err != nil {
portal . log . Errorln ( "Error leaving as puppet while cleaning up portal:" , err )
}
} else if ! puppetsOnly {
_ , err = intent . KickUser ( portal . MXID , & mautrix . ReqKickUser { UserID : member , Reason : "Deleting portal" } )
if err != nil {
portal . log . Errorln ( "Error kicking user while cleaning up portal:" , err )
}
}
}
_ , err = intent . LeaveRoom ( portal . MXID )
if err != nil {
portal . log . Errorln ( "Error leaving with main intent while cleaning up portal:" , err )
}
}
func ( portal * Portal ) getMatrixUsers ( ) ( [ ] id . UserID , error ) {
members , err := portal . MainIntent ( ) . JoinedMembers ( portal . MXID )
if err != nil {
return nil , fmt . Errorf ( "failed to get member list: %w" , err )
}
var users [ ] id . UserID
for userID := range members . Joined {
_ , isPuppet := portal . bridge . ParsePuppetMXID ( userID )
if ! isPuppet && userID != portal . bridge . Bot . UserID {
users = append ( users , userID )
}
}
return users , nil
}
2022-05-28 20:03:24 +00:00
func ( portal * Portal ) handleMatrixReaction ( sender * User , evt * event . Event ) {
if portal . IsPrivateChat ( ) && sender . DiscordID != portal . Key . Receiver {
2022-06-28 08:55:36 +00:00
go portal . sendMessageMetrics ( evt , errUserNotReceiver , "Ignoring" )
2022-05-22 19:16:42 +00:00
return
}
reaction := evt . Content . AsReaction ( )
if reaction . RelatesTo . Type != event . RelAnnotation {
2022-06-28 08:55:36 +00:00
go portal . sendMessageMetrics ( evt , fmt . Errorf ( "%w %s" , errUnknownRelationType , reaction . RelatesTo . Type ) , "Ignoring" )
2022-05-22 19:16:42 +00:00
return
}
2022-05-27 12:58:09 +00:00
msg := portal . bridge . DB . Message . GetByMXID ( portal . Key , reaction . RelatesTo . EventID )
2022-05-22 19:16:42 +00:00
if msg == nil {
2022-06-28 08:55:36 +00:00
go portal . sendMessageMetrics ( evt , errTargetNotFound , "Ignoring" )
return
2022-06-27 07:53:49 +00:00
}
firstMsg := msg
if msg . AttachmentID != "" {
firstMsg = portal . bridge . DB . Message . GetFirstByDiscordID ( portal . Key , msg . DiscordID )
// TODO should the emoji be rerouted to the first message if it's different?
2022-05-22 19:16:42 +00:00
}
// Figure out if this is a custom emoji or not.
emojiID := reaction . RelatesTo . Key
if strings . HasPrefix ( emojiID , "mxc://" ) {
uri , _ := id . ParseContentURI ( emojiID )
emoji := portal . bridge . DB . Emoji . GetByMatrixURL ( uri )
if emoji == nil {
2022-06-28 08:55:36 +00:00
go portal . sendMessageMetrics ( evt , fmt . Errorf ( "%w %s" , errUnknownEmoji , emojiID ) , "Ignoring" )
2022-05-22 19:16:42 +00:00
return
}
emojiID = emoji . APIName ( )
2022-05-27 12:59:16 +00:00
} else {
emojiID = variationselector . Remove ( emojiID )
2022-05-22 19:16:42 +00:00
}
2022-06-27 07:53:49 +00:00
existing := portal . bridge . DB . Reaction . GetByDiscordID ( portal . Key , msg . DiscordID , sender . DiscordID , emojiID )
if existing != nil {
portal . log . Debugfln ( "Dropping duplicate Matrix reaction %s (already sent as %s)" , evt . ID , existing . MXID )
2022-06-28 08:55:36 +00:00
go portal . sendMessageMetrics ( evt , nil , "" )
2022-06-27 07:53:49 +00:00
return
2022-05-28 20:03:24 +00:00
}
2022-06-27 07:53:49 +00:00
err := sender . Session . MessageReactionAdd ( msg . DiscordProtoChannelID ( ) , msg . DiscordID , emojiID )
2022-06-28 08:55:36 +00:00
go portal . sendMessageMetrics ( evt , err , "Error sending" )
if err == nil {
dbReaction := portal . bridge . DB . Reaction . New ( )
dbReaction . Channel = portal . Key
dbReaction . MessageID = msg . DiscordID
dbReaction . FirstAttachmentID = firstMsg . AttachmentID
dbReaction . Sender = sender . DiscordID
dbReaction . EmojiName = emojiID
dbReaction . ThreadID = msg . ThreadID
dbReaction . MXID = evt . ID
dbReaction . Insert ( )
2022-05-22 19:16:42 +00:00
}
}
2022-05-28 20:03:24 +00:00
func ( portal * Portal ) handleDiscordReaction ( user * User , reaction * discordgo . MessageReaction , add bool , thread * Thread ) {
2022-05-22 19:16:42 +00:00
intent := portal . bridge . GetPuppetByID ( reaction . UserID ) . IntentFor ( portal )
var discordID string
2022-05-27 12:58:09 +00:00
var matrixReaction string
2022-05-22 19:16:42 +00:00
if reaction . Emoji . ID != "" {
2022-07-02 11:48:42 +00:00
reactionMXC := portal . getEmojiMXCByDiscordID ( reaction . Emoji . ID , reaction . Emoji . Name , reaction . Emoji . Animated )
if reactionMXC . IsEmpty ( ) {
return
2022-05-22 19:16:42 +00:00
}
2022-07-02 11:48:42 +00:00
matrixReaction = reactionMXC . String ( )
discordID = reaction . Emoji . ID
2022-05-22 19:16:42 +00:00
} else {
discordID = reaction . Emoji . Name
2022-05-27 12:59:16 +00:00
matrixReaction = variationselector . Add ( reaction . Emoji . Name )
2022-05-22 19:16:42 +00:00
}
// Find the message that we're working with.
message := portal . bridge . DB . Message . GetByDiscordID ( portal . Key , reaction . MessageID )
if message == nil {
portal . log . Debugfln ( "failed to add reaction to message %s: message not found" , reaction . MessageID )
return
}
// Lookup an existing reaction
2022-06-27 07:53:49 +00:00
existing := portal . bridge . DB . Reaction . GetByDiscordID ( portal . Key , message [ 0 ] . DiscordID , reaction . UserID , discordID )
2022-05-22 19:16:42 +00:00
if ! add {
if existing == nil {
portal . log . Debugln ( "Failed to remove reaction for unknown message" , reaction . MessageID )
return
}
2022-05-30 12:58:03 +00:00
resp , err := intent . RedactEvent ( portal . MXID , existing . MXID )
2022-05-22 19:16:42 +00:00
if err != nil {
portal . log . Warnfln ( "Failed to remove reaction from %s: %v" , portal . MXID , err )
}
existing . Delete ( )
2022-05-30 12:58:03 +00:00
go portal . sendDeliveryReceipt ( resp . EventID )
2022-05-27 12:58:09 +00:00
return
} else if existing != nil {
2022-06-27 07:53:49 +00:00
portal . log . Debugfln ( "Ignoring duplicate reaction %s from %s to %s" , discordID , reaction . UserID , message [ 0 ] . DiscordID )
2022-05-22 19:16:42 +00:00
return
}
2022-06-30 19:00:12 +00:00
content := event . ReactionEventContent {
2022-05-22 19:16:42 +00:00
RelatesTo : event . RelatesTo {
2022-06-27 07:53:49 +00:00
EventID : message [ 0 ] . MXID ,
2022-05-22 19:16:42 +00:00
Type : event . RelAnnotation ,
2022-05-27 12:58:09 +00:00
Key : matrixReaction ,
2022-05-22 19:16:42 +00:00
} ,
2022-05-28 21:22:00 +00:00
}
2022-05-22 19:16:42 +00:00
2022-05-28 21:22:00 +00:00
resp , err := intent . SendMessageEvent ( portal . MXID , event . EventReaction , & content )
2022-05-22 19:16:42 +00:00
if err != nil {
portal . log . Errorfln ( "failed to send reaction from %s: %v" , reaction . MessageID , err )
return
}
if existing == nil {
dbReaction := portal . bridge . DB . Reaction . New ( )
dbReaction . Channel = portal . Key
2022-06-27 07:53:49 +00:00
dbReaction . MessageID = message [ 0 ] . DiscordID
dbReaction . FirstAttachmentID = message [ 0 ] . AttachmentID
2022-05-27 12:58:09 +00:00
dbReaction . Sender = reaction . UserID
dbReaction . EmojiName = discordID
dbReaction . MXID = resp . EventID
2022-05-28 20:03:24 +00:00
if thread != nil {
dbReaction . ThreadID = thread . ID
}
2022-05-22 19:16:42 +00:00
dbReaction . Insert ( )
2022-05-30 12:58:03 +00:00
portal . sendDeliveryReceipt ( dbReaction . MXID )
2022-05-22 19:16:42 +00:00
}
}
2022-05-29 15:48:11 +00:00
func ( portal * Portal ) handleMatrixRedaction ( sender * User , evt * event . Event ) {
if portal . IsPrivateChat ( ) && sender . DiscordID != portal . Key . Receiver {
2022-06-28 08:55:36 +00:00
go portal . sendMessageMetrics ( evt , errUserNotReceiver , "Ignoring" )
2022-05-22 19:16:42 +00:00
return
}
// First look if we're redacting a message
2022-05-27 12:58:09 +00:00
message := portal . bridge . DB . Message . GetByMXID ( portal . Key , evt . Redacts )
2022-05-22 19:16:42 +00:00
if message != nil {
2022-06-28 08:55:36 +00:00
// TODO add support for deleting individual attachments from messages
2022-05-29 15:48:11 +00:00
err := sender . Session . ChannelMessageDelete ( message . DiscordProtoChannelID ( ) , message . DiscordID )
2022-06-28 08:55:36 +00:00
go portal . sendMessageMetrics ( evt , err , "Error sending" )
if err == nil {
2022-05-28 20:03:24 +00:00
message . Delete ( )
2022-05-22 19:16:42 +00:00
}
return
}
// Now check if it's a reaction.
2022-05-27 12:58:09 +00:00
reaction := portal . bridge . DB . Reaction . GetByMXID ( evt . Redacts )
if reaction != nil && reaction . Channel == portal . Key {
2022-05-29 15:48:11 +00:00
err := sender . Session . MessageReactionRemove ( reaction . DiscordProtoChannelID ( ) , reaction . MessageID , reaction . EmojiName , reaction . Sender )
2022-06-28 08:55:36 +00:00
go portal . sendMessageMetrics ( evt , err , "Error sending" )
if err == nil {
2022-05-27 12:58:09 +00:00
reaction . Delete ( )
2022-05-22 19:16:42 +00:00
}
return
}
2022-06-28 08:55:36 +00:00
go portal . sendMessageMetrics ( evt , errTargetNotFound , "Ignoring" )
2022-05-22 19:16:42 +00:00
}
2022-05-28 20:03:24 +00:00
func ( portal * Portal ) UpdateName ( name string ) bool {
if portal . Name == name && portal . NameSet {
return false
} else if ! portal . Encrypted && portal . IsPrivateChat ( ) {
// TODO custom config option for always setting private chat portal meta?
return false
}
portal . Name = name
portal . NameSet = false
if portal . MXID != "" {
_ , err := portal . MainIntent ( ) . SetRoomName ( portal . MXID , portal . Name )
if err != nil {
portal . log . Warnln ( "Failed to update room name:" , err )
} else {
portal . NameSet = true
}
2022-05-22 19:16:42 +00:00
}
2022-05-28 20:03:24 +00:00
return true
}
2022-05-22 19:16:42 +00:00
2022-05-28 20:03:24 +00:00
func ( portal * Portal ) UpdateAvatarFromPuppet ( puppet * Puppet ) bool {
if portal . Avatar == puppet . Avatar && portal . AvatarSet {
return false
}
portal . Avatar = puppet . Avatar
portal . AvatarURL = puppet . AvatarURL
portal . AvatarSet = false
portal . updateRoomAvatar ( )
return true
}
2022-05-22 19:16:42 +00:00
2022-05-28 20:03:24 +00:00
func ( portal * Portal ) UpdateGroupDMAvatar ( iconID string ) bool {
if portal . Avatar == iconID && portal . AvatarSet {
return false
}
portal . Avatar = iconID
portal . AvatarSet = false
2022-05-30 21:34:21 +00:00
portal . AvatarURL = id . ContentURI { }
2022-05-28 20:03:24 +00:00
if portal . Avatar != "" {
uri , err := uploadAvatar ( portal . MainIntent ( ) , discordgo . EndpointGroupIcon ( portal . Key . ChannelID , portal . Avatar ) )
2022-05-22 19:16:42 +00:00
if err != nil {
2022-05-30 21:34:21 +00:00
portal . log . Warnfln ( "Failed to reupload channel avatar %s: %v" , portal . Avatar , err )
2022-05-28 20:03:24 +00:00
return true
} else {
portal . AvatarURL = uri
2022-05-22 19:16:42 +00:00
}
2022-05-28 20:03:24 +00:00
}
portal . updateRoomAvatar ( )
return true
}
func ( portal * Portal ) updateRoomAvatar ( ) {
if portal . MXID == "" {
return
2022-05-22 19:16:42 +00:00
}
2022-05-28 20:03:24 +00:00
_ , err := portal . MainIntent ( ) . SetRoomAvatar ( portal . MXID , portal . AvatarURL )
if err != nil {
portal . log . Warnln ( "Failed to update room avatar:" , err )
} else {
portal . AvatarSet = true
}
}
2022-05-22 19:16:42 +00:00
2022-05-28 20:03:24 +00:00
func ( portal * Portal ) UpdateTopic ( topic string ) bool {
if portal . Topic == topic && portal . TopicSet {
return false
}
portal . Topic = topic
portal . TopicSet = false
if portal . MXID != "" {
_ , err := portal . MainIntent ( ) . SetRoomTopic ( portal . MXID , portal . Topic )
2022-05-22 19:16:42 +00:00
if err != nil {
portal . log . Warnln ( "Failed to update room topic:" , err )
2022-06-27 08:32:31 +00:00
} else {
portal . TopicSet = true
2022-05-22 19:16:42 +00:00
}
}
2022-05-28 20:03:24 +00:00
return true
}
2022-05-22 19:16:42 +00:00
2022-05-28 20:03:24 +00:00
func ( portal * Portal ) removeFromSpace ( ) {
if portal . InSpace == "" {
return
}
_ , err := portal . MainIntent ( ) . SendStateEvent ( portal . MXID , event . StateSpaceParent , portal . InSpace . String ( ) , struct { } { } )
if err != nil {
portal . log . Warnfln ( "Failed to unset canonical space %s: %v" , portal . InSpace , err )
}
_ , err = portal . bridge . Bot . SendStateEvent ( portal . InSpace , event . StateSpaceChild , portal . MXID . String ( ) , struct { } { } )
if err != nil {
portal . log . Warnfln ( "Failed to add room to space %s: %v" , portal . InSpace , err )
}
portal . InSpace = ""
}
2022-05-22 19:16:42 +00:00
2022-05-28 20:03:24 +00:00
func ( portal * Portal ) addToSpace ( mxid id . RoomID ) bool {
if portal . InSpace == mxid {
return false
}
portal . removeFromSpace ( )
2022-05-29 08:32:19 +00:00
if mxid == "" {
return true
}
2022-05-22 19:16:42 +00:00
2022-05-28 20:03:24 +00:00
_ , err := portal . MainIntent ( ) . SendStateEvent ( portal . MXID , event . StateSpaceParent , mxid . String ( ) , & event . SpaceParentEventContent {
Via : [ ] string { portal . bridge . AS . HomeserverDomain } ,
Canonical : true ,
} )
if err != nil {
portal . log . Warnfln ( "Failed to set canonical space %s: %v" , mxid , err )
}
2022-05-22 19:16:42 +00:00
2022-05-28 20:03:24 +00:00
_ , err = portal . bridge . Bot . SendStateEvent ( mxid , event . StateSpaceChild , portal . MXID . String ( ) , & event . SpaceChildEventContent {
Via : [ ] string { portal . bridge . AS . HomeserverDomain } ,
// TODO order
} )
if err != nil {
portal . log . Warnfln ( "Failed to add room to space %s: %v" , mxid , err )
} else {
portal . InSpace = mxid
}
return true
}
func ( portal * Portal ) UpdateParent ( parentID string ) bool {
if portal . ParentID == parentID {
return false
}
portal . ParentID = parentID
if portal . ParentID != "" {
2022-05-29 08:32:19 +00:00
portal . Parent = portal . bridge . GetPortalByID ( database . NewPortalKey ( parentID , "" ) , discordgo . ChannelTypeGuildCategory )
2022-05-28 20:03:24 +00:00
} else {
portal . Parent = nil
}
return true
}
func ( portal * Portal ) ExpectedSpaceID ( ) id . RoomID {
if portal . Parent != nil {
return portal . Parent . MXID
} else if portal . Guild != nil {
return portal . Guild . MXID
}
return ""
}
func ( portal * Portal ) updateSpace ( ) bool {
if portal . MXID == "" {
return false
}
if portal . Parent != nil {
return portal . addToSpace ( portal . Parent . MXID )
} else if portal . Guild != nil {
return portal . addToSpace ( portal . Guild . MXID )
}
return false
}
func ( portal * Portal ) UpdateInfo ( source * User , meta * discordgo . Channel ) * discordgo . Channel {
changed := false
2022-05-30 17:46:21 +00:00
if meta == nil {
portal . log . Debugfln ( "UpdateInfo called without metadata, fetching from %s's state cache" , source . DiscordID )
meta , _ = source . Session . State . Channel ( portal . Key . ChannelID )
if meta == nil {
portal . log . Warnfln ( "No metadata found in state cache, fetching from server via %s" , source . DiscordID )
var err error
meta , err = source . Session . Channel ( portal . Key . ChannelID )
if err != nil {
portal . log . Errorfln ( "Failed to fetch meta via %s: %v" , source . DiscordID , err )
return nil
}
}
}
2022-05-28 20:03:24 +00:00
if portal . Type != meta . Type {
portal . log . Warnfln ( "Portal type changed from %d to %d" , portal . Type , meta . Type )
portal . Type = meta . Type
changed = true
}
if portal . OtherUserID == "" && portal . IsPrivateChat ( ) {
if len ( meta . Recipients ) == 0 {
var err error
meta , err = source . Session . Channel ( meta . ID )
2022-05-22 19:16:42 +00:00
if err != nil {
2022-05-28 20:03:24 +00:00
portal . log . Errorfln ( "Failed to get DM channel info:" , err )
2022-05-22 19:16:42 +00:00
}
}
2022-05-28 20:03:24 +00:00
portal . OtherUserID = meta . Recipients [ 0 ] . ID
portal . log . Infoln ( "Found other user ID:" , portal . OtherUserID )
changed = true
}
if meta . GuildID != "" && portal . GuildID == "" {
portal . GuildID = meta . GuildID
portal . Guild = portal . bridge . GetGuildByID ( portal . GuildID , true )
changed = true
}
// FIXME
//name, err := portal.bridge.Config.Bridge.FormatChannelname(meta, source.Session)
//if err != nil {
// portal.log.Errorln("Failed to format channel name:", err)
// return
//}
switch portal . Type {
case discordgo . ChannelTypeDM :
if portal . OtherUserID != "" {
puppet := portal . bridge . GetPuppetByID ( portal . OtherUserID )
changed = portal . UpdateAvatarFromPuppet ( puppet ) || changed
changed = portal . UpdateName ( puppet . Name ) || changed
}
case discordgo . ChannelTypeGroupDM :
changed = portal . UpdateGroupDMAvatar ( meta . Icon ) || changed
fallthrough
default :
changed = portal . UpdateName ( meta . Name ) || changed
2022-05-22 19:16:42 +00:00
}
2022-05-28 20:03:24 +00:00
changed = portal . UpdateTopic ( meta . Topic ) || changed
changed = portal . UpdateParent ( meta . ParentID ) || changed
if portal . MXID != "" && portal . ExpectedSpaceID ( ) != portal . InSpace {
changed = portal . updateSpace ( ) || changed
}
if changed {
portal . UpdateBridgeInfo ( )
portal . Update ( )
}
return meta
2022-05-22 19:16:42 +00:00
}