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 |devices accept ]", 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 |accept ]", false } } func onOffString(b bool) string { if b { return "on" } return "off" } func trustString(trusted bool) string { if trusted { return "trusted" } return "blocked" }