diff --git a/.fossil-settings/ignore-glob b/.fossil-settings/ignore-glob index cab4136..97604c9 100644 --- a/.fossil-settings/ignore-glob +++ b/.fossil-settings/ignore-glob @@ -1,4 +1,5 @@ template +ctemplate filec emojitoc **/*.cgi @@ -9,4 +10,6 @@ mastodont-c config.h treebird test/tests -scripts/*.o \ No newline at end of file +scripts/*.o +templates/*.ctt +test/unit/*.bin \ No newline at end of file diff --git a/Makefile b/Makefile index 9a32a50..df99da1 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 50ba5b6..3ac5faf 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/config.def.h b/config.def.h index a5eed39..34e65c0 100644 --- a/config.def.h +++ b/config.def.h @@ -9,8 +9,10 @@ #ifndef CONFIG_H #define CONFIG_H #include +#if !(defined(FALSE) && defined(TRUE)) #define FALSE 0 #define TRUE 1 +#endif #define UNSET NULL /* diff --git a/dist/js/main.js b/dist/js/main.js index 3e1ba95..4014458 100644 --- a/dist/js/main.js +++ b/dist/js/main.js @@ -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>/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>/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 = `${filesize_to_str(file.size)}${html_encode(file.name)}`; + 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 = `${filesize_to_str(file.size)}${html_encode(file.name)}`; - 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); - } - }); -})(); +}); diff --git a/dist/js/redom.js b/dist/js/redom.js new file mode 100644 index 0000000..0bfc134 --- /dev/null +++ b/dist/js/redom.js @@ -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 }); + +})); diff --git a/dist/js/redom.min.js b/dist/js/redom.min.js new file mode 100644 index 0000000..b849b1d --- /dev/null +++ b/dist/js/redom.min.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).redom={})}(this,(function(e){"use strict";function t(e,t){var i=function(e){for(var t=e.split(/([.#])/),i="",n="",o=1;o0;)n[o]=arguments[o+1];var r=typeof e;if("string"===r)i=t(e);else{if("function"!==r)throw new Error("At least one argument required");var l=e;i=new(Function.prototype.bind.apply(l,[null].concat(n)))}return p(w(i),n,!0),i}var b=g,N=g;function x(e){for(var t=[],n=arguments.length-1;n-- >0;)t[n]=arguments[n+1];for(var o=w(e),r=k(e,t,o.firstChild);r;){var l=r.nextSibling;i(e,r),r=l}}function k(e,t,i){for(var n=i,o=Array(t.length),r=0;r0;)n[o]=arguments[o+1];var r=typeof e;if("string"===r)i=t(e,C);else{if("function"!==r)throw new Error("At least one argument required");var l=e;i=new(Function.prototype.bind.apply(l,[null].concat(n)))}return p(w(i),n,!0),i}var T=E;E.extend=function(){for(var e=[],t=arguments.length;t--;)e[t]=arguments[t];return E.bind.apply(E,[this].concat(e))},E.ns=C,e.List=D,e.ListPool=S,e.Place=V,e.Router=j,e.el=b,e.h=N,e.html=g,e.list=A,e.listPool=function(e,t,i){return new S(e,t,i)},e.mount=s,e.place=function(e,t){return new V(e,t)},e.router=function(e,t,i){return new j(e,t,i)},e.s=T,e.setAttr=function(e,t,i){h(e,t,i)},e.setChildren=x,e.setData=c,e.setStyle=a,e.setXlink=v,e.svg=E,e.text=_,e.unmount=i,Object.defineProperty(e,"__esModule",{value:!0})})); diff --git a/dist/svg/searchmenu.svg b/dist/svg/searchmenu.svg new file mode 100644 index 0000000..48ff143 --- /dev/null +++ b/dist/svg/searchmenu.svg @@ -0,0 +1 @@ + diff --git a/dist/treebird.css b/dist/treebird.css index 0d70b85..5d97caa 100644 --- a/dist/treebird.css +++ b/dist/treebird.css @@ -1,3 +1,10 @@ +/* + * Notes: + * - Good luck :^3 + * - Look into position: sticky; to handle overflows? Not sure if this works but i saw it work i think + */ + + /* Variables */ :root { @@ -6,6 +13,18 @@ --account-overlay-gradient-bottom: #dadada; } + +input +{ + transition: .1s box-shadow; +} + +input:focus +{ + outline: 1px solid #aa0000; + box-shadow: 0px 0px 6px #aa00006f; +} + * { margin: 0; @@ -32,6 +51,18 @@ body height: 100%; } +form +{ + display: inline; +} + +html.embed +{ + background-color: unset; + scrollbar-color: #808080 #eaecf0; + scrollbar-width: thin; +} + /* Basic elements */ h1, h2, h3, h4 { @@ -85,10 +116,9 @@ a, a:visited, a:hover, a:active border-radius: 3px; } -.greentext -{ - color: #00aa00; -} +.greentext { color: #00aa00; } +.bluetext { color: #0060aa; } +.yellowtext { color: #cccc00; } /* Cleans up most of the tables */ table.ui-table td @@ -101,8 +131,7 @@ table.ui-table td #navbar { - border-top: 2px solid #fff; - background: linear-gradient(#fbfbfb, #ececef); + background: linear-gradient(#fbfbfb 40%, #f5f5f5 50%, #efefef); width: 1000px; border-bottom: 2px solid #bfbfc4; z-index: 999; @@ -139,7 +168,27 @@ table.ui-table td { vertical-align: middle; display: inline-block; - margin: 4px 0px 6px 18px; + margin: 6px 0px 6px 18px; +} + +#navbar a, +#navbar a:focus +{ + box-shadow: 0px 0px 0px #00000000 !important; + outline: 0 !important; +} + +#navbar a img +{ + transition: .4s transform, .4s filter; +} + +#navbar a:focus img +{ + box-shadow: 0px 0px rgba(0, 0, 0, 0.2); + box-shadow: 0 !important; + transform: rotateZ(360deg); + filter: drop-shadow(0px 0px 3px #aa00006f); } #navbar span.info @@ -204,10 +253,10 @@ table.ui-table td .sidebar { - width: 180px; + width: 181px; min-height: 600px; - max-width: 180px; - min-width: 180px; + max-width: 181px; + min-width: 181px; background-color: rgba(234, 236, 240, var(--sidebar-opacity)); } @@ -220,7 +269,7 @@ table.ui-table td } -.sidebar-embed-container .navigation +.embed .navigation { position: sticky; top: 0px; @@ -230,20 +279,20 @@ table.ui-table td padding: 0; } -.sidebar-embed-container .btn, -.sidebar-embed-container .nav-btn +.embed .btn, +.embed .nav-btn { background: unset; padding: 2px !important; font-size: 13px; } -.sidebar-embed-container .btn-disabled +.embed .btn-disabled { background: unset; } -.sidebar-embed-container .btn:hover +.embed .btn:hover { background: unset; background-color: rgba(0, 0, 0, 0.2); @@ -254,6 +303,112 @@ table.ui-table td border-right: 1px solid #dadada; } +#searchbox +{ + padding-top: unset; + padding-bottom: unset; + height: 26px; + border-right: unset; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + padding-left: 27px; + box-shadow: unset; + transition: width .1s ease; + width: 180px; +} + +#searchbox:focus +{ + box-shadow: 0; + outline: 0; + width: 300px; +} + +.results-header +{ + font-size: 1.4rem; + font-weight: normal; + padding: 8px 16px; + border-bottom: 1px solid #cacaca; + margin: 0; +} + +.results-header .result +{ + font-size: 1.7rem; + font-weight: bold; +} + +#navbar .search-menu-dropdown +{ + display: inline; + position: relative; + left: 30px; + /* Fix weird offset */ + margin-left: -30px; + top: 6px; +} + +.menu .btn-menu input[type=radio] +{ + display: none; +} + +.menu .btn-menu input[type=radio] + span +{ + margin-left: 8px; + position: relative; +} + +.menu .btn-menu input[type=radio]:checked + span:before +{ + position: absolute; + content: ""; + left: -12px; + top: 5px; + width: 6px; + height: 6px; + border-radius: 50%; + background-color: #202020; + font-weight: bold; +} + +.menu .btn-menu input[type=radio]:checked + span:before +{ + position: absolute; + content: ""; + left: -12px; + top: 4px; + width: 4px; + height: 4px; + border-radius: 50%; + background-color: #202020; + font-weight: bold; +} + +input[type=text], +input[type=url], +input[type=email], +input[type=password] +{ + padding: 2px; + border-radius: 3px; + box-shadow: 0px 2px 3px #00000005; + border: 1px solid #d3d3d3; +} + +.menu.menu-compact .btn-menu, +.status-compact .menu-container .menu .btn-menu +{ + font-size: 11px; +} + +#navbar .search-menu-dropdown svg +{ + stroke: #606060; + cursor: pointer; +} + #leftbar ul { margin: 0; @@ -265,41 +420,11 @@ table.ui-table td list-style-type: none; } - -.sidebarbtn-sub -{ - display: block; - color: #000; - padding: 8px; - border-radius: 4px; - padding-left: 24px; - text-decoration: none; - border: 1px solid transparent; -} - -#leftbar ul.sidebar-config -{ - background: linear-gradient(#eef1f9, #bbc3d0); - color: #fff; - border-radius: 6px; - padding: 4px; - margin: 8px; - border: 1px solid #aaa; - box-shadow: inset 0px 2px 3px rgba(0, 0, 0, 0.1); -} - -.sidebarbtn-sub.active +.sidebar-submenu .sidebarbtn.active { font-weight: bold; } -.sidebarbtn-sub:active, .sidebarbtn-sub:hover -{ - background-color: #bbc6d5; - border: 1px solid #a8accbcc; - box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1); -} - #rightbar { border-left: 1px solid #dadada; @@ -398,6 +523,36 @@ table.present th, table.present td font-weight: bold; } +.fancy-error +{ + margin: 36px 0; + text-align: center; +} + +.fancy-error-text +{ + display: block; + margin-top: 30px; + font-size: 1.2rem; + color: #666; +} + +.fancy-error-subtext +{ + display: block; + margin: 12px 24px 0 24px; + font-size: 1rem; + color: #222; +} + +.fancy-error-kaomoji +{ + display: block; + font-size: 2.4rem; + color: #aaa; + user-select: none; +} + .search-highlight { background-color: yellow; @@ -442,6 +597,7 @@ table.present th, table.present td padding: 3px 12px; width: 100%; border: 0; + font-size: 14px; cursor: pointer; text-align: left; box-sizing: border-box; @@ -464,8 +620,8 @@ select:hover input[type=range] { -webkit-appearance: none; appearance: none; - height: 12px; - background: linear-gradient(#f3f3f3, #d5d5d5); + height: 4px; + background: linear-gradient(#f3f3f3, #c5c5c5); border: 1px solid #cacaca; border-radius: 4px; outline: none; @@ -479,8 +635,9 @@ input[type=range]::-moz-range-thumb appearance: none; width: 20px; height: 20px; - border: 1px solid #303236; - background: linear-gradient(#75777c, #505256); + border-radius: 50%; + border: 1px solid #909090; + background: conic-gradient(#ddd, #aaa, #ddd, #aaa, #ddd, #aaa, #ddd); cursor: pointer; } @@ -498,7 +655,8 @@ input[type=range]::-moz-range-thumb .btn:hover, .btn:active, .btn-menu:hover, -.sidebarbtn:hover +.sidebarbtn:hover, +.sidebar .sidebarbtn:hover { background: linear-gradient(#606060, #d3d3d3); /* box-shadow: inset 0px -2px 5px rgba(0, 0, 0, 0.2); */ @@ -526,6 +684,7 @@ input[type=range]::-moz-range-thumb .btn-alt:hover { background: linear-gradient(#aa0000, #ee0000); + border-color: #800000; } .btn-disabled @@ -588,11 +747,23 @@ input[type=submit].post-btn .btn-single { + display: inline-block; border-radius: 4px; border: 1px solid #cacaca; } -.sidebarbtn, .sidebarbtn:visited +.btn-small +{ + font-size: 12px; +} + +.btn-single.btn-small +{ + padding: 4px 8px; +} + +.sidebar .sidebarbtn, +.sidebar .sidebarbtn:visited { display: block; margin: 7px 8px; @@ -603,11 +774,36 @@ input[type=submit].post-btn color: #505050; } + +.sidebar-submenu .sidebarbtn, +.sidebar-submenu .sidebarbtn:visited +{ + color: #202020; + margin-left: 0; + margin-right: 0; + border-radius: 0; + padding-left: 24px; +} + +.sidebar-submenu .sidebarbtn:hover +{ + background: #abafb4; +} + .sidebar-login { padding: 18px; } +#leftbar .sidebar-submenu +{ + border-left: 4px solid #aa0000; + background-color: rgb(214, 219, 225); + box-shadow: inset 0px 2px 3px rgba(0, 0, 0, 0.1); + padding: 5px 0; +} + + /**************************** * Notifications * ***************************/ @@ -630,40 +826,6 @@ input[type=submit].post-btn object-fit: cover; } -.notification-compact -{ - min-width: 100%; - padding: 3px; -} - -.notification-compact p -{ - margin: 0; -} - -.notification-compact .notification-content -{ - max-height: 100px; - overflow: auto; - margin-left: 2px; -} - -.notification-compact .notification-info -{ - font-size: 12px; - padding-left: 2px; - padding-right: 2px; - margin-top: 0; - vertical-align: middle; - font-weight: bold; -} - -.notification-compact .username .emoji -{ - width: 16px; - height: 16px; -} - .notification-info svg, .notification-info-format svg { @@ -683,6 +845,12 @@ input[type=submit].post-btn vertical-align: middle; } +/* CSS issue, hope for the proposed $ selected :^) */ +.status.notification-info-lookahead +{ + padding-top: 7px; +} + .notification-info svg.like, .notification-info-format svg.like { @@ -715,57 +883,18 @@ svg.in-reply-to-icon line-height: 23px; } -input[type=checkbox].hidden:not(:checked) + .reply-form +input[type=checkbox].hidden:not(:checked) + .reply-form, +input[type=checkbox].hidden:not(:checked) + .sidebar-submenu { display: none; } -.notification-compact .notification-content -{ - color: #808080; - font-size: 12px; - padding: 4px 0 2px 0; -} - -.notification-compact .notification-content.is-mention -{ - color: #101010; -} - -.notification-compact .notification-stats -{ - font-size: 10px; -} - -.notification-compact .notification-stats .status-interact -{ - margin-top: 2px; -} - -.notification-compact .notification-stats svg -{ - width: 15px; - height: 15px; -} - -.notification-compact .notification-stats .statbtn -{ - padding: 2px 4px; -} - .notification-info { margin-left: 28px; margin-bottom: 7px; } -.notification-compact .notification-info -{ - margin-left: unset; - margin-bottom: unset; -} - - .notification-info img.avatar, .notification-info .pfp-compact-td img, .notification-info-format .pfp-compact-td img @@ -784,18 +913,49 @@ input[type=checkbox].hidden:not(:checked) + .reply-form display: inline; } -.notification-text-group-with-icon, -.notification-text-group +.status.status-notification:not(.status-notification-mention) .status-content { - vertical-align: middle; + color: #808080; +} + +.notification-unread +{ + position: relative; +} + + +.notification-unread::before +{ + content:""; + position: absolute; + z-index: 0; + top: 0px; + left: 0px; + width: 15px; + height: 15px; + background: linear-gradient(135deg, rgba(255,0,0,1) 0%, rgba(0,0,0,0) 50%, rgba(0,0,0,0) 100%); +} + +.notification-unread::after +{ + content:""; + position: absolute; + z-index: 0; + top: 0px; + left: 0px; + width: 65px; + height: 65px; + background: linear-gradient(135deg, rgba(255,0,0,.2) 0%, rgba(0,0,0,0) 50%, rgba(0,0,0,0) 100%); } /*************************** * Statuses * **************************/ -.status +.status, +.notification-regular { - padding: 11px 0px 0px 11px; + padding: 11px; + padding-bottom: 6px; border-bottom: 1px dashed #cacaca; } @@ -815,9 +975,14 @@ input[type=checkbox].hidden:not(:checked) + .reply-form .status.focused, .status:target { - background-color: #ffdddd; - border-left: 3px solid #aa0000; - padding-left: 7px; + background: #ffdddd; + border-left: 2px solid #aa0000; + padding-left: 9px; +} + +.status:not(:target) .status-meta .application-name +{ + display: none; } .notification-info + .status, @@ -881,11 +1046,21 @@ input[type=checkbox].hidden:not(:checked) + .reply-form display: none; } +.status-hide:checked + .status.status-compact .action-icon +{ + position: initial; + float: left; + left: initial; + top: initial; +} + .pfp-td { + position: relative; width: 52px; border-collapse: collapse !important; padding: 6px; + z-index: 1; } .pfp-td img @@ -894,6 +1069,7 @@ input[type=checkbox].hidden:not(:checked) + .reply-form width: 52px; height: 52px; object-fit: cover; + z-index: 1; } .status table tr @@ -910,21 +1086,41 @@ input[type=checkbox].hidden:not(:checked) + .reply-form font-size: 10px; } -.status .time:before, -.notification-compact .time:before +.status .time +{ + margin-right: 4px; + vertical-align: middle; +} + +.status .status-meta +{ + vertical-align: middle; +} + +.status .application-name +{ + display: inline-block; + margin-left: 4px; + max-width: 140px; + vertical-align: middle; + overflow: hidden; + white-space: nowrap; + font-size: 13px; + text-overflow: ellipsis; +} + +.status .application-name:before { content: "•"; padding-right: 10px; - font-size: 10px; + font-size: 12px; } .status .time, -.notification-compact .time +.status .status-meta { color: #606060; - vertical-align: middle; font-size: 14px; - padding-left: 5px; text-decoration: none; } @@ -933,11 +1129,16 @@ input[type=checkbox].hidden:not(:checked) + .reply-form .notification-content { position: relative; - min-width: 0; - width: auto; + width: 100%; padding: 0 0 0 10px; } +.notification-regular .notification-content +{ + margin-top: 4px; + font-size: 14px; +} + .status .status-info > table.ui-table { width: 100%; @@ -963,15 +1164,12 @@ input[type=checkbox].hidden:not(:checked) + .reply-form .status .poster-stats { - position: relative; display: block; - max-width: 490px; } -.status-hide:checked + .status .poster-stats -{ - max-width: 550px; -} +/* .status-hide:checked + .status .poster-stats */ +/* { */ +/* } */ .poster-stats .alignend { @@ -999,15 +1197,128 @@ input[type=checkbox].hidden:not(:checked) + .reply-form min-width: 0; } +/* Compact notifications (statuses) */ +.notifications-compact, +.status-compact +{ + position: relative; + font-size: 14px; +} + +.status-compact.status.status-notification, +.notifications-compact .notification.notification-regular +{ + padding: 5px; +} + +.status-compact .pfp-td img, +.notifications-compact .notification.notification-regular .pfp-td img +{ + width: 28px; + height: 28px; +} + +.notifications-compact .notification.notification-regular .pfp-td +{ + width: 28px; +} + +.status-compact .action-icon +{ + position: absolute; + background-color: #eaecf0; + border-radius: 50%; + width: 16px; + height: 16px; + padding: 3px; + top: 15px; + left: -15px; + z-index: 8; + font-size: 12px; +} + +.status-compact .action-icon svg +{ + width: 16px; + height: 16px; + stroke: #40455a; + /* Little fix */ + position: relative; + top: -1px; +} + +.status-compact .poster-stats .alignend +{ + background-color: rgb(234, 236, 240); + /* Hack - Fix the right padding regarding the icon being a little wider than it should */ + padding: 1px 0px 1px 5px; +} + +.status-compact .menu-container .menu +{ + margin-left: -80px; +} + +.status-compact.notification-unread::before +{ + content:""; + position: absolute; + z-index: 0; + top: 0px; + left: 0px; + width: 10px; + height: 10px; + background: linear-gradient(135deg, rgba(255,0,0,1) 0%, rgba(0,0,0,0) 50%, rgba(0,0,0,0) 100%); +} + +.status-compact .in-reply-to-id +{ + font-size: 13px; +} + +.status-compact .in-reply-to-icon +{ + width: 14px; + height: 14px; + /* Pixel perfect... */ + top: -2px; +} + +.status-compact .attachments img, +.status-compact .attachments video +{ + display: inline; + margin-right: 6px; + width: 70px; + height: 40px; + background-color: #fff; + border: 1px solid #cacaca; + border-radius: 4px; + object-fit: cover; +} + +.status-compact .status-buttons +{ + float: initial; +} + .status .status-content { margin: 4px 0 0 0; overflow-wrap: anywhere; - width: 490px; line-height: 1.2; display: block; } +.status-content .title +{ + display: block; + padding: 2px 2px 7px; + margin-bottom: 7px; + font-style: italic; + border-bottom: 1px solid #cacaca; +} + .status .status-content p, .status .status-content a { @@ -1034,10 +1345,17 @@ input[type=checkbox].hidden:not(:checked) + .reply-form cursor: pointer; color: #808080; display: inline; - font-size: 12px; margin-right: 5px; } +.status-visibility .visibility +{ + stroke: #808080; + vertical-align: middle; + width: 17px; + height: 17px; +} + .status-interact { margin: 6px 0 0 0; @@ -1092,6 +1410,22 @@ input[type=checkbox].hidden:not(:checked) + .reply-form border-bottom-right-radius: 5px; } +.text-gap-hairline +{ + display: inline-block; + width: 64px; + text-align: center; + height: 1px; + background-color: #d3d3d3; + margin: 2px; +} + +.text-gap-hairline + h6 +{ + display: inline; + font-variant: small-caps; +} + /************************* * Config * ************************/ @@ -1414,6 +1748,11 @@ p} background: linear-gradient(#aa0000, #600000); } +.action +{ + vertical-align: middle; +} + /**************** * Statusbox * ****************/ @@ -1441,6 +1780,11 @@ p} padding: 5px; } +.status-buttons +{ + float: right; +} + .statusbox .statusfooter { display: block; @@ -1458,22 +1802,44 @@ p} float: right; } +.status-interact .statbtn-form +{ + display: inline-block; +} + .status-interact .statbtn { - display: block; - padding: 3px 2px 5px; - min-width: 42px; + display: inline-block; + padding: 3px 0px 3px; + min-width: 25px; +} + +.status-interact .statbtn.view-btn +{ + min-width: 19px; } .status-interact .statbtn svg { width: 18px; height: 18px; + stroke: #666; + vertical-align: middle; } -.view-btn +.status-interact svg.expand { - min-width: 25px !important; + width: 13px; + height: 13px; +} + +.status-interact .count +{ + padding-top: 3px; + font-size: 12px; + padding-bottom: 3px; + padding-right: 7px; + vertical-align: middle; } .statusbox .post-group @@ -1523,17 +1889,12 @@ p} top: 5px; } -.notification-compact .status-interact .statbtn -{ - min-width: 0px !important; -} - .status-interact svg { stroke: #303030; } -.active-anim +.interacted-anim { animation: interact .7s 1; } @@ -1544,12 +1905,12 @@ p} 100% { transform: rotateZ(360deg); } } -.status-interact svg.repeat.active +.status-interact .statbtn.interacted svg.repeat { stroke: #08d345; } -.status-interact svg.like.active +.status-interact .statbtn.interacted svg.like { fill: #fcd202; stroke: #fcd202; @@ -1565,7 +1926,7 @@ p} .status-interact svg.like:hover, .status-interact svg.like:active, -.statis-interact svg.like:focus +.status-interact svg.like:focus { stroke: #fcd202; pointer: select; @@ -1573,19 +1934,11 @@ p} .status-interact svg.repeat:hover, .status-interact svg.repeat:active, -.statis-interact svg.repeat:focus +.status-interact svg.repeat:focus { stroke: #08d345; } -.status-interact .count -{ - padding-top: 3px; - font-size: 12px; - padding-bottom: 3px; - vertical-align: middle; -} - /* Emoji */ .status-content .emoji, .notification-content .emoji, @@ -1635,12 +1988,6 @@ p} border: 1px solid #cacaca; } -.emoji-btn -{ - position: relative; - top: 2px; -} - /* Emojo picker */ .emoji-picker { @@ -1713,19 +2060,45 @@ input[type=radio].hidden:not(:checked) + .emoji-picker-emojos display: block; } +.form-group label +{ + display: inline-block; + min-width: 70px; + font-size: 0.9rem; + text-align: middle; +} + /* Simple page layout */ .simple-page { padding: 0 12px; } -.simple-page h1 +.simple-page h1, +.text-header { + font-size: 1.5rem; padding-left: 12px; padding-bottom: 7px; border-bottom: 1px solid #cacaca; } +.text-header +{ + padding-top: 10px; + margin: 0; + background-color: #f3f3f3; + background: repeating-linear-gradient( + -45deg, + #efefef, + #efefef 10px, + #f6f6f6 10px, + #f6f6f6 20px + ); + box-shadow: inset 0px -2px 2px rgba(60, 60, 60, 0.2); + text-shadow: 0px 2px 6px rgba(0, 0, 0, 0.2); +} + .simple-page h3 { margin-left: 15px; @@ -1964,6 +2337,12 @@ input[type=checkbox].hidden:not(:checked) + .list-edit-content padding: 6px; } +.navigation .nav-up +{ + width: 50px; + text-align: center; +} + .nav-symbol { color: #505050; @@ -2023,6 +2402,10 @@ input[type=checkbox].hidden:not(:checked) + .list-edit-content } /* MENUS */ + +/* Transition animations are possible with dropdown menus, e.g. fade, if you use + * visibility: visible; instead of display:block; (and their counter values) + * but some things seemed buggy like the search dropdown, so try with care */ .menu-container:hover .menu, .menu-container:hover + .menu { @@ -2049,12 +2432,24 @@ input[type=checkbox].hidden:not(:checked) + .list-edit-content padding: 4px 0 4px 0; } +.menu ul + ul +{ + border-top: 2px solid #cacaca; +} + + .menu ul li { list-style-type: none; display: block; } +.menu.menu-search.menu-compact +{ + margin-left: -8px; + margin-top: -3px; +} + .user-options-btn { cursor: pointer; @@ -2106,7 +2501,7 @@ input[type=checkbox].hidden:not(:checked) + .list-edit-content { color: #ccc; font-size: 8px; - margin: unset;e + margin: unset; } #instance-panel + .mini-links diff --git a/docs/DEVELOP.md b/docs/DEVELOP.md new file mode 100644 index 0000000..c7a0ca0 --- /dev/null +++ b/docs/DEVELOP.md @@ -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 +``` \ No newline at end of file diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 3a942ec..0309faa 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -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 diff --git a/perl/account.pm b/perl/account.pm new file mode 100644 index 0000000..5c320e4 --- /dev/null +++ b/perl/account.pm @@ -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; diff --git a/perl/attachments.pm b/perl/attachments.pm new file mode 100644 index 0000000..d7b2c13 --- /dev/null +++ b/perl/attachments.pm @@ -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; diff --git a/perl/chat.pm b/perl/chat.pm new file mode 100644 index 0000000..d4fc2ee --- /dev/null +++ b/perl/chat.pm @@ -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; diff --git a/perl/config.pm b/perl/config.pm new file mode 100644 index 0000000..9fa32e0 --- /dev/null +++ b/perl/config.pm @@ -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; diff --git a/perl/embed.pm b/perl/embed.pm new file mode 100644 index 0000000..6cc6d32 --- /dev/null +++ b/perl/embed.pm @@ -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; + diff --git a/perl/emojis.pm b/perl/emojis.pm new file mode 100644 index 0000000..a16c172 --- /dev/null +++ b/perl/emojis.pm @@ -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; diff --git a/perl/icons.pm b/perl/icons.pm new file mode 100644 index 0000000..7243d4f --- /dev/null +++ b/perl/icons.pm @@ -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 => '', + + like => '', + + expand => '', + + reply => '', + + emoji => '', + + likeboost => '', + + fileclip => '', + + 'local' => '', + + direct => '', + + private => '', + + list => '', + + unlisted => '', + + public => '', + + follow => '', + + search => '', + + 'search-menu' => '', + ); + + $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; diff --git a/perl/l10n.pm b/perl/l10n.pm new file mode 100644 index 0000000..3efd22d --- /dev/null +++ b/perl/l10n.pm @@ -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; diff --git a/perl/lists.pm b/perl/lists.pm new file mode 100644 index 0000000..8200115 --- /dev/null +++ b/perl/lists.pm @@ -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'}); +} diff --git a/perl/login.pm b/perl/login.pm new file mode 100644 index 0000000..315a684 --- /dev/null +++ b/perl/login.pm @@ -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; diff --git a/perl/main.pl b/perl/main.pl new file mode 100644 index 0000000..4a41e56 --- /dev/null +++ b/perl/main.pl @@ -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; +} diff --git a/perl/meta.pm b/perl/meta.pm new file mode 100644 index 0000000..5db57fd --- /dev/null +++ b/perl/meta.pm @@ -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; diff --git a/perl/navigation.pm b/perl/navigation.pm new file mode 100644 index 0000000..7b27737 --- /dev/null +++ b/perl/navigation.pm @@ -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'}); +} diff --git a/perl/notifications.pm b/perl/notifications.pm new file mode 100644 index 0000000..2acde3d --- /dev/null +++ b/perl/notifications.pm @@ -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; diff --git a/perl/postbox.pm b/perl/postbox.pm new file mode 100644 index 0000000..35e6475 --- /dev/null +++ b/perl/postbox.pm @@ -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'}); +} diff --git a/perl/search.pm b/perl/search.pm new file mode 100644 index 0000000..c480157 --- /dev/null +++ b/perl/search.pm @@ -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'}); +} + diff --git a/perl/status.pm b/perl/status.pm new file mode 100644 index 0000000..c366900 --- /dev/null +++ b/perl/status.pm @@ -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; diff --git a/perl/string_helpers.pm b/perl/string_helpers.pm new file mode 100644 index 0000000..20e7bb1 --- /dev/null +++ b/perl/string_helpers.pm @@ -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 = '(?='; + +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/&/&/gs; + $text =~ s//>/gs; + $text =~ s/"/"/gs; + $text; +} + +sub greentextify +{ + my $text = shift; + $text =~ s/(>.*?)(?=<|$)/$1<\/span>/gs; + $text =~ s/(<.*?)(?=<|$)/$1<\/span>/gs; + $text =~ s/(?:^|>| )(\^.*?)(?=<|$)/$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://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//gs; + $text; +} + +sub get_mentions_from_content +{ + my ($ssn, $status) = @_; + my $result = ''; + my $acct; + while ($status->{'content'} =~ + /@(?:)?.*?(?:<\/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)]; +} diff --git a/perl/template_helpers.pm b/perl/template_helpers.pm new file mode 100644 index 0000000..023800f --- /dev/null +++ b/perl/template_helpers.pm @@ -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"; + + $error + +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}); +} diff --git a/perl/timeline.pm b/perl/timeline.pm new file mode 100644 index 0000000..981c26d --- /dev/null +++ b/perl/timeline.pm @@ -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'}); +} diff --git a/src/about.c b/src/about.c index 2937a6e..7ffba85 100644 --- a/src/about.c +++ b/src/about.c @@ -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); } diff --git a/src/account.c b/src/account.c index 46995ff..578e12e 100644 --- a/src/account.c +++ b/src/account.c @@ -18,6 +18,7 @@ #include #include +#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 "%s" @@ -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; } diff --git a/src/account.h b/src/account.h index b74a29a..97876b4 100644 --- a/src/account.h +++ b/src/account.h @@ -18,9 +18,11 @@ #ifndef ACCOUNT_H #define ACCOUNT_H +#include "global_perl.h" #include #include #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 diff --git a/src/reply.h b/src/applications.c similarity index 58% rename from src/reply.h rename to src/applications.c index b01c6be..a257a97 100644 --- a/src/reply.h +++ b/src/applications.c @@ -16,19 +16,21 @@ * along with this program. If not, see . */ -#ifndef REPLY_H -#define REPLY_H -#include "session.h" -#include -#include +#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 diff --git a/src/test.h b/src/applications.h similarity index 83% rename from src/test.h rename to src/applications.h index 3d82a19..38096c1 100644 --- a/src/test.h +++ b/src/applications.h @@ -16,13 +16,11 @@ * along with this program. If not, see . */ -#ifndef TEST_H -#define TEST_H -#include +#ifndef APPLICATIONS_H +#define APPLICATIONS_H #include -#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 */ diff --git a/src/attachments.c b/src/attachments.c index 6b7ce2d..233e937 100644 --- a/src/attachments.c +++ b/src/attachments.c @@ -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 ? "
" : ""); - } - 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) { diff --git a/src/attachments.h b/src/attachments.h index 9e54e2c..131b3da 100644 --- a/src/attachments.h +++ b/src/attachments.h @@ -21,6 +21,7 @@ #include #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 diff --git a/src/base_page.c b/src/base_page.c index a47f2b8..341adf4 100644 --- a/src/base_page.c +++ b/src/base_page.c @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -#include #include #include #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 = "
Login / Register"; - const char* sidebar_embed = ""; - 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, - ¬ifs, - ¬ifs_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, + ¬ifs, + ¬ifs_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, "
%s
", - (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, "", - 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 diff --git a/src/base_page.h b/src/base_page.h index 010e08b..d2886fd 100644 --- a/src/base_page.h +++ b/src/base_page.h @@ -18,14 +18,14 @@ #ifndef BASE_PAGE_H #define BASE_PAGE_H +#include "global_perl.h" +#include "session.h" #include #include #include #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); diff --git a/src/navigation.h b/src/cgi.h similarity index 72% rename from src/navigation.h rename to src/cgi.h index e15901b..e0d2307 100644 --- a/src/navigation.h +++ b/src/cgi.h @@ -16,14 +16,15 @@ * along with this program. If not, see . */ -#ifndef NAVIGATION_H -#define NAVIGATION_H -#include -#include +/* #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 +#endif // NO_FCGI -#endif // NAVIGATION_H +#ifndef SINGLE_THREADED +#include +#endif // SINGLE_THREADED + +// #endif diff --git a/src/conversations.c b/src/conversations.c index effb6d1..24f6ed8 100644 --- a/src/conversations.c +++ b/src/conversations.c @@ -17,6 +17,8 @@ */ #include +#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 = "Chat created"; - - // 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) diff --git a/src/conversations.h b/src/conversations.h index 18ffb37..453897a 100644 --- a/src/conversations.h +++ b/src/conversations.h @@ -23,30 +23,13 @@ #include #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 diff --git a/src/cookie.c b/src/cookie.c index 6be7e9a..045faa7 100644 --- a/src/cookie.c +++ b/src/cookie.c @@ -16,11 +16,9 @@ * along with this program. If not, see . */ -#include -#include +#include "cookie.h" #include #include -#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; +} diff --git a/src/cookie.h b/src/cookie.h index a9b4e39..c40c1b7 100644 --- a/src/cookie.h +++ b/src/cookie.h @@ -19,7 +19,10 @@ #ifndef COOKIE_H #define COOKIE_H #include +#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 diff --git a/src/emoji.c b/src/emoji.c index f26ce37..2cfe1b8 100644 --- a/src/emoji.c +++ b/src/emoji.c @@ -16,19 +16,14 @@ * along with this program. If not, see . */ +#include "emoji.h" #include #include #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, "", 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, "%s", + status_id, emoji->codes, emoji->codes); + else + *size = easprintf(&emoji_str, "%s", 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) + diff --git a/src/emoji.h b/src/emoji.h index 3968009..9d9e8cf 100644 --- a/src/emoji.h +++ b/src/emoji.h @@ -20,7 +20,9 @@ #define EMOJI_H #include #include +#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 diff --git a/src/emoji_reaction.c b/src/emoji_reaction.c index 2720941..6479759 100644 --- a/src/emoji_reaction.c +++ b/src/emoji_reaction.c @@ -22,67 +22,17 @@ #include #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) diff --git a/src/emoji_reaction.h b/src/emoji_reaction.h index bab23ae..22dea36 100644 --- a/src/emoji_reaction.h +++ b/src/emoji_reaction.h @@ -19,8 +19,10 @@ #ifndef EMOJI_REACTION_H #define EMOJI_REACTION_H #include +#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 diff --git a/src/error.c b/src/error.c index f730d1a..679e14e 100644 --- a/src/error.c +++ b/src/error.c @@ -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); } diff --git a/src/error.h b/src/error.h index cb6fb72..cde3547 100644 --- a/src/error.h +++ b/src/error.h @@ -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 diff --git a/src/global_perl.c b/src/global_perl.c new file mode 100644 index 0000000..d649478 --- /dev/null +++ b/src/global_perl.c @@ -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 . + */ + +#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); +} diff --git a/src/global_perl.h b/src/global_perl.h new file mode 100644 index 0000000..563d4ac --- /dev/null +++ b/src/global_perl.h @@ -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 . + */ + +#ifndef GLOBAL_PERL_H +#define GLOBAL_PERL_H +#include +#include +#include + +// 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 */ diff --git a/src/graphsnbars.c b/src/graphsnbars.c deleted file mode 100644 index ea717a1..0000000 --- a/src/graphsnbars.c +++ /dev/null @@ -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 . - */ - -#include -#include -#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); -} diff --git a/src/graphsnbars.h b/src/graphsnbars.h deleted file mode 100644 index 344be91..0000000 --- a/src/graphsnbars.h +++ /dev/null @@ -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 . - */ - -#ifndef GRAPHS_N_BARS_H -#define GRAPHS_N_BARS_H -#include -#include -#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 */ diff --git a/src/hashtag.c b/src/hashtag.c index 2e44e33..8bdc886 100644 --- a/src/hashtag.c +++ b/src/hashtag.c @@ -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); -} diff --git a/src/hashtag.h b/src/hashtag.h index cbf2992..fb6f431 100644 --- a/src/hashtag.h +++ b/src/hashtag.h @@ -21,7 +21,6 @@ #include #include -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 */ diff --git a/src/http.c b/src/http.c index 8ce5b10..a93d477 100644 --- a/src/http.c +++ b/src/http.c @@ -27,15 +27,14 @@ #define REDIR_HTML_END "" \ "" -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 %s..." REDIR_HTML_END, - status, - loc_str, - loc_str); + PRINTF("Status: %s\r\n" + "Location: %s\r\n\r\n" + REDIR_HTML_BEGIN "Redirecting to %s..." REDIR_HTML_END, + status, + loc_str, + loc_str); } diff --git a/src/http.h b/src/http.h index 403518c..67847c2 100644 --- a/src/http.h +++ b/src/http.h @@ -18,11 +18,11 @@ #ifndef HTTP_H #define HTTP_H -#include -#include +#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 diff --git a/src/lists.c b/src/lists.c index c12c89e..a49a92b 100644 --- a/src/lists.c +++ b/src/lists.c @@ -17,6 +17,7 @@ */ #include +#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) + diff --git a/src/lists.h b/src/lists.h index 289163e..9d55d84 100644 --- a/src/lists.h +++ b/src/lists.h @@ -22,10 +22,15 @@ #include #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 diff --git a/src/local_config.c b/src/local_config.c new file mode 100644 index 0000000..d7784d3 --- /dev/null +++ b/src/local_config.c @@ -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 . + */ + +#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; +} + diff --git a/src/local_config.h b/src/local_config.h index 3eb092f..94b3333 100644 --- a/src/local_config.h +++ b/src/local_config.h @@ -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 diff --git a/src/local_config_set.c b/src/local_config_set.c index d34b889..595f6d0 100644 --- a/src/local_config_set.c +++ b/src/local_config_set.c @@ -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); diff --git a/src/local_config_set.h b/src/local_config_set.h index 0135b5f..ab0d0b8 100644 --- a/src/local_config_set.h +++ b/src/local_config_set.h @@ -19,12 +19,11 @@ #ifndef LOCAL_CONFIG_SET_H #define LOCAL_CONFIG_SET_H #include -#include -#include -#include "local_config.h" #include "session.h" #include "attachments.h" +#include "local_config.h" #include "key.h" +#include "cgi.h" enum config_page { diff --git a/src/login.c b/src/login.c index b8904db..a5ce912 100644 --- a/src/login.c +++ b/src/login.c @@ -16,28 +16,26 @@ * along with this program. If not, see . */ -#include -#include +#include "login.h" #include #include #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 +#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); } diff --git a/src/main.c b/src/main.c index d936a60..adef162 100644 --- a/src/main.c +++ b/src/main.c @@ -16,9 +16,10 @@ * along with this program. If not, see . */ +#include +#include +#include "global_perl.h" #include -#include -#include #include #include #include @@ -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; } diff --git a/src/navigation.c b/src/navigation.c deleted file mode 100644 index fe45f2b..0000000 --- a/src/navigation.c +++ /dev/null @@ -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 . - */ - -#include -#include "navigation.h" -#include "easprintf.h" - -// Pages -#include "../static/navigation.ctmpl" - -#define SUBMIT_HTML "" - -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); -} diff --git a/src/notifications.c b/src/notifications.c index cd4e4a8..fc62364 100644 --- a/src/notifications.c +++ b/src/notifications.c @@ -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, ¬ifs, ¬ifs_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, ¬ifs, ¬ifs_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, - ¬ifs, - ¬ifs_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, ¬ifs, ¬ifs_len); } - // Set theme - if (ssn->config.theme && !(strcmp(ssn->config.theme, "treebird") == 0 && - ssn->config.themeclr == 0)) - { - easprintf(&theme_str, "", - 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); diff --git a/src/notifications.h b/src/notifications.h index fc3b5a4..50b4339 100644 --- a/src/notifications.h +++ b/src/notifications.h @@ -19,36 +19,20 @@ #ifndef NOTIFICATIONS_H #define NOTIFICATIONS_H #include -#include -#include #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 diff --git a/src/page_config.c b/src/page_config.c index 85bb147..1481bb6 100644 --- a/src/page_config.c +++ b/src/page_config.c @@ -16,24 +16,20 @@ * along with this program. If not, see . */ -#include +#include "page_config.h" +#include "global_perl.h" #include #include #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 #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) diff --git a/src/page_config.h b/src/page_config.h index e30b1d9..5152755 100644 --- a/src/page_config.h +++ b/src/page_config.h @@ -20,14 +20,13 @@ #define PAGE_CONFIG_H #include #include -#include -#include #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 diff --git a/src/path.c b/src/path.c index 0b2daec..8fa63e4 100644 --- a/src/path.c +++ b/src/path.c @@ -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, diff --git a/src/path.h b/src/path.h index 26cbffe..407e0ab 100644 --- a/src/path.h +++ b/src/path.h @@ -18,14 +18,14 @@ #ifndef PATH_H #define PATH_H -#include -#include +#include "session.h" #include #include #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); diff --git a/src/query.c b/src/query.c index cc62c1f..3763536 100644 --- a/src/query.c +++ b/src/query.c @@ -16,17 +16,17 @@ * along with this program. If not, see . */ -#include -#include +#include "query.h" #include #include #include #include -#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; +} diff --git a/src/query.h b/src/query.h index 8354ccb..282d2f7 100644 --- a/src/query.h +++ b/src/query.h @@ -18,9 +18,11 @@ #ifndef QUERY_H #define QUERY_H +#include "global_perl.h" #include #include #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 diff --git a/src/reply.c b/src/reply.c deleted file mode 100644 index 7203cde..0000000 --- a/src/reply.c +++ /dev/null @@ -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 . - */ - -#define PCRE2_CODE_UNIT_WIDTH 8 - -#include -#include -#include -#include "reply.h" -#include "easprintf.h" -#include "../config.h" - -// Pages -#include "../static/post.ctmpl" - -#define ID_REPLY_SIZE 256 -#define ID_RESPONSE "" - -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 , 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 "@(?:)?.*?(?:<\\/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; -} diff --git a/test/unit/main.c b/src/request.h similarity index 68% rename from test/unit/main.c rename to src/request.h index ace6823..ccbeffd 100644 --- a/test/unit/main.c +++ b/src/request.h @@ -16,18 +16,17 @@ * along with this program. If not, see . */ -#include -#include -#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 */ diff --git a/src/scrobble.c b/src/scrobble.c index eb0aab1..521c147 100644 --- a/src/scrobble.c +++ b/src/scrobble.c @@ -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) diff --git a/src/scrobble.h b/src/scrobble.h index 9e1f90d..e2174e5 100644 --- a/src/scrobble.h +++ b/src/scrobble.h @@ -19,8 +19,9 @@ #ifndef SCROBBLE_H #define SCROBBLE_H #include +#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 */ diff --git a/src/search.c b/src/search.c index 9ae2cbe..71fb3d9 100644 --- a/src/search.c +++ b/src/search.c @@ -17,8 +17,9 @@ */ #include -#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; } diff --git a/src/search.h b/src/search.h index c7b013b..7d30abf 100644 --- a/src/search.h +++ b/src/search.h @@ -21,22 +21,12 @@ #include #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 */ diff --git a/src/session.c b/src/session.c index ac4e546..ea32235 100644 --- a/src/session.c +++ b/src/session.c @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +#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; +} + diff --git a/src/session.h b/src/session.h index 6d9a23a..5c4f968 100644 --- a/src/session.h +++ b/src/session.h @@ -19,6 +19,7 @@ #ifndef SESSION_H #define SESSION_H #include +#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 diff --git a/src/status.c b/src/status.c index 483e618..f9ff323 100644 --- a/src/status.c +++ b/src/status.c @@ -23,56 +23,23 @@ #include "helpers.h" #include "http.h" #include "base_page.h" +#include "applications.h" #include "status.h" #include "easprintf.h" #include "query.h" #include "cookie.h" #include "string_helpers.h" #include "error.h" -#include "reply.h" #include "attachments.h" #include "emoji_reaction.h" #include "../config.h" -#include "type_string.h" #include "string.h" #include "emoji.h" #include "account.h" -// Pages -#include "../static/status.ctmpl" -#include "../static/notification.ctmpl" -#include "../static/in_reply_to.ctmpl" -#include "../static/status_interactions_label.ctmpl" -#include "../static/status_interactions.ctmpl" -#include "../static/status_interaction_profile.ctmpl" -#include "../static/interactions_page.ctmpl" -#include "../static/likeboost.ctmpl" -#include "../static/reactions_btn.ctmpl" -#include "../static/interaction_buttons.ctmpl" -#include "../static/reply_link.ctmpl" -#include "../static/reply_checkbox.ctmpl" -#include "../static/menu_item.ctmpl" -#include "../static/like_btn.ctmpl" -#include "../static/repeat_btn.ctmpl" -#include "../static/reply_btn.ctmpl" -#include "../static/expand_btn.ctmpl" -#include "../static/like_btn_img.ctmpl" -#include "../static/repeat_btn_img.ctmpl" -#include "../static/reply_btn_img.ctmpl" -#include "../static/expand_btn_img.ctmpl" -#include "../static/thread_page_btn.ctmpl" - #define ACCOUNT_INTERACTIONS_LIMIT 11 #define NUM_STR "%u" -struct status_args -{ - mastodont_t* api; - struct mstdnt_status* status; - struct construct_statuses_args* args; - struct session* ssn; -}; - int try_post_status(struct session* ssn, mastodont_t* api) { if (!(keystr(ssn->post.content))) return 1; @@ -184,27 +151,6 @@ void content_status_react(PATH_ARGS) redirect(req, REDIRECT_303, referer); } -const char* status_visibility_str(enum l10n_locale loc, - enum mstdnt_visibility_type vis) -{ - switch (vis) - { - case MSTDNT_VISIBILITY_UNLISTED: - return L10N[loc][L10N_VIS_UNLISTED]; - case MSTDNT_VISIBILITY_PRIVATE: - return L10N[loc][L10N_VIS_PRIVATE]; - case MSTDNT_VISIBILITY_DIRECT: - return L10N[loc][L10N_VIS_DIRECT]; - case MSTDNT_VISIBILITY_LOCAL: - return L10N[loc][L10N_VIS_LOCAL]; - case MSTDNT_VISIBILITY_LIST: - return L10N[loc][L10N_VIS_LIST]; - case MSTDNT_VISIBILITY_PUBLIC: - default: - return L10N[loc][L10N_VIS_PUBLIC]; - } -} - int try_interact_status(struct session* ssn, mastodont_t* api, char* id) { struct mstdnt_args m_args; @@ -244,244 +190,6 @@ int try_interact_status(struct session* ssn, mastodont_t* api, char* id) return res; } -char* construct_status_interactions_label(char* status_id, - int is_favourites, - char* header, - int val, - size_t* size) -{ - struct status_interactions_label_template tdata = { - .prefix = config_url_prefix, - .status_id = status_id, - .action = is_favourites ? "favourited_by" : "reblogged_by", - .header = header, - .value = val, - }; - return tmpl_gen_status_interactions_label(&tdata, size); -} - -char* construct_interaction_buttons(struct session* ssn, - struct mstdnt_status* status, - size_t* size, - uint8_t flags) -{ - int use_img = ssn->config.interact_img; - char* interaction_html; - char* repeat_btn; - char* like_btn; - char* likeboost_html = NULL; - char* reply_count = NULL; - char* repeat_count = NULL; - char* reply_btn; - char* favourites_count = NULL; - char* emoji_picker_html = NULL; - char* reactions_btn_html = NULL; - char* time_str; - int show_nums = (flags & STATUS_NO_DOPAMEME) != STATUS_NO_DOPAMEME && - ssn->config.stat_dope; - size_t s; - - // Emojo picker - if ((flags & STATUS_EMOJI_PICKER) == STATUS_EMOJI_PICKER) - { - emoji_picker_html = construct_emoji_picker(status->id, NULL); - } - - struct reactions_btn_template tdata = { - .prefix = config_url_prefix, - .status_id = status->id, - .emoji_picker = emoji_picker_html - }; - reactions_btn_html = tmpl_gen_reactions_btn(&tdata, NULL); - - if (show_nums) - { - if (status->replies_count) - easprintf(&reply_count, NUM_STR, status->replies_count); - if (status->reblogs_count) - easprintf(&repeat_count, NUM_STR, status->reblogs_count); - if (status->favourites_count) - easprintf(&favourites_count, NUM_STR, status->favourites_count); - } - - struct likeboost_template lbdata = { - .prefix = config_url_prefix, - .status_id = status->id, - }; - likeboost_html = tmpl_gen_likeboost(&lbdata, NULL); - - time_str = reltime_to_str(status->created_at); - - // TODO cleanup? - if (use_img) - { - struct repeat_btn_img_template rpbdata = { .prefix = config_url_prefix, .repeat_active = status->reblogged ? "active" : "" }; - repeat_btn = tmpl_gen_repeat_btn_img(&rpbdata, NULL); - struct like_btn_img_template ldata = { .prefix = config_url_prefix, .favourite_active = status->favourited ? "active" : "" }; - like_btn = tmpl_gen_like_btn_img(&ldata, NULL); - } - else { - struct repeat_btn_template rpbdata = { .repeat_active = status->reblogged ? "active" : "" }; - repeat_btn = tmpl_gen_repeat_btn(&rpbdata, NULL); - struct like_btn_template ldata = { .favourite_active = status->favourited ? "active" : "" }; - like_btn = tmpl_gen_like_btn(&ldata, NULL); - } - - // Weather it should be a link or a " - -char* make_mentions_local(char* content) -{ - char* url_format; - int error; - PCRE2_SIZE erroffset; - int substitute_success = 0; - // Initial size, will be increased by 30% if pcre2_substitute cannot fit into the size - // ...why can't pcre2 just allocate a string with the size for us? Thanks... - size_t res_len = 1024; - char* res = malloc(res_len); - pcre2_code* re = pcre2_compile((PCRE2_SPTR)REGEX_MENTION, - PCRE2_ZERO_TERMINATED, PCRE2_MULTILINE, - &error, &erroffset, NULL); - if (re == NULL) - { - fprintf(stderr, "Couldn't parse regex at offset %d: %s\n", error, REGEX_MENTION + erroffset); - pcre2_code_free(re); - return NULL; - } - - int len = easprintf(&url_format, - "", - config_host_url_insecure ? "" : "s", - getenv("HTTP_HOST")); - - int rc = -1; - PCRE2_SIZE res_len_str; - while (rc < 0) - { - res_len_str = res_len; - rc = pcre2_substitute( - re, - (PCRE2_SPTR)content, - PCRE2_ZERO_TERMINATED, - 0, - PCRE2_SUBSTITUTE_EXTENDED | PCRE2_SUBSTITUTE_GLOBAL, - NULL, - NULL, - (PCRE2_SPTR)url_format, - len, - (PCRE2_UCHAR*)res, - &res_len_str - ); - if (rc < 0) - { - switch (rc) - { - case PCRE2_ERROR_NOMEMORY: - // Increase by 30% and try again - res_len = (float)res_len + ((float)res_len * .3); - res = realloc(res, res_len); - break; - default: - { - char buf[256]; - pcre2_get_error_message(rc, buf, 256); - goto out; - } - } - } - else - substitute_success = 1; - } - -out: - if (!substitute_success) - free(res); - free(url_format); - pcre2_code_free(re); - return substitute_success ? res : content; -} - -char* greentextify(char* content) -{ - if (!content) return NULL; - - int error; - PCRE2_SIZE erroffset; - int rc; - int gt_off; - int gt_len; - char* res = content; - - // Malloc'd strings - char* reg_string; - char* gt_string; - - char* oldres = NULL; - PCRE2_SIZE* re_results; - pcre2_code* re = pcre2_compile((PCRE2_SPTR)REGEX_GREENTEXT, PCRE2_ZERO_TERMINATED, 0, &error, &erroffset, NULL); - pcre2_match_data* re_data; - if (re == NULL) - { - fprintf(stderr, "Couldn't parse regex at offset %ld: %d\n", erroffset, error); - pcre2_code_free(re); - return res; - } - - re_data = pcre2_match_data_create_from_pattern(re, NULL); - - for (int ind = 0;;) - { - rc = pcre2_match(re, (PCRE2_SPTR)res, strlen(res), ind, 0, re_data, NULL); - if (rc < 0) - break; - - re_results = pcre2_get_ovector_pointer(re_data); - - // Store to last result - gt_off = re_results[2]; - gt_len = re_results[3] - gt_off; - - oldres = res; - - // Malloc find/repl strings - reg_string = malloc(gt_len + 1); - strncpy(reg_string, res + gt_off, gt_len); - reg_string[gt_len] = '\0'; - easprintf(>_string, "%s", reg_string); - - res = strrepl(res, reg_string, gt_string, STRREPL_ALL ); - if (oldres != content) free(oldres); - ind = re_results[2] + strlen(gt_string); - free(reg_string); - free(gt_string); - } - - pcre2_match_data_free(re_data); - pcre2_code_free(re); - return res; -} - -char* construct_status(struct session* ssn, - mastodont_t* api, - struct mstdnt_status* local_status, - size_t* size, - struct mstdnt_notification* local_notif, - struct construct_statuses_args* args, - uint8_t flags) -{ - struct mstdnt_args m_args; - set_mstdnt_args(&m_args, ssn); - char* stat_html; - - // Counts - char* formatted_display_name = NULL; - char* attachments = NULL; - char* emoji_reactions = NULL; - char* serialized_display_name = NULL; - char* interaction_btns = NULL; - char* notif_info = NULL; - char* post_response = NULL; - char* in_reply_to_str = NULL; - char* delete_status = NULL; - char* pin_status = NULL; - char* interactions_html = NULL; - enum l10n_locale locale = l10n_normalize(ssn->config.lang); - struct mstdnt_status* status = local_status; - // Create a "fake" notification header which contains information for - // the reblogged status - struct mstdnt_notification notif_reblog; - struct mstdnt_notification* notif = local_notif; - struct mstdnt_account* favourites = NULL; - struct mstdnt_account* reblogs = NULL; - struct mstdnt_storage favourites_storage = { 0 }; - struct mstdnt_storage reblogs_storage = { 0 }; - size_t favourites_len = 0; - size_t reblogs_len = 0; - - if (!status) return NULL; - - // If focused, show status interactions - if ((flags & STATUS_FOCUSED) == STATUS_FOCUSED && - (status->reblogs_count || status->favourites_count)) - { - if (status->favourites_count) - mastodont_status_favourited_by(api, - &m_args, - status->id, - &favourites_storage, - &favourites, - &favourites_len); - if (status->reblogs_count) - mastodont_status_reblogged_by(api, - &m_args, - status->id, - &reblogs_storage, - &reblogs, - &reblogs_len); - interactions_html = construct_status_interactions(status->id, - status->favourites_count, - status->reblogs_count, - favourites, - favourites_len, - reblogs, - reblogs_len, - NULL); - mastodont_storage_cleanup(&reblogs_storage); - mastodont_storage_cleanup(&favourites_storage); - - mstdnt_cleanup_accounts(favourites, favourites_len); - mstdnt_cleanup_accounts(reblogs, reblogs_len); - } - - // Repoint value if it's a reblog - if (status->reblog) - { - status = status->reblog; - // Point to our account - notif_reblog.account = &(local_status->account); - notif_reblog.type = MSTDNT_NOTIFICATION_REBLOG; - notif = ¬if_reblog; - } - - // Format username with emojis - serialized_display_name = sanitize_html(status->account.display_name); - formatted_display_name = emojify(serialized_display_name, - status->account.emojis, - status->account.emojis_len); - // Format status - char* parse_content = reformat_status(ssn, status->content, status->emojis, status->emojis_len); - - interaction_btns = construct_interaction_buttons(ssn, status, NULL, flags); - - // Find and replace - if (args && args->highlight_word && parse_content != status->content) - { - char* parse_content_tmp; - char* repl_str = NULL; - easprintf(&repl_str, "%s", args->highlight_word); - parse_content_tmp = parse_content; - parse_content = strrepl(parse_content, args->highlight_word, repl_str, STRREPL_ALL); - // Check if the old parse_content needed to be free'd - if (parse_content_tmp != status->content && - parse_content != parse_content_tmp) - free(parse_content_tmp); - else // No results, move back - parse_content = parse_content_tmp; - - free(repl_str); - } - - if (ssn->logged_in) - post_response = reply_status(ssn, status->in_reply_to_account_id , status); - - // Delete status menu item and pinned, logged in only - if (ssn->logged_in && strcmp(status->account.acct, ssn->acct.acct) == 0) - { - struct menu_item_template mdata = { - .prefix = config_url_prefix, - .status_id = status->id, - .itype = "delete", - .text = "Delete status" - }; - delete_status = tmpl_gen_menu_item(&mdata, NULL); - - mdata.itype = status->pinned ? "unpin" : "pin"; - mdata.text = status->pinned ? "Unpin status" : "Pin status"; - pin_status = tmpl_gen_menu_item(&mdata, NULL); - } - - if (status->media_attachments_len) - attachments = construct_attachments(ssn, status->sensitive, status->media_attachments, status->media_attachments_len, NULL); - if (status->pleroma.emoji_reactions_len) - emoji_reactions = construct_emoji_reactions(status->id, status->pleroma.emoji_reactions, status->pleroma.emoji_reactions_len, NULL); - if (notif && notif->type != MSTDNT_NOTIFICATION_MENTION) - { - char* notif_serialized_name = sanitize_html(notif->account->display_name); - char* notif_display_name = emojify(notif_serialized_name, - notif->account->emojis, - notif->account->emojis_len); - struct notification_template tdata = { - .avatar = notif->account->avatar, - .username = notif_display_name, - .action = (local_status->reblog ? notification_type_compact_str(notif->type) : notification_type_str(notif->type)), - .action_item = notification_type_svg(notif->type), - }; - notif_info = tmpl_gen_notification(&tdata, NULL); - if (notif_display_name != notif->account->display_name) - free(notif_display_name); - if (notif_serialized_name != notif_display_name && - notif_serialized_name != notif->account->display_name) - free(notif_serialized_name); - } - - if (status->in_reply_to_id && status->in_reply_to_account_id) - in_reply_to_str = get_in_reply_to(api, ssn, status, NULL); - - struct status_template tmpl = { - .status_id = status->id, - .notif_info = notif_info, - .thread_hidden = status->muted ? "checked" : "", - // TODO doesn't even need to be a hashtag, this is a temporary hack - .is_cat = status->account.note && strstr(status->account.note, "isCat") ? "is-cat" : NULL, - .is_bun = status->account.note && strstr(status->account.note, "isBun") ? " is-bun" : NULL, - .avatar = status->account.avatar, - .username = formatted_display_name, - .prefix = config_url_prefix, - .acct = status->account.acct, - .visibility = status_visibility_str(locale, status->visibility), - .unmute = status->muted ? "un" : "", - .unmute_btn = status->muted ? "Unmute thread" : "Mute thread", - .unbookmark = status->bookmarked ? "un" : "", - .unbookmark_btn = status->bookmarked ? "Remove Bookmark" : "Bookmark", - .delete_status = delete_status, - .pin_status = pin_status, - .in_reply_to_str = in_reply_to_str, - .status_content = parse_content, - .attachments = attachments, - .interactions = interactions_html, - .emoji_reactions = emoji_reactions, - .interaction_btns = interaction_btns, - .reply = post_response, - }; - - stat_html = tmpl_gen_status(&tmpl, size); - - // Cleanup - if (formatted_display_name != status->account.display_name && - formatted_display_name != serialized_display_name) - free(formatted_display_name); - if (serialized_display_name != status->account.display_name) - free(serialized_display_name); - free(interaction_btns); - free(in_reply_to_str); - free(attachments); - free(post_response); - free(emoji_reactions); - if (notif) free(notif_info); - free(delete_status); - free(pin_status); - free(interactions_html); - if (parse_content != status->content) - free(parse_content); - return stat_html; -} - -static char* construct_status_voidwrap(void* passed, size_t index, size_t* res) -{ - struct status_args* args = passed; - return construct_status(args->ssn, args->api, args->status + index, res, NULL, args->args, 0); -} - -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) -{ - if (!(statuses && size)) return NULL; - struct status_args stat_args = { - .api = api, - .status = statuses, - .args = args, - .ssn = ssn, - }; - return construct_func_strings(construct_status_voidwrap, &stat_args, size, ret_size); -} - void status_interact(PATH_ARGS) { char* referer = GET_ENV("HTTP_REFERER", req); try_interact_status(ssn, api, data[0]); - - FCGX_FPrintF(req->out, - "Status: 303 See Other\r\n" - "Location: %s#id-%s\r\n" - "Content-Length: 14\r\n\r\n" - "Redirecting...", - referer ? referer : "/", - data[0]); + + redirect(req, REDIRECT_303, referer); } void api_status_interact(PATH_ARGS) @@ -1015,131 +313,97 @@ void content_status_interactions(FCGX_Request* req, struct mstdnt_account* accts, size_t accts_len) { - char* accounts_html = construct_accounts(api, accts, accts_len, 0, NULL); - if (!accounts_html) - accounts_html = construct_error("No accounts", 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 (accts) + mXPUSHs(newRV_noinc((SV*)perlify_accounts(accts, accts_len))); + else ARG_UNDEFINED(); + mXPUSHs(newSVpv(label, 0)); - struct interactions_page_template tmpl = { - .back_ref = GET_ENV("HTTP_REFERER", req), - .interaction_str = label, - .accts = accounts_html - }; + PERL_STACK_SCALAR_CALL("account::status_interactions"); - char* output = tmpl_gen_interactions_page(&tmpl, NULL); + char* dup = PERL_GET_STACK_EXIT; struct base_page page = { .category = BASE_CAT_NONE, - .content = output, + .content = dup, + .session = session_hv, .sidebar_left = NULL }; render_base_page(&page, req, ssn, api); // Cleanup - free(accounts_html); - free(output); + Safefree(dup); } void content_status(PATH_ARGS, uint8_t flags) { struct mstdnt_args m_args; set_mstdnt_args(&m_args, ssn); - char* output; - // Status context struct mstdnt_storage storage = {0}, status_storage = {0}; struct mstdnt_status* statuses_before = NULL, *statuses_after = NULL, status = { 0 }; + char* picker = NULL; + size_t picker_len; size_t stat_before_len = 0, stat_after_len = 0; - char* before_html = NULL, *stat_html = NULL, *after_html = NULL, *stat_reply = NULL, - * thread_pagination = NULL; - - int stat_after_limit = 15; - int stat_before_limit = 15; -#define enough_statuses_before (stat_before_len > stat_before_limit) -#define enough_statuses_after (stat_after_len > stat_after_limit) - + try_post_status(ssn, api); + mastodont_get_status(api, &m_args, data[0], &status_storage, &status); mastodont_get_status_context(api, &m_args, data[0], &storage, &statuses_before, &statuses_after, &stat_before_len, &stat_after_len); + + if ((flags & STATUS_EMOJI_PICKER) == STATUS_EMOJI_PICKER) + picker = construct_emoji_picker(status.id, &picker_len); + + + 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_status(&status))); + if (statuses_before) + mXPUSHs(newRV_noinc((SV*)perlify_statuses(statuses_before, stat_before_len))); + else + ARG_UNDEFINED(); - // Get information - if (mastodont_get_status(api, &m_args, data[0], &status_storage, &status)) + if (statuses_after) + mXPUSHs(newRV_noinc((SV*)perlify_statuses(statuses_after, stat_after_len))); + else + ARG_UNDEFINED(); + + if (picker) { - stat_html = construct_error("Status not found", E_ERROR, 1, NULL); - } - else { - before_html = construct_statuses(ssn, api, - (enough_statuses_before ? - statuses_before + (stat_before_len - stat_before_limit) : statuses_before), - (enough_statuses_before ? - stat_before_limit : stat_before_len), - NULL, 0); + mXPUSHs(newSVpv(picker, picker_len)); + } else ARG_UNDEFINED(); - // Current status - stat_html = construct_status(ssn, api, &status, NULL, NULL, NULL, flags); - if ((flags & STATUS_REPLY) == STATUS_REPLY) - { - stat_reply = reply_status(ssn, - data[0], - &status); - } - } + PERL_STACK_SCALAR_CALL("status::content_status"); - // After... - // For pagination, we already start at the first, so no math required here - after_html = construct_statuses(ssn, api, statuses_after, - (enough_statuses_after ? stat_after_limit : stat_after_len), - NULL, 0); - - // Thread pagination buttons - if (statuses_before || statuses_after) - { - struct thread_page_btn_template pagination_tmpl = { - .prefix = config_url_prefix, - .status_first = (statuses_before ? statuses_before[0].id : "deadbeef"), - .status_last = (statuses_after ? statuses_after[stat_after_len-1].id : "deadbeef"), - .status_before = (statuses_before && enough_statuses_before ? statuses_before[stat_before_len - stat_before_limit].id : "deadbeef"), - .status_after = (statuses_after && enough_statuses_after ? - statuses_after[stat_after_limit].id : "deadbeef"), - }; - thread_pagination = tmpl_gen_thread_page_btn(&pagination_tmpl, NULL); - } - - - easprintf(&output, "%s%s%s%s%s%s", - thread_pagination ? thread_pagination : "", - before_html ? before_html : "", - stat_html ? stat_html : "", - stat_reply ? stat_reply : "", - after_html ? after_html : "", - thread_pagination ? thread_pagination : ""); + char* dup = PERL_GET_STACK_EXIT; struct base_page b = { .category = BASE_CAT_NONE, - .content = output, + .content = dup, + .session = session_hv, .sidebar_left = NULL }; // Output render_base_page(&b, req, ssn, api); - // Cleanup - free(before_html); - free(stat_html); - free(after_html); - free(output); - free(thread_pagination); - if ((flags & STATUS_REPLY) == STATUS_REPLY) - free(stat_reply); mstdnt_cleanup_statuses(statuses_before, stat_before_len); mstdnt_cleanup_statuses(statuses_after, stat_after_len); mstdnt_cleanup_status(&status); mastodont_storage_cleanup(&storage); mastodont_storage_cleanup(&status_storage); + Safefree(dup); + free(picker); } void notice_redirect(PATH_ARGS) @@ -1149,3 +413,61 @@ void notice_redirect(PATH_ARGS) redirect(req, REDIRECT_303, url); free(url); } + +HV* perlify_status_pleroma(const struct mstdnt_status_pleroma* pleroma) +{ + if (!pleroma) return NULL; + + HV* pleroma_hv = newHV(); + hvstores_int(pleroma_hv, "conversation_id", pleroma->conversation_id); + hvstores_int(pleroma_hv, "direct_conversation_id", pleroma->direct_conversation_id); + hvstores_int(pleroma_hv, "thread_muted", pleroma->thread_muted); + hvstores_int(pleroma_hv, "local", pleroma->local); + hvstores_int(pleroma_hv, "parent_visible", pleroma->parent_visible); + hvstores_ref(pleroma_hv, "emoji_reactions", + perlify_emoji_reactions(pleroma->emoji_reactions, pleroma->emoji_reactions_len)); + hvstores_str(pleroma_hv, "expires_at", pleroma->expires_at); + hvstores_str(pleroma_hv, "in_reply_to_account_acct", pleroma->in_reply_to_account_acct); + + return pleroma_hv; +} + +HV* perlify_status(const struct mstdnt_status* status) +{ + if (!status) return NULL; + + HV* status_hv = newHV(); + hvstores_str(status_hv, "id", status->id); + hvstores_str(status_hv, "uri", status->uri); + hvstores_int(status_hv, "created_at", status->created_at); + hvstores_ref(status_hv, "account", perlify_account(&(status->account))); + hvstores_str(status_hv, "content", status->content); + hvstores_str(status_hv, "spoiler_text", status->spoiler_text); + hvstores_str(status_hv, "url", status->url); + hvstores_str(status_hv, "in_reply_to_id", status->in_reply_to_id); + hvstores_str(status_hv, "in_reply_to_account_id", status->in_reply_to_account_id); + hvstores_str(status_hv, "language", status->language); + hvstores_str(status_hv, "text", status->text); + + hvstores_int(status_hv, "favourited", status->favourited); + hvstores_int(status_hv, "reblogged", status->reblogged); + hvstores_int(status_hv, "muted", status->muted); + hvstores_int(status_hv, "bookmarked", status->bookmarked); + hvstores_int(status_hv, "pinned", status->pinned); + + hvstores_int(status_hv, "sensitive", status->sensitive); + hvstores_int(status_hv, "visibility", ((int)(status->visibility))); + hvstores_int(status_hv, "reblogs_count", status->reblogs_count); + hvstores_int(status_hv, "favourites_count", status->favourites_count); + hvstores_int(status_hv, "replies_count", status->replies_count); + hvstores_ref(status_hv, "reblog", perlify_status(status->reblog)); + hvstores_ref(status_hv, "application", perlify_application(status->application)); + hvstores_ref(status_hv, "media_attachments", + perlify_attachments(status->media_attachments, status->media_attachments_len)); + hvstores_ref(status_hv, "emojis", perlify_emojis(status->emojis, status->emojis_len)); + hvstores_ref(status_hv, "pleroma", perlify_status_pleroma(&(status->pleroma))); + + return status_hv; +} + +PERLIFY_MULTI(status, statuses, mstdnt_status) diff --git a/src/status.h b/src/status.h index 1084105..f90648c 100644 --- a/src/status.h +++ b/src/status.h @@ -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 diff --git a/src/template/main.c b/src/template/main.c deleted file mode 100644 index 97219bc..0000000 --- a/src/template/main.c +++ /dev/null @@ -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 . - */ - - -#include -#include -#include -#include - -// 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 \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; -} - diff --git a/src/test.c b/src/test.c deleted file mode 100644 index 433b90b..0000000 --- a/src/test.c +++ /dev/null @@ -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 . - */ - -#include -#include -#include "test.h" -#include "../config.h" -#include "base_page.h" -#include "easprintf.h" - -// Pages -#include "../static/test.ctmpl" - -#define ENV_NOT_FOUND "ENV Not Found" - -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); -} diff --git a/src/timeline.c b/src/timeline.c index 445523f..b5cf850 100644 --- a/src/timeline.c +++ b/src/timeline.c @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +#include "global_perl.h" #include "timeline.h" #include #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, "

