commands.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. package bridge
  2. import (
  3. "context"
  4. "fmt"
  5. "time"
  6. "github.com/alecthomas/kong"
  7. "maunium.net/go/mautrix/appservice"
  8. "maunium.net/go/mautrix/event"
  9. "maunium.net/go/mautrix/format"
  10. "maunium.net/go/mautrix/id"
  11. "gitlab.com/beeper/discord/consts"
  12. "gitlab.com/beeper/discord/remoteauth"
  13. "gitlab.com/beeper/discord/version"
  14. )
  15. type globals struct {
  16. context *kong.Context
  17. bridge *Bridge
  18. bot *appservice.IntentAPI
  19. portal *Portal
  20. handler *commandHandler
  21. roomID id.RoomID
  22. user *User
  23. replyTo id.EventID
  24. }
  25. func (g *globals) reply(msg string) {
  26. content := format.RenderMarkdown(msg, true, false)
  27. content.MsgType = event.MsgNotice
  28. intent := g.bot
  29. if g.portal == nil {
  30. g.handler.log.Errorfln("we don't have a portal for this command")
  31. return
  32. }
  33. if g.portal.IsPrivateChat() {
  34. intent = g.portal.MainIntent()
  35. }
  36. _, err := g.portal.sendMatrixMessage(intent, event.EventMessage, &content, nil, time.Now().UTC().UnixMilli())
  37. if err != nil {
  38. g.handler.log.Warnfln("Failed to reply to command from %q: %v", g.user.MXID, err)
  39. }
  40. }
  41. type commands struct {
  42. globals
  43. Disconnect disconnectCmd `kong:"cmd,help='Disconnect from Discord'"`
  44. Help helpCmd `kong:"cmd,help='Displays this message.'"`
  45. Login loginCmd `kong:"cmd,help='Log in to Discord.'"`
  46. Logout logoutCmd `kong:"cmd,help='Log out of Discord.'"`
  47. Reconnect reconnectCmd `kong:"cmd,help='Reconnect to Discord'"`
  48. Version versionCmd `kong:"cmd,help='Displays the version of the bridge.'"`
  49. Guilds guildsCmd `kong:"cmd,help='Guild bridging management.'"`
  50. LoginMatrix loginMatrixCmd `kong:"cmd,help='Replace the puppet for your Discord account with your real Matrix account.'"`
  51. LogoutMatrix logoutMatrixCmd `kong:"cmd,help='Switch the puppet for your Discord account back to the default one.'"`
  52. PingMatrix pingMatrixCmd `kong:"cmd,help='check if your double puppet is working properly'"`
  53. }
  54. ///////////////////////////////////////////////////////////////////////////////
  55. // Help Command
  56. ///////////////////////////////////////////////////////////////////////////////
  57. type helpCmd struct {
  58. Command []string `kong:"arg,optional,help='The command to get help on.'"`
  59. }
  60. func (c *helpCmd) Run(g *globals) error {
  61. ctx, err := kong.Trace(g.context.Kong, c.Command)
  62. if err != nil {
  63. return err
  64. }
  65. if ctx.Error != nil {
  66. return err
  67. }
  68. err = ctx.PrintUsage(true)
  69. if err != nil {
  70. return err
  71. }
  72. fmt.Fprintln(g.context.Stdout)
  73. return nil
  74. }
  75. ///////////////////////////////////////////////////////////////////////////////
  76. // Version Command
  77. ///////////////////////////////////////////////////////////////////////////////
  78. type versionCmd struct{}
  79. func (c *versionCmd) Run(g *globals) error {
  80. fmt.Fprintln(g.context.Stdout, consts.Name, version.String)
  81. return nil
  82. }
  83. ///////////////////////////////////////////////////////////////////////////////
  84. // Login Command
  85. ///////////////////////////////////////////////////////////////////////////////
  86. type loginCmd struct{}
  87. func (l *loginCmd) Run(g *globals) error {
  88. if g.user.LoggedIn() {
  89. fmt.Fprintf(g.context.Stdout, "You are already logged in")
  90. return fmt.Errorf("user already logged in")
  91. }
  92. client, err := remoteauth.New()
  93. if err != nil {
  94. return err
  95. }
  96. qrChan := make(chan string)
  97. doneChan := make(chan struct{})
  98. var qrCodeEvent id.EventID
  99. go func() {
  100. code := <-qrChan
  101. resp, err := g.user.sendQRCode(g.bot, g.roomID, code)
  102. if err != nil {
  103. fmt.Fprintln(g.context.Stdout, "Failed to generate the qrcode")
  104. return
  105. }
  106. qrCodeEvent = resp
  107. }()
  108. ctx := context.Background()
  109. if err := client.Dial(ctx, qrChan, doneChan); err != nil {
  110. close(qrChan)
  111. close(doneChan)
  112. return err
  113. }
  114. <-doneChan
  115. if qrCodeEvent != "" {
  116. _, err := g.bot.RedactEvent(g.roomID, qrCodeEvent)
  117. if err != nil {
  118. fmt.Errorf("Failed to redact the qrcode: %v", err)
  119. }
  120. }
  121. user, err := client.Result()
  122. if err != nil {
  123. fmt.Fprintln(g.context.Stdout, "Failed to log in")
  124. return err
  125. }
  126. if err := g.user.Login(user.Token); err != nil {
  127. fmt.Fprintln(g.context.Stdout, "Failed to login", err)
  128. return err
  129. }
  130. g.user.Lock()
  131. g.user.ID = user.UserID
  132. g.user.Update()
  133. g.user.Unlock()
  134. fmt.Fprintln(g.context.Stdout, "Successfully logged in")
  135. return nil
  136. }
  137. ///////////////////////////////////////////////////////////////////////////////
  138. // Logout Command
  139. ///////////////////////////////////////////////////////////////////////////////
  140. type logoutCmd struct{}
  141. func (l *logoutCmd) Run(g *globals) error {
  142. if !g.user.LoggedIn() {
  143. fmt.Fprintln(g.context.Stdout, "You are not logged in")
  144. return fmt.Errorf("user is not logged in")
  145. }
  146. err := g.user.Logout()
  147. if err != nil {
  148. fmt.Fprintln(g.context.Stdout, "Failed to log out")
  149. return err
  150. }
  151. fmt.Fprintln(g.context.Stdout, "Successfully logged out")
  152. return nil
  153. }
  154. ///////////////////////////////////////////////////////////////////////////////
  155. // Disconnect Command
  156. ///////////////////////////////////////////////////////////////////////////////
  157. type disconnectCmd struct{}
  158. func (d *disconnectCmd) Run(g *globals) error {
  159. if !g.user.Connected() {
  160. fmt.Fprintln(g.context.Stdout, "You are not connected")
  161. return fmt.Errorf("user is not connected")
  162. }
  163. if err := g.user.Disconnect(); err != nil {
  164. fmt.Fprintln(g.context.Stdout, "Failed to disconnect")
  165. return err
  166. }
  167. fmt.Fprintln(g.context.Stdout, "Successfully disconnected")
  168. return nil
  169. }
  170. ///////////////////////////////////////////////////////////////////////////////
  171. // Reconnect Command
  172. ///////////////////////////////////////////////////////////////////////////////
  173. type reconnectCmd struct{}
  174. func (r *reconnectCmd) Run(g *globals) error {
  175. if g.user.Connected() {
  176. fmt.Fprintln(g.context.Stdout, "You are already connected")
  177. return fmt.Errorf("user is already connected")
  178. }
  179. if err := g.user.Connect(); err != nil {
  180. fmt.Fprintln(g.context.Stdout, "Failed to connect")
  181. return err
  182. }
  183. fmt.Fprintln(g.context.Stdout, "Successfully connected")
  184. return nil
  185. }
  186. ///////////////////////////////////////////////////////////////////////////////
  187. // LoginMatrix Command
  188. ///////////////////////////////////////////////////////////////////////////////
  189. type loginMatrixCmd struct {
  190. AccessToken string `kong:"arg,help='The shared secret to use the bridge'"`
  191. }
  192. func (m *loginMatrixCmd) Run(g *globals) error {
  193. puppet := g.bridge.GetPuppetByID(g.user.ID)
  194. err := puppet.SwitchCustomMXID(m.AccessToken, g.user.MXID)
  195. if err != nil {
  196. fmt.Fprintf(g.context.Stdout, "Failed to switch puppet: %v", err)
  197. return err
  198. }
  199. fmt.Fprintf(g.context.Stdout, "Successfully switched puppet")
  200. return nil
  201. }
  202. ///////////////////////////////////////////////////////////////////////////////
  203. // LogoutMatrix Command
  204. ///////////////////////////////////////////////////////////////////////////////
  205. type logoutMatrixCmd struct{}
  206. func (m *logoutMatrixCmd) Run(g *globals) error {
  207. return nil
  208. }
  209. ///////////////////////////////////////////////////////////////////////////////
  210. // PingMatrix Command
  211. ///////////////////////////////////////////////////////////////////////////////
  212. type pingMatrixCmd struct{}
  213. func (m *pingMatrixCmd) Run(g *globals) error {
  214. puppet := g.bridge.GetPuppetByCustomMXID(g.user.MXID)
  215. if puppet == nil || puppet.CustomIntent() == nil {
  216. fmt.Fprintf(g.context.Stdout, "You have not changed your Discord account's Matrix puppet.")
  217. return fmt.Errorf("double puppet not configured")
  218. }
  219. resp, err := puppet.CustomIntent().Whoami()
  220. if err != nil {
  221. fmt.Fprintf(g.context.Stdout, "Failed to validate Matrix login: %v", err)
  222. return err
  223. }
  224. fmt.Fprintf(g.context.Stdout, "Confirmed valid access token for %s / %s", resp.UserID, resp.DeviceID)
  225. return nil
  226. }
  227. ///////////////////////////////////////////////////////////////////////////////
  228. // Guilds Commands
  229. ///////////////////////////////////////////////////////////////////////////////
  230. type guildsCmd struct {
  231. Status guildStatusCmd `kong:"cmd,help='Show the bridge status for the guilds you are in'"`
  232. Bridge guildBridgeCmd `kong:"cmd,help='Bridge a guild'"`
  233. Unbridge guildUnbridgeCmd `kong:"cmd,help="Unbridge a guild'"`
  234. }
  235. type guildStatusCmd struct{}
  236. func (c *guildStatusCmd) Run(g *globals) error {
  237. g.user.guildsLock.Lock()
  238. defer g.user.guildsLock.Unlock()
  239. if len(g.user.guilds) == 0 {
  240. fmt.Fprintf(g.context.Stdout, "you haven't joined any guilds.")
  241. } else {
  242. for _, guild := range g.user.guilds {
  243. status := "not bridged"
  244. if guild.Bridge {
  245. status = "bridged"
  246. }
  247. fmt.Fprintf(g.context.Stdout, "%s %s %s\n", guild.GuildName, guild.GuildID, status)
  248. }
  249. }
  250. return nil
  251. }
  252. type guildBridgeCmd struct {
  253. GuildID string `kong:"arg,help='the id of the guild to unbridge'"`
  254. Entire bool `kong:"flag,help='whether or not to bridge all channels'"`
  255. }
  256. func (c *guildBridgeCmd) Run(g *globals) error {
  257. if err := g.user.bridgeGuild(c.GuildID, c.Entire); err != nil {
  258. return err
  259. }
  260. fmt.Fprintf(g.context.Stdout, "Successfully bridged guild %s", c.GuildID)
  261. return nil
  262. }
  263. type guildUnbridgeCmd struct {
  264. GuildID string `kong:"arg,help='the id of the guild to unbridge'"`
  265. }
  266. func (c *guildUnbridgeCmd) Run(g *globals) error {
  267. if err := g.user.unbridgeGuild(c.GuildID); err != nil {
  268. return err
  269. }
  270. fmt.Fprintf(g.context.Stdout, "Successfully unbridged guild %s", c.GuildID)
  271. return nil
  272. }