mirror of
https://dev.narayana.im/narayana/telegabber.git
synced 2026-08-05 12:17:06 +00:00
1229 lines
35 KiB
Go
1229 lines
35 KiB
Go
package telegram
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/pkg/errors"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
|
|
"dev.narayana.im/narayana/telegabber/persistence"
|
|
"dev.narayana.im/narayana/telegabber/xmpp/gateway"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/zelenin/go-tdlib/client"
|
|
)
|
|
|
|
const unknownCommand string = "Unknown command"
|
|
const notEnoughArguments string = "Not enough arguments"
|
|
const TelegramNotInitialized string = "Telegram connection is not initialized yet"
|
|
const TelegramAuthDone string = "Authorization is done already"
|
|
const notOnline string = "Not online"
|
|
|
|
var permissionsAdmin = client.ChatAdministratorRights{
|
|
CanChangeInfo: true,
|
|
CanPostMessages: true,
|
|
CanEditMessages: true,
|
|
CanDeleteMessages: true,
|
|
CanInviteUsers: true,
|
|
CanRestrictMembers: true,
|
|
CanPinMessages: true,
|
|
CanPromoteMembers: false,
|
|
}
|
|
var permissionsMember = client.ChatPermissions{
|
|
CanSendBasicMessages: true,
|
|
CanSendAudios: true,
|
|
CanSendDocuments: true,
|
|
CanSendPhotos: true,
|
|
CanSendVideos: true,
|
|
CanSendVideoNotes: true,
|
|
CanSendVoiceNotes: true,
|
|
CanSendPolls: true,
|
|
CanSendOtherMessages: true,
|
|
CanAddWebPagePreviews: true,
|
|
CanChangeInfo: true,
|
|
CanInviteUsers: true,
|
|
CanPinMessages: true,
|
|
CanManageTopics: true,
|
|
}
|
|
var permissionsReadonly = client.ChatPermissions{}
|
|
|
|
var transportCommands = map[string]command{
|
|
"help": command{0, []string{}, "help", false, nil},
|
|
"login": command{1, []string{"phone"}, "sign in", false, nil},
|
|
"logout": command{0, []string{}, "sign out", true, nil},
|
|
"cancelauth": command{0, []string{}, "quit the signin wizard", false, nil},
|
|
"code": command{1, []string{"xxxxx"}, "check one-time code", false, nil},
|
|
"password": command{1, []string{"********"}, "check 2fa password", false, nil},
|
|
"setusername": command{0, []string{"@username"}, "update @username", true, nil},
|
|
"setname": command{1, []string{"first", "last"}, "update name", true, nil},
|
|
"setbio": command{0, []string{"Lorem ipsum"}, "update about", true, nil},
|
|
"setpassword": command{0, []string{"old", "new"}, "set or remove password", true, nil},
|
|
"config": command{0, []string{"param", "value"}, "view or update configuration options", false, nil},
|
|
"report": command{2, []string{"chat", "comment"}, "report a chat by id or @username", true, nil},
|
|
"add": command{1, []string{"@username"}, "add @username to your chat list", true, nil},
|
|
"join": command{1, []string{"https://t.me/invite_link"}, "join to chat via invite link or @publicname", true, nil},
|
|
"supergroup": command{1, []string{"title", "description"}, "create new supergroup «title» with «description»", true, nil},
|
|
"channel": command{1, []string{"title", "description"}, "create new channel «title» with «description»", true, nil},
|
|
"preset": command{1, []string{"modern|classic|pass"}, "apply a config preset", false, nil},
|
|
"pass": command{0, []string{}, "proceed to next login stage", false, nil},
|
|
"finish": command{0, []string{}, "skip post-login configuration", false, nil},
|
|
}
|
|
|
|
var notForGroups = []ChatType{ChatTypeBasicGroup, ChatTypeSupergroup, ChatTypeChannel}
|
|
var notForPM = []ChatType{ChatTypePrivate, ChatTypeSecret}
|
|
var notForPMAndBasic = []ChatType{ChatTypePrivate, ChatTypeSecret, ChatTypeBasicGroup}
|
|
var onlyForSecret = []ChatType{ChatTypePrivate, ChatTypeBasicGroup, ChatTypeSupergroup, ChatTypeChannel}
|
|
|
|
var chatCommands = map[string]command{
|
|
"help": command{0, []string{}, "help", false, nil},
|
|
"d": command{0, []string{"n"}, "delete your last message(s)", true, nil},
|
|
"s": command{1, []string{"edited message"}, "edit your last message", true, nil},
|
|
"silent": command{1, []string{"message"}, "send a message without sound", true, nil},
|
|
"schedule": command{2, []string{"{online | 2006-01-02T15:04:05 | 15:04:05}", "message"}, "schedules a message either to timestamp or to whenever the user goes online", true, nil},
|
|
"raw": command{1, []string{"message"}, "send a raw message not interpeted as a transport command (e.g. a bot command)", true, nil},
|
|
"forward": command{2, []string{"message_id", "target_chat"}, "forwards a message", true, nil},
|
|
"vcard": command{0, []string{}, "print vCard as text", true, nil},
|
|
"add": command{1, []string{"@username"}, "add @username to your chat list", true, nil},
|
|
"join": command{1, []string{"https://t.me/invite_link"}, "join to chat via invite link or @publicname", true, nil},
|
|
"group": command{1, []string{"title"}, "create groupchat «title» with current user", true, ¬ForGroups},
|
|
"supergroup": command{1, []string{"title", "description"}, "create new supergroup «title» with «description»", true, nil},
|
|
"channel": command{1, []string{"title", "description"}, "create new channel «title» with «description»", true, nil},
|
|
"secret": command{0, []string{}, "create secretchat with current user", true, ¬ForGroups},
|
|
"search": command{0, []string{"string", "[limit]"}, "search <string> in current chat", true, nil},
|
|
"history": command{0, []string{"limit"}, "get last [limit] messages from current chat", true, nil},
|
|
"block": command{0, []string{}, "blacklist current user", true, ¬ForGroups},
|
|
"unblock": command{0, []string{}, "unblacklist current user", true, ¬ForGroups},
|
|
"invite": command{1, []string{"id or @username"}, "add user to current chat", true, ¬ForPM},
|
|
"link": command{0, []string{}, "get invite link for current chat", true, ¬ForPM},
|
|
"kick": command{1, []string{"id or @username"}, "remove user from current chat", true, ¬ForPM},
|
|
"mute": command{0, []string{"id or @username", "hours"}, "mute the whole chat or a user in current chat", true, ¬ForPMAndBasic},
|
|
"unmute": command{0, []string{"id or @username"}, "unmute the whole chat or a user in the current chat", true, ¬ForPMAndBasic},
|
|
"ban": command{1, []string{"id or @username", "hours"}, "restrict @username from current chat for [hours] or forever", true, ¬ForPM},
|
|
"unban": command{1, []string{"id or @username"}, "unbans @username in current chat (and devotes from admins)", true, ¬ForPM},
|
|
"promote": command{1, []string{"id or @username", "title"}, "promote user to admin in current chat", true, ¬ForPM},
|
|
"leave": command{0, []string{}, "leave current chat", true, ¬ForPM},
|
|
"leave!": command{0, []string{}, "leave current chat (for owners)", true, ¬ForPM},
|
|
"ttl": command{0, []string{"seconds"}, "set secret chat messages TTL before self-destroying", true, &onlyForSecret},
|
|
"close": command{0, []string{}, "close current secret chat", true, &onlyForSecret},
|
|
"delete": command{0, []string{}, "delete current chat from chat list", true, nil},
|
|
"members": command{0, []string{"query"}, "search members [by optional query] in current chat (requires admin rights)", true, nil},
|
|
}
|
|
|
|
var transportConfigurationOptions = map[string]configurationOption{
|
|
"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 (true/false)"},
|
|
"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)"},
|
|
"muc": configurationOption{"<bool>", "use MUCs instead of the legacy PM representation of group chats (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 {
|
|
RequiredArgs int
|
|
Arguments []string
|
|
Description string
|
|
LoginOnly bool
|
|
NotFor *[]ChatType
|
|
}
|
|
type configurationOption struct {
|
|
arguments string
|
|
description string
|
|
}
|
|
|
|
// CommandType disinguishes command sets by chat
|
|
type CommandType int
|
|
|
|
const (
|
|
CommandTypeTransport CommandType = iota
|
|
CommandTypeChat
|
|
)
|
|
|
|
// GetCommands exposes the set of commands
|
|
func GetCommands(typ CommandType) map[string]command {
|
|
var commandMap map[string]command
|
|
|
|
switch typ {
|
|
case CommandTypeTransport:
|
|
commandMap = transportCommands
|
|
case CommandTypeChat:
|
|
commandMap = chatCommands
|
|
}
|
|
|
|
return commandMap
|
|
}
|
|
|
|
// GetCommand obtains one command
|
|
func GetCommand(typ CommandType, cmd string) (command, bool) {
|
|
commands := GetCommands(typ)
|
|
command, ok := commands[cmd]
|
|
return command, ok
|
|
}
|
|
|
|
// SortedCommandKeys sorts a slice with command keys
|
|
func SortedCommandKeys(commandMap map[string]command) []string {
|
|
keys := make([]string, len(commandMap))
|
|
|
|
i := 0
|
|
for k := range commandMap {
|
|
keys[i] = k
|
|
i++
|
|
}
|
|
|
|
sort.Strings(keys)
|
|
|
|
return keys
|
|
}
|
|
|
|
// CommandToHelpString builds a text description of a command
|
|
func CommandToHelpString(name string, cmd command) string {
|
|
var str strings.Builder
|
|
|
|
str.WriteString("/")
|
|
str.WriteString(name)
|
|
for i, arg := range cmd.Arguments {
|
|
optional := i >= cmd.RequiredArgs
|
|
str.WriteString(" ")
|
|
if optional {
|
|
str.WriteString("[")
|
|
}
|
|
str.WriteString(arg)
|
|
if optional {
|
|
str.WriteString("]")
|
|
}
|
|
}
|
|
str.WriteString(" — ")
|
|
str.WriteString(cmd.Description)
|
|
|
|
return str.String()
|
|
}
|
|
|
|
// IsCommandFor checks the suitability of a command for a chat type
|
|
func IsCommandForChatType(cmd command, chatType ChatType) bool {
|
|
if cmd.NotFor != nil {
|
|
for _, typ := range *cmd.NotFor {
|
|
if chatType == typ {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (c *Client) helpString(typ CommandType, chatId int64) string {
|
|
var str strings.Builder
|
|
|
|
commandMap := GetCommands(typ)
|
|
chatType, _, chatTypeErr := c.GetChatType(chatId, true)
|
|
|
|
str.WriteString("Available commands:\n")
|
|
for _, name := range SortedCommandKeys(commandMap) {
|
|
command := commandMap[name]
|
|
if chatTypeErr == nil && !IsCommandForChatType(command, chatType) {
|
|
continue
|
|
}
|
|
str.WriteString(CommandToHelpString(name, command))
|
|
str.WriteString("\n")
|
|
}
|
|
|
|
if typ == CommandTypeTransport {
|
|
str.WriteString("Configuration options\n")
|
|
for _, name := range persistence.ConfigKeys {
|
|
option := transportConfigurationOptions[name]
|
|
str.WriteString(name)
|
|
str.WriteString(" ")
|
|
str.WriteString(option.arguments)
|
|
str.WriteString(" — ")
|
|
str.WriteString(option.description)
|
|
str.WriteString("\n")
|
|
}
|
|
}
|
|
str.WriteString("\nYou may use ! instead of / if it conflicts with internal commands of a client")
|
|
|
|
return str.String()
|
|
}
|
|
|
|
func parseCommand(cmdline string) (string, []string) {
|
|
bodyFields := strings.Fields(cmdline)
|
|
return bodyFields[0][1:], bodyFields[1:]
|
|
}
|
|
|
|
func rawCmdArguments(cmdline string, start uint8) string {
|
|
var state uint
|
|
// /cmd ababa galamaga
|
|
// 01 2 3 45
|
|
startState := uint(3 + 2*start)
|
|
for i, r := range cmdline {
|
|
isOdd := state%2 == 1
|
|
isSpace := unicode.IsSpace(r)
|
|
if (!isOdd && !isSpace) || (isOdd && isSpace) {
|
|
state += 1
|
|
}
|
|
if state == startState {
|
|
return cmdline[i:]
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func keyValueString(key, value string) string {
|
|
return fmt.Sprintf("%s: %s", key, value)
|
|
}
|
|
|
|
func (c *Client) usernameOrIDToID(username string) (int64, error) {
|
|
userID, err := strconv.ParseInt(username, 10, 64)
|
|
// couldn't parse the id, try to lookup as a username
|
|
if err != nil {
|
|
chat, err := c.client.SearchPublicChat(&client.SearchPublicChatRequest{
|
|
Username: username,
|
|
})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
userID = chat.Id
|
|
if userID <= 0 {
|
|
return 0, errors.New("Not a user")
|
|
}
|
|
}
|
|
|
|
return userID, nil
|
|
}
|
|
|
|
// ProcessTransportCommand executes a command sent directly to the component
|
|
// and returns a response and execution success result
|
|
func (c *Client) ProcessTransportCommand(cmdline string, resource string) (string, bool) {
|
|
cmd, args := parseCommand(cmdline)
|
|
command, ok := transportCommands[cmd]
|
|
if !ok {
|
|
return unknownCommand, false
|
|
}
|
|
if len(args) < command.RequiredArgs {
|
|
return notEnoughArguments, false
|
|
}
|
|
|
|
switch cmd {
|
|
case "login", "code", "password":
|
|
if cmd == "login" && c.Session.Login != "" {
|
|
return "Phone number already provided, use /cancelauth to start over", false
|
|
}
|
|
|
|
if cmd == "login" {
|
|
err := c.TryLogin(resource, args[0])
|
|
if err != nil {
|
|
return err.Error(), false
|
|
}
|
|
|
|
c.locks.authorizerWriteLock.Lock()
|
|
defer c.locks.authorizerWriteLock.Unlock()
|
|
|
|
c.authorizer.PhoneNumber <- args[0]
|
|
} else {
|
|
c.locks.authorizerWriteLock.Lock()
|
|
defer c.locks.authorizerWriteLock.Unlock()
|
|
|
|
if c.authorizer == nil {
|
|
return TelegramNotInitialized, false
|
|
}
|
|
|
|
if c.authorizer.isClosed {
|
|
return TelegramAuthDone, false
|
|
}
|
|
|
|
switch cmd {
|
|
// check auth code
|
|
case "code":
|
|
c.authorizer.Code <- args[0]
|
|
// check auth password
|
|
case "password":
|
|
c.authorizer.Password <- args[0]
|
|
}
|
|
}
|
|
// sign out
|
|
case "logout":
|
|
if !c.Online() && !c.locks.loginFinish.IsPending() {
|
|
return notOnline, false
|
|
}
|
|
|
|
_, err := c.client.LogOut()
|
|
if err != nil {
|
|
return errors.Wrap(err, "Logout error").Error(), false
|
|
}
|
|
|
|
for _, id := range c.cache.OwnChatsKeys() {
|
|
c.leaveChat(id)
|
|
}
|
|
|
|
c.Session.Login = ""
|
|
c.wizardStageOrPrompt(LoginStageCancel, "")
|
|
c.online = false
|
|
// cancel auth
|
|
case "cancelauth":
|
|
if c.Online() {
|
|
return "Not allowed when online, use /logout instead", false
|
|
}
|
|
c.cancelAuth()
|
|
return "Cancelled", true
|
|
// set @username
|
|
case "setusername":
|
|
if !c.Online() {
|
|
return notOnline, false
|
|
}
|
|
|
|
var username string
|
|
if len(args) > 0 {
|
|
username = args[0]
|
|
}
|
|
|
|
_, err := c.client.SetUsername(&client.SetUsernameRequest{
|
|
Username: username,
|
|
})
|
|
if err != nil {
|
|
return errors.Wrap(err, "Couldn't set username").Error(), false
|
|
}
|
|
// set My Name
|
|
case "setname":
|
|
firstname := args[0]
|
|
var lastname string
|
|
|
|
if firstname == "" {
|
|
return "The name should contain at least one character", false
|
|
}
|
|
if len(args) > 1 {
|
|
lastname = rawCmdArguments(cmdline, 1)
|
|
}
|
|
|
|
c.locks.authorizerWriteLock.Lock()
|
|
if c.authorizer != nil && !c.authorizer.isClosed {
|
|
c.authorizer.FirstName <- firstname
|
|
c.authorizer.LastName <- lastname
|
|
c.locks.authorizerWriteLock.Unlock()
|
|
} else {
|
|
c.locks.authorizerWriteLock.Unlock()
|
|
if !c.Online() {
|
|
return notOnline, false
|
|
}
|
|
|
|
_, err := c.client.SetName(&client.SetNameRequest{
|
|
FirstName: firstname,
|
|
LastName: lastname,
|
|
})
|
|
if err != nil {
|
|
return errors.Wrap(err, "Couldn't set name").Error(), false
|
|
}
|
|
}
|
|
// set About
|
|
case "setbio":
|
|
if !c.Online() {
|
|
return notOnline, false
|
|
}
|
|
|
|
_, err := c.client.SetBio(&client.SetBioRequest{
|
|
Bio: rawCmdArguments(cmdline, 0),
|
|
})
|
|
if err != nil {
|
|
return errors.Wrap(err, "Couldn't set bio").Error(), false
|
|
}
|
|
// set password
|
|
case "setpassword":
|
|
if !c.Online() {
|
|
return notOnline, false
|
|
}
|
|
|
|
var oldPassword string
|
|
var newPassword string
|
|
if len(args) > 0 {
|
|
oldPassword = args[0]
|
|
}
|
|
if len(args) > 1 {
|
|
newPassword = args[1]
|
|
}
|
|
_, err := c.client.SetPassword(&client.SetPasswordRequest{
|
|
OldPassword: oldPassword,
|
|
NewPassword: newPassword,
|
|
})
|
|
if err != nil {
|
|
return errors.Wrap(err, "Couldn't set password").Error(), false
|
|
}
|
|
case "config":
|
|
if len(args) > 1 {
|
|
if gateway.MessageOutgoingPermissionVersion == 0 && args[0] == "carbons" && args[1] == "true" {
|
|
return "The server did not allow to enable carbons", false
|
|
}
|
|
|
|
value, err := c.Session.Set(args[0], args[1])
|
|
if err != nil {
|
|
return err.Error(), false
|
|
}
|
|
if args[0] == "muc" {
|
|
switch args[1] {
|
|
case "true":
|
|
go c.MigrateToMUCs()
|
|
case "false":
|
|
go c.MigrateFromMUCs()
|
|
}
|
|
if c.loginStage == LoginStageMUC {
|
|
c.wizardStageOrPrompt(LoginStageSuccess, "")
|
|
}
|
|
}
|
|
gateway.DirtySessions = 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 {
|
|
return err.Error(), false
|
|
}
|
|
|
|
return fmt.Sprintf("%s is set to %s", args[0], value), true
|
|
}
|
|
|
|
var entries []string
|
|
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))
|
|
}
|
|
|
|
return strings.Join(entries, "\n"), true
|
|
case "report":
|
|
contact, _, err := c.GetContactByUsername(args[0], false)
|
|
if err != nil {
|
|
return err.Error(), false
|
|
}
|
|
if contact == nil {
|
|
return "Contact not found", false
|
|
}
|
|
|
|
text := rawCmdArguments(cmdline, 1)
|
|
_, err = c.client.ReportChat(&client.ReportChatRequest{
|
|
ChatId: contact.Id,
|
|
Reason: &client.ReportReasonCustom{},
|
|
Text: text,
|
|
})
|
|
if err != nil {
|
|
return err.Error(), false
|
|
} else {
|
|
return "Reported", true
|
|
}
|
|
case "add":
|
|
return c.cmdAdd(args)
|
|
case "join":
|
|
return c.cmdJoin(args)
|
|
case "supergroup":
|
|
return c.cmdSupergroup(args, cmdline)
|
|
case "channel":
|
|
return c.cmdChannel(args, cmdline)
|
|
case "help":
|
|
return c.helpString(CommandTypeTransport, 0), true
|
|
case "preset":
|
|
cancelNextStage := false
|
|
|
|
defer func() {
|
|
if !cancelNextStage {
|
|
go c.promptMUC()
|
|
}
|
|
}()
|
|
switch args[0] {
|
|
case "modern", "classic":
|
|
for _, row := range persistence.Presets[args[0]] {
|
|
_, err := c.Session.Set(row[0], row[1])
|
|
if err != nil {
|
|
return err.Error(), false
|
|
}
|
|
}
|
|
gateway.DirtySessions = true
|
|
return fmt.Sprintf("Applied preset %s", args[0]), true
|
|
default:
|
|
cancelNextStage = true
|
|
return "Invalid argument. Allowed ones are " + transportCommands["preset"].Arguments[0], false
|
|
}
|
|
case "pass":
|
|
switch c.loginStage {
|
|
case LoginStagePreset:
|
|
go c.promptMUC()
|
|
case LoginStageMUC:
|
|
c.wizardStageOrPrompt(LoginStageSuccess, "")
|
|
}
|
|
case "finish":
|
|
c.wizardStageOrPrompt(LoginStageSuccess, "")
|
|
}
|
|
|
|
return "", true
|
|
}
|
|
|
|
func (c *Client) promptMUC() {
|
|
c.wizardStageOrPrompt(LoginStageMUC, "Enable MUCs? Telegabber still supports the legacy approach of mapping Telegram group chats to personal messages in XMPP which might be suitable for some cases like logging or reliable participation in all group chats. Use `/config muc {true|false}` or /finish")
|
|
}
|
|
|
|
// ProcessChatCommand executes a command sent in a mapped chat
|
|
// and returns a response, the status of command support and the execution success result
|
|
func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, bool) {
|
|
if !c.Online() {
|
|
return notOnline, true, false
|
|
}
|
|
|
|
cmd, args := parseCommand(cmdline)
|
|
command, ok := chatCommands[cmd]
|
|
if !ok {
|
|
return unknownCommand, false, false
|
|
}
|
|
if len(args) < command.RequiredArgs {
|
|
return notEnoughArguments, true, false
|
|
}
|
|
|
|
chatType, _, chatTypeErr := c.GetChatType(chatID, true)
|
|
if chatTypeErr == nil && !IsCommandForChatType(command, chatType) {
|
|
return "Not applicable for this chat type", true, false
|
|
}
|
|
|
|
switch cmd {
|
|
// delete message
|
|
case "d":
|
|
if c.me == nil {
|
|
return "@me is not initialized", true, false
|
|
}
|
|
|
|
var limit int32
|
|
if len(args) > 0 {
|
|
limit64, err := strconv.ParseInt(args[0], 10, 32)
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
limit = int32(limit64)
|
|
} else {
|
|
limit = 1
|
|
}
|
|
|
|
messages, err := c.getLastMessages(chatID, "", c.me.Id, limit)
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
log.Debugf("pre-deletion query: %#v %#v", messages, messages.Messages)
|
|
|
|
var messageIds []int64
|
|
for _, message := range messages.Messages {
|
|
if message != nil {
|
|
messageIds = append(messageIds, message.Id)
|
|
}
|
|
}
|
|
|
|
_, err = c.client.DeleteMessages(&client.DeleteMessagesRequest{
|
|
ChatId: chatID,
|
|
MessageIds: messageIds,
|
|
Revoke: true,
|
|
})
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
// edit message
|
|
case "s":
|
|
if c.me == nil {
|
|
return "@me is not initialized", true, false
|
|
}
|
|
|
|
messages, err := c.getLastMessages(chatID, "", c.me.Id, 1)
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
if len(messages.Messages) == 0 {
|
|
return "No last message", true, false
|
|
}
|
|
|
|
message := messages.Messages[0]
|
|
if message == nil {
|
|
return "Last message is empty", true, false
|
|
}
|
|
|
|
content := c.PrepareOutgoingMessageContent(rawCmdArguments(cmdline, 0))
|
|
|
|
if content != nil {
|
|
_, err = c.client.EditMessageText(&client.EditMessageTextRequest{
|
|
ChatId: chatID,
|
|
MessageId: message.Id,
|
|
InputMessageContent: content,
|
|
})
|
|
if err != nil {
|
|
return "Message editing error", true, false
|
|
}
|
|
} else {
|
|
return "Message processing error", true, false
|
|
}
|
|
// send without sound
|
|
case "silent":
|
|
content := c.PrepareOutgoingMessageContent(rawCmdArguments(cmdline, 0))
|
|
|
|
if content != nil {
|
|
_, err := c.client.SendMessage(&client.SendMessageRequest{
|
|
ChatId: chatID,
|
|
InputMessageContent: content,
|
|
Options: &client.MessageSendOptions{
|
|
DisableNotification: true,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
} else {
|
|
return "Message processing error", true, false
|
|
}
|
|
// schedule a message to timestamp or to going online
|
|
case "schedule":
|
|
var state client.MessageSchedulingState
|
|
var result string
|
|
due := args[0]
|
|
if due == "online" {
|
|
state = &client.MessageSchedulingStateSendWhenOnline{}
|
|
result = due
|
|
} else {
|
|
if c.Session.Timezone == "" {
|
|
due += "Z"
|
|
} else {
|
|
due += c.Session.Timezone
|
|
}
|
|
|
|
switch 0 {
|
|
default:
|
|
// try bare time first
|
|
timestamp, err := time.Parse("15:04:05Z07:00", due)
|
|
if err == nil {
|
|
now := time.Now().In(c.Session.TimezoneToLocation())
|
|
// combine timestamp's time with today's date
|
|
timestamp = time.Date(
|
|
now.Year(),
|
|
now.Month(),
|
|
now.Day(),
|
|
timestamp.Hour(),
|
|
timestamp.Minute(),
|
|
timestamp.Second(),
|
|
0,
|
|
timestamp.Location(),
|
|
)
|
|
diff := timestamp.Sub(now)
|
|
if diff < 0 { // set to tomorrow
|
|
timestamp = timestamp.AddDate(0, 0, 1)
|
|
}
|
|
state = &client.MessageSchedulingStateSendAtDate{
|
|
SendDate: int32(timestamp.Unix()),
|
|
}
|
|
result = timestamp.Format(time.RFC3339)
|
|
|
|
break
|
|
}
|
|
|
|
timestamp, err = time.Parse(time.RFC3339, due)
|
|
if err == nil {
|
|
// 2038 doomsday again
|
|
state = &client.MessageSchedulingStateSendAtDate{
|
|
SendDate: int32(timestamp.Unix()),
|
|
}
|
|
result = timestamp.Format(time.RFC3339)
|
|
|
|
break
|
|
}
|
|
|
|
return "Invalid schedule time specifier", true, false
|
|
}
|
|
}
|
|
|
|
content := c.PrepareOutgoingMessageContent(rawCmdArguments(cmdline, 1))
|
|
|
|
if content != nil {
|
|
_, err := c.client.SendMessage(&client.SendMessageRequest{
|
|
ChatId: chatID,
|
|
InputMessageContent: content,
|
|
Options: &client.MessageSendOptions{
|
|
SchedulingState: state,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
return "Scheduled to " + result, true, true
|
|
} else {
|
|
return "Message processing error", true, false
|
|
}
|
|
// sends a raw non-interpreted message
|
|
case "raw":
|
|
content := c.PrepareOutgoingMessageContent(rawCmdArguments(cmdline, 0))
|
|
|
|
if content != nil {
|
|
_, err := c.client.SendMessage(&client.SendMessageRequest{
|
|
ChatId: chatID,
|
|
InputMessageContent: content,
|
|
})
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
} else {
|
|
return "Message processing error", true, false
|
|
}
|
|
// forward a message to chat
|
|
case "forward":
|
|
messageId, err := strconv.ParseInt(args[0], 10, 64)
|
|
if err != nil {
|
|
return "Cannot parse message ID", true, false
|
|
}
|
|
|
|
targetChatParts := strings.Split(args[1], "@") // full JIDs are supported too
|
|
targetChatId, err := strconv.ParseInt(targetChatParts[0], 10, 64)
|
|
if err != nil {
|
|
return "Cannot parse target chat ID", true, false
|
|
}
|
|
|
|
messages, err := c.client.ForwardMessages(&client.ForwardMessagesRequest{
|
|
ChatId: targetChatId,
|
|
FromChatId: chatID,
|
|
MessageIds: []int64{messageId},
|
|
})
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
if messages != nil && messages.Messages != nil {
|
|
for _, message := range messages.Messages {
|
|
c.ProcessIncomingMessage(targetChatId, message)
|
|
}
|
|
}
|
|
// print vCard
|
|
case "vcard":
|
|
info, err := c.GetVcardInfo(chatID)
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
_, link := c.PermastoreFile(info.Photo, true)
|
|
entries := []string{
|
|
keyValueString("Chat title", info.Fn),
|
|
keyValueString("Photo", link),
|
|
keyValueString("Usernames", c.usernamesToString(info.Nicknames)),
|
|
keyValueString("Full name", info.Given+" "+info.Family),
|
|
keyValueString("Phone number", info.Tel),
|
|
}
|
|
return strings.Join(entries, "\n"), true, true
|
|
// add @contact
|
|
case "add":
|
|
response, success := c.cmdAdd(args)
|
|
return response, true, success
|
|
// join https://t.me/publichat or @publicchat
|
|
case "join":
|
|
response, success := c.cmdJoin(args)
|
|
return response, true, success
|
|
// create new supergroup
|
|
case "supergroup":
|
|
response, success := c.cmdSupergroup(args, cmdline)
|
|
return response, true, success
|
|
// create new channel
|
|
case "channel":
|
|
response, success := c.cmdChannel(args, cmdline)
|
|
return response, true, success
|
|
// create new secret chat with current user
|
|
case "secret":
|
|
_, err := c.client.CreateNewSecretChat(&client.CreateNewSecretChatRequest{
|
|
UserId: chatID,
|
|
})
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
// create group chat with current user
|
|
case "group":
|
|
_, err := c.client.CreateNewBasicGroupChat(&client.CreateNewBasicGroupChatRequest{
|
|
UserIds: []int64{chatID},
|
|
Title: args[0],
|
|
})
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
// blacklists current user
|
|
case "block":
|
|
_, err := c.client.SetMessageSenderBlockList(&client.SetMessageSenderBlockListRequest{
|
|
SenderId: &client.MessageSenderUser{UserId: chatID},
|
|
BlockList: &client.BlockListMain{},
|
|
})
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
// unblacklists current user
|
|
case "unblock":
|
|
_, err := c.client.SetMessageSenderBlockList(&client.SetMessageSenderBlockListRequest{
|
|
SenderId: &client.MessageSenderUser{UserId: chatID},
|
|
BlockList: nil,
|
|
})
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
// invite @username to current groupchat
|
|
case "invite":
|
|
contact, _, err := c.GetContactByUsername(args[0], false)
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
if contact == nil {
|
|
return "Contact not found", true, false
|
|
}
|
|
|
|
_, err = c.client.AddChatMember(&client.AddChatMemberRequest{
|
|
ChatId: chatID,
|
|
UserId: contact.Id,
|
|
ForwardLimit: 100,
|
|
})
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
// get link to current chat
|
|
case "link":
|
|
link, err := c.client.CreateChatInviteLink(&client.CreateChatInviteLinkRequest{
|
|
ChatId: chatID,
|
|
})
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
return link.InviteLink, true, true
|
|
// kick @username from current group chat
|
|
case "kick":
|
|
contact, _, err := c.GetContactByUsername(args[0], false)
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
if contact == nil {
|
|
return "Contact not found", true, false
|
|
}
|
|
|
|
err = c.SetChatMemberStatus(chatID, contact.Id, ChatMemberStatusKicked, 0, "", "")
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
// mute [@username [n hours]]
|
|
case "mute":
|
|
if len(args) > 0 {
|
|
contact, _, err := c.GetContactByUsername(args[0], false)
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
if contact == nil {
|
|
return "Contact not found", true, false
|
|
}
|
|
|
|
var hours int64
|
|
if len(args) > 1 {
|
|
hours, err = strconv.ParseInt(args[1], 10, 32)
|
|
if err != nil {
|
|
return "Invalid number of hours", true, false
|
|
}
|
|
}
|
|
|
|
err = c.SetChatMemberStatus(chatID, contact.Id, ChatMemberStatusMuted, hours, "", "")
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
} else {
|
|
if !c.Session.IgnoreChat(chatID) {
|
|
return "Chat is already ignored", true, false
|
|
}
|
|
gateway.DirtySessions = true
|
|
}
|
|
// unmute [@username]
|
|
case "unmute":
|
|
if len(args) > 0 {
|
|
contact, _, err := c.GetContactByUsername(args[0], false)
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
if contact == nil {
|
|
return "Contact not found", true, false
|
|
}
|
|
|
|
err = c.SetChatMemberStatus(chatID, contact.Id, ChatMemberStatusUnmuted, 0, "", "")
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
} else {
|
|
if !c.Session.UnignoreChat(chatID) {
|
|
return "Chat wasn't ignored", true, false
|
|
}
|
|
gateway.DirtySessions = true
|
|
}
|
|
// ban @username from current chat [for N hours]
|
|
case "ban":
|
|
contact, _, err := c.GetContactByUsername(args[0], false)
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
if contact == nil {
|
|
return "Contact not found", true, false
|
|
}
|
|
|
|
var hours int64
|
|
if len(args) > 1 {
|
|
hours, err = strconv.ParseInt(args[1], 10, 32)
|
|
if err != nil {
|
|
return "Invalid number of hours", true, false
|
|
}
|
|
}
|
|
|
|
err = c.SetChatMemberStatus(chatID, contact.Id, ChatMemberStatusBanned, hours, "", "")
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
// unban @username
|
|
case "unban":
|
|
contact, _, err := c.GetContactByUsername(args[0], false)
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
if contact == nil {
|
|
return "Contact not found", true, false
|
|
}
|
|
|
|
err = c.SetChatMemberStatus(chatID, contact.Id, ChatMemberStatusUnbanned, 0, "", "")
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
// promote @username to admin
|
|
case "promote":
|
|
contact, _, err := c.GetContactByUsername(args[0], false)
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
if contact == nil {
|
|
return "Contact not found", true, false
|
|
}
|
|
|
|
var customTitle string
|
|
if len(args) > 1 {
|
|
customTitle = args[1]
|
|
}
|
|
|
|
// clone the permissions
|
|
err = c.SetChatMemberStatus(chatID, contact.Id, ChatMemberStatusPromoted, 0, customTitle, "")
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
// leave current chat
|
|
case "leave":
|
|
_, err := c.client.LeaveChat(&client.LeaveChatRequest{
|
|
ChatId: chatID,
|
|
})
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
|
|
err = c.leaveChat(chatID)
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
// leave current chat (for owners)
|
|
case "leave!":
|
|
err := c.DeleteChat(chatID)
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
|
|
err = c.leaveChat(chatID)
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
// set TTL
|
|
case "ttl":
|
|
var ttl int64
|
|
var err error
|
|
if len(args) > 0 {
|
|
ttl, err = strconv.ParseInt(args[0], 10, 32)
|
|
if err != nil {
|
|
return "Invalid TTL", true, false
|
|
}
|
|
}
|
|
_, err = c.client.SetChatMessageAutoDeleteTime(&client.SetChatMessageAutoDeleteTimeRequest{
|
|
ChatId: chatID,
|
|
MessageAutoDeleteTime: int32(ttl),
|
|
})
|
|
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
// close secret chat
|
|
case "close":
|
|
chat, _, err := c.GetContactByID(chatID, nil, true)
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
if chat == nil {
|
|
return "Chat not found", true, false
|
|
}
|
|
|
|
chatType := chat.Type.ChatTypeType()
|
|
if chatType == client.TypeChatTypeSecret {
|
|
chatTypeSecret, _ := chat.Type.(*client.ChatTypeSecret)
|
|
_, err = c.client.CloseSecretChat(&client.CloseSecretChatRequest{
|
|
SecretChatId: chatTypeSecret.SecretChatId,
|
|
})
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
|
|
err = c.leaveChat(chatID)
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
}
|
|
// delete current chat
|
|
case "delete":
|
|
_, err := c.client.DeleteChatHistory(&client.DeleteChatHistoryRequest{
|
|
ChatId: chatID,
|
|
RemoveFromChatList: true,
|
|
Revoke: true,
|
|
})
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
|
|
err = c.leaveChat(chatID)
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
// message search
|
|
case "search":
|
|
var limit int32 = 100
|
|
if len(args) > 1 {
|
|
newLimit, err := strconv.ParseInt(args[1], 10, 32)
|
|
if err == nil {
|
|
limit = int32(newLimit)
|
|
}
|
|
}
|
|
|
|
var query string
|
|
if len(args) > 0 {
|
|
query = args[0]
|
|
}
|
|
|
|
messages, err := c.getLastMessages(chatID, query, 0, limit)
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
|
|
c.sendMessagesReverse(chatID, messages.Messages, true, "")
|
|
// get latest entries from history
|
|
case "history":
|
|
var limit int32 = 10
|
|
if len(args) > 0 {
|
|
newLimit, err := strconv.ParseInt(args[0], 10, 32)
|
|
if err == nil {
|
|
limit = int32(newLimit)
|
|
}
|
|
}
|
|
|
|
messages, err := c.getNLastMessages(chatID, NewMessageLimitMessages(limit))
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
c.sendMessagesReverse(chatID, messages, true, "")
|
|
// chat members
|
|
case "members":
|
|
var query string
|
|
if len(args) > 0 {
|
|
query = args[0]
|
|
}
|
|
|
|
members, err := c.GetChatMembers(chatID, false, query, MembersListMembers)
|
|
if err != nil {
|
|
return err.Error(), true, false
|
|
}
|
|
|
|
var entries []string
|
|
for _, member := range members {
|
|
senderId := c.GetSenderId(member.MemberId)
|
|
entries = append(entries, fmt.Sprintf(
|
|
"%v | role: %v",
|
|
c.FormatContact(senderId),
|
|
member.Status.ChatMemberStatusType(),
|
|
))
|
|
}
|
|
|
|
return strings.Join(entries, "\n"), true, true
|
|
case "help":
|
|
return c.helpString(CommandTypeChat, chatID), true, true
|
|
default:
|
|
return "", false, false
|
|
}
|
|
|
|
return "", true, true
|
|
}
|
|
|
|
func (c *Client) cmdAdd(args []string) (string, bool) {
|
|
chat, err := c.client.SearchPublicChat(&client.SearchPublicChatRequest{
|
|
Username: args[0],
|
|
})
|
|
if err != nil {
|
|
return err.Error(), false
|
|
}
|
|
if chat == nil {
|
|
return "No error, but chat is nil", false
|
|
}
|
|
|
|
c.subscribeToID(chat.Id, chat, true)
|
|
|
|
return "", true
|
|
}
|
|
|
|
func (c *Client) cmdJoin(args []string) (string, bool) {
|
|
if strings.HasPrefix(args[0], "@") {
|
|
chat, err := c.client.SearchPublicChat(&client.SearchPublicChatRequest{
|
|
Username: args[0],
|
|
})
|
|
if err != nil {
|
|
return err.Error(), false
|
|
}
|
|
if chat == nil {
|
|
return "No error, but chat is nil", false
|
|
}
|
|
_, err = c.client.JoinChat(&client.JoinChatRequest{
|
|
ChatId: chat.Id,
|
|
})
|
|
if err != nil {
|
|
return err.Error(), false
|
|
}
|
|
} else {
|
|
_, err := c.client.JoinChatByInviteLink(&client.JoinChatByInviteLinkRequest{
|
|
InviteLink: args[0],
|
|
})
|
|
if err != nil {
|
|
return err.Error(), false
|
|
}
|
|
}
|
|
|
|
return "", true
|
|
}
|
|
|
|
func (c *Client) cmdSupergroup(args []string, cmdline string) (string, bool) {
|
|
_, err := c.client.CreateNewSupergroupChat(&client.CreateNewSupergroupChatRequest{
|
|
Title: args[0],
|
|
Description: rawCmdArguments(cmdline, 1),
|
|
})
|
|
if err != nil {
|
|
return err.Error(), false
|
|
}
|
|
|
|
return "", true
|
|
}
|
|
|
|
func (c *Client) cmdChannel(args []string, cmdline string) (string, bool) {
|
|
_, err := c.client.CreateNewSupergroupChat(&client.CreateNewSupergroupChatRequest{
|
|
Title: args[0],
|
|
Description: rawCmdArguments(cmdline, 1),
|
|
IsChannel: true,
|
|
})
|
|
if err != nil {
|
|
return err.Error(), false
|
|
}
|
|
|
|
return "", true
|
|
}
|