%s

", - 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); } diff --git a/src/timeline.h b/src/timeline.h index c2cfcd3..a4e58f4 100644 --- a/src/timeline.h +++ b/src/timeline.h @@ -18,28 +18,67 @@ #ifndef TIMELINE_H #define TIMELINE_H -#include -#include #include #include #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 diff --git a/src/type_string.c b/src/type_string.c deleted file mode 100644 index 24d20f7..0000000 --- a/src/type_string.c +++ /dev/null @@ -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 . - */ - -#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 ""; - } -} diff --git a/src/type_string.h b/src/type_string.h deleted file mode 100644 index a24f403..0000000 --- a/src/type_string.h +++ /dev/null @@ -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 . - */ - -#ifndef TYPE_STRING_H -#define TYPE_STRING_H -#include -#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 diff --git a/static/about.tmpl b/static/about.tmpl deleted file mode 100644 index 244c51d..0000000 --- a/static/about.tmpl +++ /dev/null @@ -1,28 +0,0 @@ -
- - - \ No newline at end of file diff --git a/static/account.tmpl b/static/account.tmpl deleted file mode 100644 index aad5b62..0000000 --- a/static/account.tmpl +++ /dev/null @@ -1,67 +0,0 @@ -{{%s:is_blocked}} -{{%s:menubar}} - - -{{%s:info}} - - - - - - - - -
- - - - - - - -
- - diff --git a/static/account_current_menubar.tmpl b/static/account_current_menubar.tmpl deleted file mode 100644 index 5ff4be2..0000000 --- a/static/account_current_menubar.tmpl +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/static/account_follow_btn.tmpl b/static/account_follow_btn.tmpl deleted file mode 100644 index 6157957..0000000 --- a/static/account_follow_btn.tmpl +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/static/account_info.tmpl b/static/account_info.tmpl deleted file mode 100644 index e03edf7..0000000 --- a/static/account_info.tmpl +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/static/account_sidebar.tmpl b/static/account_sidebar.tmpl deleted file mode 100644 index 278a61b..0000000 --- a/static/account_sidebar.tmpl +++ /dev/null @@ -1,36 +0,0 @@ - - diff --git a/static/attachment_audio.tmpl b/static/attachment_audio.tmpl deleted file mode 100644 index 3f63b21..0000000 --- a/static/attachment_audio.tmpl +++ /dev/null @@ -1,7 +0,0 @@ -
- -
-
diff --git a/static/attachment_gifv.tmpl b/static/attachment_gifv.tmpl deleted file mode 100644 index 0dc17c8..0000000 --- a/static/attachment_gifv.tmpl +++ /dev/null @@ -1,7 +0,0 @@ -
- - {{%s:sensitive}} -
diff --git a/static/attachment_image.tmpl b/static/attachment_image.tmpl deleted file mode 100644 index ec75542..0000000 --- a/static/attachment_image.tmpl +++ /dev/null @@ -1,4 +0,0 @@ -
- - {{%s:sensitive}} -
diff --git a/static/attachment_link.tmpl b/static/attachment_link.tmpl deleted file mode 100644 index 83dc9fd..0000000 --- a/static/attachment_link.tmpl +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/static/attachment_video.tmpl b/static/attachment_video.tmpl deleted file mode 100644 index 254e90d..0000000 --- a/static/attachment_video.tmpl +++ /dev/null @@ -1,7 +0,0 @@ -
- - {{%s:sensitive}} -
diff --git a/static/attachments.tmpl b/static/attachments.tmpl deleted file mode 100644 index 9879028..0000000 --- a/static/attachments.tmpl +++ /dev/null @@ -1,3 +0,0 @@ -
- {{%s:attachments}} -
diff --git a/static/bar.tmpl b/static/bar.tmpl deleted file mode 100644 index c3eceda..0000000 --- a/static/bar.tmpl +++ /dev/null @@ -1,3 +0,0 @@ -
-
-
diff --git a/static/bar_graph.tmpl b/static/bar_graph.tmpl deleted file mode 100644 index 83cd3f6..0000000 --- a/static/bar_graph.tmpl +++ /dev/null @@ -1,3 +0,0 @@ -
- {{%s:graph}} -
diff --git a/static/basic_page.tmpl b/static/basic_page.tmpl deleted file mode 100644 index d7f455b..0000000 --- a/static/basic_page.tmpl +++ /dev/null @@ -1,4 +0,0 @@ -
-

