From 0368b8cad82e5ab50a73d0d9bfedef3688837216 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 22 Mar 2025 18:11:26 -0400 Subject: [PATCH] Ad-Hoc config editor --- Makefile | 2 +- persistence/sessions.go | 22 +++- telegabber.go | 2 +- telegram/commands.go | 27 +++-- xmpp/handlers.go | 227 +++++++++++++++++++++++++++++----------- 5 files changed, 206 insertions(+), 74 deletions(-) diff --git a/Makefile b/Makefile index 80f1aff..364fa43 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" -VERSION := "v1.10.1" +VERSION := "v1.10.2" MAKEOPTS := "-j4" all: diff --git a/persistence/sessions.go b/persistence/sessions.go index a5f658a..6d5c3ae 100644 --- a/persistence/sessions.go +++ b/persistence/sessions.go @@ -51,7 +51,13 @@ type Session struct { IgnoreGroupDeletions bool `yaml:":ignoregroupdeletions"` } -var configKeys = []string{ +const ( + PropertyTypeUnknown byte = iota + PropertyTypeString + PropertyTypeBool +) + +var ConfigKeys = []string{ "timezone", "keeponline", "rawmessages", @@ -181,7 +187,7 @@ func (s *Session) ToMap() map[string]string { defer sessionsLock.Unlock() m := make(map[string]string) - for _, configKey := range configKeys { + for _, configKey := range ConfigKeys { value, _ := s.get(configKey) m[configKey] = value } @@ -266,6 +272,18 @@ func (s *Session) Set(key string, value string) (string, error) { return "", errors.New("Unknown session property") } +// PropertyType determines the property type +func PropertyType(key string) byte { + switch key { + case "timezone": + return PropertyTypeString + case "keeponline", "rawmessages", "asciiarrows", "oobmode", "carbons", "hideids", + "receipts", "nativeedits", "ignoregroupdeletions": + return PropertyTypeBool + } + return PropertyTypeUnknown +} + // TimezoneToLocation tries to convert config timezone to location func (s *Session) TimezoneToLocation() *time.Location { time, err := time.Parse("-07:00", s.Timezone) diff --git a/telegabber.go b/telegabber.go index 21334d2..724b7d3 100644 --- a/telegabber.go +++ b/telegabber.go @@ -16,7 +16,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.10.1" +var version string = "1.10.2" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/commands.go b/telegram/commands.go index 397ba91..71fd322 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -9,6 +9,7 @@ import ( "time" "unicode" + "dev.narayana.im/narayana/telegabber/persistence" "dev.narayana.im/narayana/telegabber/xmpp/gateway" log "github.com/sirupsen/logrus" @@ -108,9 +109,16 @@ var chatCommands = map[string]command{ } var transportConfigurationOptions = map[string]configurationOption{ - "timezone": configurationOption{"", "adjust timezone for Telegram user statuses (example: +02:00)"}, - "keeponline": configurationOption{"", "always keep telegram session online and rely on jabber offline messages (example: true)"}, - "rawmessages": configurationOption{"", "do not add additional info (message id, origin etc.) to incoming messages (example: true)"}, + "timezone": configurationOption{"", "adjust timezone for Telegram user statuses (example: +02:00)"}, + "keeponline": configurationOption{"", "always keep telegram session online and rely on jabber offline messages (true/false)"}, + "rawmessages": configurationOption{"", "do not add additional info (message id, origin etc.) to incoming messages (true/false)"}, + "asciiarrows": configurationOption{"", "replace some Unicode symbols with ASCII alternatives for better compatibility (true/false)"}, + "oobmode": configurationOption{"", "use XEP-0066 (OOB); pros: some modern clients won't show images without it, cons: very restricted, Tkabber would flood with popups (true/false)"}, + "carbons": configurationOption{"", "send carbons to your another clients, will turn on only if supported by the server (true/false)"}, + "hideids": configurationOption{"", "hide message IDs from message info (true/false)"}, + "receipts": configurationOption{"", "if enabled, XMPP read receipts are synced to Telegram, otherwise, messages are marked as read automatically (true/false)"}, + "nativeedits": configurationOption{"", "if possible, edit XMPP messages instead of showing Telegram edits as separate messages (true/false)"}, + "ignoregroupdeletions": configurationOption{"", "suppress message deletion messages in group chats (true/false)"}, } type command struct { @@ -222,7 +230,8 @@ func (c *Client) helpString(typ CommandType, chatId int64) string { if typ == CommandTypeTransport { str.WriteString("Configuration options\n") - for name, option := range transportConfigurationOptions { + for _, name := range persistence.ConfigKeys { + option := transportConfigurationOptions[name] str.WriteString(name) str.WriteString(" ") str.WriteString(option.arguments) @@ -461,7 +470,6 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin } case "config": if len(args) > 1 { - var msg string if gateway.MessageOutgoingPermissionVersion == 0 && args[0] == "carbons" && args[1] == "true" { return "The server did not allow to enable carbons", false } @@ -472,7 +480,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin } gateway.DirtySessions = true - return fmt.Sprintf("%s%s set to %s", msg, args[0], value), true + return fmt.Sprintf("%s set to %s", args[0], value), true } else if len(args) > 0 { value, err := c.Session.Get(args[0]) if err != nil { @@ -483,7 +491,12 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin } var entries []string - for key, value := range c.Session.ToMap() { + for _, key := range persistence.ConfigKeys { + value, err := c.Session.Get(key) + if err != nil { + log.Errorf("Achtung! Programming error in sessions with key %v", key) + continue + } entries = append(entries, fmt.Sprintf("%s is set to %s", key, value)) } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index aed08d9..dcc2079 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -877,86 +877,187 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command cmdType = telegram.CommandTypeTransport } if form != nil { - // just for the case the client messed the order somehow - sort.Slice(form.Fields, func(i int, j int) bool { - iField := form.Fields[i] - jField := form.Fields[j] - if iField != nil && jField != nil { - ii, iErr := strconv.ParseInt(iField.Var, 10, 64) - ji, jErr := strconv.ParseInt(jField.Var, 10, 64) - return iErr == nil && jErr == nil && ii < ji - } - return false - }) + if command.Node == "config" { + session, ok := sessions[bare] + if ok { + var infoStrings []string + var warnString, errString string + for _, field := range form.Fields { + if len(field.ValuesList) > 0 { + fieldValue := field.ValuesList[0] - var cmd strings.Builder - cmd.WriteString("/") - cmd.WriteString(command.Node) - for _, field := range form.Fields { - cmd.WriteString(" ") - if len(field.ValuesList) > 0 { - cmd.WriteString(field.ValuesList[0]) + if gateway.MessageOutgoingPermissionVersion == 0 && field.Var == "carbons" && fieldValue == "true" { + warnString = "The server did not allow to enable carbons" + continue + } + + // 10. In accordance with Section 3.2.2.1 of XML Schema Part 2: Datatypes, the allowable + // lexical representations for the xs:boolean datatype are the strings "0" and "false" + // for the concept 'false' and the strings "1" and "true" for the concept 'true'; + // implementations MUST support both styles of lexical representation. + if persistence.PropertyType(field.Var) == persistence.PropertyTypeBool { + if fieldValue == "0" { + fieldValue = "false" + } + if fieldValue == "1" { + fieldValue = "true" + } + } + + oldValue, err := session.Session.Get(field.Var) + if err != nil || oldValue != fieldValue { + value, err := session.Session.Set(field.Var, fieldValue) + if err != nil { + errString = fmt.Sprintf("Error for field %v: %v, aborting", field.Var, err.Error()) + break + } + infoStrings = append(infoStrings, fmt.Sprintf("%s set to %s", field.Var, value)) + gateway.DirtySessions = true + } + } + } + + var elements []stanza.CommandElement + if errString != "" { + elements = append(elements, &stanza.Note{ + Text: errString, + Type: stanza.CommandNoteTypeErr, + }) + } + if warnString != "" { + elements = append(elements, &stanza.Note{ + Text: warnString, + Type: stanza.CommandNoteTypeWarn, + }) + } + for _, infoString := range infoStrings { + elements = append(elements, &stanza.Note{ + Text: infoString, + Type: stanza.CommandNoteTypeInfo, + }) + } + + answer.Payload = &stanza.Command{ + SessionId: command.Node, + Node: command.Node, + Status: stanza.CommandStatusCompleted, + CommandElements: elements, + } } + } else { + // just for the case the client messed the order somehow + sort.Slice(form.Fields, func(i int, j int) bool { + iField := form.Fields[i] + jField := form.Fields[j] + if iField != nil && jField != nil { + ii, iErr := strconv.ParseInt(iField.Var, 10, 64) + ji, jErr := strconv.ParseInt(jField.Var, 10, 64) + return iErr == nil && jErr == nil && ii < ji + } + return false + }) + + var cmd strings.Builder + cmd.WriteString("/") + cmd.WriteString(command.Node) + for _, field := range form.Fields { + cmd.WriteString(" ") + if len(field.ValuesList) > 0 { + cmd.WriteString(field.ValuesList[0]) + } + } + + cmdString = cmd.String() } - - cmdString = cmd.String() } else { if command.Action == "" || command.Action == stanza.CommandActionExecute { cmd, ok := telegram.GetCommand(cmdType, command.Node) if ok && len(cmd.Arguments) > 0 { var fields []*stanza.Field - for i, arg := range cmd.Arguments { - var required *string - if i < cmd.RequiredArgs { - dummyString := "" - required = &dummyString - } + if command.Node == "config" { + session, ok := sessions[bare] + if ok { + for _, key := range persistence.ConfigKeys { + // no reason to display the item if carbons won't work + if key == "carbons" && gateway.MessageOutgoingPermissionVersion == 0 { + continue + } - var fieldType string - var options []stanza.Option - if toOk && i == 0 { - switch command.Node { - case "mute", "kick", "ban", "promote", "unmute", "unban": - session, ok := sessions[bare] - if ok { - var membersList telegram.MembersList - switch command.Node { - case "unmute": - membersList = telegram.MembersListRestricted - case "unban": - membersList = telegram.MembersListBannedAndAdministrators - } - members, err := session.GetChatMembers(toId, true, "", membersList) - if err == nil { - fieldType = stanza.FieldTypeListSingle + value, err := session.Session.Get(key) + if err != nil { + log.Errorf("Achtung! Programming error in sessions with key %v", key) + continue + } + + var fieldType string + if persistence.PropertyType(key) == persistence.PropertyTypeBool { + fieldType = stanza.FieldTypeBool + } + + field := stanza.Field{ + Var: key, + Label: key, + Type: fieldType, + ValuesList: []string{value}, + } + fields = append(fields, &field) + log.Debugf("field: %#v", field) + } + } + } else { + for i, arg := range cmd.Arguments { + var required *string + if i < cmd.RequiredArgs { + dummyString := "" + required = &dummyString + } + + var fieldType string + var options []stanza.Option + if toOk && i == 0 { + switch command.Node { + case "mute", "kick", "ban", "promote", "unmute", "unban": + session, ok := sessions[bare] + if ok { + var membersList telegram.MembersList switch command.Node { - // allow empty form - case "mute", "unmute": - options = append(options, stanza.Option{ - ValuesList: []string{""}, - }) + case "unmute": + membersList = telegram.MembersListRestricted + case "unban": + membersList = telegram.MembersListBannedAndAdministrators } - for _, member := range members { - senderId := session.GetSenderId(member.MemberId) - options = append(options, stanza.Option{ - Label: session.FormatContact(senderId), - ValuesList: []string{strconv.FormatInt(senderId, 10)}, - }) + members, err := session.GetChatMembers(toId, true, "", membersList) + if err == nil { + fieldType = stanza.FieldTypeListSingle + switch command.Node { + // allow empty form + case "mute", "unmute": + options = append(options, stanza.Option{ + ValuesList: []string{""}, + }) + } + for _, member := range members { + senderId := session.GetSenderId(member.MemberId) + options = append(options, stanza.Option{ + Label: session.FormatContact(senderId), + ValuesList: []string{strconv.FormatInt(senderId, 10)}, + }) + } } } } } - } - field := stanza.Field{ - Var: strconv.FormatInt(int64(i), 10), - Label: arg, - Required: required, - Type: fieldType, - Options: options, + field := stanza.Field{ + Var: strconv.FormatInt(int64(i), 10), + Label: arg, + Required: required, + Type: fieldType, + Options: options, + } + fields = append(fields, &field) + log.Debugf("field: %#v", field) } - fields = append(fields, &field) - log.Debugf("field: %#v", field) } form := stanza.Form{ Type: stanza.FormTypeForm,