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