< {{ %s : page_title }}

-
-{{%s : page_content}} diff --git a/static/bookmarks_page.tmpl b/static/bookmarks_page.tmpl deleted file mode 100644 index 0e34b35..0000000 --- a/static/bookmarks_page.tmpl +++ /dev/null @@ -1,7 +0,0 @@ -
-

Bookmarks

-
-
- {{%s:statuses}} -
-{{%s:navigation}} diff --git a/static/chat.tmpl b/static/chat.tmpl deleted file mode 100644 index c5680cb..0000000 --- a/static/chat.tmpl +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - -
- - -
-
- {{%s:display_name}} -
-
- {{ %s : last_message }} -
-
-
-
diff --git a/static/chat_view.tmpl b/static/chat_view.tmpl deleted file mode 100644 index 75ceeaf..0000000 --- a/static/chat_view.tmpl +++ /dev/null @@ -1,21 +0,0 @@ - -
- {{ %s : messages }} -
-
-
- - - - - -
- - - -
-
diff --git a/static/chats_page.tmpl b/static/chats_page.tmpl deleted file mode 100644 index 4ffef75..0000000 --- a/static/chats_page.tmpl +++ /dev/null @@ -1 +0,0 @@ -{{ %s : content }} diff --git a/static/config_appearance.tmpl b/static/config_appearance.tmpl deleted file mode 100644 index 764f3ef..0000000 --- a/static/config_appearance.tmpl +++ /dev/null @@ -1,42 +0,0 @@ -
-
- - -

