guildportal.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  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/mautrix"
  23. "maunium.net/go/mautrix/event"
  24. "maunium.net/go/mautrix/id"
  25. "github.com/bwmarrin/discordgo"
  26. "go.mau.fi/mautrix-discord/config"
  27. "go.mau.fi/mautrix-discord/database"
  28. )
  29. type Guild struct {
  30. *database.Guild
  31. bridge *DiscordBridge
  32. log log.Logger
  33. roomCreateLock sync.Mutex
  34. }
  35. func (br *DiscordBridge) loadGuild(dbGuild *database.Guild, id string, createIfNotExist bool) *Guild {
  36. if dbGuild == nil {
  37. if id == "" || !createIfNotExist {
  38. return nil
  39. }
  40. dbGuild = br.DB.Guild.New()
  41. dbGuild.ID = id
  42. dbGuild.Insert()
  43. }
  44. guild := br.NewGuild(dbGuild)
  45. br.guildsByID[guild.ID] = guild
  46. if guild.MXID != "" {
  47. br.guildsByMXID[guild.MXID] = guild
  48. }
  49. return guild
  50. }
  51. func (br *DiscordBridge) GetGuildByMXID(mxid id.RoomID) *Guild {
  52. br.guildsLock.Lock()
  53. defer br.guildsLock.Unlock()
  54. portal, ok := br.guildsByMXID[mxid]
  55. if !ok {
  56. return br.loadGuild(br.DB.Guild.GetByMXID(mxid), "", false)
  57. }
  58. return portal
  59. }
  60. func (br *DiscordBridge) GetGuildByID(id string, createIfNotExist bool) *Guild {
  61. br.guildsLock.Lock()
  62. defer br.guildsLock.Unlock()
  63. guild, ok := br.guildsByID[id]
  64. if !ok {
  65. return br.loadGuild(br.DB.Guild.GetByID(id), id, createIfNotExist)
  66. }
  67. return guild
  68. }
  69. func (br *DiscordBridge) GetAllGuilds() []*Guild {
  70. return br.dbGuildsToGuilds(br.DB.Guild.GetAll())
  71. }
  72. func (br *DiscordBridge) dbGuildsToGuilds(dbGuilds []*database.Guild) []*Guild {
  73. br.guildsLock.Lock()
  74. defer br.guildsLock.Unlock()
  75. output := make([]*Guild, len(dbGuilds))
  76. for index, dbGuild := range dbGuilds {
  77. if dbGuild == nil {
  78. continue
  79. }
  80. guild, ok := br.guildsByID[dbGuild.ID]
  81. if !ok {
  82. guild = br.loadGuild(dbGuild, "", false)
  83. }
  84. output[index] = guild
  85. }
  86. return output
  87. }
  88. func (br *DiscordBridge) NewGuild(dbGuild *database.Guild) *Guild {
  89. guild := &Guild{
  90. Guild: dbGuild,
  91. bridge: br,
  92. log: br.Log.Sub(fmt.Sprintf("Guild/%s", dbGuild.ID)),
  93. }
  94. return guild
  95. }
  96. func (guild *Guild) getBridgeInfo() (string, event.BridgeEventContent) {
  97. bridgeInfo := event.BridgeEventContent{
  98. BridgeBot: guild.bridge.Bot.UserID,
  99. Creator: guild.bridge.Bot.UserID,
  100. Protocol: event.BridgeInfoSection{
  101. ID: "discordgo",
  102. DisplayName: "Discord",
  103. AvatarURL: guild.bridge.Config.AppService.Bot.ParsedAvatar.CUString(),
  104. ExternalURL: "https://discord.com/",
  105. },
  106. Channel: event.BridgeInfoSection{
  107. ID: guild.ID,
  108. DisplayName: guild.Name,
  109. AvatarURL: guild.AvatarURL.CUString(),
  110. },
  111. }
  112. bridgeInfoStateKey := fmt.Sprintf("fi.mau.discord://discord/%s", guild.ID)
  113. return bridgeInfoStateKey, bridgeInfo
  114. }
  115. func (guild *Guild) UpdateBridgeInfo() {
  116. if len(guild.MXID) == 0 {
  117. guild.log.Debugln("Not updating bridge info: no Matrix room created")
  118. return
  119. }
  120. guild.log.Debugln("Updating bridge info...")
  121. stateKey, content := guild.getBridgeInfo()
  122. _, err := guild.bridge.Bot.SendStateEvent(guild.MXID, event.StateBridge, stateKey, content)
  123. if err != nil {
  124. guild.log.Warnln("Failed to update m.bridge:", err)
  125. }
  126. // TODO remove this once https://github.com/matrix-org/matrix-doc/pull/2346 is in spec
  127. _, err = guild.bridge.Bot.SendStateEvent(guild.MXID, event.StateHalfShotBridge, stateKey, content)
  128. if err != nil {
  129. guild.log.Warnln("Failed to update uk.half-shot.bridge:", err)
  130. }
  131. }
  132. func (guild *Guild) CreateMatrixRoom(user *User, meta *discordgo.Guild) error {
  133. guild.roomCreateLock.Lock()
  134. defer guild.roomCreateLock.Unlock()
  135. if guild.MXID != "" {
  136. return nil
  137. }
  138. guild.log.Infoln("Creating Matrix room for guild")
  139. guild.UpdateInfo(user, meta)
  140. bridgeInfoStateKey, bridgeInfo := guild.getBridgeInfo()
  141. initialState := []*event.Event{{
  142. Type: event.StateBridge,
  143. Content: event.Content{Parsed: bridgeInfo},
  144. StateKey: &bridgeInfoStateKey,
  145. }, {
  146. // TODO remove this once https://github.com/matrix-org/matrix-doc/pull/2346 is in spec
  147. Type: event.StateHalfShotBridge,
  148. Content: event.Content{Parsed: bridgeInfo},
  149. StateKey: &bridgeInfoStateKey,
  150. }}
  151. if !guild.AvatarURL.IsEmpty() {
  152. initialState = append(initialState, &event.Event{
  153. Type: event.StateRoomAvatar,
  154. Content: event.Content{Parsed: &event.RoomAvatarEventContent{
  155. URL: guild.AvatarURL,
  156. }},
  157. })
  158. }
  159. creationContent := map[string]interface{}{
  160. "type": event.RoomTypeSpace,
  161. }
  162. if !guild.bridge.Config.Bridge.FederateRooms {
  163. creationContent["m.federate"] = false
  164. }
  165. resp, err := guild.bridge.Bot.CreateRoom(&mautrix.ReqCreateRoom{
  166. Visibility: "private",
  167. Name: guild.Name,
  168. Preset: "private_chat",
  169. InitialState: initialState,
  170. CreationContent: creationContent,
  171. })
  172. if err != nil {
  173. guild.log.Warnln("Failed to create room:", err)
  174. return err
  175. }
  176. guild.MXID = resp.RoomID
  177. guild.NameSet = true
  178. guild.AvatarSet = !guild.AvatarURL.IsEmpty()
  179. guild.Update()
  180. guild.bridge.guildsLock.Lock()
  181. guild.bridge.guildsByMXID[guild.MXID] = guild
  182. guild.bridge.guildsLock.Unlock()
  183. guild.log.Infoln("Matrix room created:", guild.MXID)
  184. user.ensureInvited(nil, guild.MXID, false)
  185. return nil
  186. }
  187. func (guild *Guild) UpdateInfo(source *User, meta *discordgo.Guild) *discordgo.Guild {
  188. if meta.Unavailable {
  189. guild.log.Debugfln("Ignoring unavailable guild update")
  190. return meta
  191. }
  192. changed := false
  193. changed = guild.UpdateName(meta) || changed
  194. changed = guild.UpdateAvatar(meta.Icon) || changed
  195. if changed {
  196. guild.UpdateBridgeInfo()
  197. guild.Update()
  198. }
  199. return meta
  200. }
  201. func (guild *Guild) UpdateName(meta *discordgo.Guild) bool {
  202. name := guild.bridge.Config.Bridge.FormatGuildName(config.GuildNameParams{
  203. Name: meta.Name,
  204. })
  205. if guild.PlainName == meta.Name && guild.Name == name && (guild.NameSet || guild.MXID == "") {
  206. return false
  207. }
  208. guild.log.Debugfln("Updating name %q -> %q", guild.Name, name)
  209. guild.Name = name
  210. guild.PlainName = meta.Name
  211. guild.NameSet = false
  212. if guild.MXID != "" {
  213. _, err := guild.bridge.Bot.SetRoomName(guild.MXID, guild.Name)
  214. if err != nil {
  215. guild.log.Warnln("Failed to update room name: %s", err)
  216. } else {
  217. guild.NameSet = true
  218. }
  219. }
  220. return true
  221. }
  222. func (guild *Guild) UpdateAvatar(iconID string) bool {
  223. if guild.Avatar == iconID && (iconID == "") == guild.AvatarURL.IsEmpty() && (guild.AvatarSet || guild.MXID == "") {
  224. return false
  225. }
  226. guild.log.Debugfln("Updating avatar %q -> %q", guild.Avatar, iconID)
  227. guild.AvatarSet = false
  228. guild.Avatar = iconID
  229. guild.AvatarURL = id.ContentURI{}
  230. if guild.Avatar != "" {
  231. var err error
  232. guild.AvatarURL, err = uploadAvatar(guild.bridge.Bot, discordgo.EndpointGuildIcon(guild.ID, iconID))
  233. if err != nil {
  234. guild.log.Warnfln("Failed to reupload guild avatar %s: %v", guild.Avatar, err)
  235. return true
  236. }
  237. }
  238. if guild.MXID != "" {
  239. _, err := guild.bridge.Bot.SetRoomAvatar(guild.MXID, guild.AvatarURL)
  240. if err != nil {
  241. guild.log.Warnln("Failed to update room avatar:", err)
  242. } else {
  243. guild.AvatarSet = true
  244. }
  245. }
  246. return true
  247. }
  248. func (guild *Guild) cleanup() {
  249. if guild.MXID == "" {
  250. return
  251. }
  252. intent := guild.bridge.Bot
  253. if guild.bridge.SpecVersions.UnstableFeatures["com.beeper.room_yeeting"] {
  254. err := intent.BeeperDeleteRoom(guild.MXID)
  255. if err == nil || errors.Is(err, mautrix.MNotFound) {
  256. return
  257. }
  258. guild.log.Warnfln("Failed to delete %s using hungryserv yeet endpoint, falling back to normal behavior: %v", guild.MXID, err)
  259. }
  260. guild.bridge.cleanupRoom(intent, guild.MXID, false, guild.log)
  261. }
  262. func (guild *Guild) RemoveMXID() {
  263. guild.bridge.guildsLock.Lock()
  264. defer guild.bridge.guildsLock.Unlock()
  265. if guild.MXID == "" {
  266. return
  267. }
  268. delete(guild.bridge.guildsByMXID, guild.MXID)
  269. guild.MXID = ""
  270. guild.AvatarSet = false
  271. guild.NameSet = false
  272. guild.BridgingMode = database.GuildBridgeNothing
  273. guild.Update()
  274. }
  275. func (guild *Guild) Delete() {
  276. guild.Guild.Delete()
  277. guild.bridge.guildsLock.Lock()
  278. delete(guild.bridge.guildsByID, guild.ID)
  279. if guild.MXID != "" {
  280. delete(guild.bridge.guildsByMXID, guild.MXID)
  281. }
  282. guild.bridge.guildsLock.Unlock()
  283. }