Update protocol and add support for remote-config to signaldctl

This commit is contained in:
Finn 2021-08-27 02:54:09 -07:00
parent ec3d46fcf3
commit 096152394a
5 changed files with 320 additions and 40 deletions

View file

@ -0,0 +1,82 @@
// Copyright © 2021 Finn Herzfeld <finn@janky.solutions>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package remoteconfig
import (
"encoding/json"
"log"
"os"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
"gopkg.in/yaml.v2"
"gitlab.com/signald/signald-go/cmd/signaldctl/common"
"gitlab.com/signald/signald-go/cmd/signaldctl/config"
v1 "gitlab.com/signald/signald-go/signald/client-protocol/v1"
)
var (
account string
RemoteConfigCmd = &cobra.Command{
Use: "remote-config",
Short: "return a list of accounts",
PreRun: func(cmd *cobra.Command, args []string) {
if account == "" {
account = config.Config.DefaultAccount
}
},
Run: func(_ *cobra.Command, _ []string) {
go common.Signald.Listen(nil)
req := &v1.RemoteConfigRequest{Account: account}
remoteconfig, err := req.Submit(common.Signald)
if err != nil {
log.Fatal(err)
}
switch common.OutputFormat {
case common.OutputFormatJSON:
err := json.NewEncoder(os.Stdout).Encode(remoteconfig.Config)
if err != nil {
log.Fatal("error encoding response to stdout:", err)
}
case common.OutputFormatYAML:
err := yaml.NewEncoder(os.Stdout).Encode(remoteconfig.Config)
if err != nil {
log.Fatal("error encoding response to stdout:", err)
}
case common.OutputFormatCSV, common.OutputFormatTable, common.OutputFormatDefault:
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Key", "Value"})
for _, config := range remoteconfig.Config {
t.AppendRow(table.Row{config.Name, config.Value})
}
if common.OutputFormat == common.OutputFormatCSV {
t.RenderCSV()
} else {
common.StylizeTable(t)
t.Render()
}
default:
log.Fatal("Unsupported output format")
}
},
}
)
func init() {
RemoteConfigCmd.Flags().StringVarP(&account, "account", "a", "", "the signald account to use")
}

View file

@ -22,6 +22,7 @@ import (
"gitlab.com/signald/signald-go/cmd/signaldctl/cmd/account/link"
"gitlab.com/signald/signald-go/cmd/signaldctl/cmd/account/list"
"gitlab.com/signald/signald-go/cmd/signaldctl/cmd/account/register"
"gitlab.com/signald/signald-go/cmd/signaldctl/cmd/account/remoteconfig"
"gitlab.com/signald/signald-go/cmd/signaldctl/cmd/account/setprofile"
"gitlab.com/signald/signald-go/cmd/signaldctl/cmd/account/verify"
)
@ -38,4 +39,5 @@ func init() {
AccountCmd.AddCommand(register.RegisterAccountCmd)
AccountCmd.AddCommand(verify.VerifyAccountCmd)
AccountCmd.AddCommand(setprofile.SetProfileCmd)
AccountCmd.AddCommand(remoteconfig.RemoteConfigCmd)
}

View file