Appearance

- -

Theme variant

-
    -
  • - - -
  • -
-

Color Scheme

-
    -
  • - - -
  • -
  • - - -
  • -
-

Background

-
    -
  • - -
  • -
-
    -
  • - Sidebar opacity: -
  • -
- - -
-
diff --git a/static/config_general.tmpl b/static/config_general.tmpl deleted file mode 100644 index 50c8ffb..0000000 --- a/static/config_general.tmpl +++ /dev/null @@ -1,94 +0,0 @@ -
-
- - - -

General

- -

Locales

-
    -
  • - - -
  • -
- -

JavaScript

-
    -
  • - - -
  • - - - - - - - - - - - - -
- -

Statuses

-
    -
  • - - -
  • -
  • - - -
  • -
  • - - -
  • -
  • - - -
  • -
  • - - -
  • - - - - - - - - -
- -

Instance

-
    - - - - -
  • - - -
  • -
- -

Notifications

-
    -
  • - - -
  • -
- - -
-
diff --git a/static/config_sidebar.tmpl b/static/config_sidebar.tmpl deleted file mode 100644 index 8cdae5b..0000000 --- a/static/config_sidebar.tmpl +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/static/contact.tmpl b/static/contact.tmpl deleted file mode 100644 index 4f6f4af..0000000 --- a/static/contact.tmpl +++ /dev/null @@ -1,19 +0,0 @@ - - - - - -
- -
diff --git a/static/custom_emoji_reaction.tmpl b/static/custom_emoji_reaction.tmpl deleted file mode 100644 index 1904aa6..0000000 --- a/static/custom_emoji_reaction.tmpl +++ /dev/null @@ -1 +0,0 @@ - diff --git a/static/directs_page.tmpl b/static/directs_page.tmpl deleted file mode 100644 index 18a32eb..0000000 --- a/static/directs_page.tmpl +++ /dev/null @@ -1,6 +0,0 @@ -
-

