package telegram import ( "bytes" "crypto/sha1" "encoding/base64" "fmt" "github.com/pkg/errors" "io" "io/ioutil" "net/http" "os" osUser "os/user" "path/filepath" "regexp" "strconv" "strings" "time" "unicode/utf8" "dev.narayana.im/narayana/telegabber/telegram/cache" "dev.narayana.im/narayana/telegabber/telegram/formatter" "dev.narayana.im/narayana/telegabber/xmpp/gateway" log "github.com/sirupsen/logrus" "github.com/soheilhy/args" "github.com/zelenin/go-tdlib/client" ) type VCardInfo struct { Fn string Photo *client.File Nicknames []string Given string Family string Tel string Info string } type messageStub struct { MessageId int64 ChatId int64 Sender string Date int32 Text string } type BotCommand struct { Command string Description string } type BotLink struct { Description string Link string } const ( typeFileDataSha1 byte = iota typeFileDataBase64 ) var errOffline = errors.New("TDlib instance is offline") var errOverLimit = errors.New("Over limit") var spaceRegex = regexp.MustCompile(`\s+`) var replyRegex = regexp.MustCompile("\\A>>? ?([0-9]+)\\n") const newlineChar string = "\n" const messageHeaderSeparator string = " | " // no hrunicode allowed here yet // ChatType is an enum of chat types, roughly corresponding to TDLib's one but better type ChatType int const ( ChatTypeUnknown ChatType = iota ChatTypePrivate ChatTypeBasicGroup ChatTypeSupergroup ChatTypeSecret ChatTypeChannel ) // MembersList is an enum of member list filters type MembersList int const ( MembersListNone MembersList = iota MembersListMembers MembersListRestricted MembersListBanned MembersListBannedAndAdministrators MembersListAdministrators MembersListCreators ) const ( MessageLimitMessages = iota MessageLimitChars MessageLimitSince ) // MessageLimitType is an enum of MUC history limit types type MessageLimitType int // MessageLimit stores a MUC history limit type MessageLimit struct { Type MessageLimitType Messages int32 Chars int Since int64 } func NewMessageLimitMessages(stanzas int32) *MessageLimit { var limit MessageLimit limit.Type = MessageLimitMessages limit.Messages = stanzas return &limit } func NewMessageLimitChars(chars int) *MessageLimit { var limit MessageLimit limit.Type = MessageLimitChars limit.Chars = chars return &limit } func NewMessageLimitSince(since int64) *MessageLimit { var limit MessageLimit limit.Type = MessageLimitSince limit.Since = since return &limit } const AVATAR_SIZE_LIMIT int64 = 128 * 1024 const ( ChatMemberStatusIllegal = iota ChatMemberStatusKicked ChatMemberStatusMuted ChatMemberStatusUnmuted ChatMemberStatusBanned ChatMemberStatusUnbanned ChatMemberStatusPromoted ) type ChatMemberStatus int // GetContactByUsername resolves username to user id retrieves user and chat information func (c *Client) GetContactByUsername(username string) (*client.Chat, *client.User, error) { if !c.Online() { return nil, nil, errOffline } var chat *client.Chat var err error var userID int64 if strings.HasPrefix(username, "@") { chat, err = c.client.SearchPublicChat(&client.SearchPublicChatRequest{ Username: username, }) if err != nil { return nil, nil, err } userID = chat.Id } else { userID, err = strconv.ParseInt(username, 10, 64) if err != nil { return nil, nil, err } } return c.GetContactByID(userID, chat) } // GetContactByID gets user and chat information from cache (or tries to retrieve it, if missing) func (c *Client) GetContactByID(id int64, chat *client.Chat) (*client.Chat, *client.User, error) { if !c.Online() || id == 0 { return nil, nil, errOffline } var user *client.User var cacheChat *client.Chat var ok bool var err error user, ok = c.cache.GetUser(id) if !ok && id > 0 { user, err = c.client.GetUser(&client.GetUserRequest{ UserId: id, }) if err == nil { c.cache.SetUser(id, user) } } cacheChat, ok = c.cache.GetChat(id) if !ok { if chat == nil { cacheChat, err = c.client.GetChat(&client.GetChatRequest{ ChatId: id, }) if err != nil { // error is irrelevant if the user was found successfully if user != nil { return nil, user, nil } return nil, nil, err } c.cache.SetChat(id, cacheChat) } else { c.cache.SetChat(id, chat) } } if chat == nil { chat = cacheChat } return chat, user, nil } // GetChatByID gets exactly a chat from a cache, or error if chat is not found func (c *Client) GetChatByID(id int64, chat *client.Chat) (*client.Chat, error) { chat, _, err := c.GetContactByID(id, nil) if err != nil { return nil, err } else if chat == nil { return nil, errors.New("Chat not found") } return chat, nil } // GetChatType obtains chat type from its information func (c *Client) GetChatType(id int64) (ChatType, *client.Chat, error) { if !c.Online() || id == 0 { return ChatTypeUnknown, nil, errOffline } var err error chat, ok := c.cache.GetChat(id) if !ok { chat, err = c.client.GetChat(&client.GetChatRequest{ ChatId: id, }) if err != nil { return ChatTypeUnknown, nil, err } c.cache.SetChat(id, chat) } chatType := chat.Type.ChatTypeType() if chatType == client.TypeChatTypePrivate { return ChatTypePrivate, chat, nil } else if chatType == client.TypeChatTypeBasicGroup { return ChatTypeBasicGroup, chat, nil } else if chatType == client.TypeChatTypeSupergroup { supergroup, _ := chat.Type.(*client.ChatTypeSupergroup) if supergroup.IsChannel { return ChatTypeChannel, chat, nil } return ChatTypeSupergroup, chat, nil } else if chatType == client.TypeChatTypeSecret { return ChatTypeSecret, chat, nil } return ChatTypeUnknown, chat, errors.New("Unknown chat type") } // IsPM checks if a chat is PM func (c *Client) IsPM(id int64) (bool, *client.Chat, error) { typ, chat, err := c.GetChatType(id) if err != nil { return false, chat, err } if typ == ChatTypePrivate || typ == ChatTypeSecret { return true, chat, nil } return false, chat, nil } // IsBot checks if a chat is a bot func (c *Client) IsBot(id int64) (bool, error) { _, user, err := c.GetContactByID(id, nil) if err != nil { return false, err } if user == nil || user.Type == nil { return false, nil } return user.Type.UserTypeType() == client.TypeUserTypeBot, nil } // GetBotMenu retrieves the bot's attachment menu func (c *Client) GetBotMenu(id int64) (*BotLink, []*BotCommand, error) { fullInfo, err := c.client.GetUserFullInfo(&client.GetUserFullInfoRequest{ UserId: id, }) if err == nil && fullInfo.BotInfo != nil { if fullInfo.BotInfo.MenuButton != nil { menuButton := fullInfo.BotInfo.MenuButton return &BotLink{menuButton.Text, menuButton.Url}, nil, nil } else { var commands []*BotCommand for _, command := range fullInfo.BotInfo.Commands { commands = append(commands, &BotCommand{command.Command, command.Description}) } return nil, commands, nil } } return nil, nil, err } func (c *Client) userStatusToText(status client.UserStatus, chatID int64) (string, string, string) { var show, textStatus, presenceType string switch status.UserStatusType() { case client.TypeUserStatusOnline: onlineStatus, _ := status.(*client.UserStatusOnline) c.DelayedStatusesLock.Lock() c.DelayedStatuses[chatID] = &DelayedStatus{ TimestampOnline: time.Now().Unix(), TimestampExpired: int64(onlineStatus.Expires), } c.DelayedStatusesLock.Unlock() textStatus = "Online" case client.TypeUserStatusRecently: show, textStatus = "dnd", "Last seen recently" c.DelayedStatusesLock.Lock() delete(c.DelayedStatuses, chatID) c.DelayedStatusesLock.Unlock() case client.TypeUserStatusLastWeek: show, textStatus = "xa", "Last seen last week" case client.TypeUserStatusLastMonth: show, textStatus = "xa", "Last seen last month" case client.TypeUserStatusEmpty: presenceType, textStatus = "unavailable", "Last seen a long time ago" case client.TypeUserStatusOffline: offlineStatus, _ := status.(*client.UserStatusOffline) // this will stop working in 2038 O\ wasOnline := int64(offlineStatus.WasOnline) elapsed := time.Now().Unix() - wasOnline if elapsed < 3600 { show = "away" } else { show = "xa" } textStatus = c.LastSeenStatus(wasOnline) c.DelayedStatusesLock.Lock() delete(c.DelayedStatuses, chatID) c.DelayedStatusesLock.Unlock() } return show, textStatus, presenceType } // LastSeenStatus formats a timestamp to a "Last seen at" string func (c *Client) LastSeenStatus(timestamp int64) string { return time.Unix(int64(timestamp), 0). In(c.Session.TimezoneToLocation()). Format("Last seen at 15:04 02/01/2006") } func (c *Client) getFileData(tgFile *client.File, typ byte) string { var priority int32 if typ == typeFileDataSha1 { priority = 1 } else if typ == typeFileDataBase64 { priority = 32 } // avoid not-well-formed stanza errors if typ == typeFileDataBase64 && c.GetPhotoSize(tgFile) > AVATAR_SIZE_LIMIT { log.Warnf("Photo %v skipped as it's too huge", tgFile.Id) return "" } file, path, err := c.ForceOpenFile(tgFile, priority) if err == nil { defer file.Close() if typ == typeFileDataSha1 { hash := sha1.New() _, err = io.Copy(hash, file) if err == nil { return fmt.Sprintf("%x", hash.Sum(nil)) } else { log.Errorf("Error calculating hash: %v", path) } } else if typ == typeFileDataBase64 { buf := new(bytes.Buffer) binval := base64.NewEncoder(base64.StdEncoding, buf) _, err = io.Copy(binval, file) binval.Close() if err == nil { return buf.String() } else { log.Errorf("Error calculating base64: %v", path) } } } else if path != "" { log.Errorf("Photo does not exist: %v", path) } else { log.Errorf("PHOTO: %#v", err.Error()) } return "" } // SetEmptyAvatarHash puts a dummy value into the cache to avoid attempting to fetch surely missing avatars func (c *Client) SetEmptyAvatarHash(chatId int64) { c.avatarHashesLock.Lock() c.avatarHashes[chatId] = &HashedAvatar{ Hash: "", File: 0, } c.avatarHashesLock.Unlock() } // GetPhotoSize return at least a rough size func (c *Client) GetPhotoSize(photo *client.File) int64 { if photo == nil { return 0 } size := photo.Size if size == 0 { size = photo.ExpectedSize } return size } // GetPhotoSha1 computes the photo hash func (c *Client) GetPhotoSha1(photo *client.File, chatId int64) string { sha1 := c.getFileData(photo, typeFileDataSha1) c.avatarHashesLock.Lock() c.avatarHashes[chatId] = &HashedAvatar{ Hash: sha1, File: photo.Id, } c.avatarHashesLock.Unlock() return sha1 } // GetPhotoBase64 reads file data as Base64 func (c *Client) GetPhotoBase64(photo *client.File) string { return c.getFileData(photo, typeFileDataBase64) } // GetHashedAvatar obtain the avatar hash from cache or requests the avatar file immediately to calculate it func (c *Client) GetHashedAvatar(chatId int64) *HashedAvatar { c.avatarHashesLock.Lock() hashedAvatar, ok := c.avatarHashes[chatId] c.avatarHashesLock.Unlock() if !ok { log.Info("Could not find avatar in cache, fetching immediately") chat, _, err := c.GetContactByID(chatId, nil) if err != nil || chat == nil || chat.Photo == nil { return nil } file := chat.Photo.Small sha1 := c.GetPhotoSha1(file, chatId) hashedAvatar = &HashedAvatar{ Hash: sha1, File: file.Id, } } return hashedAvatar } // ProcessStatusUpdate sets contact status func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, oldArgs ...args.V) error { if !c.Online() { return nil } log.WithFields(log.Fields{ "chat_id": chatID, }).Info("Status update for") chat, user, err := c.GetContactByID(chatID, nil) if err != nil { return err } var isMUC bool if chat != nil && c.Session.MUC && c.IsGroup(chat) { // allow MUC presence hack for avatars, still discard the rest if status != "" || show != "" { return nil } isMUC = true } var photo string if chat != nil && chat.Photo != nil { photo = c.GetPhotoSha1(chat.Photo.Small, chatID) } var presenceType string if gateway.SPType.IsSet(oldArgs) { presenceType = gateway.SPType.Get(oldArgs) } // skip cache for MUCs if !isMUC { cachedStatus, ok := c.cache.GetStatus(chatID) if status == "" { if ok { var typ string show, status, typ = cachedStatus.Destruct() if presenceType == "" { presenceType = typ } log.WithFields(log.Fields{ "show": show, "status": status, "presenceType": presenceType, }).Debug("Cached status") } else if user != nil && user.Status != nil { show, status, presenceType = c.userStatusToText(user.Status, chatID) log.WithFields(log.Fields{ "show": show, "status": status, "presenceType": presenceType, }).Debug("Status to text") } else { show, status = "chat", chat.Title } } cacheShow := show if presenceType == "unavailable" { cacheShow = presenceType } c.cache.SetStatus(chatID, cacheShow, status) } newArgs := []args.V{ gateway.SPShow(show), gateway.SPStatus(status), gateway.SPPhoto(photo), gateway.SPImmed(gateway.SPImmed.Get(oldArgs)), } if presenceType != "" { newArgs = append(newArgs, gateway.SPType(presenceType)) } c.locks.mucCacheLock.Lock() chatJid := gateway.CHATJID(chatID, true) for mucId, state := range c.mucCache { occupant, ok := state.Occupants[chatID] if ok { newMucArgs := append( newArgs, gateway.SPFrom(gateway.MUCNODE(mucId)), gateway.SPResource(occupant.Nickname), gateway.SPMUCAffiliation(occupant.Affiliation), gateway.SPMUCRole(occupant.Role), gateway.SPMUCJid(chatJid), ) err := c.sendPresence(newMucArgs...) if err != nil { c.locks.mucCacheLock.Unlock() return err } } } c.locks.mucCacheLock.Unlock() if isMUC { newArgs = append(newArgs, gateway.SPFullFrom(gateway.MUCJID(chatID))) } else { newArgs = gateway.SPAppendFrom(newArgs, chatID) } return c.sendPresence(newArgs...) } // JoinMUC saves MUC join fact and sends initialization data func (c *Client) JoinMUC(chatId int64, resource string, limit *MessageLimit) { // save the nickname in this MUC, also as a marker of join c.locks.mucCacheLock.Lock() mucState, ok := c.mucCache[chatId] if !ok || mucState == nil { mucState = NewMUCState() c.mucCache[chatId] = mucState } _, ok = mucState.Resources[resource] if ok { // already joined, initializing anyway } else { mucState.Resources[resource] = true } c.locks.mucCacheLock.Unlock() log.Debugf("Resources in MUC %v: %v", chatId, mucState.Resources) c.sendMUCStatuses(chatId) messages, err := c.getNLastMessages(chatId, limit) if err == nil { c.sendMessagesReverse(chatId, messages, false, c.jid+"/"+resource) } c.sendMUCSubject(chatId, resource) } // LeaveMUC removes MUC date from the cache func (c *Client) LeaveMUC(chatId int64, resource string) { c.locks.mucCacheLock.Lock() defer c.locks.mucCacheLock.Unlock() mucState, ok := c.mucCache[chatId] if !ok || mucState == nil { return } delete(mucState.Resources, resource) log.Debugf("Resources in MUC %v: %v", chatId, mucState.Resources) if len(mucState.Resources) == 0 { delete(c.mucCache, chatId) } } // DestroyMUC removes everyone from the MUC func (c *Client) DestroyMUC(chatId int64) error { err := c.DeleteChat(chatId) if err != nil { return err } c.locks.mucCacheLock.Lock() defer c.locks.mucCacheLock.Unlock() mucState, ok := c.mucCache[chatId] if !ok || mucState == nil { return nil } c.kickMeFromMUC(chatId, nil, true, mucState) delete(c.mucCache, chatId) return nil } func (c *Client) sendMUCStatuses(chatID int64) { c.locks.mucCacheLock.Lock() defer c.locks.mucCacheLock.Unlock() mucState, ok := c.mucCache[chatID] if !ok || mucState == nil { mucState = NewMUCState() c.mucCache[chatID] = mucState } members, _ := c.client.SearchChatMembers(&client.SearchChatMembersRequest{ ChatId: chatID, Limit: 200, Filter: &client.ChatMembersFilterMembers{}, }) c.updateMUCOccupants(mucState, chatID, members.Members) } func (c *Client) updateMUCOccupants(mucState *MUCState, chatID int64, members []*client.ChatMember) { sChatId := gateway.MUCNODE(chatID) myNickname := "me" var myJid string if c.me != nil { myNickname = c.GetMUCNickname(c.me.Id) myJid = gateway.CHATJID(c.me.Id, true) } myAffiliation := "member" myRole := "participant" chat, _, _ := c.GetContactByID(chatID, nil) for _, member := range members { senderId, nickname, affiliation, role := c.TgMemberToMUCOccupant(member, chat) mucState.Occupants[senderId] = &MUCOccupant{ Nickname: nickname, Affiliation: affiliation, Role: role, Status: member.Status, } if c.me != nil && senderId == c.me.Id { myNickname = nickname myAffiliation = affiliation myRole = role continue } c.sendPresence( gateway.SPFrom(sChatId), gateway.SPResource(nickname), gateway.SPImmed(true), gateway.SPMUCAffiliation(affiliation), gateway.SPMUCRole(role), gateway.SPMUCJid(gateway.CHATJID(senderId, true)), ) } // according to the spec, own occupant entry should be sent the last c.sendPresence( gateway.SPFrom(sChatId), gateway.SPResource(myNickname), gateway.SPImmed(true), gateway.SPMUCAffiliation(myAffiliation), gateway.SPMUCRole(myRole), gateway.SPMUCJid(myJid), gateway.SPMUCStatusCodes([]uint16{100, 110, 210}), ) } func (c *Client) mucCacheHasOccupant(mucID int64, memberID int64) bool { c.locks.mucCacheLock.Lock() defer c.locks.mucCacheLock.Unlock() mucState, ok := c.mucCache[mucID] if !ok || mucState == nil { return false // no MUC to be added to } _, ok = mucState.Occupants[memberID] return ok } func (c *Client) addMUCOccupant(mucID int64, memberID int64, affiliation, role string, status client.ChatMemberStatus) bool { c.locks.mucCacheLock.Lock() defer c.locks.mucCacheLock.Unlock() mucState, ok := c.mucCache[mucID] if !ok || mucState == nil { return false } nickname := c.GetMUCNickname(memberID) err := c.sendPresence( gateway.SPFrom(gateway.MUCNODE(mucID)), gateway.SPResource(nickname), gateway.SPImmed(true), gateway.SPMUCAffiliation(affiliation), gateway.SPMUCRole(role), gateway.SPMUCJid(gateway.CHATJID(memberID, true)), ) if err == nil { mucState.Occupants[memberID] = &MUCOccupant{ Nickname: nickname, Affiliation: affiliation, Role: role, Status: status, } return true } return false } func (c *Client) sendMUCSubject(chatID int64, resource string) { pin, err := c.client.GetChatPinnedMessage(&client.GetChatPinnedMessageRequest{ ChatId: chatID, }) mucJid := gateway.MUCJID(chatID) toJid := c.jid + "/" + resource if err == nil { gateway.SendSubjectMessage( toJid, mucJid + "/" + c.GetMUCNickname(c.getMessageSenderId(pin)), c.messageToText(pin, false), strconv.FormatInt(pin.Id, 10), c.xmpp, int64(pin.Date), ) } else { gateway.SendSubjectMessage(toJid, mucJid, "", "", c.xmpp, 0) } } // GetMUCNickname generates a unique nickname for a MUC occupant func (c *Client) GetMUCNickname(chatID int64) string { if chatID == 0 { if c.me != nil { chatID = c.me.Id } else { return "me" } } return c.FormatContact(chatID) } func (c *Client) updateMUCsNickname(memberID int64, newNickname string) { c.locks.mucCacheLock.Lock() defer c.locks.mucCacheLock.Unlock() realJid := gateway.CHATJID(memberID, true) for mucId, state := range c.mucCache { oldOccupant, ok := state.Occupants[memberID] if ok { state.Occupants[memberID] = &MUCOccupant{ Nickname: newNickname, Affiliation: oldOccupant.Affiliation, Role: oldOccupant.Role, Status: oldOccupant.Status, } sMucId := gateway.MUCNODE(mucId) unavailableStatusCodes := []uint16{303, 210} availableStatusCodes := []uint16{100, 210} if c.me != nil && memberID == c.me.Id { unavailableStatusCodes = append(unavailableStatusCodes, 110) availableStatusCodes = append(availableStatusCodes, 110) } c.sendPresence( gateway.SPType("unavailable"), gateway.SPFrom(sMucId), gateway.SPResource(oldOccupant.Nickname), gateway.SPImmed(true), gateway.SPMUCAffiliation(oldOccupant.Affiliation), gateway.SPMUCRole(oldOccupant.Role), gateway.SPMUCNick(newNickname), gateway.SPMUCStatusCodes(unavailableStatusCodes), gateway.SPMUCJid(realJid), ) c.sendPresence( gateway.SPFrom(sMucId), gateway.SPResource(newNickname), gateway.SPImmed(true), gateway.SPMUCAffiliation(oldOccupant.Affiliation), gateway.SPMUCRole(oldOccupant.Role), gateway.SPMUCStatusCodes(availableStatusCodes), gateway.SPMUCJid(realJid), ) } } } // MUCHasResource checks if a MUC was joined from a given resource func (c *Client) MUCHasResource(chatID int64, resource string) bool { c.locks.mucCacheLock.Lock() defer c.locks.mucCacheLock.Unlock() mucState, ok := c.mucCache[chatID] if !ok || mucState == nil { return false } _, ok = mucState.Resources[resource] return ok } func (c *Client) getMUCJoinedJIDs(chatId int64, mucState *MUCState, lock bool) (bool, []string) { if lock { c.locks.mucCacheLock.Lock() defer c.locks.mucCacheLock.Unlock() } groupChatTos := []string{} if mucState == nil { mucState, _ = c.mucCache[chatId] } if mucState == nil { return false, nil } for resource := range mucState.Resources { groupChatTos = append(groupChatTos, c.jid + "/" + resource) } return true, groupChatTos } // GetMyMUCNickname obtains this account's nickname in a given MUC func (c *Client) GetMyMUCNickname(chatID int64) (string, bool) { if c.me == nil { return "", false } c.locks.mucCacheLock.Lock() defer c.locks.mucCacheLock.Unlock() mucState, ok := c.mucCache[chatID] if !ok || mucState == nil { return "", false } occupant, ok := mucState.Occupants[c.me.Id] if !ok { return "", false } return occupant.Nickname, true } // GetMUCMemberIdByNickname looks up the telegram ID by the MUC nickname (slow yet! (TODO)) func (c *Client) GetMUCMemberIdByNickname(chatID int64, nickname string) int64 { c.locks.mucCacheLock.Lock() defer c.locks.mucCacheLock.Unlock() mucState, ok := c.mucCache[chatID] if !ok || mucState == nil { return 0 } for memberId, occupant := range mucState.Occupants { if occupant.Nickname == nickname { return memberId } } return 0 } // NewPinnedMessage sends a text message and pins it right away func (c *Client) NewPinnedMessage(chatID int64, text, returnJid string) bool { c.locks.pinOutboxLock.Lock() msg, _ := c.ProcessOutgoingMessage(chatID, text, returnJid, 0, 0, true, true) if msg == nil { c.locks.pinOutboxLock.Unlock() return false } ch := make(chan int64) key := IntPair{chatID, msg.Id} c.pinOutbox[key] = ch c.locks.pinOutboxLock.Unlock() newId := <-ch c.locks.pinOutboxLock.Lock() delete(c.pinOutbox, key) c.locks.pinOutboxLock.Unlock() if newId == 0 { return false } ok, err := c.client.PinChatMessage(&client.PinChatMessageRequest{ ChatId: chatID, MessageId: newId, }) if err != nil { log.Errorf("failed to pin message: %v", err.Error()) c.client.DeleteMessages(&client.DeleteMessagesRequest{ ChatId: chatID, MessageIds: []int64{msg.Id}, Revoke: true, }) } return ok != nil } // FormatContact retrieves a complete "full name (@usernames)" string for display func (c *Client) FormatContact(chatID int64) string { if chatID == 0 { return "" } chat, user, err := c.GetContactByID(chatID, nil) if err != nil { return "unknown contact: " + err.Error() } var str string if chat != nil { str = fmt.Sprintf("%s (%v)", chat.Title, chat.Id) } else if user != nil { var usernames string if user.Usernames != nil { usernames = c.usernamesToString(user.Usernames.ActiveUsernames) } if usernames == "" { usernames = strconv.FormatInt(user.Id, 10) } str = fmt.Sprintf("%s %s (%v)", user.FirstName, user.LastName, usernames) } else { str = strconv.FormatInt(chatID, 10) } str = spaceRegex.ReplaceAllString(str, " ") return str } // GetSenderId extracts a sender id from a message func (c *Client) GetSenderId(sender client.MessageSender) (senderId int64) { switch sender.MessageSenderType() { case client.TypeMessageSenderUser: senderUser, _ := sender.(*client.MessageSenderUser) senderId = senderUser.UserId case client.TypeMessageSenderChat: senderChat, _ := sender.(*client.MessageSenderChat) senderId = senderChat.ChatId } return } func (c *Client) getMessageSenderId(message *client.Message) (senderId int64) { if message.SenderId != nil { senderId = c.GetSenderId(message.SenderId) } return } func (c *Client) formatSender(message *client.Message) string { return c.FormatContact(c.getMessageSenderId(message)) } func (c *Client) messageToStub(message *client.Message, preview bool, text string) *messageStub { if text == "" { text = c.messageContentToText(message.Content, message.ChatId, preview) } return &messageStub{ MessageId: message.Id, ChatId: message.ChatId, Sender: c.formatSender(message), Date: message.Date, Text: text, } } func (c *Client) getMessageReply(message *client.Message, preview bool, noContent bool) (gatewayReply *gateway.Reply, tgReply *messageStub) { if message.ReplyTo != nil && message.ReplyTo.MessageReplyToType() == client.TypeMessageReplyToMessage { replyTo, _ := message.ReplyTo.(*client.MessageReplyToMessage) var text string if replyTo.Quote != nil && replyTo.Quote.Text != nil && !noContent { text = formatter.Format( replyTo.Quote.Text.Text, replyTo.Quote.Text.Entities, c.getFormatter(), ) // make the whole quote fit one line text = strings.ReplaceAll(text, "\n", " ") } if message.ChatId == replyTo.ChatId { // obtain message from this chat replyMsg, err := c.client.GetMessage(&client.GetMessageRequest{ ChatId: message.ChatId, MessageId: replyTo.MessageId, }) if err != nil { log.Errorf("", err.Error()) return } if !noContent { tgReply = c.messageToStub(replyMsg, preview, text) } replyId, err := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, message.ChatId, replyTo.MessageId) if err != nil { replyId = strconv.FormatInt(replyTo.MessageId, 10) } gatewayReply = &gateway.Reply{ Author: fmt.Sprintf("%v@%s", c.getMessageSenderId(replyMsg), gateway.Jid.Full()), Id: replyId, } } else if !noContent { // it's safe to assume there's no need to pass ChatId here // as it's needed only for pin messages which are not allowed in replies if text == "" && replyTo.Content != nil { text = c.messageContentToText(replyTo.Content, 0, preview) } if text == "" { log.Error("Empty reply from other/unknown chat") log.Debugf("replyTo: %#v", replyTo) return } tgReply = &messageStub{ Sender: c.formatOrigin(replyTo.Origin) + " @ " + c.FormatContact(replyTo.ChatId), Date: replyTo.OriginSendDate, Text: text, } } } return } func (c *Client) formatMessage(chatID int64, messageID int64, preview bool, sender bool, message *client.Message) string { var err error if message == nil { message, err = c.client.GetMessage(&client.GetMessageRequest{ ChatId: chatID, MessageId: messageID, }) if err != nil { return fmt.Sprintf("", err.Error()) } } if message == nil { return "" } return c.formatMessageContent(preview, c.messageToStub(message, preview, ""), sender) } func (c *Client) formatMessageContent(preview bool, message *messageStub, sender bool) string { var str strings.Builder // add messageid and sender if message.MessageId != 0 { str.WriteString(fmt.Sprintf("%v | ", message.MessageId)) } if sender { str.WriteString(fmt.Sprintf("%s | ", message.Sender)) } // add date if !preview { str.WriteString( time.Unix(int64(message.Date), 0). In(c.Session.TimezoneToLocation()). Format("02 Jan 2006 15:04:05 | "), ) } // text message text := message.Text if text != "" { if !preview { str.WriteString(text) } else { newlinePos := strings.Index(text, newlineChar) if newlinePos == -1 { str.WriteString(text) } else { str.WriteString(text[0:newlinePos]) } } } return str.String() } func (c *Client) formatOrigin(origin client.MessageOrigin) string { if origin == nil { return "" } switch origin.MessageOriginType() { case client.TypeMessageOriginUser: originUser := origin.(*client.MessageOriginUser) return c.FormatContact(originUser.SenderUserId) case client.TypeMessageOriginChat: originChat := origin.(*client.MessageOriginChat) var signature string if originChat.AuthorSignature != "" { signature = fmt.Sprintf(" (%s)", originChat.AuthorSignature) } return c.FormatContact(originChat.SenderChatId) + signature case client.TypeMessageOriginHiddenUser: originUser := origin.(*client.MessageOriginHiddenUser) return originUser.SenderName case client.TypeMessageOriginChannel: channel := origin.(*client.MessageOriginChannel) var signature string if channel.AuthorSignature != "" { signature = fmt.Sprintf(" (%s)", channel.AuthorSignature) } return c.FormatContact(channel.ChatId) + signature } return "Unknown origin type" } func (c *Client) formatFile(file *client.File, compact bool) (string, string) { if file == nil { return "", "" } src, link := c.PermastoreFile(file, false) if compact { return link, link } else { return fmt.Sprintf("%s (%v kbytes) | %s", filepath.Base(src), file.Size/1024, link), link } } // PermastoreFile steals a file out of TDlib control into an independent shared directory func (c *Client) PermastoreFile(file *client.File, clone bool) (string, string) { log.Debugf("file: %#v", file) if file == nil || file.Local == nil || file.Remote == nil { return "", "" } gateway.StorageLock.Lock() defer gateway.StorageLock.Unlock() var link string var src string if c.content.Path != "" && c.content.Link != "" { src = file.Local.Path // source path _, err := os.Stat(src) if err != nil { log.Errorf("Cannot access source file: %v", err) return "", "" } size64 := uint64(file.Size) c.prepareDiskSpace(size64) basename := file.Remote.UniqueId + filepath.Ext(src) dest := c.content.Path + "/" + basename // destination path link = c.content.Link + "/" + basename // download link if clone { file, path, err := c.ForceOpenFile(file, 1) if err == nil { defer file.Close() // mode mode := os.FileMode(0644) fi, err := os.Stat(path) if err == nil { mode = fi.Mode().Perm() } // create destination tempFile, err := os.OpenFile(dest, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode) if err != nil { pathErr := err.(*os.PathError) if pathErr.Err.Error() == "file exists" { log.Warn(err.Error()) return src, link } else { log.Errorf("File creation error: %v", err) return "", "" } } defer tempFile.Close() // copy _, err = io.Copy(tempFile, file) if err != nil { log.Errorf("File copying error: %v", err) return "", "" } } else if path != "" { log.Errorf("Source file does not exist: %v", path) return "", "" } else { log.Errorf("PHOTO: %#v", err.Error()) return "", "" } } else { // move err = os.Rename(src, dest) if err != nil { linkErr := err.(*os.LinkError) if linkErr.Err.Error() == "file exists" { log.Warn(err.Error()) } else { log.Errorf("File moving error: %v", err) return "", "" } } } // chown if c.content.User != "" { user, err := osUser.Lookup(c.content.User) if err == nil { uid, err := strconv.ParseInt(user.Uid, 10, 0) if err == nil { err = os.Chown(dest, int(uid), -1) if err != nil { log.Errorf("Chown error: %v", err) } } else { log.Errorf("Broken uid: %v", err) } } else { log.Errorf("Wrong user name for chown: %v", err) } } // copy or move should have succeeded at this point gateway.CachedStorageSize += size64 } return src, link } func (c *Client) formatBantime(hours int64) int32 { var until int32 if hours > 0 { until = int32(time.Now().Unix() + hours*3600) } return until } func (c *Client) formatLocation(location *client.Location) string { return fmt.Sprintf( "coordinates: %v,%v | https://www.google.com/maps/search/%v,%v/", location.Latitude, location.Longitude, location.Latitude, location.Longitude, ) } func (c *Client) messageToText(message *client.Message, preview bool) string { if message.Content == nil { log.Warnf("Unknown message: %#v", message) return "" } return c.messageContentToText(message.Content, message.ChatId, preview) } func (c *Client) messageContentToText(content client.MessageContent, chatId int64, preview bool) string { markupMode := c.getFormatter() switch content.MessageContentType() { case client.TypeMessageSticker: sticker, _ := content.(*client.MessageSticker) return sticker.Sticker.Emoji case client.TypeMessageAnimatedEmoji: animatedEmoji, _ := content.(*client.MessageAnimatedEmoji) return animatedEmoji.Emoji case client.TypeMessageBasicGroupChatCreate, client.TypeMessageSupergroupChatCreate: return "has created chat" case client.TypeMessageChatJoinByLink: return "joined chat via invite link" case client.TypeMessageChatAddMembers: addMembers, _ := content.(*client.MessageChatAddMembers) text := "invited " if len(addMembers.MemberUserIds) > 0 { text += c.FormatContact(addMembers.MemberUserIds[0]) } return text case client.TypeMessageChatDeleteMember: deleteMember, _ := content.(*client.MessageChatDeleteMember) return "kicked " + c.FormatContact(deleteMember.UserId) case client.TypeMessagePinMessage: pinMessage, _ := content.(*client.MessagePinMessage) return "pinned message: " + c.formatMessage(chatId, pinMessage.MessageId, preview, true, nil) case client.TypeMessageChatChangeTitle: changeTitle, _ := content.(*client.MessageChatChangeTitle) return "chat title set to: " + changeTitle.Title case client.TypeMessageLocation: location, _ := content.(*client.MessageLocation) return c.formatLocation(location.Location) case client.TypeMessageVenue: venue, _ := content.(*client.MessageVenue) if preview { return venue.Venue.Title } else { return fmt.Sprintf( "*%s*\n%s\n%s", venue.Venue.Title, venue.Venue.Address, c.formatLocation(venue.Venue.Location), ) } case client.TypeMessagePhoto: photo, _ := content.(*client.MessagePhoto) if preview { return photo.Caption.Text } else { return formatter.Format( photo.Caption.Text, photo.Caption.Entities, markupMode, ) } case client.TypeMessageAudio: audio, _ := content.(*client.MessageAudio) if preview { return audio.Caption.Text } else { return formatter.Format( audio.Caption.Text, audio.Caption.Entities, markupMode, ) } case client.TypeMessageVideo: video, _ := content.(*client.MessageVideo) if preview { return video.Caption.Text } else { return formatter.Format( video.Caption.Text, video.Caption.Entities, markupMode, ) } case client.TypeMessageDocument: document, _ := content.(*client.MessageDocument) if preview { return document.Caption.Text } else { return formatter.Format( document.Caption.Text, document.Caption.Entities, markupMode, ) } case client.TypeMessageText: text, _ := content.(*client.MessageText) if preview { return text.Text.Text } else { return formatter.Format( text.Text.Text, text.Text.Entities, markupMode, ) } case client.TypeMessageVoiceNote: voice, _ := content.(*client.MessageVoiceNote) if preview { return voice.Caption.Text } else { return formatter.Format( voice.Caption.Text, voice.Caption.Entities, markupMode, ) } case client.TypeMessageVideoNote: return "" case client.TypeMessageAnimation: animation, _ := content.(*client.MessageAnimation) if preview { return animation.Caption.Text } else { return formatter.Format( animation.Caption.Text, animation.Caption.Entities, markupMode, ) } case client.TypeMessageContact: contact, _ := content.(*client.MessageContact) if preview { return contact.Contact.FirstName + " " + contact.Contact.LastName } else { var jid string if contact.Contact.UserId != 0 { jid = fmt.Sprintf("%v@%s", contact.Contact.UserId, gateway.Jid.Bare()) } return fmt.Sprintf( "*%s %s*\n%s\n%s\n%s", contact.Contact.FirstName, contact.Contact.LastName, contact.Contact.PhoneNumber, contact.Contact.Vcard, jid, ) } case client.TypeMessageDice: dice, _ := content.(*client.MessageDice) return fmt.Sprintf("%s 1d6: [%v]", dice.Emoji, dice.Value) case client.TypeMessagePoll: poll, _ := content.(*client.MessagePoll) if preview { return poll.Poll.Question } else { rows := []string{} rows = append(rows, fmt.Sprintf("*%s*", poll.Poll.Question)) for _, option := range poll.Poll.Options { var tick string if option.IsChosen { tick = "x" } else { tick = " " } rows = append(rows, fmt.Sprintf( "[%s] %s | %v%% | %v vote", tick, option.Text, option.VotePercentage, option.VoterCount, )) } return strings.Join(rows, "\n") } case client.TypeMessageChatSetMessageAutoDeleteTime: ttl, _ := content.(*client.MessageChatSetMessageAutoDeleteTime) name := c.FormatContact(ttl.FromUserId) if name == "" { if ttl.MessageAutoDeleteTime == 0 { return "The self-destruct timer was disabled" } else { return fmt.Sprintf("The self-destruct timer was set to %v seconds", ttl.MessageAutoDeleteTime) } } else { if ttl.MessageAutoDeleteTime == 0 { return fmt.Sprintf("%s disabled the self-destruct timer", name) } else { return fmt.Sprintf("%s set the self-destruct timer to %v seconds", name, ttl.MessageAutoDeleteTime) } } } return fmt.Sprintf("unknown message (%s)", content.MessageContentType()) } func (c *Client) contentToFile(content client.MessageContent) (*client.File, *client.File) { if content == nil { return nil, nil } switch content.MessageContentType() { case client.TypeMessageSticker: sticker, _ := content.(*client.MessageSticker) file := sticker.Sticker.Sticker if sticker.Sticker.Format.StickerFormatType() == client.TypeStickerFormatTgs && sticker.Sticker.Thumbnail != nil && sticker.Sticker.Thumbnail.File != nil { file = sticker.Sticker.Thumbnail.File } return file, nil case client.TypeMessageVoiceNote: voice, _ := content.(*client.MessageVoiceNote) return voice.VoiceNote.Voice, nil case client.TypeMessageVideoNote: video, _ := content.(*client.MessageVideoNote) var preview *client.File if video.VideoNote.Thumbnail != nil { preview = video.VideoNote.Thumbnail.File } return video.VideoNote.Video, preview case client.TypeMessageAnimation: animation, _ := content.(*client.MessageAnimation) var preview *client.File if animation.Animation.Thumbnail != nil { preview = animation.Animation.Thumbnail.File } return animation.Animation.Animation, preview case client.TypeMessagePhoto: photo, _ := content.(*client.MessagePhoto) sizes := photo.Photo.Sizes if len(sizes) >= 1 { file := sizes[len(sizes)-1].Photo return file, nil } return nil, nil case client.TypeMessageAudio: audio, _ := content.(*client.MessageAudio) var preview *client.File if audio.Audio.AlbumCoverThumbnail != nil { preview = audio.Audio.AlbumCoverThumbnail.File } return audio.Audio.Audio, preview case client.TypeMessageVideo: video, _ := content.(*client.MessageVideo) var preview *client.File if video.Video.Thumbnail != nil { preview = video.Video.Thumbnail.File } return video.Video.Video, preview case client.TypeMessageDocument: document, _ := content.(*client.MessageDocument) var preview *client.File if document.Document.Thumbnail != nil { preview = document.Document.Thumbnail.File } return document.Document.Document, preview } return nil, nil } func (c *Client) countCharsInLines(lines *[]string) (count int) { for _, line := range *lines { count += utf8.RuneCountInString(line) } return } func (c *Client) isCarbonsEnabled() bool { return gateway.MessageOutgoingPermissionVersion > 0 && c.Session.Carbons } func (c *Client) messageToPrefix(message *client.Message, previewString string, fileString string, suppressReply bool) (string, *gateway.Reply) { isPM, chat, err := c.IsPM(message.ChatId) if err != nil { log.Errorf("Could not determine chat type: %v", err) } // with carbons, hide for all messages in PM and only for outgoing in group chats hideSender := c.isCarbonsEnabled() && (message.IsOutgoing || isPM) || (c.Session.MUC && c.IsGroup(chat)) prefix := []string{} // message direction var directionChar string if !hideSender { if c.Session.AsciiArrows { if message.IsOutgoing { directionChar = "> " } else { directionChar = "< " } } else { if message.IsOutgoing { directionChar = "➡ " } else { directionChar = "⬅ " } } } // with hideids options enabled, hide the id for everything but non-carbons in legacy group chats if (!isPM && !c.Session.MUC && !(c.isCarbonsEnabled() && message.IsOutgoing)) || !c.Session.HideIds { prefix = append(prefix, directionChar+strconv.FormatInt(message.Id, 10)) } // show sender in group chats if !hideSender { sender := c.formatSender(message) if sender != "" { prefix = append(prefix, sender) } } // reply to var reply *gateway.Reply if !suppressReply { preview := true gwReply, tgReply := c.getMessageReply(message, preview, false) if tgReply != nil { reply = gwReply var replyStart, replyEnd int if len(prefix) > 0 { replyStart = c.countCharsInLines(&prefix) + (len(prefix)-1)*len(messageHeaderSeparator) } replyLine := "reply: " + c.formatMessageContent(preview, tgReply, true) prefix = append(prefix, replyLine) replyEnd = replyStart + utf8.RuneCountInString(replyLine) if len(prefix) > 0 { replyEnd += len(messageHeaderSeparator) } if reply != nil { reply.Start = uint64(replyStart) reply.End = uint64(replyEnd) } } } if message.ForwardInfo != nil { prefix = append(prefix, "fwd: "+c.formatOrigin(message.ForwardInfo.Origin)) } // preview if previewString != "" { prefix = append(prefix, "preview: "+previewString) } // file if fileString != "" { prefix = append(prefix, "file: "+fileString) } return strings.Join(prefix, messageHeaderSeparator), reply } func (c *Client) ensureDownloadFile(file *client.File) *client.File { gateway.StorageLock.Lock() defer gateway.StorageLock.Unlock() if file != nil { c.prepareDiskSpace(uint64(file.Size)) newFile, err := c.DownloadFile(file.Id, 1, true) if err == nil { return newFile } } return file } // \n if it is groupchat and message is not empty func (c *Client) getPrefixSeparator(chatId int64) string { var separator string if chatId < 0 { separator = "\n" } else if chatId > 0 { separator = " | " } return separator } // ProcessIncomingMessage is a legacy wrapper for SendMessageToGateway aiming only PM messages func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { chat, _, _ := c.GetContactByID(chatId, nil) safeToSend := true groupChatFrom := "" groupChatTos := []string{} if c.Session.MUC && c.IsGroup(chat) { senderId := c.getMessageSenderId(message) if senderId == 0 { log.Errorf("Invalid sender id for message %#v", message) return } switch message.Content.MessageContentType() { case client.TypeMessageChatJoinByLink: c.mucOccupantRolePresence(chatId, senderId, ChatMemberStatusUnmuted, c.GetMUCNickname(senderId)) case client.TypeMessageChatAddMembers: addMembers, _ := message.Content.(*client.MessageChatAddMembers) for _, memberId := range addMembers.MemberUserIds { c.mucOccupantRolePresence(chatId, memberId, ChatMemberStatusUnmuted, c.GetMUCNickname(memberId)) } case client.TypeMessageChatDeleteMember: deleteMember, _ := message.Content.(*client.MessageChatDeleteMember) c.mucOccupantRolePresence(chatId, deleteMember.UserId, ChatMemberStatusKicked, c.GetMUCNickname(deleteMember.UserId)) case client.TypeMessageBasicGroupChatCreate, client.TypeMessageSupergroupChatCreate: for resource := range c.resourcesRange() { gateway.InviteToMUC(chatId, c.jid+"/"+resource, c.xmpp) } } if !c.mucCacheHasOccupant(chatId, senderId) { chatMember, err := c.client.GetChatMember(&client.GetChatMemberRequest{ ChatId: chatId, MemberId: message.SenderId, }) var status client.ChatMemberStatus if err == nil { status = chatMember.Status } affiliation, role := c.memberStatusToAffiliationAndRole(status, chat) safeToSend = c.addMUCOccupant(chatId, senderId, affiliation, role, status) } groupChatFrom = gateway.MUCJID(chatId) + "/" + c.GetMUCNickname(senderId) var ok bool ok, groupChatTos = c.getMUCJoinedJIDs(chatId, nil, true) if !ok { safeToSend = false } } log.Debugf("groupChatFrom: %v groupChatTos: %#v, safeToSend: %v", groupChatFrom, groupChatTos, safeToSend) if safeToSend { c.SendMessageToGateway(chatId, message, "", false, groupChatFrom, groupChatTos) } else { mucJID := gateway.MUCJID(chatId) gateway.SendErrorMessage(c.jid, mucJID, "Cannot show a message", 500, true, c.xmpp) } } // SendMessageToGateway transfers a message to XMPP side and marks it as read on Telegram side func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id string, delay bool, groupChatFrom string, groupChatTos []string) { var isCarbon bool var jids []string var isGroupchat bool var originalFrom string if len(groupChatTos) == 0 { isCarbon = c.isCarbonsEnabled() && message.IsOutgoing jids = c.getCarbonFullJids(isCarbon, "") } else { isGroupchat = true jids = groupChatTos senderId := c.getMessageSenderId(message) if senderId != 0 { originalFrom = gateway.CHATJID(senderId, true) } } var text, oob, auxText string var reply *gateway.Reply var replyObtained bool content := message.Content if content != nil && content.MessageContentType() == client.TypeMessageChatChangePhoto { chat, err := c.client.GetChat(&client.GetChatRequest{ ChatId: chatId, }) if err == nil { c.cache.SetChat(chatId, chat) go c.ProcessStatusUpdate(chatId, "", "", gateway.SPImmed(true)) text = "" if chat.Photo == nil { c.SetEmptyAvatarHash(chatId) } else { sha1 := c.GetPhotoSha1(chat.Photo.Small, chatId) size := c.GetPhotoSize(chat.Photo.Small) for resource := range c.resourcesRange() { features, ok := c.XmppClientFeatures[resource] if ok && features != nil { for _, feature := range *features { if feature == gateway.NodeAvatarMetadataNotify { var chatJid string if isGroupchat { chatJid = gateway.MUCJID(chatId) } else { chatJid = gateway.CHATJID(chatId, false) } go gateway.SendPubSubAvatarNotification(c.xmpp, c.jid+"/"+resource, chatJid, sha1, size) break } } } } if isGroupchat { for _, jid := range jids { gateway.SendMUCStatusCode(jid, gateway.MUCJID(chatId), c.xmpp, 104) } } } } } else { text = c.messageToText(message, false) // OTR support (I do not know why would you need it, seriously) if !(strings.HasPrefix(text, "?OTR") || (c.Session.RawMessages && !c.Session.OOBMode)) { file, preview := c.contentToFile(content) // download file and preview (if present) file = c.ensureDownloadFile(file) preview = c.ensureDownloadFile(preview) previewName, _ := c.formatFile(preview, true) fileName, link := c.formatFile(file, false) oob = link oobSwap := c.Session.OOBMode && oob != "" var ignorePrefix bool if oobSwap { if text == "" || message.Content.MessageContentType() == client.TypeMessageSticker { chatType, _, err := c.GetChatType(chatId) ignorePrefix = err == nil && (chatType != ChatTypeBasicGroup && chatType != ChatTypeSupergroup) && c.isCarbonsEnabled() } } if !c.Session.RawMessages && !ignorePrefix { var newText strings.Builder prefix, prefixReply := c.messageToPrefix(message, previewName, fileName, false) reply = prefixReply replyObtained = true newText.WriteString(prefix) if text != "" { if prefix != "" { newText.WriteString(c.getPrefixSeparator(chatId)) } newText.WriteString(text) } text = newText.String() } if oobSwap { if !ignorePrefix { auxText = text } text = oob } } } if !replyObtained { reply, _ = c.getMessageReply(message, false, true) } // mark message as read if !c.Session.Receipts { c.MarkAsRead(chatId, message.Id) } // forward message to XMPP var sId string var stanzaId string strId := strconv.FormatInt(message.Id, 10) if id == "" { sId = strId stanzaId = strId } else { sId = id stanzaId = strId } var from string if groupChatFrom == "" { from = gateway.CHATNODE(chatId) } else { from = groupChatFrom } var timestamp int64 if delay { timestamp = int64(message.Date) } for _, jid := range jids { gateway.SendMessageWithOOB(jid, from, text, sId, c.xmpp, reply, timestamp, oob, "", isCarbon, isGroupchat, c.Session.Receipts, originalFrom, stanzaId) if auxText != "" { gateway.SendMessage(jid, from, auxText, sId, c.xmpp, reply, timestamp, "", isCarbon, isGroupchat, c.Session.Receipts, originalFrom, stanzaId) } } c.UpdateLastChatMessageId(chatId, sId) } // MarkAsRead marks a message as read func (c *Client) MarkAsRead(chatId, messageId int64) { c.client.ViewMessages(&client.ViewMessagesRequest{ ChatId: chatId, MessageIds: []int64{messageId}, ForceRead: true, }) } // PrepareMessageContent creates a simple text message func (c *Client) PrepareOutgoingMessageContent(text string) client.InputMessageContent { return c.prepareOutgoingMessageContent(text, nil) } // ProcessOutgoingMessage executes commands or sends messages to mapped chats, returns message id and isCommand func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid string, replyId int64, replaceId int64, isGroupchat, raw bool) (*client.Message, bool) { if !c.Online() { // we're offline return nil, false } if replaceId == 0 && !raw && (strings.HasPrefix(text, "/") || strings.HasPrefix(text, "!")) { // try to execute commands response, isCommand, _ := c.ProcessChatCommand(chatID, text) if response != "" { c.returnMessage(returnJid, chatID, response, 0, isGroupchat) } // do not send on success if isCommand { return nil, true } } log.Warnf("Sending message to chat %v", chatID) // quotations var reply int64 if replaceId == 0 && replyId == 0 { replySlice := replyRegex.FindStringSubmatch(text) if len(replySlice) > 1 { reply, _ = strconv.ParseInt(replySlice[1], 10, 64) } } else { reply = replyId } // attach a file var file *client.InputFileLocal if c.content.Upload != "" && strings.HasPrefix(text, c.content.Upload) { response, err := http.Get(text) if err != nil { c.returnError(returnJid, chatID, "Failed to fetch the uploaded file", err, 500, isGroupchat) } if response != nil && response.Body != nil { defer response.Body.Close() if response.StatusCode != 200 { c.returnMessage(returnJid, chatID, fmt.Sprintf("Received status code %v", response.StatusCode), response.StatusCode, isGroupchat) return nil, false } tempDir, err := ioutil.TempDir("", "telegabber-*") if err != nil { c.returnError(returnJid, chatID, "Failed to create a temporary directory", err, 500, isGroupchat) return nil, false } tempFile, err := os.Create(filepath.Join(tempDir, filepath.Base(text))) if err != nil { c.returnError(returnJid, chatID, "Failed to create a temporary file", err, 500, isGroupchat) return nil, false } _, err = io.Copy(tempFile, response.Body) if err != nil { c.returnError(returnJid, chatID, "Failed to write a temporary file", err, 500, isGroupchat) return nil, false } file = &client.InputFileLocal{ Path: tempFile.Name(), } } } // remove first line from text if file != nil || (reply != 0 && replyId == 0) { newlinePos := strings.Index(text, newlineChar) if newlinePos != -1 { text = text[newlinePos+1:] } else { text = "" } } content := c.prepareOutgoingMessageContent(text, file) if replaceId != 0 { tgMessage, err := c.client.EditMessageText(&client.EditMessageTextRequest{ ChatId: chatID, MessageId: replaceId, InputMessageContent: content, }) if err != nil { c.returnError(returnJid, chatID, "Not edited", err, 400, isGroupchat) return nil, false } return tgMessage, false } tgMessage, err := c.client.SendMessage(&client.SendMessageRequest{ ChatId: chatID, ReplyTo: &client.InputMessageReplyToMessage{MessageId: reply}, InputMessageContent: content, }) if err != nil { c.returnError(returnJid, chatID, "Not sent", err, 400, isGroupchat) return nil, false } return tgMessage, false } func (c *Client) returnMessage(returnJid string, chatID int64, text string, code int, isGroupchat bool) { if isGroupchat { if code != 0 { gateway.SendErrorMessage(returnJid, gateway.MUCJID(chatID), text, code, isGroupchat, c.xmpp) } else { gateway.SendTextMessage(returnJid, gateway.MUCJID(chatID), text, c.xmpp, isGroupchat) } } else { gateway.SendTextMessage(returnJid, gateway.CHATNODE(chatID), text, c.xmpp, isGroupchat) } } func (c *Client) returnError(returnJid string, chatID int64, msg string, err error, code int, isGroupchat bool) { responseError, ok := err.(client.ResponseError) log.Debugf("responseError: %#v", responseError) if ok && responseError.Err != nil { if responseError.Err.Message == "Have no write access to the chat" { code = 403 } } c.returnMessage(returnJid, chatID, fmt.Sprintf("%s: %s", msg, err.Error()), code, isGroupchat) } func (c *Client) prepareOutgoingMessageContent(text string, file *client.InputFileLocal) client.InputMessageContent { formattedText := &client.FormattedText{ Text: text, } var content client.InputMessageContent if file != nil { // we can try to send a document content = &client.InputMessageDocument{ Document: file, Caption: formattedText, } } else { // compile our message content = &client.InputMessageText{ Text: formattedText, } } return content } // ChatsKeys proxies the following function from unexported cache func (c *Client) ChatsKeys() []int64 { return c.cache.ChatsKeys() } // StatusesRange proxies the following function from unexported cache func (c *Client) StatusesRange() chan *cache.Status { return c.cache.StatusesRange() } func (c *Client) addResource(resource string) { if resource == "" { return } c.locks.resourcesLock.Lock() defer c.locks.resourcesLock.Unlock() c.resources[resource] = true } func (c *Client) deleteResource(resource string) { c.locks.resourcesLock.Lock() defer c.locks.resourcesLock.Unlock() if _, ok := c.resources[resource]; ok { delete(c.resources, resource) } } func (c *Client) resourcesRange() chan string { c.locks.resourcesLock.Lock() resourceChan := make(chan string, 1) go func() { defer func() { c.locks.resourcesLock.Unlock() close(resourceChan) }() for resource := range c.resources { resourceChan <- resource } }() return resourceChan } // resend statuses to (to another resource, for example) func (c *Client) roster(resource string) { c.locks.resourcesLock.Lock() if _, ok := c.resources[resource]; ok { c.locks.resourcesLock.Unlock() return // we know it } c.locks.resourcesLock.Unlock() log.Warnf("Sending roster for %v", resource) for _, chat := range c.cache.ChatsKeys() { c.ProcessStatusUpdate(chat, "", "") } c.sendPresence(gateway.SPStatus("Logged in as: " + c.Session.Login)) c.addResource(resource) } // get last messages from specified chat func (c *Client) getLastMessages(id int64, query string, from int64, count int32) (*client.FoundChatMessages, error) { return c.client.SearchChatMessages(&client.SearchChatMessagesRequest{ ChatId: id, Query: query, SenderId: &client.MessageSenderUser{UserId: from}, Filter: &client.SearchMessagesFilterEmpty{}, Limit: count, }) } func (c *Client) getNLastMessages(chatID int64, limit *MessageLimit) ([]*client.Message, error) { var newMessages *client.Messages var messages []*client.Message var err error var fromId int64 var safetyLimit int32 var charsCount int if limit == nil { return nil, nil } switch limit.Type { case MessageLimitMessages: safetyLimit = limit.Messages case MessageLimitChars: safetyLimit = int32(limit.Chars) if safetyLimit > 1000 { safetyLimit = 1000 } case MessageLimitSince: safetyLimit = 1000 } safetyLoop: for _ = range make([]struct{}, safetyLimit) { if len(messages) > 0 { fromId = messages[len(messages)-1].Id } newMessages, err = c.client.GetChatHistory(&client.GetChatHistoryRequest{ ChatId: chatID, FromMessageId: fromId, Limit: safetyLimit, }) if err != nil { return nil, err } if len(newMessages.Messages) == 0 { break } for _, message := range newMessages.Messages { if limit.Type == MessageLimitSince && limit.Since > int64(message.Date) { // durov… break safetyLoop } messages = append(messages, message) switch limit.Type { case MessageLimitMessages: if len(messages) >= int(limit.Messages) { break safetyLoop } case MessageLimitChars: // rough but why care if message.Content != nil && message.Content.MessageContentType() == client.TypeMessageText { textContent, _ := message.Content.(*client.MessageText) if textContent.Text != nil { charsCount += len(textContent.Text.Text) if charsCount >= limit.Chars { break safetyLoop } } } } } } return messages, nil } // GetFile retrieves a file object by id given by TDlib func (c *Client) GetFile(id int32) (*client.File, error) { return c.client.GetFile(&client.GetFileRequest{ FileId: id, }) } // DownloadFile actually obtains a file by id given by TDlib func (c *Client) DownloadFile(id int32, priority int32, synchronous bool) (*client.File, error) { return c.client.DownloadFile(&client.DownloadFileRequest{ FileId: id, Priority: priority, Synchronous: synchronous, }) } // ForceOpenFile reliably obtains a file if possible func (c *Client) ForceOpenFile(tgFile *client.File, priority int32) (*os.File, string, error) { if tgFile == nil { return nil, "", errors.New("File not found") } path := tgFile.Local.Path file, err := os.Open(path) if err == nil { return file, path, nil } else // obtain the photo right now if still not downloaded if !tgFile.Local.IsDownloadingCompleted { tdFile, tdErr := c.DownloadFile(tgFile.Id, priority, true) if tdErr == nil { path = tdFile.Local.Path file, err = os.Open(path) return file, path, err } } // give up return nil, path, err } // GetChatDescription obtains bio or description according to the chat type func (c *Client) GetChatDescription(chat *client.Chat) string { chatType := chat.Type.ChatTypeType() if chatType == client.TypeChatTypePrivate { privateType, _ := chat.Type.(*client.ChatTypePrivate) fullInfo, err := c.client.GetUserFullInfo(&client.GetUserFullInfoRequest{ UserId: privateType.UserId, }) if err == nil { if fullInfo.Bio != nil && fullInfo.Bio.Text != "" { return formatter.Format( fullInfo.Bio.Text, fullInfo.Bio.Entities, c.getFormatter(), ) } else if fullInfo.BotInfo != nil { if fullInfo.BotInfo.ShortDescription != "" { return fullInfo.BotInfo.ShortDescription } else { return fullInfo.BotInfo.Description } } } else { log.Warnf("Couldn't retrieve private chat info: %v", err.Error()) } } else if chatType == client.TypeChatTypeBasicGroup { basicGroupType, _ := chat.Type.(*client.ChatTypeBasicGroup) fullInfo, err := c.client.GetBasicGroupFullInfo(&client.GetBasicGroupFullInfoRequest{ BasicGroupId: basicGroupType.BasicGroupId, }) if err == nil { return fullInfo.Description } else { log.Warnf("Couldn't retrieve basic group info: %v", err.Error()) } } else if chatType == client.TypeChatTypeSupergroup { supergroupType, _ := chat.Type.(*client.ChatTypeSupergroup) fullInfo, err := c.client.GetSupergroupFullInfo(&client.GetSupergroupFullInfoRequest{ SupergroupId: supergroupType.SupergroupId, }) if err == nil { return fullInfo.Description } else { log.Warnf("Couldn't retrieve supergroup info: %v", err.Error()) } } return "" } // GetChatMemberCount obtains the member count depending on the chat type func (c *Client) GetChatMemberCount(chat *client.Chat) int32 { chatType := chat.Type.ChatTypeType() if chatType == client.TypeChatTypePrivate { return 2 } else if chatType == client.TypeChatTypeBasicGroup { basicGroupType, _ := chat.Type.(*client.ChatTypeBasicGroup) basicGroup, err := c.client.GetBasicGroup(&client.GetBasicGroupRequest{ BasicGroupId: basicGroupType.BasicGroupId, }) if err == nil { return basicGroup.MemberCount } else { log.Warnf("Couldn't retrieve basic group: %v", err.Error()) } } else if chatType == client.TypeChatTypeSupergroup { supergroupType, _ := chat.Type.(*client.ChatTypeSupergroup) supergroup, err := c.client.GetSupergroup(&client.GetSupergroupRequest{ SupergroupId: supergroupType.SupergroupId, }) if err == nil { return supergroup.MemberCount } else { log.Warnf("Couldn't retrieve supergroup: %v", err.Error()) } } return 0 } // GetGroupChats obtains all group chats func (c *Client) GetGroupChats() []*client.Chat { var groupChats []*client.Chat chats, err := c.client.GetChats(&client.GetChatsRequest{ Limit: chatsLimit, }) if err == nil { for _, id := range chats.ChatIds { chat, _, _ := c.GetContactByID(id, nil) if chat != nil && c.IsGroup(chat) { groupChats = append(groupChats, chat) } } } else { log.Errorf("Could not retrieve chats: %v", err) } return groupChats } // IsGroup determines if a chat is eligible to be represented as MUC func (c *Client) IsGroup(chat *client.Chat) bool { if chat == nil { return false } typ := chat.Type.ChatTypeType() return typ == client.TypeChatTypeBasicGroup } // subscribe to a Telegram ID func (c *Client) subscribeToID(id int64, chat *client.Chat, firstTime bool) { args := gateway.SimplePresence(id, "subscribe") if chat == nil { chat, _, _ = c.GetContactByID(id, nil) } if chat != nil { if c.Session.MUC && c.IsGroup(chat) { if firstTime { for resource := range c.resourcesRange() { gateway.InviteToMUC(id, c.jid+"/"+resource, c.xmpp) } } return } args = append(args, gateway.SPNickname(chat.Title)) gateway.SetNickname(c.jid, gateway.CHATNODE(id), chat.Title, c.xmpp) } c.sendPresence(args...) } func (c *Client) sendPresence(args ...args.V) error { return gateway.SendPresence(c.xmpp, c.jid, args...) } func (c *Client) prepareDiskSpace(size uint64) { if gateway.StorageQuota > 0 && c.content.Path != "" { var loweredQuota uint64 if gateway.StorageQuota >= size { loweredQuota = gateway.StorageQuota - size } if gateway.CachedStorageSize >= loweredQuota { log.Warn("Storage is rapidly clogged") gateway.CleanOldFiles(c.content.Path, loweredQuota) } } } func (c *Client) GetVcardInfo(toID int64) (VCardInfo, error) { var info VCardInfo chat, user, err := c.GetContactByID(toID, nil) if err != nil { return info, err } if chat != nil { info.Fn = chat.Title if chat.Photo != nil { info.Photo = chat.Photo.Small } info.Info = c.GetChatDescription(chat) } if user != nil { if user.Usernames != nil { info.Nicknames = make([]string, len(user.Usernames.ActiveUsernames)) copy(info.Nicknames, user.Usernames.ActiveUsernames) } info.Given = user.FirstName info.Family = user.LastName info.Tel = user.PhoneNumber } return info, nil } func (c *Client) UpdateChatNicknames() { for _, id := range c.cache.ChatsKeys() { chat, ok := c.cache.GetChat(id) if ok { if c.Session.MUC && c.IsGroup(chat) { continue } newArgs := []args.V{ gateway.SPNickname(chat.Title), } newArgs = gateway.SPAppendFrom(newArgs, id) cachedStatus, ok := c.cache.GetStatus(id) if ok { show, status, typ := cachedStatus.Destruct() newArgs = append(newArgs, gateway.SPShow(show), gateway.SPStatus(status)) if typ != "" { newArgs = append(newArgs, gateway.SPType(typ)) } } c.sendPresence(newArgs...) gateway.SetNickname(c.jid, gateway.CHATNODE(id), chat.Title, c.xmpp) } } } // AddToEditOutbox temporarily store the resource from which a replace message with given ID was sent func (c *Client) AddToEditOutbox(xmppId, resource string) { c.locks.editOutboxLock.Lock() defer c.locks.editOutboxLock.Unlock() c.editOutbox[xmppId] = resource } func (c *Client) popFromEditOutbox(xmppId string) string { c.locks.editOutboxLock.Lock() defer c.locks.editOutboxLock.Unlock() resource, ok := c.editOutbox[xmppId] if ok { delete(c.editOutbox, xmppId) } else { log.Warnf("No %v xmppId in edit outbox", xmppId) } return resource } // AddToOutbox remembers the resource from which a message with given ID was sent func (c *Client) AddToOutbox(xmppId, resource string) { c.locks.outboxLock.Lock() defer c.locks.outboxLock.Unlock() c.outbox[xmppId] = resource } func (c *Client) getFromOutbox(xmppId string) string { c.locks.outboxLock.Lock() defer c.locks.outboxLock.Unlock() resource, ok := c.outbox[xmppId] if !ok { log.Warnf("No %v xmppId in outbox", xmppId) } return resource } func (c *Client) getCarbonFullJids(isOutgoing bool, ignoredResource string) []string { var jids []string if isOutgoing { for resource := range c.resourcesRange() { if ignoredResource == "" || resource != ignoredResource { jids = append(jids, c.jid+"/"+resource) } } } else { jids = []string{c.jid} } return jids } func (c *Client) UpdateLastChatMessageId(chatId int64, messageId string) { c.locks.lastMsgIdsLock.Lock() defer c.locks.lastMsgIdsLock.Unlock() c.lastMsgIds[chatId] = messageId } func (c *Client) getLastChatMessageId(chatId int64) (string, bool) { c.locks.lastMsgIdsLock.RLock() defer c.locks.lastMsgIdsLock.RUnlock() xmppId, ok := c.lastMsgIds[chatId] return xmppId, ok } func (c *Client) getFormatter() formatter.MarkupModeType { return formatter.MarkupModeXEP0393 } func (c *Client) usernamesToString(usernames []string) string { var atUsernames []string for _, username := range usernames { atUsernames = append(atUsernames, "@"+username) } return strings.Join(atUsernames, ", ") } func (c *Client) memberStatusToAffiliationAndRole(memberStatus client.ChatMemberStatus, chat *client.Chat) (string, string) { if memberStatus != nil { switch memberStatus.ChatMemberStatusType() { case client.TypeChatMemberStatusCreator: return "owner", "moderator" case client.TypeChatMemberStatusAdministrator: return "admin", "moderator" case client.TypeChatMemberStatusMember: if chat != nil && !c.IsMessageSendingPermitted(chat.Permissions) { return "member", "visitor" } return "member", "participant" case client.TypeChatMemberStatusRestricted: restricted, _ := memberStatus.(*client.ChatMemberStatusRestricted) if !c.IsMessageSendingPermitted(restricted.Permissions) { return "member", "visitor" } return "member", "participant" case client.TypeChatMemberStatusLeft: return "none", "none" case client.TypeChatMemberStatusBanned: return "outcast", "none" } } return "member", "participant" } // TgMemberToMUCMember resolves useful data to generate a MUC occupant func (c *Client) TgMemberToMUCOccupant(member *client.ChatMember, chat *client.Chat) (senderId int64, nickname, affiliation, role string) { senderId = c.GetSenderId(member.MemberId) nickname = c.GetMUCNickname(senderId) affiliation, role = c.memberStatusToAffiliationAndRole(member.Status, chat) return } func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message, plain bool, toJid string) { sChatId := gateway.CHATNODE(chatID) mucJid := gateway.MUCJID(chatID) var plainTos []string var isMUC bool if plain { chat, err := c.GetChatByID(chatID, nil) if err == nil { isMUC = c.Session.MUC && c.IsGroup(chat) } } if isMUC { _, plainTos = c.getMUCJoinedJIDs(chatID, nil, true) } else { plainTos = []string{c.jid} } for i := len(messages) - 1; i >= 0; i-- { message := messages[i] if plain { reply, _ := c.getMessageReply(message, false, true) sId := strconv.FormatInt(message.Id, 10) var originalFrom string var from string if isMUC { senderId := c.getMessageSenderId(message) if senderId != 0 { originalFrom = gateway.CHATJID(senderId, true) } from = mucJid + "/" + c.GetMUCNickname(senderId) } else { from = sChatId } for _, to := range plainTos { gateway.SendMessage( to, from, c.formatMessage(0, 0, false, !isMUC, message), sId, c.xmpp, reply, 0, "", false, isMUC, false, originalFrom, "", ) } } else { msgId, _ := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, chatID, message.Id) c.SendMessageToGateway( chatID, message, msgId, true, mucJid + "/" + c.GetMUCNickname(c.getMessageSenderId(message)), []string{toJid}, ) } } } // GetChatMembers retrieves a list of chat members. "Limited" mode works only if there are no more than 20 members at all func (c *Client) GetChatMembers(chatID int64, limited bool, query string, membersList MembersList) ([]*client.ChatMember, error) { if membersList == MembersListCreators { chat, err := c.GetChatByID(chatID, nil) if err != nil { return nil, err } chatType := chat.Type.ChatTypeType() if chatType == client.TypeChatTypeBasicGroup { basicGroupType, _ := chat.Type.(*client.ChatTypeBasicGroup) fullInfo, err := c.client.GetBasicGroupFullInfo(&client.GetBasicGroupFullInfoRequest{ BasicGroupId: basicGroupType.BasicGroupId, }) if err != nil { return nil, err } if fullInfo.CreatorUserId != 0 { chatMember, err := c.client.GetChatMember(&client.GetChatMemberRequest{ ChatId: chatID, MemberId: &client.MessageSenderUser{UserId: fullInfo.CreatorUserId}, }) if err != nil { return nil, err } return []*client.ChatMember{chatMember}, nil } } return nil, errors.New("Creator not found") } var filters []client.ChatMembersFilter switch membersList { case MembersListMembers: filters = []client.ChatMembersFilter{&client.ChatMembersFilterMembers{}} case MembersListRestricted: filters = []client.ChatMembersFilter{&client.ChatMembersFilterRestricted{}} case MembersListBanned: filters = []client.ChatMembersFilter{&client.ChatMembersFilterBanned{}} case MembersListBannedAndAdministrators: filters = []client.ChatMembersFilter{&client.ChatMembersFilterBanned{}, &client.ChatMembersFilterAdministrators{}} case MembersListAdministrators: filters = []client.ChatMembersFilter{&client.ChatMembersFilterAdministrators{}} } limit := int32(9999) if limited { limit = 20 chat, err := c.GetChatByID(chatID, nil) if err != nil { return nil, err } chatType := chat.Type.ChatTypeType() if chatType == client.TypeChatTypeBasicGroup { basicGroupType, _ := chat.Type.(*client.ChatTypeBasicGroup) fullInfo, err := c.client.GetBasicGroupFullInfo(&client.GetBasicGroupFullInfoRequest{ BasicGroupId: basicGroupType.BasicGroupId, }) if err != nil { return nil, err } if len(fullInfo.Members) > int(limit) { return nil, errOverLimit } return fullInfo.Members, nil } else if chatType == client.TypeChatTypeSupergroup { supergroupType, _ := chat.Type.(*client.ChatTypeSupergroup) fullInfo, err := c.client.GetSupergroupFullInfo(&client.GetSupergroupFullInfoRequest{ SupergroupId: supergroupType.SupergroupId, }) if err != nil { return nil, err } if fullInfo.MemberCount > limit { return nil, errOverLimit } } else { return nil, errors.New("Inapplicable chat type") } } var members []*client.ChatMember for _, filter := range filters { chatMembers, err := c.client.SearchChatMembers(&client.SearchChatMembersRequest{ ChatId: chatID, Limit: limit, Query: query, Filter: filter, }) if err != nil { return nil, err } members = append(members, chatMembers.Members...) } return members, nil } func (c *Client) unsubscribe(chatID int64) error { args := gateway.SimplePresence(chatID, "unsubscribed") return c.sendPresence(args...) } func (c *Client) leaveChat(chatID int64) error { chat, err := c.GetChatByID(chatID, nil) if err == nil && c.Session.MUC && c.IsGroup(chat) { return c.kickMeFromMUC(chatID, []uint16{110, 307}, false, nil) } return c.unsubscribe(chatID) } // achtung: assuming a locked mucState context func (c *Client) kickMeFromMUC(chatID int64, statusCodes []uint16, destroy bool, mucState *MUCState) error { var myJid string if c.me != nil { myJid = gateway.CHATJID(c.me.Id, true) } args := []args.V{ gateway.SPFrom(gateway.MUCNODE(chatID)), gateway.SPResource(c.GetMUCNickname(0)), gateway.SPMUCAffiliation("none"), gateway.SPMUCRole("none"), gateway.SPMUCJid(myJid), gateway.SPMUCStatusCodes(statusCodes), } if destroy { _, toResources := c.getMUCJoinedJIDs(chatID, mucState, false) args = append( args, gateway.SPMUCDestroy(""), gateway.SPToResources(toResources), ) } else { args = append(args, gateway.SPType("unavailable")) } return c.sendPresence(args...) } // MigrateToMUCs unsubscribes from legacy group chats and invites to MUCs func (c *Client) MigrateToMUCs() { var chatIDs []int64 for _, chat := range c.GetGroupChats() { chatIDs = append(chatIDs, chat.Id) } c.DelayedStatusesLock.Lock() for _, chatID := range chatIDs { delete(c.DelayedStatuses, chatID) } c.DelayedStatusesLock.Unlock() for _, chatID := range chatIDs { c.unsubscribe(chatID) for resource := range c.resourcesRange() { gateway.InviteToMUC(chatID, c.jid+"/"+resource, c.xmpp) } } } // MigrateFromMUCs kicks from MUCs and subscribes back to legacy group chats func (c *Client) MigrateFromMUCs() { var chatIDs []int64 for _, chat := range c.GetGroupChats() { chatIDs = append(chatIDs, chat.Id) } c.locks.mucCacheLock.Lock() for _, chatID := range chatIDs { c.kickMeFromMUC(chatID, []uint16{110, 332}, false, nil) delete(c.mucCache, chatID) c.subscribeToID(chatID, nil, false) } c.locks.mucCacheLock.Unlock() } // SetChatMemberStatus is a handy wrapper for the following TDLib method func (c *Client) SetChatMemberStatus(chatID, userID int64, status ChatMemberStatus, numericPayload int64, stringPayload, nickname string) error { var chatMemberStatus client.ChatMemberStatus switch status { case ChatMemberStatusKicked: chatMemberStatus = &client.ChatMemberStatusLeft{} case ChatMemberStatusMuted: chatMemberStatus = &client.ChatMemberStatusRestricted{ IsMember: true, RestrictedUntilDate: c.formatBantime(numericPayload), Permissions: &permissionsReadonly, } case ChatMemberStatusUnmuted: chatMemberStatus = &client.ChatMemberStatusRestricted{ IsMember: true, RestrictedUntilDate: 0, Permissions: &permissionsMember, } case ChatMemberStatusBanned: chatMemberStatus = &client.ChatMemberStatusBanned{ BannedUntilDate: c.formatBantime(numericPayload), } case ChatMemberStatusUnbanned: chatMemberStatus = &client.ChatMemberStatusMember{} case ChatMemberStatusPromoted: chatMemberStatus = &client.ChatMemberStatusAdministrator{ CanBeEdited: true, Rights: &permissionsAdmin, CustomTitle: stringPayload, } } _, err := c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ ChatId: chatID, MemberId: &client.MessageSenderUser{UserId: userID}, Status: chatMemberStatus, }) if err == nil && nickname != "" { c.mucOccupantRolePresence(chatID, userID, status, nickname) } return err } // SetChatTitle is a handy wrapper for the following TDLib method func (c *Client) SetChatTitle(chatID int64, title string) error { _, err := c.client.SetChatTitle(&client.SetChatTitleRequest{ ChatId: chatID, Title: title, }) return err } // SetChatDescription is a handy wrapper for the following TDLib method func (c *Client) SetChatDescription(chatID int64, description string) error { _, err := c.client.SetChatDescription(&client.SetChatDescriptionRequest{ ChatId: chatID, Description: description, }) return err } // SetChatPermissions is a handy wrapper for the following TDLib method func (c *Client) SetChatPermissions(chatID int64, permissions *client.ChatPermissions) error { _, err := c.client.SetChatPermissions(&client.SetChatPermissionsRequest{ ChatId: chatID, Permissions: permissions, }) return err } // DeleteChat is a handy wrapper for the following TDLib method func (c *Client) DeleteChat(chatID int64) error { _, err := c.client.DeleteChat(&client.DeleteChatRequest{ ChatId: chatID, }) return err } // CloneChatPermissions makes a copy of ChatPermissions structure func CloneChatPermissions(permissions *client.ChatPermissions) *client.ChatPermissions { return &client.ChatPermissions{ CanSendBasicMessages: permissions.CanSendBasicMessages, CanSendAudios: permissions.CanSendAudios, CanSendDocuments: permissions.CanSendDocuments, CanSendPhotos: permissions.CanSendPhotos, CanSendVideos: permissions.CanSendVideos, CanSendVideoNotes: permissions.CanSendVideoNotes, CanSendVoiceNotes: permissions.CanSendVoiceNotes, CanSendPolls: permissions.CanSendPolls, CanSendOtherMessages: permissions.CanSendOtherMessages, CanAddWebPagePreviews: permissions.CanAddWebPagePreviews, CanChangeInfo: permissions.CanChangeInfo, CanInviteUsers: permissions.CanInviteUsers, CanPinMessages: permissions.CanPinMessages, CanManageTopics: permissions.CanManageTopics, } } func (c *Client) mucOccupantRolePresence(chatID, userID int64, status ChatMemberStatus, nickname string) { args := []args.V{ gateway.SPFrom(gateway.MUCNODE(chatID)), gateway.SPResource(nickname), gateway.SPImmed(true), gateway.SPMUCJid(gateway.CHATJID(userID, true)), } var statusCodes []uint16 var newAffiliation, newRole string switch status { case ChatMemberStatusKicked: args = append(args, gateway.SPType("unavailable")) newAffiliation = "none" newRole = "none" statusCodes = append(statusCodes, 307) if c.me != nil && userID == c.me.Id { statusCodes = append(statusCodes, 110) } case ChatMemberStatusMuted: newAffiliation = "member" newRole = "visitor" case ChatMemberStatusUnmuted, ChatMemberStatusUnbanned: newAffiliation = "member" newRole = "participant" case ChatMemberStatusBanned: args = append(args, gateway.SPType("unavailable")) newAffiliation = "outcast" newRole = "none" statusCodes = append(statusCodes, 301) case ChatMemberStatusPromoted: newAffiliation = "admin" newRole = "moderator" } args = append( args, gateway.SPMUCAffiliation(newAffiliation), gateway.SPMUCRole(newRole), gateway.SPMUCStatusCodes(statusCodes), ) c.sendPresence(args...) c.locks.mucCacheLock.Lock() mucState, ok := c.mucCache[chatID] if ok && mucState != nil { if status == ChatMemberStatusKicked || status == ChatMemberStatusBanned { delete(mucState.Occupants, userID) } else { occupant, ok := mucState.Occupants[userID] if ok { occupant.Affiliation = newAffiliation occupant.Role = newRole } } } c.locks.mucCacheLock.Unlock() } // IsMessageSendingPermitted evaluates if permissions of the chat allow message sending func (c *Client) IsMessageSendingPermitted(permissions *client.ChatPermissions) bool { if permissions == nil { return true } return permissions.CanSendBasicMessages || permissions.CanSendAudios || permissions.CanSendDocuments || permissions.CanSendPhotos || permissions.CanSendVideos || permissions.CanSendVideoNotes || permissions.CanSendVoiceNotes || permissions.CanSendPolls || permissions.CanSendOtherMessages } // GetMyStatusInChat checks the membership status of current account in the given chat func (c *Client) GetMyStatusInChat(chatID int64) ChatMemberStatus { if c.me == nil { return ChatMemberStatusIllegal } member, err := c.client.GetChatMember(&client.GetChatMemberRequest{ ChatId: chatID, MemberId: &client.MessageSenderUser{UserId: c.me.Id}, }) if err != nil { return ChatMemberStatusIllegal } return c.getChatMemberStatus(member.Status) } func (c *Client) getChatMemberStatus(status client.ChatMemberStatus) ChatMemberStatus { switch status.ChatMemberStatusType() { case client.TypeChatMemberStatusCreator, client.TypeChatMemberStatusAdministrator: return ChatMemberStatusPromoted case client.TypeChatMemberStatusMember: return ChatMemberStatusUnbanned case client.TypeChatMemberStatusRestricted: return ChatMemberStatusMuted case client.TypeChatMemberStatusLeft: return ChatMemberStatusKicked case client.TypeChatMemberStatusBanned: return ChatMemberStatusBanned } return ChatMemberStatusIllegal } // GetErrorCode obtains an error code from a Telegram response error func GetErrorCode(err error) (int32, bool) { responseError, ok := err.(client.ResponseError) if !ok || responseError.Err == nil { return 0, false } return responseError.Err.Code, true }