telegabber/telegram/commands.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, &notForGroups},
"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, &notForGroups},
"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, &notForGroups},
"unblock": command{0, []string{}, "unblacklist current user", true, &notForGroups},
"invite": command{1, []string{"id or @username"}, "add user to current chat", true, &notForPM},
"link": command{0, []string{}, "get invite link for current chat", true, &notForPM},
"kick": command{1, []string{"id or @username"}, "remove user from current chat", true, &notForPM},
"mute": command{0, []string{"id or @username", "hours"}, "mute the whole chat or a user in current chat", true, &notForPMAndBasic},
"unmute": command{0, []string{"id or @username"}, "unmute the whole chat or a user in the current chat", true, &notForPMAndBasic},
"ban": command{1, []string{"id or @username", "hours"}, "restrict @username from current chat for [hours] or forever", true, &notForPM},
"unban": command{1, []string{"id or @username"}, "unbans @username in current chat (and devotes from admins)", true, &notForPM},
"promote": command{1, []string{"id or @username", "title"}, "promote user to admin in current chat", true, &notForPM},
"leave": command{0, []string{}, "leave current chat", true, &notForPM},
"leave!": command{0, []string{}, "leave current chat (for owners)", true, &notForPM},
"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)
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.ChatsKeys() {
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])
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)
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])
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])
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])
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])
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])
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])
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])
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)
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
}