package telegram import ( "fmt" "github.com/pkg/errors" "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 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 (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)"}, "muc": configurationOption{"", "use MUCs instead of the legacy PM representation of group chats (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 configurationOption struct { arguments string description string } func (c *Client) helpString(typ gateway.CommandType, chatId int64) string { var str strings.Builder commandMap := gateway.GetCommands(typ) chatType, _, _ := c.GetChatType(chatId, true) str.WriteString("Available commands:\n") if typ == gateway.CommandTypeTransport { gateway.CommandsToHelpString(&str, chatType, gateway.OnlineFilterNotOnline, commandMap) str.WriteString("\nOnline-only commands:\n") gateway.CommandsToHelpString(&str, chatType, gateway.OnlineFilterOnline, commandMap) str.WriteString("\nConfiguration 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") } } else if typ == gateway.CommandTypeChat { gateway.CommandsToHelpString(&str, chatType, gateway.OnlineFilterAny, commandMap) } 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 := gateway.TransportCommands[cmd] if !ok { return unknownCommand, false } if len(args) < command.RequiredArgs { return notEnoughArguments, false } if command.OnlineOnly && !c.Online() { return notOnline, 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] } } return "", true // 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 } c.unsubscribeFromAll() c.Session.Login = "" c.wizardStageOrPrompt(LoginStageCancel, "") c.online = false // cleanup case "cleanup": c.unsubscribeFromAll() // 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": 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": _, 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": 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 "status": return fmt.Sprintf("Login stage: %v\nLogin: %v", c.loginStage, c.Session.Login), 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(gateway.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 " + gateway.TransportCommands["preset"].Arguments[0], false } case "pass": switch c.loginStage { case LoginStagePreset: go c.promptMUC() case LoginStageMUC: c.wizardStageOrPrompt(LoginStageSuccess, "") default: return "Not applicable here", false } case "finish": switch c.loginStage { case LoginStagePreset, LoginStageMUC: c.wizardStageOrPrompt(LoginStageSuccess, "") default: return "Not applicable here", false } } 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) { cmd, args := parseCommand(cmdline) command, ok := gateway.ChatCommands[cmd] if !ok { return unknownCommand, false, false } if len(args) < command.RequiredArgs { return notEnoughArguments, true, false } if command.OnlineOnly && !c.Online() { return notOnline, true, false } chatType, _, chatTypeErr := c.GetChatType(chatID, true) if chatTypeErr == nil && !gateway.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 } return "", true, true // 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 } return "", true, true // 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 { due += c.GetTZD() 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 } return "", true, true // 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, "") return "", true, 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, "") return "", true, 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(gateway.CommandTypeChat, chatID), true, true default: return "", false, false } return "Success", 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 "Subscription sent", 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 "Joined", 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 "Created", 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 "Created", true }