FossilOrigin-Name: aede803dd7e9334e756798bf609e43bdeb391418a7e4af79ffe23c005a1d7dcf
This commit is contained in:
nekobit 2022-10-14 13:36:45 +00:00
commit 4733000997
209 changed files with 6313 additions and 6149 deletions

View file

@ -1,4 +1,5 @@
template
ctemplate
filec
emojitoc
**/*.cgi
@ -9,4 +10,6 @@ mastodont-c
config.h
treebird
test/tests
scripts/*.o
scripts/*.o
templates/*.ctt
test/unit/*.bin

View file

@ -2,42 +2,59 @@ CC ?= cc
GIT ?= git
MASTODONT_DIR = mastodont-c/
MASTODONT = $(MASTODONT_DIR)libmastodont.a
CFLAGS += -Wall -I $(MASTODONT_DIR)include/ -Wno-unused-variable -Wno-ignored-qualifiers -I/usr/include/ -I $(MASTODONT_DIR)/libs $(shell pkg-config --cflags libcurl libpcre2-8)
LDFLAGS = -L$(MASTODONT_DIR) -lmastodont $(shell pkg-config --libs libcurl libpcre2-8) -lfcgi -lpthread
CFLAGS += -Wall -I $(MASTODONT_DIR)include/ -Wno-unused-variable -Wno-ignored-qualifiers -I/usr/include/ -I $(MASTODONT_DIR)/libs $(shell pkg-config --cflags libcurl libpcre2-8) `perl -MExtUtils::Embed -e ccopts` -DDEBUGGING_MSTATS
LDFLAGS += -L$(MASTODONT_DIR) -lmastodont $(shell pkg-config --libs libcurl libpcre2-8) -lfcgi -lpthread `perl -MExtUtils::Embed -e ldopts` -DDEBUGGING_MSTATS
SRC = $(wildcard src/*.c)
OBJ = $(patsubst %.c,%.o,$(SRC))
HEADERS = $(wildcard src/*.h) config.h
PAGES_DIR = static
PAGES = $(wildcard $(PAGES_DIR)/*.tmpl)
PAGES_CMP = $(patsubst %.tmpl,%.ctmpl,$(PAGES))
PAGES_C = $(patsubst %.tmpl, %.c,$(PAGES))
PAGES_C_OBJ = $(patsubst %.c,%.o,$(PAGES_C))
TMPL_DIR = templates
TMPLS = $(wildcard $(TMPL_DIR)/*.tt)
TMPLS_C = $(patsubst %.tt,%.ctt,$(TMPLS))
TEST_DIR = test/unit
TESTS = $(wildcard $(TEST_DIR)/t*.c)
UNIT_TESTS = $(patsubst %.c,%.bin,$(TESTS))
DIST = dist/
PREFIX ?= /usr/local
TARGET = treebird
# For tests
OBJ_NO_MAIN = $(filter-out src/main.o,$(OBJ))
MASTODONT_URL = https://fossil.nekobit.net/mastodont-c
all: $(MASTODONT_DIR) dep_build $(TARGET)
apache: all apache_start
# Not parallel friendly
#all: $(MASTODONT_DIR) dep_build $(TARGET)
$(TARGET): filec template $(PAGES_CMP) $(PAGES_C) $(PAGES_C_OBJ) $(OBJ) $(HEADERS)
ifneq ($(strip $(SINGLE_THREADED)),)
CFLAGS += -DSINGLE_THREADED
endif
ifneq ($(strip $(SINGLE_THREADED)),)
CFLAGS += -DDEBUG
endif
all:
$(MAKE) dep_build
$(MAKE) filec
$(MAKE) make_tmpls
$(MAKE) $(TARGET)
install_deps:
cpan Template::Toolkit
$(TARGET): $(HEADERS) $(OBJ)
$(CC) -o $(TARGET) $(OBJ) $(PAGES_C_OBJ) $(LDFLAGS)
template: src/template/main.o
$(CC) $(LDFLAGS) -o template $<
filec: src/file-to-c/main.o
$(CC) -o filec $<
$(CC) $(LDFLAGS) -o filec $<
emojitoc: scripts/emoji-to.o
$(CC) -o emojitoc $< $(LDFLAGS)
./emojitoc meta/emoji.json > src/emoji_codes.h
# Redirect stdout and stderr into separate contents as a hack
# Let bash do the work :)
$(PAGES_DIR)/%.ctmpl: $(PAGES_DIR)/%.tmpl
./template $< $(notdir $*) 2> $(PAGES_DIR)/$(notdir $*).c 1> $@
$(TMPL_DIR)/%.ctt: $(TMPL_DIR)/%.tt
./filec $< data_$(notdir $*)_tt > $@
make_tmpls: $(TMPLS_C)
$(MASTODONT_DIR):
cd ..; fossil clone $(MASTODONT_URL) || true
@ -48,11 +65,9 @@ install: $(TARGET)
install -d $(PREFIX)/share/treebird/
cp -r dist/ $(PREFIX)/share/treebird/
test:
make -C test
apache_start:
./scripts/fcgistarter.sh
test: all $(UNIT_TESTS)
@echo " ... Tests ready"
@./test/test.pl
dep_build:
make -C $(MASTODONT_DIR)
@ -60,10 +75,17 @@ dep_build:
%.o: %.c %.h $(PAGES)
$(CC) $(CFLAGS) -c $< -o $@
# For tests
%.bin: %.c
@$(CC) $(CFLAGS) $< -o $@ $(OBJ_NO_MAIN) $(PAGES_C_OBJ) $(LDFLAGS)
@echo -n " $@"
clean:
rm -f $(OBJ) src/file-to-c/main.o
rm -f $(PAGES_CMP)
rm -f filec
rm -f $(TMPLS_C)
rm -f test/unit/*.bin
rm -f filec ctemplate
rm $(TARGET) || true
make -C $(MASTODONT_DIR) clean
clean_deps:
@ -71,4 +93,4 @@ clean_deps:
clean_all: clean clean_deps
.PHONY: all filec clean update clean clean_deps clean_all test
.PHONY: all filec clean update clean clean_deps clean_all test install_deps

View file

@ -10,7 +10,9 @@ The goal is to create a frontend that's lightweight enough to be viewed without
usable enough to improve the experience with JS.
Treebird uses C with FCGI, mastodont-c (library designed for Treebird, but can be used
for other applications as well), and plain JavaScript for the frontend (100% optional).
for other applications as well), Perl, and **optional** JavaScript for the frontend (100% functional without
javascript, it only helps). Uses [RE:DOM](https://redom.js.org/) (2kb JS library) to assist with DOM
creation and native JS apis. (Already bundled)
## Why?
@ -24,7 +26,7 @@ This led me to one choice, to develop my own frontend.
Treebird respects compatibility with old browsers, and thus uses HTML table layouts, which are
supported even by most modern terminal web browsers. The core browser we aim to at least maintain compatibility
with is Netsurf, but most other browsers like GNU Emacs EWW, elinks, render Treebird wonderfully.
with is Netsurf, but most other browsers like GNU Emacs EWW, elinks, render Treebird just alright.
## Credits

View file

@ -9,8 +9,10 @@
#ifndef CONFIG_H
#define CONFIG_H
#include <mastodont.h>
#if !(defined(FALSE) && defined(TRUE))
#define FALSE 0
#define TRUE 1
#endif
#define UNSET NULL
/*

539
dist/js/main.js vendored
View file

@ -1,145 +1,159 @@
(function(){
Element.prototype.insertAfter = function(element) {
element.parentNode.insertBefore(this, element.nextSibling);
const { el, mount } = redom;
'use strict';
function get_cookie(cookiestr)
{
return document.cookie
.split(';')
.find(row => row.startsWith(cookiestr+'='))
.split('=')[1];
}
// TODO Check if logged in .acct value is the same
function reply_get_mentions(reply, content)
{
const regexpr = /<a target="_parent" class="mention" href="https?:\/\/.*?\/@(.*?)@(.*?)">.*?<\/a>/g;
const arr = [...content.matchAll(regexpr)];
let res = reply ? `@${reply} ` : "";
const matches = content.matchAll(regexpr);
for (let x of matches)
{
res += `@${x[1]}@${x[2]} `;
}
return res;
}
function form_enter_submit(e, that)
{
if ((e.ctrlKey || e.metaKey) && e.keyCode === 13)
that.closest('form').submit();
}
// Submit form entry on enter when in textbox/textarea
document.querySelectorAll("input[type=text], input[type=url], input[type=email], input[type=password], textarea").forEach((i) => {
i.addEventListener("keydown", e => form_enter_submit(e, i));
});
function construct_query(query)
{
let query_string = "";
let keys = Object.keys(query);
let vals = Object.values(query);
const len = keys.length;
for (let i = 0; i < keys.length; ++i)
{
query_string += keys[i] + "=" + vals[i];
if (i !== keys.length-1)
query_string += "&";
}
return query_string;
}
function send_request(url, query, type, cb, cb_args)
{
let query_str = construct_query(query);
let xhr = new XMLHttpRequest();
xhr.open(type, url);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.onreadystatechange = function() {
if (this.readyState === XMLHttpRequest.DONE)
cb(this, cb_args);
};
xhr.send(query_str);
return xhr;
}
function get_cookie(cookiestr)
{
return document.cookie
.split(';')
.find(row => row.startsWith(cookiestr+'='))
.split('=')[1];
}
function upload_file(url, file_name, file, cb, cb_args, onprogress, onload)
{
let xhr = new XMLHttpRequest();
let form_data = new FormData();
xhr.open("post", url);
form_data.append(file_name, file);
// xhr.setRequestHeader("Content-Type", "multipart/form-data");
xhr.onreadystatechange = function() {
if (this.readyState === XMLHttpRequest.DONE)
cb(this, cb_args);
};
xhr.upload.onprogress = onprogress;
xhr.upload.onload = onload;
xhr.send(form_data);
return xhr;
}
// TODO Check if logged in .acct value is the same
function reply_get_mentions(reply, content)
{
const regexpr = /<a target="_parent" class="mention" href="https?:\/\/.*?\/@(.*?)@(.*?)">.*?<\/a>/g;
const arr = [...content.matchAll(regexpr)];
let res = reply ? `@${reply} ` : "";
const matches = content.matchAll(regexpr);
function change_count_text(val, sum)
{
if (val === "")
val = 0
else
val = parseInt(val);
val += sum;
return val > 0 ? val.toString() : "";
}
for (let x of matches)
function interact_action(status, type)
{
let like = status.querySelector(".statbtn .like");
let repeat = status.querySelector(".statbtn .repeat");
let svg;
if (type.value === "like" || type.value === "unlike")
svg = [ like ];
else if (type.value === "repeat" || type.value === "unrepeat")
svg = [ repeat ];
else if (type.value === "likeboost")
svg = [ like, repeat ];
if (svg)
svg.forEach(that => {
let label = that.parentNode;
let counter = label.querySelector(".count");
let is_interacted = label.classList.contains("interacted");
if (counter)
{
res += `@${x[1]}@${x[2]} `;
counter.innerHTML = change_count_text(counter.innerHTML, is_interacted ? -1 : 1);
}
else {
// Nobody interacted with this yet, create counter
const counter = el("span.count", 1)
mount(label, counter);
is_interacted = false;
}
return res;
}
function form_enter_submit(e, that)
{
if ((e.ctrlKey || e.metaKey) && e.keyCode === 13)
that.closest('form').submit();
}
// Submit form entry on enter when in textbox/textarea
document.querySelectorAll("input[type=text], input[type=url], input[type=email], input[type=password], textarea").forEach((i) => {
i.addEventListener("keydown", e => form_enter_submit(e, i));
if (is_interacted)
{
// Animation
that.classList.remove("interacted-anim");
label.classList.remove("interacted");
// Flip itype value
type.value = type.value.replace("un", "");
}
else {
that.classList.add("interacted-anim");
label.classList.add("interacted");
type.value = "un" + type.value;
}
});
function construct_query(query)
}
function status_event(e)
{
let target = e.target.closest(".statbtn");
if (target)
{
query_string = "";
let keys = Object.keys(query);
let vals = Object.values(query);
const len = keys.length;
for (let i = 0; i < keys.length; ++i)
{
query_string += keys[i] + "=" + vals[i];
if (i !== keys.length-1)
query_string += "&";
}
return query_string;
}
function send_request(url, query, type, cb, cb_args)
{
let query_str = construct_query(query);
let xhr = new XMLHttpRequest();
xhr.open(type, url);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.onreadystatechange = function() {
if (this.readyState === XMLHttpRequest.DONE)
cb(this, cb_args);
};
xhr.send(query_str);
return xhr;
}
function upload_file(url, file_name, file, cb, cb_args, onprogress, onload)
{
let xhr = new XMLHttpRequest();
let form_data = new FormData();
xhr.open("post", url);
form_data.append(file_name, file);
// xhr.setRequestHeader("Content-Type", "multipart/form-data");
xhr.onreadystatechange = function() {
if (this.readyState === XMLHttpRequest.DONE)
cb(this, cb_args);
};
xhr.upload.onprogress = onprogress;
xhr.upload.onload = onload;
xhr.send(form_data);
return xhr;
}
function change_count_text(val, sum)
{
if (val === "")
val = 0
else
val = parseInt(val);
val += sum;
return val > 0 ? val.toString() : "";
}
function interact_action(status, type)
{
let like = status.querySelector(".statbtn .like");
let repeat = status.querySelector(".statbtn .repeat");
let svg;
if (type.value === "like" || type.value === "unlike")
svg = [ like ];
else if (type.value === "repeat" || type.value === "unrepeat")
svg = [ repeat ];
else if (type.value === "likeboost")
svg = [ like, repeat ];
svg.forEach(that => {
let label = that.parentNode;
let counter = label.querySelector(".count");
let is_active = that.classList.contains("active");
that.classList.toggle("active");
if (is_active)
{
// Animation
that.classList.remove("active-anim");
// Flip itype value
type.value = type.value.replace("un", "");
}
else {
that.classList.add("active-anim");
type.value = "un" + type.value;
}
counter.innerHTML = change_count_text(counter.innerHTML, is_active ? -1 : 1);
});
}
function status_interact_props(e)
{
let interact = e.target.closest(".statbtn");
let type = interact.parentNode.querySelector(".itype");
if (type === null)
console.log('huh');
// Don't JS these
if (target.classList.contains("reply-btn") ||
target.classList.contains("view-btn") ||
target.classList.contains("statbtn-last"))
return true;
let status = interact.closest(".status");
let type = target.parentNode.querySelector(".itype");
let status = e.target.closest(".status");
send_request("/treebird_api/v1/interact",
{
@ -160,146 +174,145 @@
e.preventDefault();
return false;
}
}
function frame_resize()
function frame_resize()
{
let rightbar_frame = document.querySelector("#rightbar .sidebar-frame");
let rbar_frame_win = rightbar_frame.contentWindow;
rightbar_frame.height = rbar_frame_win.document.body.scrollHeight;
}
function filesize_to_str(bs)
{
const val = bs === 0 ? 0 : Math.floor(Math.log(bs) / Math.log(1024));
return (bs / 1024**val).toFixed(1) * 1 + ['B', 'kB', 'MB', 'GB', 'TB'][val];
}
function html_encode(str)
{
let en = document.createElement("span");
en.textContent = str;
return en.innerHTML;
}
function construct_file_upload(file, file_content)
{
let container = document.createElement("div");
let content = document.createElement("img");
let info = document.createElement("span");
container.className = "file-upload";
info.className = "upload-info";
info.innerHTML = `<span class="filesize">${filesize_to_str(file.size)}</span> &bull; <span class="filename">${html_encode(file.name)}</span>`;
let progress_div = document.createElement("div");
progress_div.className = "file-progress";
info.appendChild(progress_div);
content.src = file_content;
content.className = "upload-content";
container.appendChild(content);
container.appendChild(info);
return container;
}
function update_uploads_json(dom)
{
let root = dom.parentNode;
let items = root.getElementsByClassName("file-upload");
let ids = [];
for (let i of items)
{
let rightbar_frame = document.querySelector("#rightbar .sidebar-frame");
let rbar_frame_win = rightbar_frame.contentWindow;
rightbar_frame.height = rbar_frame_win.document.body.scrollHeight;
if (i.dataset.id)
ids.push(i.dataset.id);
}
function filesize_to_str(bs)
// Goto statusbox
root = root.parentNode;
let file_ids = root.querySelector(".file-ids-json");
if (!file_ids)
{
const val = bs === 0 ? 0 : Math.floor(Math.log(bs) / Math.log(1024));
return (bs / 1024**val).toFixed(1) * 1 + ['B', 'kB', 'MB', 'GB', 'TB'][val];
// Create if doesn't exist
file_ids = document.createElement("input");
file_ids.type = "hidden";
file_ids.className = "file-ids-json";
file_ids.name = "fileids";
root.appendChild(file_ids);
}
function html_encode(str)
file_ids.value = JSON.stringify(ids);
}
function evt_file_upload(e)
{
let target = e.target;
let file_upload_dom = this.closest("form").querySelector(".file-uploads-container");
file_upload_dom.className = "file-uploads-container";
const files = [...this.files];
let reader;
// Clear file input
this.value = '';
// Create file upload
for (let file of files)
{
let en = document.createElement("span");
en.textContent = str;
return en.innerHTML;
reader = new FileReader();
reader.onload = (() => {
return (e) => {
let file_dom = construct_file_upload(file, e.target.result);
file_upload_dom.appendChild(file_dom);
let xhr = upload_file("/treebird_api/v1/attachment",
"file",
file,
(xhr, args) => {
// TODO errors
file_dom.dataset.id = JSON.parse(xhr.response).id;
update_uploads_json(file_dom);
}, null,
(e) => {
let upload_file_progress = file_dom
.querySelector(".file-progress");
// Add offset of 3
upload_file_progress.style.width = 3+((e.loaded/e.total)*97);
},
(e) => {
file_dom.querySelector(".upload-content").style.opacity = "1.0";
file_dom.querySelector(".file-progress").remove();
});
}
})(file);
reader.readAsDataURL(file);
}
}
// Main (when loaded)
document.addEventListener('DOMContentLoaded', () => {
let interact_btn = document.getElementsByClassName("status");
// Add event listener to add specificied buttons
for (let i = 0; i < interact_btn.length; ++i)
{
interact_btn[i].addEventListener('click', status_event);
}
// Resize notifications iFrame to full height
let rightbar_frame = document.querySelector("#rightbar .sidebar-frame");
if (rightbar_frame)
{
rightbar_frame.contentWindow.addEventListener('DOMContentLoaded', frame_resize);
}
function construct_file_upload(file, file_content)
// File upload
let file_inputs = document.querySelectorAll(".statusbox input[type=file]");
for (let file_input of file_inputs)
{
let container = document.createElement("div");
let content = document.createElement("img");
let info = document.createElement("span");
container.className = "file-upload";
info.className = "upload-info";
info.innerHTML = `<span class="filesize">${filesize_to_str(file.size)}</span> &bull; <span class="filename">${html_encode(file.name)}</span>`;
let progress_div = document.createElement("div");
progress_div.className = "file-progress";
info.appendChild(progress_div);
content.src = file_content;
content.className = "upload-content";
container.appendChild(content);
container.appendChild(info);
return container;
file_input.addEventListener('change', evt_file_upload);
}
function update_uploads_json(dom)
{
let root = dom.parentNode;
let items = root.getElementsByClassName("file-upload");
let ids = [];
for (let i of items)
{
if (i.dataset.id)
ids.push(i.dataset.id);
}
// Goto statusbox
root = root.parentNode;
let file_ids = root.querySelector(".file-ids-json");
if (!file_ids)
{
// Create if doesn't exist
file_ids = document.createElement("input");
file_ids.type = "hidden";
file_ids.className = "file-ids-json";
file_ids.name = "fileids";
root.appendChild(file_ids);
}
file_ids.value = JSON.stringify(ids);
}
function evt_file_upload(e)
{
let target = e.target;
let file_upload_dom = this.closest("form").querySelector(".file-uploads-container");
file_upload_dom.className = "file-uploads-container";
const files = [...this.files];
let reader;
// Clear file input
this.value = '';
// Create file upload
for (let file of files)
{
reader = new FileReader();
reader.onload = (() => {
return (e) => {
let file_dom = construct_file_upload(file, e.target.result);
file_upload_dom.appendChild(file_dom);
let xhr = upload_file("/treebird_api/v1/attachment",
"file",
file,
(xhr, args) => {
// TODO errors
file_dom.dataset.id = JSON.parse(xhr.response).id;
update_uploads_json(file_dom);
}, null,
(e) => {
let upload_file_progress = file_dom
.querySelector(".file-progress");
// Add offset of 3
upload_file_progress.style.width = 3+((e.loaded/e.total)*97);
},
(e) => {
file_dom.querySelector(".upload-content").style.opacity = "1.0";
file_dom.querySelector(".file-progress").remove();
});
}
})(file);
reader.readAsDataURL(file);
}
}
// Main (when loaded)
document.addEventListener('DOMContentLoaded', () => {
let reply_btn = document.getElementsByClassName("reply-btn");
let interact_btn = document.getElementsByClassName("statbtn");
// Add event listener to add specificied buttons
for (let i = 0; i < interact_btn.length; ++i)
{
interact_btn[i].addEventListener('click', status_interact_props);
}
// Resize notifications iFrame to full height
let rightbar_frame = document.querySelector("#rightbar .sidebar-frame");
if (rightbar_frame)
{
rightbar_frame.contentWindow.addEventListener('DOMContentLoaded', frame_resize);
}
// File upload
let file_inputs = document.querySelectorAll(".statusbox input[type=file]");
for (let file_input of file_inputs)
{
file_input.addEventListener('change', evt_file_upload);
}
});
})();
});

755
dist/js/redom.js vendored Normal file
View file

@ -0,0 +1,755 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.redom = {}));
})(this, (function (exports) { 'use strict';
function createElement (query, ns) {
var ref = parse(query);
var tag = ref.tag;
var id = ref.id;
var className = ref.className;
var element = ns ? document.createElementNS(ns, tag) : document.createElement(tag);
if (id) {
element.id = id;
}
if (className) {
if (ns) {
element.setAttribute('class', className);
} else {
element.className = className;
}
}
return element;
}
function parse (query) {
var chunks = query.split(/([.#])/);
var className = '';
var id = '';
for (var i = 1; i < chunks.length; i += 2) {
switch (chunks[i]) {
case '.':
className += " " + (chunks[i + 1]);
break;
case '#':
id = chunks[i + 1];
}
}
return {
className: className.trim(),
tag: chunks[0] || 'div',
id: id
};
}
function unmount (parent, child) {
var parentEl = getEl(parent);
var childEl = getEl(child);
if (child === childEl && childEl.__redom_view) {
// try to look up the view if not provided
child = childEl.__redom_view;
}
if (childEl.parentNode) {
doUnmount(child, childEl, parentEl);
parentEl.removeChild(childEl);
}
return child;
}
function doUnmount (child, childEl, parentEl) {
var hooks = childEl.__redom_lifecycle;
if (hooksAreEmpty(hooks)) {
childEl.__redom_lifecycle = {};
return;
}
var traverse = parentEl;
if (childEl.__redom_mounted) {
trigger(childEl, 'onunmount');
}
while (traverse) {
var parentHooks = traverse.__redom_lifecycle || {};
for (var hook in hooks) {
if (parentHooks[hook]) {
parentHooks[hook] -= hooks[hook];
}
}
if (hooksAreEmpty(parentHooks)) {
traverse.__redom_lifecycle = null;
}
traverse = traverse.parentNode;
}
}
function hooksAreEmpty (hooks) {
if (hooks == null) {
return true;
}
for (var key in hooks) {
if (hooks[key]) {
return false;
}
}
return true;
}
/* global Node, ShadowRoot */
var hookNames = ['onmount', 'onremount', 'onunmount'];
var shadowRootAvailable = typeof window !== 'undefined' && 'ShadowRoot' in window;
function mount (parent, child, before, replace) {
var parentEl = getEl(parent);
var childEl = getEl(child);
if (child === childEl && childEl.__redom_view) {
// try to look up the view if not provided
child = childEl.__redom_view;
}
if (child !== childEl) {
childEl.__redom_view = child;
}
var wasMounted = childEl.__redom_mounted;
var oldParent = childEl.parentNode;
if (wasMounted && (oldParent !== parentEl)) {
doUnmount(child, childEl, oldParent);
}
if (before != null) {
if (replace) {
var beforeEl = getEl(before);
if (beforeEl.__redom_mounted) {
trigger(beforeEl, 'onunmount');
}
parentEl.replaceChild(childEl, beforeEl);
} else {
parentEl.insertBefore(childEl, getEl(before));
}
} else {
parentEl.appendChild(childEl);
}
doMount(child, childEl, parentEl, oldParent);
return child;
}
function trigger (el, eventName) {
if (eventName === 'onmount' || eventName === 'onremount') {
el.__redom_mounted = true;
} else if (eventName === 'onunmount') {
el.__redom_mounted = false;
}
var hooks = el.__redom_lifecycle;
if (!hooks) {
return;
}
var view = el.__redom_view;
var hookCount = 0;
view && view[eventName] && view[eventName]();
for (var hook in hooks) {
if (hook) {
hookCount++;
}
}
if (hookCount) {
var traverse = el.firstChild;
while (traverse) {
var next = traverse.nextSibling;
trigger(traverse, eventName);
traverse = next;
}
}
}
function doMount (child, childEl, parentEl, oldParent) {
var hooks = childEl.__redom_lifecycle || (childEl.__redom_lifecycle = {});
var remount = (parentEl === oldParent);
var hooksFound = false;
for (var i = 0, list = hookNames; i < list.length; i += 1) {
var hookName = list[i];
if (!remount) { // if already mounted, skip this phase
if (child !== childEl) { // only Views can have lifecycle events
if (hookName in child) {
hooks[hookName] = (hooks[hookName] || 0) + 1;
}
}
}
if (hooks[hookName]) {
hooksFound = true;
}
}
if (!hooksFound) {
childEl.__redom_lifecycle = {};
return;
}
var traverse = parentEl;
var triggered = false;
if (remount || (traverse && traverse.__redom_mounted)) {
trigger(childEl, remount ? 'onremount' : 'onmount');
triggered = true;
}
while (traverse) {
var parent = traverse.parentNode;
var parentHooks = traverse.__redom_lifecycle || (traverse.__redom_lifecycle = {});
for (var hook in hooks) {
parentHooks[hook] = (parentHooks[hook] || 0) + hooks[hook];
}
if (triggered) {
break;
} else {
if (traverse.nodeType === Node.DOCUMENT_NODE ||
(shadowRootAvailable && (traverse instanceof ShadowRoot)) ||
(parent && parent.__redom_mounted)
) {
trigger(traverse, remount ? 'onremount' : 'onmount');
triggered = true;
}
traverse = parent;
}
}
}
function setStyle (view, arg1, arg2) {
var el = getEl(view);
if (typeof arg1 === 'object') {
for (var key in arg1) {
setStyleValue(el, key, arg1[key]);
}
} else {
setStyleValue(el, arg1, arg2);
}
}
function setStyleValue (el, key, value) {
el.style[key] = value == null ? '' : value;
}
/* global SVGElement */
var xlinkns = 'http://www.w3.org/1999/xlink';
function setAttr (view, arg1, arg2) {
setAttrInternal(view, arg1, arg2);
}
function setAttrInternal (view, arg1, arg2, initial) {
var el = getEl(view);
var isObj = typeof arg1 === 'object';
if (isObj) {
for (var key in arg1) {
setAttrInternal(el, key, arg1[key], initial);
}
} else {
var isSVG = el instanceof SVGElement;
var isFunc = typeof arg2 === 'function';
if (arg1 === 'style' && typeof arg2 === 'object') {
setStyle(el, arg2);
} else if (isSVG && isFunc) {
el[arg1] = arg2;
} else if (arg1 === 'dataset') {
setData(el, arg2);
} else if (!isSVG && (arg1 in el || isFunc) && (arg1 !== 'list')) {
el[arg1] = arg2;
} else {
if (isSVG && (arg1 === 'xlink')) {
setXlink(el, arg2);
return;
}
if (initial && arg1 === 'class') {
arg2 = el.className + ' ' + arg2;
}
if (arg2 == null) {
el.removeAttribute(arg1);
} else {
el.setAttribute(arg1, arg2);
}
}
}
}
function setXlink (el, arg1, arg2) {
if (typeof arg1 === 'object') {
for (var key in arg1) {
setXlink(el, key, arg1[key]);
}
} else {
if (arg2 != null) {
el.setAttributeNS(xlinkns, arg1, arg2);
} else {
el.removeAttributeNS(xlinkns, arg1, arg2);
}
}
}
function setData (el, arg1, arg2) {
if (typeof arg1 === 'object') {
for (var key in arg1) {
setData(el, key, arg1[key]);
}
} else {
if (arg2 != null) {
el.dataset[arg1] = arg2;
} else {
delete el.dataset[arg1];
}
}
}
function text (str) {
return document.createTextNode((str != null) ? str : '');
}
function parseArgumentsInternal (element, args, initial) {
for (var i = 0, list = args; i < list.length; i += 1) {
var arg = list[i];
if (arg !== 0 && !arg) {
continue;
}
var type = typeof arg;
if (type === 'function') {
arg(element);
} else if (type === 'string' || type === 'number') {
element.appendChild(text(arg));
} else if (isNode(getEl(arg))) {
mount(element, arg);
} else if (arg.length) {
parseArgumentsInternal(element, arg, initial);
} else if (type === 'object') {
setAttrInternal(element, arg, null, initial);
}
}
}
function ensureEl (parent) {
return typeof parent === 'string' ? html(parent) : getEl(parent);
}
function getEl (parent) {
return (parent.nodeType && parent) || (!parent.el && parent) || getEl(parent.el);
}
function isNode (arg) {
return arg && arg.nodeType;
}
function html (query) {
var args = [], len = arguments.length - 1;
while ( len-- > 0 ) args[ len ] = arguments[ len + 1 ];
var element;
var type = typeof query;
if (type === 'string') {
element = createElement(query);
} else if (type === 'function') {
var Query = query;
element = new (Function.prototype.bind.apply( Query, [ null ].concat( args) ));
} else {
throw new Error('At least one argument required');
}
parseArgumentsInternal(getEl(element), args, true);
return element;
}
var el = html;
var h = html;
html.extend = function extendHtml () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
return html.bind.apply(html, [ this ].concat( args ));
};
function setChildren (parent) {
var children = [], len = arguments.length - 1;
while ( len-- > 0 ) children[ len ] = arguments[ len + 1 ];
var parentEl = getEl(parent);
var current = traverse(parent, children, parentEl.firstChild);
while (current) {
var next = current.nextSibling;
unmount(parent, current);
current = next;
}
}
function traverse (parent, children, _current) {
var current = _current;
var childEls = Array(children.length);
for (var i = 0; i < children.length; i++) {
childEls[i] = children[i] && getEl(children[i]);
}
for (var i$1 = 0; i$1 < children.length; i$1++) {
var child = children[i$1];
if (!child) {
continue;
}
var childEl = childEls[i$1];
if (childEl === current) {
current = current.nextSibling;
continue;
}
if (isNode(childEl)) {
var next = current && current.nextSibling;
var exists = child.__redom_index != null;
var replace = exists && next === childEls[i$1 + 1];
mount(parent, child, current, replace);
if (replace) {
current = next;
}
continue;
}
if (child.length != null) {
current = traverse(parent, child, current);
}
}
return current;
}
function listPool (View, key, initData) {
return new ListPool(View, key, initData);
}
var ListPool = function ListPool (View, key, initData) {
this.View = View;
this.initData = initData;
this.oldLookup = {};
this.lookup = {};
this.oldViews = [];
this.views = [];
if (key != null) {
this.key = typeof key === 'function' ? key : propKey(key);
}
};
ListPool.prototype.update = function update (data, context) {
var ref = this;
var View = ref.View;
var key = ref.key;
var initData = ref.initData;
var keySet = key != null;
var oldLookup = this.lookup;
var newLookup = {};
var newViews = Array(data.length);
var oldViews = this.views;
for (var i = 0; i < data.length; i++) {
var item = data[i];
var view = (void 0);
if (keySet) {
var id = key(item);
view = oldLookup[id] || new View(initData, item, i, data);
newLookup[id] = view;
view.__redom_id = id;
} else {
view = oldViews[i] || new View(initData, item, i, data);
}
view.update && view.update(item, i, data, context);
var el = getEl(view.el);
el.__redom_view = view;
newViews[i] = view;
}
this.oldViews = oldViews;
this.views = newViews;
this.oldLookup = oldLookup;
this.lookup = newLookup;
};
function propKey (key) {
return function (item) {
return item[key];
};
}
function list (parent, View, key, initData) {
return new List(parent, View, key, initData);
}
var List = function List (parent, View, key, initData) {
this.View = View;
this.initData = initData;
this.views = [];
this.pool = new ListPool(View, key, initData);
this.el = ensureEl(parent);
this.keySet = key != null;
};
List.prototype.update = function update (data, context) {
if ( data === void 0 ) data = [];
var ref = this;
var keySet = ref.keySet;
var oldViews = this.views;
this.pool.update(data, context);
var ref$1 = this.pool;
var views = ref$1.views;
var lookup = ref$1.lookup;
if (keySet) {
for (var i = 0; i < oldViews.length; i++) {
var oldView = oldViews[i];
var id = oldView.__redom_id;
if (lookup[id] == null) {
oldView.__redom_index = null;
unmount(this, oldView);
}
}
}
for (var i$1 = 0; i$1 < views.length; i$1++) {
var view = views[i$1];
view.__redom_index = i$1;
}
setChildren(this, views);
if (keySet) {
this.lookup = lookup;
}
this.views = views;
};
List.extend = function extendList (parent, View, key, initData) {
return List.bind(List, parent, View, key, initData);
};
list.extend = List.extend;
/* global Node */
function place (View, initData) {
return new Place(View, initData);
}
var Place = function Place (View, initData) {
this.el = text('');
this.visible = false;
this.view = null;
this._placeholder = this.el;
if (View instanceof Node) {
this._el = View;
} else if (View.el instanceof Node) {
this._el = View;
this.view = View;
} else {
this._View = View;
}
this._initData = initData;
};
Place.prototype.update = function update (visible, data) {
var placeholder = this._placeholder;
var parentNode = this.el.parentNode;
if (visible) {
if (!this.visible) {
if (this._el) {
mount(parentNode, this._el, placeholder);
unmount(parentNode, placeholder);
this.el = getEl(this._el);
this.visible = visible;
} else {
var View = this._View;
var view = new View(this._initData);
this.el = getEl(view);
this.view = view;
mount(parentNode, view, placeholder);
unmount(parentNode, placeholder);
}
}
this.view && this.view.update && this.view.update(data);
} else {
if (this.visible) {
if (this._el) {
mount(parentNode, placeholder, this._el);
unmount(parentNode, this._el);
this.el = placeholder;
this.visible = visible;
return;
}
mount(parentNode, placeholder, this.view);
unmount(parentNode, this.view);
this.el = placeholder;
this.view = null;
}
}
this.visible = visible;
};
/* global Node */
function router (parent, Views, initData) {
return new Router(parent, Views, initData);
}
var Router = function Router (parent, Views, initData) {
this.el = ensureEl(parent);
this.Views = Views;
this.initData = initData;
};
Router.prototype.update = function update (route, data) {
if (route !== this.route) {
var Views = this.Views;
var View = Views[route];
this.route = route;
if (View && (View instanceof Node || View.el instanceof Node)) {
this.view = View;
} else {
this.view = View && new View(this.initData, data);
}
setChildren(this.el, [this.view]);
}
this.view && this.view.update && this.view.update(data, route);
};
var ns = 'http://www.w3.org/2000/svg';
function svg (query) {
var args = [], len = arguments.length - 1;
while ( len-- > 0 ) args[ len ] = arguments[ len + 1 ];
var element;
var type = typeof query;
if (type === 'string') {
element = createElement(query, ns);
} else if (type === 'function') {
var Query = query;
element = new (Function.prototype.bind.apply( Query, [ null ].concat( args) ));
} else {
throw new Error('At least one argument required');
}
parseArgumentsInternal(getEl(element), args, true);
return element;
}
var s = svg;
svg.extend = function extendSvg () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
return svg.bind.apply(svg, [ this ].concat( args ));
};
svg.ns = ns;
exports.List = List;
exports.ListPool = ListPool;
exports.Place = Place;
exports.Router = Router;
exports.el = el;
exports.h = h;
exports.html = html;
exports.list = list;
exports.listPool = listPool;
exports.mount = mount;
exports.place = place;
exports.router = router;
exports.s = s;
exports.setAttr = setAttr;
exports.setChildren = setChildren;
exports.setData = setData;
exports.setStyle = setStyle;
exports.setXlink = setXlink;
exports.svg = svg;
exports.text = text;
exports.unmount = unmount;
Object.defineProperty(exports, '__esModule', { value: true });
}));

