#include "client-utils.h" #include "purple-info.h" #include "config.h" #include "format.h" #include "file-transfer.h" #include #include #include #include enum { MAX_MESSAGE_PARTS = 10, }; const char *errorCodeMessage() { // TRANSLATOR: In-line error message, appears after a colon (':'), arguments will be a number and some error text from Telegram return _("code {0} ({1})"); } static std::string messageTypeToString(const td::td_api::MessageContent &content) { #define C(type) case td::td_api::type::ID: return #type; switch (content.get_id()) { C(messageText) C(messageAnimation) C(messageAudio) C(messageDocument) C(messagePhoto) C(messageExpiredPhoto) C(messageSticker) C(messageVideo) C(messageExpiredVideo) C(messageVideoNote) C(messageVoiceNote) C(messageLocation) C(messageVenue) C(messageContact) C(messageGame) C(messagePoll) C(messageInvoice) C(messageCall) C(messageBasicGroupChatCreate) C(messageSupergroupChatCreate) C(messageChatChangeTitle) C(messageChatChangePhoto) C(messageChatDeletePhoto) C(messageChatAddMembers) C(messageChatJoinByLink) C(messageChatDeleteMember) C(messageChatUpgradeTo) C(messageChatUpgradeFrom) C(messagePinMessage) C(messageScreenshotTaken) C(messageChatSetTtl) C(messageCustomServiceAction) C(messageGameScore) C(messagePaymentSuccessful) C(messagePaymentSuccessfulBot) C(messageContactRegistered) C(messageWebsiteConnected) C(messagePassportDataSent) C(messagePassportDataReceived) C(messageUnsupported) } #undef C return "id " + std::to_string(content.get_id()); } std::string getUnsupportedMessageDescription(const td::td_api::MessageContent &content) { // TRANSLSATOR: In-line placeholder when an unsupported message is being replied to. return formatMessage(_("Unsupported message type {}"), messageTypeToString(content)); } std::string proxyTypeToString(PurpleProxyType proxyType) { switch (proxyType) { case PURPLE_PROXY_NONE: case PURPLE_PROXY_USE_GLOBAL: case PURPLE_PROXY_USE_ENVVAR: return "unknown"; case PURPLE_PROXY_HTTP: return "HTTP"; case PURPLE_PROXY_SOCKS4: return "SOCKS4"; case PURPLE_PROXY_SOCKS5: return "SOCKS5"; case PURPLE_PROXY_TOR: return "TOR"; } return "unknown"; } const char *getPurpleStatusId(const td::td_api::UserStatus &tdStatus) { if (tdStatus.get_id() == td::td_api::userStatusOnline::ID) return purple_primitive_get_id_from_type(PURPLE_STATUS_AVAILABLE); else return purple_primitive_get_id_from_type(PURPLE_STATUS_OFFLINE); } std::string getPurpleBuddyName(const td::td_api::user &user) { // Prepend "id" so it's not accidentally equal to our phone number which is account name return "id" + std::to_string(user.id_); } std::vector getUsersByPurpleName(const char *buddyName, TdAccountData &account, const char *action) { std::vector result; int32_t userId = stringToUserId(buddyName); if (userId != 0) { const td::td_api::user *tdUser = account.getUser(userId); if (tdUser != nullptr) result.push_back(tdUser); else if (action) purple_debug_warning(config::pluginId, "Cannot %s: no user with id %s\n", action, buddyName); } else { account.getUsersByDisplayName(buddyName, result); if (action) { if (result.empty()) purple_debug_warning(config::pluginId, "Cannot %s: no user with display name '%s'\n", action, buddyName); else if (result.size() != 1) purple_debug_warning(config::pluginId, "Cannot %s: more than one user with display name '%s'\n", action, buddyName); } } return result; } PurpleConversation *getImConversation(PurpleAccount *account, const char *username) { PurpleConversation *conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, username, account); if (conv == NULL) conv = purple_conversation_new(PURPLE_CONV_TYPE_IM, account, username); return conv; } PurpleConvChat *getChatConversation(TdAccountData &account, const td::td_api::chat &chat, int chatPurpleId) { std::string chatName = getPurpleChatName(chat); bool newChatCreated = false; // If account logged off with chats open, these chats will be purple_conv_chat_left()'d but not // purple_conversation_destroy()'d by purple_connection_destroy. So when logging back in, // conversation will exist but not necessarily with correct libpurple id. Therefore, lookup by // libpurple id using purple_find_chat cannot be used. PurpleConversation *conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_CHAT, chatName.c_str(), account.purpleAccount); // Such pre-open chats will (unless some other logic intervenes) be inactive (as in, // purple_conv_chat_has_left returns true) and if that's the case, serv_got_joined_chat must // still be called to make them active and thus able to send or receive messages, because that's // the kind of thing we were called for here. if ((conv == NULL) || purple_conv_chat_has_left(purple_conversation_get_chat_data(conv))) { if (chatPurpleId != 0) { purple_debug_misc(config::pluginId, "Creating conversation for chat %s (purple id %d)\n", chat.title_.c_str(), chatPurpleId); serv_got_joined_chat(purple_account_get_connection(account.purpleAccount), chatPurpleId, chatName.c_str()); conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_CHAT, chatName.c_str(), account.purpleAccount); if (conv == NULL) purple_debug_warning(config::pluginId, "Did not create conversation for chat %s\n", chat.title_.c_str()); else { // Sometimes when the group has just been created, or we left it and then got // messageChatDeleteMember, the chat will not be in buddy list. In that case, // libpurpleis going to use chatXXXXXXXXXXX as chat title. Set chat title explicitly // to prevent that. PurpleChat *purpleChat = purple_blist_find_chat(account.purpleAccount, chatName.c_str()); if (!purpleChat) { purple_debug_misc(config::pluginId, "Setting conversation title to '%s'\n", chat.title_.c_str()); purple_conversation_set_title(conv, chat.title_.c_str()); } newChatCreated = true; } } else purple_debug_warning(config::pluginId, "No internal ID for chat %s\n", chat.title_.c_str()); } if (conv) { PurpleConvChat *purpleChat = purple_conversation_get_chat_data(conv); if (purpleChat && newChatCreated) { int32_t basicGroupId = getBasicGroupId(chat); const td::td_api::basicGroupFullInfo *groupInfo = basicGroupId ? account.getBasicGroupInfo(basicGroupId) : nullptr; if (groupInfo) updateChatConversation(purpleChat, *groupInfo, account); int32_t supergroupId = getSupergroupId(chat); if (supergroupId) { const td::td_api::supergroupFullInfo *supergroupInfo = account.getSupergroupInfo(supergroupId); const td::td_api::chatMembers *members = account.getSupergroupMembers(supergroupId); if (supergroupInfo) updateChatConversation(purpleChat, *supergroupInfo, account); if (members) updateSupergroupChatMembers(purpleChat, *members, account); } } return purpleChat; } return NULL; } PurpleConvChat *findChatConversation(PurpleAccount *account, const td::td_api::chat &chat) { std::string name = getPurpleChatName(chat); PurpleConversation *conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_CHAT, name.c_str(), account); if (conv) return purple_conversation_get_chat_data(conv); return NULL; } void updatePrivateChat(TdAccountData &account, const td::td_api::chat *chat, const td::td_api::user &user) { std::string purpleUserName = getPurpleBuddyName(user); std::string alias = chat ? chat->title_ : makeBasicDisplayName(user); PurpleBuddy *buddy = purple_find_buddy(account.purpleAccount, purpleUserName.c_str()); if (buddy == NULL) { purple_debug_misc(config::pluginId, "Adding new buddy %s for user %s\n", alias.c_str(), purpleUserName.c_str()); const ContactRequest *contactReq = account.findContactRequest(user.id_); PurpleGroup *group = (contactReq && !contactReq->groupName.empty()) ? purple_find_group(contactReq->groupName.c_str()) : NULL; if (group) purple_debug_misc(config::pluginId, "Adding into group %s\n", purple_group_get_name(group)); buddy = purple_buddy_new(account.purpleAccount, purpleUserName.c_str(), alias.c_str()); purple_blist_add_buddy(buddy, NULL, group, NULL); // If a new buddy has been added here, it means that there was updateNewChat with the private // chat. This means either we added them to contacts or started messaging them, or they // messaged us. Either way, there is no need to for any extra notification about new contact // because the user will be aware anyway. // Now, in case this buddy resulted from sending a message to group chat member std::string displayName = account.getDisplayName(user); PurpleConversation *oldConv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, displayName.c_str(), account.purpleAccount); if (oldConv) { purple_conv_im_write(purple_conversation_get_im_data(oldConv), "", // TRANSLATOR: In-chat status update _("Future messages in this conversation will be shown in a different tab"), PURPLE_MESSAGE_SYSTEM, time(NULL)); } } else { purple_blist_alias_buddy(buddy, alias.c_str()); const char *oldPhotoIdStr = purple_blist_node_get_string(PURPLE_BLIST_NODE(buddy), BuddyOptions::ProfilePhotoId); int64_t oldPhotoId = 0; if (oldPhotoIdStr) sscanf(oldPhotoIdStr, "%" G_GINT64_FORMAT, &oldPhotoId); if (user.profile_photo_ && user.profile_photo_->small_) { const td::td_api::file &photo = *user.profile_photo_->small_; if (photo.local_ && photo.local_->is_downloading_completed_ && (user.profile_photo_->id_ != oldPhotoId)) { gchar *img = NULL; size_t len; GError *err = NULL; g_file_get_contents(photo.local_->path_.c_str(), &img, &len, &err); if (err) { purple_debug_warning(config::pluginId, "Failed to load profile photo %s for %s: %s\n", photo.local_->path_.c_str(), purpleUserName.c_str(), err->message); g_error_free(err); } else { std::string newPhotoIdStr = std::to_string(user.profile_photo_->id_); purple_blist_node_set_string(PURPLE_BLIST_NODE(buddy), BuddyOptions::ProfilePhotoId, newPhotoIdStr.c_str()); purple_debug_info(config::pluginId, "Loaded new profile photo for %s (id %s)\n", purpleUserName.c_str(), newPhotoIdStr.c_str()); purple_buddy_icons_set_for_user(account.purpleAccount, purpleUserName.c_str(), img, len, NULL); } } } else if (oldPhotoId) { purple_debug_info(config::pluginId, "Removing profile photo from %s\n", purpleUserName.c_str()); purple_blist_node_remove_setting(PURPLE_BLIST_NODE(buddy), BuddyOptions::ProfilePhotoId); purple_buddy_icons_set_for_user(account.purpleAccount, purpleUserName.c_str(), NULL, 0, NULL); } } } static void updateGroupChat(PurpleAccount *purpleAccount, const td::td_api::chat &chat, const td::td_api::object_ptr &groupStatus, const char *groupType, int32_t groupId) { if (!isGroupMember(groupStatus)) { purple_debug_misc(config::pluginId, "Skipping %s %d because we are not a member\n", groupType, groupId); return; } std::string chatName = getPurpleChatName(chat); PurpleChat *purpleChat = purple_blist_find_chat(purpleAccount, chatName.c_str()); if (!purpleChat) { purple_debug_misc(config::pluginId, "Adding new chat for %s %d (%s)\n", groupType, groupId, chat.title_.c_str()); purpleChat = purple_chat_new(purpleAccount, chat.title_.c_str(), getChatComponents(chat)); purple_blist_add_chat(purpleChat, NULL, NULL); } else { const char *oldName = purple_chat_get_name(purpleChat); if (chat.title_ != oldName) { purple_debug_misc(config::pluginId, "Renaming chat '%s' to '%s'\n", oldName, chat.title_.c_str()); purple_blist_alias_chat(purpleChat, chat.title_.c_str()); } } const char *oldPhotoId = purple_blist_node_get_string(PURPLE_BLIST_NODE(purpleChat), BuddyOptions::ProfilePhotoId); if (chat.photo_ && chat.photo_->small_) { const td::td_api::file &photo = *chat.photo_->small_; if (photo.local_ && photo.local_->is_downloading_completed_ && photo.remote_ && !photo.remote_->unique_id_.empty() && (!oldPhotoId || (photo.remote_->unique_id_ != oldPhotoId))) { gchar *img = NULL; size_t len; GError *err = NULL; g_file_get_contents(photo.local_->path_.c_str(), &img, &len, &err); if (err) { purple_debug_warning(config::pluginId, "Failed to load chat photo %s for %s: %s\n", photo.local_->path_.c_str(), chat.title_.c_str(), err->message); g_error_free(err); } else { purple_blist_node_set_string(PURPLE_BLIST_NODE(purpleChat), BuddyOptions::ProfilePhotoId, photo.remote_->unique_id_.c_str()); purple_debug_info(config::pluginId, "Loaded new chat photo for %s (id %s)\n", chat.title_.c_str(), photo.remote_->unique_id_.c_str()); purple_buddy_icons_node_set_custom_icon(PURPLE_BLIST_NODE(purpleChat), reinterpret_cast(img), len); } } } else if (oldPhotoId) { purple_debug_info(config::pluginId, "Removing chat photo from %s\n", chat.title_.c_str()); purple_blist_node_remove_setting(PURPLE_BLIST_NODE(purpleChat), BuddyOptions::ProfilePhotoId); purple_buddy_icons_node_set_custom_icon(PURPLE_BLIST_NODE(purpleChat), NULL, 0); } } void updateBasicGroupChat(TdAccountData &account, int32_t groupId) { const td::td_api::basicGroup *group = account.getBasicGroup(groupId); const td::td_api::chat *chat = account.getBasicGroupChatByGroup(groupId); if (!group) purple_debug_misc(config::pluginId, "Basic group %d does not exist yet\n", groupId); else if (!chat) purple_debug_misc(config::pluginId, "Chat for basic group %d does not exist yet\n", groupId); else updateGroupChat(account.purpleAccount, *chat, group->status_, "basic group", groupId); } void updateSupergroupChat(TdAccountData &account, int32_t groupId) { const td::td_api::supergroup *group = account.getSupergroup(groupId); const td::td_api::chat *chat = account.getSupergroupChatByGroup(groupId); if (!group) purple_debug_misc(config::pluginId, "Supergroup %d does not exist yet\n", groupId); else if (!chat) purple_debug_misc(config::pluginId, "Chat for supergroup %d does not exist yet\n", groupId); else updateGroupChat(account.purpleAccount, *chat, group->status_, "supergroup", groupId); } void removeGroupChat(PurpleAccount *purpleAccount, const td::td_api::chat &chat) { std::string chatName = getPurpleChatName(chat); PurpleChat *purpleChat = purple_blist_find_chat(purpleAccount, chatName.c_str()); if (purpleChat) purple_blist_remove_chat(purpleChat); } static PurpleMessageFlags getNotificationFlags(PurpleMessageFlags extraFlags) { unsigned flags = (extraFlags & PURPLE_MESSAGE_ERROR) | (extraFlags & PURPLE_MESSAGE_NO_LOG); if (flags == 0) flags = PURPLE_MESSAGE_SYSTEM; return (PurpleMessageFlags)flags; } void showMessageTextIm(TdAccountData &account, const char *purpleUserName, const char *text, const char *notification, time_t timestamp, PurpleMessageFlags flags) { PurpleConversation *conv = NULL; if (text) { if (flags & PURPLE_MESSAGE_SEND) { // serv_got_im seems to work for messages sent from another client, but not for // echoed messages from this client. Therefore, this (code snippet from facebook plugin). conv = getImConversation(account.purpleAccount, purpleUserName); purple_conv_im_write(purple_conversation_get_im_data(conv), purple_account_get_name_for_display(account.purpleAccount), text, flags, timestamp); } else { serv_got_im(purple_account_get_connection(account.purpleAccount), purpleUserName, text, flags, timestamp); } } if (notification) { if (conv == NULL) conv = getImConversation(account.purpleAccount, purpleUserName); purple_conv_im_write(purple_conversation_get_im_data(conv), purpleUserName, notification, getNotificationFlags(flags), timestamp); } } static void showMessageTextChat(TdAccountData &account, const td::td_api::chat &chat, const TgMessageInfo &message, const char *text, const char *notification, PurpleMessageFlags flags) { // Again, doing what facebook plugin does int purpleId = account.getPurpleChatId(chat.id_); PurpleConvChat *conv = getChatConversation(account, chat, purpleId); if (text) { if (flags & PURPLE_MESSAGE_SEND) { if (conv) purple_conv_chat_write(conv, purple_account_get_name_for_display(account.purpleAccount), text, flags, message.timestamp); } else { if (purpleId != 0) serv_got_chat_in(purple_account_get_connection(account.purpleAccount), purpleId, message.sender.empty() ? "someone" : message.sender.c_str(), flags, text, message.timestamp); } } if (notification) { if (conv) // Protocol plugins mostly use who="" for such messages, but this currently causes problems // with Spectrum. Use some non-empty string. Pidgin will ignore the who parameter for // notification messages. purple_conv_chat_write(conv, " ", notification, getNotificationFlags(flags), message.timestamp); } } std::string getMessageText(const td::td_api::formattedText &text) { char *newText = purple_markup_escape_text(text.text_.c_str(), text.text_.size()); std::string result(newText); g_free(newText); return result; } std::string makeInlineImageText(int imgstoreId) { return "\n"; } static std::string quoteMessage(const td::td_api::message *message, TdAccountData &account) { const td::td_api::user *originalAuthor = nullptr; if (message) originalAuthor = account.getUser(message->sender_user_id_); std::string originalName; if (originalAuthor) originalName = account.getDisplayName(*originalAuthor); else { // message == NULL means it could not be fetched, or took too long to fetch // TRANSLATOR: In-line placeholder if the original author of a quote is unknown. Is at the beginning of the line if and only if you make it so, see "&bt {} wrote:"... originalName = _("Unknown user"); } std::string text; if (!message || !message->content_) { // TRANSLATOR: In-chat placeholder when something unknown is being replied to. text = _("[message unavailable]"); } else switch (message->content_->get_id()) { case td::td_api::messageText::ID: { const td::td_api::messageText &messageText = static_cast(*message->content_); if (messageText.text_) text = getMessageText(*messageText.text_); else text = ""; break; } case td::td_api::messagePhoto::ID: { const td::td_api::messagePhoto &photo = static_cast(*message->content_); // TRANSLATOR: In-line placeholder when a photo is being replied to. text = _("[photo]"); if (photo.caption_) text += " " + photo.caption_->text_; break; } case td::td_api::messageDocument::ID: { const td::td_api::messageDocument &document = static_cast(*message->content_); if (document.document_) { // TRANSLATOR: In-line placeholder when a file is being replied to. Arguments will be the file name and MIME type (e.g. "application/gzip") text = formatMessage(_("[file: {0} ({1})]"), {document.document_->file_name_, document.document_->mime_type_}); } else { // Not supposed to be possible, but just in case text = "[file]"; } if (document.caption_) text += " " + document.caption_->text_; break; } case td::td_api::messageVideo::ID: { const td::td_api::messageVideo &video = static_cast(*message->content_); if (video.video_) { // TRANSLATOR: In-line placeholder when a video is being replied to. Argument will be the file name. text = formatMessage(_("[video: {}]"), video.video_->file_name_); } else { // Not supposed to be possible, but just in case text = "[video]"; } if (video.caption_) text += " " + video.caption_->text_; break; } case td::td_api::messageSticker::ID: // TRANSLATOR: In-line placeholder when a sticker is being replied to. text = _("[sticker]"); break; default: text = '[' + getUnsupportedMessageDescription(*message->content_) + ']'; break; } for (unsigned i = 0; i < text.size(); i++) if (text[i] == '\n') text[i] = ' '; // TRANSLATOR: In-chat notification of a reply. Arguments will be username and the original text or description thereof. Please preserve the HTML. return formatMessage(_("> {0} wrote:\n> {1}"), {originalName, text}); } void showMessageText(TdAccountData &account, const td::td_api::chat &chat, const TgMessageInfo &message, const char *text, const char *notification, uint32_t extraFlags) { PurpleMessageFlags directionFlag = message.outgoing ? PURPLE_MESSAGE_SEND : PURPLE_MESSAGE_RECV; PurpleMessageFlags flags = (PurpleMessageFlags) (extraFlags | directionFlag); if (message.outgoing && !message.sentLocally) flags = (PurpleMessageFlags) (flags | PURPLE_MESSAGE_REMOTE_SEND); std::string newText; if (message.repliedMessageId != 0) newText = quoteMessage(message.repliedMessage.get(), account); if (!message.forwardedFrom.empty()) { if (!newText.empty()) newText += "\n"; // TRANSLATOR: In-chat notification of forward. Argument will be a username. Please preserve the HTML. newText += formatMessage(_("Forwarded from {}:"), message.forwardedFrom); } if (text) { if (!newText.empty()) newText += "\n"; newText += text; } if (!newText.empty()) text = newText.c_str(); const td::td_api::user *privateUser = account.getUserByPrivateChat(chat); if (privateUser) { std::string userName; if (isChatInContactList(chat, privateUser)) userName = getPurpleBuddyName(*privateUser); else userName = account.getDisplayName(*privateUser); showMessageTextIm(account, userName.c_str(), text, notification, message.timestamp, flags); } if (getBasicGroupId(chat) || getSupergroupId(chat)) showMessageTextChat(account, chat, message, text, notification, flags); } void showChatNotification(TdAccountData &account, const td::td_api::chat &chat, const char *notification, PurpleMessageFlags flags) { TgMessageInfo messageInfo; messageInfo.type = TgMessageInfo::Type::Other; messageInfo.timestamp = (flags == PURPLE_MESSAGE_NO_LOG) ? 0 : time(NULL); messageInfo.outgoing = true; showMessageText(account, chat, messageInfo, NULL, notification, flags); } std::string makeBasicDisplayName(const td::td_api::user &user) { std::string result = user.first_name_; if (!result.empty() && !user.last_name_.empty()) result += ' '; result += user.last_name_; return result; } std::string getSenderPurpleName(const td::td_api::chat &chat, const td::td_api::message &message, TdAccountData &account) { if (!message.is_outgoing_ && (getBasicGroupId(chat) || getSupergroupId(chat))) { if (message.sender_user_id_) return account.getDisplayName(message.sender_user_id_); else if (!message.author_signature_.empty()) return message.author_signature_; else if (message.is_channel_post_) { // TRANSLATOR: The "sender" of a message that was posted to a channel. Will be used like a username. return _("Channel post"); } else if (message.forward_info_ && message.forward_info_->origin_) switch (message.forward_info_->origin_->get_id()) { case td::td_api::messageForwardOriginUser::ID: return account.getDisplayName(static_cast(*message.forward_info_->origin_).sender_user_id_); case td::td_api::messageForwardOriginHiddenUser::ID: return static_cast(*message.forward_info_->origin_).sender_name_; case td::td_api::messageForwardOriginChannel::ID: return static_cast(*message.forward_info_->origin_).author_signature_; } } // For outgoing messages, our name will be used instead // For private chats, sender name will be determined from the chat instead return ""; } std::string getForwardSource(const td::td_api::messageForwardInfo &forwardInfo, TdAccountData &account) { if (!forwardInfo.origin_) return ""; switch (forwardInfo.origin_->get_id()) { case td::td_api::messageForwardOriginUser::ID: return account.getDisplayName(static_cast(*forwardInfo.origin_).sender_user_id_); case td::td_api::messageForwardOriginHiddenUser::ID: return static_cast(*forwardInfo.origin_).sender_name_; case td::td_api::messageForwardOriginChannel::ID: { const td::td_api::chat *chat = account.getChat(static_cast(*forwardInfo.origin_).chat_id_); if (chat) return chat->title_; } } return ""; } void getNamesFromAlias(const char *alias, std::string &firstName, std::string &lastName) { if (!alias) alias = ""; const char *name1end = alias; while (*name1end && isspace(*name1end)) name1end++; while (*name1end && !isspace(*name1end)) name1end++; firstName = std::string(alias, name1end-alias); const char *name2start = name1end; while (*name2start && isspace(*name2start)) name2start++; lastName = name2start; } static void findChatsByComponents(PurpleBlistNode *node, const char *joinString, const char *groupName, int groupType, std::vector &result) { PurpleBlistNodeType nodeType = purple_blist_node_get_type(node); if (nodeType == PURPLE_BLIST_CHAT_NODE) { PurpleChat *chat = PURPLE_CHAT(node); GHashTable *components = purple_chat_get_components(chat); const char *nodeName = getChatName(components); const char *nodeJoinString = getChatJoinString(components); const char *nodeGroupName = getChatGroupName(components); int nodeGroupType = getChatGroupType(components); if (!nodeName) nodeName = ""; if (!nodeJoinString) nodeJoinString = ""; if (!nodeGroupName) nodeGroupName = ""; if (!strcmp(nodeName, "") && !strcmp(nodeJoinString, joinString)) { if ((*joinString != '\0') || (!strcmp(nodeGroupName, groupName) && (nodeGroupType == groupType))) { result.push_back(chat); } } } for (PurpleBlistNode *child = purple_blist_node_get_first_child(node); child; child = purple_blist_node_get_sibling_next(child)) { findChatsByComponents(child, joinString, groupName, groupType, result); } } std::vectorfindChatsByJoinString(const std::string &joinString) { std::vector result; for (PurpleBlistNode *root = purple_blist_get_root(); root; root = purple_blist_node_get_sibling_next(root)) // LOL { findChatsByComponents(root, joinString.c_str(), "", 0, result); } return result; } std::vector findChatsByNewGroup(const char *name, int type) { std::vector result; for (PurpleBlistNode *root = purple_blist_get_root(); root; root = purple_blist_node_get_sibling_next(root)) // LOL { findChatsByComponents(root, "", name, type, result); } return result; } static void setChatMembers(PurpleConvChat *purpleChat, const std::vector> &members, const TdAccountData &account) { GList *flags = NULL; std::vector nameData; for (const auto &member: members) { if (!member || !isGroupMember(member->status_)) continue; const td::td_api::user *user = account.getUser(member->user_id_); if (!user || (user->type_ && (user->type_->get_id() == td::td_api::userTypeDeleted::ID))) continue; std::string userName = getPurpleBuddyName(*user); const char *phoneNumber = getCanonicalPhoneNumber(user->phone_number_.c_str()); if (purple_find_buddy(account.purpleAccount, userName.c_str())) // libpurple will be able to map user name to alias because there is a buddy nameData.emplace_back(userName); else if (!strcmp(getCanonicalPhoneNumber(purple_account_get_username(account.purpleAccount)), phoneNumber)) // This is us, so again libpurple will map phone number to alias nameData.emplace_back(purple_account_get_username(account.purpleAccount)); else { // Use first and last name instead std::string displayName = account.getDisplayName(*user); nameData.emplace_back(displayName); } PurpleConvChatBuddyFlags flag; if (member->status_->get_id() == td::td_api::chatMemberStatusCreator::ID) flag = PURPLE_CBFLAGS_FOUNDER; else if (member->status_->get_id() == td::td_api::chatMemberStatusAdministrator::ID) flag = PURPLE_CBFLAGS_OP; else flag = PURPLE_CBFLAGS_NONE; flags = g_list_append(flags, GINT_TO_POINTER(flag)); } GList *names = NULL; for (const std::string &name: nameData) names = g_list_append(names, const_cast(name.c_str())); purple_conv_chat_clear_users(purpleChat); purple_conv_chat_add_users(purpleChat, names, NULL, flags, false); g_list_free(names); g_list_free(flags); } void updateChatConversation(PurpleConvChat *purpleChat, const td::td_api::basicGroupFullInfo &groupInfo, const TdAccountData &account) { purple_conv_chat_set_topic(purpleChat, NULL, groupInfo.description_.c_str()); setChatMembers(purpleChat, groupInfo.members_, account); } void updateChatConversation(PurpleConvChat *purpleChat, const td::td_api::supergroupFullInfo &groupInfo, const TdAccountData &account) { purple_conv_chat_set_topic(purpleChat, NULL, groupInfo.description_.c_str()); } void updateSupergroupChatMembers(PurpleConvChat* purpleChat, const td::td_api::chatMembers& members, const TdAccountData& account) { setChatMembers(purpleChat, members.members_, account); } struct MessagePart { bool isImage = false; int imageId = 0; std::string text; }; static size_t splitTextChunk(MessagePart &part, const char *text, size_t length, TdAccountData &account) { enum {MIN_LENGTH_LIMIT = 8}; unsigned lengthLimit = part.isImage ? account.options.maxCaptionLength : account.options.maxMessageLength; if (lengthLimit == 0) purple_debug_warning(config::pluginId, "No %s length limit\n", part.isImage ? "caption" : "message"); else if (lengthLimit <= MIN_LENGTH_LIMIT) purple_debug_warning(config::pluginId, "%u is a ridiculous %s length limit\n", lengthLimit, part.isImage ? "caption" : "message"); if ((lengthLimit <= MIN_LENGTH_LIMIT) || (length <= lengthLimit)) { part.text = std::string(text, length); return length; } // Try to truncate at a line break, with no lower length limit in case of image caption unsigned newlineSplitLowerLimit = part.isImage ? 1 : lengthLimit/2; for (unsigned chunkLength = lengthLimit; chunkLength >= newlineSplitLowerLimit; chunkLength--) if (text[chunkLength-1] == '\n') { part.text = std::string(text, chunkLength-1); return chunkLength; } // Try to truncate to a whole number of utf-8 characters size_t chunkLen; const char *pos = g_utf8_find_prev_char(text, text+lengthLimit); if (pos != NULL) { const char *next = g_utf8_find_next_char(pos, NULL); if (next == text+lengthLimit) chunkLen = lengthLimit; else chunkLen = pos-text; } else chunkLen = lengthLimit; part.text = std::string(text, chunkLen); return chunkLen; } static void appendText(std::vector &parts, const char *s, size_t len, TdAccountData &account) { if (len != 0) { if (parts.empty()) parts.emplace_back(); std::string sourceText(s, len); char *halfNewText = purple_markup_strip_html(sourceText.c_str()); char *newText = purple_unescape_html(halfNewText); g_free(halfNewText); const char *remaining = newText; size_t lenRemaining = strlen(newText); while (lenRemaining) { size_t chunkLength = splitTextChunk(parts.back(), remaining, lenRemaining, account); lenRemaining -= chunkLength; remaining += chunkLength; if (lenRemaining) parts.emplace_back(); } g_free(newText); } } static void parseMessage(const char *message, std::vector &parts, TdAccountData &account) { parts.clear(); if (!message) return; const char *s = message; const char *textStart = message; while (*s) { bool isImage = false; long imageId; char *pastImage = NULL; if (!strncasecmp(s, "", 2) && (imageId <= INT32_MAX) && (imageId >= INT32_MIN)) { isImage = true; pastImage += 2; if (*pastImage == '\n') pastImage++; } } if (isImage) { appendText(parts, textStart, s-textStart, account); parts.emplace_back(); parts.back().isImage = true; parts.back().imageId = imageId; s = pastImage; textStart = pastImage; } else s++; } appendText(parts, textStart, s-textStart, account); } int transmitMessage(int64_t chatId, const char *message, TdTransceiver &transceiver, TdAccountData &account, TdTransceiver::ResponseCb response) { std::vector parts; parseMessage(message, parts, account); if (parts.size() > MAX_MESSAGE_PARTS) return -E2BIG; for (const MessagePart &input: parts) { td::td_api::object_ptr sendMessageRequest = td::td_api::make_object(); sendMessageRequest->chat_id_ = chatId; char *tempFileName = NULL; bool hasImage = false; if (input.isImage) hasImage = saveImage(input.imageId, &tempFileName); if (hasImage) { td::td_api::object_ptr content = td::td_api::make_object(); content->photo_ = td::td_api::make_object(tempFileName); content->caption_ = td::td_api::make_object(); content->caption_->text_ = input.text; sendMessageRequest->input_message_content_ = std::move(content); purple_debug_misc(config::pluginId, "Sending photo %s\n", tempFileName); } else { td::td_api::object_ptr content = td::td_api::make_object(); content->text_ = td::td_api::make_object(); content->text_->text_ = input.text; sendMessageRequest->input_message_content_ = std::move(content); } int64_t chatId = sendMessageRequest->chat_id_; uint64_t requestId = transceiver.sendQuery(std::move(sendMessageRequest), response); account.addPendingRequest(requestId, chatId, tempFileName); if (tempFileName) g_free(tempFileName); } return 0; } std::string getSenderDisplayName(const td::td_api::chat &chat, const TgMessageInfo &message, PurpleAccount *account) { if (message.outgoing) return purple_account_get_name_for_display(account); else if (isPrivateChat(chat)) return chat.title_; else return message.sender; } std::string makeNoticeWithSender(const td::td_api::chat &chat, const TgMessageInfo &message, const char *noticeText, PurpleAccount *account) { std::string prefix = getSenderDisplayName(chat, message, account); if (!prefix.empty()) prefix += ": "; return prefix + noticeText; } void showGenericFileInline(const td::td_api::chat &chat, const TgMessageInfo &message, const std::string &filePath, const std::string &fileDescription, TdAccountData &account) { if (filePath.find('"') != std::string::npos) { std::string notice = makeNoticeWithSender(chat, message, "Cannot show file: path contains quotes", account.purpleAccount); showMessageText(account, chat, message, NULL, notice.c_str()); } else { std::string text = "" + fileDescription + ""; showMessageText(account, chat, message, text.c_str(), NULL); } } void notifySendFailed(const td::td_api::updateMessageSendFailed &sendFailed, TdAccountData &account) { if (sendFailed.message_) { const td::td_api::chat *chat = account.getChat(sendFailed.message_->chat_id_); if (chat) { TgMessageInfo messageInfo; messageInfo.type = TgMessageInfo::Type::Other; messageInfo.timestamp = sendFailed.message_->date_; messageInfo.outgoing = true; std::string errorMessage = formatMessage(errorCodeMessage(), {std::to_string(sendFailed.error_code_), sendFailed.error_message_}); // TRANSLATOR: In-chat error message, argument will be text. errorMessage = formatMessage(_("Failed to send message: {}"), errorMessage); showMessageText(account, *chat, messageInfo, NULL, errorMessage.c_str()); } } } static void closeSecretChat(int32_t secretChatId, TdTransceiver &transceiver) { transceiver.sendQuery(td::td_api::make_object(secretChatId), nullptr); } static void secretChatNotSupported(int32_t secretChatId, const std::string &userDescription, TdTransceiver &transceiver, PurpleAccount *purpleAccount) { closeSecretChat(secretChatId, transceiver); std::string message = formatMessage("Rejected secret chat with {}", userDescription); purple_notify_info(purple_account_get_connection(purpleAccount), "Secret chat", message.c_str(), "Secret chats not supported"); } struct SecretChatInfo { int32_t secretChatId; std::string userDescription; TdTransceiver *transceiver; PurpleAccount *purpleAccount; }; static void acceptSecretChatCb(SecretChatInfo *data) { std::unique_ptr info(data); secretChatNotSupported(info->secretChatId, info->userDescription, *info->transceiver, info->purpleAccount); } static void discardSecretChatCb(SecretChatInfo *data) { std::unique_ptr info(data); closeSecretChat(info->secretChatId, *info->transceiver); } void updateSecretChat(td::td_api::object_ptr secretChat, TdTransceiver &transceiver, TdAccountData &account) { if (!secretChat) return; int32_t secretChatId = secretChat->id_; bool isExisting = account.getSecretChat(secretChatId); const td::td_api::user *user = account.getUser(secretChat->user_id_); account.addSecretChat(std::move(secretChat)); std::string userDescription; if (user) userDescription = '\'' + account.getDisplayName(*user) + '\''; else { // Not supposed to be possible, because every user id should be preceded by user info userDescription = "(unknown user)"; } if (!isExisting) { const char *secretChatHandling = purple_account_get_string(account.purpleAccount, AccountOptions::AcceptSecretChats, AccountOptions::AcceptSecretChatsDefault); if (!strcmp(secretChatHandling, AccountOptions::AcceptSecretChatsNever)) { closeSecretChat(secretChatId, transceiver); // TRANSLATOR: Dialog content, argument will be a username std::string message = formatMessage(_("Rejected secret chat with {}"), userDescription); purple_notify_info(purple_account_get_connection(account.purpleAccount), // TRANSLATOR: Dialog title _("Secret chat"), message.c_str(), NULL); } else if (!strcmp(secretChatHandling, AccountOptions::AcceptSecretChatsAlways)) secretChatNotSupported(secretChatId, userDescription, transceiver, account.purpleAccount); else { // TRANSLATOR: Dialog content, argument will be a username, options will be "_Accept" and "_Cancel". std::string message = formatMessage(_("Accept secret chat with {} on this device?"), userDescription); SecretChatInfo *data = new SecretChatInfo{secretChatId, userDescription, &transceiver, account.purpleAccount}; purple_request_action(purple_account_get_connection(account.purpleAccount), // TRANSLATOR: Dialog title _("Secret chat"), message.c_str(), // TRANSLATOR: Dialog secondary content. Options will be "_Accept" and "_Cancel". _("Secret chats can only have one " "end point. If you accept a secret chat on this device, its messages will not be available anywhere " "else. If you decline, you can still accept the chat on other devices."), 0, account.purpleAccount, NULL, NULL, data, 2, // TRANSLATOR: Dialog option, regarding a secret chat; the alternative is "_Cancel" _("_Accept"), acceptSecretChatCb, // TRANSLATOR: Dialog option, regarding a secret chat; the alternative is "_Accept" _("_Cancel"), discardSecretChatCb); } } } void updateOption(const td::td_api::updateOption &option, TdAccountData &account) { if ((option.name_ == "version") && option.value_ && (option.value_->get_id() == td::td_api::optionValueString::ID)) { purple_debug_misc(config::pluginId, "tdlib version: %s\n", static_cast(*option.value_).value_.c_str()); } else if ((option.name_ == "message_caption_length_max") && option.value_ && (option.value_->get_id() == td::td_api::optionValueInteger::ID)) { account.options.maxCaptionLength = std::max(0, static_cast(*option.value_).value_); } else if ((option.name_ == "message_text_length_max") && option.value_ && (option.value_->get_id() == td::td_api::optionValueInteger::ID)) { account.options.maxMessageLength = std::max(0, static_cast(*option.value_).value_); } else purple_debug_misc(config::pluginId, "Option update %s\n", option.name_.c_str()); } void populateGroupChatList(PurpleRoomlist *roomlist, const std::vector &chats, const TdAccountData &account) { for (const td::td_api::chat *chat: chats) if (account.isGroupChatWithMembership(*chat)) { PurpleRoomlistRoom *room = purple_roomlist_room_new(PURPLE_ROOMLIST_ROOMTYPE_ROOM, chat->title_.c_str(), NULL); purple_roomlist_room_add_field (roomlist, room, getPurpleChatName(*chat).c_str()); int32_t groupId = getBasicGroupId(*chat); if (groupId) { const td::td_api::basicGroupFullInfo *fullInfo = account.getBasicGroupInfo(groupId); if (fullInfo && !fullInfo->description_.empty()) purple_roomlist_room_add_field(roomlist, room, fullInfo->description_.c_str()); } groupId = getSupergroupId(*chat); if (groupId) { const td::td_api::supergroupFullInfo *fullInfo = account.getSupergroupInfo(groupId); if (fullInfo && !fullInfo->description_.empty()) purple_roomlist_room_add_field(roomlist, room, fullInfo->description_.c_str()); } purple_roomlist_room_add(roomlist, room); } purple_roomlist_set_in_progress(roomlist, FALSE); } AccountThread::AccountThread(PurpleAccount* purpleAccount, AccountThread::Callback callback) { m_accountUserName = purple_account_get_username(purpleAccount); m_accountProtocolId = purple_account_get_protocol_id(purpleAccount); m_callback = callback; } void AccountThread::threadFunc() { run(); g_idle_add(&AccountThread::mainThreadCallback, this); } static bool g_singleThread = false; void AccountThread::setSingleThread() { g_singleThread = true; } bool AccountThread::isSingleThread() { return g_singleThread; } void AccountThread::startThread() { if (!g_singleThread) { if (!m_thread.joinable()) m_thread = std::thread(std::bind(&AccountThread::threadFunc, this)); } else { run(); mainThreadCallback(this); } } gboolean AccountThread::mainThreadCallback(gpointer data) { AccountThread *self = static_cast(data); PurpleAccount *account = purple_accounts_find(self->m_accountUserName.c_str(), self->m_accountProtocolId.c_str()); PurpleTdClient *tdClient = account ? getTdClient(account) : nullptr; if (self->m_thread.joinable()) self->m_thread.join(); if (tdClient) (tdClient->*(self->m_callback))(self); return FALSE; // this idle callback will not be called again }