guildportal.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. // mautrix-discord - A Matrix-Discord puppeting bridge.
  2. // Copyright (C) 2022 Tulir Asokan
  3. //
  4. // This program is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Affero General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // This program is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Affero General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Affero General Public License
  15. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. package main
  17. import (
  18. "errors"
  19. "fmt"
  20. "sync"
  21. log "maunium.net/go/maulogger/v2"
  22. "maunium.net/go/maulogger/v2/maulogadapt"
  23. "maunium.net/go/mautrix"
  24. "maunium.net/go/mautrix/event"
  25. "maunium.net/go/mautrix/id"
  26. "github.com/bwmarrin/discordgo"
  27. "go.mau.fi/mautrix-discord/config"
  28. "go.mau.fi/mautrix-discord/database"
  29. )
  30. type Guild struct {
  31. *database.Guild
  32. bridge *DiscordBridge
  33. log log.Logger
  34. roomCreateLock sync.Mutex
  35. }
  36. func (br *DiscordBridge) loadGuild(dbGuild *database.Guild, id string, createIfNotExist bool) *Guild {
  37. if dbGuild == nil {
  38. if id == "" || !createIfNotExist {
  39. return nil
  40. }
  41. dbGuild = br.DB.Guild.New()
  42. dbGuild.ID = id
  43. dbGuild.Insert()
  44. }
  45. guild := br.NewGuild(dbGuild)
  46. br.guildsByID[guild.ID] = guild
  47. if guild.MXID != "" {
  48. br.guildsByMXID[guild.MXID] = guild
  49. }
  50. return guild
  51. }
  52. func (br *DiscordBridge) GetGuildByMXID(mxid id.RoomID) *Guild {
  53. br.guildsLock.Lock()
  54. defer br.guildsLock.Unlock()
  55. portal, ok := br.guildsByMXID[mxid]
  56. if !ok {
  57. return br.loadGuild(br.DB.Guild.GetByMXID(mxid), "", false)
  58. }
  59. return portal
  60. }
  61. func (br *DiscordBridge) GetGuildByID(id string, createIfNotExist bool) *Guild {
  62. br.guildsLock.Lock()
  63. defer br.guildsLock.Unlock()
  64. guild, ok := br.guildsByID[id]
  65. if !ok {
  66. return br.loadGuild(br.DB.Guild.GetByID(id), id, createIfNotExist)
  67. }
  68. return guild
  69. }
  70. func (br *DiscordBridge) GetAllGuilds() []*Guild {
  71. return br.dbGuildsToGuilds(br.DB.Guild.GetAll())
  72. }
  73. func (br *DiscordBridge) dbGuildsToGuilds(dbGuilds []*database.Guild) []*Guild {
  74. br.guildsLock.Lock()
  75. defer br.guildsLock.Unlock()
  76. output := make([]*Guild, len(dbGuilds))
  77. for index, dbGuild := range dbGuilds {
  78. if dbGuild == nil {
  79. continue
  80. }
  81. guild, ok := br.guildsByID[dbGuild.ID]
  82. if !ok {
  83. guild = br.loadGuild(dbGuild, "", false)
  84. }
  85. output[index] = guild
  86. }
  87. return output
  88. }
  89. func (br *DiscordBridge) NewGuild(dbGuild *database.Guild) *Guild {
  90. guild := &Guild{
  91. Guild: dbGuild,
  92. bridge: br,
  93. log: br.Log.Sub(fmt.Sprintf("Guild/%s", dbGuild.ID)),
  94. }
  95. return guild
  96. }
  97. func (guild *Guild) getBridgeInfo() (string, event.BridgeEventContent) {
  98. bridgeInfo := event.BridgeEventContent{
  99. BridgeBot: guild.bridge.Bot.UserID,
  100. Creator: guild.bridge.Bot.UserID,
  101. Protocol: event.BridgeInfoSection{
  102. ID: "discordgo",
  103. DisplayName: "Discord",
  104. AvatarURL: guild.bridge.Config.AppService.Bot.ParsedAvatar.CUString(),
  105. ExternalURL: "https://discord.com/",
  106. },
  107. Channel: event.BridgeInfoSection{
  108. ID: guild.ID,
  109. DisplayName: guild.Name,
  110. AvatarURL: guild.AvatarURL.CUString(),
  111. },
  112. }
  113. bridgeInfoStateKey := fmt.Sprintf("fi.mau.discord://discord/%s", guild.ID)
  114. return bridgeInfoStateKey, bridgeInfo
  115. }
  116. func (guild *Guild) UpdateBridgeInfo() {
  117. if len(guild.MXID) == 0 {
  118. guild.log.Debugln("Not updating bridge info: no Matrix room created")
  119. return
  120. }
  121. guild.log.Debugln("Updating bridge info...")
  122. stateKey, content := guild.getBridgeInfo()
  123. _, err := guild.bridge.Bot.SendStateEvent(guild.MXID, event.StateBridge, stateKey, content)
  124. if err != nil {
  125. guild.log.Warnln("Failed to update m.bridge:", err)
  126. }
  127. // TODO remove this once https://github.com/matrix-org/matrix-doc/pull/2346 is in spec
  128. _, err = guild.bridge.Bot.SendStateEvent(guild.MXID, event.StateHalfShotBridge, stateKey, content)
  129. if err != nil {
  130. guild.log.Warnln("Failed to update uk.half-shot.bridge:", err)
  131. }
  132. }
  133. func (guild *Guild) CreateMatrixRoom(user *User, meta *discordgo.Guild) error {
  134. guild.roomCreateLock.Lock()
  135. defer guild.roomCreateLock.Unlock()
  136. if guild.MXID != "" {
  137. return nil
  138. }
  139. guild.log.Infoln("Creating Matrix room for guild")
  140. guild.UpdateInfo(user, meta)
  141. bridgeInfoStateKey, bridgeInfo := guild.getBridgeInfo()
  142. initialState := []*event.Event{{
  143. Type: event.StateBridge,
  144. Content: event.Content{Parsed: bridgeInfo},
  145. StateKey: &bridgeInfoStateKey,
  146. }, {
  147. // TODO remove this once https://github.com/matrix-org/matrix-doc/pull/2346 is in spec
  148. Type: event.StateHalfShotBridge,
  149. Content: event.Content{Parsed: bridgeInfo},
  150. StateKey: &bridgeInfoStateKey,
  151. }}
  152. if !guild.AvatarURL.IsEmpty() {
  153. initialState = append(initialState, &event.Event{
  154. Type: event.StateRoomAvatar,
  155. Content: event.Content{Parsed: &event.RoomAvatarEventContent{
  156. URL: guild.AvatarURL,
  157. }},
  158. })
  159. }
  160. creationContent := map[string]interface{}{
  161. "type": event.RoomTypeSpace,
  162. }
  163. if !guild.bridge.Config.Bridge.FederateRooms {
  164. creationContent["m.federate"] = false
  165. }
  166. resp, err := guild.bridge.Bot.CreateRoom(&mautrix.ReqCreateRoom{
  167. Visibility: "private",
  168. Name: guild.Name,
  169. Preset: "private_chat",
  170. InitialState: initialState,
  171. CreationContent: creationContent,
  172. })
  173. if err != nil {
  174. guild.log.Warnln("Failed to create room:", err)
  175. return err
  176. }
  177. guild.MXID = resp.RoomID
  178. guild.NameSet = true
  179. guild.AvatarSet = !guild.AvatarURL.IsEmpty()
  180. guild.Update()
  181. guild.bridge.guildsLock.Lock()
  182. guild.bridge.guildsByMXID[guild.MXID] = guild
  183. guild.bridge.guildsLock.Unlock()
  184. guild.log.Infoln("Matrix room created:", guild.MXID)
  185. user.ensureInvited(nil, guild.MXID, false, true)
  186. return nil
  187. }
  188. func (guild *Guild) UpdateInfo(source *User, meta *discordgo.Guild) *discordgo.Guild {
  189. if meta.Unavailable {
  190. guild.log.Debugfln("Ignoring unavailable guild update")
  191. return meta
  192. }
  193. changed := false
  194. changed = guild.UpdateName(meta) || changed
  195. changed = guild.UpdateAvatar(meta.Icon) || changed
  196. if changed {
  197. guild.UpdateBridgeInfo()
  198. guild.Update()
  199. }
  200. source.ensureInvited(nil, guild.MXID, false, false)
  201. return meta
  202. }
  203. func (guild *Guild) UpdateName(meta *discordgo.Guild) bool {
  204. name := guild.bridge.Config.Bridge.FormatGuildName(config.GuildNameParams{
  205. Name: meta.Name,
  206. })
  207. if guild.PlainName == meta.Name && guild.Name == name && (guild.NameSet || guild.MXID == "") {
  208. return false
  209. }
  210. guild.log.Debugfln("Updating name %q -> %q", guild.Name, name)
  211. guild.Name = name
  212. guild.PlainName = meta.Name
  213. guild.NameSet = false
  214. if guild.MXID != "" {
  215. _, err := guild.bridge.Bot.SetRoomName(guild.MXID, guild.Name)
  216. if err != nil {
  217. guild.log.Warnln("Failed to update room name: %s", err)
  218. } else {
  219. guild.NameSet = true
  220. }
  221. }
  222. return true
  223. }
  224. func (guild *Guild) UpdateAvatar(iconID string) bool {
  225. if guild.Avatar == iconID && (iconID == "") == guild.AvatarURL.IsEmpty() && (guild.AvatarSet || guild.MXID == "") {
  226. return false
  227. }
  228. guild.log.Debugfln("Updating avatar %q -> %q", guild.Avatar, iconID)
  229. guild.AvatarSet = false
  230. guild.Avatar = iconID
  231. guild.AvatarURL = id.ContentURI{}
  232. if guild.Avatar != "" {
  233. // TODO direct media support
  234. copied, err := guild.bridge.copyAttachmentToMatrix(guild.bridge.Bot, discordgo.EndpointGuildIcon(guild.ID, iconID), false, AttachmentMeta{
  235. AttachmentID: fmt.Sprintf("guild_avatar/%s/%s", guild.ID, iconID),
  236. })
  237. if err != nil {
  238. guild.log.Warnfln("Failed to reupload guild avatar %s: %v", iconID, err)
  239. return true
  240. }
  241. guild.AvatarURL = copied.MXC
  242. }
  243. if guild.MXID != "" {
  244. _, err := guild.bridge.Bot.SetRoomAvatar(guild.MXID, guild.AvatarURL)
  245. if err != nil {
  246. guild.log.Warnln("Failed to update room avatar:", err)
  247. } else {
  248. guild.AvatarSet = true
  249. }
  250. }
  251. return true
  252. }
  253. func (guild *Guild) cleanup() {
  254. if guild.MXID == "" {
  255. return
  256. }
  257. intent := guild.bridge.Bot
  258. if guild.bridge.SpecVersions.Supports(mautrix.BeeperFeatureRoomYeeting) {
  259. err := intent.BeeperDeleteRoom(guild.MXID)
  260. if err != nil && !errors.Is(err, mautrix.MNotFound) {
  261. guild.log.Errorfln("Failed to delete %s using hungryserv yeet endpoint: %v", guild.MXID, err)
  262. }
  263. return
  264. }
  265. guild.bridge.cleanupRoom(intent, guild.MXID, false, *maulogadapt.MauAsZero(guild.log))
  266. }
  267. func (guild *Guild) RemoveMXID() {
  268. guild.bridge.guildsLock.Lock()
  269. defer guild.bridge.guildsLock.Unlock()
  270. if guild.MXID == "" {
  271. return
  272. }
  273. delete(guild.bridge.guildsByMXID, guild.MXID)
  274. guild.MXID = ""
  275. guild.AvatarSet = false
  276. guild.NameSet = false
  277. guild.BridgingMode = database.GuildBridgeNothing
  278. guild.Update()
  279. }
  280. func (guild *Guild) Delete() {
  281. guild.Guild.Delete()
  282. guild.bridge.guildsLock.Lock()
  283. delete(guild.bridge.guildsByID, guild.ID)
  284. if guild.MXID != "" {
  285. delete(guild.bridge.guildsByMXID, guild.MXID)
  286. }
  287. guild.bridge.guildsLock.Unlock()
  288. }