Direct Messages

-
-
- {{%s:direct_content}} -
diff --git a/static/embed.tmpl b/static/embed.tmpl deleted file mode 100644 index 20b1112..0000000 --- a/static/embed.tmpl +++ /dev/null @@ -1,21 +0,0 @@ - - - - Embed - - - - - -
- {{ %s : embed }} -
- - diff --git a/static/emoji.tmpl b/static/emoji.tmpl deleted file mode 100644 index 67b6917..0000000 --- a/static/emoji.tmpl +++ /dev/null @@ -1 +0,0 @@ -{{%s:emoji}} diff --git a/static/emoji_picker.tmpl b/static/emoji_picker.tmpl deleted file mode 100644 index 70cd38a..0000000 --- a/static/emoji_picker.tmpl +++ /dev/null @@ -1,81 +0,0 @@ -
- - - - - - - - - - - -
- - - - - - - - - - - - - - - -
- -
- -
- {{%s:emojis_smileys}} -
- -
- {{%s:emojis_animals}} -
- -
- {{%s:emojis_food}} -
- -
- {{%s:emojis_travel}} -
- -
- {{%s:emojis_activities}} -
- -
- {{%s:emojis_objects}} -
- -
- {{%s:emojis_symbols}} -
- -
- {{%s:emojis_flags}} -
-
-
diff --git a/static/emoji_plain.tmpl b/static/emoji_plain.tmpl deleted file mode 100644 index 5fc698f..0000000 --- a/static/emoji_plain.tmpl +++ /dev/null @@ -1 +0,0 @@ -{{%s:emoji}} diff --git a/static/emoji_reaction.tmpl b/static/emoji_reaction.tmpl deleted file mode 100644 index 1d1b710..0000000 --- a/static/emoji_reaction.tmpl +++ /dev/null @@ -1 +0,0 @@ -{{%s:emoji_display}} {{%u:emoji_count}} diff --git a/static/emoji_reactions.tmpl b/static/emoji_reactions.tmpl deleted file mode 100644 index 0c78115..0000000 --- a/static/emoji_reactions.tmpl +++ /dev/null @@ -1,3 +0,0 @@ -
    - {{%s:emojis}} -
