diff --git a/src/api/api.go b/src/api/api.go index becf036..547cf04 100644 --- a/src/api/api.go +++ b/src/api/api.go @@ -220,6 +220,14 @@ type RemoteDeleteRequest struct { Timestamp int64 `json:"timestamp"` } +type DeleteLocalAccountDataRequest struct { + IgnoreRegistered bool `json:"ignore_registered" example:"false"` +} + +type DeviceLinkUriResponse struct { + DeviceLinkUri string `json:"device_link_uri"` +} + type Api struct { signalClient *client.SignalClient wsMutex sync.Mutex @@ -328,6 +336,43 @@ func (a *Api) UnregisterNumber(c *gin.Context) { c.Writer.WriteHeader(204) } +// @Summary Delete local account data +// @Tags Devices +// @Description Delete all local data for the specified account. Only use this after unregistering the account or after removing a linked device. +// @Accept json +// @Produce json +// @Param number path string true "Registered Phone Number" +// @Param data body DeleteLocalAccountDataRequest false "Cleanup options" +// @Success 204 +// @Failure 400 {object} Error +// @Router /v1/devices/{number}/local-data [delete] +func (a *Api) DeleteLocalAccountData(c *gin.Context) { + number, err := url.PathUnescape(c.Param("number")) + if err != nil { + c.JSON(400, Error{Msg: "Couldn't process request - malformed number"}) + return + } + if number == "" { + c.JSON(400, Error{Msg: "Couldn't process request - number missing"}) + return + } + + req := DeleteLocalAccountDataRequest{} + if c.Request.Body != nil && c.Request.ContentLength != 0 { + if err := c.BindJSON(&req); err != nil { + c.JSON(400, Error{Msg: "Couldn't process request - invalid request"}) + return + } + } + + if err := a.signalClient.DeleteLocalAccountData(number, req.IgnoreRegistered); err != nil { + c.JSON(400, Error{Msg: err.Error()}) + return + } + + c.Status(http.StatusNoContent) +} + // @Summary Verify a registered phone number. // @Tags Devices // @Description Verify a registered phone number with the signal network. @@ -1103,6 +1148,30 @@ func (a *Api) GetQrCodeLink(c *gin.Context) { c.Data(200, "image/png", png) } +// @Summary Get raw device link URI +// @Tags Devices +// @Description Generate the deviceLinkUri string for linking without scanning a QR code. +// @Produce json +// @Param device_name query string true "Device Name" +// @Success 200 {object} DeviceLinkUriResponse +// @Failure 400 {object} Error +// @Router /v1/qrcodelink/raw [get] +func (a *Api) GetQrCodeLinkUri(c *gin.Context) { + deviceName := c.Query("device_name") + if deviceName == "" { + c.JSON(400, Error{Msg: "Please provide a name for the device"}) + return + } + + deviceLinkUri, err := a.signalClient.GetDeviceLinkUri(deviceName) + if err != nil { + c.JSON(400, Error{Msg: err.Error()}) + return + } + + c.JSON(200, DeviceLinkUriResponse{DeviceLinkUri: deviceLinkUri}) +} + // @Summary List all accounts // @Tags Accounts // @Description Lists all of the accounts linked or registered @@ -1974,6 +2043,40 @@ func (a *Api) ListDevices(c *gin.Context) { c.JSON(200, devices) } +// @Summary Remove linked device +// @Tags Devices +// @Description Remove a linked device from the primary account. +// @Param number path string true "Registered Phone Number" +// @Param deviceId path int true "Device ID from listDevices" +// @Success 204 +// @Failure 400 {object} Error +// @Router /v1/devices/{number}/{deviceId} [delete] +func (a *Api) RemoveDevice(c *gin.Context) { + number, err := url.PathUnescape(c.Param("number")) + if err != nil { + c.JSON(400, Error{Msg: "Couldn't process request - malformed number"}) + return + } + if number == "" { + c.JSON(400, Error{Msg: "Couldn't process request - number missing"}) + return + } + + deviceIdStr := c.Param("deviceId") + deviceId, err := strconv.ParseInt(deviceIdStr, 10, 64) + if err != nil { + c.JSON(400, Error{Msg: "deviceId must be numeric"}) + return + } + + err = a.signalClient.RemoveDevice(number, deviceId) + if err != nil { + c.JSON(400, Error{Msg: err.Error()}) + return + } + c.Status(http.StatusNoContent) +} + // @Summary Set account specific settings. // @Tags General // @Description Set account specific settings. diff --git a/src/client/client.go b/src/client/client.go index 379bff4..d6558b2 100644 --- a/src/client/client.go +++ b/src/client/client.go @@ -232,6 +232,7 @@ type ListContactsResponse struct { } type ListDevicesResponse struct { + Id int64 `json:"id"` Name string `json:"name"` LastSeenTimestamp int64 `json:"last_seen_timestamp"` CreationTimestamp int64 `json:"creation_timestamp"` @@ -783,6 +784,32 @@ func (s *SignalClient) UnregisterNumber(number string, deleteAccount bool, delet return err } +func (s *SignalClient) DeleteLocalAccountData(number string, ignoreRegistered bool) error { + if s.signalCliMode == JsonRpc { + type Request struct { + IgnoreRegistered bool `json:"ignore-registered,omitempty"` + } + req := Request{} + if ignoreRegistered { + req.IgnoreRegistered = true + } + + jsonRpc2Client, err := s.getJsonRpc2Client() + if err != nil { + return err + } + _, err = jsonRpc2Client.getRaw("deleteLocalAccountData", &number, req) + return err + } else { + cmd := []string{"--config", s.signalCliConfig, "-a", number, "deleteLocalAccountData"} + if ignoreRegistered { + cmd = append(cmd, "--ignore-registered") + } + _, err := s.cliClient.Execute(true, cmd, "") + return err + } +} + func (s *SignalClient) VerifyRegisteredNumber(number string, token string, pin string) error { if s.signalCliMode == JsonRpc { type Request struct { @@ -1463,25 +1490,7 @@ func (s *SignalClient) GetQrCodeLink(deviceName string, qrCodeVersion int) ([]by return []byte{}, errors.New("Couldn't create QR code: " + err.Error()) } - go (func() { - type FinishRequest struct { - DeviceLinkUri string `json:"deviceLinkUri"` - DeviceName string `json:"deviceName"` - } - - req := FinishRequest{ - DeviceLinkUri: resp.DeviceLinkUri, - DeviceName: deviceName, - } - - result, err := jsonRpc2Client.getRaw("finishLink", nil, &req) - if err != nil { - log.Debug("Error linking device: ", err.Error()) - return - } - log.Debug("Linking device result: ", result) - s.signalCliApiConfig.Load(s.signalCliApiConfigPath) - })() + s.finishLinkAsync(jsonRpc2Client, deviceName, resp.DeviceLinkUri) return png, nil } @@ -1506,6 +1515,57 @@ func (s *SignalClient) GetQrCodeLink(deviceName string, qrCodeVersion int) ([]by return png, nil } +func (s *SignalClient) GetDeviceLinkUri(deviceName string) (string, error) { + if s.signalCliMode == JsonRpc { + type StartResponse struct { + DeviceLinkUri string `json:"deviceLinkUri"` + } + jsonRpc2Client, err := s.getJsonRpc2Client() + if err != nil { + return "", err + } + + raw, err := jsonRpc2Client.getRaw("startLink", nil, struct{}{}) + if err != nil { + return "", errors.New("Couldn't start link: " + err.Error()) + } + + var resp StartResponse + if err := json.Unmarshal([]byte(raw), &resp); err != nil { + return "", errors.New("Couldn't parse startLink response: " + err.Error()) + } + + // Complete the linking handshake in the background, just like GetQrCodeLink does. + s.finishLinkAsync(jsonRpc2Client, deviceName, resp.DeviceLinkUri) + return resp.DeviceLinkUri, nil + } + + cmd := []string{"--config", s.signalCliConfig, "link", "-n", deviceName} + deviceLinkUri, err := s.cliClient.Execute(false, cmd, "") + if err != nil { + return "", errors.New("Couldn't create link URI: " + err.Error()) + } + return strings.TrimSpace(deviceLinkUri), nil +} + +func (s *SignalClient) finishLinkAsync(jsonRpc2Client *JsonRpc2Client, deviceName string, deviceLinkUri string) { + type finishRequest struct { + DeviceLinkUri string `json:"deviceLinkUri"` + DeviceName string `json:"deviceName"` + } + + go func() { + req := finishRequest{DeviceLinkUri: deviceLinkUri, DeviceName: deviceName} + result, err := jsonRpc2Client.getRaw("finishLink", nil, &req) + if err != nil { + log.Debug("Error linking device: ", err.Error()) + return + } + log.Debug("Linking device result: ", result) + s.signalCliApiConfig.Load(s.signalCliApiConfigPath) + }() +} + func (s *SignalClient) GetAccounts() ([]string, error) { accounts := make([]string, 0) var rawData string @@ -2285,6 +2345,7 @@ func (s *SignalClient) ListDevices(number string) ([]ListDevicesResponse, error) for _, entry := range signalCliResp { deviceEntry := ListDevicesResponse{ + Id: entry.Id, Name: entry.Name, CreationTimestamp: entry.CreatedTimestamp, LastSeenTimestamp: entry.LastSeenTimestamp, @@ -2295,6 +2356,25 @@ func (s *SignalClient) ListDevices(number string) ([]ListDevicesResponse, error) return resp, nil } +func (s *SignalClient) RemoveDevice(number string, deviceId int64) error { + var err error + if s.signalCliMode == JsonRpc { + type Request struct { + DeviceId int64 `json:"deviceId"` + } + request := Request{DeviceId: deviceId} + jsonRpc2Client, err := s.getJsonRpc2Client() + if err != nil { + return err + } + _, err = jsonRpc2Client.getRaw("removeDevice", &number, request) + } else { + cmd := []string{"--config", s.signalCliConfig, "-a", number, "removeDevice", "--deviceId", strconv.FormatInt(deviceId, 10)} + _, err = s.cliClient.Execute(true, cmd, "") + } + return err +} + func (s *SignalClient) SetTrustMode(number string, trustMode utils.SignalCliTrustMode) error { if s.signalCliMode == JsonRpc { return errors.New("Not supported in json-rpc mode, use the environment variable JSON_RPC_TRUST_NEW_IDENTITIES instead!") diff --git a/src/main.go b/src/main.go index 0303960..4fb0d89 100644 --- a/src/main.go +++ b/src/main.go @@ -230,6 +230,7 @@ func main() { link := v1.Group("qrcodelink") { link.GET("", api.GetQrCodeLink) + link.GET("/raw", api.GetQrCodeLinkUri) } accounts := v1.Group("accounts") @@ -247,6 +248,8 @@ func main() { { 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")