@ -2,9 +2,9 @@
"doc_version": "v1",
"version": {
"name": "signald",
"version": "0.13.1+git2021-07-09rabe585d6.42",
"version": "0.14.1+git2021-08-27r6dd96a17.28",
"branch": "main",
"commit": "abe585d68fdb00b440c1b0c517e1ece2bc095ba3"
"commit": "6dd96a17c921766b0d284165e571476744ac2829"
},
"info": "This document describes objects that may be used when communicating with signald.",
"types": {
@ -256,7 +256,7 @@
},
"version": {
"type": "String",
"example": "\"0.13.1+git2021-07-09rabe585d6.42\""
"example": "\"0.14.1+git2021-08-27r6dd96a17.28\""
},
"branch": {
"type": "String",
@ -264,7 +264,7 @@
},
"commit": {
"type": "String",
"example": "\"abe585d68fdb00b440c1b0c517e1ece2bc095ba3\""
"example": "\"6dd96a17c921766b0d284165e571476744ac2829\""
}
}
},
@ -439,19 +439,26 @@
"type": "String",
"example": "\"Parkdale Run Club\""
},
"description": {
"type": "String",
"example": "\"A club for running in Parkdale\""
},
"memberCount": {
"type": "int",
"example": "3"
},
"addFromInviteLink": {
"type": "int"
"type": "int",
"doc": "The access level required in order to join the group from the invite link, as an AccessControl.AccessRequired enum from the upstream Signal groups.proto file. This is UNSATISFIABLE (4) when the group link is disabled; ADMINISTRATOR (3) when the group link is enabled, but an administrator must approve new members; and ANY (1) when the group link is enabled and no approval is required. See theGroupAccessControl structure and the upstream enum ordinals."
},
"revision": {
"type": "int",
"doc": "The Group V2 revision. This is incremented by clients whenever they update group information, and it is often used by clients to determine if the local group state is out-of-date with the server's revision.",
"example": "5"
},
"pendingAdminApproval": {
"type": "boolean"
"type": "boolean",
"doc": "Whether the account is waiting for admin approval in order to be added to the group."
}
}
},
@ -490,6 +497,11 @@
"type": "String",
"example": "\"Parkdale Run Club\""
},
"description": {
"type": "String",
"doc": "A new group description. Set to empty string to remove an existing description.",
"example": "\"A club for running in Parkdale\""
},
"avatar": {
"type": "String",
"example": "\"/tmp/image.jpg\""
@ -522,7 +534,7 @@
"doc": "regenerate the group link password, invalidating the old one"
}
},
"doc": "modify a group. Note that only one modification action may be preformed at once"
"doc": "modify a group. Note that only one modification action may be performed at once"
},
"GroupInfo": {
"fields": {
@ -553,14 +565,20 @@
},
"avatarFile": {
"type": "String",
"doc": "Path to new profile avatar file, if the avatar should be updated",
"doc": "Path to new profile avatar file. If unset or null, unset the profile avatar",
"example": "\"/tmp/image.jpg\""
},
"about": {
"type": "String"
"type": "String",
"doc": "an optional about string. If unset, null or an empty string will unset profile about field"
},
"emoji": {
"type": "String"
"type": "String",
"doc": "an optional single emoji character. If unset, null or an empty string will unset profile emoji"
},
"mobilecoin_address": {
"type": "String",
"doc": "an optional *base64-encoded* MobileCoin address to set in the profile. Note that this is not the traditional MobileCoin address encoding, which is custom. Clients are responsible for converting between MobileCoin's custom base58 on the user-facing side and base64 encoding on the signald side. If unset, null or an empty string, will empty the profile payment address"
}
}
},
@ -680,6 +698,10 @@
},
"expiration_time": {
"type": "int"
},
"mobilecoin_address": {
"type": "String",
"doc": "*base64-encoded* mobilecoin address. Note that this is not the traditional MobileCoin address encoding. Clients are responsible for converting between MobileCoin's custom base58 on the user-facing side and base64 encoding on the signald side. If unset, null or an empty string, will empty the profile payment address"
}
},
"doc": "Information about a Signal user"
@ -1221,6 +1243,51 @@
}
}
},
"SendPaymentRequest": {
"fields": {
"account": {
"type": "String",
"doc": "the account to use",
"example": "\"+12024561414\"",
"required": true
},
"address": {
"type": "JsonAddress",
"version": "v1",
"doc": "the address to send the payment message to",
"required": true
},
"payment": {
"type": "Payment",
"version": "v1",
"required": true
},
"when": {
"type": "Long"
}
},
"doc": "send a mobilecoin payment"
},
"RemoteConfigRequest": {
"fields": {
"account": {
"type": "String",
"doc": "The account to use to retrieve the remote config",
"example": "\"+12024561414\"",
"required": true
}
},
"doc": "Retrieves the remote config (feature flags) from the server."
},
"RemoteConfigList": {
"fields": {
"config": {
"list": true,
"type": "RemoteConfig",
"version": "v1"
}
}
},
"JsonDataMessage": {
"fields": {
"timestamp": {
@ -1667,13 +1734,6 @@
},
"doc": "a Signal server"
},
"RemoteDelete": {
"fields": {
"target_sent_timestamp": {
"type": "long"
}
}
},
"Payment": {
"fields": {
"receipt": {
@ -1687,6 +1747,28 @@
},
"doc": "details about a MobileCoin payment"
},
"RemoteConfig": {
"fields": {
"name": {
"type": "String",
"doc": "The name of this remote config entry. These names may be prefixed with the platform type (\"android.\", \"ios.\", \"desktop.\", etc.) Typically, clients only handle the relevant configs for its platform, hardcoding the names it cares about handling and ignoring the rest.",
"example": "desktop.mediaQuality.levels"
},
"value": {
"type": "String",
"doc": "The value for this remote config entry. Even though this is a string, it could be a boolean as a string, an integer/long value, a comma-delimited list, etc. Clients usually consume this by hardcoding the feature flagsit should track in the app and assuming that the server will send the type that the client expects. If an unexpected type occurs, it falls back to a default value.",
"example": "1:2,61:2,81:2,82:2,65:2,31:2,47:2,41:2,32:2,385:2,971:2,974:2,49:2,33:2,*:1"
}
},
"doc": "A remote config (feature flag) entry."
},
"RemoteDelete": {
"fields": {
"target_sent_timestamp": {
"type": "long"
}
}
},
"JsonSentTranscriptMessage": {
"fields": {
"destination": {
@ -1846,7 +1928,14 @@
}
},
"ServerCDN": {
"fields": {}
"fields": {
"number": {
"type": "int"
},
"url": {
"type": "String"
}
}
}
},
"v0": {
@ -2826,7 +2915,7 @@
"update_group": {
"request": "UpdateGroupRequest",
"response": "GroupInfo",
"doc": "modify a group. Note that only one modification action may be preformed at once"
"doc": "modify a group. Note that only one modification action may be performed at once"
},
"set_profile": {
"request": "SetProfile"
@ -2963,6 +3052,16 @@
},
"delete_server": {
"request": "RemoveServerRequest"
},
"send_payment": {
"request": "SendPaymentRequest",
"response": "SendResponse",
"doc": "send a mobilecoin payment"
},
"get_remote_config": {
"request": "RemoteConfigRequest",
"response": "RemoteConfigList",
"doc": "Retrieves the remote config (feature flags) from the server."
}
}
}

View file

@ -449,6 +449,39 @@ func (r *GetProfileRequest) Submit(conn *signald.Signald) (response Profile, err
}
// Submit: Retrieves the remote config (feature flags) from the server.
func (r *RemoteConfigRequest) Submit(conn *signald.Signald) (response RemoteConfigList, err error) {
r.Version = "v1"
r.Type = "get_remote_config"
if r.ID == "" {
r.ID = signald.GenerateID()
}
err = conn.RawRequest(r)
if err != nil {
log.Println("signald-go: error submitting request to signald")
return
}
responseChannel := conn.GetResponseListener(r.ID)
defer conn.CloseResponseListener(r.ID)
rawResponse := <-responseChannel
if rawResponse.Error != nil {
err = fmt.Errorf("signald error: %s", string(rawResponse.Error))
return
}
err = json.Unmarshal(rawResponse.Data, &response)
if err != nil {
rawResponseJson, _ := rawResponse.Data.MarshalJSON()
log.Println("signald-go: error unmarshalling response from signald of type", rawResponse.Type, string(rawResponseJson))
return
}
return response, nil
}
func (r *GetServersRequest) Submit(conn *signald.Signald) (response ServerList, err error) {
r.Version = "v1"
r.Type = "get_servers"
@ -950,6 +983,39 @@ func (r *SendRequest) Submit(conn *signald.Signald) (response SendResponse, err
}
// Submit: send a mobilecoin payment
func (r *SendPaymentRequest) Submit(conn *signald.Signald) (response SendResponse, err error) {
r.Version = "v1"
r.Type = "send_payment"
if r.ID == "" {
r.ID = signald.GenerateID()
}
err = conn.RawRequest(r)
if err != nil {
log.Println("signald-go: error submitting request to signald")
return
}
responseChannel := conn.GetResponseListener(r.ID)
defer conn.CloseResponseListener(r.ID)
rawResponse := <-responseChannel
if rawResponse.Error != nil {
err = fmt.Errorf("signald error: %s", string(rawResponse.Error))
return
}
err = json.Unmarshal(rawResponse.Data, &response)
if err != nil {
rawResponseJson, _ := rawResponse.Data.MarshalJSON()
log.Println("signald-go: error unmarshalling response from signald of type", rawResponse.Type, string(rawResponseJson))
return
}
return response, nil
}
// Submit: set this device's name. This will show up on the mobile device on the same account under
func (r *SetDeviceNameRequest) Submit(conn *signald.Signald) (err error) {
r.Version = "v1"
@ -1171,7 +1237,7 @@ func (r *UpdateContactRequest) Submit(conn *signald.Signald) (response Profile,
}
// Submit: modify a group. Note that only one modification action may be preformed at once
// Submit: modify a group. Note that only one modification action may be performed at once
func (r *UpdateGroupRequest) Submit(conn *signald.Signald) (response GroupInfo, err error) {
r.Version = "v1"
r.Type = "update_group"

View file

@ -290,11 +290,12 @@ type JsonGroupInfo struct {
}
type JsonGroupJoinInfo struct {
AddFromInviteLink int32 `json:"addFromInviteLink,omitempty" yaml:"addFromInviteLink,omitempty"`
AddFromInviteLink int32 `json:"addFromInviteLink,omitempty" yaml:"addFromInviteLink,omitempty"` // The access level required in order to join the group from the invite link, as an AccessControl.AccessRequired enum from the upstream Signal groups.proto file. This is UNSATISFIABLE (4) when the group link is disabled; ADMINISTRATOR (3) when the group link is enabled, but an administrator must approve new members; and ANY (1) when the group link is enabled and no approval is required. See theGroupAccessControl structure and the upstream enum ordinals.
Description string `json:"description,omitempty" yaml:"description,omitempty"`
GroupID string `json:"groupID,omitempty" yaml:"groupID,omitempty"`
MemberCount int32 `json:"memberCount,omitempty" yaml:"memberCount,omitempty"`
PendingAdminApproval bool `json:"pendingAdminApproval,omitempty" yaml:"pendingAdminApproval,omitempty"`
Revision int32 `json:"revision,omitempty" yaml:"revision,omitempty"`
PendingAdminApproval bool `json:"pendingAdminApproval,omitempty" yaml:"pendingAdminApproval,omitempty"` // Whether the account is waiting for admin approval in order to be added to the group.
Revision int32 `json:"revision,omitempty" yaml:"revision,omitempty"` // The Group V2 revision. This is incremented by clients whenever they update group information, and it is often used by clients to determine if the local group state is out-of-date with the server's revision.
Title string `json:"title,omitempty" yaml:"title,omitempty"`
}
@ -480,16 +481,17 @@ type Payment struct {
// Profile: Information about a Signal user
type Profile struct {
About string `json:"about,omitempty" yaml:"about,omitempty"`
Address *JsonAddress `json:"address,omitempty" yaml:"address,omitempty"`
Avatar string `json:"avatar,omitempty" yaml:"avatar,omitempty"` // path to avatar on local disk
Capabilities *Capabilities `json:"capabilities,omitempty" yaml:"capabilities,omitempty"`
Color string `json:"color,omitempty" yaml:"color,omitempty"` // color of the chat with this user
Emoji string `json:"emoji,omitempty" yaml:"emoji,omitempty"`
ExpirationTime int32 `json:"expiration_time,omitempty" yaml:"expiration_time,omitempty"`
InboxPosition int32 `json:"inbox_position,omitempty" yaml:"inbox_position,omitempty"`
Name string `json:"name,omitempty" yaml:"name,omitempty"` // The user's name from local contact names if available, or if not in contact list their Signal profile name
ProfileName string `json:"profile_name,omitempty" yaml:"profile_name,omitempty"` // The user's Signal profile name
About string `json:"about,omitempty" yaml:"about,omitempty"`
Address *JsonAddress `json:"address,omitempty" yaml:"address,omitempty"`
Avatar string `json:"avatar,omitempty" yaml:"avatar,omitempty"` // path to avatar on local disk
Capabilities *Capabilities `json:"capabilities,omitempty" yaml:"capabilities,omitempty"`
Color string `json:"color,omitempty" yaml:"color,omitempty"` // color of the chat with this user
Emoji string `json:"emoji,omitempty" yaml:"emoji,omitempty"`
ExpirationTime int32 `json:"expiration_time,omitempty" yaml:"expiration_time,omitempty"`
InboxPosition int32 `json:"inbox_position,omitempty" yaml:"inbox_position,omitempty"`
MobilecoinAddress string `json:"mobilecoin_address,omitempty" yaml:"mobilecoin_address,omitempty"` // *base64-encoded* mobilecoin address. Note that this is not the traditional MobileCoin address encoding. Clients are responsible for converting between MobileCoin's custom base58 on the user-facing side and base64 encoding on the signald side. If unset, null or an empty string, will empty the profile payment address
Name string `json:"name,omitempty" yaml:"name,omitempty"` // The user's name from local contact names if available, or if not in contact list their Signal profile name
ProfileName string `json:"profile_name,omitempty" yaml:"profile_name,omitempty"` // The user's Signal profile name
}
type ProfileList struct {
@ -521,6 +523,22 @@ type RegisterRequest struct {
Voice bool `json:"voice,omitempty" yaml:"voice,omitempty"` // set to true to request a voice call instead of an SMS for verification
}
// RemoteConfig: A remote config (feature flag) entry.
type RemoteConfig struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"` // The name of this remote config entry. These names may be prefixed with the platform type ("android.", "ios.", "desktop.", etc.) Typically, clients only handle the relevant configs for its platform, hardcoding the names it cares about handling and ignoring the rest.
Value string `json:"value,omitempty" yaml:"value,omitempty"` // The value for this remote config entry. Even though this is a string, it could be a boolean as a string, an integer/long value, a comma-delimited list, etc. Clients usually consume this by hardcoding the feature flagsit should track in the app and assuming that the server will send the type that the client expects. If an unexpected type occurs, it falls back to a default value.
}
type RemoteConfigList struct {
Config []*RemoteConfig `json:"config,omitempty" yaml:"config,omitempty"`
}
// RemoteConfigRequest: Retrieves the remote config (feature flags) from the server.
type RemoteConfigRequest struct {
Request
Account string `json:"account,omitempty" yaml:"account,omitempty"` // The account to use to retrieve the remote config
}
type RemoteDelete struct {
TargetSentTimestamp int64 `json:"target_sent_timestamp,omitempty" yaml:"target_sent_timestamp,omitempty"`
}
@ -571,6 +589,15 @@ type ResolveAddressRequest struct {
Partial *JsonAddress `json:"partial,omitempty" yaml:"partial,omitempty"` // The partial address, missing fields
}
// SendPaymentRequest: send a mobilecoin payment
type SendPaymentRequest struct {
Request
Account string `json:"account,omitempty" yaml:"account,omitempty"` // the account to use
Address *JsonAddress `json:"address,omitempty" yaml:"address,omitempty"` // the address to send the payment message to
Payment *Payment `json:"payment,omitempty" yaml:"payment,omitempty"`
When int64 `json:"when,omitempty" yaml:"when,omitempty"`
}
type SendRequest struct {
Request
Attachments []*v0.JsonAttachment `json:"attachments,omitempty" yaml:"attachments,omitempty"`
@ -603,6 +630,8 @@ type Server struct {
}
type ServerCDN struct {
Number int32 `json:"number,omitempty" yaml:"number,omitempty"`
Url string `json:"url,omitempty" yaml:"url,omitempty"`
}
type ServerList struct {
@ -627,11 +656,12 @@ type SetExpirationRequest struct {
type SetProfile struct {
Request
About string `json:"about,omitempty" yaml:"about,omitempty"`
Account string `json:"account,omitempty" yaml:"account,omitempty"` // The phone number of the account to use
AvatarFile string `json:"avatarFile,omitempty" yaml:"avatarFile,omitempty"` // Path to new profile avatar file, if the avatar should be updated
Emoji string `json:"emoji,omitempty" yaml:"emoji,omitempty"`
Name string `json:"name,omitempty" yaml:"name,omitempty"` // New profile name. Set to empty string for no profile name
About string `json:"about,omitempty" yaml:"about,omitempty"` // an optional about string. If unset, null or an empty string will unset profile about field
Account string `json:"account,omitempty" yaml:"account,omitempty"` // The phone number of the account to use
AvatarFile string `json:"avatarFile,omitempty" yaml:"avatarFile,omitempty"` // Path to new profile avatar file. If unset or null, unset the profile avatar
Emoji string `json:"emoji,omitempty" yaml:"emoji,omitempty"` // an optional single emoji character. If unset, null or an empty string will unset profile emoji
MobilecoinAddress string `json:"mobilecoin_address,omitempty" yaml:"mobilecoin_address,omitempty"` // an optional *base64-encoded* MobileCoin address to set in the profile. Note that this is not the traditional MobileCoin address encoding, which is custom. Clients are responsible for converting between MobileCoin's custom base58 on the user-facing side and base64 encoding on the signald side. If unset, null or an empty string, will empty the profile payment address
Name string `json:"name,omitempty" yaml:"name,omitempty"` // New profile name. Set to empty string for no profile name
}
// SubscribeRequest: receive incoming messages. After making a subscribe request, incoming messages will be sent to the client encoded as ClientMessageWrapper. Send an unsubscribe request or disconnect from the socket to stop receiving messages.
@ -682,13 +712,14 @@ type UpdateContactRequest struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
}
// UpdateGroupRequest: modify a group. Note that only one modification action may be preformed at once
// UpdateGroupRequest: modify a group. Note that only one modification action may be performed at once
type UpdateGroupRequest struct {
Request
Account string `json:"account,omitempty" yaml:"account,omitempty"` // The identifier of the account to interact with
AddMembers []*JsonAddress `json:"addMembers,omitempty" yaml:"addMembers,omitempty"`
Avatar string `json:"avatar,omitempty" yaml:"avatar,omitempty"`
GroupID string `json:"groupID,omitempty" yaml:"groupID,omitempty"` // the ID of the group to update
Description string `json:"description,omitempty" yaml:"description,omitempty"` // A new group description. Set to empty string to remove an existing description.
GroupID string `json:"groupID,omitempty" yaml:"groupID,omitempty"` // the ID of the group to update
RemoveMembers []*JsonAddress `json:"removeMembers,omitempty" yaml:"removeMembers,omitempty"`
ResetLink bool `json:"resetLink,omitempty" yaml:"resetLink,omitempty"` // regenerate the group link password, invalidating the old one
Title string `json:"title,omitempty" yaml:"title,omitempty"`