2022-05-22 19:16:42 +00:00
package main
import (
2022-05-30 12:58:03 +00:00
"errors"
2022-05-22 19:16:42 +00:00
"fmt"
2023-01-28 01:16:33 +00:00
"html"
2023-01-13 17:35:17 +00:00
"reflect"
2022-05-28 20:03:24 +00:00
"strconv"
2022-05-22 19:16:42 +00:00
"strings"
"sync"
"time"
2022-05-28 20:03:24 +00:00
"github.com/bwmarrin/discordgo"
2023-01-28 13:08:38 +00:00
"github.com/gabriel-vasile/mimetype"
2022-05-28 20:03:24 +00:00
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"
2023-01-28 13:08:38 +00:00
"maunium.net/go/mautrix/bridge/status"
"maunium.net/go/mautrix/crypto/attachment"
2022-05-22 19:16:42 +00:00
"maunium.net/go/mautrix/event"
2023-01-28 13:08:38 +00:00
"maunium.net/go/mautrix/format"
2022-05-22 19:16:42 +00:00
"maunium.net/go/mautrix/id"
2023-01-28 13:08:38 +00:00
"maunium.net/go/mautrix/util"
"maunium.net/go/mautrix/util/variationselector"
2022-05-22 19:16:42 +00:00
2022-07-08 17:48:36 +00:00
"go.mau.fi/mautrix-discord/config"
2022-05-22 19:16:42 +00:00
"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
2022-07-08 14:06:02 +00:00
2023-01-28 12:47:41 +00:00
recentMessages * util . RingBuffer [ string , * discordgo . Message ]
2023-01-29 11:45:00 +00:00
commands map [ string ] * discordgo . ApplicationCommand
commandsLock sync . RWMutex
2022-07-08 14:06:02 +00:00
currentlyTyping [ ] id . UserID
currentlyTypingLock sync . Mutex
2022-05-22 19:16:42 +00:00
}
2023-01-28 12:47:41 +00:00
const recentMessageBufferSize = 32
2022-10-28 20:35:31 +00:00
var _ bridge . Portal = ( * Portal ) ( nil )
var _ bridge . ReadReceiptHandlingPortal = ( * Portal ) ( nil )
var _ bridge . MembershipHandlingPortal = ( * Portal ) ( nil )
var _ bridge . TypingPortal = ( * Portal ) ( nil )
//var _ bridge.MetaHandlingPortal = (*Portal)(nil)
//var _ bridge.DisappearingPortal = (*Portal)(nil)
2022-05-22 19:16:42 +00:00
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 (
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 ( ) )
}
2023-01-13 15:01:23 +00:00
func ( br * DiscordBridge ) GetAllPortalsInGuild ( guildID string ) [ ] * Portal {
return br . dbPortalsToPortals ( br . DB . Portal . GetAllInGuild ( guildID ) )
}
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 ) ,
2023-01-28 12:47:41 +00:00
recentMessages : util . NewRingBuffer [ string , * discordgo . Message ] ( recentMessageBufferSize ) ,
2023-01-29 11:45:00 +00:00
commands : make ( map [ string ] * discordgo . ApplicationCommand ) ,
2022-05-22 19:16:42 +00:00
}
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
}
2023-01-13 17:35:17 +00:00
type CustomBridgeInfoContent struct {
event . BridgeEventContent
RoomType string ` json:"com.beeper.room_type,omitempty" `
}
func init ( ) {
event . TypeMap [ event . StateBridge ] = reflect . TypeOf ( CustomBridgeInfoContent { } )
event . TypeMap [ event . StateHalfShotBridge ] = reflect . TypeOf ( CustomBridgeInfoContent { } )
}
func ( portal * Portal ) getBridgeInfo ( ) ( string , CustomBridgeInfoContent ) {
2022-05-27 13:19:54 +00:00
bridgeInfo := event . BridgeEventContent {
BridgeBot : portal . bridge . Bot . UserID ,
Creator : portal . MainIntent ( ) . UserID ,
Protocol : event . BridgeInfoSection {
2022-07-20 14:32:51 +00:00
ID : "discordgo" ,
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
}
2023-01-13 17:35:17 +00:00
var roomType string
if portal . Type == discordgo . ChannelTypeDM || portal . Type == discordgo . ChannelTypeGroupDM {
roomType = "dm"
}
return bridgeInfoStateKey , CustomBridgeInfoContent { bridgeInfo , roomType }
2022-05-27 13:19:54 +00:00
}
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-07-09 14:10:47 +00:00
if portal . Encrypted && portal . IsPrivateChat ( ) {
err = portal . bridge . Bot . EnsureJoined ( portal . MXID , appservice . EnsureJoinedParams { BotOverride : portal . MainIntent ( ) . Client } )
if err != nil {
portal . log . Errorfln ( "Failed to ensure bridge bot is joined to private chat portal: %v" , err )
}
}
2022-08-19 20:10:03 +00:00
if portal . GuildID == "" {
user . addPrivateChannelToSpace ( portal )
} else {
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
}
2023-01-28 11:54:32 +00:00
func ( portal * Portal ) createMediaFailedMessage ( bridgeErr error ) * event . MessageEventContent {
return & event . MessageEventContent {
2022-05-22 19:16:42 +00:00
Body : fmt . Sprintf ( "Failed to bridge media: %v" , bridgeErr ) ,
MsgType : event . MsgNotice ,
}
2023-01-28 11:54:32 +00:00
}
2022-05-22 19:16:42 +00:00
2023-01-28 11:54:32 +00:00
func ( portal * Portal ) sendMediaFailedMessage ( intent * appservice . IntentAPI , bridgeErr error ) id . EventID {
resp , err := portal . sendMatrixMessage ( intent , event . EventMessage , portal . createMediaFailedMessage ( bridgeErr ) , 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 )
2023-01-27 23:57:44 +00:00
return ""
2022-05-22 19:16:42 +00:00
}
2023-01-27 23:57:44 +00:00
return resp . EventID
2022-05-22 19:16:42 +00:00
}
2022-07-03 09:58:57 +00:00
const DiscordStickerSize = 160
func ( portal * Portal ) handleDiscordFile ( typeName string , intent * appservice . IntentAPI , id , url string , content * event . MessageEventContent , ts time . Time , threadRelation * event . RelatesTo ) * database . MessagePart {
2023-01-27 23:57:44 +00:00
dbFile , err := portal . bridge . copyAttachmentToMatrix ( intent , url , portal . Encrypted , id , content . Info . MimeType )
2022-07-03 09:58:57 +00:00
if err != nil {
2023-01-27 23:57:44 +00:00
errorEventID := portal . sendMediaFailedMessage ( intent , err )
if errorEventID != "" {
return & database . MessagePart {
AttachmentID : id ,
MXID : errorEventID ,
}
}
2022-07-03 09:58:57 +00:00
return nil
}
2023-01-27 23:57:44 +00:00
content . Info . Size = dbFile . Size
if content . Info . Width == 0 && content . Info . Height == 0 {
content . Info . Width = dbFile . Width
content . Info . Height = dbFile . Height
}
if dbFile . DecryptionInfo != nil {
content . File = & event . EncryptedFileInfo {
EncryptedFile : * dbFile . DecryptionInfo ,
URL : dbFile . MXC . CUString ( ) ,
}
} else {
content . URL = dbFile . MXC . CUString ( )
2022-07-03 09:58:57 +00:00
}
evtType := event . EventMessage
if typeName == "sticker" && ( content . Info . Width > DiscordStickerSize || content . Info . Height > DiscordStickerSize ) {
if content . Info . Width > content . Info . Height {
content . Info . Height /= content . Info . Width / DiscordStickerSize
content . Info . Width = DiscordStickerSize
} else if content . Info . Width < content . Info . Height {
content . Info . Width /= content . Info . Height / DiscordStickerSize
content . Info . Height = DiscordStickerSize
} else {
content . Info . Width = DiscordStickerSize
content . Info . Height = DiscordStickerSize
}
evtType = event . EventSticker
2022-11-30 13:10:56 +00:00
} else if typeName == "sticker" {
evtType = event . EventSticker
2022-07-03 09:58:57 +00:00
}
resp , err := portal . sendMatrixMessage ( intent , evtType , content , nil , ts . UnixMilli ( ) )
if err != nil {
portal . log . Warnfln ( "Failed to send %s to Matrix: %v" , typeName , err )
return nil
}
// Update the fallback reply event for the next attachment
if threadRelation != nil {
threadRelation . InReplyTo . EventID = resp . EventID
}
return & database . MessagePart {
AttachmentID : id ,
MXID : resp . EventID ,
}
}
func ( portal * Portal ) handleDiscordSticker ( intent * appservice . IntentAPI , sticker * discordgo . Sticker , ts time . Time , threadRelation * event . RelatesTo ) * database . MessagePart {
var mime string
switch sticker . FormatType {
case discordgo . StickerFormatTypePNG :
mime = "image/png"
case discordgo . StickerFormatTypeAPNG :
mime = "image/apng"
case discordgo . StickerFormatTypeLottie :
2022-07-08 14:06:02 +00:00
mime = "application/json"
2022-07-03 09:58:57 +00:00
}
content := & event . MessageEventContent {
Body : sticker . Name , // TODO find description from somewhere?
Info : & event . FileInfo {
MimeType : mime ,
} ,
RelatesTo : threadRelation ,
}
return portal . handleDiscordFile ( "sticker" , intent , sticker . ID , sticker . URL ( ) , content , ts , threadRelation )
}
func ( portal * Portal ) handleDiscordAttachment ( intent * appservice . IntentAPI , att * discordgo . MessageAttachment , ts time . Time , threadRelation * event . RelatesTo ) * database . MessagePart {
2022-05-22 19:16:42 +00:00
// var captionContent *event.MessageEventContent
2022-07-03 09:58:57 +00:00
// if att.Description != "" {
2022-05-22 19:16:42 +00:00
// captionContent = &event.MessageEventContent{
2022-07-03 09:58:57 +00:00
// Body: att.Description,
2022-05-22 19:16:42 +00:00
// MsgType: event.MsgNotice,
// }
// }
// portal.Log.Debugfln("captionContent: %#v", captionContent)
content := & event . MessageEventContent {
2022-07-03 09:58:57 +00:00
Body : att . Filename ,
2022-05-22 19:16:42 +00:00
Info : & event . FileInfo {
2022-07-03 09:58:57 +00:00
Height : att . Height ,
MimeType : att . ContentType ,
Width : att . Width ,
2022-05-22 19:16:42 +00:00
// This gets overwritten later after the file is uploaded to the homeserver
2022-07-03 09:58:57 +00:00
Size : att . Size ,
2022-05-22 19:16:42 +00:00
} ,
2022-05-28 20:03:24 +00:00
RelatesTo : threadRelation ,
2022-05-22 19:16:42 +00:00
}
2022-07-03 09:58:57 +00:00
switch strings . ToLower ( strings . Split ( att . ContentType , "/" ) [ 0 ] ) {
2022-05-22 19:16:42 +00:00
case "audio" :
content . MsgType = event . MsgAudio
case "image" :
content . MsgType = event . MsgImage
case "video" :
content . MsgType = event . MsgVideo
default :
content . MsgType = event . MsgFile
}
2022-07-03 09:58:57 +00:00
return portal . handleDiscordFile ( "attachment" , intent , att . ID , att . URL , content , ts , threadRelation )
2022-05-22 19:16:42 +00:00
}
2023-01-28 11:54:32 +00:00
type ConvertedMessage struct {
Content * event . MessageEventContent
Extra map [ string ] any
}
2023-01-29 00:13:02 +00:00
func ( portal * Portal ) convertDiscordVideoEmbed ( intent * appservice . IntentAPI , embed * discordgo . MessageEmbed ) * ConvertedMessage {
2023-01-28 12:56:48 +00:00
dbFile , err := portal . bridge . copyAttachmentToMatrix ( intent , embed . Video . ProxyURL , portal . Encrypted , "" , "" )
2023-01-28 11:54:32 +00:00
if err != nil {
2023-01-29 00:13:02 +00:00
return & ConvertedMessage { Content : portal . createMediaFailedMessage ( err ) }
2023-01-28 11:54:32 +00:00
}
content := & event . MessageEventContent {
MsgType : event . MsgVideo ,
Body : embed . URL ,
Info : & event . FileInfo {
Width : embed . Video . Width ,
Height : embed . Video . Height ,
MimeType : dbFile . MimeType ,
Size : dbFile . Size ,
} ,
}
if content . Info . Width == 0 && content . Info . Height == 0 {
content . Info . Width = dbFile . Width
content . Info . Height = dbFile . Height
}
if dbFile . DecryptionInfo != nil {
content . File = & event . EncryptedFileInfo {
EncryptedFile : * dbFile . DecryptionInfo ,
URL : dbFile . MXC . CUString ( ) ,
}
} else {
content . URL = dbFile . MXC . CUString ( )
}
extra := map [ string ] any { }
if embed . Type == discordgo . EmbedTypeGifv {
extra [ "info" ] = map [ string ] any {
"fi.mau.discord.gifv" : true ,
"fi.mau.loop" : true ,
"fi.mau.autoplay" : true ,
"fi.mau.hide_controls" : true ,
"fi.mau.no_audio" : true ,
}
}
2023-01-29 00:13:02 +00:00
return & ConvertedMessage { Content : content , Extra : extra }
2023-01-28 11:54:32 +00:00
}
func ( portal * Portal ) handleDiscordVideoEmbed ( intent * appservice . IntentAPI , embed * discordgo . MessageEmbed , msgID string , index int , ts time . Time , threadRelation * event . RelatesTo ) * database . MessagePart {
content := portal . convertDiscordVideoEmbed ( intent , embed )
content . Content . RelatesTo = threadRelation
resp , err := portal . sendMatrixMessage ( intent , event . EventMessage , content . Content , content . Extra , ts . UnixMilli ( ) )
if err != nil {
portal . log . Warnfln ( "Failed to send embed #%d of message %s to Matrix: %v" , index + 1 , msgID , err )
return nil
}
// Update the fallback reply event for the next attachment
if threadRelation != nil {
threadRelation . InReplyTo . EventID = resp . EventID
}
return & database . MessagePart {
2023-01-29 00:13:02 +00:00
AttachmentID : fmt . Sprintf ( "video_%s" , embed . URL ) ,
2023-01-28 11:54:32 +00:00
MXID : resp . EventID ,
}
}
2023-01-28 01:16:33 +00:00
const (
embedHTMLWrapper = ` <blockquote class="discord-embed">%s</blockquote> `
embedHTMLWrapperColor = ` <blockquote class="discord-embed" background-color="#%06X">%s</blockquote> `
2023-01-29 21:26:58 +00:00
embedHTMLAuthorWithImage = ` <p class="discord-embed-author"><img data-mx-emoticon height="24" src="%s" title="Author icon" alt=""> <span>%s</span></p> `
2023-01-28 01:16:33 +00:00
embedHTMLAuthorPlain = ` <p class="discord-embed-author"><span>%s</span></p> `
embedHTMLAuthorLink = ` <a href="%s">%s</a> `
embedHTMLTitleWithLink = ` <p class="discord-embed-title"><a href="%s"><strong>%s</strong></a></p> `
embedHTMLTitlePlain = ` <p class="discord-embed-title"><strong>%s</strong></p> `
embedHTMLDescription = ` <p class="discord-embed-description">%s</p> `
embedHTMLFieldName = ` <th>%s</th> `
embedHTMLFieldValue = ` <td>%s</td> `
embedHTMLFields = ` <table class="discord-embed-fields"><tr>%s</tr><tr>%s</tr></table> `
embedHTMLLinearField = ` <p class="discord-embed-field" x-inline="%s"><strong>%s</strong><br><span>%s</span></p> `
2023-01-29 13:23:10 +00:00
embedHTMLImage = ` <p class="discord-embed-image"><img src="%s" alt="" title="Embed image"></p> `
2023-01-29 21:26:58 +00:00
embedHTMLFooterWithImage = ` <p class="discord-embed-footer"><sub><img data-mx-emoticon height="20" src="%s" title="Footer icon" alt=""> <span>%s</span>%s</sub></p> `
2023-01-28 01:16:33 +00:00
embedHTMLFooterPlain = ` <p class="discord-embed-footer"><sub><span>%s</span>%s</sub></p> `
embedHTMLFooterOnlyDate = ` <p class="discord-embed-footer"><sub>%s</sub></p> `
embedHTMLDate = ` <time datetime="%s">%s</time> `
embedFooterDateSeparator = ` • `
)
2023-01-29 00:13:02 +00:00
func ( portal * Portal ) convertDiscordRichEmbed ( intent * appservice . IntentAPI , embed * discordgo . MessageEmbed , msgID string , index int ) string {
2023-01-28 01:16:33 +00:00
var htmlParts [ ] string
if embed . Author != nil {
var authorHTML string
authorNameHTML := html . EscapeString ( embed . Author . Name )
if embed . Author . URL != "" {
authorNameHTML = fmt . Sprintf ( embedHTMLAuthorLink , embed . Author . URL , authorNameHTML )
}
authorHTML = fmt . Sprintf ( embedHTMLAuthorPlain , authorNameHTML )
if embed . Author . ProxyIconURL != "" {
dbFile , err := portal . bridge . copyAttachmentToMatrix ( intent , embed . Author . ProxyIconURL , false , "" , "" )
2023-01-28 10:46:36 +00:00
if err != nil {
portal . log . Warnfln ( "Failed to reupload author icon in embed #%d of message %s: %v" , index + 1 , msgID , err )
} else {
2023-01-28 01:16:33 +00:00
authorHTML = fmt . Sprintf ( embedHTMLAuthorWithImage , dbFile . MXC , authorNameHTML )
}
}
htmlParts = append ( htmlParts , authorHTML )
}
if embed . Title != "" {
var titleHTML string
baseTitleHTML := portal . renderDiscordMarkdownOnlyHTML ( embed . Title )
if embed . URL != "" {
titleHTML = fmt . Sprintf ( embedHTMLTitleWithLink , html . EscapeString ( embed . URL ) , baseTitleHTML )
} else {
titleHTML = fmt . Sprintf ( embedHTMLTitlePlain , baseTitleHTML )
}
htmlParts = append ( htmlParts , titleHTML )
}
if embed . Description != "" {
htmlParts = append ( htmlParts , fmt . Sprintf ( embedHTMLDescription , portal . renderDiscordMarkdownOnlyHTML ( embed . Description ) ) )
}
for i := 0 ; i < len ( embed . Fields ) ; i ++ {
item := embed . Fields [ i ]
if portal . bridge . Config . Bridge . EmbedFieldsAsTables {
splitItems := [ ] * discordgo . MessageEmbedField { item }
if item . Inline && len ( embed . Fields ) > i + 1 && embed . Fields [ i + 1 ] . Inline {
splitItems = append ( splitItems , embed . Fields [ i + 1 ] )
i ++
if len ( embed . Fields ) > i + 1 && embed . Fields [ i + 1 ] . Inline {
splitItems = append ( splitItems , embed . Fields [ i + 1 ] )
i ++
}
}
headerParts := make ( [ ] string , len ( splitItems ) )
contentParts := make ( [ ] string , len ( splitItems ) )
for j , splitItem := range splitItems {
headerParts [ j ] = fmt . Sprintf ( embedHTMLFieldName , portal . renderDiscordMarkdownOnlyHTML ( splitItem . Name ) )
contentParts [ j ] = fmt . Sprintf ( embedHTMLFieldValue , portal . renderDiscordMarkdownOnlyHTML ( splitItem . Value ) )
}
htmlParts = append ( htmlParts , fmt . Sprintf ( embedHTMLFields , strings . Join ( headerParts , "" ) , strings . Join ( contentParts , "" ) ) )
} else {
htmlParts = append ( htmlParts , fmt . Sprintf ( embedHTMLLinearField ,
strconv . FormatBool ( item . Inline ) ,
portal . renderDiscordMarkdownOnlyHTML ( item . Name ) ,
portal . renderDiscordMarkdownOnlyHTML ( item . Value ) ,
) )
}
}
2023-01-28 10:56:30 +00:00
if embed . Image != nil {
dbFile , err := portal . bridge . copyAttachmentToMatrix ( intent , embed . Image . ProxyURL , false , "" , "" )
if err != nil {
portal . log . Warnfln ( "Failed to reupload image in embed #%d of message %s: %v" , index + 1 , msgID , err )
} else {
htmlParts = append ( htmlParts , fmt . Sprintf ( embedHTMLImage , dbFile . MXC ) )
}
}
2023-01-28 01:16:33 +00:00
var embedDateHTML string
if embed . Timestamp != "" {
formattedTime := embed . Timestamp
parsedTS , err := time . Parse ( time . RFC3339 , embed . Timestamp )
2023-01-28 10:46:36 +00:00
if err != nil {
portal . log . Warnfln ( "Failed to parse timestamp in embed #%d of message %s: %v" , index + 1 , msgID , err )
} else {
2023-01-28 01:16:33 +00:00
formattedTime = parsedTS . Format ( discordTimestampStyle ( 'F' ) . Format ( ) )
}
embedDateHTML = fmt . Sprintf ( embedHTMLDate , embed . Timestamp , formattedTime )
}
if embed . Footer != nil {
var footerHTML string
var datePart string
if embedDateHTML != "" {
datePart = embedFooterDateSeparator + embedDateHTML
}
footerHTML = fmt . Sprintf ( embedHTMLFooterPlain , html . EscapeString ( embed . Footer . Text ) , datePart )
if embed . Footer . ProxyIconURL != "" {
dbFile , err := portal . bridge . copyAttachmentToMatrix ( intent , embed . Footer . ProxyIconURL , false , "" , "" )
2023-01-28 10:46:36 +00:00
if err != nil {
portal . log . Warnfln ( "Failed to reupload footer icon in embed #%d of message %s: %v" , index + 1 , msgID , err )
} else {
2023-01-28 01:16:33 +00:00
footerHTML = fmt . Sprintf ( embedHTMLFooterWithImage , dbFile . MXC , html . EscapeString ( embed . Footer . Text ) , datePart )
}
}
htmlParts = append ( htmlParts , footerHTML )
} else if embed . Timestamp != "" {
htmlParts = append ( htmlParts , fmt . Sprintf ( embedHTMLFooterOnlyDate , embedDateHTML ) )
}
2023-01-28 01:19:14 +00:00
if len ( htmlParts ) == 0 {
2023-01-29 00:13:02 +00:00
return ""
2023-01-28 01:19:14 +00:00
}
2023-01-28 01:16:33 +00:00
compiledHTML := strings . Join ( htmlParts , "" )
if embed . Color != 0 {
compiledHTML = fmt . Sprintf ( embedHTMLWrapperColor , embed . Color , compiledHTML )
} else {
compiledHTML = fmt . Sprintf ( embedHTMLWrapper , compiledHTML )
}
2023-01-29 00:13:02 +00:00
return compiledHTML
}
2023-01-28 01:16:33 +00:00
2023-01-29 00:13:02 +00:00
type BeeperLinkPreview struct {
mautrix . RespPreviewURL
MatchedURL string ` json:"matched_url" `
ImageEncryption * event . EncryptedFileInfo ` json:"beeper:image:encryption,omitempty" `
}
func ( portal * Portal ) convertDiscordLinkEmbedImage ( intent * appservice . IntentAPI , url string , width , height int , preview * BeeperLinkPreview ) {
dbFile , err := portal . bridge . copyAttachmentToMatrix ( intent , url , portal . Encrypted , "" , "" )
2023-01-28 01:16:33 +00:00
if err != nil {
2023-01-29 00:13:02 +00:00
portal . log . Warnfln ( "Failed to copy image in URL preview: %v" , err )
} else {
if width != 0 || height != 0 {
preview . ImageWidth = width
preview . ImageHeight = height
} else {
preview . ImageWidth = dbFile . Width
preview . ImageHeight = dbFile . Height
}
preview . ImageSize = dbFile . Size
preview . ImageType = dbFile . MimeType
if dbFile . Encrypted {
preview . ImageEncryption = & event . EncryptedFileInfo {
EncryptedFile : * dbFile . DecryptionInfo ,
URL : dbFile . MXC . CUString ( ) ,
}
} else {
preview . ImageURL = dbFile . MXC . CUString ( )
}
}
}
func ( portal * Portal ) convertDiscordLinkEmbedToBeeper ( intent * appservice . IntentAPI , embed * discordgo . MessageEmbed ) * BeeperLinkPreview {
var preview BeeperLinkPreview
preview . MatchedURL = embed . URL
preview . Title = embed . Title
preview . Description = embed . Description
if embed . Image != nil {
portal . convertDiscordLinkEmbedImage ( intent , embed . Image . ProxyURL , embed . Image . Width , embed . Image . Height , & preview )
} else if embed . Thumbnail != nil {
portal . convertDiscordLinkEmbedImage ( intent , embed . Thumbnail . ProxyURL , embed . Thumbnail . Width , embed . Thumbnail . Height , & preview )
}
return & preview
}
const msgInteractionTemplateHTML = ` < blockquote >
< a href = "https://matrix.to/#/%s" > % s < / a > used < font color = "#3771bb" > / % s < / font >
< / blockquote > `
2023-01-29 13:36:53 +00:00
const msgComponentTemplateHTML = ` <p>This message contains interactive elements. Use the Discord app to interact with the message.</p> `
2023-01-29 00:13:02 +00:00
func ( portal * Portal ) convertDiscordTextMessage ( intent * appservice . IntentAPI , msg * discordgo . Message , relation * event . RelatesTo , isEdit bool ) * ConvertedMessage {
var htmlParts [ ] string
if msg . Interaction != nil {
puppet := portal . bridge . GetPuppetByID ( msg . Interaction . User . ID )
puppet . UpdateInfo ( nil , msg . Interaction . User )
htmlParts = append ( htmlParts , fmt . Sprintf ( msgInteractionTemplateHTML , puppet . MXID , puppet . Name , msg . Interaction . Name ) )
}
if msg . Content != "" && ! isPlainGifMessage ( msg ) {
htmlParts = append ( htmlParts , portal . renderDiscordMarkdownOnlyHTML ( msg . Content ) )
}
previews := make ( [ ] * BeeperLinkPreview , 0 )
for i , embed := range msg . Embeds {
switch embed . Type {
case discordgo . EmbedTypeRich , discordgo . EmbedTypeImage :
htmlParts = append ( htmlParts , portal . convertDiscordRichEmbed ( intent , embed , msg . ID , i ) )
case discordgo . EmbedTypeLink , discordgo . EmbedTypeArticle :
previews = append ( previews , portal . convertDiscordLinkEmbedToBeeper ( intent , embed ) )
case discordgo . EmbedTypeVideo , discordgo . EmbedTypeGifv :
// Ignore video embeds, they're handled as separate messages
default :
portal . log . Warnfln ( "Unknown type %s in embed #%d of message %s" , embed . Type , i + 1 , msg . ID )
}
}
2023-01-29 13:36:53 +00:00
if len ( msg . Components ) > 0 {
htmlParts = append ( htmlParts , msgComponentTemplateHTML )
}
2023-01-29 00:13:02 +00:00
if len ( htmlParts ) == 0 {
2023-01-28 01:16:33 +00:00
return nil
}
2023-01-29 13:59:47 +00:00
fullHTML := strings . Join ( htmlParts , "\n" )
if ! msg . MentionEveryone {
fullHTML = strings . ReplaceAll ( fullHTML , "@room" , "@\u2063ro\u2063om" )
}
content := format . HTMLToContent ( fullHTML )
2023-01-29 00:13:02 +00:00
if relation != nil {
content . RelatesTo = relation . Copy ( )
}
extraContent := map [ string ] any {
"com.beeper.linkpreviews" : previews ,
2023-01-28 01:16:33 +00:00
}
2023-01-29 00:13:02 +00:00
if msg . MessageReference != nil && ! isEdit {
//key := database.PortalKey{msg.MessageReference.ChannelID, user.ID}
replyTo := portal . bridge . DB . Message . GetByDiscordID ( portal . Key , msg . MessageReference . MessageID )
if len ( replyTo ) > 0 {
if content . RelatesTo == nil {
content . RelatesTo = & event . RelatesTo { }
}
content . RelatesTo . SetReplyTo ( replyTo [ 0 ] . MXID )
}
2023-01-28 01:16:33 +00:00
}
2023-01-29 00:13:02 +00:00
return & ConvertedMessage { Content : & content , Extra : extraContent }
2023-01-28 01:16:33 +00:00
}
2023-01-28 11:54:32 +00:00
func isPlainGifMessage ( msg * discordgo . Message ) bool {
2023-01-28 12:56:48 +00:00
return len ( msg . Embeds ) == 1 && msg . Embeds [ 0 ] . Video != nil && msg . Embeds [ 0 ] . URL == msg . Content && msg . Embeds [ 0 ] . Type == discordgo . EmbedTypeGifv
2023-01-28 11:54:32 +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
}
2023-01-27 22:55:33 +00:00
switch msg . Type {
case discordgo . MessageTypeChannelNameChange , discordgo . MessageTypeChannelIconChange , discordgo . MessageTypeChannelPinnedMessage :
// These are handled via channel updates
2022-05-22 19:16:42 +00:00
return
}
2023-01-28 12:47:41 +00:00
portal . recentMessages . Push ( msg . ID , msg )
2022-05-22 19:16:42 +00:00
// 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
2023-01-29 13:31:56 +00:00
for _ , mention := range msg . Mentions {
puppet := portal . bridge . GetPuppetByID ( mention . ID )
puppet . UpdateInfo ( nil , mention )
}
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 )
2023-01-29 00:13:02 +00:00
textPart := portal . convertDiscordTextMessage ( intent , msg , threadRelation , false )
if textPart != nil {
resp , err := portal . sendMatrixMessage ( intent , event . EventMessage , textPart . Content , textPart . Extra , ts . UnixMilli ( ) )
2022-05-22 19:16:42 +00:00
if err != nil {
2022-07-09 09:20:18 +00:00
portal . log . Warnfln ( "Failed to send message %s to matrix: %v" , msg . ID , err )
2022-05-22 19:16:42 +00:00
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
}
2022-07-03 09:58:57 +00:00
for _ , att := range msg . Attachments {
part := portal . handleDiscordAttachment ( intent , att , ts , threadRelation )
if part != nil {
parts = append ( parts , * part )
}
}
for _ , sticker := range msg . StickerItems {
part := portal . handleDiscordSticker ( intent , sticker , ts , threadRelation )
2022-06-27 07:53:49 +00:00
if part != nil {
parts = append ( parts , * part )
}
2022-05-22 19:16:42 +00:00
}
2023-01-29 00:13:02 +00:00
handledURLs := make ( map [ string ] struct { } )
2023-01-28 01:16:33 +00:00
for i , embed := range msg . Embeds {
2023-01-29 00:13:02 +00:00
// Ignore non-video embeds, they're handled in convertDiscordTextMessage
if embed . Type != discordgo . EmbedTypeVideo && embed . Type != discordgo . EmbedTypeGifv {
continue
}
// Discord deduplicates embeds by URL. It makes things easier for us too.
if _ , handled := handledURLs [ embed . URL ] ; handled {
continue
2023-01-28 11:54:32 +00:00
}
2023-01-29 00:13:02 +00:00
handledURLs [ embed . URL ] = struct { } { }
part := portal . handleDiscordVideoEmbed ( intent , embed , msg . ID , i , ts , threadRelation )
2023-01-28 01:16:33 +00:00
if part != nil {
parts = append ( parts , * part )
}
}
2022-07-02 20:18:49 +00:00
if len ( parts ) == 0 {
2023-01-27 22:55:33 +00:00
portal . log . Warnfln ( "Unhandled message %s (type %d)" , msg . ID , msg . Type )
2022-07-02 20:18:49 +00:00
} else {
portal . markMessageHandled ( msg . ID , 0 , msg . Author . ID , ts , threadID , parts )
}
2022-05-22 19:16:42 +00:00
}
2022-10-28 20:35:31 +00:00
const JoinThreadReaction = "join thread"
func ( portal * Portal ) sendThreadCreationNotice ( thread * Thread ) {
thread . creationNoticeLock . Lock ( )
defer thread . creationNoticeLock . Unlock ( )
if thread . CreationNoticeMXID != "" {
return
}
creationNotice := "Thread created. React to this message with \"join thread\" to join the thread on Discord."
if portal . bridge . Config . Bridge . AutojoinThreadOnOpen {
creationNotice = "Thread created. Opening this thread will auto-join you to it on Discord."
}
resp , err := portal . sendMatrixMessage ( portal . MainIntent ( ) , event . EventMessage , & event . MessageEventContent {
Body : creationNotice ,
MsgType : event . MsgNotice ,
RelatesTo : ( & event . RelatesTo { } ) . SetThread ( thread . RootMXID , thread . RootMXID ) ,
} , nil , time . Now ( ) . UnixMilli ( ) )
if err != nil {
portal . log . Errorfln ( "Failed to send thread creation notice: %v" , err )
return
}
portal . bridge . threadsLock . Lock ( )
thread . CreationNoticeMXID = resp . EventID
portal . bridge . threadsByCreationNoticeMXID [ resp . EventID ] = thread
portal . bridge . threadsLock . Unlock ( )
thread . Update ( )
portal . log . Debugfln ( "Sent notice %s about thread for %s being created" , thread . CreationNoticeMXID , thread . ID )
resp , err = portal . MainIntent ( ) . SendMessageEvent ( portal . MXID , event . EventReaction , & event . ReactionEventContent {
RelatesTo : event . RelatesTo {
Type : event . RelAnnotation ,
EventID : thread . CreationNoticeMXID ,
Key : JoinThreadReaction ,
} ,
} )
if err != nil {
portal . log . Errorfln ( "Failed to send prefilled reaction to thread creation notice: %v" , err )
} else {
portal . log . Debugfln ( "Sent prefilled reaction %s to thread creation notice %s" , resp . EventID , thread . CreationNoticeMXID )
}
}
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-10-28 20:35:31 +00:00
thread := 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-10-28 20:35:31 +00:00
if thread . CreationNoticeMXID == "" {
portal . sendThreadCreationNotice ( thread )
}
2022-05-28 20:03:24 +00:00
}
2022-05-22 19:16:42 +00:00
if msg . Author == nil {
2023-01-28 12:47:41 +00:00
creationMessage , ok := portal . recentMessages . Get ( msg . ID )
if ! ok {
portal . log . Debugfln ( "Dropping edit with no author of non-recent message %s" , msg . ID )
2022-05-22 19:16:42 +00:00
return
}
2023-01-28 12:47:41 +00:00
portal . log . Debugfln ( "Found original message %s in cache for edit without author" , msg . ID )
if len ( msg . Embeds ) > 0 {
creationMessage . Embeds = msg . Embeds
}
if len ( msg . Attachments ) > 0 {
creationMessage . Attachments = msg . Attachments
}
if len ( msg . Components ) > 0 {
creationMessage . Components = msg . Components
}
// TODO are there other fields that need copying?
msg = creationMessage
} else {
portal . recentMessages . Replace ( msg . ID , msg )
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-03 09:58:57 +00:00
for _ , remainingSticker := range msg . StickerItems {
if _ , found := attachmentMap [ remainingSticker . ID ] ; found {
delete ( attachmentMap , remainingSticker . ID )
}
}
2023-01-29 00:13:02 +00:00
for _ , remainingEmbed := range msg . Embeds {
// Other types of embeds are sent inline with the text message part
if remainingEmbed . Type != discordgo . EmbedTypeVideo && remainingEmbed . Type != discordgo . EmbedTypeGifv {
continue
}
embedID := "video_" + remainingEmbed . URL
if _ , found := attachmentMap [ embedID ] ; found {
delete ( attachmentMap , embedID )
}
}
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
}
2023-01-29 00:13:02 +00:00
var converted * ConvertedMessage
// Slightly hacky special case: messages with gif links will get an embed with the gif.
// The link isn't rendered on Discord, so just edit the link message into a gif message on Matrix too.
2023-01-28 12:47:41 +00:00
if isPlainGifMessage ( msg ) {
2023-01-29 00:13:02 +00:00
converted = portal . convertDiscordVideoEmbed ( intent , msg . Embeds [ 0 ] )
2023-01-28 12:47:41 +00:00
} else {
2023-01-29 00:13:02 +00:00
converted = portal . convertDiscordTextMessage ( intent , msg , nil , true )
}
if converted == nil {
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 )
return
2023-01-28 12:47:41 +00:00
}
2023-01-29 00:13:02 +00:00
converted . Content . SetEdit ( existing [ 0 ] . MXID )
2023-01-28 12:51:14 +00:00
extraContentCopy := map [ string ] any { }
2023-01-29 00:13:02 +00:00
for key , value := range converted . Extra {
2023-01-28 12:51:14 +00:00
extraContentCopy [ key ] = value
}
2023-01-29 00:13:02 +00:00
extraContentCopy [ "m.new_content" ] = converted . Extra
converted . Extra = extraContentCopy
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
2023-01-29 00:13:02 +00:00
resp , err := portal . sendMatrixMessage ( intent , event . EventMessage , converted . Content , converted . Extra , editTS )
2022-05-22 19:16:42 +00:00
if err != nil {
2022-07-09 09:20:18 +00:00
portal . log . Warnfln ( "Failed to send message %s to matrix: %v" , msg . ID , err )
2022-05-22 19:16:42 +00:00
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 {
2023-01-28 13:08:38 +00:00
case event . EventMessage , event . EventSticker :
2022-05-22 19:16:42 +00:00
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" )
)
2022-08-15 13:43:55 +00:00
func errorToStatusReason ( err error ) ( reason event . MessageStatusReason , status event . MessageStatus , isCertain , sendNotice bool , humanMessage string ) {
2022-06-28 08:55:36 +00:00
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 ) :
2022-08-15 13:43:55 +00:00
return event . MessageStatusUnsupported , event . MessageStatusFail , true , true , ""
2022-06-28 08:55:36 +00:00
case errors . Is ( err , attachment . HashMismatch ) ,
errors . Is ( err , attachment . InvalidKey ) ,
errors . Is ( err , attachment . InvalidInitVector ) :
2022-08-15 13:43:55 +00:00
return event . MessageStatusUndecryptable , event . MessageStatusFail , true , true , ""
2022-06-28 08:55:36 +00:00
case errors . Is ( err , errUserNotReceiver ) :
2022-08-15 13:43:55 +00:00
return event . MessageStatusNoPermission , event . MessageStatusFail , true , false , ""
2022-06-28 08:55:36 +00:00
case errors . Is ( err , errUnknownEditTarget ) :
2022-08-15 13:43:55 +00:00
return event . MessageStatusGenericError , event . MessageStatusFail , true , false , ""
2022-06-28 08:55:36 +00:00
case errors . Is ( err , errTargetNotFound ) :
2022-08-20 08:43:43 +00:00
return event . MessageStatusGenericError , event . MessageStatusFail , true , false , ""
2022-06-28 08:55:36 +00:00
default :
2022-08-15 13:43:55 +00:00
return event . MessageStatusGenericError , event . MessageStatusRetriable , false , true , ""
2022-06-28 08:55:36 +00:00
}
}
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 ,
} ,
2022-08-15 13:43:55 +00:00
Status : event . MessageStatusSuccess ,
2022-06-28 08:55:36 +00:00
}
2022-08-15 13:43:55 +00:00
if err == nil {
content . Status = event . MessageStatusSuccess
} else {
content . Reason , content . Status , _ , _ , content . Message = errorToStatusReason ( err )
2022-06-28 08:55:36 +00:00
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 {
2023-01-28 13:08:38 +00:00
case event . EventMessage , event . EventSticker :
2022-06-28 08:55:36 +00:00
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 )
2022-08-15 13:43:55 +00:00
reason , statusCode , isCertain , sendNotice , _ := errorToStatusReason ( err )
checkpointStatus := status . ReasonToCheckpointStatus ( reason , statusCode )
portal . bridge . SendMessageCheckpoint ( evt , status . MsgStepRemote , err , checkpointStatus , 0 )
2022-06-28 08:55:36 +00:00
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 )
2022-08-15 13:43:55 +00:00
portal . bridge . SendMessageSuccessCheckpoint ( evt , status . MsgStepRemote , 0 )
2022-06-28 08:55:36 +00:00
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
2023-01-28 13:43:16 +00:00
var description string
2023-01-28 13:08:38 +00:00
if evt . Type == event . EventSticker {
content . MsgType = event . MsgImage
if mimeData := mimetype . Lookup ( content . Info . MimeType ) ; mimeData != nil {
2023-01-28 13:43:16 +00:00
description = content . Body
2023-01-28 13:08:38 +00:00
content . Body = "sticker" + mimeData . Extension ( )
}
}
2022-05-22 19:16:42 +00:00
switch content . MsgType {
case event . MsgText , event . MsgEmote , event . MsgNotice :
2022-10-28 20:45:35 +00:00
if replyToMXID := content . RelatesTo . GetNonFallbackReplyTo ( ) ; replyToMXID != "" {
2022-05-28 20:03:24 +00:00
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
}
2023-01-28 13:43:16 +00:00
att := & discordgo . MessageAttachment {
ID : "0" ,
Filename : content . Body ,
Description : description ,
}
sendReq . Attachments = [ ] * discordgo . MessageAttachment { att }
2022-06-28 08:55:36 +00:00
if content . FileName != "" && content . FileName != content . Body {
2023-01-28 13:43:16 +00:00
att . Filename = content . FileName
2022-06-28 08:55:36 +00:00
sendReq . Content = portal . parseMatrixHTML ( sender , content )
}
2023-01-28 13:43:16 +00:00
prep , err := sender . Session . ChannelAttachmentCreate ( channelID , & discordgo . ReqPrepareAttachments {
Files : [ ] * discordgo . FilePrepare { {
Size : len ( data ) ,
Name : att . Filename ,
ID : sender . NextDiscordUploadID ( ) ,
} } ,
} )
if err != nil {
go portal . sendMessageMetrics ( evt , err , "Error preparing to reupload media in" )
return
}
prepared := prep . Attachments [ 0 ]
att . UploadedFilename = prepared . UploadFilename
err = uploadDiscordAttachment ( prepared . UploadURL , data )
if err != nil {
go portal . sendMessageMetrics ( evt , err , "Error reuploading media in" )
return
}
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 ) {
2022-07-09 13:51:43 +00:00
sender := brSender . ( * User )
if portal . IsPrivateChat ( ) && sender . DiscordID == portal . Key . Receiver {
portal . log . Debugln ( "User left private chat portal, cleaning up and deleting..." )
portal . cleanup ( false )
2022-07-09 14:10:47 +00:00
portal . RemoveMXID ( )
2022-07-09 13:51:43 +00:00
} else {
portal . cleanupIfEmpty ( )
}
2022-05-22 19:16:42 +00:00
}
2022-07-09 14:10:47 +00:00
func ( portal * Portal ) HandleMatrixKick ( brSender bridge . User , brTarget bridge . Ghost ) { }
func ( portal * Portal ) HandleMatrixInvite ( brSender bridge . User , brTarget bridge . Ghost ) { }
2022-05-22 19:16:42 +00:00
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..." )
portal . cleanup ( false )
2022-07-09 13:51:43 +00:00
portal . RemoveMXID ( )
}
}
func ( portal * Portal ) RemoveMXID ( ) {
portal . bridge . portalsLock . Lock ( )
defer portal . bridge . portalsLock . Unlock ( )
if portal . MXID == "" {
return
2022-05-22 19:16:42 +00:00
}
2022-07-09 13:51:43 +00:00
delete ( portal . bridge . portalsByMXID , portal . MXID )
portal . MXID = ""
2023-01-13 15:01:23 +00:00
portal . AvatarSet = false
portal . NameSet = false
portal . TopicSet = false
portal . Encrypted = false
portal . InSpace = ""
portal . FirstEventID = ""
2022-07-09 13:51:43 +00:00
portal . Update ( )
portal . bridge . DB . Message . DeleteAll ( portal . Key )
2022-05-22 19:16:42 +00:00
}
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
}
2023-01-11 15:31:40 +00:00
intent := portal . MainIntent ( )
if portal . bridge . SpecVersions . UnstableFeatures [ "com.beeper.room_yeeting" ] {
err := intent . BeeperDeleteRoom ( portal . MXID )
if err == nil || errors . Is ( err , mautrix . MNotFound ) {
return
}
portal . log . Warnfln ( "Failed to delete %s using hungryserv yeet endpoint, falling back to normal behavior: %v" , portal . MXID , err )
}
2022-05-22 19:16:42 +00:00
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
}
2023-01-13 15:01:23 +00:00
portal . bridge . cleanupRoom ( intent , portal . MXID , puppetsOnly , portal . log )
}
func ( br * DiscordBridge ) cleanupRoom ( intent * appservice . IntentAPI , mxid id . RoomID , puppetsOnly bool , log log . Logger ) {
members , err := intent . JoinedMembers ( mxid )
2022-05-22 19:16:42 +00:00
if err != nil {
2023-01-13 15:01:23 +00:00
log . Errorln ( "Failed to get portal members for cleanup:" , err )
2022-05-22 19:16:42 +00:00
return
}
for member := range members . Joined {
if member == intent . UserID {
continue
}
2023-01-13 15:01:23 +00:00
puppet := br . GetPuppetByMXID ( member )
2023-01-11 15:30:24 +00:00
if puppet != nil {
2023-01-13 15:01:23 +00:00
_ , err = puppet . DefaultIntent ( ) . LeaveRoom ( mxid )
2022-05-22 19:16:42 +00:00
if err != nil {
2023-01-13 15:01:23 +00:00
log . Errorln ( "Error leaving as puppet while cleaning up portal:" , err )
2022-05-22 19:16:42 +00:00
}
} else if ! puppetsOnly {
2023-01-13 15:01:23 +00:00
_ , err = intent . KickUser ( mxid , & mautrix . ReqKickUser { UserID : member , Reason : "Deleting portal" } )
2022-05-22 19:16:42 +00:00
if err != nil {
2023-01-13 15:01:23 +00:00
log . Errorln ( "Error kicking user while cleaning up portal:" , err )
2022-05-22 19:16:42 +00:00
}
}
}
2023-01-13 15:01:23 +00:00
_ , err = intent . LeaveRoom ( mxid )
2022-05-22 19:16:42 +00:00
if err != nil {
2023-01-13 15:01:23 +00:00
log . Errorln ( "Error leaving with main intent while cleaning up portal:" , err )
2022-05-22 19:16:42 +00:00
}
}
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-10-28 20:35:31 +00:00
if reaction . RelatesTo . Key == JoinThreadReaction {
thread := portal . bridge . GetThreadByRootOrCreationNoticeMXID ( reaction . RelatesTo . EventID )
if thread == nil {
go portal . sendMessageMetrics ( evt , errTargetNotFound , "Ignoring thread join" )
return
}
thread . Join ( sender )
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
}
2023-01-27 19:20:38 +00:00
extraContent := map [ string ] any { }
if reaction . Emoji . ID != "" {
extraContent [ "fi.mau.discord.reaction" ] = map [ string ] any {
"id" : reaction . Emoji . ID ,
"name" : reaction . Emoji . Name ,
2023-01-27 19:23:10 +00:00
"mxc" : matrixReaction ,
}
if ! portal . bridge . Config . Bridge . CustomEmojiReactions {
content . RelatesTo . Key = fmt . Sprintf ( ":%s:" , reaction . Emoji . Name )
2023-01-27 19:20:38 +00:00
}
}
2022-05-22 19:16:42 +00:00
2023-01-27 19:20:38 +00:00
resp , err := intent . SendMessageEvent ( portal . MXID , event . EventReaction , & event . Content {
Parsed : & content ,
Raw : extraContent ,
} )
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-10-28 20:35:31 +00:00
func ( portal * Portal ) HandleMatrixReadReceipt ( brUser bridge . User , eventID id . EventID , receipt event . ReadReceipt ) {
2022-07-08 08:54:09 +00:00
sender := brUser . ( * User )
if sender . Session == nil {
return
}
2022-10-28 20:35:31 +00:00
var thread * Thread
discordThreadID := ""
if receipt . ThreadID != "" && receipt . ThreadID != event . ReadReceiptThreadMain {
thread = portal . bridge . GetThreadByRootMXID ( receipt . ThreadID )
if thread != nil {
discordThreadID = thread . ID
}
}
if thread != nil {
if portal . bridge . Config . Bridge . AutojoinThreadOnOpen {
thread . Join ( sender )
}
if eventID == thread . CreationNoticeMXID {
portal . log . Debugfln ( "Dropping Matrix read receipt from %s for thread creation notice %s of %s" , sender . MXID , thread . CreationNoticeMXID , thread . ID )
return
}
}
2022-07-08 08:54:09 +00:00
msg := portal . bridge . DB . Message . GetByMXID ( portal . Key , eventID )
if msg == nil {
2022-10-28 20:35:31 +00:00
msg = portal . bridge . DB . Message . GetClosestBefore ( portal . Key , discordThreadID , receipt . Timestamp )
2022-07-08 08:54:09 +00:00
if msg == nil {
portal . log . Debugfln ( "Dropping Matrix read receipt from %s for %s: no messages found" , sender . MXID , eventID )
2022-07-08 17:49:30 +00:00
return
2022-07-08 08:54:09 +00:00
} else {
portal . log . Debugfln ( "Matrix read receipt target %s from %s not found, using closest message %s" , eventID , sender . MXID , msg . MXID )
}
}
2022-10-28 20:35:31 +00:00
if receipt . ThreadID != "" && msg . ThreadID != discordThreadID {
portal . log . Debugfln ( "Dropping Matrix read receipt from %s for %s in unexpected thread (receipt: %s, message: %s)" , receipt . ThreadID , msg . ThreadID )
return
}
2022-07-08 08:54:09 +00:00
resp , err := sender . Session . ChannelMessageAckNoToken ( msg . DiscordProtoChannelID ( ) , msg . DiscordID )
if err != nil {
2022-09-12 09:40:18 +00:00
portal . log . Warnfln ( "Failed to handle read receipt for %s/%s from %s: %v" , msg . MXID , msg . DiscordID , sender . MXID , err )
2022-07-08 08:54:09 +00:00
} else if resp . Token != nil {
portal . log . Debugfln ( "Marked %s/%s as read by %s (and got unexpected non-nil token %s)" , msg . MXID , msg . DiscordID , sender . MXID , * resp . Token )
} else {
2022-10-28 20:35:31 +00:00
portal . log . Debugfln ( "Marked %s/%s in %s as read by %s" , msg . MXID , msg . DiscordID , msg . DiscordProtoChannelID ( ) , sender . MXID )
2022-07-08 08:54:09 +00:00
}
}
2022-07-08 14:06:02 +00:00
func typingDiff ( prev , new [ ] id . UserID ) ( started [ ] id . UserID ) {
OuterNew :
for _ , userID := range new {
for _ , previousUserID := range prev {
if userID == previousUserID {
continue OuterNew
}
}
started = append ( started , userID )
}
return
}
func ( portal * Portal ) HandleMatrixTyping ( newTyping [ ] id . UserID ) {
portal . currentlyTypingLock . Lock ( )
defer portal . currentlyTypingLock . Unlock ( )
startedTyping := typingDiff ( portal . currentlyTyping , newTyping )
portal . currentlyTyping = newTyping
for _ , userID := range startedTyping {
user := portal . bridge . GetUserByMXID ( userID )
if user != nil && user . Session != nil {
user . ViewingChannel ( portal )
err := user . Session . ChannelTyping ( portal . Key . ChannelID )
if err != nil {
portal . log . Warnfln ( "Failed to mark %s as typing: %v" , user . MXID , err )
} else {
portal . log . Debugfln ( "Marked %s as typing" , user . MXID )
}
}
}
}
2022-07-08 17:48:36 +00:00
func ( portal * Portal ) UpdateName ( meta * discordgo . Channel ) bool {
var parentName , guildName string
if portal . Parent != nil {
parentName = portal . Parent . PlainName
}
if portal . Guild != nil {
guildName = portal . Guild . PlainName
}
plainNameChanged := portal . PlainName != meta . Name
portal . PlainName = meta . Name
return portal . UpdateNameDirect ( portal . bridge . Config . Bridge . FormatChannelName ( config . ChannelNameParams {
Name : meta . Name ,
ParentName : parentName ,
GuildName : guildName ,
NSFW : meta . NSFW ,
Type : meta . Type ,
} ) ) || plainNameChanged
}
func ( portal * Portal ) UpdateNameDirect ( name string ) bool {
2022-07-08 17:52:19 +00:00
if portal . Name == name && ( portal . NameSet || portal . MXID == "" ) {
2022-05-28 20:03:24 +00:00
return false
2022-07-08 17:48:36 +00:00
} else if ! portal . Encrypted && ! portal . bridge . Config . Bridge . PrivateChatPortalMeta && portal . IsPrivateChat ( ) {
2022-05-28 20:03:24 +00:00
return false
}
2022-07-08 17:55:31 +00:00
portal . log . Debugfln ( "Updating name %q -> %q" , portal . Name , name )
2022-05-28 20:03:24 +00:00
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 {
2022-07-08 17:52:19 +00:00
if portal . Avatar == puppet . Avatar && portal . AvatarURL == puppet . AvatarURL && ( portal . AvatarSet || portal . MXID == "" ) {
2022-05-28 20:03:24 +00:00
return false
}
2022-07-08 17:55:31 +00:00
portal . log . Debugfln ( "Updating avatar from puppet %q -> %q" , portal . Avatar , puppet . Avatar )
2022-05-28 20:03:24 +00:00
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 {
2022-07-08 17:52:19 +00:00
if portal . Avatar == iconID && ( iconID == "" ) == portal . AvatarURL . IsEmpty ( ) && ( portal . AvatarSet || portal . MXID == "" ) {
2022-05-28 20:03:24 +00:00
return false
}
2022-07-08 17:55:31 +00:00
portal . log . Debugfln ( "Updating group DM avatar %q -> %q" , portal . Avatar , iconID )
2022-05-28 20:03:24 +00:00
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 {
2022-07-08 17:52:19 +00:00
if portal . Topic == topic && ( portal . TopicSet || portal . MXID == "" ) {
2022-05-28 20:03:24 +00:00
return false
}
2022-07-08 17:55:31 +00:00
portal . log . Debugfln ( "Updating topic %q -> %q" , portal . Topic , topic )
2022-05-28 20:03:24 +00:00
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
}
2022-07-08 17:55:31 +00:00
portal . log . Debugfln ( "Removing room from space %s" , portal . InSpace )
2022-05-28 20:03:24 +00:00
_ , 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-07-08 17:55:31 +00:00
portal . log . Debugfln ( "Adding room to space %s" , mxid )
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
}
2022-07-08 17:55:31 +00:00
portal . log . Debugfln ( "Updating parent ID %q -> %q" , portal . ParentID , parentID )
2022-05-28 20:03:24 +00:00
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 ""
}
2022-08-19 20:10:03 +00:00
func ( portal * Portal ) updateSpace ( ) bool {
2022-05-28 20:03:24 +00:00
if portal . MXID == "" {
return false
}
2022-08-19 20:10:03 +00:00
if portal . Parent != nil {
2022-05-28 20:03:24 +00:00
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
}
switch portal . Type {
case discordgo . ChannelTypeDM :
if portal . OtherUserID != "" {
puppet := portal . bridge . GetPuppetByID ( portal . OtherUserID )
changed = portal . UpdateAvatarFromPuppet ( puppet ) || changed
2022-07-08 17:48:36 +00:00
changed = portal . UpdateNameDirect ( puppet . Name ) || changed
2022-05-28 20:03:24 +00:00
}
case discordgo . ChannelTypeGroupDM :
changed = portal . UpdateGroupDMAvatar ( meta . Icon ) || changed
fallthrough
default :
2022-07-08 17:48:36 +00:00
changed = portal . UpdateName ( meta ) || 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
2022-08-19 20:10:03 +00:00
// Private channels are added to the space in User.handlePrivateChannel
if portal . GuildID != "" && portal . MXID != "" && portal . ExpectedSpaceID ( ) != portal . InSpace {
changed = portal . updateSpace ( ) || changed
2022-05-28 20:03:24 +00:00
}
if changed {
portal . UpdateBridgeInfo ( )
portal . Update ( )
}
return meta
2022-05-22 19:16:42 +00:00
}