/* * Treebird - Lightweight frontend for Pleroma * Copyright (C) 2022 Nekobit * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ #include #include #define PCRE2_CODE_UNIT_WIDTH 8 #include #include "helpers.h" #include "http.h" #include "base_page.h" #include "status.h" #include "easprintf.h" #include "query.h" #include "cookie.h" #include "string_helpers.h" #include "error.h" #include "reply.h" #include "attachments.h" #include "emoji_reaction.h" #include "../config.h" #include "type_string.h" #include "string.h" #include "emoji.h" #include "account.h" // Pages #include "../static/status.ctmpl" #include "../static/notification.ctmpl" #include "../static/in_reply_to.ctmpl" #include "../static/status_interactions_label.ctmpl" #include "../static/status_interactions.ctmpl" #include "../static/status_interaction_profile.ctmpl" #include "../static/interactions_page.ctmpl" #include "../static/likeboost.ctmpl" #include "../static/reactions_btn.ctmpl" #include "../static/interaction_buttons.ctmpl" #include "../static/reply_link.ctmpl" #include "../static/reply_checkbox.ctmpl" #include "../static/menu_item.ctmpl" #include "../static/like_btn.ctmpl" #include "../static/repeat_btn.ctmpl" #include "../static/reply_btn.ctmpl" #include "../static/expand_btn.ctmpl" #include "../static/like_btn_img.ctmpl" #include "../static/repeat_btn_img.ctmpl" #include "../static/reply_btn_img.ctmpl" #include "../static/expand_btn_img.ctmpl" #include "../static/thread_page_btn.ctmpl" #define ACCOUNT_INTERACTIONS_LIMIT 11 #define NUM_STR "%u" struct status_args { mastodont_t* api; struct mstdnt_status* status; struct construct_statuses_args* args; struct session* ssn; }; int try_post_status(struct session* ssn, mastodont_t* api) { if (!(keystr(ssn->post.content))) return 1; struct mstdnt_args m_args; set_mstdnt_args(&m_args, ssn); // Flip m_args to NOT (which is set by set_mstdnt_args) // This is because we want to upload files too, so it's just // a MIME post request m_args.flags ^= MSTDNT_FLAG_NO_URI_SANITIZE; struct mstdnt_storage storage = { 0 }, *att_storage = NULL; char** files = NULL; size_t files_len = 0; struct mstdnt_attachment* attachments = NULL; char** media_ids = NULL; cJSON* json_ids = NULL; size_t json_ids_len = 0; // Upload images if (!ssn->post.file_ids.is_set) try_upload_media(&att_storage, ssn, api, &attachments, &media_ids); else { // Parse json file ids json_ids = cJSON_Parse(keystr(ssn->post.file_ids)); json_ids_len = cJSON_GetArraySize(json_ids); if (json_ids_len) { media_ids = malloc(json_ids_len * sizeof(char*)); // TODO error cJSON* id; int i = 0; cJSON_ArrayForEach(id, json_ids) { media_ids[i] = id->valuestring; ++i; } } } // Cookie copy and read struct mstdnt_status_args args = { .content_type = "text/plain", .expires_in = 0, .in_reply_to_conversation_id = NULL, .in_reply_to_id = keystr(ssn->post.replyid), .language = NULL, .media_ids = media_ids, .media_ids_len = (!ssn->post.file_ids.is_set ? keyfile(ssn->post.files).array_size : (json_ids ? json_ids_len : 0)), .poll = NULL, .preview = 0, .scheduled_at = NULL, .sensitive = 0, .spoiler_text = NULL, .status = keystr(ssn->post.content), .visibility = keystr(ssn->post.visibility), }; // Finally, create (no error checking) mastodont_create_status(api, &m_args, &args, &storage); mastodont_storage_cleanup(&storage); if (att_storage) cleanup_media_storages(ssn, att_storage); if (json_ids) free(media_ids); else cleanup_media_ids(ssn, media_ids); free(attachments); if (json_ids) cJSON_Delete(json_ids); return 0; } int try_react_status(struct session* ssn, mastodont_t* api, char* id, char* emoji) { struct mstdnt_args m_args; set_mstdnt_args(&m_args, ssn); struct mstdnt_storage storage = { 0 }; struct mstdnt_status status = { 0 }; mastodont_status_emoji_react(api, &m_args, id, emoji, &storage, &status); mstdnt_cleanup_status(&status); mastodont_storage_cleanup(&storage); return 0; } void content_status_create(PATH_ARGS) { char* referer = getenv("HTTP_REFERER"); try_post_status(ssn, api); redirect(req, REDIRECT_303, referer); } void content_status_react(PATH_ARGS) { char* referer = getenv("HTTP_REFERER"); try_react_status(ssn, api, data[0], data[1]); redirect(req, REDIRECT_303, referer); } const char* status_visibility_str(enum l10n_locale loc, enum mstdnt_visibility_type vis) { switch (vis) { case MSTDNT_VISIBILITY_UNLISTED: return L10N[loc][L10N_VIS_UNLISTED]; case MSTDNT_VISIBILITY_PRIVATE: return L10N[loc][L10N_VIS_PRIVATE]; case MSTDNT_VISIBILITY_DIRECT: return L10N[loc][L10N_VIS_DIRECT]; case MSTDNT_VISIBILITY_LOCAL: return L10N[loc][L10N_VIS_LOCAL]; case MSTDNT_VISIBILITY_LIST: return L10N[loc][L10N_VIS_LIST]; case MSTDNT_VISIBILITY_PUBLIC: default: return L10N[loc][L10N_VIS_PUBLIC]; } } int try_interact_status(struct session* ssn, mastodont_t* api, char* id) { struct mstdnt_args m_args; set_mstdnt_args(&m_args, ssn); int res = 0; struct mstdnt_storage storage = { 0 }; if (!(keystr(ssn->post.itype) && id)) return 1; // Pretty up the type if (strcmp(keystr(ssn->post.itype), "like") == 0 || strcmp(keystr(ssn->post.itype), "likeboost") == 0) res = mastodont_favourite_status(api, &m_args, id, &storage, NULL); // Not else if because possibly a like-boost if (strcmp(keystr(ssn->post.itype), "repeat") == 0 || strcmp(keystr(ssn->post.itype), "likeboost") == 0) res = mastodont_reblog_status(api, &m_args, id, &storage, NULL); else if (strcmp(keystr(ssn->post.itype), "bookmark") == 0) res = mastodont_bookmark_status(api, &m_args, id, &storage, NULL); else if (strcmp(keystr(ssn->post.itype), "pin") == 0) res = mastodont_pin_status(api, &m_args, id, &storage, NULL); else if (strcmp(keystr(ssn->post.itype), "mute") == 0) res = mastodont_mute_conversation(api, &m_args, id, &storage, NULL); else if (strcmp(keystr(ssn->post.itype), "delete") == 0) res = mastodont_delete_status(api, &m_args, id, &storage, NULL); else if (strcmp(keystr(ssn->post.itype), "unlike") == 0) res = mastodont_unfavourite_status(api, &m_args, id, &storage, NULL); else if (strcmp(keystr(ssn->post.itype), "unrepeat") == 0) res = mastodont_unreblog_status(api, &m_args, id, &storage, NULL); else if (strcmp(keystr(ssn->post.itype), "unbookmark") == 0) res = mastodont_unbookmark_status(api, &m_args, id, &storage, NULL); else if (strcmp(keystr(ssn->post.itype), "unpin") == 0) res = mastodont_unpin_status(api, &m_args, id, &storage, NULL); else if (strcmp(keystr(ssn->post.itype), "unmute") == 0) res = mastodont_unmute_conversation(api, &m_args, id, &storage, NULL); mastodont_storage_cleanup(&storage); return res; } char* construct_status_interactions_label(char* status_id, int is_favourites, char* header, int val, size_t* size) { struct status_interactions_label_template tdata = { .prefix = config_url_prefix, .status_id = status_id, .action = is_favourites ? "favourited_by" : "reblogged_by", .header = header, .value = val, }; return tmpl_gen_status_interactions_label(&tdata, size); } char* construct_interaction_buttons(struct session* ssn, struct mstdnt_status* status, size_t* size, uint8_t flags) { int use_img = ssn->config.interact_img; char* interaction_html; char* repeat_btn; char* like_btn; char* likeboost_html = NULL; char* reply_count = NULL; char* repeat_count = NULL; char* reply_btn; char* favourites_count = NULL; char* emoji_picker_html = NULL; char* reactions_btn_html = NULL; char* time_str; int show_nums = (flags & STATUS_NO_DOPAMEME) != STATUS_NO_DOPAMEME && ssn->config.stat_dope; size_t s; // Emojo picker if ((flags & STATUS_EMOJI_PICKER) == STATUS_EMOJI_PICKER) { emoji_picker_html = construct_emoji_picker(status->id, NULL); } struct reactions_btn_template tdata = { .prefix = config_url_prefix, .status_id = status->id, .emoji_picker = emoji_picker_html }; reactions_btn_html = tmpl_gen_reactions_btn(&tdata, NULL); if (show_nums) { if (status->replies_count) easprintf(&reply_count, NUM_STR, status->replies_count); if (status->reblogs_count) easprintf(&repeat_count, NUM_STR, status->reblogs_count); if (status->favourites_count) easprintf(&favourites_count, NUM_STR, status->favourites_count); } struct likeboost_template lbdata = { .prefix = config_url_prefix, .status_id = status->id, }; likeboost_html = tmpl_gen_likeboost(&lbdata, NULL); time_str = reltime_to_str(status->created_at); // TODO cleanup? if (use_img) { struct repeat_btn_img_template rpbdata = { .prefix = config_url_prefix, .repeat_active = status->reblogged ? "active" : "" }; repeat_btn = tmpl_gen_repeat_btn_img(&rpbdata, NULL); struct like_btn_img_template ldata = { .prefix = config_url_prefix, .favourite_active = status->favourited ? "active" : "" }; like_btn = tmpl_gen_like_btn_img(&ldata, NULL); } else { struct repeat_btn_template rpbdata = { .repeat_active = status->reblogged ? "active" : "" }; repeat_btn = tmpl_gen_repeat_btn(&rpbdata, NULL); struct like_btn_template ldata = { .favourite_active = status->favourited ? "active" : "" }; like_btn = tmpl_gen_like_btn(&ldata, NULL); } // Weather it should be a link or a