/*
* 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