Ad-Hoc config editor

This commit is contained in:
Bohdan Horbeshko 2025-03-22 18:11:26 -04:00
parent 7ebcdb0826
commit 0368b8cad8
5 changed files with 206 additions and 74 deletions

View file

@ -2,7 +2,7 @@
COMMIT := $(shell git rev-parse --short HEAD) COMMIT := $(shell git rev-parse --short HEAD)
TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551"
VERSION := "v1.10.1" VERSION := "v1.10.2"
MAKEOPTS := "-j4" MAKEOPTS := "-j4"
all: all:

View file

@ -51,7 +51,13 @@ type Session struct {
IgnoreGroupDeletions bool `yaml:":ignoregroupdeletions"` IgnoreGroupDeletions bool `yaml:":ignoregroupdeletions"`
} }
var configKeys = []string{ const (
PropertyTypeUnknown byte = iota
PropertyTypeString
PropertyTypeBool
)
var ConfigKeys = []string{
"timezone", "timezone",
"keeponline", "keeponline",
"rawmessages", "rawmessages",
@ -181,7 +187,7 @@ func (s *Session) ToMap() map[string]string {
defer sessionsLock.Unlock() defer sessionsLock.Unlock()
m := make(map[string]string) m := make(map[string]string)
for _, configKey := range configKeys { for _, configKey := range ConfigKeys {
value, _ := s.get(configKey) value, _ := s.get(configKey)
m[configKey] = value m[configKey] = value
} }
@ -266,6 +272,18 @@ func (s *Session) Set(key string, value string) (string, error) {
return "", errors.New("Unknown session property") 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 // TimezoneToLocation tries to convert config timezone to location
func (s *Session) TimezoneToLocation() *time.Location { func (s *Session) TimezoneToLocation() *time.Location {
time, err := time.Parse("-07:00", s.Timezone) time, err := time.Parse("-07:00", s.Timezone)

View file

@ -16,7 +16,7 @@ import (
goxmpp "gosrc.io/xmpp" goxmpp "gosrc.io/xmpp"
) )
var version string = "1.10.1" var version string = "1.10.2"
var commit string var commit string
var sm *goxmpp.StreamManager var sm *goxmpp.StreamManager

View file

@ -9,6 +9,7 @@ import (
"time" "time"
"unicode" "unicode"
"dev.narayana.im/narayana/telegabber/persistence"
"dev.narayana.im/narayana/telegabber/xmpp/gateway" "dev.narayana.im/narayana/telegabber/xmpp/gateway"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -108,9 +109,16 @@ var chatCommands = map[string]command{
} }
var transportConfigurationOptions = map[string]configurationOption{ var transportConfigurationOptions = map[string]configurationOption{
"timezone": configurationOption{"<timezone>", "adjust timezone for Telegram user statuses (example: +02:00)"}, "timezone": configurationOption{"<timezone>", "adjust timezone for Telegram user statuses (example: +02:00)"},
"keeponline": configurationOption{"<bool>", "always keep telegram session online and rely on jabber offline messages (example: true)"}, "keeponline": configurationOption{"<bool>", "always keep telegram session online and rely on jabber offline messages (true/false)"},
"rawmessages": configurationOption{"<bool>", "do not add additional info (message id, origin etc.) to incoming messages (example: true)"}, "rawmessages": configurationOption{"<bool>", "do not add additional info (message id, origin etc.) to incoming messages (true/false)"},
"asciiarrows": configurationOption{"<bool>", "replace some Unicode symbols with ASCII alternatives for better compatibility (true/false)"},
"oobmode": configurationOption{"<bool>", "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{"<bool>", "send carbons to your another clients, will turn on only if supported by the server (true/false)"},
"hideids": configurationOption{"<bool>", "hide message IDs from message info (true/false)"},
"receipts": configurationOption{"<bool>", "if enabled, XMPP read receipts are synced to Telegram, otherwise, messages are marked as read automatically (true/false)"},
"nativeedits": configurationOption{"<bool>", "if possible, edit XMPP messages instead of showing Telegram edits as separate messages (true/false)"},
"ignoregroupdeletions": configurationOption{"<bool>", "suppress message deletion messages in group chats (true/false)"},
} }
type command struct { type command struct {
@ -222,7 +230,8 @@ func (c *Client) helpString(typ CommandType, chatId int64) string {
if typ == CommandTypeTransport { if typ == CommandTypeTransport {
str.WriteString("Configuration options\n") str.WriteString("Configuration options\n")
for name, option := range transportConfigurationOptions { for _, name := range persistence.ConfigKeys {
option := transportConfigurationOptions[name]
str.WriteString(name) str.WriteString(name)
str.WriteString(" ") str.WriteString(" ")
str.WriteString(option.arguments) str.WriteString(option.arguments)
@ -461,7 +470,6 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin
} }
case "config": case "config":
if len(args) > 1 { if len(args) > 1 {
var msg string
if gateway.MessageOutgoingPermissionVersion == 0 && args[0] == "carbons" && args[1] == "true" { if gateway.MessageOutgoingPermissionVersion == 0 && args[0] == "carbons" && args[1] == "true" {
return "The server did not allow to enable carbons", false 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 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 { } else if len(args) > 0 {
value, err := c.Session.Get(args[0]) value, err := c.Session.Get(args[0])
if err != nil { if err != nil {
@ -483,7 +491,12 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin
} }
var entries []string 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)) entries = append(entries, fmt.Sprintf("%s is set to %s", key, value))
} }

View file

@ -877,86 +877,187 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command
cmdType = telegram.CommandTypeTransport cmdType = telegram.CommandTypeTransport
} }
if form != nil { if form != nil {
// just for the case the client messed the order somehow if command.Node == "config" {
sort.Slice(form.Fields, func(i int, j int) bool { session, ok := sessions[bare]
iField := form.Fields[i] if ok {
jField := form.Fields[j] var infoStrings []string
if iField != nil && jField != nil { var warnString, errString string
ii, iErr := strconv.ParseInt(iField.Var, 10, 64) for _, field := range form.Fields {
ji, jErr := strconv.ParseInt(jField.Var, 10, 64) if len(field.ValuesList) > 0 {
return iErr == nil && jErr == nil && ii < ji fieldValue := field.ValuesList[0]
}
return false
})
var cmd strings.Builder if gateway.MessageOutgoingPermissionVersion == 0 && field.Var == "carbons" && fieldValue == "true" {
cmd.WriteString("/") warnString = "The server did not allow to enable carbons"
cmd.WriteString(command.Node) continue
for _, field := range form.Fields { }
cmd.WriteString(" ")
if len(field.ValuesList) > 0 { // 10. In accordance with Section 3.2.2.1 of XML Schema Part 2: Datatypes, the allowable
cmd.WriteString(field.ValuesList[0]) // 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 { } else {
if command.Action == "" || command.Action == stanza.CommandActionExecute { if command.Action == "" || command.Action == stanza.CommandActionExecute {
cmd, ok := telegram.GetCommand(cmdType, command.Node) cmd, ok := telegram.GetCommand(cmdType, command.Node)
if ok && len(cmd.Arguments) > 0 { if ok && len(cmd.Arguments) > 0 {
var fields []*stanza.Field var fields []*stanza.Field
for i, arg := range cmd.Arguments { if command.Node == "config" {
var required *string session, ok := sessions[bare]
if i < cmd.RequiredArgs { if ok {
dummyString := "" for _, key := range persistence.ConfigKeys {
required = &dummyString // no reason to display the item if carbons won't work
} if key == "carbons" && gateway.MessageOutgoingPermissionVersion == 0 {
continue
}
var fieldType string value, err := session.Session.Get(key)
var options []stanza.Option if err != nil {
if toOk && i == 0 { log.Errorf("Achtung! Programming error in sessions with key %v", key)
switch command.Node { continue
case "mute", "kick", "ban", "promote", "unmute", "unban": }
session, ok := sessions[bare]
if ok { var fieldType string
var membersList telegram.MembersList if persistence.PropertyType(key) == persistence.PropertyTypeBool {
switch command.Node { fieldType = stanza.FieldTypeBool
case "unmute": }
membersList = telegram.MembersListRestricted
case "unban": field := stanza.Field{
membersList = telegram.MembersListBannedAndAdministrators Var: key,
} Label: key,
members, err := session.GetChatMembers(toId, true, "", membersList) Type: fieldType,
if err == nil { ValuesList: []string{value},
fieldType = stanza.FieldTypeListSingle }
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 { switch command.Node {
// allow empty form case "unmute":
case "mute", "unmute": membersList = telegram.MembersListRestricted
options = append(options, stanza.Option{ case "unban":
ValuesList: []string{""}, membersList = telegram.MembersListBannedAndAdministrators
})
} }
for _, member := range members { members, err := session.GetChatMembers(toId, true, "", membersList)
senderId := session.GetSenderId(member.MemberId) if err == nil {
options = append(options, stanza.Option{ fieldType = stanza.FieldTypeListSingle
Label: session.FormatContact(senderId), switch command.Node {
ValuesList: []string{strconv.FormatInt(senderId, 10)}, // 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{ field := stanza.Field{
Var: strconv.FormatInt(int64(i), 10), Var: strconv.FormatInt(int64(i), 10),
Label: arg, Label: arg,
Required: required, Required: required,
Type: fieldType, Type: fieldType,
Options: options, Options: options,
}
fields = append(fields, &field)
log.Debugf("field: %#v", field)
} }
fields = append(fields, &field)
log.Debugf("field: %#v", field)
} }
form := stanza.Form{ form := stanza.Form{
Type: stanza.FormTypeForm, Type: stanza.FormTypeForm,