main.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  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/event"
  27. "maunium.net/go/mautrix"
  28. "maunium.net/go/mautrix-appservice"
  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. Start()
  114. Stop()
  115. }
  116. func NewBridge() *Bridge {
  117. bridge := &Bridge{
  118. usersByMXID: make(map[id.UserID]*User),
  119. usersByJID: make(map[types.WhatsAppID]*User),
  120. managementRooms: make(map[id.RoomID]*User),
  121. portalsByMXID: make(map[id.RoomID]*Portal),
  122. portalsByJID: make(map[database.PortalKey]*Portal),
  123. puppets: make(map[types.WhatsAppID]*Puppet),
  124. puppetsByCustomMXID: make(map[id.UserID]*Puppet),
  125. }
  126. var err error
  127. bridge.Config, err = config.Load(*configPath)
  128. if err != nil {
  129. fmt.Fprintln(os.Stderr, "Failed to load config:", err)
  130. os.Exit(10)
  131. }
  132. return bridge
  133. }
  134. func (bridge *Bridge) ensureConnection() {
  135. for {
  136. resp, err := bridge.Bot.Whoami()
  137. if err != nil {
  138. if httpErr, ok := err.(mautrix.HTTPError); ok && httpErr.RespError != nil && httpErr.RespError.ErrCode == "M_UNKNOWN_ACCESS_TOKEN" {
  139. bridge.Log.Fatalln("Access token invalid. Is the registration installed in your homeserver correctly?")
  140. os.Exit(16)
  141. }
  142. bridge.Log.Errorfln("Failed to connect to homeserver: %v. Retrying in 10 seconds...", err)
  143. time.Sleep(10 * time.Second)
  144. } else if resp.UserID != bridge.Bot.UserID {
  145. bridge.Log.Fatalln("Unexpected user ID in whoami call: got %s, expected %s", resp.UserID, bridge.Bot.UserID)
  146. os.Exit(17)
  147. } else {
  148. break
  149. }
  150. }
  151. }
  152. func (bridge *Bridge) Init() {
  153. var err error
  154. bridge.AS, err = bridge.Config.MakeAppService()
  155. if err != nil {
  156. _, _ = fmt.Fprintln(os.Stderr, "Failed to initialize AppService:", err)
  157. os.Exit(11)
  158. }
  159. _, _ = bridge.AS.Init()
  160. bridge.Bot = bridge.AS.BotIntent()
  161. bridge.Log = log.Create()
  162. bridge.Config.Logging.Configure(bridge.Log)
  163. log.DefaultLogger = bridge.Log.(*log.BasicLogger)
  164. if len(bridge.Config.Logging.FileNameFormat) > 0 {
  165. err = log.OpenFile()
  166. if err != nil {
  167. _, _ = fmt.Fprintln(os.Stderr, "Failed to open log file:", err)
  168. os.Exit(12)
  169. }
  170. }
  171. bridge.AS.Log = log.Sub("Matrix")
  172. bridge.Log.Debugln("Initializing database")
  173. bridge.DB, err = database.New(bridge.Config.AppService.Database.Type, bridge.Config.AppService.Database.URI)
  174. if err != nil && (err != upgrades.UnsupportedDatabaseVersion || !*ignoreUnsupportedDatabase) {
  175. bridge.Log.Fatalln("Failed to initialize database:", err)
  176. os.Exit(14)
  177. }
  178. if len(bridge.Config.AppService.StateStore) > 0 && bridge.Config.AppService.StateStore != "./mx-state.json" {
  179. version, err := upgrades.GetVersion(bridge.DB.DB)
  180. if version < 0 && err == nil {
  181. bridge.Log.Fatalln("Non-standard state store path. Please move the state store to ./mx-state.json " +
  182. "and update the config. The state store will be migrated into the db on the next launch.")
  183. os.Exit(18)
  184. }
  185. }
  186. bridge.Log.Debugln("Initializing state store")
  187. bridge.StateStore = database.NewSQLStateStore(bridge.DB)
  188. bridge.AS.StateStore = bridge.StateStore
  189. bridge.DB.SetMaxOpenConns(bridge.Config.AppService.Database.MaxOpenConns)
  190. bridge.DB.SetMaxIdleConns(bridge.Config.AppService.Database.MaxIdleConns)
  191. ss := bridge.Config.AppService.Provisioning.SharedSecret
  192. if len(ss) > 0 && ss != "disable" {
  193. bridge.Provisioning = &ProvisioningAPI{bridge: bridge}
  194. }
  195. bridge.Log.Debugln("Initializing Matrix event processor")
  196. bridge.EventProcessor = appservice.NewEventProcessor(bridge.AS)
  197. bridge.Log.Debugln("Initializing Matrix event handler")
  198. bridge.MatrixHandler = NewMatrixHandler(bridge)
  199. bridge.Formatter = NewFormatter(bridge)
  200. err = bridge.initCrypto()
  201. if err != nil {
  202. bridge.Log.Fatalln("Error initializing end-to-bridge encryption:", err)
  203. os.Exit(19)
  204. }
  205. }
  206. func (bridge *Bridge) Start() {
  207. err := bridge.DB.Init()
  208. if err != nil {
  209. bridge.Log.Fatalln("Failed to initialize database:", err)
  210. os.Exit(15)
  211. }
  212. if bridge.Provisioning != nil {
  213. bridge.Log.Debugln("Initializing provisioning API")
  214. bridge.Provisioning.Init()
  215. }
  216. bridge.LoadRelaybot()
  217. bridge.Log.Debugln("Checking connection to homeserver")
  218. bridge.ensureConnection()
  219. bridge.Log.Debugln("Starting application service HTTP server")
  220. go bridge.AS.Start()
  221. bridge.Log.Debugln("Starting event processor")
  222. go bridge.EventProcessor.Start()
  223. go bridge.UpdateBotProfile()
  224. go bridge.Crypto.Start()
  225. go bridge.StartUsers()
  226. }
  227. func (bridge *Bridge) LoadRelaybot() {
  228. if !bridge.Config.Bridge.Relaybot.Enabled {
  229. return
  230. }
  231. bridge.Relaybot = bridge.GetUserByMXID("relaybot")
  232. if bridge.Relaybot.HasSession() {
  233. bridge.Log.Debugln("Relaybot is enabled")
  234. } else {
  235. bridge.Log.Debugln("Relaybot is enabled, but not logged in")
  236. }
  237. bridge.Relaybot.ManagementRoom = bridge.Config.Bridge.Relaybot.ManagementRoom
  238. bridge.Relaybot.IsRelaybot = true
  239. bridge.Relaybot.Connect(false)
  240. }
  241. func (bridge *Bridge) UpdateBotProfile() {
  242. bridge.Log.Debugln("Updating bot profile")
  243. botConfig := bridge.Config.AppService.Bot
  244. var err error
  245. var mxc id.ContentURI
  246. if botConfig.Avatar == "remove" {
  247. err = bridge.Bot.SetAvatarURL(mxc)
  248. } else if len(botConfig.Avatar) > 0 {
  249. mxc, err = id.ParseContentURI(botConfig.Avatar)
  250. if err == nil {
  251. err = bridge.Bot.SetAvatarURL(mxc)
  252. }
  253. }
  254. if err != nil {
  255. bridge.Log.Warnln("Failed to update bot avatar:", err)
  256. }
  257. if botConfig.Displayname == "remove" {
  258. err = bridge.Bot.SetDisplayName("")
  259. } else if len(botConfig.Avatar) > 0 {
  260. err = bridge.Bot.SetDisplayName(botConfig.Displayname)
  261. }
  262. if err != nil {
  263. bridge.Log.Warnln("Failed to update bot displayname:", err)
  264. }
  265. }
  266. func (bridge *Bridge) StartUsers() {
  267. bridge.Log.Debugln("Starting users")
  268. for _, user := range bridge.GetAllUsers() {
  269. go user.Connect(false)
  270. }
  271. bridge.Log.Debugln("Starting custom puppets")
  272. for _, loopuppet := range bridge.GetAllPuppetsWithCustomMXID() {
  273. go func(puppet *Puppet) {
  274. puppet.log.Debugln("Starting custom puppet", puppet.CustomMXID)
  275. err := puppet.StartCustomMXID()
  276. if err != nil {
  277. puppet.log.Errorln("Failed to start custom puppet:", err)
  278. }
  279. }(loopuppet)
  280. }
  281. }
  282. func (bridge *Bridge) Stop() {
  283. bridge.Crypto.Stop()
  284. bridge.AS.Stop()
  285. bridge.EventProcessor.Stop()
  286. for _, user := range bridge.usersByJID {
  287. if user.Conn == nil {
  288. continue
  289. }
  290. bridge.Log.Debugln("Disconnecting", user.MXID)
  291. sess, err := user.Conn.Disconnect()
  292. if err != nil {
  293. bridge.Log.Errorfln("Error while disconnecting %s: %v", user.MXID, err)
  294. } else if len(sess.Wid) > 0 {
  295. user.SetSession(&sess)
  296. }
  297. }
  298. }
  299. func (bridge *Bridge) Main() {
  300. if *generateRegistration {
  301. bridge.GenerateRegistration()
  302. return
  303. } else if *migrateFrom {
  304. bridge.MigrateDatabase()
  305. return
  306. }
  307. bridge.Init()
  308. bridge.Log.Infoln("Bridge initialization complete, starting...")
  309. bridge.Start()
  310. bridge.Log.Infoln("Bridge started!")
  311. c := make(chan os.Signal)
  312. signal.Notify(c, os.Interrupt, syscall.SIGTERM)
  313. <-c
  314. bridge.Log.Infoln("Interrupt received, stopping...")
  315. bridge.Stop()
  316. bridge.Log.Infoln("Bridge stopped.")
  317. os.Exit(0)
  318. }
  319. func main() {
  320. flag.SetHelpTitles(
  321. "mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.",
  322. "mautrix-whatsapp [-h] [-c <path>] [-r <path>] [-g] [--migrate-db <source type> <source uri>]")
  323. err := flag.Parse()
  324. if err != nil {
  325. fmt.Fprintln(os.Stderr, err)
  326. flag.PrintHelp()
  327. os.Exit(1)
  328. } else if *wantHelp {
  329. flag.PrintHelp()
  330. os.Exit(0)
  331. }
  332. NewBridge().Main()
  333. }