#include "../../plugins/Pleroma.hpp" #include "../../include/Json.hpp" #include "../../include/NetUtils.hpp" extern "C" { #include } namespace QuickMedia { struct HtmlParseUserdata { std::string result; }; static void html_parse_callback(HtmlParser *html_parser, HtmlParseType parse_type, void *userdata) { HtmlParseUserdata *parse_userdata = (HtmlParseUserdata*)userdata; if(parse_type == HTML_PARSE_TEXT) { parse_userdata->result.append(html_parser->text_stripped.data, html_parser->text_stripped.size); } else if(parse_type == HTML_PARSE_TAG_END) { if(html_parser->tag_name.size == 4 && strncmp(html_parser->tag_name.data, "span", 4) == 0) parse_userdata->result += ' '; else if(html_parser->tag_name.size == 1 && strncmp(html_parser->tag_name.data, "p", 1) == 0) parse_userdata->result += "\n\n"; else if(html_parser->tag_name.size == 2 && strncmp(html_parser->tag_name.data, "br", 2) == 0) parse_userdata->result += '\n'; } } std::shared_ptr post_json_to_body_item(const rapidjson::Value &post_json, const std::string &instance, int &num_favorites, int &num_reblogs, int &num_replies) { num_favorites = 0; num_reblogs = 0; num_replies = 0; if(!post_json.IsObject()) return nullptr; const rapidjson::Value &account_json = GetMember(post_json, "account"); if(!account_json.IsObject()) return nullptr; const rapidjson::Value &acct_json = GetMember(account_json, "acct"); if(!acct_json.IsString()) return nullptr; std::string username; const rapidjson::Value &display_name_json = GetMember(account_json, "display_name"); if(display_name_json.IsString()) { username.append(display_name_json.GetString(), display_name_json.GetStringLength()); username += ' '; } username.append(acct_json.GetString(), acct_json.GetStringLength()); if(!strchr(acct_json.GetString(), '@')) username += "@" + instance; auto body_item = BodyItem::create(""); body_item->set_author(std::move(username)); const rapidjson::Value &content_json = GetMember(post_json, "content"); if(content_json.IsString()) { std::string content(content_json.GetString(), content_json.GetStringLength()); HtmlParseUserdata parse_userdata; HtmlParser html_parser; html_parser_init(&html_parser, content.data(), content.size(), html_parse_callback, &parse_userdata); html_parser_parse(&html_parser); html_parser_deinit(&html_parser); while(!parse_userdata.result.empty() && parse_userdata.result.back() == '\n') parse_userdata.result.pop_back(); html_unescape_sequences(parse_userdata.result); body_item->set_description(std::move(parse_userdata.result)); } const rapidjson::Value &avatar_json = GetMember(account_json, "avatar"); if(avatar_json.IsString()) { body_item->thumbnail_url = std::string(avatar_json.GetString(), avatar_json.GetStringLength()); body_item->thumbnail_size = sf::Vector2i(48, 48); body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE; } // TODO: Display all attached images by embedded images in Text. Each image in Text should always be on its own line const rapidjson::Value &media_attachments_json = GetMember(post_json, "media_attachments"); if(media_attachments_json.IsArray() && !media_attachments_json.Empty() && media_attachments_json.GetArray()[0].IsObject()) { const rapidjson::Value &media_attachment_json = media_attachments_json.GetArray()[0]; const rapidjson::Value &preview_url = GetMember(media_attachment_json, "preview_url"); const rapidjson::Value &type_json = GetMember(media_attachment_json, "type"); // TODO: Support video preview image, or display dummy image that shows there is a video if(preview_url.IsString() && type_json.IsString() && strcmp(type_json.GetString(), "image") == 0) { body_item->thumbnail_url = std::string(preview_url.GetString(), preview_url.GetStringLength()); body_item->thumbnail_size = sf::Vector2i(0, 0); body_item->thumbnail_mask_type = ThumbnailMaskType::NONE; } } const rapidjson::Value &favourites_count_json = GetMember(post_json, "favourites_count"); if(favourites_count_json.IsNumber()) num_favorites = favourites_count_json.GetInt(); const rapidjson::Value &reblogs_count_json = GetMember(post_json, "reblogs_count"); if(reblogs_count_json.IsNumber()) num_reblogs = reblogs_count_json.GetInt(); const rapidjson::Value &replies_count_json = GetMember(post_json, "replies_count"); if(replies_count_json.IsNumber()) num_replies = replies_count_json.GetInt(); const rapidjson::Value &id_json = GetMember(post_json, "id"); if(id_json.IsString()) body_item->url.assign(id_json.GetString(), id_json.GetStringLength()); return body_item; } PluginResult Pleroma::get_home_posts(BodyItems &results, const std::string &max_id) { std::vector additional_args = { { "-X", "GET" }, { "-H", "Authorization: Bearer " + auth_token } }; char request_url[512]; if(max_id.empty()) snprintf(request_url, sizeof(request_url), "https://%s/api/v1/timelines/home", instance.c_str()); else snprintf(request_url, sizeof(request_url), "https://%s/api/v1/timelines/home?max_id=%s", instance.c_str(), max_id.c_str()); rapidjson::Document json_root; DownloadResult download_result = download_json(json_root, request_url, std::move(additional_args), true); if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); if(!json_root.IsArray()) return PluginResult::ERR; for(auto &post_json : json_root.GetArray()) { int post_num_favourites = 0; int post_num_reblogs = 0; int post_num_replies = 0; auto post_body_item = post_json_to_body_item(post_json, instance, post_num_favourites, post_num_reblogs, post_num_replies); if(!post_body_item) continue; const rapidjson::Value &reblog_json = GetMember(post_json, "reblog"); if(reblog_json.IsObject()) { auto reblog_body_item = post_json_to_body_item(reblog_json, instance, post_num_favourites, post_num_reblogs, post_num_replies); if(reblog_body_item) { post_body_item->set_author(post_body_item->get_author() + " reposted:\n" + reblog_body_item->get_author()); post_body_item->set_description(reblog_body_item->get_description()); if(reblog_body_item->thumbnail_url.empty()) { post_body_item->thumbnail_url.clear(); post_body_item->thumbnail_size = sf::Vector2i(0, 0); post_body_item->thumbnail_mask_type = ThumbnailMaskType::NONE; } else { post_body_item->thumbnail_url = std::move(reblog_body_item->thumbnail_url); post_body_item->thumbnail_size = reblog_body_item->thumbnail_size; post_body_item->thumbnail_mask_type = reblog_body_item->thumbnail_mask_type; } } } post_body_item->add_reaction("Replies: " + std::to_string(post_num_replies), nullptr); post_body_item->add_reaction("Reposts: " + std::to_string(post_num_reblogs), nullptr); post_body_item->add_reaction("👍: " + std::to_string(post_num_favourites), nullptr); results.push_back(std::move(post_body_item)); } return PluginResult::OK; } PluginResult PleromaHomePage::lazy_fetch(BodyItems &result_items) { PluginResult result = pleroma->get_home_posts(result_items); if(result == PluginResult::OK && !result_items.empty()) last_item_id = result_items.back()->url; return result; } PluginResult PleromaHomePage::get_page(const std::string&, int page, BodyItems &result_items) { // TODO: handle search param |arg1| while(current_page < page && !last_item_id.empty()) { PluginResult result = pleroma->get_home_posts(result_items, last_item_id); if(result != PluginResult::OK) return result; ++current_page; } if(!result_items.empty()) last_item_id = result_items.back()->url; else last_item_id.clear(); return PluginResult::OK; } }