#include "client-utils.h" #include "purple-info.h" #include "config.h" #include "format.h" #include "receiving.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_AWAY); } 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::string getSecretChatBuddyName(SecretChatId secretChatId) { return "secret" + std::to_string(secretChatId.value()); } std::vector getUsersByPurpleName(const char *buddyName, TdAccountData &account, const char *action) { std::vector result; UserId userId = purpleBuddyNameToUserId(buddyName); if (userId.valid()) { 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) { BasicGroupId basicGroupId = getBasicGroupId(chat); const td::td_api::basicGroupFullInfo *groupInfo = basicGroupId.valid() ? account.getBasicGroupInfo(basicGroupId) : nullptr; if (groupInfo) updateChatConversation(purpleChat, *groupInfo, account); SupergroupId supergroupId = getSupergroupId(chat); if (supergroupId.valid()) { 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(getId(user)); 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 or from // someone new messaging us 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(TdAccountData &account, 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(account.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(account.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()); } } if (account.isExpectedChat(getId(chat))) { PurpleConversation *baseConv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_CHAT, chatName.c_str(), account.purpleAccount); if (baseConv && purple_conv_chat_has_left(purple_conversation_get_chat_data(baseConv))) { purple_debug_misc(config::pluginId, "Rejoining chat %s as previously requested\n", chatName.c_str()); serv_got_joined_chat(purple_account_get_connection(account.purpleAccount), account.getPurpleChatId(getId(chat)), chatName.c_str()); } account.removeExpectedChat(getId(chat)); } 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, BasicGroupId 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.value()); else if (!chat) purple_debug_misc(config::pluginId, "Chat for basic group %d does not exist yet\n", groupId.value()); else updateGroupChat(account, *chat, group->status_, "basic group", groupId.value()); } void updateSupergroupChat(TdAccountData &account, SupergroupId 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.value()); else if (!chat) purple_debug_misc(config::pluginId, "Chat for supergroup %d does not exist yet\n", groupId.value()); else updateGroupChat(account, *chat, group->status_, "supergroup", groupId.value()); } 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); } 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 getIncomingGroupchatSenderPurpleName(const td::td_api::chat &chat, const td::td_api::message &message, const TdAccountData &account) { if (!message.is_outgoing_ && (getBasicGroupId(chat).valid() || getSupergroupId(chat).valid())) { UserId senderId = getSenderUserId(message); if (senderId.valid()) return account.getDisplayName(senderId); 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(getSenderUserId(static_cast(*message.forward_info_->origin_))); 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 and secret chats, sender name will be determined from the chat instead return ""; } std::string getForwardSource(const td::td_api::messageForwardInfo &forwardInfo, const TdAccountData &account) { if (!forwardInfo.origin_) return ""; switch (forwardInfo.origin_->get_id()) { case td::td_api::messageForwardOriginUser::ID: return account.getDisplayName(getSenderUserId(static_cast(*forwardInfo.origin_))); 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(getChatId(static_cast(*forwardInfo.origin_))); 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(getUserId(*member)); 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(ChatId 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.value(); 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); } 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) || getSecretChatId(chat).valid()) return chat.title_; else return message.incomingGroupchatSender; } std::string getDownloadXferPeerName(ChatId chatId, const TgMessageInfo &message, TdAccountData &account) { const td::td_api::chat *chat = account.getChat(chatId); if (chat) { const td::td_api::user *privateUser = account.getUserByPrivateChat(*chat); if (privateUser) return getPurpleBuddyName(*privateUser); auto secretChatId = getSecretChatId(*chat); if (secretChatId.valid()) return getSecretChatBuddyName(secretChatId); } return message.incomingGroupchatSender; } void notifySendFailed(const td::td_api::updateMessageSendFailed &sendFailed, TdAccountData &account) { if (sendFailed.message_) { const td::td_api::chat *chat = account.getChat(getChatId(*sendFailed.message_)); if (chat) { 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); showChatNotification(account, *chat, errorMessage.c_str(), sendFailed.message_->date_, (PurpleMessageFlags)0); } } } 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()); BasicGroupId groupId = getBasicGroupId(*chat); if (groupId.valid()) { 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()); } SupergroupId supergroupId = getSupergroupId(*chat); if (supergroupId.valid()) { const td::td_api::supergroupFullInfo *fullInfo = account.getSupergroupInfo(supergroupId); 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) { m_accountUserName = purple_account_get_username(purpleAccount); m_accountProtocolId = purple_account_get_protocol_id(purpleAccount); } 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) self->callback(tdClient); return FALSE; // this idle callback will not be called again }