diff --git a/static/error.tmpl b/static/error.tmpl deleted file mode 100644 index 37ee540..0000000 --- a/static/error.tmpl +++ /dev/null @@ -1 +0,0 @@ -{{%s:error}} diff --git a/static/error_404.tmpl b/static/error_404.tmpl deleted file mode 100644 index 6291b6b..0000000 --- a/static/error_404.tmpl +++ /dev/null @@ -1,4 +0,0 @@ -
-

404

-

{{%s:error}}

-
diff --git a/static/expand_btn.tmpl b/static/expand_btn.tmpl deleted file mode 100644 index 19e5569..0000000 --- a/static/expand_btn.tmpl +++ /dev/null @@ -1 +0,0 @@ - diff --git a/static/expand_btn_img.tmpl b/static/expand_btn_img.tmpl deleted file mode 100644 index 6a57ac1..0000000 --- a/static/expand_btn_img.tmpl +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/favourites_page.tmpl b/static/favourites_page.tmpl deleted file mode 100644 index 60de2ff..0000000 --- a/static/favourites_page.tmpl +++ /dev/null @@ -1,7 +0,0 @@ -
-

Favorites

-
-
- {{%s:statuses}} -
-{{%s:navigation}} diff --git a/static/follow_svg.tmpl b/static/follow_svg.tmpl deleted file mode 100644 index 1c4063b..0000000 --- a/static/follow_svg.tmpl +++ /dev/null @@ -1 +0,0 @@ - diff --git a/static/hashtag.tmpl b/static/hashtag.tmpl deleted file mode 100644 index 7acfba1..0000000 --- a/static/hashtag.tmpl +++ /dev/null @@ -1 +0,0 @@ -#{{%s:tag}} diff --git a/static/hashtag_page.tmpl b/static/hashtag_page.tmpl deleted file mode 100644 index bbf8dd7..0000000 --- a/static/hashtag_page.tmpl +++ /dev/null @@ -1,7 +0,0 @@ -
-

Hashtag - #{{%s:tag}}

-
-
- {{%s:statuses}} -
-{{%s:navigation}} diff --git a/static/in_reply_to.tmpl b/static/in_reply_to.tmpl deleted file mode 100644 index 0b75e40..0000000 --- a/static/in_reply_to.tmpl +++ /dev/null @@ -1,3 +0,0 @@ - - {{%s:in_reply_to_text}} {{%s:acct}} - diff --git a/static/index.tmpl b/static/index.tmpl deleted file mode 100644 index 394338f..0000000 --- a/static/index.tmpl +++ /dev/null @@ -1,87 +0,0 @@ - - - - - {{ %s : title }} - - - {{ %s : theme_str }} - - - - - -
- - - - - - - - - - - - - - -
- {{ %s : main }} -
-
- - - - - - diff --git a/static/instance.tmpl b/static/instance.tmpl deleted file mode 100644 index bc95646..0000000 --- a/static/instance.tmpl +++ /dev/null @@ -1 +0,0 @@ -Instance information diff --git a/static/interaction_buttons.tmpl b/static/interaction_buttons.tmpl deleted file mode 100644 index 641f8d8..0000000 --- a/static/interaction_buttons.tmpl +++ /dev/null @@ -1,39 +0,0 @@ -
- - - - - - {{%s:likeboost_btn}} - {{%s:reactions_btn}} - - - -
- {{ %s : reply_btn }} - -
- - -
-
-
- - -
-
- - {{ %s : expand_btn }} - - - {{%s:rel_time}} -
-
diff --git a/static/interactions_page.tmpl b/static/interactions_page.tmpl deleted file mode 100644 index 46aea1b..0000000 --- a/static/interactions_page.tmpl +++ /dev/null @@ -1,4 +0,0 @@ -
-

< {{ %s : interaction_str }}

-
-{{ %s : accts }} diff --git a/static/license.tmpl b/static/license.tmpl deleted file mode 100644 index 27986e8..0000000 --- a/static/license.tmpl +++ /dev/null @@ -1,680 +0,0 @@ -
-

< {{ %s : license_str }}

-

GNU AFFERO GENERAL PUBLIC LICENSE

-

Version 3, 19 November 2007

- -

Copyright © 2007 Free Software Foundation, - Inc. <https://fsf.org/> -
- Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed.

- -

Preamble

- -

The GNU Affero General Public License is a free, copyleft license - for software and other kinds of works, specifically designed to ensure - cooperation with the community in the case of network server software.

- -

The licenses for most software and other practical works are - designed to take away your freedom to share and change the works. By - contrast, our General Public Licenses are intended to guarantee your - freedom to share and change all versions of a program--to make sure it - remains free software for all its users.

- -

When we speak of free software, we are referring to freedom, not - price. Our General Public Licenses are designed to make sure that you - have the freedom to distribute copies of free software (and charge for - them if you wish), that you receive source code or can get it if you - want it, that you can change the software or use pieces of it in new - free programs, and that you know you can do these things.

- -

Developers that use our General Public Licenses protect your rights - with two steps: (1) assert copyright on the software, and (2) offer - you this License which gives you legal permission to copy, distribute - and/or modify the software.

- -

A secondary benefit of defending all users' freedom is that - improvements made in alternate versions of the program, if they - receive widespread use, become available for other developers to - incorporate. Many developers of free software are heartened and - encouraged by the resulting cooperation. However, in the case of - software used on network servers, this result may fail to come about. - The GNU General Public License permits making a modified version and - letting the public access it on a server without ever releasing its - source code to the public.

- -

The GNU Affero General Public License is designed specifically to - ensure that, in such cases, the modified source code becomes available - to the community. It requires the operator of a network server to - provide the source code of the modified version running there to the - users of that server. Therefore, public use of a modified version, on - a publicly accessible server, gives the public access to the source - code of the modified version.

- -

An older license, called the Affero General Public License and - published by Affero, was designed to accomplish similar goals. This is - a different license, not a version of the Affero GPL, but Affero has - released a new version of the Affero GPL which permits relicensing under - this license.

- -

The precise terms and conditions for copying, distribution and - modification follow.

- -

TERMS AND CONDITIONS

- -

0. Definitions.

- -

"This License" refers to version 3 of the GNU Affero General Public - License.

- -

"Copyright" also means copyright-like laws that apply to other kinds - of works, such as semiconductor masks.

- -

"The Program" refers to any copyrightable work licensed under this - License. Each licensee is addressed as "you". "Licensees" and - "recipients" may be individuals or organizations.

- -

To "modify" a work means to copy from or adapt all or part of the work - in a fashion requiring copyright permission, other than the making of an - exact copy. The resulting work is called a "modified version" of the - earlier work or a work "based on" the earlier work.

- -

A "covered work" means either the unmodified Program or a work based - on the Program.

- -

To "propagate" a work means to do anything with it that, without - permission, would make you directly or secondarily liable for - infringement under applicable copyright law, except executing it on a - computer or modifying a private copy. Propagation includes copying, - distribution (with or without modification), making available to the - public, and in some countries other activities as well.

- -

To "convey" a work means any kind of propagation that enables other - parties to make or receive copies. Mere interaction with a user through - a computer network, with no transfer of a copy, is not conveying.

- -

An interactive user interface displays "Appropriate Legal Notices" - to the extent that it includes a convenient and prominently visible - feature that (1) displays an appropriate copyright notice, and (2) - tells the user that there is no warranty for the work (except to the - extent that warranties are provided), that licensees may convey the - work under this License, and how to view a copy of this License. If - the interface presents a list of user commands or options, such as a - menu, a prominent item in the list meets this criterion.

