telegabber/telegram/omemo_commands.go
2026-07-30 08:52:59 -04:00

195 lines
6 KiB
Go

package telegram
import (
"errors"
"fmt"
"strings"
"dev.narayana.im/narayana/telegabber/e2ee"
"dev.narayana.im/narayana/telegabber/xmpp/gateway"
)
// 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 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.omemoChatStatusString(chatID)
}
switch args[0] {
case "on", "off":
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 := 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(chatID, args[1:])
default:
return "Usage: /omemo [on|off|clear|devices list|devices block <id>|devices accept <id>]", false
}
}
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 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",
status.Mode, overrideText, onOffString(status.Effective)), true
}
// 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 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]
}
switch sub {
case "list":
status, err := c.GetOmemoChatStatus(chatID)
if err != nil {
return err.Error(), false
}
if len(status.Devices) == 0 {
return "No known devices for this chat's peer yet", true
}
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
case "block", "accept":
if len(args) < 2 {
return notEnoughArguments, false
}
if err := c.SetOmemoDeviceBlocked(chatID, args[1], sub == "block"); err != nil {
return err.Error(), false
}
return fmt.Sprintf("Device %s %sed", args[1], sub), true
default:
return "Usage: /omemo devices [list|block <id>|accept <id>]", false
}
}
func onOffString(b bool) string {
if b {
return "on"
}
return "off"
}
func trustString(trusted bool) string {
if trusted {
return "trusted"
}
return "blocked"
}