main.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. // mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
  2. // Copyright (C) 2020 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. "os"
  20. "os/signal"
  21. "sync"
  22. "syscall"
  23. "time"
  24. flag "maunium.net/go/mauflag"
  25. log "maunium.net/go/maulogger/v2"
  26. "maunium.net/go/mautrix"
  27. "maunium.net/go/mautrix/appservice"
  28. "maunium.net/go/mautrix/event"
  29. "maunium.net/go/mautrix/id"
  30. "maunium.net/go/mautrix-whatsapp/config"
  31. "maunium.net/go/mautrix-whatsapp/database"
  32. "maunium.net/go/mautrix-whatsapp/database/upgrades"
  33. "maunium.net/go/mautrix-whatsapp/types"
  34. )
  35. var configPath = flag.MakeFull("c", "config", "The path to your config file.", "config.yaml").String()
  36. //var baseConfigPath = flag.MakeFull("b", "base-config", "The path to the example config file.", "example-config.yaml").String()
  37. var registrationPath = flag.MakeFull("r", "registration", "The path where to save the appservice registration.", "registration.yaml").String()
  38. var generateRegistration = flag.MakeFull("g", "generate-registration", "Generate registration and quit.", "false").Bool()
  39. var ignoreUnsupportedDatabase = flag.Make().LongKey("ignore-unsupported-database").Usage("Run even if database is too new").Default("false").Bool()
  40. var migrateFrom = flag.Make().LongKey("migrate-db").Usage("Source database type and URI to migrate from.").Bool()
  41. var wantHelp, _ = flag.MakeHelpFlag()
  42. func (bridge *Bridge) GenerateRegistration() {
  43. reg, err := bridge.Config.NewRegistration()
  44. if err != nil {
  45. fmt.Fprintln(os.Stderr, "Failed to generate registration:", err)
  46. os.Exit(20)
  47. }
  48. err = reg.Save(*registrationPath)
  49. if err != nil {
  50. fmt.Fprintln(os.Stderr, "Failed to save registration:", err)
  51. os.Exit(21)
  52. }
  53. err = bridge.Config.Save(*configPath)
  54. if err != nil {
  55. fmt.Fprintln(os.Stderr, "Failed to save config:", err)
  56. os.Exit(22)
  57. }
  58. fmt.Println("Registration generated. Add the path to the registration to your Synapse config, restart it, then start the bridge.")
  59. os.Exit(0)
  60. }
  61. func (bridge *Bridge) MigrateDatabase() {
  62. oldDB, err := database.New(flag.Arg(0), flag.Arg(1))
  63. if err != nil {
  64. fmt.Println("Failed to open old database:", err)
  65. os.Exit(30)
  66. }
  67. err = oldDB.Init()
  68. if err != nil {
  69. fmt.Println("Failed to upgrade old database:", err)
  70. os.Exit(31)
  71. }
  72. newDB, err := database.New(bridge.Config.AppService.Database.Type, bridge.Config.AppService.Database.URI)
  73. if err != nil {
  74. bridge.Log.Fatalln("Failed to open new database:", err)
  75. os.Exit(32)
  76. }
  77. err = newDB.Init()
  78. if err != nil {
  79. fmt.Println("Failed to upgrade new database:", err)
  80. os.Exit(33)
  81. }
  82. database.Migrate(oldDB, newDB)
  83. }
  84. type Bridge struct {
  85. AS *appservice.AppService
  86. EventProcessor *appservice.EventProcessor
  87. MatrixHandler *MatrixHandler
  88. Config *config.Config
  89. DB *database.Database
  90. Log log.Logger
  91. StateStore *database.SQLStateStore
  92. Provisioning *ProvisioningAPI
  93. Bot *appservice.IntentAPI
  94. Formatter *Formatter
  95. Relaybot *User
  96. Crypto Crypto
  97. usersByMXID map[id.UserID]*User
  98. usersByJID map[types.WhatsAppID]*User
  99. usersLock sync.Mutex
  100. managementRooms map[id.RoomID]*User
  101. managementRoomsLock sync.Mutex
  102. portalsByMXID map[id.RoomID]*Portal
  103. portalsByJID map[database.PortalKey]*Portal
  104. portalsLock sync.Mutex
  105. puppets map[types.WhatsAppID]*Puppet
  106. puppetsByCustomMXID map[id.UserID]*Puppet
  107. puppetsLock sync.Mutex
  108. }
  109. type Crypto interface {
  110. HandleMemberEvent(*event.Event)
  111. Decrypt(*event.Event) (*event.Event, error)
  112. Encrypt(id.RoomID, event.Type, event.Content) (*event.EncryptedEventContent, error)
  113. Init() error
  114. Start()
  115. Stop()
  116. }
  117. func NewBridge() *Bridge {
  118. bridge := &Bridge{
  119. usersByMXID: make(map[id.UserID]*User),
  120. usersByJID: make(map[types.WhatsAppID]*User),
  121. managementRooms: make(map[id.RoomID]*User),
  122. portalsByMXID: make(map[id.RoomID]*Portal),
  123. portalsByJID: make(map[database.PortalKey]*Portal),
  124. puppets: make(map[types.WhatsAppID]*Puppet),
  125. puppetsByCustomMXID: make(map[id.UserID]*Puppet),
  126. }
  127. var err error
  128. bridge.Config, err = config.Load(*configPath)
  129. if err != nil {
  130. fmt.Fprintln(os.Stderr, "Failed to load config:", err)
  131. os.Exit(10)
  132. }
  133. return bridge
  134. }
  135. func (bridge *Bridge) ensureConnection() {
  136. for {
  137. resp, err := bridge.Bot.Whoami()
  138. if err != nil {
  139. if httpErr, ok := err.(mautrix.HTTPError); ok && httpErr.RespError != nil && httpErr.RespError.ErrCode == "M_UNKNOWN_ACCESS_TOKEN" {
  140. bridge.Log.Fatalln("Access token invalid. Is the registration installed in your homeserver correctly?")
  141. os.Exit(16)
  142. }
  143. bridge.Log.Errorfln("Failed to connect to homeserver: %v. Retrying in 10 seconds...", err)
  144. time.Sleep(10 * time.Second)
  145. } else if resp.UserID != bridge.Bot.UserID {
  146. bridge.Log.Fatalln("Unexpected user ID in whoami call: got %s, expected %s", resp.UserID, bridge.Bot.UserID)
  147. os.Exit(17)
  148. } else {
  149. break
  150. }
  151. }
  152. }
  153. func (bridge *Bridge) Init() {
  154. var err error
  155. bridge.AS, err = bridge.Config.MakeAppService()
  156. if err != nil {
  157. _, _ = fmt.Fprintln(os.Stderr, "Failed to initialize AppService:", err)
  158. os.Exit(11)
  159. }
  160. _, _ = bridge.AS.Init()
  161. bridge.Bot = bridge.AS.BotIntent()
  162. bridge.Log = log.Create()
  163. bridge.Config.Logging.Configure(bridge.Log)
  164. log.DefaultLogger = bridge.Log.(*log.BasicLogger)
  165. if len(bridge.Config.Logging.FileNameFormat) > 0 {
  166. err = log.OpenFile()
  167. if err != nil {
  168. _, _ = fmt.Fprintln(os.Stderr, "Failed to open log file:", err)
  169. os.Exit(12)
  170. }
  171. }
  172. bridge.AS.Log = log.Sub("Matrix")
  173. bridge.Log.Debugln("Initializing database")
  174. bridge.DB, err = database.New(bridge.Config.AppService.Database.Type, bridge.Config.AppService.Database.URI)
  175. if err != nil && (err != upgrades.UnsupportedDatabaseVersion || !*ignoreUnsupportedDatabase) {
  176. bridge.Log.Fatalln("Failed to initialize database:", err)
  177. os.Exit(14)
  178. }
  179. if len(bridge.Config.AppService.StateStore) > 0 && bridge.Config.AppService.StateStore != "./mx-state.json" {
  180. version, err := upgrades.GetVersion(bridge.DB.DB)
  181. if version < 0 && err == nil {
  182. bridge.Log.Fatalln("Non-standard state store path. Please move the state store to ./mx-state.json " +
  183. "and update the config. The state store will be migrated into the db on the next launch.")
  184. os.Exit(18)
  185. }
  186. }
  187. bridge.Log.Debugln("Initializing state store")
  188. bridge.StateStore = database.NewSQLStateStore(bridge.DB)
  189. bridge.AS.StateStore = bridge.StateStore
  190. bridge.DB.SetMaxOpenConns(bridge.Config.AppService.Database.MaxOpenConns)
  191. bridge.DB.SetMaxIdleConns(bridge.Config.AppService.Database.MaxIdleConns)
  192. ss := bridge.Config.AppService.Provisioning.SharedSecret
  193. if len(ss) > 0 && ss != "disable" {
  194. bridge.Provisioning = &ProvisioningAPI{bridge: bridge}
  195. }
  196. bridge.Log.Debugln("Initializing Matrix event processor")
  197. bridge.EventProcessor = appservice.NewEventProcessor(bridge.AS)
  198. bridge.Log.Debugln("Initializing Matrix event handler")
  199. bridge.MatrixHandler = NewMatrixHandler(bridge)
  200. bridge.Formatter = NewFormatter(bridge)
  201. bridge.Crypto = NewCryptoHelper(bridge)
  202. }
  203. func (bridge *Bridge) Start() {
  204. err := bridge.DB.Init()
  205. if err != nil {
  206. bridge.Log.Fatalln("Failed to initialize database:", err)
  207. os.Exit(15)
  208. }
  209. if bridge.Crypto != nil {
  210. err := bridge.Crypto.Init()
  211. if err != nil {
  212. bridge.Log.Fatalln("Error initializing end-to-bridge encryption:", err)
  213. os.Exit(19)
  214. }
  215. }
  216. if bridge.Provisioning != nil {
  217. bridge.Log.Debugln("Initializing provisioning API")
  218. bridge.Provisioning.Init()
  219. }
  220. bridge.LoadRelaybot()
  221. bridge.Log.Debugln("Checking connection to homeserver")
  222. bridge.ensureConnection()
  223. bridge.Log.Debugln("Starting application service HTTP server")
  224. go bridge.AS.Start()
  225. bridge.Log.Debugln("Starting event processor")
  226. go bridge.EventProcessor.Start()
  227. go bridge.UpdateBotProfile()
  228. go bridge.Crypto.Start()
  229. go bridge.StartUsers()
  230. }
  231. func (bridge *Bridge) LoadRelaybot() {
  232. if !bridge.Config.Bridge.Relaybot.Enabled {
  233. return
  234. }
  235. bridge.Relaybot = bridge.GetUserByMXID("relaybot")
  236. if bridge.Relaybot.HasSession() {
  237. bridge.Log.Debugln("Relaybot is enabled")
  238. } else {
  239. bridge.Log.Debugln("Relaybot is enabled, but not logged in")
  240. }
  241. bridge.Relaybot.ManagementRoom = bridge.Config.Bridge.Relaybot.ManagementRoom
  242. bridge.Relaybot.IsRelaybot = true
  243. bridge.Relaybot.Connect(false)
  244. }
  245. func (bridge *Bridge) UpdateBotProfile() {
  246. bridge.Log.Debugln("Updating bot profile")
  247. botConfig := bridge.Config.AppService.Bot
  248. var err error
  249. var mxc id.ContentURI
  250. if botConfig.Avatar == "remove" {
  251. err = bridge.Bot.SetAvatarURL(mxc)
  252. } else if len(botConfig.Avatar) > 0 {
  253. mxc, err = id.ParseContentURI(botConfig.Avatar)
  254. if err == nil {
  255. err = bridge.Bot.SetAvatarURL(mxc)
  256. }
  257. }
  258. if err != nil {
  259. bridge.Log.Warnln("Failed to update bot avatar:", err)
  260. }
  261. if botConfig.Displayname == "remove" {
  262. err = bridge.Bot.SetDisplayName("")
  263. } else if len(botConfig.Avatar) > 0 {
  264. err = bridge.Bot.SetDisplayName(botConfig.Displayname)
  265. }
  266. if err != nil {
  267. bridge.Log.Warnln("Failed to update bot displayname:", err)
  268. }
  269. }
  270. func (bridge *Bridge) StartUsers() {
  271. bridge.Log.Debugln("Starting users")
  272. for _, user := range bridge.GetAllUsers() {
  273. go user.Connect(false)
  274. }
  275. bridge.Log.Debugln("Starting custom puppets")
  276. for _, loopuppet := range bridge.GetAllPuppetsWithCustomMXID() {
  277. go func(puppet *Puppet) {
  278. puppet.log.Debugln("Starting custom puppet", puppet.CustomMXID)
  279. err := puppet.StartCustomMXID()
  280. if err != nil {
  281. puppet.log.Errorln("Failed to start custom puppet:", err)
  282. }
  283. }(loopuppet)
  284. }
  285. }
  286. func (bridge *Bridge) Stop() {
  287. bridge.Crypto.Stop()
  288. bridge.AS.Stop()
  289. bridge.EventProcessor.Stop()
  290. for _, user := range bridge.usersByJID {
  291. if user.Conn == nil {
  292. continue
  293. }
  294. bridge.Log.Debugln("Disconnecting", user.MXID)
  295. sess, err := user.Conn.Disconnect()
  296. if err != nil {
  297. bridge.Log.Errorfln("Error while disconnecting %s: %v", user.MXID, err)
  298. } else if len(sess.Wid) > 0 {
  299. user.SetSession(&sess)
  300. }
  301. }
  302. }
  303. func (bridge *Bridge) Main() {
  304. if *generateRegistration {
  305. bridge.GenerateRegistration()
  306. return
  307. } else if *migrateFrom {
  308. bridge.MigrateDatabase()
  309. return
  310. }
  311. bridge.Init()
  312. bridge.Log.Infoln("Bridge initialization complete, starting...")
  313. bridge.Start()
  314. bridge.Log.Infoln("Bridge started!")
  315. c := make(chan os.Signal)
  316. signal.Notify(c, os.Interrupt, syscall.SIGTERM)
  317. <-c
  318. bridge.Log.Infoln("Interrupt received, stopping...")
  319. bridge.Stop()
  320. bridge.Log.Infoln("Bridge stopped.")
  321. os.Exit(0)
  322. }
  323. func main() {
  324. flag.SetHelpTitles(
  325. "mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.",
  326. "mautrix-whatsapp [-h] [-c <path>] [-r <path>] [-g] [--migrate-db <source type> <source uri>]")
  327. err := flag.Parse()
  328. if err != nil {
  329. fmt.Fprintln(os.Stderr, err)
  330. flag.PrintHelp()
  331. os.Exit(1)
  332. } else if *wantHelp {
  333. flag.PrintHelp()
  334. os.Exit(0)
  335. }
  336. NewBridge().Main()
  337. }