- -

1. Source Code.

- -

The "source code" for a work means the preferred form of the work - for making modifications to it. "Object code" means any non-source - form of a work.

- -

A "Standard Interface" means an interface that either is an official - standard defined by a recognized standards body, or, in the case of - interfaces specified for a particular programming language, one that - is widely used among developers working in that language.

- -

The "System Libraries" of an executable work include anything, other - than the work as a whole, that (a) is included in the normal form of - packaging a Major Component, but which is not part of that Major - Component, and (b) serves only to enable use of the work with that - Major Component, or to implement a Standard Interface for which an - implementation is available to the public in source code form. A - "Major Component", in this context, means a major essential component - (kernel, window system, and so on) of the specific operating system - (if any) on which the executable work runs, or a compiler used to - produce the work, or an object code interpreter used to run it.

- -

The "Corresponding Source" for a work in object code form means all - the source code needed to generate, install, and (for an executable - work) run the object code and to modify the work, including scripts to - control those activities. However, it does not include the work's - System Libraries, or general-purpose tools or generally available free - programs which are used unmodified in performing those activities but - which are not part of the work. For example, Corresponding Source - includes interface definition files associated with source files for - the work, and the source code for shared libraries and dynamically - linked subprograms that the work is specifically designed to require, - such as by intimate data communication or control flow between those - subprograms and other parts of the work.

- -

The Corresponding Source need not include anything that users - can regenerate automatically from other parts of the Corresponding - Source.

- -

The Corresponding Source for a work in source code form is that - same work.

- -

2. Basic Permissions.

- -

All rights granted under this License are granted for the term of - copyright on the Program, and are irrevocable provided the stated - conditions are met. This License explicitly affirms your unlimited - permission to run the unmodified Program. The output from running a - covered work is covered by this License only if the output, given its - content, constitutes a covered work. This License acknowledges your - rights of fair use or other equivalent, as provided by copyright law.

- -

You may make, run and propagate covered works that you do not - convey, without conditions so long as your license otherwise remains - in force. You may convey covered works to others for the sole purpose - of having them make modifications exclusively for you, or provide you - with facilities for running those works, provided that you comply with - the terms of this License in conveying all material for which you do - not control copyright. Those thus making or running the covered works - for you must do so exclusively on your behalf, under your direction - and control, on terms that prohibit them from making any copies of - your copyrighted material outside their relationship with you.

- -

Conveying under any other circumstances is permitted solely under - the conditions stated below. Sublicensing is not allowed; section 10 - makes it unnecessary.

- -

3. Protecting Users' Legal Rights From Anti-Circumvention Law.

- -

No covered work shall be deemed part of an effective technological - measure under any applicable law fulfilling obligations under article - 11 of the WIPO copyright treaty adopted on 20 December 1996, or - similar laws prohibiting or restricting circumvention of such - measures.

- -

When you convey a covered work, you waive any legal power to forbid - circumvention of technological measures to the extent such circumvention - is effected by exercising rights under this License with respect to - the covered work, and you disclaim any intention to limit operation or - modification of the work as a means of enforcing, against the work's - users, your or third parties' legal rights to forbid circumvention of - technological measures.

- -

4. Conveying Verbatim Copies.

- -

You may convey verbatim copies of the Program's source code as you - receive it, in any medium, provided that you conspicuously and - appropriately publish on each copy an appropriate copyright notice; - keep intact all notices stating that this License and any - non-permissive terms added in accord with section 7 apply to the code; - keep intact all notices of the absence of any warranty; and give all - recipients a copy of this License along with the Program.

- -

You may charge any price or no price for each copy that you convey, - and you may offer support or warranty protection for a fee.

- -

5. Conveying Modified Source Versions.

- -

You may convey a work based on the Program, or the modifications to - produce it from the Program, in the form of source code under the - terms of section 4, provided that you also meet all of these conditions:

- -
    - -
  • a) The work must carry prominent notices stating that you modified - it, and giving a relevant date.
  • - -
  • b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices".
  • - -
  • c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it.
  • - -
  • d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so.
  • - -
- -

A compilation of a covered work with other separate and independent - works, which are not by their nature extensions of the covered work, - and which are not combined with it such as to form a larger program, - in or on a volume of a storage or distribution medium, is called an - "aggregate" if the compilation and its resulting copyright are not - used to limit the access or legal rights of the compilation's users - beyond what the individual works permit. Inclusion of a covered work - in an aggregate does not cause this License to apply to the other - parts of the aggregate.

- -

6. Conveying Non-Source Forms.

- -

You may convey a covered work in object code form under the terms - of sections 4 and 5, provided that you also convey the - machine-readable Corresponding Source under the terms of this License, - in one of these ways:

- -
    - -
  • a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange.
  • - -
  • b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge.
  • - -
  • c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b.
  • - -
  • d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements.
  • - -
  • e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d.
  • - -
- -

A separable portion of the object code, whose source code is excluded - from the Corresponding Source as a System Library, need not be - included in conveying the object code work.

- -

A "User Product" is either (1) a "consumer product", which means any - tangible personal property which is normally used for personal, family, - or household purposes, or (2) anything designed or sold for incorporation - into a dwelling. In determining whether a product is a consumer product, - doubtful cases shall be resolved in favor of coverage. For a particular - product received by a particular user, "normally used" refers to a - typical or common use of that class of product, regardless of the status - of the particular user or of the way in which the particular user - actually uses, or expects or is expected to use, the product. A product - is a consumer product regardless of whether the product has substantial - commercial, industrial or non-consumer uses, unless such uses represent - the only significant mode of use of the product.

- -

"Installation Information" for a User Product means any methods, - procedures, authorization keys, or other information required to install - and execute modified versions of a covered work in that User Product from - a modified version of its Corresponding Source. The information must - suffice to ensure that the continued functioning of the modified object - code is in no case prevented or interfered with solely because - modification has been made.

- -

If you convey an object code work under this section in, or with, or - specifically for use in, a User Product, and the conveying occurs as - part of a transaction in which the right of possession and use of the - User Product is transferred to the recipient in perpetuity or for a - fixed term (regardless of how the transaction is characterized), the - Corresponding Source conveyed under this section must be accompanied - by the Installation Information. But this requirement does not apply - if neither you nor any third party retains the ability to install - modified object code on the User Product (for example, the work has - been installed in ROM).

- -

The requirement to provide Installation Information does not include a - requirement to continue to provide support service, warranty, or updates - for a work that has been modified or installed by the recipient, or for - the User Product in which it has been modified or installed. Access to a - network may be denied when the modification itself materially and - adversely affects the operation of the network or violates the rules and - protocols for communication across the network.

- -

Corresponding Source conveyed, and Installation Information provided, - in accord with this section must be in a format that is publicly - documented (and with an implementation available to the public in - source code form), and must require no special password or key for - unpacking, reading or copying.

- -

7. Additional Terms.

- -

"Additional permissions" are terms that supplement the terms of this - License by making exceptions from one or more of its conditions. - Additional permissions that are applicable to the entire Program shall - be treated as though they were included in this License, to the extent - that they are valid under applicable law. If additional permissions - apply only to part of the Program, that part may be used separately - under those permissions, but the entire Program remains governed by - this License without regard to the additional permissions.

- -

When you convey a copy of a covered work, you may at your option - remove any additional permissions from that copy, or from any part of - it. (Additional permissions may be written to require their own - removal in certain cases when you modify the work.) You may place - additional permissions on material, added by you to a covered work, - for which you have or can give appropriate copyright permission.

- -

Notwithstanding any other provision of this License, for material you - add to a covered work, you may (if authorized by the copyright holders of - that material) supplement the terms of this License with terms:

- -
    - -
  • a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or
  • - -
  • b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or
  • - -
  • c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or
  • - -
  • d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or
  • - -
  • e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or
  • - -
  • f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors.
  • - -
- -

All other non-permissive additional terms are considered "further - restrictions" within the meaning of section 10. If the Program as you - received it, or any part of it, contains a notice stating that it is - governed by this License along with a term that is a further restriction, - you may remove that term. If a license document contains a further - restriction but permits relicensing or conveying under this License, you - may add to a covered work material governed by the terms of that license - document, provided that the further restriction does not survive such - relicensing or conveying.

- -

If you add terms to a covered work in accord with this section, you - must place, in the relevant source files, a statement of the - additional terms that apply to those files, or a notice indicating - where to find the applicable terms.

- -

Additional terms, permissive or non-permissive, may be stated in the - form of a separately written license, or stated as exceptions; - the above requirements apply either way.

- -

8. Termination.

- -

You may not propagate or modify a covered work except as expressly - provided under this License. Any attempt otherwise to propagate or - modify it is void, and will automatically terminate your rights under - this License (including any patent licenses granted under the third - paragraph of section 11).

- -

However, if you cease all violation of this License, then your - license from a particular copyright holder is reinstated (a) - provisionally, unless and until the copyright holder explicitly and - finally terminates your license, and (b) permanently, if the copyright - holder fails to notify you of the violation by some reasonable means - prior to 60 days after the cessation.

- -

Moreover, your license from a particular copyright holder is - reinstated permanently if the copyright holder notifies you of the - violation by some reasonable means, this is the first time you have - received notice of violation of this License (for any work) from that - copyright holder, and you cure the violation prior to 30 days after - your receipt of the notice.

- -

Termination of your rights under this section does not terminate the - licenses of parties who have received copies or rights from you under - this License. If your rights have been terminated and not permanently - reinstated, you do not qualify to receive new licenses for the same - material under section 10.

- -

9. Acceptance Not Required for Having Copies.

- -

You are not required to accept this License in order to receive or - run a copy of the Program. Ancillary propagation of a covered work - occurring solely as a consequence of using peer-to-peer transmission - to receive a copy likewise does not require acceptance. However, - nothing other than this License grants you permission to propagate or - modify any covered work. These actions infringe copyright if you do - not accept this License. Therefore, by modifying or propagating a - covered work, you indicate your acceptance of this License to do so.

- -

10. Automatic Licensing of Downstream Recipients.

- -

Each time you convey a covered work, the recipient automatically - receives a license from the original licensors, to run, modify and - propagate that work, subject to this License. You are not responsible - for enforcing compliance by third parties with this License.

- -

An "entity transaction" is a transaction transferring control of an - organization, or substantially all assets of one, or subdividing an - organization, or merging organizations. If propagation of a covered - work results from an entity transaction, each party to that - transaction who receives a copy of the work also receives whatever - licenses to the work the party's predecessor in interest had or could - give under the previous paragraph, plus a right to possession of the - Corresponding Source of the work from the predecessor in interest, if - the predecessor has it or can get it with reasonable efforts.

- -

You may not impose any further restrictions on the exercise of the - rights granted or affirmed under this License. For example, you may - not impose a license fee, royalty, or other charge for exercise of - rights granted under this License, and you may not initiate litigation - (including a cross-claim or counterclaim in a lawsuit) alleging that - any patent claim is infringed by making, using, selling, offering for - sale, or importing the Program or any portion of it.

- -

11. Patents.

- -

A "contributor" is a copyright holder who authorizes use under this - License of the Program or a work on which the Program is based. The - work thus licensed is called the contributor's "contributor version".

- -

A contributor's "essential patent claims" are all patent claims - owned or controlled by the contributor, whether already acquired or - hereafter acquired, that would be infringed by some manner, permitted - by this License, of making, using, or selling its contributor version, - but do not include claims that would be infringed only as a - consequence of further modification of the contributor version. For - purposes of this definition, "control" includes the right to grant - patent sublicenses in a manner consistent with the requirements of - this License.

- -

Each contributor grants you a non-exclusive, worldwide, royalty-free - patent license under the contributor's essential patent claims, to - make, use, sell, offer for sale, import and otherwise run, modify and - propagate the contents of its contributor version.

- -

In the following three paragraphs, a "patent license" is any express - agreement or commitment, however denominated, not to enforce a patent - (such as an express permission to practice a patent or covenant not to - sue for patent infringement). To "grant" such a patent license to a - party means to make such an agreement or commitment not to enforce a - patent against the party.

- -

If you convey a covered work, knowingly relying on a patent license, - and the Corresponding Source of the work is not available for anyone - to copy, free of charge and under the terms of this License, through a - publicly available network server or other readily accessible means, - then you must either (1) cause the Corresponding Source to be so - available, or (2) arrange to deprive yourself of the benefit of the - patent license for this particular work, or (3) arrange, in a manner - consistent with the requirements of this License, to extend the patent - license to downstream recipients. "Knowingly relying" means you have - actual knowledge that, but for the patent license, your conveying the - covered work in a country, or your recipient's use of the covered work - in a country, would infringe one or more identifiable patents in that - country that you have reason to believe are valid.

- -

If, pursuant to or in connection with a single transaction or - arrangement, you convey, or propagate by procuring conveyance of, a - covered work, and grant a patent license to some of the parties - receiving the covered work authorizing them to use, propagate, modify - or convey a specific copy of the covered work, then the patent license - you grant is automatically extended to all recipients of the covered - work and works based on it.

- -

A patent license is "discriminatory" if it does not include within - the scope of its coverage, prohibits the exercise of, or is - conditioned on the non-exercise of one or more of the rights that are - specifically granted under this License. You may not convey a covered - work if you are a party to an arrangement with a third party that is - in the business of distributing software, under which you make payment - to the third party based on the extent of your activity of conveying - the work, and under which the third party grants, to any of the - parties who would receive the covered work from you, a discriminatory - patent license (a) in connection with copies of the covered work - conveyed by you (or copies made from those copies), or (b) primarily - for and in connection with specific products or compilations that - contain the covered work, unless you entered into that arrangement, - or that patent license was granted, prior to 28 March 2007.

- -

Nothing in this License shall be construed as excluding or limiting - any implied license or other defenses to infringement that may - otherwise be available to you under applicable patent law.

- -

12. No Surrender of Others' Freedom.

- -

If conditions are imposed on you (whether by court order, agreement or - otherwise) that contradict the conditions of this License, they do not - excuse you from the conditions of this License. If you cannot convey a - covered work so as to satisfy simultaneously your obligations under this - License and any other pertinent obligations, then as a consequence you may - not convey it at all. For example, if you agree to terms that obligate you - to collect a royalty for further conveying from those to whom you convey - the Program, the only way you could satisfy both those terms and this - License would be to refrain entirely from conveying the Program.

- -

13. Remote Network Interaction; Use with the GNU General Public License.

- -

Notwithstanding any other provision of this License, if you modify the - Program, your modified version must prominently offer all users - interacting with it remotely through a computer network (if your version - supports such interaction) an opportunity to receive the Corresponding - Source of your version by providing access to the Corresponding Source - from a network server at no charge, through some standard or customary - means of facilitating copying of software. This Corresponding Source - shall include the Corresponding Source for any work covered by version 3 - of the GNU General Public License that is incorporated pursuant to the - following paragraph.

- -

Notwithstanding any other provision of this License, you have permission - to link or combine any covered work with a work licensed under version 3 - of the GNU General Public License into a single combined work, and to - convey the resulting work. The terms of this License will continue to - apply to the part which is the covered work, but the work with which it is - combined will remain governed by version 3 of the GNU General Public - License.

