diff --git a/cmd/signaldctl/cmd/group/leave/leave-group.go b/cmd/signaldctl/cmd/group/leave/leave-group.go new file mode 100644 index 0000000..7d59b88 --- /dev/null +++ b/cmd/signaldctl/cmd/group/leave/leave-group.go @@ -0,0 +1,88 @@ +// Copyright © 2021 Finn Herzfeld +// +// 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 . + +package leave + +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" + "gitlab.com/signald/signald-go/signald/client-protocol/v1" +) + +var ( + account string + LeaveGroupCmd = &cobra.Command{ + Use: "leave ", + Short: "leave a group (or decline an invitation to a group)", + PreRun: func(cmd *cobra.Command, args []string) { + if account == "" { + account = config.Config.DefaultAccount + } + if account == "" { + log.Fatal("No account specified. Please specify with --account or set a default") + } + if len(args) == 0 { + common.Must(cmd.Help()) + log.Fatal("must specify a group ID") + } + }, + Run: func(_ *cobra.Command, args []string) { + go common.Signald.Listen(nil) + req := v1.LeaveGroupRequest{Account: account, GroupID: args[0]} + resp, err := req.Submit(common.Signald) + if err != nil { + log.Fatal(err, "error communicating with signald") + } + + switch common.OutputFormat { + case common.OutputFormatJSON: + err := json.NewEncoder(os.Stdout).Encode(resp) + if err != nil { + log.Fatal(err, "error encoding response to stdout") + } + case common.OutputFormatYAML: + err := yaml.NewEncoder(os.Stdout).Encode(resp) + if err != nil { + log.Fatal(err, "error encoding response to stdout") + } + case common.OutputFormatCSV, common.OutputFormatTable, common.OutputFormatDefault: + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.AppendHeader(table.Row{"ID", "Title", "Members"}) + t.AppendRow(table.Row{resp.V2.ID, resp.V2.Title, len(resp.V2.Members)}) + if common.OutputFormat == common.OutputFormatCSV { + t.RenderCSV() + } else { + common.StylizeTable(t) + t.Render() + } + default: + log.Fatal("Unsupported output format") + } + }, + } +) + +func init() { + AcceptGroupInvitationCmd.Flags().StringVarP(&account, "account", "a", "", "the signald account to use") +} diff --git a/protocol.json b/protocol.json index ec08625..c2a3c7d 100644 --- a/protocol.json +++ b/protocol.json @@ -2,9 +2,9 @@ "doc_version": "v1", "version": { "name": "signald", - "version": "0.12.0+git2021-02-08r98148393.17", + "version": "0.12.0+git2021-02-12r1f18d539.24", "branch": "main", - "commit": "9814839376b0aceed8329fecc7477a5da7d0cae8" + "commit": "1f18d539bb0d09ea51948e9559ab5e04ca9359a5" }, "info": "This document describes objects that may be used when communicating with signald.", "types": { @@ -34,7 +34,7 @@ }, "timestamp": { "type": "long", - "example": "1612746778552" + "example": "1613092537706" }, "timestampISO": { "type": "String" @@ -44,7 +44,7 @@ }, "serverDeliveredTimestamp": { "type": "long", - "example": "161274677855280" + "example": "161309253770680" }, "hasLegacyMessage": { "type": "boolean" @@ -124,7 +124,7 @@ }, "timestamp": { "type": "long", - "example": "1612746778552" + "example": "1613092537706" } } }, @@ -165,7 +165,7 @@ }, "version": { "type": "String", - "example": "\"0.12.0+git2021-02-08r98148393.17\"" + "example": "\"0.12.0+git2021-02-12r1f18d539.24\"" }, "branch": { "type": "String", @@ -173,7 +173,7 @@ }, "commit": { "type": "String", - "example": "\"9814839376b0aceed8329fecc7477a5da7d0cae8\"" + "example": "\"1f18d539bb0d09ea51948e9559ab5e04ca9359a5\"" } } }, @@ -517,7 +517,7 @@ "list": true, "type": "Long", "doc": "List of messages to mark as read", - "example": "1612746778552", + "example": "1613092537706", "required": true } } @@ -620,12 +620,61 @@ } } }, + "CreateGroupRequest": { + "fields": { + "account": { + "type": "String", + "doc": "The account to interact with", + "example": "\"+12024561414\"", + "required": true + }, + "title": { + "type": "String", + "example": "\"Parkdale Run Club\"", + "required": true + }, + "avatar": { + "type": "String", + "example": "\"/tmp/image.jpg\"" + }, + "members": { + "list": true, + "type": "JsonAddress", + "version": "v1" + }, + "timer": { + "type": "int", + "doc": "the message expiration timer" + }, + "member_role": { + "type": "String", + "doc": "The role of all members other than the group creator. Options are ADMINISTRATOR or DEFAULT (case insensitive)", + "example": "\"ADMINISTRATOR\"" + } + } + }, + "LeaveGroupRequest": { + "fields": { + "account": { + "type": "String", + "doc": "The account to use", + "example": "\"+12024561414\"", + "required": true + }, + "groupID": { + "type": "String", + "doc": "The group to leave", + "example": "\"EdSqI90cS0UomDpgUXOlCoObWvQOXlH5G3Z2d3f4ayE=\"", + "required": true + } + } + }, "JsonDataMessage": { "fields": { "timestamp": { "type": "long", "doc": "the timestamp that the message was sent at, according to the sender's device. This is used to uniquely identify this message for things like reactions and quotes.", - "example": "1612746778552" + "example": "1613092537706" }, "attachments": { "list": true, @@ -762,7 +811,7 @@ "id": { "type": "long", "doc": "the client timestamp of the message being quoted", - "example": "1612746778552" + "example": "1613092537706" }, "author": { "type": "JsonAddress", @@ -850,7 +899,7 @@ "targetSentTimestamp": { "type": "long", "doc": "the client timestamp of the message being reacted to", - "example": "1612746778552" + "example": "1613092537706" } } }, @@ -930,7 +979,7 @@ }, "timestamp": { "type": "long", - "example": "1612746778552" + "example": "1613092537706" }, "expirationStartTimestamp": { "type": "long" @@ -968,7 +1017,7 @@ }, "timestamp": { "type": "long", - "example": "1612746778552" + "example": "1613092537706" } } }, @@ -980,7 +1029,7 @@ }, "timestamp": { "type": "long", - "example": "1612746778552" + "example": "1613092537706" } } }, @@ -1746,6 +1795,14 @@ "list_contacts": { "request": "ListContactsRequest", "response": "ProfileList" + }, + "create_group": { + "request": "CreateGroupRequest", + "response": "JsonGroupV2Info" + }, + "leave_group": { + "request": "LeaveGroupRequest", + "response": "GroupInfo" } }, "v1alpha1": { diff --git a/signald/client-protocol/v1/requests.go b/signald/client-protocol/v1/requests.go index ba78fe1..ca9a239 100644 --- a/signald/client-protocol/v1/requests.go +++ b/signald/client-protocol/v1/requests.go @@ -76,6 +76,38 @@ func (r *ApproveMembershipRequest) Submit(conn *signald.Signald) (response JsonG } +func (r *CreateGroupRequest) Submit(conn *signald.Signald) (response JsonGroupV2Info, err error) { + r.Version = "v1" + r.Type = "create_group" + 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: Query the server for the latest state of a known group func (r *GetGroupRequest) Submit(conn *signald.Signald) (response JsonGroupV2Info, err error) { r.Version = "v1" @@ -208,6 +240,38 @@ func (r *JoinGroupRequest) Submit(conn *signald.Signald) (response JsonGroupJoin } +func (r *LeaveGroupRequest) Submit(conn *signald.Signald) (response GroupInfo, err error) { + r.Version = "v1" + r.Type = "leave_group" + 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 *ListContactsRequest) Submit(conn *signald.Signald) (response ProfileList, err error) { r.Version = "v1" r.Type = "list_contacts" diff --git a/signald/client-protocol/v1/structs.go b/signald/client-protocol/v1/structs.go index f4e6e5d..3551ef2 100644 --- a/signald/client-protocol/v1/structs.go +++ b/signald/client-protocol/v1/structs.go @@ -33,6 +33,16 @@ type Capabilities struct { Storage bool `json:"storage,omitempty" yaml:"storage,omitempty"` } +type CreateGroupRequest struct { + Request + Account string `json:"account,omitempty" yaml:"account,omitempty"` // The account to interact with + Avatar string `json:"avatar,omitempty" yaml:"avatar,omitempty"` + Member_role string `json:"member_role,omitempty" yaml:"member_role,omitempty"` // The role of all members other than the group creator. Options are ADMINISTRATOR or DEFAULT (case insensitive) + Members []*JsonAddress `json:"members,omitempty" yaml:"members,omitempty"` + Timer int32 `json:"timer,omitempty" yaml:"timer,omitempty"` // the message expiration timer + Title string `json:"title,omitempty" yaml:"title,omitempty"` +} + // GetGroupRequest: Query the server for the latest state of a known group type GetGroupRequest struct { Request @@ -254,6 +264,12 @@ type JsonViewOnceOpenMessage struct { Timestamp int64 `json:"timestamp,omitempty" yaml:"timestamp,omitempty"` } +type LeaveGroupRequest struct { + Request + Account string `json:"account,omitempty" yaml:"account,omitempty"` // The account to use + GroupID string `json:"groupID,omitempty" yaml:"groupID,omitempty"` // The group to leave +} + type LinkedDevices struct { Devices []*v0.DeviceInfo `json:"devices,omitempty" yaml:"devices,omitempty"` }