package main import ( "encoding/json" "flag" "io/ioutil" "net/http" "os" "plugin" "strconv" "github.com/bbernhard/signal-cli-rest-api/api" "github.com/bbernhard/signal-cli-rest-api/client" docs "github.com/bbernhard/signal-cli-rest-api/docs" "github.com/bbernhard/signal-cli-rest-api/utils" "github.com/gin-gonic/gin" "github.com/robfig/cron/v3" log "github.com/sirupsen/logrus" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" ) // @title Signal Cli REST API // @version 1.0 // @description This is the Signal Cli REST API documentation. // @tag.name General // @tag.description Some general endpoints. // @tag.name Devices // @tag.description Register and link Devices. // @tag.name Accounts // @tag.description List registered and linked accounts // @tag.name Groups // @tag.description Create, List and Delete Signal Groups. // @tag.name Messages // @tag.description Send and Receive Signal Messages. // @tag.name Attachments // @tag.description List and Delete Attachments. // @tag.name Profiles // @tag.description Update Profile. // @tag.name Identities // @tag.description List and Trust Identities. // @tag.name Reactions // @tag.description React to messages. // @tag.name Receipts // @tag.description Send receipts for messages. // @tag.name Search // @tag.description Search the Signal Service. // @tag.name Sticker Packs // @tag.description List and Install Sticker Packs // @host localhost:8080 // @schemes http // @BasePath / func main() { signalCliConfig := flag.String("signal-cli-config", "/home/.local/share/signal-cli/", "Config directory where signal-cli config is stored") attachmentTmpDir := flag.String("attachment-tmp-dir", "/tmp/", "Attachment tmp directory") avatarTmpDir := flag.String("avatar-tmp-dir", "/tmp/", "Avatar tmp directory") flag.Parse() logLevel := utils.GetEnv("LOG_LEVEL", "") if logLevel != "" { err := utils.SetLogLevel(logLevel) if err != nil { log.Error("Couldn't set log level to '", logLevel, "'. Falling back to the info log level") utils.SetLogLevel("info") } } if utils.GetEnv("SWAGGER_USE_HTTPS_AS_PREFERRED_SCHEME", "false") == "false" { docs.SwaggerInfo.Schemes = []string{"http", "https"} } else { docs.SwaggerInfo.Schemes = []string{"https", "http"} } router := gin.New() router.Use(gin.LoggerWithConfig(gin.LoggerConfig{ SkipPaths: []string{"/v1/health"}, //do not log the health requests (to avoid spamming the log file) })) router.Use(gin.Recovery()) port := utils.GetEnv("PORT", "8080") if _, err := strconv.Atoi(port); err != nil { log.Fatal("Invalid PORT ", port, " set. PORT needs to be a number") } defaultSwaggerIp := utils.GetEnv("HOST_IP", "127.0.0.1") swaggerIp := utils.GetEnv("SWAGGER_IP", defaultSwaggerIp) swaggerHost := utils.GetEnv("SWAGGER_HOST", swaggerIp+":"+port) docs.SwaggerInfo.Host = swaggerHost log.Info("Started Signal Messenger REST API") supportsSignalCliNative := "0" if _, err := os.Stat("/usr/bin/signal-cli-native"); err == nil { supportsSignalCliNative = "1" } err := os.Setenv("SUPPORTS_NATIVE", supportsSignalCliNative) if err != nil { log.Fatal("Couldn't set env variable: ", err.Error()) } useNative := utils.GetEnv("USE_NATIVE", "") if useNative != "" { log.Warning("The env variable USE_NATIVE is deprecated. Please use the env variable MODE instead") } signalCliMode := client.Normal mode := utils.GetEnv("MODE", "normal") if mode == "normal" { signalCliMode = client.Normal } else if mode == "json-rpc" { signalCliMode = client.JsonRpc } else if mode == "native" { signalCliMode = client.Native } if useNative != "" { _, modeEnvVariableSet := os.LookupEnv("MODE") if modeEnvVariableSet { log.Fatal("You have both the USE_NATIVE and the MODE env variable set. Please remove the deprecated env variable USE_NATIVE!") } } if useNative == "1" || signalCliMode == client.Native { if supportsSignalCliNative == "0" { log.Error("signal-cli-native is not support on this system...falling back to signal-cli") signalCliMode = client.Normal } } if signalCliMode == client.JsonRpc { _, autoReceiveScheduleEnvVariableSet := os.LookupEnv("AUTO_RECEIVE_SCHEDULE") if autoReceiveScheduleEnvVariableSet { log.Fatal("Env variable AUTO_RECEIVE_SCHEDULE can't be used with mode json-rpc") } _, signalCliCommandTimeoutEnvVariableSet := os.LookupEnv("SIGNAL_CLI_CMD_TIMEOUT") if signalCliCommandTimeoutEnvVariableSet { log.Fatal("Env variable SIGNAL_CLI_CMD_TIMEOUT can't be used with mode json-rpc") } } webhookUrl := utils.GetEnv("RECEIVE_WEBHOOK_URL", "") if webhookUrl != "" && signalCliMode != client.JsonRpc { log.Fatal("Env variable RECEIVE_WEBHOOK_URL can only be used with mode json-rpc!") } jsonRpc2ClientConfigPathPath := *signalCliConfig + "/jsonrpc2.yml" signalCliApiConfigPath := *signalCliConfig + "/api-config.yml" signalClient := client.NewSignalClient(*signalCliConfig, *attachmentTmpDir, *avatarTmpDir, signalCliMode, jsonRpc2ClientConfigPathPath, signalCliApiConfigPath, webhookUrl) err = signalClient.Init(15) if err != nil { log.Fatal("Couldn't init Signal Client: ", err.Error()) } api := api.NewApi(signalClient) v1 := router.Group("/v1") { about := v1.Group("/about") { about.GET("", api.About) } configuration := v1.Group("/configuration") { configuration.GET("", api.GetConfiguration) configuration.POST("", api.SetConfiguration) configuration.POST(":number/settings", api.SetTrustMode) configuration.GET(":number/settings", api.GetTrustMode) } health := v1.Group("/health") { health.GET("", api.Health) } register := v1.Group("/register") { register.POST(":number", api.RegisterNumber) register.POST(":number/verify/:token", api.VerifyRegisteredNumber) } unregister := v1.Group("unregister") { unregister.POST(":number", api.UnregisterNumber) } sendV1 := v1.Group("/send") { sendV1.POST("", api.Send) } receive := v1.Group("/receive") { receive.GET(":number", api.Receive) } groups := v1.Group("/groups") { groups.POST(":number", api.CreateGroup) groups.GET(":number", api.GetGroups) groups.GET(":number/:groupid", api.GetGroup) groups.GET(":number/:groupid/avatar", api.GetGroupAvatar) groups.DELETE(":number/:groupid", api.DeleteGroup) groups.POST(":number/:groupid/block", api.BlockGroup) groups.POST(":number/:groupid/join", api.JoinGroup) groups.POST(":number/:groupid/quit", api.QuitGroup) groups.PUT(":number/:groupid", api.UpdateGroup) groups.POST(":number/:groupid/members", api.AddMembersToGroup) groups.DELETE(":number/:groupid/members", api.RemoveMembersFromGroup) groups.POST(":number/:groupid/admins", api.AddAdminsToGroup) groups.DELETE(":number/:groupid/admins", api.RemoveAdminsFromGroup) } link := v1.Group("qrcodelink") { link.GET("", api.GetQrCodeLink) link.GET("/raw", api.GetQrCodeLinkUri) } accounts := v1.Group("accounts") { accounts.GET("", api.GetAccounts) accounts.POST(":number/rate-limit-challenge", api.SubmitRateLimitChallenge) accounts.PUT(":number/settings", api.UpdateAccountSettings) accounts.POST(":number/username", api.SetUsername) accounts.DELETE(":number/username", api.RemoveUsername) accounts.POST(":number/pin", api.SetPin) accounts.DELETE(":number/pin", api.RemovePin) } devices := v1.Group("devices") { devices.POST(":number", api.AddDevice) devices.GET(":number", api.ListDevices) devices.DELETE(":number/:deviceId", api.RemoveDevice) devices.DELETE(":number/local-data", api.DeleteLocalAccountData) } attachments := v1.Group("attachments") { attachments.GET("", api.GetAttachments) attachments.DELETE(":attachment", api.RemoveAttachment) attachments.GET(":attachment", api.ServeAttachment) } stickerPacks := v1.Group("sticker-packs") { stickerPacks.GET(":number", api.ListInstalledStickerPacks) stickerPacks.POST(":number", api.AddStickerPack) } profiles := v1.Group("profiles") { profiles.PUT(":number", api.UpdateProfile) } identities := v1.Group("identities") { identities.GET(":number", api.ListIdentities) identities.PUT(":number/trust/:numbertotrust", api.TrustIdentity) } typingIndicator := v1.Group("typing-indicator") { typingIndicator.PUT(":number", api.SendStartTyping) typingIndicator.DELETE(":number", api.SendStopTyping) } remoteDelete := v1.Group("remote-delete") { remoteDelete.DELETE(":number", api.RemoteDelete) } reactions := v1.Group("/reactions") { reactions.POST(":number", api.SendReaction) reactions.DELETE(":number", api.RemoveReaction) } receipts := v1.Group("/receipts") { receipts.POST(":number", api.SendReceipt) } search := v1.Group("/search") { search.GET("", api.SearchForNumbers) search.GET(":number", api.SearchForNumbers) } contacts := v1.Group("/contacts") { contacts.GET(":number", api.ListContacts) contacts.PUT(":number", api.UpdateContact) contacts.GET(":number/:uuid", api.ListContact) contacts.GET(":number/:uuid/avatar", api.GetProfileAvatar) contacts.POST(":number/sync", api.SendContacts) } polls := v1.Group("/polls") { polls.POST(":number", api.CreatePoll) polls.POST(":number/vote", api.VoteInPoll) polls.DELETE(":number", api.ClosePoll) } if utils.GetEnv("ENABLE_PLUGINS", "false") == "true" { signalCliRestApiPluginSharedObjDir := utils.GetEnv("SIGNAL_CLI_REST_API_PLUGIN_SHARED_OBJ_DIR", "") sharedObj, err := plugin.Open(signalCliRestApiPluginSharedObjDir + "signal-cli-rest-api_plugin_loader.so") if err != nil { log.Fatal("Couldn't load shared object: ", err) } pluginHandlerSymbol, err := sharedObj.Lookup("PluginHandler") if err != nil { log.Fatal("Couldn't get PluginHandler: ", err) } pluginHandler, ok := pluginHandlerSymbol.(utils.PluginHandler) if !ok { log.Fatal("Couldn't cast PluginHandler") } plugins := v1.Group("/plugins") { pluginConfigs := utils.NewPluginConfigs() err := pluginConfigs.Load("/plugins") if err != nil { log.Fatal("Couldn't load plugin configs: ", err.Error()) } for _, pluginConfig := range pluginConfigs.Configs { if pluginConfig.Method == "GET" { plugins.GET(pluginConfig.Endpoint, pluginHandler.ExecutePlugin(pluginConfig)) } else if pluginConfig.Method == "POST" { plugins.POST(pluginConfig.Endpoint, pluginHandler.ExecutePlugin(pluginConfig)) } else if pluginConfig.Method == "DELETE" { plugins.DELETE(pluginConfig.Endpoint, pluginHandler.ExecutePlugin(pluginConfig)) } else if pluginConfig.Method == "PUT" { plugins.PUT(pluginConfig.Endpoint, pluginHandler.ExecutePlugin(pluginConfig)) } } } } } v2 := router.Group("/v2") { sendV2 := v2.Group("/send") { sendV2.POST("", api.SendV2) } } protocol := "http" if utils.GetEnv("SWAGGER_USE_HTTPS_AS_PREFERRED_SCHEME", "false") == "true" { protocol = "https" } swaggerUrl := ginSwagger.URL(protocol + "://" + swaggerHost + "/swagger/doc.json") router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, swaggerUrl)) autoReceiveSchedule := utils.GetEnv("AUTO_RECEIVE_SCHEDULE", "") if autoReceiveSchedule != "" { p := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) schedule, err := p.Parse(autoReceiveSchedule) if err != nil { log.Fatal("AUTO_RECEIVE_SCHEDULE: Invalid schedule: ", err.Error()) } type SignalCliAccountConfig struct { Number string `json:"number"` } type SignalCliAccountConfigs struct { Accounts []SignalCliAccountConfig `json:"accounts"` } autoReceiveScheduleReceiveTimeout := utils.GetEnv("AUTO_RECEIVE_SCHEDULE_RECEIVE_TIMEOUT", "10") autoReceiveScheduleIgnoreAttachments := utils.GetEnv("AUTO_RECEIVE_SCHEDULE_IGNORE_ATTACHMENTS", "false") autoReceiveScheduleIgnoreStories := utils.GetEnv("AUTO_RECEIVE_SCHEDULE_IGNORE_STORIES", "false") autoReceiveScheduleIgnoreAvatars := utils.GetEnv("AUTO_RECEIVE_SCHEDULE_IGNORE_AVATARS", "false") autoReceiveScheduleIgnoreStickers := utils.GetEnv("AUTO_RECEIVE_SCHEDULE_IGNORE_STICKERS", "false") autoReceiveScheduleSendReadReceipts := utils.GetEnv("AUTO_RECEIVE_SCHEDULE_SEND_READ_RECEIPTS", "false") c := cron.New() c.Schedule(schedule, cron.FuncJob(func() { accountsJsonPath := *signalCliConfig + "/data/accounts.json" if _, err := os.Stat(accountsJsonPath); err == nil { signalCliConfigJsonData, err := ioutil.ReadFile(accountsJsonPath) if err != nil { log.Fatal("AUTO_RECEIVE_SCHEDULE: Couldn't read accounts.json: ", err.Error()) } var signalCliAccountConfigs SignalCliAccountConfigs err = json.Unmarshal(signalCliConfigJsonData, &signalCliAccountConfigs) if err != nil { log.Fatal("AUTO_RECEIVE_SCHEDULE: Couldn't parse accounts.json: ", err.Error()) } for _, account := range signalCliAccountConfigs.Accounts { client := &http.Client{} log.Debug("AUTO_RECEIVE_SCHEDULE: Calling receive for number ", account.Number) req, err := http.NewRequest("GET", "http://127.0.0.1:"+port+"/v1/receive/"+account.Number, nil) if err != nil { log.Error("AUTO_RECEIVE_SCHEDULE: Couldn't call receive for number ", account.Number, ": ", err.Error()) } q := req.URL.Query() q.Add("timeout", autoReceiveScheduleReceiveTimeout) q.Add("ignore_attachments", autoReceiveScheduleIgnoreAttachments) q.Add("ignore_stories", autoReceiveScheduleIgnoreStories) q.Add("ignore_avatars", autoReceiveScheduleIgnoreAvatars) q.Add("ignore_stickers", autoReceiveScheduleIgnoreStickers) q.Add("send_read_receipts", autoReceiveScheduleSendReadReceipts) req.URL.RawQuery = q.Encode() resp, err := client.Do(req) if err != nil { log.Error("AUTO_RECEIVE_SCHEDULE: Couldn't call receive for number ", account.Number, ": ", err.Error()) } if resp.StatusCode != 200 { jsonResp, err := ioutil.ReadAll(resp.Body) resp.Body.Close() if err != nil { log.Error("AUTO_RECEIVE_SCHEDULE: Couldn't read json response: ", err.Error()) continue } type ReceiveResponse struct { Error string `json:"error"` } var receiveResponse ReceiveResponse err = json.Unmarshal(jsonResp, &receiveResponse) if err != nil { log.Error("AUTO_RECEIVE_SCHEDULE: Couldn't parse json response: ", err.Error()) continue } log.Error("AUTO_RECEIVE_SCHEDULE: Couldn't call receive for number ", account.Number, ": ", receiveResponse) } } } else { log.Info("AUTO_RECEIVE_SCHEDULE: accounts.json doesn't exist") } })) c.Start() } router.Run() }