- -

14. Revised Versions of this License.

- -

The Free Software Foundation may publish revised and/or new versions of - the GNU Affero General Public License from time to time. Such new - versions will be similar in spirit to the present version, but may differ - in detail to address new problems or concerns.

- -

Each version is given a distinguishing version number. If the - Program specifies that a certain numbered version of the GNU Affero - General Public License "or any later version" applies to it, you have - the option of following the terms and conditions either of that - numbered version or of any later version published by the Free - Software Foundation. If the Program does not specify a version number - of the GNU Affero General Public License, you may choose any version - ever published by the Free Software Foundation.

- -

If the Program specifies that a proxy can decide which future - versions of the GNU Affero General Public License can be used, that - proxy's public statement of acceptance of a version permanently - authorizes you to choose that version for the Program.

- -

Later license versions may give you additional or different - permissions. However, no additional obligations are imposed on any - author or copyright holder as a result of your choosing to follow a - later version.

- -

15. Disclaimer of Warranty.

- -

THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY - APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT - HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY - OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, - THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM - IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF - ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

- -

16. Limitation of Liability.

- -

IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING - WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS - THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY - GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE - USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF - DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD - PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), - EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF - SUCH DAMAGES.

- -

17. Interpretation of Sections 15 and 16.

- -

If the disclaimer of warranty and limitation of liability provided - above cannot be given local legal effect according to their terms, - reviewing courts shall apply local law that most closely approximates - an absolute waiver of all civil liability in connection with the - Program, unless a warranty or assumption of liability accompanies a - copy of the Program in return for a fee.

- -

END OF TERMS AND CONDITIONS

- -

How to Apply These Terms to Your New Programs

- -

If you develop a new program, and you want it to be of the greatest - possible use to the public, the best way to achieve this is to make it - free software which everyone can redistribute and change under these terms.

- -

To do so, attach the following notices to the program. It is safest - to attach them to the start of each source file to most effectively - state the exclusion of warranty; and each file should have at least - the "copyright" line and a pointer to where the full notice is found.

- -
    <one line to give the program's name and a brief idea of what it does.>
-    Copyright (C) <year>  <name of author>
-
-    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/>.
-  
- -

Also add information on how to contact you by electronic and paper mail.

- -

If your software can interact with users remotely through a computer - network, you should also make sure that it provides a way for users to - get its source. For example, if your program is a web application, its - interface could display a "Source" link that leads users to an archive - of the code. There are many ways you could offer source, and different - solutions will be better for different programs; see section 13 for the - specific requirements.

- -

You should also get your employer (if you work as a programmer) or school, - if any, to sign a "copyright disclaimer" for the program, if necessary. - For more information on this, and how to apply and follow the GNU AGPL, see - <https://www.gnu.org/licenses/>.

-
diff --git a/static/like_btn.tmpl b/static/like_btn.tmpl deleted file mode 100644 index ce973f2..0000000 --- a/static/like_btn.tmpl +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/static/like_btn_img.tmpl b/static/like_btn_img.tmpl deleted file mode 100644 index f9049a7..0000000 --- a/static/like_btn_img.tmpl +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/like_svg.tmpl b/static/like_svg.tmpl deleted file mode 100644 index 3b6527c..0000000 --- a/static/like_svg.tmpl +++ /dev/null @@ -1 +0,0 @@ - diff --git a/static/likeboost.tmpl b/static/likeboost.tmpl deleted file mode 100644 index bab876a..0000000 --- a/static/likeboost.tmpl +++ /dev/null @@ -1,9 +0,0 @@ - -
- - -
- diff --git a/static/list.tmpl b/static/list.tmpl deleted file mode 100644 index 85f9270..0000000 --- a/static/list.tmpl +++ /dev/null @@ -1,15 +0,0 @@ -
  • - {{%s:list}} - - -
    -
    - - -
    -
    -
  • diff --git a/static/list_edit.tmpl b/static/list_edit.tmpl deleted file mode 100644 index 9118d6c..0000000 --- a/static/list_edit.tmpl +++ /dev/null @@ -1 +0,0 @@ -asdasd \ No newline at end of file diff --git a/static/lists.tmpl b/static/lists.tmpl deleted file mode 100644 index 3db9ec1..0000000 --- a/static/lists.tmpl +++ /dev/null @@ -1,17 +0,0 @@ -
    -

    Lists

    - -
    -
      - {{%s:lists}} -
    - -
      -
      -
    • - -
    • -
      -
    -
    -
    diff --git a/static/login.tmpl b/static/login.tmpl deleted file mode 100644 index 3bfdea8..0000000 --- a/static/login.tmpl +++ /dev/null @@ -1,29 +0,0 @@ -
    -

    {{%s:login_header}}

    - - {{%s:error}} - -
    -
    - - -
    -
    - -
    -
    -
    - -
    -
    - -
    {{%s:instance_text}}
    - -
    -
    - - - -
    -
    -
    diff --git a/static/menu_item.tmpl b/static/menu_item.tmpl deleted file mode 100644 index 7cb83e6..0000000 --- a/static/menu_item.tmpl +++ /dev/null @@ -1,6 +0,0 @@ -
  • -
    - - -
    -
  • diff --git a/static/message.tmpl b/static/message.tmpl deleted file mode 100644 index 77183f4..0000000 --- a/static/message.tmpl +++ /dev/null @@ -1,10 +0,0 @@ -
    - -
    -
    - {{ %s : content }} - 12:00 -
    -
    -
    - diff --git a/static/navigation.tmpl b/static/navigation.tmpl deleted file mode 100644 index 07501b5..0000000 --- a/static/navigation.tmpl +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - diff --git a/static/notification.tmpl b/static/notification.tmpl deleted file mode 100644 index 3fdad91..0000000 --- a/static/notification.tmpl +++ /dev/null @@ -1,10 +0,0 @@ -
    - -
    - - {{%s:username}} - {{%s:action}} - - {{%s:action_item}} -
    -
    diff --git a/static/notification_action.tmpl b/static/notification_action.tmpl deleted file mode 100644 index 31e40fb..0000000 --- a/static/notification_action.tmpl +++ /dev/null @@ -1,25 +0,0 @@ - - - - - -
    - - - - - - -
    -
    - - {{%s:display_name}} - {{%s:action}} - - {{%s:notif_svg}} -
    - - @{{%s:acct}} - -
    -
    diff --git a/static/notifications.tmpl b/static/notifications.tmpl deleted file mode 100644 index 93feac5..0000000 --- a/static/notifications.tmpl +++ /dev/null @@ -1,3 +0,0 @@ -
    - {{%s:notifications}} -
    diff --git a/static/notifications_embed.tmpl b/static/notifications_embed.tmpl deleted file mode 100644 index e8d7e25..0000000 --- a/static/notifications_embed.tmpl +++ /dev/null @@ -1,25 +0,0 @@ - - - - - Notifications embed - - {{ %s : theme_str }} - - - - - - diff --git a/static/notifications_page.tmpl b/static/notifications_page.tmpl deleted file mode 100644 index cb9cd55..0000000 --- a/static/notifications_page.tmpl +++ /dev/null @@ -1,7 +0,0 @@ -
    -

    Notifications

    -
    -
    - {{%s:notifications}} -
    -{{%s:navigation}} diff --git a/static/post.tmpl b/static/post.tmpl deleted file mode 100644 index 8e492b0..0000000 --- a/static/post.tmpl +++ /dev/null @@ -1,64 +0,0 @@ -
    - {{%s:reply_input}} -
    - -
    -
    - - -
    - - - - - - - - - - -
    -
    -
    - -
    -
    - -
    -
    - diff --git a/static/quick_login.tmpl b/static/quick_login.tmpl deleted file mode 100644 index f334aee..0000000 --- a/static/quick_login.tmpl +++ /dev/null @@ -1,15 +0,0 @@ - diff --git a/static/reactions_btn.tmpl b/static/reactions_btn.tmpl deleted file mode 100644 index 18e9e42..0000000 --- a/static/reactions_btn.tmpl +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - {{%s:emoji_picker}} - diff --git a/static/repeat_btn.tmpl b/static/repeat_btn.tmpl deleted file mode 100644 index 68ab520..0000000 --- a/static/repeat_btn.tmpl +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/static/repeat_btn_img.tmpl b/static/repeat_btn_img.tmpl deleted file mode 100644 index b3b6398..0000000 --- a/static/repeat_btn_img.tmpl +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/repeat_svg.tmpl b/static/repeat_svg.tmpl deleted file mode 100644 index 6712353..0000000 --- a/static/repeat_svg.tmpl +++ /dev/null @@ -1 +0,0 @@ - diff --git a/static/reply_btn.tmpl b/static/reply_btn.tmpl deleted file mode 100644 index 0dd62bd..0000000 --- a/static/reply_btn.tmpl +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/static/reply_btn_img.tmpl b/static/reply_btn_img.tmpl deleted file mode 100644 index 3edcc5e..0000000 --- a/static/reply_btn_img.tmpl +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/reply_checkbox.tmpl b/static/reply_checkbox.tmpl deleted file mode 100644 index 080eb2d..0000000 --- a/static/reply_checkbox.tmpl +++ /dev/null @@ -1,5 +0,0 @@ - - diff --git a/static/reply_link.tmpl b/static/reply_link.tmpl deleted file mode 100644 index 0de803f..0000000 --- a/static/reply_link.tmpl +++ /dev/null @@ -1,5 +0,0 @@ - - {{ %s : reply_btn }} - {{%s:reply_count}} - - \ No newline at end of file diff --git a/static/scrobble.tmpl b/static/scrobble.tmpl deleted file mode 100644 index 28bcc2e..0000000 --- a/static/scrobble.tmpl +++ /dev/null @@ -1,28 +0,0 @@ -
    - - - -
    - - - {{%s:username}} {{%s:activity}} -
    - - - - - - - - - - - - - - - - - -
    {{%s:title_key}}{{%s:title}}
    {{%s:artist_key}}{{%s:artist}}
    {{%s:album_key}}{{%s:album}}
    {{%s:length_key}}{{%d:length}}
    -
    diff --git a/static/search.tmpl b/static/search.tmpl deleted file mode 100644 index b3f9e1b..0000000 --- a/static/search.tmpl +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - -
    - - - - - -
    - -
    - {{%s:results}} -
    diff --git a/static/search_all.tmpl b/static/search_all.tmpl deleted file mode 100644 index 6a45d41..0000000 --- a/static/search_all.tmpl +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - -
    - - - - - -
    - -
    - -
    - {{%s:statuses_results}} -
    - -
    - {{%s:accounts_results}} -
    - -
    - {{%s:hashtags_results}} -
    -
    diff --git a/static/status.tmpl b/static/status.tmpl deleted file mode 100644 index ca7bcd2..0000000 --- a/static/status.tmpl +++ /dev/null @@ -1,54 +0,0 @@ - -
    - {{ %s : notif_info }} - - - - - -
    - - -
    - {{%s:username}} - {{%s:acct}} - - - - -
    -
    - {{%s:in_reply_to_str}} - - {{%s:status_content}} - - {{%s:attachments}} - {{%s:interactions}} - {{%s:emoji_reactions}} - {{%s:interaction_btns}} -
    -
    -
    - -{{ %s : reply }} - diff --git a/static/status_interaction_profile.tmpl b/static/status_interaction_profile.tmpl deleted file mode 100644 index 3ae1e72..0000000 --- a/static/status_interaction_profile.tmpl +++ /dev/null @@ -1 +0,0 @@ - diff --git a/static/status_interactions.tmpl b/static/status_interactions.tmpl deleted file mode 100644 index fb16fa5..0000000 --- a/static/status_interactions.tmpl +++ /dev/null @@ -1,11 +0,0 @@ -
    -
    - - {{%s:reblogs_count}} - - {{%s:favourites_count}} -
    -
    - {{%s:users}} -
    -
    diff --git a/static/status_interactions_label.tmpl b/static/status_interactions_label.tmpl deleted file mode 100644 index 6445520..0000000 --- a/static/status_interactions_label.tmpl +++ /dev/null @@ -1,4 +0,0 @@ - - {{%s:header}} - {{%d:value}} - diff --git a/static/test.tmpl b/static/test.tmpl deleted file mode 100644 index c979367..0000000 --- a/static/test.tmpl +++ /dev/null @@ -1,52 +0,0 @@ -
    -

    Test page

    -

    Test your nginx/apache and browser here

    -
    - - -
    - -
    - - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    ENVValue
    HTTP_COOKIE{{%s:HTTP_COOKIE}}
    PATH_INFO{{%s:PATH_INFO}}
    QUERY_STRING{{%s:QUERY_STRING}}
    REQUEST_METHOD{{%s:REQUEST_METHOD}}
    SCRIPT_NAME{{%s:SCRIPT_NAME}}
    HTTP_REFERER{{%s:HTTP_REFERER}}
    HTTP_USER_AGENT{{%s:HTTP_USER_AGENT}}
    CONTENT_LENGTH{{%s:CONTENT_LENGTH}}
    -
    diff --git a/static/thread_page_btn.tmpl b/static/thread_page_btn.tmpl deleted file mode 100644 index 479f871..0000000 --- a/static/thread_page_btn.tmpl +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - diff --git a/static/timeline_options.tmpl b/static/timeline_options.tmpl deleted file mode 100644 index 1b584f3..0000000 --- a/static/timeline_options.tmpl +++ /dev/null @@ -1,14 +0,0 @@ - diff --git a/templates/about.tt b/templates/about.tt new file mode 100644 index 0000000..0f3d690 --- /dev/null +++ b/templates/about.tt @@ -0,0 +1,28 @@ +
    +
    +

    Treebird

    +
    +
    +

    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.

    + +

    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.

    + +

    Treebird was created by Nekobit, who created mastodont-c, a library that can communicate with Revolver's, Pleroma's, and Mastodon's REST APIs.

    + +

    Other contributors

    + + +

    Treebird is licensed in AGPLv3, and mastodont-c is LGPLv3 licensed. Other licenses apply to libraries as well. View the AGPLv3 license here.

    + + View the Fossil Repository View the License +
    +
    + + + diff --git a/templates/account.tt b/templates/account.tt new file mode 100644 index 0000000..1b4503a --- /dev/null +++ b/templates/account.tt @@ -0,0 +1,84 @@ +[% IF relationship && relationship.blocked_by %] + You are blocked by this user. You can still interact, but they will not be notifed of your interactions. +[% END %] +[% IF acct.id == ssn.account.id %] + +[% END %] + + +[% IF acct.note %] + +[% END %] + + + + + + + + +
    + + + + + + + +
    + + + diff --git a/static/account_stub.tmpl b/templates/account_item.tt similarity index 55% rename from static/account_stub.tmpl rename to templates/account_item.tt index 0588754..fc971eb 100644 --- a/static/account_stub.tmpl +++ b/templates/account_item.tt @@ -1,16 +1,16 @@