forked from mirror/mautrix-discord
334 lines
8.8 KiB
Go
334 lines
8.8 KiB
Go
// mautrix-discord - A Matrix-Discord puppeting bridge.
|
|
// Copyright (C) 2022 Tulir Asokan
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
|
|
log "maunium.net/go/maulogger/v2"
|
|
"maunium.net/go/maulogger/v2/maulogadapt"
|
|
|
|
"maunium.net/go/mautrix"
|
|
"maunium.net/go/mautrix/event"
|
|
"maunium.net/go/mautrix/id"
|
|
|
|
"github.com/bwmarrin/discordgo"
|
|
|
|
"go.mau.fi/mautrix-discord/config"
|
|
"go.mau.fi/mautrix-discord/database"
|
|
)
|
|
|
|
type Guild struct {
|
|
*database.Guild
|
|
|
|
bridge *DiscordBridge
|
|
log log.Logger
|
|
|
|
roomCreateLock sync.Mutex
|
|
}
|
|
|
|
func (br *DiscordBridge) loadGuild(dbGuild *database.Guild, id string, createIfNotExist bool) *Guild {
|
|
if dbGuild == nil {
|
|
if id == "" || !createIfNotExist {
|
|
return nil
|
|
}
|
|
|
|
dbGuild = br.DB.Guild.New()
|
|
dbGuild.ID = id
|
|
dbGuild.Insert()
|
|
}
|
|
|
|
guild := br.NewGuild(dbGuild)
|
|
|
|
br.guildsByID[guild.ID] = guild
|
|
if guild.MXID != "" {
|
|
br.guildsByMXID[guild.MXID] = guild
|
|
}
|
|
|
|
return guild
|
|
}
|
|
|
|
func (br *DiscordBridge) GetGuildByMXID(mxid id.RoomID) *Guild {
|
|
br.guildsLock.Lock()
|
|
defer br.guildsLock.Unlock()
|
|
|
|
portal, ok := br.guildsByMXID[mxid]
|
|
if !ok {
|
|
return br.loadGuild(br.DB.Guild.GetByMXID(mxid), "", false)
|
|
}
|
|
|
|
return portal
|
|
}
|
|
|
|
func (br *DiscordBridge) GetGuildByID(id string, createIfNotExist bool) *Guild {
|
|
br.guildsLock.Lock()
|
|
defer br.guildsLock.Unlock()
|
|
|
|
guild, ok := br.guildsByID[id]
|
|
if !ok {
|
|
return br.loadGuild(br.DB.Guild.GetByID(id), id, createIfNotExist)
|
|
}
|
|
|
|
return guild
|
|
}
|
|
|
|
func (br *DiscordBridge) GetAllGuilds() []*Guild {
|
|
return br.dbGuildsToGuilds(br.DB.Guild.GetAll())
|
|
}
|
|
|
|
func (br *DiscordBridge) dbGuildsToGuilds(dbGuilds []*database.Guild) []*Guild {
|
|
br.guildsLock.Lock()
|
|
defer br.guildsLock.Unlock()
|
|
|
|
output := make([]*Guild, len(dbGuilds))
|
|
for index, dbGuild := range dbGuilds {
|
|
if dbGuild == nil {
|
|
continue
|
|
}
|
|
|
|
guild, ok := br.guildsByID[dbGuild.ID]
|
|
if !ok {
|
|
guild = br.loadGuild(dbGuild, "", false)
|
|
}
|
|
|
|
output[index] = guild
|
|
}
|
|
|
|
return output
|
|
}
|
|
|
|
func (br *DiscordBridge) NewGuild(dbGuild *database.Guild) *Guild {
|
|
guild := &Guild{
|
|
Guild: dbGuild,
|
|
bridge: br,
|
|
log: br.Log.Sub(fmt.Sprintf("Guild/%s", dbGuild.ID)),
|
|
}
|
|
|
|
return guild
|
|
}
|
|
|
|
func (guild *Guild) getBridgeInfo() (string, event.BridgeEventContent) {
|
|
bridgeInfo := event.BridgeEventContent{
|
|
BridgeBot: guild.bridge.Bot.UserID,
|
|
Creator: guild.bridge.Bot.UserID,
|
|
Protocol: event.BridgeInfoSection{
|
|
ID: "discordgo",
|
|
DisplayName: "Discord",
|
|
AvatarURL: guild.bridge.Config.AppService.Bot.ParsedAvatar.CUString(),
|
|
ExternalURL: "https://discord.com/",
|
|
},
|
|
Channel: event.BridgeInfoSection{
|
|
ID: guild.ID,
|
|
DisplayName: guild.Name,
|
|
AvatarURL: guild.AvatarURL.CUString(),
|
|
},
|
|
}
|
|
bridgeInfoStateKey := fmt.Sprintf("fi.mau.discord://discord/%s", guild.ID)
|
|
return bridgeInfoStateKey, bridgeInfo
|
|
}
|
|
|
|
func (guild *Guild) UpdateBridgeInfo() {
|
|
if len(guild.MXID) == 0 {
|
|
guild.log.Debugln("Not updating bridge info: no Matrix room created")
|
|
return
|
|
}
|
|
guild.log.Debugln("Updating bridge info...")
|
|
stateKey, content := guild.getBridgeInfo()
|
|
_, err := guild.bridge.Bot.SendStateEvent(guild.MXID, event.StateBridge, stateKey, content)
|
|
if err != nil {
|
|
guild.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 = guild.bridge.Bot.SendStateEvent(guild.MXID, event.StateHalfShotBridge, stateKey, content)
|
|
if err != nil {
|
|
guild.log.Warnln("Failed to update uk.half-shot.bridge:", err)
|
|
}
|
|
}
|
|
|
|
func (guild *Guild) CreateMatrixRoom(user *User, meta *discordgo.Guild) error {
|
|
guild.roomCreateLock.Lock()
|
|
defer guild.roomCreateLock.Unlock()
|
|
if guild.MXID != "" {
|
|
return nil
|
|
}
|
|
guild.log.Infoln("Creating Matrix room for guild")
|
|
guild.UpdateInfo(user, meta)
|
|
|
|
bridgeInfoStateKey, bridgeInfo := guild.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,
|
|
}}
|
|
|
|
if !guild.AvatarURL.IsEmpty() {
|
|
initialState = append(initialState, &event.Event{
|
|
Type: event.StateRoomAvatar,
|
|
Content: event.Content{Parsed: &event.RoomAvatarEventContent{
|
|
URL: guild.AvatarURL,
|
|
}},
|
|
})
|
|
}
|
|
|
|
creationContent := map[string]interface{}{
|
|
"type": event.RoomTypeSpace,
|
|
}
|
|
if !guild.bridge.Config.Bridge.FederateRooms {
|
|
creationContent["m.federate"] = false
|
|
}
|
|
|
|
resp, err := guild.bridge.Bot.CreateRoom(&mautrix.ReqCreateRoom{
|
|
Visibility: "private",
|
|
Name: guild.Name,
|
|
Preset: "private_chat",
|
|
InitialState: initialState,
|
|
CreationContent: creationContent,
|
|
})
|
|
if err != nil {
|
|
guild.log.Warnln("Failed to create room:", err)
|
|
return err
|
|
}
|
|
|
|
guild.MXID = resp.RoomID
|
|
guild.NameSet = true
|
|
guild.AvatarSet = !guild.AvatarURL.IsEmpty()
|
|
guild.Update()
|
|
guild.bridge.guildsLock.Lock()
|
|
guild.bridge.guildsByMXID[guild.MXID] = guild
|
|
guild.bridge.guildsLock.Unlock()
|
|
guild.log.Infoln("Matrix room created:", guild.MXID)
|
|
|
|
user.ensureInvited(nil, guild.MXID, false, true)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (guild *Guild) UpdateInfo(source *User, meta *discordgo.Guild) *discordgo.Guild {
|
|
if meta.Unavailable {
|
|
guild.log.Debugfln("Ignoring unavailable guild update")
|
|
return meta
|
|
}
|
|
changed := false
|
|
changed = guild.UpdateName(meta) || changed
|
|
changed = guild.UpdateAvatar(meta.Icon) || changed
|
|
if changed {
|
|
guild.UpdateBridgeInfo()
|
|
guild.Update()
|
|
}
|
|
source.ensureInvited(nil, guild.MXID, false, false)
|
|
return meta
|
|
}
|
|
|
|
func (guild *Guild) UpdateName(meta *discordgo.Guild) bool {
|
|
name := guild.bridge.Config.Bridge.FormatGuildName(config.GuildNameParams{
|
|
Name: meta.Name,
|
|
})
|
|
if guild.PlainName == meta.Name && guild.Name == name && (guild.NameSet || guild.MXID == "") {
|
|
return false
|
|
}
|
|
guild.log.Debugfln("Updating name %q -> %q", guild.Name, name)
|
|
guild.Name = name
|
|
guild.PlainName = meta.Name
|
|
guild.NameSet = false
|
|
if guild.MXID != "" {
|
|
_, err := guild.bridge.Bot.SetRoomName(guild.MXID, guild.Name)
|
|
if err != nil {
|
|
guild.log.Warnln("Failed to update room name: %s", err)
|
|
} else {
|
|
guild.NameSet = true
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (guild *Guild) UpdateAvatar(iconID string) bool {
|
|
if guild.Avatar == iconID && (iconID == "") == guild.AvatarURL.IsEmpty() && (guild.AvatarSet || guild.MXID == "") {
|
|
return false
|
|
}
|
|
guild.log.Debugfln("Updating avatar %q -> %q", guild.Avatar, iconID)
|
|
guild.AvatarSet = false
|
|
guild.Avatar = iconID
|
|
guild.AvatarURL = id.ContentURI{}
|
|
if guild.Avatar != "" {
|
|
// TODO direct media support
|
|
copied, err := guild.bridge.copyAttachmentToMatrix(guild.bridge.Bot, discordgo.EndpointGuildIcon(guild.ID, iconID), false, AttachmentMeta{
|
|
AttachmentID: fmt.Sprintf("guild_avatar/%s/%s", guild.ID, iconID),
|
|
})
|
|
if err != nil {
|
|
guild.log.Warnfln("Failed to reupload guild avatar %s: %v", iconID, err)
|
|
return true
|
|
}
|
|
guild.AvatarURL = copied.MXC
|
|
}
|
|
if guild.MXID != "" {
|
|
_, err := guild.bridge.Bot.SetRoomAvatar(guild.MXID, guild.AvatarURL)
|
|
if err != nil {
|
|
guild.log.Warnln("Failed to update room avatar:", err)
|
|
} else {
|
|
guild.AvatarSet = true
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (guild *Guild) cleanup() {
|
|
if guild.MXID == "" {
|
|
return
|
|
}
|
|
intent := guild.bridge.Bot
|
|
if guild.bridge.SpecVersions.UnstableFeatures["com.beeper.room_yeeting"] {
|
|
err := intent.BeeperDeleteRoom(guild.MXID)
|
|
if err != nil && !errors.Is(err, mautrix.MNotFound) {
|
|
guild.log.Errorfln("Failed to delete %s using hungryserv yeet endpoint: %v", guild.MXID, err)
|
|
}
|
|
return
|
|
}
|
|
guild.bridge.cleanupRoom(intent, guild.MXID, false, *maulogadapt.MauAsZero(guild.log))
|
|
}
|
|
|
|
func (guild *Guild) RemoveMXID() {
|
|
guild.bridge.guildsLock.Lock()
|
|
defer guild.bridge.guildsLock.Unlock()
|
|
if guild.MXID == "" {
|
|
return
|
|
}
|
|
delete(guild.bridge.guildsByMXID, guild.MXID)
|
|
guild.MXID = ""
|
|
guild.AvatarSet = false
|
|
guild.NameSet = false
|
|
guild.BridgingMode = database.GuildBridgeNothing
|
|
guild.Update()
|
|
}
|
|
|
|
func (guild *Guild) Delete() {
|
|
guild.Guild.Delete()
|
|
guild.bridge.guildsLock.Lock()
|
|
delete(guild.bridge.guildsByID, guild.ID)
|
|
if guild.MXID != "" {
|
|
delete(guild.bridge.guildsByMXID, guild.MXID)
|
|
}
|
|
guild.bridge.guildsLock.Unlock()
|
|
|
|
}
|