mirror of
https://dev.narayana.im/narayana/telegabber.git
synced 2026-08-05 04:07:07 +00:00
Ad-Hoc mapping for /omemo commands
This commit is contained in:
parent
aa7d31a4f7
commit
40c1a8ceff
3 changed files with 334 additions and 46 deletions
|
|
@ -1,6 +1,7 @@
|
|||
package telegram
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
|
|
@ -8,73 +9,145 @@ import (
|
|||
"dev.narayana.im/narayana/telegabber/xmpp/gateway"
|
||||
)
|
||||
|
||||
// cmdOmemoChat implements the chat-scoped "/omemo" command: with no
|
||||
// arguments, reports this chat's current OMEMO state (account-wide mode,
|
||||
// any override, and the resulting effective state); "on"/"off" forces this
|
||||
// specific chat regardless of the account-wide mode (persistence.Session.OMEMO,
|
||||
// via e2ee.Mode/e2ee.ShouldEncrypt); "clear" removes that override, reverting
|
||||
// to following the account-wide mode; "devices ..." manages this chat's
|
||||
// peer's device trust (see cmdOmemoDevices).
|
||||
func (c *Client) cmdOmemoChat(chatID int64, args []string) (string, bool) {
|
||||
// OmemoDeviceStatus is one known device of a chat's peer - shared data
|
||||
// backing both the text "/omemo devices" command and the ad-hoc command's
|
||||
// form (xmpp/omemo_command.go), so both surfaces always agree.
|
||||
type OmemoDeviceStatus struct {
|
||||
ID string
|
||||
Trusted bool
|
||||
}
|
||||
|
||||
// OmemoChatStatus is a chat's current OMEMO state - shared data backing
|
||||
// both the text "/omemo" command and the ad-hoc command's form.
|
||||
type OmemoChatStatus struct {
|
||||
Mode string // account-wide mode: "on"/"auto"/"off" (e2ee.Mode.String())
|
||||
OverrideSet bool
|
||||
Override bool // meaningful only if OverrideSet
|
||||
Effective bool // e2ee.ShouldEncrypt's actual verdict right now
|
||||
Devices []OmemoDeviceStatus
|
||||
}
|
||||
|
||||
// GetOmemoChatStatus reports chatID's current OMEMO state.
|
||||
func (c *Client) GetOmemoChatStatus(chatID int64) (OmemoChatStatus, error) {
|
||||
backend, ok := gateway.E2EE.Backend()
|
||||
if !ok {
|
||||
return "OMEMO is not enabled on this transport", false
|
||||
return OmemoChatStatus{}, errors.New("OMEMO is not enabled on this transport")
|
||||
}
|
||||
owner := e2ee.OwnedPeer(c.Session.Login, gateway.CHATJID(chatID, false))
|
||||
|
||||
mode, err := e2ee.ParseMode(c.Session.OMEMO)
|
||||
if err != nil {
|
||||
return OmemoChatStatus{}, err
|
||||
}
|
||||
override, isSet, err := backend.Override(owner)
|
||||
if err != nil {
|
||||
return OmemoChatStatus{}, err
|
||||
}
|
||||
active, err := e2ee.ShouldEncrypt(backend, owner, mode)
|
||||
if err != nil {
|
||||
return OmemoChatStatus{}, err
|
||||
}
|
||||
devices, err := backend.Devices(owner, e2ee.PeerID(c.jid))
|
||||
if err != nil {
|
||||
return OmemoChatStatus{}, err
|
||||
}
|
||||
|
||||
statuses := make([]OmemoDeviceStatus, len(devices))
|
||||
for i, d := range devices {
|
||||
statuses[i] = OmemoDeviceStatus{ID: string(d.ID), Trusted: d.Trusted}
|
||||
}
|
||||
|
||||
return OmemoChatStatus{
|
||||
Mode: mode.String(),
|
||||
OverrideSet: isSet,
|
||||
Override: override,
|
||||
Effective: active,
|
||||
Devices: statuses,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetOmemoChatOverride forces (on non-nil) or clears (on nil) chatID's
|
||||
// OMEMO override.
|
||||
func (c *Client) SetOmemoChatOverride(chatID int64, on *bool) error {
|
||||
backend, ok := gateway.E2EE.Backend()
|
||||
if !ok {
|
||||
return errors.New("OMEMO is not enabled on this transport")
|
||||
}
|
||||
owner := e2ee.OwnedPeer(c.Session.Login, gateway.CHATJID(chatID, false))
|
||||
if on == nil {
|
||||
return backend.ClearOverride(owner)
|
||||
}
|
||||
return backend.SetOverride(owner, *on)
|
||||
}
|
||||
|
||||
// SetOmemoDeviceBlocked blocks or unblocks one of chatID's peer's devices.
|
||||
func (c *Client) SetOmemoDeviceBlocked(chatID int64, deviceID string, blocked bool) error {
|
||||
backend, ok := gateway.E2EE.Backend()
|
||||
if !ok {
|
||||
return errors.New("OMEMO is not enabled on this transport")
|
||||
}
|
||||
owner := e2ee.OwnedPeer(c.Session.Login, gateway.CHATJID(chatID, false))
|
||||
peer := e2ee.PeerID(c.jid)
|
||||
if blocked {
|
||||
return backend.BlockDevice(owner, peer, e2ee.DeviceID(deviceID))
|
||||
}
|
||||
return backend.UnblockDevice(owner, peer, e2ee.DeviceID(deviceID))
|
||||
}
|
||||
|
||||
// cmdOmemoChat implements the chat-scoped "/omemo" text command: with no
|
||||
// arguments, reports this chat's current OMEMO state (account-wide mode,
|
||||
// any override, and the resulting effective state); "on"/"off" forces this
|
||||
// specific chat regardless of the account-wide mode; "clear" removes that
|
||||
// override, reverting to following the account-wide mode; "devices ..."
|
||||
// manages this chat's peer's device trust (see cmdOmemoDevices). The
|
||||
// ad-hoc command surface (xmpp/omemo_command.go) offers the same controls
|
||||
// as a form, built from the same GetOmemoChatStatus/SetOmemoChatOverride/
|
||||
// SetOmemoDeviceBlocked methods this uses.
|
||||
func (c *Client) cmdOmemoChat(chatID int64, args []string) (string, bool) {
|
||||
if len(args) == 0 {
|
||||
return c.omemoChatStatus(backend, owner)
|
||||
return c.omemoChatStatusString(chatID)
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "on", "off":
|
||||
if err := backend.SetOverride(owner, args[0] == "on"); err != nil {
|
||||
on := args[0] == "on"
|
||||
if err := c.SetOmemoChatOverride(chatID, &on); err != nil {
|
||||
return err.Error(), false
|
||||
}
|
||||
return fmt.Sprintf("OMEMO forced %s for this chat", args[0]), true
|
||||
case "clear":
|
||||
if err := backend.ClearOverride(owner); err != nil {
|
||||
if err := c.SetOmemoChatOverride(chatID, nil); err != nil {
|
||||
return err.Error(), false
|
||||
}
|
||||
return "OMEMO override cleared for this chat, now following the account-wide mode", true
|
||||
case "devices":
|
||||
return c.cmdOmemoDevices(backend, owner, args[1:])
|
||||
return c.cmdOmemoDevices(chatID, args[1:])
|
||||
default:
|
||||
return "Usage: /omemo [on|off|clear|devices list|devices block <id>|devices accept <id>]", false
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) omemoChatStatus(backend e2ee.Backend, owner e2ee.PeerID) (string, bool) {
|
||||
mode, err := e2ee.ParseMode(c.Session.OMEMO)
|
||||
if err != nil {
|
||||
return err.Error(), false
|
||||
}
|
||||
override, isSet, err := backend.Override(owner)
|
||||
if err != nil {
|
||||
return err.Error(), false
|
||||
}
|
||||
active, err := e2ee.ShouldEncrypt(backend, owner, mode)
|
||||
func (c *Client) omemoChatStatusString(chatID int64) (string, bool) {
|
||||
status, err := c.GetOmemoChatStatus(chatID)
|
||||
if err != nil {
|
||||
return err.Error(), false
|
||||
}
|
||||
|
||||
overrideText := "not set (following the account-wide mode)"
|
||||
if isSet {
|
||||
overrideText = fmt.Sprintf("forced %s", onOffString(override))
|
||||
if status.OverrideSet {
|
||||
overrideText = fmt.Sprintf("forced %s", onOffString(status.Override))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Account-wide mode: %s\nThis chat's override: %s\nEffective right now: %s",
|
||||
mode, overrideText, onOffString(active)), true
|
||||
status.Mode, overrideText, onOffString(status.Effective)), true
|
||||
}
|
||||
|
||||
// cmdOmemoDevices lists, blocks, or accepts (unblocks) devices of owner's
|
||||
// chat's real peer. TOFU trust itself always stays on (a never-before-seen
|
||||
// cmdOmemoDevices lists, blocks, or accepts (unblocks) devices of chatID's
|
||||
// real peer. TOFU trust itself always stays on (a never-before-seen
|
||||
// device is always trusted automatically) - block/accept only manage a
|
||||
// manual override layered on top of that, per Backend.BlockDevice's doc
|
||||
// comment.
|
||||
func (c *Client) cmdOmemoDevices(backend e2ee.Backend, owner e2ee.PeerID, args []string) (string, bool) {
|
||||
peer := e2ee.PeerID(c.jid)
|
||||
|
||||
// manual override layered on top of that, per e2ee.Backend.BlockDevice's
|
||||
// doc comment.
|
||||
func (c *Client) cmdOmemoDevices(chatID int64, args []string) (string, bool) {
|
||||
sub := "list"
|
||||
if len(args) > 0 {
|
||||
sub = args[0]
|
||||
|
|
@ -82,15 +155,15 @@ func (c *Client) cmdOmemoDevices(backend e2ee.Backend, owner e2ee.PeerID, args [
|
|||
|
||||
switch sub {
|
||||
case "list":
|
||||
devices, err := backend.Devices(owner, peer)
|
||||
status, err := c.GetOmemoChatStatus(chatID)
|
||||
if err != nil {
|
||||
return err.Error(), false
|
||||
}
|
||||
if len(devices) == 0 {
|
||||
if len(status.Devices) == 0 {
|
||||
return "No known devices for this chat's peer yet", true
|
||||
}
|
||||
lines := make([]string, 0, len(devices))
|
||||
for _, d := range devices {
|
||||
lines := make([]string, 0, len(status.Devices))
|
||||
for _, d := range status.Devices {
|
||||
lines = append(lines, fmt.Sprintf("%s: %s", d.ID, trustString(d.Trusted)))
|
||||
}
|
||||
return strings.Join(lines, "\n"), true
|
||||
|
|
@ -98,17 +171,10 @@ func (c *Client) cmdOmemoDevices(backend e2ee.Backend, owner e2ee.PeerID, args [
|
|||
if len(args) < 2 {
|
||||
return notEnoughArguments, false
|
||||
}
|
||||
device := e2ee.DeviceID(args[1])
|
||||
var err error
|
||||
if sub == "block" {
|
||||
err = backend.BlockDevice(owner, peer, device)
|
||||
} else {
|
||||
err = backend.UnblockDevice(owner, peer, device)
|
||||
}
|
||||
if err != nil {
|
||||
if err := c.SetOmemoDeviceBlocked(chatID, args[1], sub == "block"); err != nil {
|
||||
return err.Error(), false
|
||||
}
|
||||
return fmt.Sprintf("Device %s %sed", device, sub), true
|
||||
return fmt.Sprintf("Device %s %sed", args[1], sub), true
|
||||
default:
|
||||
return "Usage: /omemo devices [list|block <id>|accept <id>]", false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1859,6 +1859,15 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command
|
|||
CommandElements: elements,
|
||||
}
|
||||
}
|
||||
} else if command.Node == "omemo" {
|
||||
session, ok := sessions[bare]
|
||||
if ok {
|
||||
if toOk {
|
||||
answer.Payload = submitOmemoChatForm(session, toId, form)
|
||||
} else {
|
||||
answer.Payload = submitOmemoTransportForm(session, form)
|
||||
}
|
||||
}
|
||||
} else if !toOk && command.Node == "loginwizard" {
|
||||
var session *telegram.Client
|
||||
answer.Payload, cancelSend, session = loginWizardPayload(bare, form, resource, command.Action)
|
||||
|
|
@ -1952,6 +1961,15 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command
|
|||
log.Debugf("field: %#v", field)
|
||||
}
|
||||
}
|
||||
} else if command.Node == "omemo" {
|
||||
session, ok := sessions[bare]
|
||||
if ok {
|
||||
if toOk {
|
||||
fields = buildOmemoChatFormFields(session, toId)
|
||||
} else {
|
||||
fields = buildOmemoTransportFormFields(session)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i, arg := range cmd.Arguments {
|
||||
var required *string
|
||||
|
|
|
|||
204
xmpp/omemo_command.go
Normal file
204
xmpp/omemo_command.go
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
package xmpp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"dev.narayana.im/narayana/telegabber/telegram"
|
||||
"dev.narayana.im/narayana/telegabber/xmpp/gateway"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
// buildOmemoTransportFormFields builds the initial (no form submitted yet)
|
||||
// field list for the transport-level "/omemo" ad-hoc command: a single
|
||||
// list-single field for the account-wide mode, pre-selected to the current
|
||||
// value. Plugs into handleSetQueryCommand's shared per-command
|
||||
// field-then-form-then-answer construction, the same way "config"'s own
|
||||
// field-building does.
|
||||
func buildOmemoTransportFormFields(session *telegram.Client) []*stanza.Field {
|
||||
mode, _ := session.Session.Get("omemo")
|
||||
|
||||
return []*stanza.Field{
|
||||
{
|
||||
Var: "mode",
|
||||
Label: "OMEMO mode",
|
||||
Type: stanza.FieldTypeListSingle,
|
||||
Options: []stanza.Option{
|
||||
{Label: "Automatic (default)", ValuesList: []string{"auto"}},
|
||||
{Label: "Always on", ValuesList: []string{"on"}},
|
||||
{Label: "Always off", ValuesList: []string{"off"}},
|
||||
},
|
||||
ValuesList: []string{mode},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// buildOmemoChatFormFields builds the initial field list for the
|
||||
// chat-scoped "/omemo" ad-hoc command: a read-only effective-status field,
|
||||
// a list-single override control, and two list-single "block"/"accept"
|
||||
// fields - each an empty-by-default choice (no selection = no action)
|
||||
// among the CURRENT opposite-state devices (block's options are the
|
||||
// currently-trusted devices, accept's are the currently-blocked ones).
|
||||
// This mirrors handleSetQueryCommand's existing mute/kick/ban/unmute/unban
|
||||
// field-building exactly (a list-single of opposite-state members with a
|
||||
// blank leading option), rather than a separate reported/item table plus
|
||||
// list-multi fields - one form field per action, same as those commands.
|
||||
func buildOmemoChatFormFields(session *telegram.Client, chatID int64) []*stanza.Field {
|
||||
status, err := session.GetOmemoChatStatus(chatID)
|
||||
if err != nil {
|
||||
return []*stanza.Field{
|
||||
{Type: stanza.FieldTypeFixed, Label: "Error", ValuesList: []string{err.Error()}},
|
||||
}
|
||||
}
|
||||
|
||||
overrideValue := ""
|
||||
if status.OverrideSet {
|
||||
overrideValue = omemoOnOff(status.Override)
|
||||
}
|
||||
|
||||
fields := []*stanza.Field{
|
||||
{
|
||||
Var: "effective",
|
||||
Label: "Effective right now",
|
||||
Type: stanza.FieldTypeFixed,
|
||||
ValuesList: []string{fmt.Sprintf("%s (account-wide mode: %s)", omemoOnOff(status.Effective), status.Mode)},
|
||||
},
|
||||
{
|
||||
Var: "override",
|
||||
Label: "Override for this chat",
|
||||
Type: stanza.FieldTypeListSingle,
|
||||
Options: []stanza.Option{
|
||||
{Label: "Follow account-wide mode", ValuesList: []string{""}},
|
||||
{Label: "Always on", ValuesList: []string{"on"}},
|
||||
{Label: "Always off", ValuesList: []string{"off"}},
|
||||
},
|
||||
ValuesList: []string{overrideValue},
|
||||
},
|
||||
}
|
||||
|
||||
// allow an empty selection ("no action"), same as mute/unmute's own
|
||||
// blank leading option
|
||||
blockOptions := []stanza.Option{{ValuesList: []string{""}}}
|
||||
acceptOptions := []stanza.Option{{ValuesList: []string{""}}}
|
||||
for _, d := range status.Devices {
|
||||
option := stanza.Option{Label: d.ID, ValuesList: []string{d.ID}}
|
||||
if d.Trusted {
|
||||
blockOptions = append(blockOptions, option)
|
||||
} else {
|
||||
acceptOptions = append(acceptOptions, option)
|
||||
}
|
||||
}
|
||||
|
||||
return append(fields,
|
||||
&stanza.Field{Var: "block", Label: "Block device", Type: stanza.FieldTypeListSingle, Options: blockOptions},
|
||||
&stanza.Field{Var: "accept", Label: "Accept (unblock) device", Type: stanza.FieldTypeListSingle, Options: acceptOptions},
|
||||
)
|
||||
}
|
||||
|
||||
// submitOmemoTransportForm applies a submitted transport-level "/omemo"
|
||||
// form.
|
||||
func submitOmemoTransportForm(session *telegram.Client, form *stanza.Form) *stanza.Command {
|
||||
var mode string
|
||||
for _, field := range form.Fields {
|
||||
if field != nil && field.Var == "mode" && len(field.ValuesList) > 0 {
|
||||
mode = field.ValuesList[0]
|
||||
}
|
||||
}
|
||||
|
||||
var note stanza.Note
|
||||
if value, err := session.Session.Set("omemo", mode); err != nil {
|
||||
note = stanza.Note{Text: err.Error(), Type: stanza.CommandNoteTypeErr}
|
||||
} else {
|
||||
gateway.DirtySessions = true
|
||||
note = stanza.Note{Text: fmt.Sprintf("omemo mode set to %s", value), Type: stanza.CommandNoteTypeInfo}
|
||||
}
|
||||
|
||||
return &stanza.Command{
|
||||
SessionId: "omemo",
|
||||
Node: "omemo",
|
||||
Status: stanza.CommandStatusCompleted,
|
||||
CommandElements: []stanza.CommandElement{¬e},
|
||||
}
|
||||
}
|
||||
|
||||
// submitOmemoChatForm applies a submitted chat-scoped "/omemo" form: the
|
||||
// override change (if submitted), then at most one block and one accept
|
||||
// (each field is a single empty-or-one-device selection, not a multi-pick
|
||||
// - see buildOmemoChatFormFields) - all in one round trip, since XEP-0050
|
||||
// single-stage commands only get one.
|
||||
func submitOmemoChatForm(session *telegram.Client, chatID int64, form *stanza.Form) *stanza.Command {
|
||||
var overrideValue string
|
||||
var overrideSubmitted bool
|
||||
var blockID, acceptID string
|
||||
for _, field := range form.Fields {
|
||||
if field == nil {
|
||||
continue
|
||||
}
|
||||
switch field.Var {
|
||||
case "override":
|
||||
overrideSubmitted = true
|
||||
if len(field.ValuesList) > 0 {
|
||||
overrideValue = field.ValuesList[0]
|
||||
}
|
||||
case "block":
|
||||
if len(field.ValuesList) > 0 {
|
||||
blockID = field.ValuesList[0]
|
||||
}
|
||||
case "accept":
|
||||
if len(field.ValuesList) > 0 {
|
||||
acceptID = field.ValuesList[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var notes []stanza.CommandElement
|
||||
if overrideSubmitted {
|
||||
switch overrideValue {
|
||||
case "":
|
||||
if err := session.SetOmemoChatOverride(chatID, nil); err != nil {
|
||||
notes = append(notes, &stanza.Note{Text: err.Error(), Type: stanza.CommandNoteTypeErr})
|
||||
} else {
|
||||
notes = append(notes, &stanza.Note{Text: "OMEMO override cleared for this chat", Type: stanza.CommandNoteTypeInfo})
|
||||
}
|
||||
case "on", "off":
|
||||
on := overrideValue == "on"
|
||||
if err := session.SetOmemoChatOverride(chatID, &on); err != nil {
|
||||
notes = append(notes, &stanza.Note{Text: err.Error(), Type: stanza.CommandNoteTypeErr})
|
||||
} else {
|
||||
notes = append(notes, &stanza.Note{Text: fmt.Sprintf("OMEMO forced %s for this chat", overrideValue), Type: stanza.CommandNoteTypeInfo})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if blockID != "" {
|
||||
if err := session.SetOmemoDeviceBlocked(chatID, blockID, true); err != nil {
|
||||
notes = append(notes, &stanza.Note{Text: fmt.Sprintf("Failed to block device %s: %v", blockID, err), Type: stanza.CommandNoteTypeErr})
|
||||
} else {
|
||||
notes = append(notes, &stanza.Note{Text: fmt.Sprintf("Device %s blocked", blockID), Type: stanza.CommandNoteTypeInfo})
|
||||
}
|
||||
}
|
||||
if acceptID != "" {
|
||||
if err := session.SetOmemoDeviceBlocked(chatID, acceptID, false); err != nil {
|
||||
notes = append(notes, &stanza.Note{Text: fmt.Sprintf("Failed to accept device %s: %v", acceptID, err), Type: stanza.CommandNoteTypeErr})
|
||||
} else {
|
||||
notes = append(notes, &stanza.Note{Text: fmt.Sprintf("Device %s accepted", acceptID), Type: stanza.CommandNoteTypeInfo})
|
||||
}
|
||||
}
|
||||
if len(notes) == 0 {
|
||||
notes = append(notes, &stanza.Note{Text: "No changes", Type: stanza.CommandNoteTypeInfo})
|
||||
}
|
||||
|
||||
return &stanza.Command{
|
||||
SessionId: "omemo",
|
||||
Node: "omemo",
|
||||
Status: stanza.CommandStatusCompleted,
|
||||
CommandElements: notes,
|
||||
}
|
||||
}
|
||||
|
||||
func omemoOnOff(b bool) string {
|
||||
if b {
|
||||
return "on"
|
||||
}
|
||||
return "off"
|
||||
}
|
||||
Loading…
Reference in a new issue