guildportal.go 7.5 KB

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