1
dist/js/redom.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/svg/searchmenu.svg vendored Normal file
View file

@ -0,0 +1 @@
<svg width="20" height="20" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><circle cx="10.107" cy="8.7642" r="6.7391" stroke-width="1.6848"/><line x1="18.531" x2="14.866" y1="17.188" y2="13.524" stroke-width="1.6848"/><path d="m14.756 20.738 1.9607 1.9607 1.9091-1.9297" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.6554"/></svg>

After

Width:  |  Height:  |  Size: 446 B

759
dist/treebird.css vendored

File diff suppressed because it is too large Load diff

11
docs/DEVELOP.md Normal file
View file

@ -0,0 +1,11 @@
# Developing Treebird
Treebird development is a bit hacky. There are better ways to work with development
### Compiler flags
You can compile Treebird with some helpful flags, such as single_threaded to improve debugging for Treebird.
```
make SINGLE_THREADED=1 all
```

View file

@ -8,22 +8,45 @@ For the following GNU/Linux distributions, you will need the following libraries
###### Debian
`# apt install libcurl4-gnutls-dev libpcre2-dev libfcgi-dev base-devel`
`# apt install libcurl4-gnutls-dev libpcre2-dev libfcgi-dev build-essential perl libperl-dev libtemplate-perl`
###### Void GNU/Linux
`# xbps-install libcurl libcurl-devel base-devel pcre2 pcre2-devel fcgi fcgi-devel`
`# xbps-install libcurl libcurl-devel base-devel pcre2 pcre2-devel fcgi fcgi-devel perl-Template-Toolkit`
###### Arch
`# pacman -S curl base-devel`
`# pacman -S curl base-devel perl perl-template-toolkit`
###### Gentoo
TODO
Create a copy of `config.def.h` at `config.h`, edit the file with your information
Run `make`. This will also clone mastodont-c, and compile both it and Treebird.
Run `make`. (**hint:** Pass -j3 to speed up compilation). This will also clone mastodont-c, and compile both it and Treebird.
If you `fossil update` any changes, `make update` should be run after updating
## Perl dependencies manual install
**Note:** You **WONT** need to do this if your distribution above included all the deps (Template Toolkit)
At the moment, all of them listed above do, but if your distro is nonstandard, keep reading:
---
Treebird renders most of the content that you see in Perl using the Template Toolkit.
You can install it by running `make install_deps`
If that doesn't work, you can open a CPAN shell
```
perl -MCPAN -e shell
install Template::Toolkit
```
## Installation
Run `# make install`
@ -33,15 +56,14 @@ If this succeeds (assuming you used default variables), you can now find Treebir
- `/usr/local/share/treebird/` - Contains CSS, images, and other meta files
- `/usr/local/bin/treebird` - Regular executable CGI file, test it by running it as is, it shouldn't spit anything out
### Using NGINX
## Development
For developing Treebird, see `DEVELOP.md`.
## Nginx
Treebird can be served over nginx by using a FastCGI daemon such as spawn-fcgi.
The example static files will be in `/usr/local/share/treebird/`, with `treebird.cgi` at `/usr/local/bin/treebird`.
After running `make`, Treebird's files will be in the `dist/` directory. _Copy_, ***DO NOT MOVE***, **everything but treebird.cgi** of this folder to your web server. Copy `treebird.cgi` to another directory of your choosing.
## Nginx
An example Nginx configuration is available in [treebird.nginx.conf](./sample/treebird.nginx.conf).
* Make sure to change `example.com` to your instance's domain.
* Make sure to change the `root` to wherever the static files are being stored
@ -51,7 +73,7 @@ An example Nginx configuration is available in [treebird.nginx.conf](./sample/tr
Apache hasn't caused many troubles, and is in fact, what I use for development. You can see how to start
spawn-fcgi in `scripts/fcgistarter.sh`.
Example Apache configuration is available in [treebird.apache.conf](./sample/treebird.apache.conf).
An example Apache configuration is available in [treebird.apache.conf](./sample/treebird.apache.conf).
#### spawn-fcgi

118
perl/account.pm Normal file
View file

@ -0,0 +1,118 @@
package account;
use strict;
use warnings;
use Exporter 'import';
our @EXPORT = qw( account content_statuses generate_account_list generate_account_item status_interactions );
use template_helpers 'to_template';
use l10n 'lang';
use status 'generate_status';
use string_helpers qw( simple_escape emojify random_error_kaomoji format_username );
use navigation 'generate_navigation';
sub generate_account
{
my ($ssn, $data, $acct, $relationship, $content) = @_;
my %vars = (
prefix => '',
ssn => $ssn,
lang => \&lang,
relationship => $relationship,
content => $content,
acct => $acct,
escape => \&simple_escape,
emojify => \&emojify,
);
to_template(\%vars, \$data->{'account.tt'});
}
sub content_statuses
{
my ($ssn, $data, $acct, $relationship, $statuses) = @_;
my %vars = (
prefix => '',
ssn => $ssn,
acct => $acct,
statuses => $statuses,
create_status => sub { generate_status($ssn, $data, shift); },
# Make subroutine so Perl doesn't autovivify
nav => sub { generate_navigation($ssn, $data, $statuses->[0]->{id}, $statuses->[-1]->{id}) },
random_error_kaomoji => \&random_error_kaomoji,
);
generate_account($ssn, $data, $acct, $relationship, to_template(\%vars, \$data->{'account_statuses.tt'}));
}
sub generate_account_item
{
my ($ssn, $data, $account) = @_;
my %vars = (
prefix => '',
ssn => $ssn,
account => $account,
# Functions
icon => \&get_icon,
lang => \&lang,
format_username => \&format_username,
);
to_template(\%vars, \$data->{'account_item.tt'});
}
sub generate_account_list
{
my ($ssn, $data, $accounts, $title) = @_;
my %vars = (
prefix => '',
ssn => $ssn,
accounts => $accounts,
title => $title,
create_account => sub { generate_account_item($ssn, $data, shift); },
nav => sub { generate_navigation($ssn, $data, $accounts->[0]->{id}, $accounts->[-1]->{id}) },
);
to_template(\%vars, \$data->{'accounts.tt'});
}
sub status_interactions
{
my ($ssn, $data, $accounts, $label) = @_;
my %vars = (
prefix => '',
ssn => $ssn,
data => $data,
accounts => $accounts,
label => $label,
# Functions
create_account => sub { generate_account_item($ssn, $data, shift); },
);
to_template(\%vars, \$data->{'status_interactions.tt'});
}
sub content_accounts
{
my ($ssn, $data, $acct, $relationship, $accounts, $title) = @_;
my $acct_list_page = generate_account_list($ssn, $data, $accounts, $title);
# Should we create a full accounts view?
if ($acct)
{
generate_account($ssn, $data, $acct, $relationship, $acct_list_page);
}
else {
return $acct_list_page;
}
}
1;

26
perl/attachments.pm Normal file
View file

@ -0,0 +1,26 @@
package attachments;
use strict;
use warnings;
use Exporter 'import';
our @EXPORT = qw( generate_attachment );
use template_helpers 'to_template';
use icons 'get_icon';
sub generate_attachment
{
my ($ssn, $data, $att, $sensitive) = @_;
my %vars = (
prefix => '',
ssn => $ssn,
attachment => $att,
sensitive => $sensitive,
icon => \&get_icon,
);
to_template(\%vars, \$data->{'attachment.tt'});
}
1;

42
perl/chat.pm Normal file
View file

@ -0,0 +1,42 @@
package chat;
use strict;
use warnings;
use Exporter 'import';
our @EXPORTS = qw( content_chats construct_chat );
use template_helpers 'to_template';
use string_helpers qw( format_username emojify reltime_to_str );
sub construct_chat
{
my ($ssn, $data, $chat, $messages) = @_;
my %vars = (
prefix => '',
ssn => $ssn,
chat => $chat,
messages => $messages,
format_username => \&format_username,
emojify => \&emojify,
reltime => \&reltime_to_str,
);
to_template(\%vars, \$data->{'chat.tt'});
}
sub content_chats
{
my ($ssn, $data, $chats) = @_;
my %vars = (
prefix => '',
ssn => $ssn,
chats => $chats,
format_username => \&format_username,
);
to_template(\%vars, \$data->{'content_chats.tt'});
}
1;

20
perl/config.pm Normal file
View file

@ -0,0 +1,20 @@
package config;
use strict;
use warnings;
our @EXPORT = qw( general appearance );
use Exporter 'import';
use template_helpers 'simple_page';
sub general
{
simple_page @_, 'config_general.tt';
}
sub appearance
{
simple_page @_, 'config_appearance.tt';
}
1;

25
perl/embed.pm Normal file
View file

@ -0,0 +1,25 @@
package embed;
use strict;
use warnings;
use Exporter 'import';
our @EXPORT = qw( generate_embedded_page );
use template_helpers 'to_template';
sub generate_embedded_page
{
my ($ssn, $data, $content) = @_;
my %vars = (
prefix => '',
ssn => $ssn,
content => $content,
);
to_template(\%vars, \$data->{'embed.tt'});
}
1;

35
perl/emojis.pm Normal file
View file

@ -0,0 +1,35 @@
package emojis;
use strict;
use warnings;
use Exporter 'import';
our @EXPORT = qw( generate_emoji );
use template_helpers 'to_template';
sub generate_emoji
{
my ($ssn, $data, $status_id, $emoji) = @_;
my %vars = (
prefix => '',
ssn => $ssn,
status_id => $status_id,
emoji => $emoji
);
to_template(\%vars, \$data->{'emoji.tt'});
}
sub emoji_picker
{
my ($data, $emojis) = @_;
my %vars = (
e => $emojis
);
to_template(\%vars, \$data->{'emoji_picker.tt'});
}
1;

75
perl/icons.pm Normal file
View file

@ -0,0 +1,75 @@
package icons;
use strict;
use warnings;
use Scalar::Util 'looks_like_number';
use Exporter 'import';
our @EXPORT = qw( &get_icon &get_icon_svg &get_icon_png &visibility_to_icon );
sub get_icon
{
my ($ico, $is_png) = @_;
$is_png ||= 0;
$is_png ? get_icon_png($ico) : get_icon_svg($ico);
}
sub get_icon_svg
{
my %res = (
repeat => '<svg class="repeat" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 2.1l4 4-4 4"/><path d="M3 12.2v-2a4 4 0 0 1 4-4h12.8M7 21.9l-4-4 4-4"/><path d="M21 11.8v2a4 4 0 0 1-4 4H4.2"/></svg>',
like => '<svg class="like" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>',
expand => '<svg class="expand" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6M14 10l6.1-6.1M9 21H3v-6M10 14l-6.1 6.1"/></svg>',
reply => '<svg class="reply" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 9l6 6-6 6"/><path d="M4 4v7a4 4 0 0 0 4 4h11"/></svg>',
emoji => '<svg class="emoji-btn" width="20" height="20" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="10"/><g><line x1="9" x2="9" y1="6.9367" y2="11.755" stroke-width="1.7916"/><line x1="15" x2="15" y1="6.9367" y2="11.755" stroke-width="1.7916"/><path d="m7.0891 15.099s4.7206 4.7543 9.7109 0" stroke-linecap="round" stroke-linejoin="miter" stroke-width="1.9764"/></g></svg>',
likeboost => '<svg class="one-click-software" width="20" height="20" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><g stroke-width="1.98"><path d="m19.15 8.5061 2.7598 2.7598-2.7598 2.7598"/><path d="m14.756 11.325s2.5484-0.05032 6.3258 0.01026m-15.639 10.807-2.7598-2.7598 2.7598-2.7598"/><path d="m22.4 15.327v1.2259c0 1.156-1.2356 2.7598-2.7598 2.7598h-16.664"/></g><polygon transform="matrix(.60736 0 0 .60736 .60106 .63577)" points="18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2 15.09 8.26 22 9.27 17 14.14" stroke-width="2.9656"/></g></svg>',
fileclip => '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path></svg>',
'local' => '<svg class="visibility vis-local" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 9v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V9"/><path d="M9 22V12h6v10M2 10.6L12 2l10 8.6"/></svg>',
direct => '<svg class="visibility vis-direct xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>',
private => '<svg class="visibility vis-private" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>',
list => '<svg class="visibility vis-list" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>',
unlisted => '<svg class="visibility vis-unlisted" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 9.9-1"></path></svg>',
public => '<svg class="visibility vis-public" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>',
follow => '<svg class="follow" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg>',
search => '<svg class="search" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>',
'search-menu' => '<svg class="search-menu" width="20" height="20" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><circle cx="10.107" cy="8.7642" r="6.7391" stroke-width="1.6848"/><line x1="18.531" x2="14.866" y1="17.188" y2="13.524" stroke-width="1.6848"/><path d="m14.756 20.738 1.9607 1.9607 1.9091-1.9297" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.6554"/></svg>',
);
$res{$_[0]};
}
sub visibility_to_icon
{
# I thought of an array, but I don't want to call get_icon UNLESS
# we know the visibility
return unless looks_like_number($_[0]);
my $vis = $_[0];
return get_icon('public') if $vis == 1;
return get_icon('unlisted') if $vis == 2;
return get_icon('private') if $vis == 3;
return get_icon('list') if $vis == 4;
return get_icon('direct') if $vis == 5;
return get_icon('local') if $vis == 6;
# Assume local for anything else, because well... I'm not sure
get_icon('local');
}
1;

96
perl/l10n.pm Normal file
View file

@ -0,0 +1,96 @@
package l10n;
use Exporter 'import';
our @EXPORT = qw( &lang %L10N );
our %L10N = (
EN_US => {
APP_NAME => 'Treebird',
HOME => 'Home',
LOCAL => 'Local',
FEDERATED => 'Federated',
NOTIFICATIONS => 'Notifications',
LISTS => 'Lists',
FAVOURITES => 'Favorites',
BOOKMARKS => 'Bookmarks',
DIRECT => 'Direct',
CONFIG => 'Config',
SEARCH_PLACEHOLDER => 'Search',
SEARCH_BUTTON => 'Search',
GENERAL => 'General',
ACCOUNT => 'Account',
JAVASCRIPT => 'JavaScript',
CFG_QUICK_ACTIONS => 'Quick actions - Likes, Boosts, etc done in the background',
CFG_QUICK_REPLY => 'Quick reply - Replies don\'t require redirects',
LIVE_STATUSES => 'Live statuses - Statuses fetch on the fly',
APPEARANCE => 'Appearance',
VARIANT => 'Variant',
THEME_TREEBIRD20 => 'Treebird - Default, simple theme',
THEME_TREEBIRD30 => 'Treebird 3.0 - Flat, modern theme',
COLOR_SCHEME => 'Color Scheme',
LIGHT => 'Light',
DARK => 'Dark',
SAVE => 'Save',
ACCT_MENU => 'Menu',
SUBSCRIBE => 'Subscribe',
UNSUBSCRIBE => 'Unsubscribe',
BLOCK => 'Block',
UNBLOCK => 'Unblock',
MUTE => 'Mute',
UNMUTE => 'Unmute',
TAB_STATUSES => 'Statuses',
TAB_FOLLOWING => 'Following',
TAB_FOLLOWERS => 'Followers',
TAB_SCROBBLES => 'Scrobbles',
TAB_MEDIA => 'Media',
TAB_PINNED => 'Pinned',
FOLLOWS_YOU => 'Follows you!',
FOLLOW => 'Follow',
FOLLOW_PENDING => 'Follow pending',
FOLLOWING => 'Following!',
BLOCKED => 'You are blocked by this user.',
REPLY => 'Reply',
REPEAT => 'Repeat',
LIKE => 'Like',
QUICK => 'Quick',
VIEW => 'View',
IN_REPLY_TO => 'In reply to',
PAGE_NOT_FOUND => 'Content not found',
STATUS_NOT_FOUND => 'Status not found',
ACCOUNT_NOT_FOUND => 'Account not found',
VIS_PUBLIC => 'Public',
VIS_UNLISTED => 'Unlisted',
VIS_PRIVATE => 'Private',
VIS_DIRECT => 'Direct',
VIS_LOCAL => 'Local',
VIS_LIST => 'List',
LOGIN => 'Login',
REGISTER => 'Register',
USERNAME => 'Username',
PASSWORD => 'Password',
LOGIN_BTN => 'Login',
LOGIN_HEADER => 'Login / Register',
LOGIN_FAIL => 'Couldn\'t login',
NOTIF_LIKED => 'liked your status',
NOTIF_REACTED_WITH => 'reacted with',
NOTIF_REPEATED => 'repeated your status',
NOTIF_FOLLOW => 'followed you',
NOTIF_FOLLOW_REQUEST => 'wants to follow you',
NOTIF_POLL => 'poll results',
NOTIF_COMPACT_LIKED => 'liked',
NOTIF_COMPACT_REACTED_WITH => 'reacted',
NOTIF_COMPACT_REPEATED => 'repeated',
NOTIF_COMPACT_FOLLOW => 'followed',
COMPACT_FOLLOW_REQUEST => 'wants to follow',
NOTIF_COMPACT_POLL => 'poll',
},
# TODO bring over Spanish and Chinese
);
sub lang
{
$L10N{'EN_US'}->{shift(@_)}
}
return 1;

19
perl/lists.pm Normal file
View file

@ -0,0 +1,19 @@
package lists;
use strict;
use warnings;
use Exporter 'import';
our @EXPORTS = qw( content_lists );
use template_helpers 'to_template';
sub content_lists
{
my ($ssn, $data, $lists) = @_;
my %vars = (
lists => $lists
);
to_template(\%vars, \$data->{'content_lists.tt'});
}

24
perl/login.pm Normal file
View file

@ -0,0 +1,24 @@
package login;
use strict;
use warnings;
use Exporter 'import';
our @EXPORT = qw( content_login );
use l10n 'lang';
use template_helpers 'to_template';
sub content_login
{
my ($ssn, $data, $error) = @_;
my %vars = (
error => $error,
lang => \&lang,
);
to_template(\%vars, \$data->{'login.tt'});
}
1;

59
perl/main.pl Normal file
View file

@ -0,0 +1,59 @@
use strict;
use warnings;
# Modules
use Template;
use l10n qw( &lang %L10N );
use notifications qw( generate_notification content_notifications );
use template_helpers qw( &to_template );
use timeline;
use icons 'get_icon';
use status;
use account;
use lists;
use search;
use chat;
use config;
use embed;
use meta;
use login;
# use Devel::Leak;
# my $handle;
# Devel::Leak::NoteSV($handle);
# sub leaky_friend
# {
# $count = Devel::Leak::CheckSV($handle);
# my $leakstr = "Memory: $count SVs\n";
# print STDERR $leakstr;
# }
sub base_page
{
my ($ssn,
$data,
$main,
$notifs) = @_;
my $result;
my %vars = (
prefix => '',
ssn => $ssn,
title => $L10N{'EN_US'}->{'APP_NAME'},
lang => \&lang,
main => $main,
icon => \&get_icon,
sidebar_opacity => $ssn->{config}->{sidebar_opacity} / 255,
acct => $ssn->{account},
data => $data,
notifs => $notifs,
notification => \&generate_notification,
);
my $ret = to_template(\%vars, \$data->{'main.tt'});
# leaky_friend();
return $ret;
}

21
perl/meta.pm Normal file
View file

@ -0,0 +1,21 @@
package meta;
use strict;
use warnings;
our @EXPORT = qw( );
use Exporter 'import';
use template_helpers 'simple_page';
sub about
{
simple_page @_, 'about.tt';
}
sub license
{
simple_page @_, 'license.tt';
}
1;

25
perl/navigation.pm Normal file
View file

@ -0,0 +1,25 @@
package navigation;
use strict;
use warnings;
use Exporter 'import';
our @EXPORT = qw( generate_navigation );
use template_helpers 'to_template';
use l10n 'lang';
sub generate_navigation
{
my ($ssn, $data, $first_id, $last_id) = @_;
my %vars = (
prefix => '',
ssn => $ssn,
lang => \&lang,
start_id => $ssn->{post}->{start_id} || $first_id,
prev_id => $first_id,
next_id => $last_id,
);
to_template(\%vars, \$data->{'navigation.tt'});
}

63
perl/notifications.pm Normal file
View file

@ -0,0 +1,63 @@
package notifications;
use strict;
use warnings;
use Exporter 'import';
our @EXPORT = qw( generate_notification content_notifications embed_notifications );
use template_helpers 'to_template';
use status 'generate_status';
use string_helpers qw( random_error_kaomoji );
use icons 'get_icon';
use embed 'generate_embedded_page';
use navigation 'generate_navigation';
sub generate_notification
{
my ($ssn, $data, $notif, $is_compact) = @_;
$is_compact ||= 0;
my %vars = (
prefix => '',
ssn => $ssn,
notif => $notif,
compact => $is_compact,
create_status => sub { generate_status($ssn, $data, shift, shift, $is_compact); },
icon => \&get_icon,
);
to_template(\%vars, \$data->{'notification.tt'});
}
sub content_notifications
{
my ($ssn, $data, $notifs) = @_;
my %vars = (
prefix => '',
ssn => $ssn,
notifs => $notifs,
notification => sub { generate_notification($ssn, $data, shift); },
random_error_kaomoji => \&random_error_kaomoji,
);
to_template(\%vars, \$data->{'content_notifs.tt'});
}
sub embed_notifications
{
my ($ssn, $data, $notifs) = @_;
my %vars = (
prefix => '',
ssn => $ssn,
notifs => $notifs,
notification => sub { generate_notification($ssn, $data, shift, 1); },
nav => sub { generate_navigation($ssn, $data, $notifs->[0]->{id}, $notifs->[-1]->{id}) },
);
generate_embedded_page($ssn, $data, to_template(\%vars, \$data->{'notifs_embed.tt'}));
}
1;

26
perl/postbox.pm Normal file
View file

@ -0,0 +1,26 @@
package postbox;
use strict;
use warnings;
use template_helpers 'to_template';
use string_helpers qw( get_mentions_from_content );
use Exporter 'import';
our @EXPORT = qw( generate_postbox );
use icons 'get_icon';
sub generate_postbox
{
my ($ssn, $data, $status) = @_;
my %vars = (
prefix => '',
ssn => $ssn,
data => $data,
status => $status,
icon => \&get_icon,
mentionify => \&get_mentions_from_content,
);
to_template(\%vars, \$data->{'postbox.tt'});
}

106
perl/search.pm Normal file
View file

@ -0,0 +1,106 @@
package search;
use strict;
use warnings;
use Exporter 'import';
our @EXPORTS = qw( content_search content_search_tags content_search_accounts content_search_statuses search_tags search_accounts search_statuses );
use template_helpers 'to_template';
use status 'generate_status';
use account 'generate_account_item';
use constant
{
SEARCH_CAT_STATUSES => 0,
SEARCH_CAT_ACCOUNTS => 1,
SEARCH_CAT_TAGS => 2
};
sub search_page
{
my ($ssn, $data, $tab, $content) = @_;
my %vars = (
prefix => '',
ssn => $ssn,
tab => $tab,
content => $content,
);
to_template(\%vars, \$data->{'search.tt'});
}
# CONTENT
sub search_accounts
{
my ($ssn, $data, $search) = @_;
my %vars = (
prefix => '',
ssn => $ssn,
search => $search,
create_account => sub { generate_account_item($ssn, $data, shift); },
);
to_template(\%vars, \$data->{'search_accounts.tt'})
}
sub search_statuses
{
my ($ssn, $data, $search) = @_;
my %vars = (
prefix => '',
ssn => $ssn,
search => $search,
create_status => sub { generate_status($ssn, $data, shift); },
);
to_template(\%vars, \$data->{'search_statuses.tt'})
}
sub search_tags
{
my ($ssn, $data, $search) = @_;
my %vars = (
prefix => '',
ssn => $ssn,
search => $search,
);
to_template(\%vars, \$data->{'search_tags.tt'})
}
sub content_search_accounts
{
search_page($_[0], $_[1], SEARCH_CAT_ACCOUNTS, search_accounts(@_));
}
sub content_search_statuses
{
search_page($_[0], $_[1], SEARCH_CAT_STATUSES, search_statuses(@_));
}
sub content_search_tags
{
search_page($_[0], $_[1], SEARCH_CAT_TAGS, search_tags(@_));
}
sub content_search
{
my ($ssn, $data, $search) = @_;
my %vars = (
prefix => '',
ssn => $ssn,
search => $search,
statuses => search_statuses(@_),
accounts => search_accounts(@_),
hashtags => search_tags(@_),
);
to_template(\%vars, \$data->{'content_search.tt'});
}

99
perl/status.pm Normal file
View file

@ -0,0 +1,99 @@
package status;
use strict;
use warnings;
use string_helpers qw( reltime_to_str greentextify emojify format_username localize_mentions simple_escape );
use icons qw( get_icon visibility_to_icon );
use attachments 'generate_attachment';
use postbox 'generate_postbox';
use emojis 'generate_emoji';
use Exporter 'import';
use l10n 'lang';
our @EXPORT = qw( content_status generate_status );
use template_helpers 'to_template';
# Useful variable to prevent collisions
my $rel_context = 0;
sub generate_status
{
my ($ssn, $data, $status, $notif, $is_compact, $picker) = @_;
my $boost_acct;
# Move status reference for boosts and keep account
# I hate this design but blame MastoAPI, not me.
if ($status->{reblog})
{
$boost_acct = $status->{account};
$status = $status->{reblog};
}
my $is_statusey_notif = ($notif && ($notif->{type} eq 'mention' || $notif->{type} eq 'status'));
my %vars = (
prefix => '',
ssn => $ssn,
status => $status,
boost => $boost_acct, # May be undef
data => $data,
emoji_picker => $picker,
notif => $notif, # May be undef
compact => $is_compact, # May be undef
is_statusey_notif => $is_statusey_notif,
unique_toggle_id => $rel_context++,
interacted_with => $boost_acct || ($notif && !$is_statusey_notif),
# Functions
action_to_string => sub {
return lang('NOTIF_LIKED') if $notif && $notif->{type} eq 'favourite';
return lang('NOTIF_REPEATED') if $boost_acct || $notif->{type} eq 'reblog';
return lang('NOTIF_REACTED_WITH') .' '. $notif->{emoji} if $notif->{type} eq 'emoji reaction';
},
action_to_icon => sub {
return get_icon('like') if $notif && $notif->{type} eq 'favourite';
return get_icon('repeat') if $boost_acct || $notif->{type} eq 'reblog';
return $notif->{emoji} if $notif && $notif->{type} eq 'emoji reaction';
},
icon => \&get_icon,
lang => \&lang,
rel_to_str => \&reltime_to_str,
vis_to_icon => \&visibility_to_icon,
make_att => \&generate_attachment,
make_emoji => \&generate_emoji,
greentextify => \&greentextify,
emojify => \&emojify,
escape => \&simple_escape,
fix_mentions => \&localize_mentions,
format_username => \&format_username,
make_postbox => \&generate_postbox,
);
to_template(\%vars, \$data->{'status.tt'});
}
sub content_status
{
my ($ssn, $data, $status, $statuses_before, $statuses_after, $picker) = @_;
$rel_context = 0;
my %vars = (
prefix => '',
ssn => $ssn,
status => $status,
picker => $picker,
statuses_before => $statuses_before,
statuses_after => $statuses_after,
# Functions
create_status => sub { generate_status($ssn, $data, shift, 0, 0, shift) },
);
to_template(\%vars, \$data->{'content_status.tt'});
}
1;

103
perl/string_helpers.pm Normal file
View file

@ -0,0 +1,103 @@
package string_helpers;
use strict;
use warnings;
use Exporter 'import';
use Scalar::Util 'looks_like_number';
our @EXPORT = qw( reltime_to_str greentextify emojify format_username get_mentions_from_content localize_mentions simple_escape random_error_kaomoji );
my $re_mentions = '(?=<a .*?mention.*?)<a .*?href="https?:\/\/(.*?)\/(?:@|users\/|\/u)?(.*?)?".*?>';
sub reltime_to_str
{
return unless looks_like_number($_[0]);
my $since = time() - $_[0];
return $since . 's' if $since < 60;
return int($since / 60) . 'm' if $since < 60 * 60;
return int($since / (60 * 60)) . 'h' if $since < 60 * 60 * 24;
return int($since / (60 * 60 * 24)) . 'd' if $since < 60 * 60 * 24 * 31;
return int($since / (60 * 60 * 24 * 31)) . 'mon' if $since < 60 * 60 * 24 * 365;
return int($since / (60 * 60 * 24 * 365)) . 'yr';
}
sub simple_escape
{
my $text = shift;
$text =~ s/&/&amp;/gs;
$text =~ s/</&lt;/gs;
$text =~ s/>/&gt;/gs;
$text =~ s/"/&quot;/gs;
$text;
}
sub greentextify
{
my $text = shift;
$text =~ s/(&gt;.*?)(?=<|$)/<span class="greentext">$1<\/span>/gs;
$text =~ s/(&lt;.*?)(?=<|$)/<span class="bluetext">$1<\/span>/gs;
$text =~ s/(?:^|>| )(\^.*?)(?=<|$)/<span class="yellowtext">$1<\/span>/gs;
$text;
}
sub emojify
{
my ($text, $emojis) = @_;
if ($emojis)
{
foreach my $emoji (@{$emojis})
{
my $emo = $emoji->{shortcode};
my $url = $emoji->{url};
$text =~ s/:$emo:/<img class="emoji" src="$url" loading="lazy">/gsi;
}
}
$text;
}
sub format_username
{
my $account = shift;
return unless $account;
#TODO ESCAPE DISPLAY NAME
emojify(simple_escape($account->{display_name}), $account->{emojis});
}
sub localize_mentions
{
my $text = shift;
# idk how to work around this
my $at = '@';
$text =~ s/$re_mentions/<a target="_parent" class="mention" href="\/$at$2$at$1">/gs;
$text;
}
sub get_mentions_from_content
{
my ($ssn, $status) = @_;
my $result = '';
my $acct;
while ($status->{'content'} =~
/<a .*?href=\"https?:\/\/(.*?)\/(?:@|users\/|u\/)?(.*?)?\".*?>@(?:<span>)?.*?(?:<\/span>)?/gs)
{
$acct = $2 . '@' . $1;
# TODO this does not account for the domain (alt interference)
$result .= '@' . $acct unless $ssn->{account}->{acct} eq $2;
}
($status->{account}->{acct} eq $ssn->{account}->{acct})
? $result : '@' . $status->{account}->{acct} . ' ' . $result;
}
sub random_error_kaomoji
{
my @messages = (
"(; ̄Д ̄)",
"(`Δ´)!",
"¯\\_(ツ)_/¯",
"(ノ´・ω・)ノ ミ ┸━┸",
"(╯°□°)╯︵ ┻━┻",
);
@messages[rand(scalar @messages)];
}

55
perl/template_helpers.pm Normal file
View file

@ -0,0 +1,55 @@
package template_helpers;
use strict;
use warnings;
use Exporter 'import';
our @EXPORT = qw( to_template simple_page );
use string_helpers 'simple_escape';
my $template = Template->new(
{
INTERPOLATE => 1,
POST_CHOMP => 1,
EVAL_PERL => 1,
TRIM => 1
});
sub pretty_error($)
{
my $error = simple_escape(shift);
<< "END_ERROR";
<span class="e-error error-pad">
$error
</span>
END_ERROR
}
sub to_template
{
my ($vars, $data) = @_;
my $result;
return 0 unless ref $data;
return 0 unless ref $vars;
# TODO HTML error formatting
$template->process($data, $vars, \$result) ||
return pretty_error($template->error());
$result;
}
# Generic simple page with only session data and pages.
# Pretty commonly done, so useful function.
sub simple_page
{
my ($ssn, $data, $page) = @_;
my %vars = (
prefix => '',
ssn => $ssn,
);
to_template(\%vars, \$data->{$page});
}

33
perl/timeline.pm Normal file
View file

@ -0,0 +1,33 @@
package timeline;
use strict;
use warnings;
use Exporter 'import';
our @EXPORT = qw( content_timeline );
use template_helpers 'to_template';
use icons 'get_icon';
use postbox 'generate_postbox';
use status 'generate_status';
use navigation 'generate_navigation';
sub content_timeline
{
my ($ssn, $data, $statuses, $title, $show_post_box, $fake_timeline) = @_;
my %vars = (
prefix => '',
ssn => $ssn,
data => $data,
statuses => $statuses,
title => $title,
fake_timeline => $fake_timeline,
show_post_box => $show_post_box,
postbox => \&generate_postbox,
create_status => sub { generate_status($ssn, $data, shift); },
# Don't autovivify statuses
nav => sub { generate_navigation($ssn, $data, $statuses->[0]->{id}, $statuses->[-1]->{id}) },
);
to_template(\%vars, \$data->{'timeline.tt'});
}

View file

@ -20,40 +20,47 @@
#include "base_page.h"
#include "about.h"
#include "../static/about.ctmpl"
#include "../static/license.ctmpl"
void content_about(PATH_ARGS)
{
PERL_STACK_INIT;
HV* session_hv = perlify_session(ssn);
mXPUSHs(newRV_inc((SV*)session_hv));
mXPUSHs(newRV_inc((SV*)template_files));
PERL_STACK_SCALAR_CALL("meta::about");
char* dup = PERL_GET_STACK_EXIT;
struct base_page b = {
.category = BASE_CAT_NONE,
.content = (char*)data_about,
.content = dup,
.session = session_hv,
.sidebar_left = NULL
};
// Output
render_base_page(&b, req, ssn, api);
Safefree(dup);
}
void content_about_license(PATH_ARGS)
{
char* page;
char* referer = GET_ENV("HTTP_REFERER", req);
struct license_template tdata = {
.back_ref = referer,
.license_str = "License"
};
page = tmpl_gen_license(&tdata, NULL);
PERL_STACK_INIT;
HV* session_hv = perlify_session(ssn);
XPUSHs(newRV_noinc((SV*)session_hv));
XPUSHs(newRV_noinc((SV*)template_files));
PERL_STACK_SCALAR_CALL("meta::license");
char* dup = PERL_GET_STACK_EXIT;
struct base_page b = {
.category = BASE_CAT_NONE,
.content = page,
.content = dup,
.session = session_hv,
.sidebar_left = NULL
};
// Output
render_base_page(&b, req, ssn, api);
free(page);
Safefree(dup);
}

View file

@ -18,6 +18,7 @@
#include <string.h>
#include <stdlib.h>
#include "global_perl.h"
#include "helpers.h"
#include "base_page.h"
#include "error.h"
@ -30,19 +31,8 @@
#include "base_page.h"
#include "scrobble.h"
#include "string_helpers.h"
#include "navigation.h"
#include "emoji.h"
// Files
#include "../static/account.ctmpl"
#include "../static/account_info.ctmpl"
#include "../static/account_follow_btn.ctmpl"
#include "../static/favourites_page.ctmpl"
#include "../static/bookmarks_page.ctmpl"
#include "../static/account_stub.ctmpl"
#include "../static/account_sidebar.ctmpl"
#include "../static/account_current_menubar.ctmpl"
#include "../static/basic_page.ctmpl"
#include "timeline.h"
#define FOLLOWS_YOU_HTML "<span class=\"acct-badge\">%s</span>"
@ -53,116 +43,58 @@ struct account_args
uint8_t flags;
};
char* load_account_info(struct mstdnt_account* acct,
size_t* size)
static char* accounts_page(HV* session_hv,
mastodont_t* api,
struct mstdnt_account* acct,
struct mstdnt_relationship* rel,
char* header,
struct mstdnt_storage* storage,
struct mstdnt_account* accts,
size_t accts_len)
{
char* info_html;
char* note = emojify(acct->note,
acct->emojis,
acct->emojis_len);
struct account_info_template data = {
.acct_note = note
};
info_html = tmpl_gen_account_info(&data, size);
if (note != acct->note)
free(note);
return info_html;
}
char* construct_account_sidebar(struct mstdnt_account* acct, size_t* size)
{
char* result = NULL;
char* sanitized_display_name = NULL;
char* display_name = NULL;
char* header_css = NULL;
if (acct->display_name)
{
sanitized_display_name = sanitize_html(acct->display_name);
display_name = emojify(sanitized_display_name,
acct->emojis,
acct->emojis_len);
}
easprintf(&header_css, "style=\"background: linear-gradient(var(--account-overlay-gradient-top), var(--account-overlay-gradient-bottom)), url(%s);\"", acct->header);
struct account_sidebar_template data = {
.prefix = config_url_prefix,
.avatar = acct->avatar,
.username = display_name,
.header = acct->header ? header_css : "",
.statuses_text = L10N[L10N_EN_US][L10N_TAB_STATUSES],
.following_text = L10N[L10N_EN_US][L10N_TAB_FOLLOWING],
.followers_text = L10N[L10N_EN_US][L10N_TAB_FOLLOWERS],
.statuses_count = acct->statuses_count,
.following_count = acct->following_count,
.followers_count = acct->followers_count,
.acct = acct->acct,
};
result = tmpl_gen_account_sidebar(&data, size);
if (sanitized_display_name != acct->display_name) free(sanitized_display_name);
if (display_name != sanitized_display_name &&
display_name != acct->display_name)
free(display_name);
free(header_css);
return result;
}
// TODO put account stuff into one function to cleanup a bit
static char* account_followers_cb(struct session* ssn,
mastodont_t* api,
struct mstdnt_account* acct,
void* _args)
{
struct mstdnt_account_args args = {
.max_id = keystr(ssn->post.max_id),
.since_id = NULL,
.min_id = keystr(ssn->post.min_id),
.offset = 0,
.limit = 20,
.with_relationships = 0,
};
struct mstdnt_args m_args;
set_mstdnt_args(&m_args, ssn);
char* accounts_html = NULL, *navigation_box = NULL;
char* output;
struct mstdnt_storage storage = { 0 };
struct mstdnt_account* accounts = NULL;
size_t accts_len = 0;
char* start_id;
PERL_STACK_INIT;
XPUSHs(newRV_noinc((SV*)session_hv));
XPUSHs(newRV_noinc((SV*)template_files));
if (acct)
mXPUSHs(newRV_noinc((SV*)perlify_account(acct)));
else ARG_UNDEFINED();
if (rel)
mXPUSHs(newRV_noinc((SV*)perlify_relationship(rel)));
else ARG_UNDEFINED();
if (mastodont_get_followers(api, &m_args, acct->id, &args, &storage, &accounts, &accts_len))
{
accounts_html = construct_error(storage.error, E_ERROR, 1, NULL);
}
else {
accounts_html = construct_accounts(api, accounts, accts_len, 0, NULL);
if (!accounts_html)
accounts_html = construct_error("No followers...", E_NOTICE, 1, NULL);
}
if (accts && accts_len)
mXPUSHs(newRV_noinc((SV*)perlify_accounts(accts, accts_len)));
else ARG_UNDEFINED();
if (accounts)
{
// If not set, set it
start_id = keystr(ssn->post.start_id) ? keystr(ssn->post.start_id) : accounts[0].id;
navigation_box = construct_navigation_box(start_id,
accounts[0].id,
accounts[accts_len-1].id,
NULL);
}
easprintf(&output, "%s%s",
STR_NULL_EMPTY(accounts_html),
STR_NULL_EMPTY(navigation_box));
// perlapi doesn't specify if a string length of 0 calls strlen so calling just to be safe...
if (header)
mXPUSHp(header, strlen(header));
mastodont_storage_cleanup(&storage);
mstdnt_cleanup_accounts(accounts, accts_len);
if (accounts_html) free(accounts_html);
if (navigation_box) free(navigation_box);
PERL_STACK_SCALAR_CALL("account::content_accounts");
output = PERL_GET_STACK_EXIT;
mastodont_storage_cleanup(storage);
mstdnt_cleanup_accounts(accts, accts_len);
return output;
}
static char* account_following_cb(struct session* ssn,
static char* account_followers_cb(HV* session_hv,
struct session* ssn,
mastodont_t* api,
struct mstdnt_account* acct,
struct mstdnt_relationship* rel,
void* _args)
{
struct mstdnt_args m_args;
set_mstdnt_args(&m_args, ssn);
struct mstdnt_storage storage = { 0 };
struct mstdnt_account* accounts = NULL;
size_t accts_len = 0;
char* result;
struct mstdnt_account_args args = {
.max_id = keystr(ssn->post.max_id),
.since_id = NULL,
@ -171,99 +103,94 @@ static char* account_following_cb(struct session* ssn,
.limit = 20,
.with_relationships = 0,
};
mastodont_get_followers(api, &m_args, acct->id, &args, &storage, &accounts, &accts_len);
return accounts_page(session_hv, api, acct, rel, NULL, &storage, accounts, accts_len);
}
static char* account_following_cb(HV* session_hv,
struct session* ssn,
mastodont_t* api,
struct mstdnt_account* acct,
struct mstdnt_relationship* rel,
void* _args)
{
struct mstdnt_args m_args;
set_mstdnt_args(&m_args, ssn);
char* accounts_html = NULL, *navigation_box = NULL;
char* output;
struct mstdnt_storage storage = { 0 };
struct mstdnt_account* accounts = NULL;
size_t accts_len = 0;
char* start_id;
char* result;
if (mastodont_get_following(api, &m_args, acct->id, &args, &storage, &accounts, &accts_len))
{
accounts_html = construct_error(storage.error, E_ERROR, 1, NULL);
}
else {
accounts_html = construct_accounts(api, accounts, accts_len, 0, NULL);
if (!accounts_html)
accounts_html = construct_error("Not following anyone", E_NOTICE, 1, NULL);
}
struct mstdnt_account_args args = {
.max_id = keystr(ssn->post.max_id),
.since_id = NULL,
.min_id = keystr(ssn->post.min_id),
.offset = 0,
.limit = 20,
.with_relationships = 0,
};
mastodont_get_following(api, &m_args, acct->id, &args, &storage, &accounts, &accts_len);
if (accounts)
{
// If not set, set it
start_id = keystr(ssn->post.start_id) ? keystr(ssn->post.start_id) : accounts[0].id;
navigation_box = construct_navigation_box(start_id,
accounts[0].id,
accounts[accts_len-1].id,
NULL);
}
easprintf(&output, "%s%s",
STR_NULL_EMPTY(accounts_html),
STR_NULL_EMPTY(navigation_box));
mastodont_storage_cleanup(&storage);
mstdnt_cleanup_accounts(accounts, accts_len);
if (accounts_html) free(accounts_html);
if (navigation_box) free(navigation_box);
return output;
return accounts_page(session_hv, api, acct, rel, NULL, &storage, accounts, accts_len);
}
static char* account_statuses_cb(struct session* ssn,
static char* account_statuses_cb(HV* session_hv,
struct session* ssn,
mastodont_t* api,
struct mstdnt_account* acct,
struct mstdnt_relationship* rel,
void* _args)
{
struct mstdnt_args m_args;
set_mstdnt_args(&m_args, ssn);
struct mstdnt_account_statuses_args* args = _args;
char* statuses_html = NULL, *navigation_box = NULL;
char* output;
struct mstdnt_storage storage = { 0 };
struct mstdnt_status* statuses = NULL;
size_t statuses_len = 0;
char* start_id;
char* result;
if (mastodont_get_account_statuses(api, &m_args, acct->id, args, &storage, &statuses, &statuses_len))
{
statuses_html = construct_error(storage.error, E_ERROR, 1, NULL);
}
else {
statuses_html = construct_statuses(ssn, api, statuses, statuses_len, NULL, NULL);
if (!statuses_html)
statuses_html = construct_error("No statuses", E_NOTICE, 1, NULL);
}
mastodont_get_account_statuses(api, &m_args, acct->id, args, &storage, &statuses, &statuses_len);
if (statuses)
{
// If not set, set it
start_id = keystr(ssn->post.start_id) ? keystr(ssn->post.start_id) : statuses[0].id;
navigation_box = construct_navigation_box(start_id,
statuses[0].id,
statuses[statuses_len-1].id,
NULL);
}
easprintf(&output, "%s%s",
STR_NULL_EMPTY(statuses_html),
STR_NULL_EMPTY(navigation_box));
PERL_STACK_INIT;
XPUSHs(newRV_noinc((SV*)session_hv));
XPUSHs(newRV_noinc((SV*)template_files));
mXPUSHs(newRV_noinc((SV*)perlify_account(acct)));
if (rel)
mXPUSHs(newRV_noinc((SV*)perlify_relationship(rel)));
else ARG_UNDEFINED();
if (statuses && statuses_len)
mXPUSHs(newRV_noinc((SV*)perlify_statuses(statuses, statuses_len)));
else ARG_UNDEFINED();
PERL_STACK_SCALAR_CALL("account::content_statuses");
result = PERL_GET_STACK_EXIT;
mastodont_storage_cleanup(&storage);
mstdnt_cleanup_statuses(statuses, statuses_len);
if (statuses_html) free(statuses_html);
if (navigation_box) free(navigation_box);
return output;
return result;
}
static char* account_scrobbles_cb(struct session* ssn, mastodont_t* api, struct mstdnt_account* acct, void* _args)
static char* account_scrobbles_cb(HV* session_hv,
struct session* ssn,
mastodont_t* api,
struct mstdnt_account* acct,
struct mstdnt_relationship* rel,
void* _args)
{
(void)_args;
char* scrobbles_html = NULL;
struct mstdnt_args m_args;
set_mstdnt_args(&m_args, ssn);
struct mstdnt_storage storage = { 0 };
struct mstdnt_scrobble* scrobbles = NULL;
size_t scrobbles_len = 0;
char* result;
struct mstdnt_get_scrobbles_args args = {
.max_id = NULL,
.min_id = NULL,
@ -271,22 +198,26 @@ static char* account_scrobbles_cb(struct session* ssn, mastodont_t* api, struct
.offset = 0,
.limit = 20
};
struct mstdnt_args m_args;
set_mstdnt_args(&m_args, ssn);
mastodont_get_scrobbles(api, &m_args, acct->id, &args, &storage, &scrobbles, &scrobbles_len);
PERL_STACK_INIT;
XPUSHs(newRV_noinc((SV*)session_hv));
XPUSHs(newRV_noinc((SV*)template_files));
mXPUSHs(newRV_noinc((SV*)perlify_account(acct)));
if (rel)
mXPUSHs(newRV_noinc((SV*)perlify_relationship(rel)));
else ARG_UNDEFINED();
if (mastodont_get_scrobbles(api, &m_args, acct->id, &args, &storage, &scrobbles, &scrobbles_len))
{
scrobbles_html = construct_error(storage.error, E_ERROR, 1, NULL);
}
else {
scrobbles_html = construct_scrobbles(scrobbles, scrobbles_len, NULL);
if (!scrobbles_html)
scrobbles_html = construct_error("No scrobbles", E_NOTICE, 1, NULL);
}
if (scrobbles && scrobbles_len)
mXPUSHs(newRV_noinc((SV*)perlify_scrobbles(scrobbles, scrobbles_len)));
else ARG_UNDEFINED();
PERL_STACK_SCALAR_CALL("account::content_scrobbles");
result = PERL_GET_STACK_EXIT;
mastodont_storage_cleanup(&storage);
free(scrobbles);
return scrobbles_html;
return result;
}
void get_account_info(mastodont_t* api, struct session* ssn)
@ -299,16 +230,27 @@ void get_account_info(mastodont_t* api, struct session* ssn)
}
}
/**
* Fetches the account information, and then calls a callback on the information received which
* passes the account information
*
* @param req The request context
* @param ssn The session, which will get transcribed into Perl
* @param api Initiated mstdnt API
* @param id User's ID to fetch
* @param args The arguments to pass into the callback
* @param tab Current tab to focus
* @param callback Calls back with a perlified session, session and api as you passed in, the account,
* the relationship, and additional arguments passed
*/
static void fetch_account_page(FCGX_Request* req,
struct session* ssn,
mastodont_t* api,
char* id,
void* args,
enum account_tab tab,
char* (*callback)(struct session* ssn, mastodont_t* api, struct mstdnt_account* acct, void* args))
char* (*callback)(HV* ssn_hv, struct session* ssn, mastodont_t* api, struct mstdnt_account* acct, struct mstdnt_relationship* rel, void* args))
{
char* account_page;
char* data;
struct mstdnt_storage storage = { 0 },
relations_storage = { 0 };
struct mstdnt_account acct = { 0 };
@ -319,32 +261,18 @@ static void fetch_account_page(FCGX_Request* req,
int lookup_type = config_experimental_lookup ? MSTDNT_LOOKUP_ACCT : MSTDNT_LOOKUP_ID;
if (mastodont_get_account(api, &m_args, lookup_type, id, &acct, &storage))
{
account_page = construct_error(storage.error, E_ERROR, 1, NULL);
}
else {
// Relationships may fail
mastodont_get_relationships(api, &m_args, &(acct.id), 1, &relations_storage, &relationships, &relationships_len);
data = callback(ssn, api,
&acct, args);
account_page = load_account_page(ssn,
api,
&acct,
relationships,
tab,
data,
NULL);
if (!account_page)
account_page = construct_error("Couldn't load page", E_ERROR, 1, NULL);
free(data);
}
mastodont_get_account(api, &m_args, lookup_type, id, &acct, &storage);
// Relationships may fail
mastodont_get_relationships(api, &m_args, &(acct.id), 1, &relations_storage, &relationships, &relationships_len);
struct base_page b = {
HV* session_hv = perlify_session(ssn);
char* data = callback(session_hv, ssn, api, &acct, relationships, args);
struct base_page b = {
.category = BASE_CAT_NONE,
.content = account_page,
.content = data,
.session = session_hv,
.sidebar_left = NULL
};
@ -355,216 +283,7 @@ static void fetch_account_page(FCGX_Request* req,
mstdnt_cleanup_relationships(relationships);
mastodont_storage_cleanup(&storage);
mastodont_storage_cleanup(&relations_storage);
free(account_page);
}
size_t construct_account_page(struct session* ssn,
char** result,
struct account_page* page,
char* content)
{
if (!page->account)
{
*result = NULL;
return 0;
}
size_t size;
struct mstdnt_relationship* rel = page->relationship;
int is_same_user = ssn->logged_in && strcmp(ssn->acct.acct, page->acct) == 0;
char* follow_btn = NULL,
* follow_btn_text = NULL,
* follows_you = NULL,
* info_html = NULL,
* is_blocked = NULL,
* menubar = NULL,
* display_name = NULL,
* sanitized_display_name = NULL;
sanitized_display_name = sanitize_html(page->display_name);
display_name = emojify(sanitized_display_name,
page->account->emojis,
page->account->emojis_len);
// Check if note is not empty
if (page->note && strcmp(page->note, "") != 0)
{
info_html = load_account_info(page->account, NULL);
}
// Display follow button only if not the same user
if (rel && !is_same_user)
{
if (MSTDNT_FLAG_ISSET(rel->flags, MSTDNT_RELATIONSHIP_FOLLOWED_BY))
easprintf(&follows_you, FOLLOWS_YOU_HTML, L10N[page->locale][L10N_FOLLOWS_YOU]);
if (MSTDNT_FLAG_ISSET(rel->flags, MSTDNT_RELATIONSHIP_BLOCKED_BY))
is_blocked = construct_error(L10N[page->locale][L10N_BLOCKED], E_NOTICE, 0, NULL);
if (MSTDNT_FLAG_ISSET(rel->flags, MSTDNT_RELATIONSHIP_REQUESTED))
follow_btn_text = L10N[page->locale][L10N_FOLLOW_PENDING];
else if (MSTDNT_FLAG_ISSET(rel->flags, MSTDNT_RELATIONSHIP_FOLLOWING))
follow_btn_text = L10N[page->locale][L10N_FOLLOWING];
else
follow_btn_text = L10N[page->locale][L10N_FOLLOW];
struct account_follow_btn_template data = {
.prefix = config_url_prefix,
.active = (rel && MSTDNT_FLAG_ISSET(rel->flags, MSTDNT_RELATIONSHIP_FOLLOWING)
? "active" : ""),
.follow_text = follow_btn_text,
.unfollow = (rel && (MSTDNT_FLAG_ISSET(rel->flags, MSTDNT_RELATIONSHIP_FOLLOWING) ||
MSTDNT_FLAG_ISSET(rel->flags, MSTDNT_RELATIONSHIP_REQUESTED))
? "un" : ""),
.userid = page->id,
};
follow_btn = tmpl_gen_account_follow_btn(&data, NULL);
}
// Display menubar with extra options for access if same user
if (is_same_user)
{
struct account_current_menubar_template acmdata = {
.prefix = config_url_prefix,
.blocked_str = "Blocks",
.muted_str = "Mutes",
.favourited_str = "Favorites",
};
menubar = tmpl_gen_account_current_menubar(&acmdata, NULL);
}
struct account_template acct_data = {
.is_blocked = STR_NULL_EMPTY(is_blocked),
.header = page->header_image,
.menubar = menubar,
.display_name = display_name,
.acct = page->acct,
.prefix = config_url_prefix,
.userid = page->id,
.follows_you = follows_you,
.unsubscribe = (rel && MSTDNT_FLAG_ISSET(rel->flags,
MSTDNT_RELATIONSHIP_NOTIFYING)
? "un" : ""),
.subscribe_text = (rel && MSTDNT_FLAG_ISSET(rel->flags,
MSTDNT_RELATIONSHIP_NOTIFYING)
? L10N[page->locale][L10N_UNSUBSCRIBE] : L10N[page->locale][L10N_SUBSCRIBE]),
.unblock = (rel && MSTDNT_FLAG_ISSET(rel->flags,
MSTDNT_RELATIONSHIP_BLOCKING)
? "un" : ""),
.block_text = (rel && MSTDNT_FLAG_ISSET(rel->flags,
MSTDNT_RELATIONSHIP_BLOCKING)
? L10N[page->locale][L10N_UNBLOCK] : L10N[page->locale][L10N_BLOCK]),
.unmute = (rel && MSTDNT_FLAG_ISSET(rel->flags,
MSTDNT_RELATIONSHIP_MUTING)
? "un" : ""),
.mute_text = (rel && MSTDNT_FLAG_ISSET(rel->flags,
MSTDNT_RELATIONSHIP_MUTING)
? L10N[page->locale][L10N_UNMUTE] : L10N[page->locale][L10N_MUTE]),
.tab_statuses_text = L10N[page->locale][L10N_TAB_STATUSES],
.statuses_count = page->statuses_count,
.tab_following_text = L10N[page->locale][L10N_TAB_FOLLOWING],
.following_count = page->following_count,
.tab_followers_text = L10N[page->locale][L10N_TAB_FOLLOWERS],
.followers_count = page->followers_count,
.follow_btn = follow_btn,
.avatar = page->profile_image,
.info = info_html,
.tab_statuses_focused = MAKE_FOCUSED_IF(page->tab, ACCT_TAB_STATUSES),
.tab_statuses_text = L10N[page->locale][L10N_TAB_STATUSES],
.tab_scrobbles_focused = MAKE_FOCUSED_IF(page->tab, ACCT_TAB_SCROBBLES),
.tab_scrobbles_text = L10N[page->locale][L10N_TAB_SCROBBLES],
.tab_media_focused = MAKE_FOCUSED_IF(page->tab, ACCT_TAB_MEDIA),
.tab_media_text = L10N[page->locale][L10N_TAB_MEDIA],
.tab_pinned_focused = MAKE_FOCUSED_IF(page->tab, ACCT_TAB_PINNED),
.tab_pinned_text = L10N[page->locale][L10N_TAB_PINNED],
.acct_content = content
};
*result = tmpl_gen_account(&acct_data, &size);
free(info_html);
free(follows_you);
free(follow_btn);
free(is_blocked);
free(menubar);
if (sanitized_display_name != page->display_name) free(sanitized_display_name);
if (display_name != page->display_name &&
display_name != sanitized_display_name)
free(display_name);
return size;
}
char* construct_account(mastodont_t* api,
struct mstdnt_account* acct,
uint8_t flags,
size_t* size)
{
char* result;
char* sanitized_display_name = sanitize_html(acct->display_name);
struct account_stub_template data = {
.prefix = config_url_prefix,
.acct = acct->acct,
.avatar = acct->avatar,
.display_name = sanitized_display_name,
};
result = tmpl_gen_account_stub(&data, size);
if (sanitized_display_name != acct->display_name) free(sanitized_display_name);
return result;
}
static char* construct_account_voidwrap(void* passed, size_t index, size_t* res)
{
struct account_args* args = passed;
return construct_account(args->api, args->accts + index, args->flags, res);
}
char* construct_accounts(mastodont_t* api,
struct mstdnt_account* accounts,
size_t size,
uint8_t flags,
size_t* ret_size)
{
if (!(accounts && size)) return NULL;
struct account_args acct_args = {
.api = api,
.accts = accounts,
.flags = flags,
};
return construct_func_strings(construct_account_voidwrap, &acct_args, size, ret_size);
}
char* load_account_page(struct session* ssn,
mastodont_t* api,
struct mstdnt_account* acct,
struct mstdnt_relationship* relationship,
enum account_tab tab,
char* content,
size_t* res_size)
{
size_t size;
char* result;
struct account_page page = {
.locale = l10n_normalize(ssn->config.lang),
.account = acct,
.header_image = acct->header,
.profile_image = acct->avatar,
.acct = acct->acct,
.display_name = acct->display_name,
.statuses_count = acct->statuses_count,
.following_count = acct->following_count,
.followers_count = acct->followers_count,
.note = acct->note,
.id = acct->id,
.tab = tab,
.relationship = relationship,
};
size = construct_account_page(ssn, &result, &page, content);
if (res_size) *res_size = size;
return result;
Safefree(data);
}
void content_account_statuses(PATH_ARGS)
@ -671,14 +390,9 @@ void content_account_action(PATH_ARGS)
void content_account_bookmarks(PATH_ARGS)
{
size_t status_count = 0, statuses_html_count = 0;
size_t statuses_len = 0;
struct mstdnt_status* statuses = NULL;
struct mstdnt_storage storage = { 0 };
char* status_format = NULL,
*navigation_box = NULL,
*output = NULL;
char* start_id;
struct mstdnt_bookmarks_args args = {
.max_id = keystr(ssn->post.max_id),
.since_id = NULL,
@ -688,86 +402,11 @@ void content_account_bookmarks(PATH_ARGS)
struct mstdnt_args m_args;
set_mstdnt_args(&m_args, ssn);
if (mastodont_get_bookmarks(api, &m_args, &args, &storage, &statuses, &status_count))
{
status_format = construct_error(storage.error, E_ERROR, 1, NULL);
}
else {
// Construct statuses into HTML
status_format = construct_statuses(ssn, api, statuses, status_count, NULL, &statuses_html_count);
if (!status_format)
status_format = construct_error("Couldn't load posts", E_ERROR, 1, NULL);
}
mastodont_get_bookmarks(api, &m_args, &args, &storage, &statuses, &statuses_len);
// Create post box
if (statuses)
{
// If not set, set it
start_id = keystr(ssn->post.start_id) ? keystr(ssn->post.start_id) : statuses[0].id;
navigation_box = construct_navigation_box(start_id,
statuses[0].id,
statuses[status_count-1].id,
NULL);
}
struct bookmarks_page_template tdata = {
.statuses = status_format,
.navigation = navigation_box
};
output = tmpl_gen_bookmarks_page(&tdata, NULL);
struct base_page b = {
.category = BASE_CAT_BOOKMARKS,
.content = output,
.sidebar_left = NULL
};
// Output
render_base_page(&b, req, ssn, api);
// Cleanup
mastodont_storage_cleanup(&storage);
mstdnt_cleanup_statuses(statuses, status_count);
free(status_format);
free(navigation_box);
free(output);
content_timeline(req, ssn, api, &storage, statuses, statuses_len, BASE_CAT_BOOKMARKS, "Bookmarks", 0, 1);
}
static void accounts_page(FCGX_Request* req,
mastodont_t* api,
struct session* ssn,
struct mstdnt_storage* storage,
char* header,
struct mstdnt_account* accts,
size_t accts_len)
{
char* output;
char* content = construct_accounts(api, accts, accts_len, 0, NULL);
if (!content)
content = construct_error("No accounts here!", E_NOTICE, 1, NULL);
struct basic_page_template tdata = {
.back_ref = getenv("HTTP_REFERER"),
.page_title = header,
.page_content = content,
};
output = tmpl_gen_basic_page(&tdata, NULL);
struct base_page b = {
.category = BASE_CAT_NONE,
.content = output,
.sidebar_left = NULL
};
// Output
render_base_page(&b, req, ssn, api);
mastodont_storage_cleanup(storage);
free(output);
free(content);
}
void content_account_blocked(PATH_ARGS)
{
struct mstdnt_account_args args = {
@ -786,8 +425,18 @@ void content_account_blocked(PATH_ARGS)
mastodont_get_blocks(api, &m_args, &args, &storage, &accts, &accts_len);
accounts_page(req, api, ssn, &storage, "Blocked users", accts, accts_len);
mstdnt_cleanup_accounts(accts, accts_len);
HV* session_hv = perlify_session(ssn);
char* result = accounts_page(session_hv, api, NULL, NULL, "Blocked users", &storage, accts, accts_len);
struct base_page b = {
.category = BASE_CAT_NONE,
.content = result,
.session = session_hv,
.sidebar_left = NULL
};
render_base_page(&b, req, ssn, api);
free(result);
}
void content_account_muted(PATH_ARGS)
@ -808,70 +457,86 @@ void content_account_muted(PATH_ARGS)
mastodont_get_mutes(api, &m_args, &args, &storage, &accts, &accts_len);
accounts_page(req, api, ssn, &storage, "Muted users", accts, accts_len);
mstdnt_cleanup_accounts(accts, accts_len);
HV* session_hv = perlify_session(ssn);
char* result = accounts_page(session_hv, api, NULL, NULL, "Muted users", &storage, accts, accts_len);
struct base_page b = {
.category = BASE_CAT_NONE,
.content = result,
.session = session_hv,
.sidebar_left = NULL
};
render_base_page(&b, req, ssn, api);
free(result);
}
void content_account_favourites(PATH_ARGS)
{
size_t status_count = 0, statuses_html_count = 0;
struct mstdnt_args m_args;
set_mstdnt_args(&m_args, ssn);
size_t statuses_len = 0;
struct mstdnt_status* statuses = NULL;
struct mstdnt_storage storage = { 0 };
char* status_format = NULL,
*navigation_box = NULL,
*output = NULL,
*page = NULL;
char* start_id;
struct mstdnt_favourites_args args = {
.max_id = keystr(ssn->post.max_id),
.min_id = keystr(ssn->post.min_id),
.limit = 20,
};
struct mstdnt_args m_args;
set_mstdnt_args(&m_args, ssn);
if (mastodont_get_favourites(api, &m_args, &args, &storage, &statuses, &status_count))
{
status_format = construct_error(storage.error, E_ERROR, 1, NULL);
}
else {
// Construct statuses into HTML
status_format = construct_statuses(ssn, api, statuses, status_count, NULL, &statuses_html_count);
if (!status_format)
status_format = construct_error("Couldn't load posts", E_ERROR, 1, NULL);
}
// Create post box
if (statuses)
{
// If not set, set it
start_id = keystr(ssn->post.start_id) ? keystr(ssn->post.start_id) : statuses[0].id;
navigation_box = construct_navigation_box(start_id,
statuses[0].id,
statuses[status_count-1].id,
NULL);
}
struct favourites_page_template tdata = {
.statuses = status_format,
.navigation = navigation_box
};
output = tmpl_gen_favourites_page(&tdata, NULL);
struct base_page b = {
.category = BASE_CAT_FAVOURITES,
.content = output,
.sidebar_left = NULL
};
// Output
render_base_page(&b, req, ssn, api);
// Cleanup
mastodont_storage_cleanup(&storage);
mstdnt_cleanup_statuses(statuses, status_count);
if (status_format) free(status_format);
if (navigation_box) free(navigation_box);
if (output) free(output);
mastodont_get_favourites(api, &m_args, &args, &storage, &statuses, &statuses_len);
content_timeline(req, ssn, api, &storage, statuses, statuses_len, BASE_CAT_BOOKMARKS, "Favorites", 0, 1);
}
PERLIFY_MULTI(account, accounts, mstdnt_account)
HV* perlify_account(const struct mstdnt_account* acct)
{
if (!acct) return NULL;
HV* acct_hv = newHV();
hvstores_str(acct_hv, "id", acct->id);
hvstores_str(acct_hv, "username", acct->username);
hvstores_str(acct_hv, "acct", acct->acct);
hvstores_str(acct_hv, "display_name", acct->display_name);
hvstores_str(acct_hv, "note", acct->note);
hvstores_str(acct_hv, "avatar", acct->avatar);
hvstores_str(acct_hv, "avatar_static", acct->avatar_static);
hvstores_str(acct_hv, "header", acct->header);
hvstores_str(acct_hv, "header_static", acct->header_static);
hvstores_int(acct_hv, "created_at", acct->created_at);
hvstores_str(acct_hv, "last_status_at", acct->last_status_at);
hvstores_str(acct_hv, "mute_expires_at", acct->mute_expires_at);
hvstores_int(acct_hv, "statuses_count", acct->statuses_count);
hvstores_int(acct_hv, "followers_count", acct->followers_count);
hvstores_int(acct_hv, "following_count", acct->following_count);
hvstores_int(acct_hv, "bot", acct->bot);
hvstores_int(acct_hv, "suspended", acct->suspended);
hvstores_int(acct_hv, "locked", acct->locked);
hvstores_int(acct_hv, "discoverable", acct->discoverable);
hvstores_ref(acct_hv, "emojis", perlify_emojis(acct->emojis, acct->emojis_len));
return acct_hv;
}
HV* perlify_relationship(const struct mstdnt_relationship* rel)
{
if (!rel) return NULL;
HV* rel_hv = newHV();
hvstores_str(rel_hv, "id", rel->id);
hvstores_int(rel_hv, "following", MSTDNT_T_FLAG_ISSET(rel, MSTDNT_RELATIONSHIP_FOLLOWING));
hvstores_int(rel_hv, "requested", MSTDNT_T_FLAG_ISSET(rel, MSTDNT_RELATIONSHIP_REQUESTED));
hvstores_int(rel_hv, "endoresed", MSTDNT_T_FLAG_ISSET(rel, MSTDNT_RELATIONSHIP_ENDORSED));
hvstores_int(rel_hv, "followed_by", MSTDNT_T_FLAG_ISSET(rel, MSTDNT_RELATIONSHIP_FOLLOWED_BY));
hvstores_int(rel_hv, "muting", MSTDNT_T_FLAG_ISSET(rel, MSTDNT_RELATIONSHIP_MUTING));
hvstores_int(rel_hv, "muting_notifs", MSTDNT_T_FLAG_ISSET(rel, MSTDNT_RELATIONSHIP_MUTING_NOTIFS));
hvstores_int(rel_hv, "showing_reblogs", MSTDNT_T_FLAG_ISSET(rel, MSTDNT_RELATIONSHIP_SHOWING_REBLOGS));
hvstores_int(rel_hv, "notifying", MSTDNT_T_FLAG_ISSET(rel, MSTDNT_RELATIONSHIP_NOTIFYING));
hvstores_int(rel_hv, "blocking", MSTDNT_T_FLAG_ISSET(rel, MSTDNT_RELATIONSHIP_BLOCKING));
hvstores_int(rel_hv, "domain_blocking", MSTDNT_T_FLAG_ISSET(rel, MSTDNT_RELATIONSHIP_DOMAIN_BLOCKING));
hvstores_int(rel_hv, "blocked_by", MSTDNT_T_FLAG_ISSET(rel, MSTDNT_RELATIONSHIP_BLOCKED_BY));
return rel_hv;
}

View file

@ -18,9 +18,11 @@
#ifndef ACCOUNT_H
#define ACCOUNT_H
#include "global_perl.h"
#include <stddef.h>
#include <mastodont.h>
#include "session.h"
#include "path.h"
#include "l10n.h"
#define ACCOUNT_NOP 0
@ -55,33 +57,6 @@ struct account_page
};
void get_account_info(mastodont_t* api, struct session* ssn);
char* construct_account_sidebar(struct mstdnt_account* acct, size_t* size);
char* construct_account(mastodont_t* api,
struct mstdnt_account* account,
uint8_t flags,
size_t* size);
char* construct_accounts(mastodont_t* api,
struct mstdnt_account* accounts,
size_t size,
uint8_t flags,
size_t* ret_size);
size_t construct_account_page(struct session *ssn,
char** result,
struct account_page* page,
char* content);
char* load_account_page(struct session* ssn,
mastodont_t* api,
struct mstdnt_account* acct,
struct mstdnt_relationship* relationship,
enum account_tab tab,
char* content,
size_t* res_size);
char* load_account_info(struct mstdnt_account* acct,
size_t* size);
void content_account_followers(PATH_ARGS);
void content_account_following(PATH_ARGS);
@ -95,4 +70,8 @@ void content_account_action(PATH_ARGS);
void content_account_favourites(PATH_ARGS);
void content_account_bookmarks(PATH_ARGS);
HV* perlify_account(const struct mstdnt_account* acct);
AV* perlify_accounts(const struct mstdnt_account* accounts, size_t len);
HV* perlify_relationship(const struct mstdnt_relationship* rel);
#endif // ACCOUNT_H

View file

@ -16,19 +16,21 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef REPLY_H
#define REPLY_H
#include "session.h"
#include <stddef.h>
#include <mastodont.h>
#include "applications.h"
char* construct_post_box(struct mstdnt_status* reply_id,
char* default_content,
size_t* size);
HV* perlify_application(const struct mstdnt_app* app)
{
if (!app) return NULL;
HV* app_hv = newHV();
hvstores_str(app_hv, "id", app->id);
hvstores_str(app_hv, "name", app->name);
hvstores_str(app_hv, "website", app->website);
hvstores_str(app_hv, "redirect_uri", app->redirect_uri);
hvstores_str(app_hv, "client_id", app->client_id);
hvstores_str(app_hv, "client_secret", app->client_secret);
hvstores_str(app_hv, "vapid_key", app->vapid_key);
char* reply_status(struct session* ssn,
char* id,
struct mstdnt_status* status);
return app_hv;
}
#endif // REPLY_H

View file

@ -16,13 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef TEST_H
#define TEST_H
#include <stddef.h>
#ifndef APPLICATIONS_H
#define APPLICATIONS_H
#include <mastodont.h>
#include "session.h"
#include "path.h"
#include "global_perl.h"
void content_test(PATH_ARGS);
HV* perlify_application(const struct mstdnt_app* app);
#endif /* TEST_H */
#endif /* APPLICATIONS_H */

View file

@ -24,13 +24,6 @@
#include "attachments.h"
#include "string_helpers.h"
// Pages
#include "../static/attachments.ctmpl"
#include "../static/attachment_image.ctmpl"
#include "../static/attachment_gifv.ctmpl"
#include "../static/attachment_video.ctmpl"
#include "../static/attachment_link.ctmpl"
#include "../static/attachment_audio.ctmpl"
struct attachments_args
{
@ -123,79 +116,22 @@ void cleanup_media_ids(struct session* ssn, char** media_ids)
free(media_ids);
}
char* construct_attachment(struct session* ssn,
mstdnt_bool sensitive,
struct mstdnt_attachment* att,
size_t* str_size)
HV* perlify_attachment(const struct mstdnt_attachment* const attachment)
{
// Due to how similar the attachment templates are, we're just going to use their data files
// and not generate any templates, saves some LOC!
char* att_html;
size_t s;
const char* attachment_str;
if (!att) return NULL;
if (ssn->config.stat_attachments)
switch (att->type)
{
case MSTDNT_ATTACHMENT_IMAGE:
attachment_str = data_attachment_image; break;
case MSTDNT_ATTACHMENT_GIFV:
attachment_str = data_attachment_gifv; break;
case MSTDNT_ATTACHMENT_VIDEO:
attachment_str = data_attachment_video; break;
case MSTDNT_ATTACHMENT_AUDIO:
attachment_str = data_attachment_audio; break;
case MSTDNT_ATTACHMENT_UNKNOWN: // Fall through
default:
attachment_str = data_attachment_link; break;
}
else
attachment_str = data_attachment_link;
// Images/visible content displays sensitive placeholder after
if ((att->type == MSTDNT_ATTACHMENT_IMAGE ||
att->type == MSTDNT_ATTACHMENT_GIFV ||
att->type == MSTDNT_ATTACHMENT_VIDEO) &&
ssn->config.stat_attachments)
{
s = easprintf(&att_html, attachment_str,
att->url,
sensitive ? "<div class=\"sensitive-contain sensitive\"></div>" : "");
}
else {
s = easprintf(&att_html, attachment_str,
sensitive ? "sensitive" : "",
att->url);
}
if (str_size) *str_size = s;
return att_html;
if (!attachment) return NULL;
HV* attach_hv = newHV();
hvstores_str(attach_hv, "id", attachment->id);
hvstores_int(attach_hv, "type", attachment->type);
hvstores_str(attach_hv, "url", attachment->url);
hvstores_str(attach_hv, "preview_url", attachment->preview_url);
hvstores_str(attach_hv, "remote_url", attachment->remote_url);
hvstores_str(attach_hv, "hash", attachment->hash);
hvstores_str(attach_hv, "description", attachment->description);
hvstores_str(attach_hv, "blurhash", attachment->blurhash);
return attach_hv;
}
static char* construct_attachments_voidwrap(void* passed, size_t index, size_t* res)
{
struct attachments_args* args = passed;
return construct_attachment(args->ssn, args->sensitive, args->atts + index, res);
}
char* construct_attachments(struct session* ssn,
mstdnt_bool sensitive,
struct mstdnt_attachment* atts,
size_t atts_len,
size_t* str_size)
{
size_t elements_size;
struct attachments_args args = { ssn, atts, sensitive };
char* elements = construct_func_strings(construct_attachments_voidwrap, &args, atts_len, &elements_size);
char* att_view;
size_t s = easprintf(&att_view, data_attachments, elements);
if (str_size) *str_size = s;
// Cleanup
free(elements);
return att_view;
}
PERLIFY_MULTI(attachment, attachments, mstdnt_attachment)
void api_attachment_create(PATH_ARGS)
{

View file

@ -21,6 +21,7 @@
#include <mastodont.h>
#include "path.h"
#include "session.h"
#include "global_perl.h"
#define FILES_READY(ssn) (ssn->post.files.type.f.array_size && \
ssn->post.files.type.f.content && \
@ -33,8 +34,10 @@ int try_upload_media(struct mstdnt_storage** storage,
char*** media_ids);
void cleanup_media_storages(struct session* ssn, struct mstdnt_storage* storage);
void cleanup_media_ids(struct session* ssn, char** media_ids);
char* construct_attachment(struct session* ssn, mstdnt_bool sensitive, struct mstdnt_attachment* att, size_t* str_size);
char* construct_attachments(struct session* ssn, mstdnt_bool sensitive, struct mstdnt_attachment* atts, size_t atts_len, size_t* str_size);
void api_attachment_create(PATH_ARGS);
// Perl
HV* perlify_attachment(const struct mstdnt_attachment* const attachment);
AV* perlify_attachments(const struct mstdnt_attachment* const attachments, size_t len);
#endif // ATTACHMENTS_H

View file

@ -16,7 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <fcgi_stdio.h>
#include <string.h>
#include <stdlib.h>
#include "helpers.h"
@ -28,179 +27,69 @@
#include "../config.h"
#include "local_config_set.h"
#include "account.h"
#include "cgi.h"
#include "global_cache.h"
// Files
#include "../static/index.ctmpl"
#include "../static/quick_login.ctmpl"
#define BODY_STYLE "style=\"background:url('%s');\""
void render_base_page(struct base_page* page, FCGX_Request* req, struct session* ssn, mastodont_t* api)
{
struct mstdnt_args m_args;
set_mstdnt_args(&m_args, ssn);
char* cookie = GET_ENV("HTTP_COOKIE", req);
enum l10n_locale locale = l10n_normalize(ssn->config.lang);
char* theme_str = NULL;
const char* login_string = "<a href=\"login\" id=\"login-header\">Login / Register</a>";
const char* sidebar_embed = "<iframe class=\"sidebar-frame\" loading=\"lazy\" src=\"/notifications_compact\"></iframe>";
char* background_url_css = NULL;
// Sidebar
char* sidebar_str,
* main_sidebar_str = NULL,
* account_sidebar_str = NULL,
* instance_str = NULL;
// Mastodont, used for notifications sidebar
struct mstdnt_storage storage = { 0 };
struct mstdnt_notification* notifs = NULL;
size_t notifs_len = 0;
#define SIDEBAR_CSS_LEN 128
char sidebar_css[SIDEBAR_CSS_LEN];
if (keyint(ssn->cookies.logged_in))
login_string = "";
if (ssn->config.background_url)
// Fetch notification (if not iFrame)
if (keystr(ssn->cookies.logged_in) && keystr(ssn->cookies.access_token) &&
!ssn->config.notif_embed)
{
easprintf(&background_url_css, BODY_STYLE, ssn->config.background_url);
}
// If user is logged in
if (keystr(ssn->cookies.logged_in) && keystr(ssn->cookies.access_token))
{
account_sidebar_str = construct_account_sidebar(&(ssn->acct), NULL);
// Get / Show notifications on sidebar
if (ssn->config.notif_embed)
{
main_sidebar_str = (char*)sidebar_embed;
}
else {
struct mstdnt_get_notifications_args args = {
.exclude_types = 0,
.account_id = NULL,
.exclude_visibilities = 0,
.include_types = 0,
.with_muted = 1,
.max_id = NULL,
.min_id = NULL,
.since_id = NULL,
.offset = 0,
.limit = 8,
};
if (mastodont_get_notifications(api,
&m_args,
&args,
&storage,
&notifs,
&notifs_len) == 0)
{
main_sidebar_str = construct_notifications_compact(ssn, api, notifs, notifs_len, NULL);
}
mstdnt_cleanup_notifications(notifs, notifs_len);
mastodont_storage_cleanup(&storage);
}
}
else {
// Construct small login page
struct quick_login_template tdata = {
.prefix = config_url_prefix,
.username = L10N[locale][L10N_USERNAME],
.password = L10N[locale][L10N_PASSWORD],
.login = L10N[locale][L10N_LOGIN_BTN],
struct mstdnt_notifications_args args = {
.exclude_types = 0,
.account_id = NULL,
.exclude_visibilities = 0,
.include_types = 0,
.with_muted = 1,
.max_id = NULL,
.min_id = NULL,
.since_id = NULL,
.offset = 0,
.limit = 8,
};
main_sidebar_str = tmpl_gen_quick_login(&tdata, NULL);
mastodont_get_notifications(
api,
&m_args,
&args,
&storage,
&notifs,
&notifs_len
);
}
// Combine into sidebar
easprintf(&sidebar_str, "%s%s",
account_sidebar_str ? account_sidebar_str : "",
main_sidebar_str ? main_sidebar_str : "");
PERL_STACK_INIT;
// Create instance panel
if (g_cache.panel_html.response)
easprintf(&instance_str, "<div class=\"static-html\" id=\"instance-panel\">%s</div>",
(g_cache.panel_html.response ?
g_cache.panel_html.response : ""));
HV* real_ssn = page->session ? page->session : perlify_session(ssn);
mXPUSHs(newRV_noinc((SV*)real_ssn));
mXPUSHs(newRV_inc((SV*)template_files));
mXPUSHs(newSVpv(page->content, 0));
if (ssn->config.theme && !(strcmp(ssn->config.theme, "treebird") == 0 &&
ssn->config.themeclr == 0))
if (notifs && notifs_len)
{
easprintf(&theme_str, "<link rel=\"stylesheet\" type=\"text/css\" href=\"/%s%s.css\">",
ssn->config.theme,
ssn->config.themeclr ? "-dark" : "");
mXPUSHs(newRV_noinc(perlify_notifications(notifs, notifs_len)));
}
else ARG_UNDEFINED();
if (ssn->config.sidebar_opacity)
{
float sidebar_opacity = (float)ssn->config.sidebar_opacity / 255.0f;
snprintf(sidebar_css, SIDEBAR_CSS_LEN, ":root { --sidebar-opacity: %.2f; }",
sidebar_opacity);
}
// Run function
PERL_STACK_SCALAR_CALL("base_page");
char* dup = PERL_GET_STACK_EXIT;
struct index_template index_tmpl = {
.title = L10N[locale][L10N_APP_NAME],
.sidebar_css = sidebar_css,
.theme_str = theme_str,
.prefix = config_url_prefix,
.background_url = background_url_css,
.name = L10N[locale][L10N_APP_NAME],
.sidebar_cnt = login_string,
.placeholder = L10N[locale][L10N_SEARCH_PLACEHOLDER],
.search_btn = L10N[locale][L10N_SEARCH_BUTTON],
.active_home = CAT_TEXT(page->category, BASE_CAT_HOME),
.home = L10N[locale][L10N_HOME],
.active_local = CAT_TEXT(page->category, BASE_CAT_LOCAL),
.local = L10N[locale][L10N_LOCAL],
.active_federated = CAT_TEXT(page->category, BASE_CAT_FEDERATED),
.federated = L10N[locale][L10N_FEDERATED],
.active_notifications = CAT_TEXT(page->category, BASE_CAT_NOTIFICATIONS),
.notifications = L10N[locale][L10N_NOTIFICATIONS],
.active_lists = CAT_TEXT(page->category, BASE_CAT_LISTS),
.lists = L10N[locale][L10N_LISTS],
.active_favourites = CAT_TEXT(page->category, BASE_CAT_FAVOURITES),
.favourites = L10N[locale][L10N_FAVOURITES],
.active_bookmarks = CAT_TEXT(page->category, BASE_CAT_BOOKMARKS),
.bookmarks = L10N[locale][L10N_BOOKMARKS],
.active_direct = CAT_TEXT(page->category, BASE_CAT_DIRECT),
.direct = L10N[locale][L10N_DIRECT],
.active_chats = CAT_TEXT(page->category, BASE_CAT_CHATS),
.chats = "Chats",
.active_config = CAT_TEXT(page->category, BASE_CAT_CONFIG),
.config = L10N[locale][L10N_CONFIG],
.sidebar_leftbar = page->sidebar_left,
.instance_panel = ssn->config.instance_panel ? instance_str : "",
.main = page->content,
.sidebar_rightbar = sidebar_str,
.about_link_str = "About",
.license_link_str = "License",
.source_link_str = "Source code",
};
size_t len;
char* data = tmpl_gen_index(&index_tmpl, &len);
send_result(req, NULL, "text/html", dup, 0);
if (!data)
{
perror("malloc");
goto cleanup;
}
send_result(req, NULL, "text/html", data, len);
// Cleanup
/* cleanup_all: */
free(data);
cleanup:
free(sidebar_str);
if (main_sidebar_str != sidebar_embed) free(main_sidebar_str);
free(account_sidebar_str);
free(background_url_css);
free(instance_str);
free(theme_str);
mstdnt_cleanup_notifications(notifs, notifs_len);
mastodont_storage_cleanup(&storage);
Safefree(dup);
}
void send_result(FCGX_Request* req, char* status, char* content_type, char* data, size_t data_len)
@ -216,7 +105,7 @@ void send_result(FCGX_Request* req, char* status, char* content_type, char* data
"Content-Length: %d\r\n\r\n",
status ? status : "200 OK",
content_type ? content_type : "text/html",
data_len + 1);
data_len);
#ifdef SINGLE_THREADED
puts(data);
#else

View file

@ -18,14 +18,14 @@
#ifndef BASE_PAGE_H
#define BASE_PAGE_H
#include "global_perl.h"
#include "session.h"
#include <fcgi_stdio.h>
#include <fcgiapp.h>
#include <mastodont.h>
#include "l10n.h"
#include "local_config.h"
#include "path.h"
#include "session.h"
enum base_category
{
BASE_CAT_NONE,
@ -46,6 +46,7 @@ struct base_page
enum base_category category;
char* content;
char* sidebar_left;
HV* session;
};
void render_base_page(struct base_page* page, FCGX_Request* req, struct session* ssn, mastodont_t* api);

View file

@ -16,14 +16,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef NAVIGATION_H
#define NAVIGATION_H
#include <stddef.h>
#include <mastodont.h>
/* #ifndef IMPORT_CGI_H */
/* #define IMPORT_CGI_H */
char* construct_navigation_box(char* start_id,
char* prev_id,
char* next_id,
size_t* size);
#ifndef NO_FCGI
#include <fcgi_stdio.h>
#endif // NO_FCGI
#endif // NAVIGATION_H
#ifndef SINGLE_THREADED
#include <fcgiapp.h>
#endif // SINGLE_THREADED
// #endif

View file

@ -17,6 +17,8 @@
*/
#include <stdlib.h>
#include "account.h"
#include "emoji.h"
#include "../config.h"
#include "conversations.h"
#include "helpers.h"
@ -24,138 +26,6 @@
#include "error.h"
#include "base_page.h"
// Files
#include "../static/chat.ctmpl"
#include "../static/chats_page.ctmpl"
#include "../static/message.ctmpl"
#include "../static/chat_view.ctmpl"
#include "../static/embed.ctmpl"
struct construct_message_args
{
struct mstdnt_message* msg;
struct mstdnt_account* you;
struct mstdnt_account* them;
size_t msg_size; // Read messages backwards
};
struct construct_chats_args
{
mastodont_t* api;
struct mstdnt_args* args;
struct mstdnt_chat* chats;
};
char* construct_chat(mastodont_t* api,
struct mstdnt_args* m_args,
struct mstdnt_chat* chat,
size_t* size)
{
char* result;
char* msg_id = NULL;
char* last_message = "<span class=\"empty-chat-text\">Chat created</span>";
// Get latest message
struct mstdnt_storage storage = { 0 };
struct mstdnt_message* messages = NULL;
size_t messages_len = 0;
struct mstdnt_chats_args args = {
.with_muted = MSTDNT_TRUE,
.offset = 0,
.limit = 1,
};
if (mastodont_get_chat_messages(api, m_args, chat->id, &args, &storage,
&messages, &messages_len) == 0 && messages_len == 1)
{
last_message = messages[0].content;
msg_id = messages[0].id;
}
struct chat_template data = {
.id = chat->id,
.prefix = config_url_prefix,
.acct = chat->account.acct,
.avatar = chat->account.avatar,
.display_name = chat->account.display_name,
.message_id = msg_id,
.last_message = last_message,
};
result = tmpl_gen_chat(&data, size);
mastodont_storage_cleanup(&storage);
// TODO cleanup messages
return result;
}
static char* construct_chat_voidwrap(void* passed, size_t index, size_t* res)
{
struct construct_chats_args* args = passed;
return construct_chat(args->api, args->args, args->chats + index, res);
}
char* construct_chats(mastodont_t* api,
struct mstdnt_args* m_args,
struct mstdnt_chat* chats,
size_t size,
size_t* ret_size)
{
struct construct_chats_args args = {
.api = api,
.args = m_args,
.chats = chats,
};
return construct_func_strings(construct_chat_voidwrap, &args, size, ret_size);
}
char* construct_message(struct mstdnt_message* msg,
struct mstdnt_account* you,
struct mstdnt_account* them,
size_t* size)
{
char* result;
if (!(you && them)) return NULL;
int is_you = strcmp(you->id, msg->account_id) == 0;
struct message_template data = {
.id = msg->id,
.content = msg->content,
.is_you = is_you ? "message-you" : NULL,
.avatar = is_you ? you->avatar : them->avatar
};
result = tmpl_gen_message(&data, size);
return result;
}
static char* construct_message_voidwrap(void* passed, size_t index, size_t* res)
{
struct construct_message_args* args = passed;
return construct_message(args->msg + (args->msg_size - index - 1), args->you, args->them, res);
}
char* construct_messages(struct mstdnt_message* messages,
struct mstdnt_account* you,
struct mstdnt_account* them,
size_t size,
size_t* ret_size)
{
struct construct_message_args args = {
.msg = messages,
.you = you,
.them = them,
.msg_size = size
};
return construct_func_strings(construct_message_voidwrap, &args, size, ret_size);
}
char* construct_chats_view(char* lists_string, size_t* size)
{
struct chats_page_template data = {
.content = lists_string,
};
return tmpl_gen_chats_page(&data, size);
}
void content_chats(PATH_ARGS)
{
struct mstdnt_args m_args;
@ -163,8 +33,6 @@ void content_chats(PATH_ARGS)
struct mstdnt_chat* chats = NULL;
size_t chats_len = 0;
struct mstdnt_storage storage = { 0 };
char* chats_page = NULL;
char* chats_html = NULL;
struct mstdnt_chats_args args = {
.with_muted = MSTDNT_TRUE,
@ -175,20 +43,25 @@ void content_chats(PATH_ARGS)
.limit = 20,
};
if (mastodont_get_chats_v2(api, &m_args, &args, &storage, &chats, &chats_len))
{
chats_page = construct_error(storage.error, E_ERROR, 1, NULL);
}
else {
chats_html = construct_chats(api, &m_args, chats, chats_len, NULL);
if (!chats_html)
chats_html = construct_error("No chats", E_NOTICE, 1, NULL);
chats_page = construct_chats_view(chats_html, NULL);
}
mastodont_get_chats_v2(api, &m_args, &args, &storage, &chats, &chats_len);
PERL_STACK_INIT;
HV* session_hv = perlify_session(ssn);
XPUSHs(newRV_noinc((SV*)session_hv));
XPUSHs(newRV_noinc((SV*)template_files));
if (chats)
mXPUSHs(newRV_noinc((SV*)perlify_chats(chats, chats_len)));
else ARG_UNDEFINED();
PERL_STACK_SCALAR_CALL("chat::content_chats");
// Duplicate so we can free the TMPs
char* dup = PERL_GET_STACK_EXIT;
struct base_page b = {
.category = BASE_CAT_CHATS,
.content = chats_page,
.content = dup,
.session = session_hv,
.sidebar_left = NULL
};
@ -197,12 +70,11 @@ void content_chats(PATH_ARGS)
// Cleanup
mastodont_storage_cleanup(&storage);
free(chats_page);
free(chats_html);
// TOOD cleanup chats
mstdnt_cleanup_chats(chats, chats_len);
Safefree(dup);
}
char* construct_chat_view(struct session* ssn, mastodont_t* api, char* id, size_t* len)
void content_chat_view(PATH_ARGS)
{
struct mstdnt_args m_args;
set_mstdnt_args(&m_args, ssn);
@ -211,9 +83,6 @@ char* construct_chat_view(struct session* ssn, mastodont_t* api, char* id, size_
size_t messages_len = 0;
struct mstdnt_storage storage = { 0 }, storage_chat = { 0 };
struct mstdnt_chat chat;
struct mstdnt_storage acct_storage = { 0 };
char* chats_page = NULL;
char* messages_html = NULL;
struct mstdnt_chats_args args = {
.with_muted = MSTDNT_TRUE,
@ -224,72 +93,71 @@ char* construct_chat_view(struct session* ssn, mastodont_t* api, char* id, size_
.limit = 20,
};
if (len) *len = 0;
mastodont_get_chat_messages(api, &m_args, data[0], &args, &storage, &messages, &messages_len);
int chat_code = mastodont_get_chat(api, &m_args, data[0],
&storage_chat, &chat);
if (mastodont_get_chat_messages(api, &m_args, id,
&args, &storage, &messages, &messages_len) ||
mastodont_get_chat(api, &m_args, id,
&storage_chat, &chat))
{
chats_page = construct_error(storage.error, E_ERROR, 1, NULL);
}
else {
messages_html = construct_messages(messages, &(ssn->acct), &(chat.account), messages_len, NULL);
if (!messages_html)
messages_html = construct_error("This is the start of something new...", E_NOTICE, 1, NULL);
PERL_STACK_INIT;
HV* session_hv = perlify_session(ssn);
XPUSHs(newRV_noinc((SV*)session_hv));
XPUSHs(newRV_noinc((SV*)template_files));
if (chat_code == 0)
mXPUSHs(newRV_noinc((SV*)perlify_chat(&chat)));
else ARG_UNDEFINED();
if (messages)
mXPUSHs(newRV_noinc((SV*)perlify_messages(messages, messages_len)));
else ARG_UNDEFINED();
PERL_STACK_SCALAR_CALL("chat::construct_chat");
struct chat_view_template tmpl = {
.back_link = "/chats",
.prefix = config_url_prefix,
.avatar = chat.account.avatar,
.acct = chat.account.acct,
.messages = messages_html
};
chats_page = tmpl_gen_chat_view(&tmpl, len);
}
mastodont_storage_cleanup(&storage);
mastodont_storage_cleanup(&acct_storage);
free(messages_html);
// TODO cleanup messages
return chats_page;
}
void content_chat_view(PATH_ARGS)
{
char* chat_view = construct_chat_view(ssn, api, data[0], NULL);
// Duplicate so we can free the TMPs
char* dup = PERL_GET_STACK_EXIT;
struct base_page b = {
.category = BASE_CAT_CHATS,
.content = chat_view,
.content = dup,
.session = session_hv,
.sidebar_left = NULL
};
// Output
render_base_page(&b, req, ssn, api);
free(chat_view);
mastodont_storage_cleanup(&storage);
mastodont_storage_cleanup(&storage_chat);
mstdnt_cleanup_chat(&chat);
mstdnt_cleanup_messages(messages);
Safefree(dup);
}
void content_chat_embed(PATH_ARGS)
HV* perlify_chat(const struct mstdnt_chat* chat)
{
size_t result_len;
char* result;
char* chat_view = construct_chat_view(ssn, api, data[0], NULL);
if (!chat) return NULL;
struct embed_template tmpl = {
.stylesheet = "treebird20",
.embed = chat_view,
};
HV* chat_hv = newHV();
hvstores_ref(chat_hv, "account", perlify_account(&(chat->account)));
hvstores_str(chat_hv, "id", chat->id);
hvstores_int(chat_hv, "unread", chat->unread);
result = tmpl_gen_embed(&tmpl, &result_len);
// Output
send_result(req, NULL, NULL, result, result_len);
free(chat_view);
free(result);
return chat_hv;
}
PERLIFY_MULTI(chat, chats, mstdnt_chat)
HV* perlify_message(const struct mstdnt_message* message)
{
if (!message) return NULL;
HV* message_hv = newHV();
hvstores_str(message_hv, "account_id", message->account_id);
hvstores_str(message_hv, "chat_id", message->chat_id);
hvstores_str(message_hv, "id", message->id);
hvstores_str(message_hv, "content", message->content);
hvstores_int(message_hv, "created_at", message->created_at);
hvstores_ref(message_hv, "emojis", perlify_emojis(message->emojis, message->emojis_len));
hvstores_int(message_hv, "unread", message->unread);
return message_hv;
}
PERLIFY_MULTI(message, messages, mstdnt_message)

View file

@ -23,30 +23,13 @@
#include <mastodont.h>
#include "session.h"
char* construct_chat(mastodont_t* api,
struct mstdnt_args* m_args,
struct mstdnt_chat* chat,
size_t* size);
char* construct_chats(mastodont_t* api,
struct mstdnt_args* m_args,
struct mstdnt_chat* chats,
size_t size,
size_t* ret_size);
char* construct_chats_view(char* lists_string, size_t* size);
// Message
char* construct_message(struct mstdnt_message* message,
struct mstdnt_account* your_profile,
struct mstdnt_account* their_profile,
size_t* size);
char* construct_messages(struct mstdnt_message* message,
struct mstdnt_account* your_profile,
struct mstdnt_account* their_profile,
size_t size,
size_t* ret_size);
void content_chats(PATH_ARGS);
char* construct_chat_view(struct session* ssn, mastodont_t* api, char* id, size_t* len);
void content_chat_embed(PATH_ARGS);
void content_chat_view(PATH_ARGS);
AV* perlify_chats(const struct mstdnt_chat* chats, size_t chats_len);
HV* perlify_chat(const struct mstdnt_chat* chat);
AV* perlify_messages(const struct mstdnt_message* messages, size_t messages_len);
HV* perlify_message(const struct mstdnt_message* message);
#endif // LISTS_H

View file

@ -16,11 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <fcgi_stdio.h>
#include <fcgiapp.h>
#include "cookie.h"
#include <string.h>
#include <stdlib.h>
#include "cookie.h"
#include "env.h"
enum cookie_state
@ -31,7 +29,7 @@ enum cookie_state
STATE_V_START,
};
char* read_cookies_env(FCGX_Request* req, struct cookie_values* cookies)
char* read_cookies_env(REQUEST_T req, struct cookie_values* cookies)
{
struct http_cookie_info info;
char* cookies_env = GET_ENV("HTTP_COOKIE", req);
@ -164,3 +162,35 @@ int cookie_get_val(char* src, char* key, struct http_cookie_info* info)
return 1;
}
HV* perlify_cookies(struct cookie_values* cookies)
{
HV* ssn_cookies_hv = newHV();
hv_stores(ssn_cookies_hv, "lang", newSViv(keyint(cookies->lang)));
hv_stores(ssn_cookies_hv, "interact_img", newSViv(keyint(cookies->interact_img)));
hv_stores(ssn_cookies_hv, "themeclr", newSViv(keyint(cookies->themeclr)));
hv_stores(ssn_cookies_hv, "jsactions", newSViv(keyint(cookies->jsactions)));
hv_stores(ssn_cookies_hv, "jsreply", newSViv(keyint(cookies->jsreply)));
hv_stores(ssn_cookies_hv, "jslive", newSViv(keyint(cookies->jslive)));
hv_stores(ssn_cookies_hv, "js", newSViv(keyint(cookies->js)));
hv_stores(ssn_cookies_hv, "interact_img", newSViv(keyint(cookies->interact_img)));
hv_stores(ssn_cookies_hv, "statattachments", newSViv(keyint(cookies->stat_attachments)));
hv_stores(ssn_cookies_hv, "statgreentexts", newSViv(keyint(cookies->stat_greentexts)));
hv_stores(ssn_cookies_hv, "statdope", newSViv(keyint(cookies->stat_dope)));
hv_stores(ssn_cookies_hv, "statoneclicksoftware", newSViv(keyint(cookies->stat_oneclicksoftware)));
hv_stores(ssn_cookies_hv, "statemojolikes", newSViv(keyint(cookies->stat_emojo_likes)));
hv_stores(ssn_cookies_hv, "stathidemuted", newSViv(keyint(cookies->stat_hide_muted)));
hv_stores(ssn_cookies_hv, "instanceshowshoutbox", newSViv(keyint(cookies->instance_show_shoutbox)));
hv_stores(ssn_cookies_hv, "instancepanel", newSViv(keyint(cookies->instance_panel)));
hv_stores(ssn_cookies_hv, "notifembed", newSViv(keyint(cookies->notif_embed)));
hv_stores(ssn_cookies_hv, "access_token", newSVpv(keystr(cookies->access_token), 0));
hv_stores(ssn_cookies_hv, "logged_in", newSVpv(keystr(cookies->logged_in), 0));
hv_stores(ssn_cookies_hv, "theme", newSVpv(keystr(cookies->theme), 0));
hv_stores(ssn_cookies_hv, "instance_url", newSVpv(keystr(cookies->instance_url), 0));
hv_stores(ssn_cookies_hv, "background_url", newSVpv(keystr(cookies->background_url), 0));
hv_stores(ssn_cookies_hv, "client_id", newSVpv(keystr(cookies->client_id), 0));
hv_stores(ssn_cookies_hv, "client_secret", newSVpv(keystr(cookies->client_secret), 0));
return ssn_cookies_hv;
}

View file

@ -19,7 +19,10 @@
#ifndef COOKIE_H
#define COOKIE_H
#include <stddef.h>
#include "global_perl.h"
#include "key.h"
#include "cgi.h"
#include "request.h"
struct cookie_values
{
@ -58,7 +61,9 @@ struct http_cookie_info
// Stupidly fast simple cookie parser
char* parse_cookies(char* begin, struct http_cookie_info* info);
char* read_cookies_env(FCGX_Request* req, struct cookie_values* cookies);
char* read_cookies_env(REQUEST_T req, struct cookie_values* cookies);
int cookie_get_val(char* src, char* key, struct http_cookie_info* info);
HV* perlify_cookies(struct cookie_values* cookies);
#endif // COOKIE_H

View file

@ -16,19 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "emoji.h"
#include <stdlib.h>
#include <string.h>
#include "base_page.h"
#include "string.h"
#include "emoji.h"
#include "easprintf.h"
#include "string_helpers.h"
// Pages
#include "../static/emoji.ctmpl"
#include "../static/emoji_plain.ctmpl"
#include "../static/emoji_picker.ctmpl"
enum emoji_categories
{
EMO_CAT_SMILEYS,
@ -42,38 +37,6 @@ enum emoji_categories
EMO_CAT_LEN
};
char* emojify(char* content, struct mstdnt_emoji* emos, size_t emos_len)
{
if (!content) return NULL;
size_t sc_len;
char* oldres = NULL;
char* res = content;
char* emoji_url_str;
char* coloned;
for (size_t i = 0; i < emos_len; ++i)
{
oldres = res;
// Add colons around string
sc_len = strlen(emos[i].shortcode);
// 3 = \0 and two :
coloned = malloc(sc_len+3);
coloned[0] = ':';
strncpy(coloned + 1, emos[i].shortcode, sc_len);
coloned[sc_len+1] = ':';
coloned[sc_len+2] = '\0';
easprintf(&emoji_url_str, "<img class=\"emoji\" src=\"%s\" loading=\"lazy\">", emos[i].url);
res = strrepl(res, coloned, emoji_url_str, STRREPL_ALL);
if (oldres != content && res != oldres) free(oldres);
// Cleanup
free(emoji_url_str);
free(coloned);
}
return res;
}
struct construct_emoji_picker_args
{
char* status_id;
@ -84,21 +47,14 @@ char* construct_emoji(struct emoji_info* emoji, char* status_id, size_t* size)
{
if (!emoji)
return NULL;
char* emoji_str;
if (status_id)
{
struct emoji_template data = {
.status_id = status_id,
.emoji = emoji->codes
};
return tmpl_gen_emoji(&data, size);
}
else {
struct emoji_plain_template data = {
.emoji = emoji->codes
};
return tmpl_gen_emoji_plain(&data, size);
}
*size = easprintf(&emoji_str, "<a href=\"/status/%s/react/%s\" class=\"emoji\">%s</a>",
status_id, emoji->codes, emoji->codes);
else
*size = easprintf(&emoji_str, "<span class=\"emoji\">%s</span>", emoji->codes);
return emoji_str;
}
static char* construct_emoji_voidwrap(void* passed, size_t index, size_t* res)
@ -136,30 +92,48 @@ char* construct_emoji_picker(char* status_id, size_t* size)
};
char* emojis[EMO_CAT_LEN];
size_t len[EMO_CAT_LEN];
// TODO refactor to use #define lol
emojis[EMO_CAT_SMILEYS] = construct_func_strings(construct_emoji_voidwrap, args + EMO_CAT_SMILEYS, EMOJO_CAT_ANIMALS - EMOJO_CAT_SMILEY, NULL);
emojis[EMO_CAT_ANIMALS] = construct_func_strings(construct_emoji_voidwrap, args + EMO_CAT_ANIMALS, EMOJO_CAT_FOOD - EMOJO_CAT_ANIMALS, NULL);
emojis[EMO_CAT_FOOD] = construct_func_strings(construct_emoji_voidwrap, args + EMO_CAT_FOOD, EMOJO_CAT_TRAVEL - EMOJO_CAT_FOOD, NULL);
emojis[EMO_CAT_TRAVEL] = construct_func_strings(construct_emoji_voidwrap, args + EMO_CAT_TRAVEL, EMOJO_CAT_ACTIVITIES - EMOJO_CAT_TRAVEL, NULL);
emojis[EMO_CAT_ACTIVITIES] = construct_func_strings(construct_emoji_voidwrap, args + EMO_CAT_ACTIVITIES, EMOJO_CAT_OBJECTS - EMOJO_CAT_ACTIVITIES, NULL);
emojis[EMO_CAT_OBJECTS] = construct_func_strings(construct_emoji_voidwrap, args + EMO_CAT_OBJECTS, EMOJO_CAT_SYMBOLS - EMOJO_CAT_OBJECTS, NULL);
emojis[EMO_CAT_SYMBOLS] = construct_func_strings(construct_emoji_voidwrap, args + EMO_CAT_SYMBOLS, EMOJO_CAT_FLAGS - EMOJO_CAT_SYMBOLS, NULL);
emojis[EMO_CAT_FLAGS] = construct_func_strings(construct_emoji_voidwrap, args + EMO_CAT_FLAGS, EMOJO_CAT_MAX - EMOJO_CAT_FLAGS, NULL);
struct emoji_picker_template data = {
.emojis_smileys = emojis[EMO_CAT_SMILEYS],
.emojis_animals = emojis[EMO_CAT_ANIMALS],
.emojis_food = emojis[EMO_CAT_FOOD],
.emojis_travel = emojis[EMO_CAT_TRAVEL],
.emojis_activities = emojis[EMO_CAT_ACTIVITIES],
.emojis_objects = emojis[EMO_CAT_OBJECTS],
.emojis_symbols = emojis[EMO_CAT_SYMBOLS],
.emojis_flags = emojis[EMO_CAT_FLAGS],
};
emojis[EMO_CAT_SMILEYS] = construct_func_strings(construct_emoji_voidwrap, args + EMO_CAT_SMILEYS, EMOJO_CAT_ANIMALS - EMOJO_CAT_SMILEY, len);
emojis[EMO_CAT_ANIMALS] = construct_func_strings(construct_emoji_voidwrap, args + EMO_CAT_ANIMALS, EMOJO_CAT_FOOD - EMOJO_CAT_ANIMALS, len + 1);
emojis[EMO_CAT_FOOD] = construct_func_strings(construct_emoji_voidwrap, args + EMO_CAT_FOOD, EMOJO_CAT_TRAVEL - EMOJO_CAT_FOOD, len + 2);
emojis[EMO_CAT_TRAVEL] = construct_func_strings(construct_emoji_voidwrap, args + EMO_CAT_TRAVEL, EMOJO_CAT_ACTIVITIES - EMOJO_CAT_TRAVEL, len + 3);
emojis[EMO_CAT_ACTIVITIES] = construct_func_strings(construct_emoji_voidwrap, args + EMO_CAT_ACTIVITIES, EMOJO_CAT_OBJECTS - EMOJO_CAT_ACTIVITIES, len + 4);
emojis[EMO_CAT_OBJECTS] = construct_func_strings(construct_emoji_voidwrap, args + EMO_CAT_OBJECTS, EMOJO_CAT_SYMBOLS - EMOJO_CAT_OBJECTS, len + 5);
emojis[EMO_CAT_SYMBOLS] = construct_func_strings(construct_emoji_voidwrap, args + EMO_CAT_SYMBOLS, EMOJO_CAT_FLAGS - EMOJO_CAT_SYMBOLS, len + 6);
emojis[EMO_CAT_FLAGS] = construct_func_strings(construct_emoji_voidwrap, args + EMO_CAT_FLAGS, EMOJO_CAT_MAX - EMOJO_CAT_FLAGS, len + 7);
emoji_picker_html = tmpl_gen_emoji_picker(&data, size);
PERL_STACK_INIT;
XPUSHs(newRV_noinc((SV*)template_files));
AV* av = newAV();
av_extend(av, EMO_CAT_LEN);
for (int i = 0; i < EMO_CAT_LEN; ++i)
{
av_store(av, i, newSVpv(emojis[i], 0));
}
mXPUSHs(newRV_noinc((SV*)av));
PERL_STACK_SCALAR_CALL("emojis::emoji_picker");
char* dup = PERL_GET_STACK_EXIT;
// Cleanup
for (size_t i = 0; i < EMO_CAT_LEN; ++i)
free(emojis[i]);
return emoji_picker_html;
return dup;
}
HV* perlify_emoji(const struct mstdnt_emoji* const emoji)
{
if (!emoji) return NULL;
HV* emoji_hv = newHV();
hvstores_str(emoji_hv, "shortcode", emoji->shortcode);
hvstores_str(emoji_hv, "url", emoji->url);
hvstores_str(emoji_hv, "static_url", emoji->static_url);
hvstores_int(emoji_hv, "visible_in_picker", emoji->visible_in_picker);
hvstores_str(emoji_hv, "category", emoji->category);
return emoji_hv;
}
PERLIFY_MULTI(emoji, emojis, mstdnt_emoji)

View file

@ -20,7 +20,9 @@
#define EMOJI_H
#include <stddef.h>
#include <mastodont.h>
#include "global_perl.h"
#include "emoji_codes.h"
#include "path.h"
#define EMOJI_FACTOR_NUM 32
@ -31,9 +33,12 @@ enum emoji_picker_cat
EMOJI_CAT_FACES,
};
char* emojify(char* content, struct mstdnt_emoji* emos, size_t emos_len);
char* construct_emoji(struct emoji_info* emoji, char* status_id, size_t* size);
void content_emoji_picker(PATH_ARGS);
char* construct_emoji_picker(char* status_id, size_t* size);
// Perl
HV* perlify_emoji(const struct mstdnt_emoji* const emoji);
AV* perlify_emojis(const struct mstdnt_emoji* const emos, size_t len);
#endif // EMOJI_H

View file

@ -22,67 +22,17 @@
#include <stdlib.h>
#include "easprintf.h"
// Templates
#include "../static/custom_emoji_reaction.ctmpl"
#include "../static/emoji_reaction.ctmpl"
#include "../static/emoji_reactions.ctmpl"
struct construct_emoji_reactions_args
HV* perlify_emoji_reaction(const struct mstdnt_emoji_reaction* const emoji)
{
struct mstdnt_emoji_reaction* emojis;
char* id;
};
char* construct_emoji_reaction(char* id, struct mstdnt_emoji_reaction* emo, size_t* str_size)
{
char* ret;
char* emoji = emo->name;
if (emo->url)
{
struct custom_emoji_reaction_template c_data = {
.url = emo->url,
};
emoji = tmpl_gen_custom_emoji_reaction(&c_data, NULL);
}
struct emoji_reaction_template data = {
.prefix = config_url_prefix,
.status_id = id,
.custom_emoji = emo->url ? "custom-emoji-container" : NULL,
.emoji = emo->name,
.emoji_display = emoji,
.emoji_active = emo->me ? "active" : NULL,
.emoji_count = emo->count
};
ret = tmpl_gen_emoji_reaction(&data, str_size);
if (emoji != emo->name)
free(emoji);
return ret;
if (!emoji) return NULL;
HV* emoji_hv = newHV();
hvstores_str(emoji_hv, "name", emoji->name);
hvstores_str(emoji_hv, "url", emoji->url);
hvstores_str(emoji_hv, "static_url", emoji->static_url);
hvstores_int(emoji_hv, "count", emoji->count);
hvstores_int(emoji_hv, "me", emoji->me);
return emoji_hv;
}
static char* construct_emoji_reactions_voidwrap(void* passed, size_t index, size_t* res)
{
struct construct_emoji_reactions_args* args = passed;
return construct_emoji_reaction(args->id, args->emojis + index, res);
}
char* construct_emoji_reactions(char* id, struct mstdnt_emoji_reaction* emos, size_t emos_len, size_t* str_size)
{
size_t elements_size;
struct construct_emoji_reactions_args args = {
.emojis = emos,
.id = id
};
char* elements = construct_func_strings(construct_emoji_reactions_voidwrap, &args, emos_len, &elements_size);
char* emos_view;
struct emoji_reactions_template data = {
.emojis = elements
};
emos_view = tmpl_gen_emoji_reactions(&data, str_size);
// Cleanup
free(elements);
return emos_view;
}
PERLIFY_MULTI(emoji_reaction, emoji_reactions, mstdnt_emoji_reaction)

View file

@ -19,8 +19,10 @@
#ifndef EMOJI_REACTION_H
#define EMOJI_REACTION_H
#include <mastodont.h>
#include "global_perl.h"
char* construct_emoji_reaction(char* id, struct mstdnt_emoji_reaction* emo, size_t* str_len);
char* construct_emoji_reactions(char* id, struct mstdnt_emoji_reaction* emos, size_t emos_len, size_t* str_len);
// Perl
HV* perlify_emoji_reaction(const struct mstdnt_emoji_reaction* const emoji);
AV* perlify_emoji_reactions(const struct mstdnt_emoji_reaction* const emos, size_t len);
#endif // EMOJI_REACTION_H

View file

@ -21,46 +21,12 @@
#include "easprintf.h"
#include "l10n.h"
// Pages
#include "../static/error_404.ctmpl"
#include "../static/error.ctmpl"
char* construct_error(const char* error, enum error_type type, unsigned pad, size_t* size)
{
char* class;
switch (type)
{
case E_ERROR:
class = "error"; break;
case E_WARNING:
class = "warning"; break;
case E_NOTICE:
class = "notice"; break;
}
struct error_template data = {
.err_type = class,
.is_padded = pad ? "error-pad" : NULL,
.error = error ? error : "An error occured",
};
return tmpl_gen_error(&data, size);
}
void content_not_found(FCGX_Request* req, struct session* ssn, mastodont_t* api, char* path)
{
char* page;
struct error_404_template data = {
.error = L10N[L10N_EN_US][L10N_PAGE_NOT_FOUND]
};
page = tmpl_gen_error_404(&data, NULL);
struct base_page b = {
.content = page,
.content = "Content not found",
.sidebar_left = NULL
};
render_base_page(&b, req, ssn, api);
free(page);
}

View file

@ -23,14 +23,6 @@
#include "session.h"
#include "path.h"
enum error_type
{
E_ERROR,
E_WARNING,
E_NOTICE
};
char* construct_error(const char* error, enum error_type type, unsigned pad, size_t* size);
void content_not_found(FCGX_Request* req, struct session* ssn, mastodont_t* api, char* path);
#endif // ERROR_H

100
src/global_perl.c Normal file
View file

@ -0,0 +1,100 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "global_perl.h"
/* TODO let's generate this file dynamically with a Perl script? */
#include "../templates/main.ctt"
#include "../templates/notification.ctt"
#include "../templates/status.ctt"
#include "../templates/content_status.ctt"
#include "../templates/emoji_picker.ctt"
#include "../templates/attachment.ctt"
#include "../templates/emoji.ctt"
#include "../templates/postbox.ctt"
#include "../templates/timeline.ctt"
#include "../templates/account.ctt"
#include "../templates/account_statuses.ctt"
#include "../templates/account_scrobbles.ctt"
#include "../templates/content_notifs.ctt"
#include "../templates/content_lists.ctt"
#include "../templates/navigation.ctt"
#include "../templates/accounts.ctt"
#include "../templates/account_item.ctt"
#include "../templates/content_search.ctt"
#include "../templates/search_accounts.ctt"
#include "../templates/search_statuses.ctt"
#include "../templates/search_tags.ctt"
#include "../templates/search.ctt"
#include "../templates/content_chats.ctt"
#include "../templates/chat.ctt"
#include "../templates/config_general.ctt"
#include "../templates/config_appearance.ctt"
#include "../templates/embed.ctt"
#include "../templates/notifs_embed.ctt"
#include "../templates/about.ctt"
#include "../templates/license.ctt"
#include "../templates/login.ctt"
#include "../templates/status_interactions.ctt"
PerlInterpreter* my_perl;
HV* template_files;
pthread_mutex_t perllock_mutex = PTHREAD_MUTEX_INITIALIZER;
void init_template_files(pTHX)
{
template_files = newHV();
hv_stores(template_files, "main.tt", newSVpv(data_main_tt, data_main_tt_size));
hv_stores(template_files, "notification.tt", newSVpv(data_notification_tt, data_notification_tt_size));
hv_stores(template_files, "status.tt", newSVpv(data_status_tt, data_status_tt_size));
hv_stores(template_files, "content_status.tt", newSVpv(data_content_status_tt, data_content_status_tt_size));
hv_stores(template_files, "emoji_picker.tt", newSVpv(data_emoji_picker_tt, data_emoji_picker_tt_size));
hv_stores(template_files, "attachment.tt", newSVpv(data_attachment_tt, data_attachment_tt_size));
hv_stores(template_files, "emoji.tt", newSVpv(data_emoji_tt, data_emoji_tt_size));
hv_stores(template_files, "postbox.tt", newSVpv(data_postbox_tt, data_postbox_tt_size));
hv_stores(template_files, "timeline.tt", newSVpv(data_timeline_tt, data_timeline_tt_size));
hv_stores(template_files, "account.tt", newSVpv(data_account_tt, data_account_tt_size));
hv_stores(template_files, "account_statuses.tt", newSVpv(data_account_statuses_tt, data_account_statuses_tt_size));
hv_stores(template_files, "account_scrobbles.tt", newSVpv(data_account_scrobbles_tt, data_account_scrobbles_tt_size));
hv_stores(template_files, "content_notifs.tt", newSVpv(data_content_notifs_tt, data_content_notifs_tt_size));
hv_stores(template_files, "content_lists.tt", newSVpv(data_content_lists_tt, data_content_lists_tt_size));
hv_stores(template_files, "navigation.tt", newSVpv(data_navigation_tt, data_navigation_tt_size));
hv_stores(template_files, "accounts.tt", newSVpv(data_accounts_tt, data_accounts_tt_size));
hv_stores(template_files, "account_item.tt", newSVpv(data_account_item_tt, data_account_item_tt_size));
hv_stores(template_files, "content_search.tt", newSVpv(data_content_search_tt, data_content_search_tt_size));
hv_stores(template_files, "search.tt", newSVpv(data_search_tt, data_search_tt_size));
hv_stores(template_files, "search_accounts.tt", newSVpv(data_search_accounts_tt, data_search_accounts_tt_size));
hv_stores(template_files, "search_statuses.tt", newSVpv(data_search_statuses_tt, data_search_statuses_tt_size));
hv_stores(template_files, "search_tags.tt", newSVpv(data_search_tags_tt, data_search_tags_tt_size));
hv_stores(template_files, "content_chats.tt", newSVpv(data_content_chats_tt, data_content_chats_tt_size));
hv_stores(template_files, "chat.tt", newSVpv(data_chat_tt, data_chat_tt_size));
hv_stores(template_files, "config_general.tt", newSVpv(data_config_general_tt, data_config_general_tt_size));
hv_stores(template_files, "config_appearance.tt", newSVpv(data_config_appearance_tt, data_config_appearance_tt_size));
hv_stores(template_files, "embed.tt", newSVpv(data_embed_tt, data_embed_tt_size));
hv_stores(template_files, "notifs_embed.tt", newSVpv(data_notifs_embed_tt, data_notifs_embed_tt_size));
hv_stores(template_files, "about.tt", newSVpv(data_about_tt, data_about_tt_size));
hv_stores(template_files, "license.tt", newSVpv(data_license_tt, data_license_tt_size));
hv_stores(template_files, "login.tt", newSVpv(data_login_tt, data_login_tt_size));
hv_stores(template_files, "status_interactions.tt", newSVpv(data_status_interactions_tt, data_status_interactions_tt_size));
}
void cleanup_template_files()
{
hv_undef(template_files);
}

82
src/global_perl.h Normal file
View file

@ -0,0 +1,82 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#ifndef GLOBAL_PERL_H
#define GLOBAL_PERL_H
#include <EXTERN.h>
#include <perl.h>
#include <pthread.h>
// hv_stores(ssn_hv, "id", newSVpv(acct->id, 0));
// TODO use sv_usepvn_flags soon
#define hvstores_str(hv, key, val) if ((val)) { hv_stores((hv), key, newSVpvn((val), strlen(val))); }
#define hvstores_int(hv, key, val) hv_stores((hv), key, newSViv((val)))
#define hvstores_ref(hv, key, val) if (1) { \
SV* tmp = (SV*)(val); \
if (tmp) \
hv_stores((hv), key, newRV_noinc(tmp)); \
}
/* Seeing all this shit littered in Treebird's code made me decide to write some macros */
#define PERL_STACK_INIT perl_lock(); \
dSP; \
ENTER; \
SAVETMPS; \
PUSHMARK(SP)
#define PERL_STACK_SCALAR_CALL(name) PUTBACK; \
call_pv((name), G_SCALAR); \
SPAGAIN
/* you MUST assign scalar from savesharedsvpv, then free when done */
#define PERL_GET_STACK_EXIT savesvpv(POPs); \
PUTBACK; \
FREETMPS; \
LEAVE; \
perl_unlock()
#define PERLIFY_MULTI(type, types, mstype) AV* perlify_##types(const struct mstype* const types, size_t len) { \
if (!(types && len)) return NULL; \
AV* av = newAV(); \
av_extend(av, len-1); \
for (size_t i = 0; i < len; ++i) \
av_store(av, i, newRV_noinc((SV*)perlify_##type(types + i))); \
return av; \
}
extern PerlInterpreter* my_perl;
extern HV* template_files;
extern pthread_mutex_t perllock_mutex;
#ifndef SINGLE_THREADED
#define perl_lock() do { pthread_mutex_lock(&perllock_mutex); } while (0)
#define perl_unlock() do { pthread_mutex_unlock(&perllock_mutex); } while (0)
#else
// NOOP
#define perl_lock() ;;
#define perl_unlock() ;;
#endif
#define ARG_UNDEFINED() do { mXPUSHs(&PL_sv_undef); } while (0)
void init_template_files(pTHX);
void cleanup_template_files();
#endif /* GLOBAL_PERL_H */

View file

@ -1,121 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#include <string.h>
#include <time.h>
#include "graphsnbars.h"
#include "easprintf.h"
#include "string_helpers.h"
// Pages
#include "../static/bar.ctmpl"
#include "../static/bar_graph.ctmpl"
struct hashtags_graph_args
{
struct mstdnt_tag* tags;
size_t tags_len;
unsigned max;
time_t rel_day;
size_t days;
};
char* construct_bar_graph_container(char* bars, size_t* size)
{
struct bar_graph_template data = {
.graph = bars,
};
return tmpl_gen_bar_graph(&data, size);
}
char* construct_bar(float value, size_t* size)
{
struct bar_template data = {
.value = value * 100
};
return tmpl_gen_bar(&data, size);
}
static char* construct_hashgraph_voidwrap(void* passed, size_t index, size_t* res)
{
unsigned curr_sum = 0;
struct hashtags_graph_args* args = passed;
struct mstdnt_tag* tags = args->tags;
size_t tags_len = args->tags_len;
unsigned max = args->max;
time_t rel_day = args->rel_day;
size_t days = args->days;
for (int i = 0; i < tags_len; ++i)
{
for (int j = 0; j < tags[i].history_len; ++j)
{
if (tags[i].history[j].day == rel_day-((days-index-1)*86400))
curr_sum += tags[i].history[j].uses;
}
}
return construct_bar((float)curr_sum / max, res);
}
char* construct_hashtags_graph(struct mstdnt_tag* tags,
size_t tags_len,
size_t days,
size_t* ret_size)
{
unsigned max_sum = 0;
unsigned curr_sum = 0;
size_t max_history_len = 0;
// Get current time at midnight for basis, copy over
time_t t = time(NULL);
struct tm* mn_ptr = gmtime(&t);
struct tm mn;
memcpy(&mn, mn_ptr, sizeof(mn));
mn.tm_hour = 0;
mn.tm_min = 0;
mn.tm_sec = 0;
time_t rel_day = timegm(&mn);
// Run a loop through all the hashtags, sum each set up,
// then get the largest sum
for (size_t i = 0; i < days; ++i)
{
for (size_t j = 0; j < tags_len && i < tags[j].history_len; ++j)
{
if (tags[j].history_len > max_history_len)
max_history_len = tags[j].history_len;
if (tags[j].history[i].day >= rel_day-(i*86400))
curr_sum += tags[j].history[i].uses;
}
if (curr_sum > max_sum)
max_sum = curr_sum;
curr_sum = 0;
}
struct hashtags_graph_args args = {
.tags = tags,
.tags_len = tags_len,
.max = max_sum,
.rel_day = rel_day,
.days = max_history_len,
};
return construct_func_strings(construct_hashgraph_voidwrap, &args, max_history_len, ret_size);
}

View file

@ -1,32 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#ifndef GRAPHS_N_BARS_H
#define GRAPHS_N_BARS_H
#include <stddef.h>
#include <mastodont.h>
#include "session.h"
char* construct_bar_graph_container(char* bars, size_t* size);
char* construct_bar(float value, size_t* size);
char* construct_hashtags_graph(struct mstdnt_tag* tags,
size_t tags_len,
size_t days,
size_t* ret_size);
#endif /* GRAPHS_N_BARS_H */

View file

@ -22,44 +22,7 @@
#include "easprintf.h"
#include "../config.h"
// Pages
#include "../static/hashtag.ctmpl"
#include "../static/hashtag_page.ctmpl"
#define TAG_SIZE_INITIAL 12
static unsigned hashtag_history_daily_uses(size_t max, struct mstdnt_history* history, size_t history_len)
{
unsigned total = 0;
for (int i = 0; i < history_len && i < max; ++i)
total += history[i].uses;
return total;
}
char* construct_hashtag(struct mstdnt_tag* hashtag, size_t* size)
{
// Lol!
unsigned hash_size = TAG_SIZE_INITIAL +
CLAMP(hashtag_history_daily_uses(7, hashtag->history, hashtag->history_len)*2, 0, 42);
struct hashtag_template data = {
.prefix = config_url_prefix,
.tag = hashtag->name,
.tag_size = hash_size,
};
return tmpl_gen_hashtag(&data, size);
}
static char* construct_hashtag_voidwrap(void* passed, size_t index, size_t* res)
{
return construct_hashtag((struct mstdnt_tag*)passed + index, res);
}
char* construct_hashtags(struct mstdnt_tag* hashtags, size_t size, size_t* ret_size)
{
if (!(hashtags && size)) return NULL;
return construct_func_strings(construct_hashtag_voidwrap, hashtags, size, ret_size);
}

View file

@ -21,7 +21,6 @@
#include <stddef.h>
#include <mastodont.h>
char* construct_hashtag(struct mstdnt_tag* hashtag, size_t* size);
char* construct_hashtags(struct mstdnt_tag* hashtags, size_t size, size_t* ret_size);
// TODO?
#endif /* HASHTAG_H */

View file

@ -27,15 +27,14 @@
#define REDIR_HTML_END "</body>" \
"</html>"
void redirect(FCGX_Request* req, char* status, char* location)
void redirect(REQUEST_T req, char* status, char* location)
{
char* loc_str = location ? location : "/";
FCGX_FPrintF(req->out,
"Status: %s\r\n"
"Location: %s\r\n\r\n"
REDIR_HTML_BEGIN "Redirecting to <a href=\"\">%s</a>..." REDIR_HTML_END,
status,
loc_str,
loc_str);
PRINTF("Status: %s\r\n"
"Location: %s\r\n\r\n"
REDIR_HTML_BEGIN "Redirecting to <a href=\"\">%s</a>..." REDIR_HTML_END,
status,
loc_str,
loc_str);
}

View file

@ -18,11 +18,11 @@
#ifndef HTTP_H
#define HTTP_H
#include <fcgi_stdio.h>
#include <fcgiapp.h>
#include "request.h"
#include "cgi.h"
#define REDIRECT_303 "303 See Other"
void redirect(FCGX_Request* req, char* status, char* location);
void redirect(REQUEST_T req, char* status, char* location);
#endif // HTTP_H

View file

@ -17,6 +17,7 @@
*/
#include <stdlib.h>
#include "global_perl.h"
#include "helpers.h"
#include "base_page.h"
#include "../config.h"
@ -28,55 +29,14 @@
#include "lists.h"
#include "string_helpers.h"
#include "http.h"
// Files
#include "../static/account.ctmpl"
#include "../static/list.ctmpl"
#include "../static/lists.ctmpl"
char* construct_list(struct mstdnt_list* list, size_t* size)
{
char* result;
char* title = list->title;
char* list_name = sanitize_html(title);
struct list_template data = {
.list = list_name,
.prefix = config_url_prefix,
.list_id = list->id
};
result = tmpl_gen_list(&data, size);
if (list_name != title)
free(list_name);
return result;
}
static char* construct_list_voidwrap(void* passed, size_t index, size_t* res)
{
return construct_list((struct mstdnt_list*)passed + index, res);
}
char* construct_lists(struct mstdnt_list* lists, size_t size, size_t* ret_size)
{
return construct_func_strings(construct_list_voidwrap, lists, size, ret_size);
}
char* construct_lists_view(char* lists_string, size_t* size)
{
struct lists_template data = {
.lists = lists_string,
.prefix = config_url_prefix
};
return tmpl_gen_lists(&data, size);
}
void content_lists(PATH_ARGS)
{
struct mstdnt_args m_args;
set_mstdnt_args(&m_args, ssn);
struct mstdnt_list* lists = NULL;
size_t size_list = 0;
size_t lists_len = 0;
struct mstdnt_storage storage = { 0 };
char* lists_format = NULL;
char* lists_page = NULL;
if (ssn->post.title.is_set)
{
@ -89,20 +49,24 @@ void content_lists(PATH_ARGS)
mastodont_storage_cleanup(&create_storage);
}
if (mastodont_get_lists(api, &m_args, &storage, &lists, &size_list))
{
lists_page = construct_error(storage.error, E_ERROR, 1, NULL);
}
else {
lists_format = construct_lists(lists, size_list, NULL);
if (!lists_format)
lists_format = construct_error("No lists", E_ERROR, 1, NULL);
lists_page = construct_lists_view(lists_format, NULL);
}
mastodont_get_lists(api, &m_args, &storage, &lists, &lists_len);
PERL_STACK_INIT;
HV* session_hv = perlify_session(ssn);
XPUSHs(newRV_noinc((SV*)session_hv));
XPUSHs(newRV_noinc((SV*)template_files));
if (lists)
mXPUSHs(newRV_noinc((SV*)perlify_lists(lists, lists_len)));
PERL_STACK_SCALAR_CALL("lists::content_lists");
// Duplicate so we can free the TMPs
char* dup = PERL_GET_STACK_EXIT;
struct base_page b = {
.category = BASE_CAT_LISTS,
.content = lists_page,
.content = dup,
.session = session_hv,
.sidebar_left = NULL
};
@ -111,9 +75,8 @@ void content_lists(PATH_ARGS)
// Cleanup
mastodont_storage_cleanup(&storage);
if (lists_format) free(lists_format);
if (lists_page) free(lists_page);
mstdnt_cleanup_lists(lists);
Safefree(dup);
}
void list_edit(PATH_ARGS)
@ -139,3 +102,18 @@ void list_edit(PATH_ARGS)
redirect(req, REDIRECT_303, referer);
mastodont_storage_cleanup(&storage);
}
HV* perlify_list(const struct mstdnt_list* list)
{
if (!list) return NULL;
HV* list_hv = newHV();
hvstores_str(list_hv, "id", list->id);
hvstores_str(list_hv, "title", list->title);
// hvstores_int(list_hv, "replies_policy", list->replies_policy);
return list_hv;
}
PERLIFY_MULTI(list, lists, mstdnt_list)

View file

@ -22,10 +22,15 @@
#include <mastodont.h>
#include "session.h"
char* construct_list(struct mstdnt_list* list, size_t* size);
char* construct_lists(struct mstdnt_list* lists, size_t size, size_t* ret_size);
char* construct_lists_view(char* lists_string, size_t* size);
/** Creates the main lists view */
void content_lists(PATH_ARGS);
/** Creates a list and then redirects */
void list_edit(PATH_ARGS);
/** Converts list to perl hash */
HV* perlify_list(const struct mstdnt_list* list);
/** Converts lists to perl array */
AV* perlify_lists(const struct mstdnt_list* lists, size_t len);
#endif // LISTS_H

47
src/local_config.c Normal file
View file

@ -0,0 +1,47 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "global_perl.h"
#include "local_config.h"
HV* perlify_config(struct local_config* config)
{
HV* ssn_config_hv = newHV();
hv_stores(ssn_config_hv, "logged_in", newSVpv(config->logged_in, 0));
hv_stores(ssn_config_hv, "theme", newSVpv(config->theme, 0));
hv_stores(ssn_config_hv, "background_url", newSVpv(config->background_url, 0));
hv_stores(ssn_config_hv, "lang", newSViv(config->lang));
hv_stores(ssn_config_hv, "jsactions", newSViv(config->jsactions));
hv_stores(ssn_config_hv, "jslive", newSViv(config->jslive));
hv_stores(ssn_config_hv, "js", newSViv(config->js));
hv_stores(ssn_config_hv, "interact_img", newSViv(config->interact_img));
hv_stores(ssn_config_hv, "stat_attachments", newSViv(config->stat_attachments));
hv_stores(ssn_config_hv, "stat_greentexts", newSViv(config->stat_greentexts));
hv_stores(ssn_config_hv, "stat_dope", newSViv(config->stat_dope));
hv_stores(ssn_config_hv, "stat_oneclicksoftware", newSViv(config->stat_oneclicksoftware));
hv_stores(ssn_config_hv, "stat_emojo_likes", newSViv(config->stat_emojo_likes));
hv_stores(ssn_config_hv, "stat_hide_muted", newSViv(config->stat_hide_muted));
hv_stores(ssn_config_hv, "instance_show_shoutbox", newSViv(config->instance_show_shoutbox));
hv_stores(ssn_config_hv, "instance_panel", newSViv(config->instance_panel));
hv_stores(ssn_config_hv, "notif_embed", newSViv(config->notif_embed));
hv_stores(ssn_config_hv, "sidebar_opacity", newSViv(config->sidebar_opacity));
return ssn_config_hv;
}

View file

@ -18,6 +18,7 @@
#ifndef LOCAL_CONFIG_H
#define LOCAL_CONFIG_H
#include "global_perl.h"
#include "query.h"
struct local_config
@ -26,7 +27,6 @@ struct local_config
char* theme;
char* background_url;
int lang;
int themeclr;
int jsactions;
int jsreply;
int jslive;
@ -44,4 +44,6 @@ struct local_config
int sidebar_opacity;
};
HV* perlify_config(struct local_config* config);
#endif // LOCAL_CONFIG_H

View file

@ -34,9 +34,8 @@ void set_config_str(FCGX_Request* req,
{
if (ssn->post.set.is_set && post->is_set && page == curr_page)
{
FCGX_FPrintF(req->out,
"Set-Cookie: %s=%s; HttpOnly; Path=/; Max-Age=31536000; SameSite=Strict;\r\n",
cookie_name, keypstr(post));
PRINTF("Set-Cookie: %s=%s; HttpOnly; Path=/; Max-Age=31536000; SameSite=Strict;\r\n",
cookie_name, keypstr(post));
}
if ((ssn->post.set.is_set && post->is_set) || cookie->is_set)
@ -60,9 +59,8 @@ void set_config_int(FCGX_Request* req,
{
if (ssn->post.set.is_set && page == curr_page)
{
FCGX_FPrintF(req->out,
"Set-Cookie: %s=%d; HttpOnly; Path=/; Max-Age=31536000; SameSite=Strict;\r\n",
cookie_name, post_bool_intp(post));
PRINTF("Set-Cookie: %s=%d; HttpOnly; Path=/; Max-Age=31536000; SameSite=Strict;\r\n",
cookie_name, post_bool_intp(post));
}
// Checks if boolean option
@ -93,7 +91,6 @@ struct mstdnt_storage* load_config(FCGX_Request* req,
set_config_str(req, ssn, &(ssn->config.background_url), "background_url", &(atm), &(ssn->cookies.background_url), page, CONFIG_APPEARANCE);
set_config_int(LOAD_CFG_SIM("sidebaropacity", sidebar_opacity), CONFIG_APPEARANCE);
set_config_str(LOAD_CFG_SIM("theme", theme), CONFIG_APPEARANCE);
set_config_int(LOAD_CFG_SIM("themeclr", themeclr), CONFIG_APPEARANCE);
set_config_int(LOAD_CFG_SIM("jsactions", jsactions), CONFIG_GENERAL);
set_config_int(LOAD_CFG_SIM("jsreply", jsreply), CONFIG_GENERAL);
set_config_int(LOAD_CFG_SIM("jslive", jslive), CONFIG_GENERAL);

View file

@ -19,12 +19,11 @@
#ifndef LOCAL_CONFIG_SET_H
#define LOCAL_CONFIG_SET_H
#include <mastodont.h>
#include <fcgi_stdio.h>
#include <fcgiapp.h>
#include "local_config.h"
#include "session.h"
#include "attachments.h"
#include "local_config.h"
#include "key.h"
#include "cgi.h"
enum config_page
{

View file

@ -16,28 +16,26 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <curl/curl.h>
#include <fcgi_stdio.h>
#include "login.h"
#include <string.h>
#include <stdlib.h>
#include "helpers.h"
#include "query.h"
#include "base_page.h"
#include "login.h"
#include "error.h"
#include "easprintf.h"
#include "../config.h"
#include "http.h"
// Files
#include "../static/login.ctmpl"
#include <curl/curl.h>
#include "cgi.h"
#include "request.h"
#define LOGIN_SCOPE "read+write+follow+push"
void apply_access_token(FCGX_Request* req, char* token)
static void apply_access_token(REQUEST_T req, char* token)
{
FCGX_FPrintF(req->out, "Set-Cookie: access_token=%s; Path=/; Max-Age=31536000\r\n", token);
FCGX_FPrintF(req->out, "Set-Cookie: logged_in=t; Path=/; Max-Age=31536000\r\n");
PRINTF("Set-Cookie: access_token=%s; Path=/; Max-Age=31536000\r\n", token);
PUT("Set-Cookie: logged_in=t; Path=/; Max-Age=31536000\r\n");
// if config_url_prefix is empty, make it root
redirect(req, REDIRECT_303, config_url_prefix &&
config_url_prefix[0] != '\0' ? config_url_prefix : "/");
@ -102,9 +100,9 @@ void content_login_oauth(PATH_ARGS)
decode_url, encode_id, urlify_redirect_url);
// Set cookie and redirect
FCGX_FPrintF(req->out, "Set-Cookie: instance_url=%s; Path=/; Max-Age=3153600\r\n", decode_url);
FCGX_FPrintF(req->out, "Set-Cookie: client_id=%s; Path=/; Max-Age=3153600\r\n", app.client_id);
FCGX_FPrintF(req->out, "Set-Cookie: client_secret=%s; Path=/; Max-Age=3153600\r\n", app.client_secret);
PRINTF("Set-Cookie: instance_url=%s; Path=/; Max-Age=3153600\r\n", decode_url);
PRINTF("Set-Cookie: client_id=%s; Path=/; Max-Age=3153600\r\n", app.client_id);
PRINTF("Set-Cookie: client_secret=%s; Path=/; Max-Age=3153600\r\n", app.client_secret);
redirect(req, REDIRECT_303, url);
free(url);
@ -163,7 +161,7 @@ void content_login(PATH_ARGS)
if (mastodont_register_app(api, &m_args, &args_app, &storage, &app) != 0)
{
error = construct_error(oauth_store.error, E_ERROR, 1, NULL);
// error = construct_error(oauth_store.error, E_ERROR, 1, NULL);
}
else {
struct mstdnt_application_args args_token = {
@ -183,14 +181,14 @@ void content_login(PATH_ARGS)
&oauth_store,
&token) != 0 && oauth_store.error)
{
error = construct_error(oauth_store.error, E_ERROR, 1, NULL);
//error = construct_error(oauth_store.error, E_ERROR, 1, NULL);
}
else {
if (url_link)
FCGX_FPrintF(req->out, "Set-Cookie: instance_url=%s; Path=/; Max-Age=31536000\r\n", url_link);
PRINTF("Set-Cookie: instance_url=%s; Path=/; Max-Age=31536000\r\n", url_link);
else
// Clear
FCGX_FPrintF(req->out, "Set-Cookie: instance_url=; Path=/; Max-Age=-1\r\n");
PUT("Set-Cookie: instance_url=; Path=/; Max-Age=-1\r\n");
apply_access_token(req, token.access_token);
free(url_link);
@ -206,23 +204,21 @@ void content_login(PATH_ARGS)
}
}
// Concat
struct login_template tdata = {
.login_header = L10N[L10N_EN_US][L10N_LOGIN],
.error = error,
.prefix = config_url_prefix,
.username = L10N[L10N_EN_US][L10N_USERNAME],
.password = L10N[L10N_EN_US][L10N_PASSWORD],
.login_submit = L10N[L10N_EN_US][L10N_LOGIN_BTN],
.instance_text = "Or",
.instance_url = "Instance url",
.instance_submit = "Authorize"
};
page = tmpl_gen_login(&tdata, NULL);
PERL_STACK_INIT;
HV* session_hv = perlify_session(ssn);
XPUSHs(newRV_noinc((SV*)session_hv));
XPUSHs(newRV_noinc((SV*)template_files));
if (storage.error || oauth_store.error)
mXPUSHs(newSVpv(storage.error ? storage.error : oauth_store.error, 0));
PERL_STACK_SCALAR_CALL("login::content_login");
page = PERL_GET_STACK_EXIT;
struct base_page b = {
.category = BASE_CAT_NONE,
.content = page,
.session = session_hv,
.sidebar_left = NULL
};
@ -232,6 +228,5 @@ void content_login(PATH_ARGS)
// Cleanup
mastodont_storage_cleanup(&storage);
mastodont_storage_cleanup(&oauth_store);
if (error) free(error);
if (page) free(page);
Safefree(page);
}

View file

@ -16,9 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <EXTERN.h>
#include <perl.h>
#include "global_perl.h"
#include <pthread.h>
#include <fcgi_stdio.h>
#include <fcgiapp.h>
#include <string.h>
#include <mastodont.h>
#include <stdlib.h>
@ -37,16 +38,30 @@
#include "timeline.h"
#include "session.h"
#include "notifications.h"
#include "test.h"
#include "env.h"
#include "search.h"
#include "about.h"
#include "local_config_set.h"
#include "global_cache.h"
#include "conversations.h"
#include "request.h"
#include "cgi.h"
#define THREAD_COUNT 20
// Allow dynamic loading for Perl
static void xs_init (pTHX);
void boot_DynaLoader (pTHX_ CV* cv);
#ifdef DEBUG
static int quit = 0;
static void exit_treebird(PATH_ARGS)
{
quit = 1;
exit(1);
}
#endif
/*******************
* Path handling *
******************/
@ -57,7 +72,6 @@ static struct path_info paths[] = {
{ "/config", content_config },
{ "/login/oauth", content_login_oauth },
{ "/login", content_login },
{ "/test", content_test },
{ "/user/:/action/:", content_account_action },
{ "/user/:", content_account_statuses },
{ "/@:/scrobbles", content_account_scrobbles },
@ -95,24 +109,30 @@ static struct path_info paths[] = {
{ "/blocked", content_account_blocked },
{ "/muted", content_account_muted },
{ "/notifications_compact", content_notifications_compact },
{ "/notification/:/read", content_notifications_read },
{ "/notification/:/delete", content_notifications_clear },
{ "/notifications/read", content_notifications_read },
{ "/notifications/clear", content_notifications_clear },
{ "/notifications", content_notifications },
{ "/tag/:", content_tl_tag },
{ "/chats/:", content_chat_view },
{ "/chats", content_chats },
{ "/chats_embed/:", content_chat_embed },
#ifdef DEBUG
{ "/quit", exit_treebird },
{ "/exit", exit_treebird },
#endif
// API
{ "/treebird_api/v1/notifications", api_notifications },
{ "/treebird_api/v1/interact", api_status_interact },
{ "/treebird_api/v1/attachment", api_attachment_create },
};
static void application(mastodont_t* api, FCGX_Request* req)
static void application(mastodont_t* api, REQUEST_T req)
{
// Default config
struct session ssn = {
.config = {
.theme = "treebird20",
.themeclr = 0,
.lang = L10N_EN_US,
.jsactions = 1,
.jsreply = 1,
@ -166,7 +186,8 @@ static void application(mastodont_t* api, FCGX_Request* req)
cleanup_media_storages(&ssn, attachments);
}
static void* cgi_start(void* arg)
#ifndef SINGLE_THREADED
static void* threaded_fcgi_start(void* arg)
{
mastodont_t* api = arg;
int rc;
@ -189,12 +210,46 @@ static void* cgi_start(void* arg)
return NULL;
}
#else
void cgi_start(mastodont_t* api)
{
while (FCGI_Accept() >= 0 && quit == 0)
{
application(api, NULL);
}
}
#endif
int main(void)
void xs_init(pTHX)
{
static const char file[] = __FILE__;
dXSUB_SYS;
PERL_UNUSED_CONTEXT;
newXS("DynaLoader::boot_DynaLoader", boot_DynaLoader, file);
}
int main(int argc, char **argv, char **env)
{
// Global init
mastodont_global_curl_init();
#ifndef SINGLE_THREADED
FCGX_Init();
#endif
// Initialize Perl
PERL_SYS_INIT3(&argc, &argv, &env);
my_perl = perl_alloc();
perl_construct(my_perl);
//char* perl_argv[] = { "", "-e", data_main_pl, NULL };
char* perl_argv[] = { "", "-I", "perl/", "perl/main.pl", NULL };
perl_parse(my_perl, xs_init, (sizeof(perl_argv) / sizeof(perl_argv[0])) - 1, perl_argv, (char**)NULL);
PL_exit_flags |= PERL_EXIT_DESTRUCT_END;
PL_perl_destruct_level = 1;
perl_run(my_perl);
init_template_files(aTHX);
// Initiate mastodont library
mastodont_t api;
@ -202,17 +257,32 @@ int main(void)
// Fetch information about the current instance
load_instance_info_cache(&api);
#ifndef SINGLE_THREADED
// Start thread pool
pthread_t id[THREAD_COUNT];
for (unsigned i = 0; i < THREAD_COUNT; ++i)
pthread_create(&id[i], NULL, cgi_start, &api);
pthread_create(&id[i], NULL, threaded_fcgi_start, &api);
// Hell, let's not sit around here either
threaded_fcgi_start(&api);
FCGX_ShutdownPending();
for (unsigned i = 0; i < THREAD_COUNT; ++i)
pthread_join(id[i], NULL);
#else
cgi_start(&api);
#endif
free_instance_info_cache();
mastodont_cleanup(&api);
mastodont_global_curl_cleanup();
mastodont_cleanup(&api);
cleanup_template_files();
perl_destruct(my_perl);
perl_free(my_perl);
PERL_SYS_TERM();
return 0;
}

View file

@ -1,43 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#include <string.h>
#include "navigation.h"
#include "easprintf.h"
// Pages
#include "../static/navigation.ctmpl"
#define SUBMIT_HTML "<input type=\"submit\" class=\"hidden\">"
char* construct_navigation_box(char* start_id,
char* prev_id,
char* next_id,
size_t* size)
{
int is_start = start_id && prev_id ? strcmp(start_id, prev_id) == 0 : 0;
struct navigation_template tdata = {
.start_id = start_id,
.min_id = prev_id,
.prev_active = is_start ? "btn-disabled" : NULL,
.prev_submit = is_start ? "" : SUBMIT_HTML,
.max_id = next_id
};
return tmpl_gen_navigation(&tdata, size);
}

View file

@ -25,253 +25,76 @@
#include "base_page.h"
#include "string_helpers.h"
#include "easprintf.h"
#include "navigation.h"
#include "http.h"
#include "status.h"
#include "error.h"
#include "emoji.h"
#include "account.h"
#include "../config.h"
// Pages
#include "../static/notifications_page.ctmpl"
#include "../static/notifications.ctmpl"
#include "../static/notification_action.ctmpl"
#include "../static/notification.ctmpl"
#include "../static/notification_compact.ctmpl"
#include "../static/like_svg.ctmpl"
#include "../static/repeat_svg.ctmpl"
#include "../static/notifications_embed.ctmpl"
struct notification_args
{
struct session* ssn;
mastodont_t* api;
struct mstdnt_notification* notifs;
};
char* construct_notification(struct session* ssn,
mastodont_t* api,
struct mstdnt_notification* notif,
size_t* size)
{
char* notif_html;
if (notif->status)
{
// Construct status with notification_info
notif_html = construct_status(ssn, api, notif->status, size, notif, NULL, 0);
}
else {
notif_html = construct_notification_action(notif, size);
}
return notif_html;
}
char* construct_notification_action(struct mstdnt_notification* notif, size_t* size)
{
char* res;
char* serialized_display_name = sanitize_html(notif->account->display_name);
char* display_name = emojify(serialized_display_name,
notif->account->emojis,
notif->account->emojis_len);
struct notification_action_template tdata = {
.avatar = notif->account->avatar,
.acct = notif->account->acct,
.display_name = display_name,
.prefix = config_url_prefix,
.action = notification_type_compact_str(notif->type),
.notif_svg = notification_type_svg(notif->type)
};
res = tmpl_gen_notification_action(&tdata, size);
/* // Cleanup */
if (display_name != notif->account->display_name &&
display_name != serialized_display_name)
free(display_name);
if (serialized_display_name != notif->account->display_name)
free(serialized_display_name);
return res;
}
char* construct_notification_compact(struct session* ssn,
mastodont_t* api,
struct mstdnt_notification* notif,
size_t* size)
{
char* notif_html;
char* status_format = NULL;
char* notif_stats = NULL;
const char* type_str = notification_type_compact_str(notif->type);
const char* type_svg = notification_type_svg(notif->type);
if (notif->status)
{
if (notif->type == MSTDNT_NOTIFICATION_MENTION)
notif_stats = construct_interaction_buttons(ssn, notif->status, NULL,
STATUS_NO_LIKEBOOST | STATUS_NO_DOPAMEME);
status_format = reformat_status(ssn,
notif->status->content,
notif->status->emojis,
notif->status->emojis_len);
}
char* serialized_display_name = sanitize_html(notif->account->display_name);
char* display_name = emojify(serialized_display_name,
notif->account->emojis,
notif->account->emojis_len);
struct notification_compact_template tdata = {
.avatar = notif->account->avatar,
.has_icon = strlen(type_svg) == 0 ? "" : "-with-icon",
.acct = notif->account->acct,
.display_name = display_name,
.action = type_str,
.notif_svg = type_svg,
.is_status = (notif->type == MSTDNT_NOTIFICATION_STATUS ||
notif->type == MSTDNT_NOTIFICATION_MENTION ? "is-mention" : NULL),
/* Might show follower address */
.content = (notif->type == MSTDNT_NOTIFICATION_FOLLOW ?
notif->account->acct : status_format),
.stats = notif_stats
};
notif_html = tmpl_gen_notification_compact(&tdata, size);
if (status_format &&
status_format != notif->status->content) free(status_format);
if (notif_stats) free(notif_stats);
if (serialized_display_name != notif->account->display_name)
free(serialized_display_name);
if (display_name != notif->account->display_name &&
display_name != serialized_display_name)
free(display_name);
return notif_html;
}
static char* construct_notification_voidwrap(void* passed, size_t index, size_t* res)
{
struct notification_args* args = passed;
return construct_notification(args->ssn, args->api, args->notifs + index, res);
}
static char* construct_notification_compact_voidwrap(void* passed, size_t index, size_t* res)
{
struct notification_args* args = passed;
return construct_notification_compact(args->ssn, args->api, args->notifs + index, res);
}
char* construct_notifications(struct session* ssn,
mastodont_t* api,
struct mstdnt_notification* notifs,
size_t size,
size_t* ret_size)
{
struct notification_args args = {
.ssn = ssn,
.api = api,
.notifs = notifs
};
return construct_func_strings(construct_notification_voidwrap, &args, size, ret_size);
}
char* construct_notifications_compact(struct session* ssn,
mastodont_t* api,
struct mstdnt_notification* notifs,
size_t size,
size_t* ret_size)
{
struct notification_args args = {
.ssn = ssn,
.api = api,
.notifs = notifs
};
return construct_func_strings(construct_notification_compact_voidwrap,
&args,
size,
ret_size);
}
void content_notifications(PATH_ARGS)
{
struct mstdnt_args m_args;
set_mstdnt_args(&m_args, ssn);
char* page, *notif_html = NULL;
struct mstdnt_storage storage = { 0 };
struct mstdnt_notification* notifs = NULL;
size_t notifs_len = 0;
char* start_id;
char* navigation_box = NULL;
if (keystr(ssn->cookies.logged_in))
{
struct mstdnt_get_notifications_args args = {
.exclude_types = 0,
.account_id = NULL,
.exclude_visibilities = 0,
.include_types = 0,
.with_muted = 1,
.max_id = keystr(ssn->post.max_id),
.min_id = keystr(ssn->post.min_id),
.since_id = NULL,
.offset = 0,
.limit = 20,
};
if (mastodont_get_notifications(api, &m_args, &args, &storage, &notifs, &notifs_len) == 0)
{
if (notifs && notifs_len)
{
notif_html = construct_notifications(ssn, api, notifs, notifs_len, NULL);
start_id = keystr(ssn->post.start_id) ? keystr(ssn->post.start_id) : notifs[0].id;
navigation_box = construct_navigation_box(start_id,
notifs[0].id,
notifs[notifs_len-1].id,
NULL);
mstdnt_cleanup_notifications(notifs, notifs_len);
}
else
notif_html = construct_error("No notifications", E_NOTICE, 1, NULL);
}
else
notif_html = construct_error(storage.error, E_ERROR, 1, NULL);
}
struct notifications_page_template tdata = {
.notifications = notif_html,
.navigation = navigation_box
struct mstdnt_notifications_args args = {
.exclude_types = 0,
.account_id = NULL,
.exclude_visibilities = 0,
.include_types = 0,
.with_muted = 1,
.max_id = keystr(ssn->post.max_id),
.min_id = keystr(ssn->post.min_id),
.since_id = NULL,
.offset = 0,
.limit = 20,
};
page = tmpl_gen_notifications_page(&tdata, NULL);
if (keystr(ssn->cookies.logged_in))
mastodont_get_notifications(api, &m_args, &args, &storage, &notifs, &notifs_len);
PERL_STACK_INIT;
HV* session_hv = perlify_session(ssn);
mXPUSHs(newRV_inc((SV*)session_hv));
mXPUSHs(newRV_inc((SV*)template_files));
if (notifs)
mXPUSHs(newRV_noinc((SV*)perlify_notifications(notifs, notifs_len)));
// ARGS
PERL_STACK_SCALAR_CALL("notifications::content_notifications");
// Duplicate so we can free the TMPs
char* dup = PERL_GET_STACK_EXIT;
struct base_page b = {
.category = BASE_CAT_NOTIFICATIONS,
.content = page,
.content = dup,
.session = session_hv,
.sidebar_left = NULL
};
// Output
render_base_page(&b, req, ssn, api);
mastodont_storage_cleanup(&storage);
if (notif_html) free(notif_html);
if (navigation_box) free(navigation_box);
if (page) free(page);
mstdnt_cleanup_notifications(notifs, notifs_len);
Safefree(dup);
}
void content_notifications_compact(PATH_ARGS)
{
char* theme_str = NULL;
struct mstdnt_args m_args;
set_mstdnt_args(&m_args, ssn);
char* page, *notif_html = NULL;
char* page;
struct mstdnt_storage storage = { 0 };
struct mstdnt_notification* notifs = NULL;
size_t notifs_len = 0;
char* start_id = NULL;
char* navigation_box = NULL;
if (keystr(ssn->cookies.logged_in))
{
struct mstdnt_get_notifications_args args = {
struct mstdnt_notifications_args args = {
.exclude_types = 0,
.account_id = NULL,
.exclude_visibilities = 0,
@ -284,58 +107,99 @@ void content_notifications_compact(PATH_ARGS)
.limit = 20,
};
if (mastodont_get_notifications(api,
&m_args,
&args,
&storage,
&notifs,
&notifs_len) == 0)
{
if (notifs && notifs_len)
{
notif_html = construct_notifications_compact(ssn, api, notifs, notifs_len, NULL);
start_id = keystr(ssn->post.start_id) ? keystr(ssn->post.start_id) : notifs[0].id;
navigation_box = construct_navigation_box(start_id,
notifs[0].id,
notifs[notifs_len-1].id,
NULL);
mstdnt_cleanup_notifications(notifs, notifs_len);
}
else
notif_html = construct_error("No notifications", E_NOTICE, 1, NULL);
}
else
notif_html = construct_error(storage.error, E_ERROR, 1, NULL);
mastodont_get_notifications(api, &m_args, &args, &storage, &notifs, &notifs_len);
}
// Set theme
if (ssn->config.theme && !(strcmp(ssn->config.theme, "treebird") == 0 &&
ssn->config.themeclr == 0))
{
easprintf(&theme_str, "<link rel=\"stylesheet\" type=\"text/css\" href=\"/%s%s.css\">",
ssn->config.theme,
ssn->config.themeclr ? "-dark" : "");
}
size_t len;
struct notifications_embed_template tdata = {
.theme_str = theme_str,
.notifications = notif_html,
.navigation_box = navigation_box
};
PERL_STACK_INIT;
HV* session_hv = perlify_session(ssn);
mXPUSHs(newRV_noinc((SV*)session_hv));
mXPUSHs(newRV_inc((SV*)template_files));
if (notifs)
mXPUSHs(newRV_noinc((SV*)perlify_notifications(notifs, notifs_len)));
page = tmpl_gen_notifications_embed(&tdata, &len);
PERL_STACK_SCALAR_CALL("notifications::embed_notifications");
send_result(req, NULL, NULL, page, len);
page = PERL_GET_STACK_EXIT;
send_result(req, NULL, NULL, page, 0);
mastodont_storage_cleanup(&storage);
free(notif_html);
free(navigation_box);
free(page);
free(theme_str);
mstdnt_cleanup_notifications(notifs, notifs_len);
Safefree(page);
}
void content_notifications_clear(PATH_ARGS)
{
char* referer = GET_ENV("HTTP_REFERER", req);
struct mstdnt_args m_args;
set_mstdnt_args(&m_args, ssn);
struct mstdnt_storage storage = { 0 };
if (data)
{
mastodont_notification_dismiss(api, &m_args, &storage, data[0]);
}
else {
mastodont_notifications_clear(api, &m_args, &storage);
}
mastodont_storage_cleanup(&storage);
redirect(req, REDIRECT_303, referer);
}
void content_notifications_read(PATH_ARGS)
{
char* referer = GET_ENV("HTTP_REFERER", req);
struct mstdnt_args m_args;
set_mstdnt_args(&m_args, ssn);
struct mstdnt_storage storage = { 0 };
if (data)
{
struct mstdnt_notifications_args args = { .id = data[0] };
mastodont_notifications_read(api, &m_args, &args, &storage, NULL);
}
else {
struct mstdnt_notifications_args args = { .max_id = keystr(ssn->post.max_id) };
mastodont_notifications_read(api, &m_args, &args, &storage, NULL);
}
mastodont_storage_cleanup(&storage);
redirect(req, REDIRECT_303, referer);
}
// Converts it into a perl struct
static HV* perlify_notification_pleroma(struct mstdnt_notification_pleroma* notif)
{
if (!notif) return NULL;
HV* notif_pl_hv = newHV();
hvstores_int(notif_pl_hv, "is_muted", notif->is_muted);
hvstores_int(notif_pl_hv, "is_seen", notif->is_seen);
return notif_pl_hv;
}
// Converts it into a perl struct
HV* perlify_notification(const struct mstdnt_notification* const notif)
{
if (!notif) return NULL;
HV* notif_hv = newHV();
hvstores_str(notif_hv, "id", notif->id);
hvstores_int(notif_hv, "created_at", notif->created_at);
hvstores_str(notif_hv, "emoji", notif->emoji);
hvstores_str(notif_hv, "type", mstdnt_notification_t_to_str(notif->type));
hvstores_ref(notif_hv, "account", perlify_account(notif->account));
hvstores_ref(notif_hv, "pleroma", perlify_notification_pleroma(notif->pleroma));
hvstores_ref(notif_hv, "status", perlify_status(notif->status));
return notif_hv;
}
PERLIFY_MULTI(notification, notifications, mstdnt_notification)
void api_notifications(PATH_ARGS)
{
send_result(req, NULL, "application/json", "{\"status\":0}", 0);

View file

@ -19,36 +19,20 @@
#ifndef NOTIFICATIONS_H
#define NOTIFICATIONS_H
#include <mastodont.h>
#include <fcgi_stdio.h>
#include <fcgiapp.h>
#include "session.h"
#include "path.h"
#include "type_string.h"
char* construct_notification(struct session* ssn,
mastodont_t* api,
struct mstdnt_notification* notif,
size_t* size);
char* construct_notification_action(struct mstdnt_notification* notif, size_t* size);
char* construct_notification_compact(struct session* ssn,
mastodont_t* api,
struct mstdnt_notification* notif,
size_t* size);
char* construct_notifications(struct session* ssn,
mastodont_t* api,
struct mstdnt_notification* notifs,
size_t size,
size_t* ret_size);
char* construct_notifications_compact(struct session* ssn,
mastodont_t* api,
struct mstdnt_notification* notifs,
size_t size,
size_t* ret_size);
#include "global_perl.h"
#include "cgi.h"
// Page contents
void content_notifications(PATH_ARGS);
void content_notifications_compact(PATH_ARGS);
void content_notifications_clear(PATH_ARGS);
void content_notifications_read(PATH_ARGS);
void api_notifications(PATH_ARGS);
HV* perlify_notification(const struct mstdnt_notification* const notif);
AV* perlify_notifications(const struct mstdnt_notification* const notif, size_t len);
#endif // NOTIFICATION_H

View file

@ -16,24 +16,20 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <fcgi_stdio.h>
#include "page_config.h"
#include "global_perl.h"
#include <stdlib.h>
#include <string.h>
#include "http.h"
#include "base_page.h"
#include "../config.h"
#include "easprintf.h"
#include "page_config.h"
#include "query.h"
#include "cookie.h"
#include "local_config_set.h"
#include "string_helpers.h"
#include "l10n.h"
// Pages
#include "../static/config_general.ctmpl"
#include "../static/config_appearance.ctmpl"
#include "../static/config_sidebar.ctmpl"
#include <fcgi_stdio.h>
#define bool_checked(key) (ssn->config.key ? "checked" : "")
@ -44,69 +40,51 @@ enum config_category
CONFIG_CAT_ACCOUNT
};
static char* construct_config_sidebar(enum config_category cat, size_t* size)
{
struct config_sidebar_template tdata = {
.prefix = config_url_prefix,
.general_active = CAT_TEXT(cat, CONFIG_CAT_GENERAL),
.general = L10N[L10N_EN_US][L10N_GENERAL],
.appearance_active = CAT_TEXT(cat, CONFIG_CAT_APPEARANCE),
.appearance = L10N[L10N_EN_US][L10N_APPEARANCE],
.account_active = CAT_TEXT(cat, CONFIG_CAT_ACCOUNT),
.account = L10N[L10N_EN_US][L10N_ACCOUNT],
};
return tmpl_gen_config_sidebar(&tdata, size);
}
void content_config_general(PATH_ARGS)
{
char* sidebar_html = construct_config_sidebar(CONFIG_CAT_GENERAL, NULL);
struct config_general_template tdata = {
.js_on = bool_checked(js),
.jsactions_on = bool_checked(jsactions),
.jsreply_on = bool_checked(jsreply),
.jslive_on = bool_checked(jslive),
.status_attachments_on = bool_checked(stat_attachments),
.status_greentexts_on = bool_checked(stat_greentexts),
.status_dopameme_on = bool_checked(stat_dope),
.status_oneclicksoftware_on = bool_checked(stat_oneclicksoftware),
.status_emojo_likes_on = bool_checked(stat_emojo_likes),
.status_hide_muted_on = bool_checked(stat_hide_muted),
.instance_show_shoutbox_on = bool_checked(instance_show_shoutbox),
.instance_panel_on = bool_checked(instance_panel),
.notifications_embed_on = bool_checked(notif_embed)
};
char* general_page = tmpl_gen_config_general(&tdata, NULL);
PERL_STACK_INIT;
HV* session_hv = perlify_session(ssn);
XPUSHs(newRV_noinc((SV*)session_hv));
mXPUSHs(newRV_inc((SV*)template_files));
PERL_STACK_SCALAR_CALL("config::general");
char* dup = PERL_GET_STACK_EXIT;
struct base_page b = {
.category = BASE_CAT_CONFIG,
.content = general_page,
.sidebar_left = sidebar_html
.content = dup,
.session = session_hv,
.sidebar_left = NULL
};
render_base_page(&b, req, ssn, api);
// Cleanup
free(sidebar_html);
free(general_page);
Safefree(dup);
}
void content_config_appearance(PATH_ARGS)
{
char* sidebar_html = construct_config_sidebar(CONFIG_CAT_APPEARANCE, NULL);
PERL_STACK_INIT;
HV* session_hv = perlify_session(ssn);
XPUSHs(newRV_noinc((SV*)session_hv));
XPUSHs(newRV_noinc((SV*)template_files));
PERL_STACK_SCALAR_CALL("config::appearance");
char* dup = PERL_GET_STACK_EXIT;
struct base_page b = {
.category = BASE_CAT_CONFIG,
.content = data_config_appearance,
.sidebar_left = sidebar_html
.content = dup,
.session = session_hv,
.sidebar_left = NULL
};
render_base_page(&b, req, ssn, api);
// Cleanup
free(sidebar_html);
Safefree(dup);
}
void content_config(PATH_ARGS)

View file

@ -20,14 +20,13 @@
#define PAGE_CONFIG_H
#include <stddef.h>
#include <mastodont.h>
#include <fcgi_stdio.h>
#include <fcgiapp.h>
#include "path.h"
#include "session.h"
#include "cgi.h"
void content_config_appearance(PATH_ARGS);
void content_config_general(PATH_ARGS);
void content_config_account(PATH_ARGS);
//void content_config_account(PATH_ARGS);
void content_config(PATH_ARGS);
#endif // PAGE_CONFIG_H

View file

@ -23,7 +23,7 @@
#include "account.h"
#include "error.h"
int parse_path(FCGX_Request* req,
int parse_path(REQUEST_T req,
struct session* ssn,
mastodont_t* api,
struct path_info* path_info)
@ -101,7 +101,7 @@ breakpt:
return res;
}
void handle_paths(FCGX_Request* req,
void handle_paths(REQUEST_T req,
struct session* ssn,
mastodont_t* api,
struct path_info* paths,

View file

@ -18,14 +18,14 @@
#ifndef PATH_H
#define PATH_H
#include <fcgi_stdio.h>
#include <fcgiapp.h>
#include "session.h"
#include <mastodont.h>
#include <stddef.h>
#include "env.h"
#include "session.h"
#include "cgi.h"
#include "request.h"
#define PATH_ARGS FCGX_Request* req, struct session* ssn, mastodont_t* api, char** data
#define PATH_ARGS REQUEST_T req, struct session* ssn, mastodont_t* api, char** data
struct path_info
{
@ -33,13 +33,14 @@ struct path_info
void (*callback)(PATH_ARGS);
};
void handle_paths(FCGX_Request* req,
struct session* ssn,
mastodont_t* api,
struct path_info* paths,
size_t paths_len);
void handle_paths(
REQUEST_T req,
struct session* ssn,
mastodont_t* api,
struct path_info* paths,
size_t paths_len);
int parse_path(FCGX_Request* req,
int parse_path(REQUEST_T req,
struct session* ssn,
mastodont_t* api,
struct path_info* path_info);

View file

@ -16,17 +16,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <fcgi_stdio.h>
#include <fcgiapp.h>
#include "query.h"
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "query.h"
#include "env.h"
#include "mime.h"
#include "cgi.h"
char* read_get_data(FCGX_Request* req, struct get_values* query)
char* read_get_data(REQUEST_T req, struct get_values* query)
{
struct http_query_info info = { 0 };
char* query_string = GET_ENV("QUERY_STRING", req);
@ -37,6 +37,7 @@ char* read_get_data(FCGX_Request* req, struct get_values* query)
{ "offset", &(query->offset), key_string },
{ "q", &(query->query), key_string },
{ "code", &(query->code), key_string },
{ "type", &(query->type), key_int },
};
// END Query references
@ -75,7 +76,7 @@ char* read_get_data(FCGX_Request* req, struct get_values* query)
char* read_post_data(FCGX_Request* req, struct post_values* post)
char* read_post_data(REQUEST_T req, struct post_values* post)
{
ptrdiff_t begin_curr_size;
struct http_query_info query_info;
@ -142,7 +143,11 @@ char* read_post_data(FCGX_Request* req, struct post_values* post)
}
// fread should be a macro to FCGI_fread, which is set by FCGI_Accept in previous definitions
#ifndef SINGLE_THREADED
size_t len = FCGX_GetStr(post_query, content_length, req->in);
#else
size_t len = fread(post_query, 1, content_length, stdin);
#endif
post_query[content_length] = '\0';
// For shifting through
@ -211,7 +216,7 @@ char* parse_query(char* begin, struct http_query_info* info)
return end ? NULL : begin+1;
}
char* try_handle_post(FCGX_Request* req, void (*call)(struct http_query_info*, void*), void* arg)
char* try_handle_post(REQUEST_T req, void (*call)(struct http_query_info*, void*), void* arg)
{
char* request_method = GET_ENV("REQUEST_METHOD", req);
char* post_query = NULL, * p_query_read;
@ -228,10 +233,12 @@ char* try_handle_post(FCGX_Request* req, void (*call)(struct http_query_info*, v
return NULL;
}
#ifdef SINGLE_THREADED
read(STDIN_FILENO, post_query, content_length);
int size = read(STDIN_FILENO, post_query, content_length);
#else
FCGX_GetStr(post_query, content_length, req->in);
int size = FCGX_GetStr(post_query, content_length, req->in);
#endif
if (size != content_length)
return NULL;
post_query[content_length] = '\0';
@ -262,3 +269,61 @@ void free_files(struct file_array* files)
}
free(content);
}
// TODO use hvstores_XXX macros
HV* perlify_post_values(struct post_values* post)
{
HV* ssn_post_hv = newHV();
// This ugly...
hv_stores(ssn_post_hv, "theme", newSVpv(keystr(post->theme), 0));
hv_stores(ssn_post_hv, "themeclr", newSViv(keyint(post->themeclr)));
hv_stores(ssn_post_hv, "lang", newSViv(keyint(post->lang)));
hv_stores(ssn_post_hv, "title", newSViv(keyint(post->title)));
hv_stores(ssn_post_hv, "jsactions", newSViv(keyint(post->jsactions)));
hv_stores(ssn_post_hv, "jsreply", newSViv(keyint(post->jsreply)));
hv_stores(ssn_post_hv, "jslive", newSViv(keyint(post->jslive)));
hv_stores(ssn_post_hv, "js", newSViv(keyint(post->js)));
hv_stores(ssn_post_hv, "interact_img", newSViv(keyint(post->interact_img)));
hv_stores(ssn_post_hv, "stat_attachments", newSViv(keyint(post->stat_attachments)));
hv_stores(ssn_post_hv, "stat_greentexts", newSViv(keyint(post->stat_greentexts)));
hv_stores(ssn_post_hv, "stat_dope", newSViv(keyint(post->stat_dope)));
hv_stores(ssn_post_hv, "stat_oneclicksoftware", newSViv(keyint(post->stat_oneclicksoftware)));
hv_stores(ssn_post_hv, "stat_emojo_likes", newSViv(keyint(post->stat_emojo_likes)));
hv_stores(ssn_post_hv, "stat_hide_muted", newSViv(keyint(post->stat_hide_muted)));
hv_stores(ssn_post_hv, "instance_show_shoutbox", newSViv(keyint(post->instance_show_shoutbox)));
hv_stores(ssn_post_hv, "instance_panel", newSViv(keyint(post->instance_panel)));
hv_stores(ssn_post_hv, "notif_embed", newSViv(keyint(post->notif_embed)));
hv_stores(ssn_post_hv, "set", newSViv(keyint(post->set)));
hv_stores(ssn_post_hv, "only_media", newSViv(keyint(post->only_media)));
hv_stores(ssn_post_hv, "replies_only", newSViv(keyint(post->replies_only)));
hv_stores(ssn_post_hv, "replies_policy", newSViv(keyint(post->replies_policy)));
hv_stores(ssn_post_hv, "emojoindex", newSViv(keyint(post->emojoindex)));
hv_stores(ssn_post_hv, "sidebar_opacity", newSViv(keyint(post->sidebar_opacity)));
hv_stores(ssn_post_hv, "file_ids", newSVpv(keystr(post->file_ids), 0));
hv_stores(ssn_post_hv, "content", newSVpv(keystr(post->content), 0));
hv_stores(ssn_post_hv, "itype", newSVpv(keystr(post->itype), 0));
hv_stores(ssn_post_hv, "id", newSVpv(keystr(post->id), 0));
hv_stores(ssn_post_hv, "username", newSVpv(keystr(post->username), 0));
hv_stores(ssn_post_hv, "password", newSVpv(keystr(post->password), 0));
hv_stores(ssn_post_hv, "replyid", newSVpv(keystr(post->replyid), 0));
hv_stores(ssn_post_hv, "visibility", newSVpv(keystr(post->visibility), 0));
hv_stores(ssn_post_hv, "instance", newSVpv(keystr(post->instance), 0));
hv_stores(ssn_post_hv, "min_id", newSVpv(keystr(post->min_id), 0));
hv_stores(ssn_post_hv, "max_id", newSVpv(keystr(post->max_id), 0));
hv_stores(ssn_post_hv, "start_id", newSVpv(keystr(post->start_id), 0));
return ssn_post_hv;
}
HV* perlify_get_values(struct get_values* get)
{
HV* ssn_query_hv = newHV();
hv_stores(ssn_query_hv, "offset", newSVpv(keystr(get->offset), 0));
hv_stores(ssn_query_hv, "query", newSVpv(keystr(get->query), 0));
hv_stores(ssn_query_hv, "code", newSVpv(keystr(get->code), 0));
hvstores_int(ssn_query_hv, "type", keyint(get->type));
return ssn_query_hv;
}

View file

@ -18,9 +18,11 @@
#ifndef QUERY_H
#define QUERY_H
#include "global_perl.h"
#include <fcgi_stdio.h>
#include <stddef.h>
#include "key.h"
#include "request.h"
struct http_query_info
{
@ -79,14 +81,19 @@ struct get_values
struct key offset; // String
struct key query; // String
struct key code; // String
struct key type; // Int
};
char* read_get_data(FCGX_Request* req, struct get_values* query);
char* read_post_data(FCGX_Request* req, struct post_values* post);
char* read_get_data(REQUEST_T req, struct get_values* query);
char* read_post_data(REQUEST_T req, struct post_values* post);
/* A stupidly quick query parser */
char* parse_query(char* begin, struct http_query_info* info);
char* try_handle_post(FCGX_Request* req, void (*call)(struct http_query_info*, void*), void* arg);
char* try_handle_post(REQUEST_T req, void (*call)(struct http_query_info*, void*), void* arg);
void free_files(struct file_array* files);
// Perl stuff
HV* perlify_post_values(struct post_values* post);
HV* perlify_get_values(struct get_values* get);
#endif // QUERY_H

View file

@ -1,183 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#define PCRE2_CODE_UNIT_WIDTH 8
#include <pcre2.h>
#include <stdlib.h>
#include <string.h>
#include "reply.h"
#include "easprintf.h"
#include "../config.h"
// Pages
#include "../static/post.ctmpl"
#define ID_REPLY_SIZE 256
#define ID_RESPONSE "<input type=\"hidden\" name=\"replyid\" value=\"%s\">"
char* construct_post_box(struct mstdnt_status* reply_status,
char* default_content,
size_t* size)
{
#define C_S "checked"
#define D_S "disabled"
char* reply_html;
char id_reply[ID_REPLY_SIZE];
enum mstdnt_visibility_type vis = MSTDNT_VISIBILITY_PUBLIC;
// Put hidden post request and check visibility
if (reply_status)
{
snprintf(id_reply, ID_REPLY_SIZE, ID_RESPONSE, reply_status->id);
vis = reply_status->visibility;
}
/*
* Mastodont-c orders the visibility type from smallest (PUBLIC) to
* largest (LOCAL), so we take advantage of the enum values
*/
struct post_template tdata = {
.prefix = config_url_prefix,
.reply_input = reply_status ? id_reply : NULL,
.content = default_content,
.public_checked = vis == MSTDNT_VISIBILITY_PUBLIC ? C_S : NULL,
// You can reply with public to unlisted posts
.public_disabled = vis > MSTDNT_VISIBILITY_UNLISTED ? D_S : NULL,
.unlisted_checked = vis == MSTDNT_VISIBILITY_UNLISTED ? C_S : NULL,
.unlisted_disabled = vis > MSTDNT_VISIBILITY_UNLISTED ? D_S : NULL,
.private_checked = vis == MSTDNT_VISIBILITY_PRIVATE ? C_S : NULL,
.private_disabled = vis > MSTDNT_VISIBILITY_PRIVATE ? D_S : NULL,
.direct_checked = vis == MSTDNT_VISIBILITY_DIRECT ? C_S : NULL,
.local_checked = vis == MSTDNT_VISIBILITY_LOCAL ? C_S : NULL,
};
return tmpl_gen_post(&tdata, size);
}
/* Some comments:
* - Misskey does not return <span>, but we still regex to make sure it's a highlight
* - The order of parameters in a tag can be changed (mastodon does this),
* so we just grep for regex href
* - Misskey/Mastodon adds an @ symbol in the href param, while pleroma adds /users and honk adds /u
*/
#define REGEX_REPLY "<a .*?href=\"https?:\\/\\/(.*?)\\/(?:@|users/|u/)?(.*?)?\".*?>@(?:<span>)?.*?(?:<\\/span>)?"
char* reply_status(struct session* ssn, char* id, struct mstdnt_status* status)
{
char* content = status->content;
size_t content_len = strlen(status->content);
char* stat_reply;
// Regex
pcre2_code* re;
PCRE2_SIZE* re_results;
pcre2_match_data* re_data;
// Regex data
int rc;
int error;
PCRE2_SIZE erroffset;
int url_off, url_len, name_off, name_len;
// Replies
size_t replies_size = 0, replies_size_orig;
char* replies = NULL;
char* instance_domain = malloc(sizeof(config_instance_url)+sizeof("https:///")+1);
// sscanf instead of regex works here and requires less work, we just need to trim off the slash at the end
if (sscanf(config_instance_url, "https://%s/", instance_domain) == 0)
if (sscanf(config_instance_url, "http://%s/", instance_domain) == 0)
{
free(instance_domain);
return NULL;
}
instance_domain[strlen(instance_domain)] = '\0';
// Remove ports, if any. Needed for development or if
// the server actually supports these
char* port_val = strchr(instance_domain, ':');
if (port_val) *port_val = '\0';
// Load first reply
if (ssn->logged_in && strcmp(status->account.acct, ssn->acct.acct) != 0)
{
replies = malloc(replies_size = strlen(status->account.acct)+2);
replies[0] = '@';
strcpy(replies+1, status->account.acct);
replies[replies_size-1] = ' ';
}
// Compile regex
re = pcre2_compile((PCRE2_SPTR)REGEX_REPLY, PCRE2_ZERO_TERMINATED, 0, &error, &erroffset, NULL);
if (re == NULL)
{
fprintf(stderr, "Couldn't parse regex at offset %ld: %d\n", erroffset, error);
free(replies);
pcre2_code_free(re);
}
re_data = pcre2_match_data_create_from_pattern(re, NULL);
for (int ind = 0;;)
{
rc = pcre2_match(re, (PCRE2_SPTR)content, content_len, ind, 0, re_data, NULL);
if (rc < 0)
break;
re_results = pcre2_get_ovector_pointer(re_data);
// Store to last result
ind = re_results[5];
// Read out
url_off = re_results[2];
url_len = re_results[3] - url_off;
name_off = re_results[4];
name_len = re_results[5] - name_off;
int instance_cmp = strncmp(instance_domain, content+url_off, url_len);
// Is this the same as us?
// Cut off url_len by one to slice the '/' at the end
if (instance_cmp == 0 &&
strncmp(ssn->acct.acct, content+name_off, name_len) == 0)
continue;
replies_size_orig = replies_size;
replies_size += (instance_cmp!=0?url_len:0)+name_len+3-(instance_cmp==0); // Bool as int :^)
// Realloc string
replies = realloc(replies, replies_size+1);
replies[replies_size_orig] = '@';
memcpy(replies + replies_size_orig + 1, content + name_off, name_len);
if (instance_cmp != 0)
{
replies[replies_size_orig+1+name_len] = '@';
memcpy(replies + replies_size_orig + 1 + name_len + 1, content + url_off, url_len);
}
replies[replies_size-1] = ' ';
}
if (replies)
replies[replies_size-1] = '\0';
pcre2_match_data_free(re_data);
stat_reply = construct_post_box(status, replies, NULL);
pcre2_code_free(re);
free(replies);
free(instance_domain);
return stat_reply;
}

View file

@ -16,18 +16,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <stdio.h>
#include <stdlib.h>
#include "../test.h"
#ifndef REQUEST_H
#define REQUEST_H
// Imports
#include "mime_multipart.c"
#include "string_test.c"
int main()
{
struct test tests[] = {
{ "Mime multipart parser", mime_multipart_test },
{ "Strings", string_replace_test }
};
return iterate_tests(tests, sizeof(tests)/sizeof(tests[0]));
}
#ifdef SINGLE_THREADED
#define PRINTF(str, ...) printf(str, __VA_ARGS__)
#define PUT(str) printf(str)
#define REQUEST_T void*
#else
#define PRINTF(str, ...) FCGX_FPrintF(req->out, str, __VA_ARGS__)
#define PUT(str) FCGX_FPrintF(req->out, str)
#define REQUEST_T FCGX_Request*
#endif
#endif /* REQUEST_H */

View file

@ -19,35 +19,23 @@
#include "scrobble.h"
#include "easprintf.h"
#include "string_helpers.h"
#include "account.h"
#include "../static/scrobble.ctmpl"
char* construct_scrobble(struct mstdnt_scrobble* scrobble, size_t* size)
// Converts it into a perl struct
HV* perlify_scrobble(const struct mstdnt_scrobble* const scrobble)
{
struct scrobble_template tdata = {
.scrobble_id = scrobble->id,
.avatar = scrobble->account.avatar,
.username = scrobble->account.display_name,
.activity = "is listening to...",
.title_key = "Title",
.title = scrobble->title,
.artist_key = "Artist",
.artist = scrobble->artist,
.album_key = "Album",
.album = scrobble->album,
.length_key = "Duration",
.length = scrobble->length
};
if (!scrobble) return NULL;
HV* scrobble_hv = newHV();
return tmpl_gen_scrobble(&tdata, size);
hvstores_str(scrobble_hv, "album", scrobble->album);
hvstores_str(scrobble_hv, "artist", scrobble->artist);
hvstores_str(scrobble_hv, "id", scrobble->id);
hvstores_str(scrobble_hv, "title", scrobble->title);
hvstores_int(scrobble_hv, "created_at", scrobble->created_at);
hvstores_int(scrobble_hv, "length", scrobble->created_at);
hvstores_ref(scrobble_hv, "account", perlify_account(&(scrobble->account)));
return scrobble_hv;
}
static char* construct_scrobble_voidwrap(void* passed, size_t index, size_t* res)
{
return construct_scrobble((struct mstdnt_scrobble*)passed + index, res);
}
char* construct_scrobbles(struct mstdnt_scrobble* scrobbles, size_t scrobbles_len, size_t* ret_size)
{
return construct_func_strings(construct_scrobble_voidwrap, scrobbles, scrobbles_len, ret_size);
}
PERLIFY_MULTI(scrobble, scrobbles, mstdnt_scrobble)

View file

@ -19,8 +19,9 @@
#ifndef SCROBBLE_H
#define SCROBBLE_H
#include <mastodont.h>
#include "global_perl.h"
char* construct_scrobble(struct mstdnt_scrobble* scrobble, size_t* size);
char* construct_scrobbles(struct mstdnt_scrobble* scrobbles, size_t scrobbles_len, size_t* ret_size);
HV* perlify_scrobble(const struct mstdnt_scrobble* const scrobble);
AV* perlify_scrobbles(const struct mstdnt_scrobble* const scrobble, size_t len);
#endif /* SCROBBLE_H */

View file

@ -17,8 +17,9 @@
*/
#include <stdlib.h>
#include "helpers.h"
#include "search.h"
#include "http.h"
#include "helpers.h"
#include "easprintf.h"
#include "../config.h"
#include "string_helpers.h"
@ -27,54 +28,11 @@
#include "hashtag.h"
#include "error.h"
#include "account.h"
#include "graphsnbars.h"
// Pages
#include "../static/search.ctmpl"
#include "../static/search_all.ctmpl"
void search_page(FCGX_Request* req,
struct session* ssn,
mastodont_t* api,
enum search_tab tab,
char* content)
{
char* out_data;
struct search_template tdata = {
.prefix = config_url_prefix,
.query = keystr(ssn->query.query),
.accounts_active = MAKE_FOCUSED_IF(tab, SEARCH_ACCOUNTS),
.accounts = "Accounts",
.hashtags_active = MAKE_FOCUSED_IF(tab, SEARCH_HASHTAGS),
.hashtags = "Hashtags",
.statuses_active = MAKE_FOCUSED_IF(tab, SEARCH_STATUSES),
.statuses = "Statuses",
.results = content
};
out_data = tmpl_gen_search(&tdata, NULL);
struct base_page b = {
.category = BASE_CAT_NONE,
.content = out_data,
.sidebar_left = NULL
};
// Output
render_base_page(&b, req, ssn, api);
free(out_data);
}
void content_search_all(PATH_ARGS)
{
struct mstdnt_args m_args;
set_mstdnt_args(&m_args, ssn);
char* out_data = NULL;
char* statuses_html = NULL;
char* accounts_html = NULL;
char* tags_html = NULL,
* tags_graph = NULL,
* tags_bars = NULL,
* tags_page = NULL;
struct mstdnt_storage storage = { 0 };
struct mstdnt_search_args args = {
.account_id = NULL,
@ -90,72 +48,58 @@ void content_search_all(PATH_ARGS)
};
struct mstdnt_search_results results = { 0 };
if (mastodont_search(api,
&m_args,
keystr(ssn->query.query),
&storage,
&args,
&results) == 0)
// Perform redirect to correct direct page
if (keyint(ssn->query.type))
{
// Statuses, make sure to set the highlight word
struct construct_statuses_args statuses_args = {
.highlight_word = keystr(ssn->query.query),
};
statuses_html = construct_statuses(ssn, api, results.statuses, results.statuses_len, &statuses_args, NULL);
if (!statuses_html)
statuses_html = construct_error("No statuses", E_ERROR, 1, NULL);
// Accounts
accounts_html = construct_accounts(api, results.accts, results.accts_len, 0, NULL);
if (!accounts_html)
accounts_html = construct_error("No accounts", E_ERROR, 1, NULL);
// Hashtags
tags_html = construct_hashtags(results.tags, results.tags_len, NULL);
if (!tags_html)
tags_html = construct_error("No hashtags", E_ERROR, 1, NULL);
tags_bars = construct_hashtags_graph(results.tags,
results.tags_len,
14,
NULL);
if (tags_bars)
tags_graph = construct_bar_graph_container(tags_bars, NULL);
free(tags_bars);
char* query = keystr(ssn->query.query);
query = curl_easy_escape(api->curl, query, 0);
char* url;
// Note: This can be zero, which is just "nothing"
switch (keyint(ssn->query.type))
{
case 1:
easprintf(&url, "/search/statuses?q=%s", query);
redirect(req, REDIRECT_303, url);
break;
case 2:
easprintf(&url, "/search/accounts?q=%s", query);
redirect(req, REDIRECT_303, url);
break;
case 3:
easprintf(&url, "/search/hashtags?q=%s", query);
redirect(req, REDIRECT_303, url);
break;
}
free(url);
curl_free(query);
return;
}
easprintf(&tags_page, "%s%s", STR_NULL_EMPTY(tags_graph), tags_html);
mastodont_search(api, &m_args, keystr(ssn->query.query), &storage, &args, &results);
PERL_STACK_INIT;
HV* session_hv = perlify_session(ssn);
XPUSHs(newRV_noinc((SV*)session_hv));
XPUSHs(newRV_noinc((SV*)template_files));
mXPUSHs(newRV_noinc((SV*)perlify_search_results(&results)));
// Construct search page
struct search_all_template tdata = {
.accounts = "Accounts",
.hashtags = "Hashtags",
.statuses = "Statuses",
.statuses_results = statuses_html,
.hashtags_results = tags_page,
.accounts_results = accounts_html
};
out_data = tmpl_gen_search_all(&tdata, NULL);
PERL_STACK_SCALAR_CALL("search::content_search");
// Duplicate so we can free the TMPs
char* dup = PERL_GET_STACK_EXIT;
struct base_page b = {
.category = BASE_CAT_NONE,
.content = out_data,
.content = dup,
.session = session_hv,
.sidebar_left = NULL
};
// Output
render_base_page(&b, req, ssn, api);
free(out_data);
free(statuses_html);
free(accounts_html);
free(tags_html);
free(tags_graph);
free(tags_page);
mstdnt_cleanup_search_results(&results);
mastodont_storage_cleanup(&storage);
Safefree(dup);
}
void content_search_statuses(PATH_ARGS)
@ -178,35 +122,37 @@ void content_search_statuses(PATH_ARGS)
};
struct mstdnt_search_results results = { 0 };
if (mastodont_search(api,
&m_args,
keystr(ssn->query.query),
&storage,
&args,
&results) == 0)
{
struct construct_statuses_args statuses_args = {
.highlight_word = keystr(ssn->query.query),
};
statuses_html = construct_statuses(ssn, api, results.statuses, results.statuses_len, &statuses_args, NULL);
if (!statuses_html)
statuses_html = construct_error("No statuses", E_ERROR, 1, NULL);
}
else
statuses_html = construct_error("An error occured.", E_ERROR, 1, NULL);
mastodont_search(api, &m_args, keystr(ssn->query.query), &storage, &args, &results);
PERL_STACK_INIT;
HV* session_hv = perlify_session(ssn);
XPUSHs(newRV_noinc((SV*)session_hv));
XPUSHs(newRV_noinc((SV*)template_files));
mXPUSHs(newRV_noinc((SV*)perlify_search_results(&results)));
search_page(req, ssn, api, SEARCH_STATUSES, STR_NULL_EMPTY(statuses_html));
if (statuses_html) free(statuses_html);
PERL_STACK_SCALAR_CALL("search::content_search_statuses");
// Duplicate so we can free the TMPs
char* dup = PERL_GET_STACK_EXIT;
struct base_page b = {
.category = BASE_CAT_NONE,
.content = dup,
.session = session_hv,
.sidebar_left = NULL
};
render_base_page(&b, req, ssn, api);
mstdnt_cleanup_search_results(&results);
mastodont_storage_cleanup(&storage);
Safefree(dup);
}
void content_search_accounts(PATH_ARGS)
{
struct mstdnt_args m_args;
set_mstdnt_args(&m_args, ssn);
char* accounts_html;
struct mstdnt_storage storage = { 0 };
struct mstdnt_search_args args = {
.account_id = NULL,
@ -222,35 +168,37 @@ void content_search_accounts(PATH_ARGS)
};
struct mstdnt_search_results results = { 0 };
if (mastodont_search(api,
&m_args,
keystr(ssn->query.query),
&storage,
&args,
&results) == 0)
{
accounts_html = construct_accounts(api, results.accts, results.accts_len, 0, NULL);
if (!accounts_html)
accounts_html = construct_error("No accounts", E_ERROR, 1, NULL);
}
else
accounts_html = construct_error("An error occured.", E_ERROR, 1, NULL);
mastodont_search(api, &m_args, keystr(ssn->query.query), &storage, &args, &results);
search_page(req, ssn, api, SEARCH_ACCOUNTS, STR_NULL_EMPTY(accounts_html));
PERL_STACK_INIT;
HV* session_hv = perlify_session(ssn);
XPUSHs(newRV_noinc((SV*)session_hv));
XPUSHs(newRV_noinc((SV*)template_files));
mXPUSHs(newRV_noinc((SV*)perlify_search_results(&results)));
PERL_STACK_SCALAR_CALL("search::content_search_accounts");
// Duplicate so we can free the TMPs
char* dup = PERL_GET_STACK_EXIT;
struct base_page b = {
.category = BASE_CAT_NONE,
.content = dup,
.session = session_hv,
.sidebar_left = NULL
};
render_base_page(&b, req, ssn, api);
if (accounts_html) free(accounts_html);
mstdnt_cleanup_search_results(&results);
mastodont_storage_cleanup(&storage);
Safefree(dup);
}
void content_search_hashtags(PATH_ARGS)
{
struct mstdnt_args m_args;
set_mstdnt_args(&m_args, ssn);
char* tags_html;
char* tags_graph = NULL;
char* tags_bars = NULL;
char* tags_page;
struct mstdnt_storage storage = { 0 };
struct mstdnt_search_args args = {
.account_id = NULL,
@ -266,36 +214,23 @@ void content_search_hashtags(PATH_ARGS)
};
struct mstdnt_search_results results = { 0 };
if (mastodont_search(api,
&m_args,
keystr(ssn->query.query),
&storage,
&args,
&results) == 0)
{
tags_html = construct_hashtags(results.tags, results.tags_len, NULL);
if (!tags_html)
tags_html = construct_error("No hashtags", E_ERROR, 1, NULL);
tags_bars = construct_hashtags_graph(results.tags,
results.tags_len,
14,
NULL);
if (tags_bars)
tags_graph = construct_bar_graph_container(tags_bars, NULL);
if (tags_bars) free(tags_bars);
}
else
tags_html = construct_error("An error occured.", E_ERROR, 1, NULL);
easprintf(&tags_page, "%s%s", STR_NULL_EMPTY(tags_graph), tags_html);
mastodont_search(api, &m_args, keystr(ssn->query.query), &storage, &args, &results);
search_page(req, ssn, api, SEARCH_HASHTAGS, tags_page);
// TODO
if (tags_html) free(tags_html);
if (tags_graph) free(tags_graph);
free(tags_page);
mstdnt_cleanup_search_results(&results);
mastodont_storage_cleanup(&storage);
// Safefree(dup);
}
HV* perlify_search_results(struct mstdnt_search_results* results)
{
if (!results) return NULL;
HV* search_hv = newHV();
hvstores_ref(search_hv, "accounts", perlify_accounts(results->accts, results->accts_len));
hvstores_ref(search_hv, "statuses", perlify_statuses(results->statuses, results->statuses_len));
// TODO tags
return search_hv;
}

View file

@ -21,22 +21,12 @@
#include <mastodont.h>
#include "session.h"
#include "path.h"
#include "global_perl.h"
enum search_tab
{
SEARCH_STATUSES,
SEARCH_ACCOUNTS,
SEARCH_HASHTAGS,
};
void search_page(FCGX_Request* req,
struct session* ssn,
mastodont_t* api,
enum search_tab tab,
char* content);
void content_search_all(PATH_ARGS);
void content_search_statuses(PATH_ARGS);
void content_search_accounts(PATH_ARGS);
void content_search_hashtags(PATH_ARGS);
HV* perlify_search_results(struct mstdnt_search_results* results);
#endif /* SEARCH_H */

View file

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "account.h"
#include "session.h"
#include "../config.h"
@ -29,3 +30,23 @@ const char* const get_token(struct session* ssn)
{
return keystr(ssn->cookies.access_token);
}
HV* perlify_session(struct session* ssn)
{
HV* ssn_hv = newHV();
hvstores_int(ssn_hv, "logged_in", ssn->logged_in);
HV* ssn_post_values = perlify_post_values(&(ssn->post));
HV* ssn_get_values = perlify_get_values(&(ssn->query));
HV* ssn_cookie_values = perlify_cookies(&(ssn->cookies));
HV* acct_hv = perlify_account(&(ssn->acct));
// Config
HV* ssn_config = perlify_config(&(ssn->config));
hvstores_ref(ssn_hv, "config", ssn_config);
hvstores_ref(ssn_hv, "cookies", ssn_cookie_values);
hvstores_ref(ssn_hv, "query", ssn_get_values);
hvstores_ref(ssn_hv, "post", ssn_post_values);
hvstores_ref(ssn_hv, "account", acct_hv);
return ssn_hv;
}

View file

@ -19,6 +19,7 @@
#ifndef SESSION_H
#define SESSION_H
#include <mastodont.h>
#include "global_perl.h"
#include "query.h"
#include "local_config.h"
#include "cookie.h"
@ -36,5 +37,6 @@ struct session
const char* const get_instance(struct session* ssn);
const char* const get_token(struct session* ssn);
HV* perlify_session(struct session* ssn);
#endif // SESSION_H

File diff suppressed because it is too large Load diff

View file

@ -23,6 +23,7 @@
#include "l10n.h"
#include "path.h"
#include "session.h"
#include "global_perl.h"
// Flags
#define STATUS_NOOP 0
@ -52,64 +53,17 @@ void content_status_create(PATH_ARGS);
void content_status_react(PATH_ARGS);
// HTML Builders
char* construct_status(struct session* ssn,
mastodont_t* api,
struct mstdnt_status* status,
size_t* size,
struct mstdnt_notification* notif,
struct construct_statuses_args* args,
uint8_t flags);
char* construct_statuses(struct session* ssn,
mastodont_t* api,
struct mstdnt_status* statuses,
size_t size,
struct construct_statuses_args* args,
size_t* ret_size);
char* construct_interaction_buttons(struct session* ssn,
struct mstdnt_status* status,
size_t* size,
uint8_t flags);
// Reply to
/** Deprecated: May be used in the future for Mastodon only */
char* get_in_reply_to(mastodont_t* api,
struct session* ssn,
struct mstdnt_status* status,
size_t* size);
char* construct_in_reply_to(struct mstdnt_status* status,
struct mstdnt_account* account,
size_t* size);
char* construct_status_interactions(char* status_id,
int fav_count,
int reblog_count,
struct mstdnt_account* fav_accounts,
size_t fav_accounts_len,
struct mstdnt_account* reblog_accounts,
size_t reblog_accounts_len,
size_t* size);
char* construct_status_interaction_profiles(struct mstdnt_account* reblogs,
struct mstdnt_account* favourites,
size_t reblogs_len,
size_t favourites_len,
size_t* ret_size);
char* construct_status_interaction_profile(struct interact_profile_args* args, size_t index, size_t* size);
char* construct_status_interactions_label(char* status_id,
int is_favourites,
char* header,
int val,
size_t* size);
char* reformat_status(struct session* ssn,
char* content,
struct mstdnt_emoji* emos,
size_t emos_len);
char* greentextify(char* content);
char* make_mentions_local(char* content);
void status_view_reblogs(PATH_ARGS);
void status_view_favourites(PATH_ARGS);
const char* status_visibility_str(enum l10n_locale locale, enum mstdnt_visibility_type visibility);
void content_status_interactions(FCGX_Request* req,
struct session* ssn,
mastodont_t* api,
@ -134,4 +88,9 @@ void notice_redirect(PATH_ARGS);
// API
void api_status_interact(PATH_ARGS);
// Perl
HV* perlify_status_pleroma(const struct mstdnt_status_pleroma* pleroma);
HV* perlify_status(const struct mstdnt_status* status);
AV* perlify_statuses(const struct mstdnt_status* statuses, size_t len);
#endif // STATUS_H

View file

@ -1,295 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
// TODO error handling
enum args
{
ARG_FILENAME = 1,
ARG_VARIABLE
};
enum tmpl_type
{
TMPL_INT,
TMPL_UINT,
TMPL_STR,
TMPL_STRLEN,
TMPL_FLOAT,
};
struct tmpl_token
{
enum tmpl_type type;
char* token;
int used; // Internal use only
};
long filesize(FILE* file)
{
long orig = ftell(file);
fseek(file, 0, SEEK_END);
long size = ftell(file);
fseek(file, orig, SEEK_SET);
return size;
}
void chexput(const char* buf, size_t size)
{
for (size_t i = 0; i < size && buf; ++i)
{
printf("0X%hhX,", buf[i]);
}
}
char* strnws(char* str)
{
for (; isblank(*str); ++str);
return str;
}
char* strwsc(char* str, char stop)
{
for (; !isblank(*str) && *str != stop; ++str);
return str;
}
char* tkn_typetostr(enum tmpl_type tkn)
{
switch (tkn)
{
case TMPL_INT:
return "int";
case TMPL_STR:
return "const char*";
case TMPL_STRLEN:
return "char*";
case TMPL_UINT:
return "unsigned";
case TMPL_FLOAT:
return "float";
}
return "";
}
enum tmpl_type tkn_type(char* str)
{
if (strcmp(str, "string") == 0 ||
strcmp(str, "str") == 0 ||
strcmp(str, "%s") == 0)
return TMPL_STR;
else if (strcmp(str, "stringlen") == 0 ||
strcmp(str, "strlen") == 0 ||
strcmp(str, "%.s") == 0)
return TMPL_STRLEN;
else if (strcmp(str, "int") == 0 ||
strcmp(str, "i") == 0 ||
strcmp(str, "%d") == 0)
return TMPL_INT;
else if (strcmp(str, "unsigned") == 0 ||
strcmp(str, "uint") == 0 ||
strcmp(str, "%u") == 0)
return TMPL_UINT;
else if (strcmp(str, "float") == 0 ||
strcmp(str, "%f") == 0)
return TMPL_FLOAT;
// TODO Real error handling
return TMPL_INT;
}
char* parse_tmpl_token(char* buf, struct tmpl_token* tkn)
{
tkn->used = 0;
char* type_begin;
char* type_end;
char* tkn_begin;
char* tkn_end;
// skip {{
buf += 2;
type_begin = strnws(buf);
type_end = strwsc(type_begin, ':');
if (*type_end != ':') buf = strchr(buf, ':');
else buf = type_end;
*type_end = '\0';
tkn->type = tkn_type(type_begin);
++buf;
tkn_begin = strnws(buf);
tkn_end = strwsc(tkn_begin, '}');
if (*tkn_end == '}') buf = tkn_end + 2;
else buf = strstr(buf, "}}") + 2;
*tkn_end = '\0';
tkn->token = tkn_begin;
return buf;
}
void print_template(char* var, char* buf)
{
char* buf_prev = buf;
char* buf_curr = buf;
// Store result
struct tmpl_token* tokens = NULL;
size_t tokens_len = 0;
printf("#ifndef __%s\n"
"#define __%s\n"
"#include <stddef.h>\n"
"static const char data_%s[] = {", var, var, var);
while (1)
{
buf_curr = strstr(buf_curr, "{{");
if (!buf_curr) break;
// Create tokens array
tokens = realloc(tokens, sizeof(struct tmpl_token) * ++tokens_len);
if (!tokens)
{
perror("realloc");
break;
}
// Print up to this point
chexput(buf_prev, buf_curr - buf_prev);
buf_prev = buf_curr = parse_tmpl_token(buf_curr, tokens + (tokens_len-1));
// Print type
switch (tokens[tokens_len-1].type)
{
case TMPL_INT:
// I'm lazy so we'll use this
chexput("%d", 2);
break;
case TMPL_STR:
chexput("%s", 2);
break;
case TMPL_STRLEN:
chexput("%.s", 3);
break;
case TMPL_UINT:
chexput("%u", 2);
break;
case TMPL_FLOAT:
chexput("%f", 2);
break;
}
}
// Print remainder if any
chexput(buf_prev, strlen(buf_prev));
puts("0};");
// Only create struct and function when there are tokens detected
if (tokens_len)
{
printf("struct %s_template {", var);
int should_print = 0;
// Print tokens
for (size_t i = 0; i < tokens_len; ++i)
{
should_print = 1;
// Check if used
for (size_t j = 0; j < tokens_len; ++j)
{
if (i != j &&
strcmp(tokens[i].token, tokens[j].token) == 0 &&
tokens[j].used)
should_print = 0;
}
if (should_print)
{
printf("%s %s;\n", tkn_typetostr(tokens[i].type), tokens[i].token);
if (tokens[i].type == TMPL_STRLEN)
printf("size_t %s_len;\n", tokens[i].token);
tokens[i].used = 1;
}
}
// Generate function
printf("};\n");
printf("char* tmpl_gen_%s(struct %s_template* data, size_t* size);", var, var);
// Pipe the contents of the real function code into stderr, then we can redirect it
// We could also just write the file directly but this works better with the Makefile
// and I am lazy
fprintf(stderr, "#include \"%s.ctmpl\"\n"
"#include \"../src/easprintf.h\"\n"
"char* tmpl_gen_%s(struct %s_template* data, size_t* size){\n"
"char* ret;\n"
"size_t s = easprintf(&ret, data_%s, ", var, var, var, var);
for (size_t i = 0; i < tokens_len; ++i)
{
fprintf(stderr, "data->%s", tokens[i].token);
// No (null) strings, make them empty
if (tokens[i].type == TMPL_STR || tokens[i].type == TMPL_STRLEN)
fprintf(stderr, "?data->%s:\"\"", tokens[i].token);
fputs(i < tokens_len-1 ? ", " : "", stderr);
}
fputs(");\n"
"if (size) *size = s;\n"
"return ret;\n}", stderr);
}
// Done!
puts("\n#endif");
// Cleanup
free(tokens);
}
int main(int argc, char** argv)
{
char* buf;
FILE* file = fopen(argv[ARG_FILENAME], "rb");
long size = filesize(file);
if (!(buf = malloc(size)))
{
perror("malloc");
return 1;
}
if (fread(buf, 1, size, file) != size)
{
fputs("Didn't read correctly!", stderr);
free(buf);
return 1;
}
fclose(file);
buf[size-1] = '\0';
print_template(argv[ARG_VARIABLE], buf);
free(buf);
return 0;
}

View file

@ -1,80 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#include <stdlib.h>
#include <string.h>
#include "test.h"
#include "../config.h"
#include "base_page.h"
#include "easprintf.h"
// Pages
#include "../static/test.ctmpl"
#define ENV_NOT_FOUND "<span style=\"color:red;\">ENV Not Found</span>"
enum env_tbl_index
{
ENV_HTTP_COOKIE = 0,
ENV_PATH_INFO,
ENV_QUERY_STRING,
ENV_REQUEST_METHOD,
ENV_SCRIPT_NAME,
ENV_HTTP_REFERER,
ENV_HTTP_USER_AGENT,
ENV_CONTENT_LENGTH,
};
#define ENV_TBL_GET(index) (env_tbl[(index)] ? env_tbl[(index)] : ENV_NOT_FOUND)
void content_test(PATH_ARGS)
{
char* env_tbl[] = {
GET_ENV("HTTP_COOKIE", req),
GET_ENV("PATH_INFO", req),
GET_ENV("QUERY_STRING", req),
GET_ENV("REQUEST_METHOD", req),
GET_ENV("SCRIPT_NAME", req),
GET_ENV("HTTP_REFERER", req),
GET_ENV("HTTP_USER_AGENT", req),
GET_ENV("CONTENT_LENGTH", req)
};
char* page;
struct test_template tdata = {
.HTTP_COOKIE = ENV_TBL_GET(ENV_HTTP_COOKIE),
.PATH_INFO = ENV_TBL_GET(ENV_PATH_INFO),
.QUERY_STRING = ENV_TBL_GET(ENV_QUERY_STRING),
.REQUEST_METHOD = ENV_TBL_GET(ENV_REQUEST_METHOD),
.SCRIPT_NAME = ENV_TBL_GET(ENV_SCRIPT_NAME),
.HTTP_REFERER = ENV_TBL_GET(ENV_HTTP_REFERER),
.HTTP_USER_AGENT = ENV_TBL_GET(ENV_HTTP_USER_AGENT),
.CONTENT_LENGTH = ENV_TBL_GET(ENV_CONTENT_LENGTH)
};
page = tmpl_gen_test(&tdata, NULL);
struct base_page b = {
.category = BASE_CAT_NONE,
.content = page,
.sidebar_left = NULL
};
// Output
render_base_page(&b, req, ssn, api);
if (page) free(page);
}

View file

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "global_perl.h"
#include "timeline.h"
#include <stdlib.h>
#include "helpers.h"
@ -25,16 +26,11 @@
#include "index.h"
#include "status.h"
#include "easprintf.h"
#include "reply.h"
#include "navigation.h"
#include "query.h"
#include "error.h"
#include "string_helpers.h"
#include "../static/timeline_options.ctmpl"
#include "../static/navigation.ctmpl"
void content_timeline(FCGX_Request* req,
void content_timeline(REQUEST_T req,
struct session* ssn,
mastodont_t* api,
struct mstdnt_storage* storage,
@ -42,67 +38,34 @@ void content_timeline(FCGX_Request* req,
size_t statuses_len,
enum base_category cat,
char* header_text,
int show_post_box)
int show_post_box,
int fake_timeline)
{
size_t statuses_html_count = 0;
char* status_format = NULL,
* header = NULL,
* post_box = NULL,
* navigation_box = NULL,
* timeline_options,
* output = NULL,
* start_id;
if (storage->error)
status_format = construct_error(storage->error, E_ERROR, 1, NULL);
else
{
// Construct statuses into HTML
status_format = construct_statuses(ssn, api, statuses, statuses_len, NULL, &statuses_html_count);
if (!status_format)
status_format = construct_error("No statuses", E_NOTICE, 1, NULL);
}
// Create post box
if (show_post_box)
post_box = construct_post_box(NULL, "", NULL);
if (statuses)
{
// If not set, set it
start_id = keystr(ssn->post.start_id) ? keystr(ssn->post.start_id) : statuses[0].id;
navigation_box = construct_navigation_box(start_id,
statuses[0].id,
statuses[statuses_len-1].id,
NULL);
}
// Create timeline options/menubar
struct timeline_options_template todata = {
.only_media = "Only media?",
.replies = "Replies?",
.only_media_active = keyint(ssn->post.only_media) ? "checked" : NULL,
};
timeline_options = tmpl_gen_timeline_options(&todata, NULL);
// Display a header bar, usually customized for specific pages
if (header_text)
{
easprintf(&header, "<div class=\"simple-page simple-page-header\"><h1>%s</h1></div>",
header_text);
}
PERL_STACK_INIT;
HV* session_hv = perlify_session(ssn);
mXPUSHs(newRV_inc((SV*)session_hv));
mXPUSHs(newRV_inc((SV*)template_files));
easprintf(&output, "%s%s%s%s%s",
STR_NULL_EMPTY(header),
STR_NULL_EMPTY(post_box),
STR_NULL_EMPTY(timeline_options),
STR_NULL_EMPTY(status_format),
STR_NULL_EMPTY(navigation_box));
if (statuses)
mXPUSHs(newRV_noinc((SV*)perlify_statuses(statuses, statuses_len)));
else ARG_UNDEFINED();
if (header_text)
mXPUSHs(newSVpv(header_text, 0));
else ARG_UNDEFINED();
mXPUSHi(show_post_box);
mXPUSHi(fake_timeline);
PERL_STACK_SCALAR_CALL("timeline::content_timeline");
// Duplicate to free temps
char* dup = PERL_GET_STACK_EXIT;
struct base_page b = {
.category = cat,
.content = output,
.content = dup,
.session = session_hv,
.sidebar_left = NULL
};
@ -112,15 +75,10 @@ void content_timeline(FCGX_Request* req,
// Cleanup
mastodont_storage_cleanup(storage);
mstdnt_cleanup_statuses(statuses, statuses_len);
free(status_format);
free(post_box);
free(header);
free(timeline_options);
free(navigation_box);
free(output);
Safefree(dup);
}
void tl_home(FCGX_Request* req, struct session* ssn, mastodont_t* api, int local)
void tl_home(REQUEST_T req, struct session* ssn, mastodont_t* api, int local)
{
struct mstdnt_args m_args = { 0 };
set_mstdnt_args(&m_args, ssn);
@ -148,10 +106,10 @@ void tl_home(FCGX_Request* req, struct session* ssn, mastodont_t* api, int local
mastodont_timeline_home(api, &m_args, &args, &storage, &statuses, &statuses_len);
content_timeline(req, ssn, api, &storage, statuses, statuses_len, BASE_CAT_HOME, NULL, 1);
content_timeline(req, ssn, api, &storage, statuses, statuses_len, BASE_CAT_HOME, NULL, 1, 0);
}
void tl_direct(FCGX_Request* req, struct session* ssn, mastodont_t* api)
void tl_direct(REQUEST_T req, struct session* ssn, mastodont_t* api)
{
struct mstdnt_args m_args = { 0 };
set_mstdnt_args(&m_args, ssn);
@ -176,10 +134,10 @@ void tl_direct(FCGX_Request* req, struct session* ssn, mastodont_t* api)
mastodont_timeline_direct(api, &m_args, &args, &storage, &statuses, &statuses_len);
content_timeline(req, ssn, api, &storage, statuses, statuses_len, BASE_CAT_DIRECT, "Direct", 0);
content_timeline(req, ssn, api, &storage, statuses, statuses_len, BASE_CAT_DIRECT, "Direct", 0, 0);
}
void tl_public(FCGX_Request* req, struct session* ssn, mastodont_t* api, int local, enum base_category cat)
void tl_public(REQUEST_T req, struct session* ssn, mastodont_t* api, int local, enum base_category cat)
{
struct mstdnt_args m_args = { 0 };
set_mstdnt_args(&m_args, ssn);
@ -206,10 +164,10 @@ void tl_public(FCGX_Request* req, struct session* ssn, mastodont_t* api, int loc
mastodont_timeline_public(api, &m_args, &args, &storage, &statuses, &statuses_len);
content_timeline(req, ssn, api, &storage, statuses, statuses_len, cat, NULL, 1);
content_timeline(req, ssn, api, &storage, statuses, statuses_len, cat, NULL, 1, 0);
}
void tl_list(FCGX_Request* req, struct session* ssn, mastodont_t* api, char* list_id)
void tl_list(REQUEST_T req, struct session* ssn, mastodont_t* api, char* list_id)
{
struct mstdnt_args m_args;
set_mstdnt_args(&m_args, ssn);
@ -233,11 +191,11 @@ void tl_list(FCGX_Request* req, struct session* ssn, mastodont_t* api, char* lis
mastodont_timeline_list(api, &m_args, list_id, &args, &storage, &statuses, &statuses_len);
content_timeline(req, ssn, api, &storage, statuses, statuses_len, BASE_CAT_LISTS, "List timeline", 0);
content_timeline(req, ssn, api, &storage, statuses, statuses_len, BASE_CAT_LISTS, "List timeline", 0, 0);
}
void tl_tag(FCGX_Request* req, struct session* ssn, mastodont_t* api, char* tag_id)
void tl_tag(REQUEST_T req, struct session* ssn, mastodont_t* api, char* tag_id)
{
struct mstdnt_args m_args;
set_mstdnt_args(&m_args, ssn);
@ -262,7 +220,7 @@ void tl_tag(FCGX_Request* req, struct session* ssn, mastodont_t* api, char* tag_
easprintf(&header, "Hashtag - #%s", tag_id);
content_timeline(req, ssn, api, &storage, statuses, statuses_len, BASE_CAT_NONE, header, 0);
content_timeline(req, ssn, api, &storage, statuses, statuses_len, BASE_CAT_NONE, header, 0, 0);
free(header);
}

View file

@ -18,28 +18,67 @@
#ifndef TIMELINE_H
#define TIMELINE_H
#include <fcgi_stdio.h>
#include <fcgiapp.h>
#include <stddef.h>
#include <mastodont.h>
#include "path.h"
#include "session.h"
#include "base_page.h"
#include "cgi.h"
#include "request.h"
// Federated and local are here
void tl_home(FCGX_Request* req, struct session* ssn, mastodont_t* api, int local);
void tl_direct(FCGX_Request* req, struct session* ssn, mastodont_t* api);
void tl_public(FCGX_Request* req, struct session* ssn, mastodont_t* api, int local, enum base_category cat);
void tl_list(FCGX_Request* req, struct session* ssn, mastodont_t* api, char* list_id);
void tl_tag(FCGX_Request* req, struct session* ssn, mastodont_t* api, char* tag);
/** Wrapper for content_tl_federated */
void tl_home(REQUEST_T req, struct session* ssn, mastodont_t* api, int local);
/** Wrapper for content_tl_direct */
void tl_direct(REQUEST_T req, struct session* ssn, mastodont_t* api);
/** Wrapper for content_tl_federated */
void tl_public(REQUEST_T req, struct session* ssn, mastodont_t* api, int local, enum base_category cat);
/** Wrapper for content_tl_list */
void tl_list(REQUEST_T req, struct session* ssn, mastodont_t* api, char* list_id);
/** Wrapper for content_tl_tag */
void tl_tag(REQUEST_T req, struct session* ssn, mastodont_t* api, char* tag);
/* ------------------------------------------------ */
/** Federated timeline */
void content_tl_federated(PATH_ARGS);
/** Home timeline. Shows federated timeline if not logged in */
void content_tl_home(PATH_ARGS);
/** Direct message timeline */
void content_tl_direct(PATH_ARGS);
/** Local/instance timeline */
void content_tl_local(PATH_ARGS);
/** List timeline */
void content_tl_list(PATH_ARGS);
/** Hashtag timeline */
void content_tl_tag(PATH_ARGS);
void content_timeline(FCGX_Request* req,
/**
* Used to create generic timeline content. This timeline includes other features
* such as viewing only media, hiding muted, etc. as options on the top of the
* timeline, so this should only be used for API's which are considered "timelines"
* to Pleroma/Mastodon.
*
* @param req This request
* @param ssn This session
* @param api The api
* @param storage The storage for statuses, will be cleaned up in this function, do NOT
* cleanup yourself.
* @param statuses The statuses, will be cleaned up in this function, do NOT cleanup yourself.
* @param statuses_len Length of `statuses`
* @param cat The category to "highlight" on the sidebar
* @param header A header that is displayed above the timeline.
* @param show_post_box If the post box should be shown or not.
*/
void content_timeline(REQUEST_T req,
struct session* ssn,
mastodont_t* api,
struct mstdnt_storage* storage,
@ -47,6 +86,7 @@ void content_timeline(FCGX_Request* req,
size_t statuses_len,
enum base_category cat,
char* header,
int show_post_box);
int show_post_box,
int fake_timeline);
#endif // TIMELINE_H

View file

@ -1,67 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "type_string.h"
// Icons
#include "../static/like_svg.ctmpl"
#include "../static/repeat_svg.ctmpl"
#include "../static/follow_svg.ctmpl"
const char* notification_type_str(mstdnt_notification_t type)
{
switch (type)
{
case MSTDNT_NOTIFICATION_FOLLOW: return L10N[L10N_EN_US][L10N_NOTIF_FOLLOW];
case MSTDNT_NOTIFICATION_FOLLOW_REQUEST: return L10N[L10N_EN_US][L10N_NOTIF_FOLLOW_REQUEST];
case MSTDNT_NOTIFICATION_REBLOG: return L10N[L10N_EN_US][L10N_NOTIF_REPEATED];
case MSTDNT_NOTIFICATION_FAVOURITE: return L10N[L10N_EN_US][L10N_NOTIF_LIKED];
case MSTDNT_NOTIFICATION_POLL: return L10N[L10N_EN_US][L10N_NOTIF_POLL];
case MSTDNT_NOTIFICATION_EMOJI_REACT: return L10N[L10N_EN_US][L10N_NOTIF_REACTED_WITH];
default: return "";
}
}
const char* notification_type_compact_str(mstdnt_notification_t type)
{
switch (type)
{
case MSTDNT_NOTIFICATION_FOLLOW: return L10N[L10N_EN_US][L10N_NOTIF_COMPACT_FOLLOW];
case MSTDNT_NOTIFICATION_FOLLOW_REQUEST: return L10N[L10N_EN_US][L10N_NOTIF_COMPACT_FOLLOW_REQUEST];
case MSTDNT_NOTIFICATION_REBLOG: return L10N[L10N_EN_US][L10N_NOTIF_COMPACT_REPEATED];
case MSTDNT_NOTIFICATION_FAVOURITE: return L10N[L10N_EN_US][L10N_NOTIF_COMPACT_LIKED];
case MSTDNT_NOTIFICATION_POLL: return L10N[L10N_EN_US][L10N_NOTIF_COMPACT_POLL];
case MSTDNT_NOTIFICATION_EMOJI_REACT: return L10N[L10N_EN_US][L10N_NOTIF_COMPACT_REACTED_WITH];
default: return "";
}
}
const char* notification_type_svg(mstdnt_notification_t type)
{
switch (type)
{
case MSTDNT_NOTIFICATION_FOLLOW: return data_follow_svg;
case MSTDNT_NOTIFICATION_FOLLOW_REQUEST: return "";
case MSTDNT_NOTIFICATION_REBLOG: return data_repeat_svg;
case MSTDNT_NOTIFICATION_FAVOURITE: return data_like_svg;
case MSTDNT_NOTIFICATION_POLL: return "";
case MSTDNT_NOTIFICATION_EMOJI_REACT: return "";
default: return "";
}
}

View file

@ -1,28 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#ifndef TYPE_STRING_H
#define TYPE_STRING_H
#include <mastodont.h>
#include "l10n.h"
const char* notification_type_svg(mstdnt_notification_t type);
const char* notification_type_str(mstdnt_notification_t type);
const char* notification_type_compact_str(mstdnt_notification_t type);
#endif // TYPE_STRING_H

View file

@ -1,28 +0,0 @@
<div class="simple-page">
<div class="about-header">
<h1><img id="about-icon" src="/treebird_logo.png"> <span>Treebird</span></h1>
</div>
<div class="about-content">
<p>Treebird is a Pleroma frontend that is lightweight, efficient, and true to the web. It's written in C with FCGI making it simple and fast to work with and deploy. It is very tight to the philosophy of how the internet has always worked; Javascript provides extra sugar (or scripting) to improve the experience but not a requirement, while still being simple enough that anyone can use it.</p>
<p>Treebird was created in response to PleromaFE performance issues and ironically a lack of PleromaAPI backend support. Treebird resembles GNU Social in appearance by default, but keeps the familiarity that many are used to of PleromaFE.</p>
<p>Treebird was created by <a href="/@nekobit@rdrama.cc">Nekobit</a>, who created <a href="https://fossil.nekobit.net/mastodont-c/home">mastodont-c</a>, a library that can communicate with Revolver's, Pleroma's, and Mastodon's REST APIs.</p>
<h3>Other contributors</h3>
<ul>
<li><a href="/@coyote@pl.lain.sh" alt="Created Solarized themes for Treebird">Coyote</a></li>
<li><a href="/@grumbulon@freecumextremist.com" alt="Helped with the original dark theme, and created the original treebird.dev website">Grumbulon</a></li>
<li><a href="/@khan@sleepy.cafe" alt="Started idea of treebird">Khan</a></li>
<li><a href="/@sam@froth.zone" alt="Testing with Nginx and documentation">SamTherapy</a></li>
<li><a href="/@pch_xyz@seediqbale.xyz" alt="Chinese (Traditional) translations">Pacific Coast Highway (pch_xyz)</a></li>
</ul>
<p>Treebird is licensed in AGPLv3, and mastodont-c is LGPLv3 licensed. Other licenses apply to libraries as well. <a href="/about/license">View the AGPLv3 license here.</a></p>
<a class="btn btn-single" href="https://fossil.nekobit.net/treebird/home">View the Fossil Repository</a> <a class="btn btn-single" href="/about/license">View the License</a>
</div>
</div>
<!-- Scripts -->
<script src="/js/worm.js"></script>

View file

@ -1,67 +0,0 @@
{{%s:is_blocked}}
{{%s:menubar}}
<div class="account">
<div class="acct-banner" style="background-image:url('{{%s:header}}');">
{{%s:follows_you}}
<div class="acct-info-data">
<span class="acct-displayname">{{%s:display_name}}</span>
<span class="acct-username">{{%s:acct}}</span>
</div>
<span class="menu-container user-options-btn">
Menu
<div class="menu menu-options">
<ul>
<li><a class="nolink" href="{{%s:prefix}}/user/{{%s:userid}}/action/{{%s:unsubscribe}}subscribe"><input class="btn-menu" type="button" value="{{%s:subscribe_text}}"></a></li>
<li><a class="nolink" href="{{%s:prefix}}/user/{{%s:userid}}/action/{{%s:unblock}}block"><input class="btn-menu" type="button" value="{{%s:block_text}}"></a></li>
<li><a class="nolink" href="{{%s:prefix}}/user/{{%s:userid}}/action/{{%s:unmute}}mute"><input class="btn-menu" type="button" value="{{%s:mute_text}}"></a></li>
</ul>
</div>
</span>
</div>
<div class="acct-header">
<a href="{{%s:prefix}}/@{{%s:acct}}" class="header-btn btn">
<span class="btn-header">{{%s:tab_statuses_text}}</span>
<span class="btn-content">{{%d:statuses_count}}</span>
</a>
<a href="{{%s:prefix}}/@{{%s:acct}}/following" class="header-btn btn">
<span class="btn-header">{{%s:tab_following_text}}</span>
<span class="btn-content">{{%d:following_count}}</span>
</a>
<a href="{{%s:prefix}}/@{{%s:acct}}/followers" class="header-btn btn">
<span class="btn-header">{{%s:tab_followers_text}}</span>
<span class="btn-content">{{%d:followers_count}}</span>
</a>
{{%s:follow_btn}}
</div>
<div class="acct-pfp-wrapper">
<img class="acct-pfp" src="{{%s:avatar}}">
</div>
</div>
{{%s:info}}
<table class="tabs ui-table">
<tr>
<td>
<a href="{{%s:prefix}}/@{{%s:acct}}/statuses"><input class="tab-btn btn {{%s:tab_statuses_focused}}" type="button" value="{{%s:tab_statuses_text}}"></a>
</td>
<td>
<a href="{{%s:prefix}}/@{{%s:acct}}/scrobbles"><input class="tab-btn btn {{%s:tab_scrobbles_focused}}" type="button" value="{{%s:tab_scrobbles_text}}"></a>
</td>
<td>
<a href="{{%s:prefix}}/@{{%s:acct}}/media"><input class="tab-btn btn {{%s:tab_media_focused}}" type="button" value="{{%s:tab_media_text}}"></a>
</td>
<td>
<a href="{{%s:prefix}}/@{{%s:acct}}/pinned"><input class="tab-btn btn {{%s:tab_pinned_focused}}" type="button" value="{{%s:tab_pinned_text}}"></a>
</td>
</tr>
</table>
<div class="account-content">
{{%s:acct_content}}
</div>

View file

@ -1,7 +0,0 @@
<div class="menubar">
<a href="{{%s:prefix}}/blocked">{{ %s : blocked_str }}</a>
<span class="bullet-separate">&bull;</span>
<a href="{{%s:prefix}}/muted">{{ %s : muted_str }}</a>
<span class="bullet-separate">&bull;</span>
<a href="{{%s:prefix}}/favourites">{{ %s : favourited_str }}</a>
</div>

View file

@ -1,3 +0,0 @@
<a href="{{%s:prefix}}/user/{{%s:userid}}/action/{{%s:unfollow}}follow" class="follow-btn btn {{%s:active}}">
{{%s:follow_text}}
</a>

View file

@ -1,3 +0,0 @@
<div class="account-info">
<div class="account-note">{{%s:acct_note}}</div>
</div>

View file

@ -1,36 +0,0 @@
<div class="account-sidebar" {{ %s : header }}>
<table class="acct-info">
<tr>
<td>
<img src="{{%s:avatar}}" class="acct-pfp" loading="lazy">
</td>
<td class="acct-info-right">
<span class="username">{{%s:username}}</span>
<span class="acct">@<span class="acct-js-grep">{{%s:acct}}</span></span>
</td>
</tr>
</table>
<table class="acct-stats">
<tr>
<td class="header-btn btn">
<a href="{{%s:prefix}}/@{{%s:acct}}">
<span class="btn-header">{{%s:statuses_text}}</span>
<span class="btn-content">{{%d:statuses_count}}</span>
</a>
</td>
<td class="header-btn btn">
<a href="{{%s:prefix}}/@{{%s:acct}}/following">
<span class="btn-header">{{%s:following_text}}</span>
<span class="btn-content">{{%d:following_count}}</span>
</a>
</td>
<td class="header-btn btn">
<a href="{{%s:prefix}}/@{{%s:acct}}/followers">
<span class="btn-header">{{%s:followers_text}}</span>
<span class="btn-content">{{%d:followers_count}}</span>
</a>
</td>
</tr>
</table>
</div>

View file

@ -1,7 +0,0 @@
<div class="attachment-container attachment-audio">
<!-- Here even if not sensitive -->
<div class="sensitive-placeholder {{%s:sensitive}}"></div>
<audio width="256" controls preload="metadata">
<source src="{{%s:src}}">
</video>
</div>

View file

@ -1,7 +0,0 @@
<div class="attachment-container attachment-gifv">
<video width="256" autoplay muted>
<source src="{{%s:src}}">
[ GIFV ]
</video>
{{%s:sensitive}}
</div>

Some files were not shown because too many files have changed in this diff Show more