Moved hunchentoot utils to seperate package. Update readme

ご主人様
not manx 4 years ago
parent 4214c213e3
commit a31d24c28d
Signed by: C-xC-c
GPG Key ID: F52ED472284EF2F4

1
.gitignore vendored

@ -1 +1,2 @@
src/config.lisp src/config.lisp
\#*#

@ -1,117 +1,83 @@
* BantFlags * BantFlags
A user script and backend enabling user created flags on [[https://boards.4chan.org/bant][/bant/]], A user script and backend enabling user created flags on [[https://boards.4chan.org/bant][/bant/]],
originally based on [[https://github.com/flaghunters/Extra-Flags-for-4chan][extraflags]]. originally based on [[https://github.com/flaghunters/Extra-Flags-for-4chan][extraflags]].
[[https://flags.plum.moe/bantflags.user.js][Install bantflags]] [[https://flags.plum.moe/bantflags.user.js][Install bantflags]]
** Userscript ** Userscript
The userscript uses of ~GM_xmlhttpRequest~ to get and post flags with The userscript uses of ~GM_xmlhttpRequest~ to get and post flags with
the backend . A user's flags are stored between pages using the backend . A user's flags are stored between pages using
~GM_setValue~ and ~GM_getValue~. ~GM_setValue~ and ~GM_getValue~.
Old versions of GreaseMonkey will be able to recieve updates to the Old versions of GreaseMonkey will be able to recieve updates to the
script through the ~@updateURL~ and ~@downloadURL~ directives, though script through the ~@updateURL~ and ~@downloadURL~ directives, though
these were depricated sometime in GreaseMonkey 3.x and updates are these were depricated sometime in GreaseMonkey 3.x and updates are
only checked from the location the script was downloaded from so be only checked from the location the script was downloaded from so be
careful where you upload links. careful where you upload links.
On self hosting, changing ~back_end~ to your domain /should/ be all On self hosting, changing ~back_end~ to your domain /should/ be all
you need to do, but don't take this as fact. I haven't tested the you need to do, but don't take this as fact. I haven't tested the
example nginx config. example nginx config.
The userscript has been designed specifically to target ECMAScript The userscript has been designed specifically to target ECMAScript
2015 (ES6), making liberal use of arrow functions, and const/let 2015 (ES6), making liberal use of arrow functions, and const/let
declarations. Update your hecking browser. declarations. Update your hecking browser.
** Backend ** Backend
*** Prerequisites *** Prerequisites
- .NET Core 3.1 - I use SBCL
- MariaDB / MySQL *** Dependancies
- hunchentoot
*** .NET dependancies - [[https://github.com/C-xC-c/hunchenhelpers][hunchenhelpers]], my hunchentoot helper library
- Nito.AsyncEX - clsql
- Newtonsoft.Json - jonathan, the JSON encoder/decoder
- MySql.Data - cl-ppcre
- Microsoft.AspNetCore.Mvc.NewtonsoftJson
- Microsoft.AspNetCore.StaticFiles
- Microsoft.AspNetCore.Razor
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Tools
- Magick.NET-Q8-AnyCPU
*** Setup *** Setup
1) [[https://dotnet.microsoft.com/download/dotnet-core][Install .NET Core]] 1. clone the project
2) Clone and build the BantFlags solution. 2. Symlink src/ to your ~/quicklisp/local-projects
3) Create the database using [[https://github.com/C-xC-c/BantFlags/blob/master/Environment/database.sql][database.sql]]. 3. Move ~src/config.example.org~ to ~src/config.org~ and change it
- *Change the password*. to whatever your settings are.
4) configure ~BantFlags/appsettings.example.json~ with your connection 4. Type the following into your repl:
string and webroot (where you'll serve the flags from *without a #+BEGIN_SRC lisp
trailing slash*) and rename it to ~appsettings.json~ (ql:quickload :bantflags)
- [[./BantFlags/appsettings.example.json][example appsettings.json]] (bantflags:main)
- ASP.NET Core applications look for a folder called ~wwwroot~ in #+END_SRC
the same directory as the application for static files. However You will almost certainly have several issues building clsql, the
you can choose to logically seperate these by providing a vaild database connector used. I've [[https://plum.moe/words/bludgeoning-clsql-and-mariadb.html][written a blog post]] on some of the
directory to ~webroot~. issues I've encountered personally, but there's no guarantee it'll
- That is to say, if the bantflags application is in work. Piece of shit.
~/var/www/bantflags/BantFlags.dll~, the program will look for
the folder ~/var/www/bantflags/wwwroot/~ to host static content,
or whatever directory is provided to ~wwwroot~.
5) If you're hosting on your GNU/Linux distribution of choice, Create a
folder called ~keys~ in the same directory as the bantflags
executable.
- E.G. ~/var/www/bantflags/keys/~
- This is because ASP.NET Core uses some cryptic bullshit anti
forgery token when processing HTML forms, and it's unable to
persistantly store the decryption keys in memory on
GNU/Linux. This directory will store said keys when you or
users upload flags to /upload. The path uses
~AppDomain.CurrentDomain.BaseDirectory~ internally,
I.E. wherever the program is.
6) Add flags to the backend by uploading them to the flag console (/Upload).
- Flags must be 16x11 pixels and under 15kb. Their names must not
exceed 100 characters and cannot contain either "||" or ",".
7) Configure your webserver of choice to forward requests to kestral
- [[https://github.com/C-xC-c/BantFlags/blob/master/Environment/nginx.conf][Example nginx config.]]
8) Run with ~dotnet BantFlags.dll~ or create a service to run it as a
daemon.
- [[https://github.com/C-xC-c/BantFlags/blob/master/Environment/bantflags.service][Example systemd service.]]
9) ???
10) profit.
*** Database *** Database
Tables look like this: Tables look like this:
*posts* *posts*
| id | post_nr | board | | id | post_nr | board |
| 1 | 12345 | bant | | 1 | 12345 | bant |
| 2 | 56789 | bant | | 2 | 56789 | bant |
*flags* *flags*
| id | flag | | id | flag |
| 1 | patchouli | | 1 | patchouli |
| 2 | chen | | 2 | chen |
*postflags* *postflags*
| id | post_nr | flag | | id | post_nr | flag |
| 1 | 1 | 1 | | 1 | 1 | 1 |
| 2 | 1 | 2 | | 2 | 1 | 2 |
| 2 | 2 | 2 | | 2 | 2 | 2 |
where ~post_nr~ and ~flag~ in *postflags* are the id fields in their where ~post_nr~ and ~flag~ in *postflags* are the id fields in their
respective tables. respective tables.
*** API *** API
The backend exposes three endpoints for the userscript to get and post The backend exposes three endpoints for the userscript to get and
flags. Flags themselves are hosted from the ~flags/~ directory. This post flags. Flags themselves are hosted from ~flags/~ which is
will be whatever value you gave to ~webroot~ (or ~www-root/flags/~ from ~config.lisp~ on the filesystem
~/path/to/bantflags/wwwroot/~ if no value is provided) + ~flags/~.
| route | purpse |
|------------+--------------------------------------------|
| /api/get | Get flags using post numbers in the thread |
| /api/post | Add flags to the database |
| /api/flags | List the flags we support |
| /flags/* | The flag images |
** Backwards Compatibility | route | purpse |
The API is 1:1 compatable with all previous versions of |------------+--------------------------------------------|
bantflags. Further improvements are achieved by encoding a ~version~ | /api/get | Get flags using post numbers in the thread |
variable when poking endpoints which allows for breaking changes in | /api/post | Add flags to the database |
the script and backend while guaranteeing data can be parsed on both | /api/flags | List the flags we support |
ends. See [[https://github.com/C-xC-c/BantFlags/tree/master/Docs/][Docs/{endpoint}]] for changes and compatibility. | /flags/* | The flag images |
** Notes
You will get an error like =Recursive lock attempt #<SB-THREAD:MUTEX
"global-message-log-lock" owner: #<SB-THREAD:THREAD
"hunchentoot-worker-127.0.0.1:54454" RUNNING {1001DED5E3}>>.= if you
try and log to a file that doesn't exist / you don't have permissions
to read/write.

@ -60,15 +60,15 @@ let flagsLoaded = false;
// //
const debug = text => { const debug = text => {
if (debugMode) if (debugMode)
console.log('[BantFlags] ' + text); console.log('[BantFlags] ' + text);
} }
// Test unqiue CSS paths to figure out what board software we're using. // Test unqiue CSS paths to figure out what board software we're using.
const software = { const software = {
yotsuba: window.location.host === 'boards.4chan.org', yotsuba: window.location.host === 'boards.4chan.org',
nodegucaDoushio: document.querySelector('b[id="sync"], span[id="sync"]') !== null, nodegucaDoushio: document.querySelector('b[id="sync"], span[id="sync"]') !== null,
foolfuuka: document.querySelector('div[id="main"] article header .post_data') !== null foolfuuka: document.querySelector('div[id="main"] article header .post_data') !== null
}; };
const makeElement = (tag, options) => Object.assign(document.createElement(tag), options); const makeElement = (tag, options) => Object.assign(document.createElement(tag), options);
@ -86,142 +86,142 @@ const addGlobalStyle = css => document.head.appendChild(makeElement('style', { i
* @param {string} data - text for the form body. * @param {string} data - text for the form body.
* @param {Function} func - The function run when we recieve a response. Response data is sent directly to it. */ * @param {Function} func - The function run when we recieve a response. Response data is sent directly to it. */
const makeRequest = ((method, url, data, func) => { const makeRequest = ((method, url, data, func) => {
xmlHttpRequest({ xmlHttpRequest({
method: method, method: method,
url: url, url: url,
data: data, data: data,
headers: { "Content-Type": 'application/x-www-form-urlencoded' }, headers: { "Content-Type": 'application/x-www-form-urlencoded' },
onload: func onload: func
}); });
}); });
/** Itterate over selected flags are store them across browser sessions.*/ /** Itterate over selected flags are store them across browser sessions.*/
function saveFlags() { function saveFlags() {
regions = []; regions = [];
const selectedFlags = document.querySelectorAll(".bantflags_flag"); const selectedFlags = document.querySelectorAll(".bantflags_flag");
for (var i = 0; i < selectedFlags.length; i++) { for (var i = 0; i < selectedFlags.length; i++) {
regions[i] = selectedFlags[i].title; regions[i] = selectedFlags[i].title;
} }
setValue(namespace, regions); setValue(namespace, regions);
} }
/** Add a flag to our selection. /** Add a flag to our selection.
* @param {string} flag - The flag to add to our selection. Either passed from saved flags or the current value of flagSelect */ * @param {string} flag - The flag to add to our selection. Either passed from saved flags or the current value of flagSelect */
function setFlag(flag) { function setFlag(flag) {
let UID = Math.random().toString(36).substring(7); let UID = Math.random().toString(36).substring(7);
let flagName = flag ? flag : document.querySelector('#flagSelect input').value; let flagName = flag ? flag : document.querySelector('#flagSelect input').value;
let flagContainer = document.getElementById('bantflags_container'); let flagContainer = document.getElementById('bantflags_container');
flagContainer.appendChild(makeElement('img', { flagContainer.appendChild(makeElement('img', {
title: flagName, title: flagName,
src: flagSource(flagName), src: flagSource(flagName),
id: UID, id: UID,
className: 'bantflags_flag' className: 'bantflags_flag'
})); }));
if (flagContainer.children.length >= max_flags) if (flagContainer.children.length >= max_flags)
toggleFlagButton('off'); toggleFlagButton('off');
document.getElementById(UID).addEventListener("click", e => { document.getElementById(UID).addEventListener("click", e => {
flagContainer.removeChild(e.target); flagContainer.removeChild(e.target);
toggleFlagButton('on'); toggleFlagButton('on');
saveFlags(); saveFlags();
}); });
if (!flag) // We've added a new flag to our selection if (!flag) // We've added a new flag to our selection
saveFlags(); saveFlags();
} }
function init() { function init() {
let flagsForm = makeElement('div', { let flagsForm = makeElement('div', {
className: 'flagsForm', className: 'flagsForm',
innerHTML: '<span id="bantflags_container"></span><button type="button" id="append_flag_button" title="Click to add selected flag to your flags. Click on flags to remove them. Saving happens automatically, you only need to refresh the pages that have an outdated flaglist on the page."><<</button><button id="flagLoad" type="button">Click to load flags.</button><div id="flagSelect" ><ul class="hide"></ul><input type="button" value="(You)" onclick=""></div>' innerHTML: '<span id="bantflags_container"></span><button type="button" id="append_flag_button" title="Click to add selected flag to your flags. Click on flags to remove them. Saving happens automatically, you only need to refresh the pages that have an outdated flaglist on the page."><<</button><button id="flagLoad" type="button">Click to load flags.</button><div id="flagSelect" ><ul class="hide"></ul><input type="button" value="(You)" onclick=""></div>'
}); });
// Where do we append the flagsForm to? // Where do we append the flagsForm to?
if (software.yotsuba) { document.getElementById('delform').appendChild(flagsForm); } if (software.yotsuba) { document.getElementById('delform').appendChild(flagsForm); }
else if (software.nodegucaDoushio) { document.querySelector('section').append(flagsForm); } // As posts are added the flagForm moves up the page. Could we append this after .section? else if (software.nodegucaDoushio) { document.querySelector('section').append(flagsForm); } // As posts are added the flagForm moves up the page. Could we append this after .section?
for (let i = 0; i < regions.length; i++) { for (let i = 0; i < regions.length; i++) {
setFlag(regions[i]); setFlag(regions[i]);
} }
document.getElementById('append_flag_button').addEventListener('click', () => flagsLoaded ? setFlag() : alert('Load flags before adding them.')); document.getElementById('append_flag_button').addEventListener('click', () => flagsLoaded ? setFlag() : alert('Load flags before adding them.'));
document.getElementById('flagLoad').addEventListener('click', makeFlagSelect, { once: true }); document.getElementById('flagLoad').addEventListener('click', makeFlagSelect, { once: true });
} }
/** Get flag data from server and fill flags form. */ /** Get flag data from server and fill flags form. */
function makeFlagSelect() { function makeFlagSelect() {
makeRequest( makeRequest(
"GET", "GET",
api_flags, api_flags,
"", // We can't send data, it's a GET request. "", // We can't send data, it's a GET request.
function (resp) { function (resp) {
debug('Loading flags.'); debug('Loading flags.');
if (resp.status !== 200) { if (resp.status !== 200) {
console.log('Couldn\'t get flag list from server') console.log('Couldn\'t get flag list from server')
return; return;
} }
let flagSelect = document.getElementById('flagSelect'); let flagSelect = document.getElementById('flagSelect');
let flagList = flagSelect.querySelector('ul'); let flagList = flagSelect.querySelector('ul');
let flagInput = flagSelect.querySelector('input'); let flagInput = flagSelect.querySelector('input');
let flags = resp.responseText.split('\n'); let flags = resp.responseText.split('\n');
for (var i = 0; i < flags.length; i++) { for (var i = 0; i < flags.length; i++) {
let flag = flags[i]; let flag = flags[i];
flagList.appendChild(makeElement('li',{ flagList.appendChild(makeElement('li',{
innerHTML: `<img src="${flagSource(flag)}" title="${flag}"><span>${flag}</span>` innerHTML: `<img src="${flagSource(flag)}" title="${flag}"><span>${flag}</span>`
})); }));
} }
flagSelect.addEventListener('click', function (e) { flagSelect.addEventListener('click', function (e) {
// So it works if we click the flag image // So it works if we click the flag image
const node = e.target.nodeName === 'LI' ? e.target : e.target.parentNode; const node = e.target.nodeName === 'LI' ? e.target : e.target.parentNode;
if (node.nodeName === 'LI') { if (node.nodeName === 'LI')
flagInput.value = node.querySelector('span').innerHTML; flagInput.value = node.querySelector('span').innerHTML;
}
flagList.classList.toggle('hide'); flagList.classList.toggle('hide');
}); });
document.getElementById('flagLoad').style.display = 'none'; document.getElementById('flagLoad').style.display = 'none';
document.querySelector('.flagsForm').style.marginRight = "200px"; // Element has position: absolute and is ~200px long. document.querySelector('.flagsForm').style.marginRight = "200px"; // flagsForm has position: absolute and is ~200px long.
flagSelect.style.display = 'inline-block'; flagSelect.style.display = 'inline-block';
flagsLoaded = true; flagsLoaded = true;
}); });
} }
/** add all of the post numbers on the page to postNrs. */ /** add all of the post numbers on the page to postNrs. */
function getPosts(selector) { function getPosts(selector) {
const posts = document.querySelectorAll(selector); const posts = document.querySelectorAll(selector);
for (let i = 0; i < posts.length; i++) { for (let i = 0; i < posts.length; i++) {
const postNumber = software.yotsuba const postNumber = software.yotsuba
? posts[i].id.substr(2) // Fuck you 4chan. ? posts[i].id.substr(2) // Fuck you 4chan.
: posts[i].id; : posts[i].id;
postNrs.push(postNumber); postNrs.push(postNumber);
} }
debug(postNrs); debug(postNrs);
} }
/** Get flags from the database using values in postNrs and pass the response on to onFlagsLoad */ /** Get flags from the database using values in postNrs and pass the response on to onFlagsLoad */
function resolveFlags() { function resolveFlags() {
makeRequest( makeRequest(
'POST', 'POST',
api_get, api_get,
'post_nrs=' + encodeURIComponent(postNrs) + '&board=' + encodeURIComponent(board_id) + '&version=' + version, 'post_nrs=' + encodeURIComponent(postNrs) + '&board=' + encodeURIComponent(board_id) + '&version=' + version,
function (resp) { function (resp) {
if (resp.status !== 200) { if (resp.status !== 200) {
console.log('[bantflags] Couldn\'t load flags. Refresh the page.'); console.log('[bantflags] Couldn\'t load flags. Refresh the page');
return; debug(resp.responseText);
} return;
}
const jsonData = JSON.parse(resp.responseText); const jsonData = JSON.parse(resp.responseText);
debug(`JSON: ${resp.responseText}`); debug(`JSON: ${resp.responseText}`);
@ -256,93 +256,93 @@ function resolveFlags() {
}); });
postNrs = []; postNrs = [];
}); });
} }
function main() { function main() {
// See Docs/styles.css // See Docs/styles.css
addGlobalStyle('.bantFlag{padding: 0px 0px 0px 5px; display: inline-block; width: 16px; height: 11px; position: relative;} .bantflags_flag{padding: 1px;} [title^="Romania"]{ position: relative; animation: shakeAnim 0.1s linear infinite;} @keyframes shakeAnim{ 0% {left: 1px;} 25% {top: 2px;} 50% {left: 1px;} 75% {left: 0px;} 100% {left: 2px;}}.flagsForm{float: right; clear: right; margin: 20px 10px;} #flagSelect{display:none;} #flagSelect ul{list-style-type: none;padding: 0;margin-bottom: 0;cursor: pointer;bottom: 100%;height: 200px;overflow: auto;position: absolute;width:200px;background-color:#fff} #flagSelect ul li {display: block;} #flagSelect ul li:hover {background-color: #ddd;}#flagSelect {position: absolute;}#flagSelect input {width: 200px;} #flagSelect .hide {display: none;}#flagSelect img {margin-left: 2px;}') addGlobalStyle('.bantFlag{padding: 0px 0px 0px 5px; display: inline-block; width: 16px; height: 11px; position: relative;} .bantflags_flag{padding: 1px;} [title^="Romania"]{ position: relative; animation: shakeAnim 0.1s linear infinite;} @keyframes shakeAnim{ 0% {left: 1px;} 25% {top: 2px;} 50% {left: 1px;} 75% {left: 0px;} 100% {left: 2px;}}.flagsForm{float: right; clear: right; margin: 20px 10px;} #flagSelect{display:none;} #flagSelect ul{list-style-type: none;padding: 0;margin-bottom: 0;cursor: pointer;bottom: 100%;height: 200px;overflow: auto;position: absolute;width:200px;background-color:#fff} #flagSelect ul li {display: block;} #flagSelect ul li:hover {background-color: #ddd;}#flagSelect {position: absolute;}#flagSelect input {width: 200px;} #flagSelect .hide {display: none;}#flagSelect img {margin-left: 2px;}')
if (software.yotsuba) { if (software.yotsuba) {
getPosts('.postContainer'); getPosts('.postContainer');
addGlobalStyle('.flag{top: 0px;left: -1px}'); addGlobalStyle('.flag{top: 0px;left: -1px}');
init(); init();
} }
if (software.nodegucaDoushio) { if (software.nodegucaDoushio) {
getPosts('section[id], article[id]'); getPosts('section[id], article[id]');
addGlobalStyle('.bantFlag {cursor: default} .bantFlag img {pointer-events: none;}'); addGlobalStyle('.bantFlag {cursor: default} .bantFlag img {pointer-events: none;}');
init(); init();
} }
if (software.foolfuuka) { if (software.foolfuuka) {
getPosts('article[id]'); getPosts('article[id]');
addGlobalStyle('.bantFlag{top: -2px !important;left: -1px !important}'); addGlobalStyle('.bantFlag{top: -2px !important;left: -1px !important}');
} }
board_id = window.location.pathname.split('/')[1]; board_id = window.location.pathname.split('/')[1];
debug(board_id); debug(board_id);
resolveFlags(); resolveFlags();
} }
if (isGM4) { // Fuck you GM4 if (isGM4) { // Fuck you GM4
(async () => { (async () => {
regions = await getValue(namespace); regions = await getValue(namespace);
main(); main();
})(); })();
} }
else { else {
regions = getValue(namespace); regions = getValue(namespace);
main(); main();
} }
const postFlags = (post_nr, func = resp => debug(resp.responseText)) => makeRequest( const postFlags = (post_nr, func = resp => debug(resp.responseText)) => makeRequest(
'POST', 'POST',
api_post, api_post,
`post_nr=${encodeURIComponent(post_nr)}&board=${encodeURIComponent(board_id)}&regions=${encodeURIComponent(regions)}&version=${version}`, `post_nr=${encodeURIComponent(post_nr)}&board=${encodeURIComponent(board_id)}&regions=${encodeURIComponent(regions)}&version=${version}`,
func); func);
if (software.yotsuba) { if (software.yotsuba) {
const GetEvDetail = e => e.detail || e.wrappedJSObject.detail; const GetEvDetail = e => e.detail || e.wrappedJSObject.detail;
// 4chanX and native extension respectively // 4chanX and native extension respectively
document.addEventListener('QRPostSuccessful', e => postFlags(e.detail.postID)); document.addEventListener('QRPostSuccessful', e => postFlags(e.detail.postID));
document.addEventListener('4chanQRPostSuccess', e => postFlags(GetEvDetail(e).postId)); document.addEventListener('4chanQRPostSuccess', e => postFlags(GetEvDetail(e).postId));
// I need to look at these. // I need to look at these.
document.addEventListener('ThreadUpdate', function (e) { document.addEventListener('ThreadUpdate', function (e) {
var evDetail = GetEvDetail(e); var evDetail = GetEvDetail(e);
var evDetailClone = typeof cloneInto === 'function' ? cloneInto(evDetail, unsafeWindow) : evDetail; var evDetailClone = typeof cloneInto === 'function' ? cloneInto(evDetail, unsafeWindow) : evDetail;
//ignore if 404 event //ignore if 404 event
if (evDetail[404] === true) { if (evDetail[404] === true) {
return; return;
} }
evDetailClone.newPosts.forEach(function (post_board_nr) { evDetailClone.newPosts.forEach(function (post_board_nr) {
var post_nr = post_board_nr.split('.')[1]; var post_nr = post_board_nr.split('.')[1];
postNrs.push(post_nr); postNrs.push(post_nr);
}); });
resolveFlags(); resolveFlags();
}, false); }, false);
document.addEventListener('4chanThreadUpdated', function (e) { document.addEventListener('4chanThreadUpdated', function (e) {
var evDetail = GetEvDetail(e); var evDetail = GetEvDetail(e);
let threadID = window.location.pathname.split('/')[3]; let threadID = window.location.pathname.split('/')[3];
let postsContainer = Array.prototype.slice.call(document.getElementById('t' + threadID).childNodes); let postsContainer = Array.prototype.slice.call(document.getElementById('t' + threadID).childNodes);
let lastPosts = postsContainer.slice(Math.max(postsContainer.length - evDetail.count, 1)); //get the last n elements (where n is evDetail.count) let lastPosts = postsContainer.slice(Math.max(postsContainer.length - evDetail.count, 1)); //get the last n elements (where n is evDetail.count)
lastPosts.forEach(function (post_container) { lastPosts.forEach(function (post_container) {
var post_nr = post_container.id.replace('pc', ''); var post_nr = post_container.id.replace('pc', '');
postNrs.push(post_nr); postNrs.push(post_nr);
}); });
resolveFlags(); resolveFlags();
}, false); }, false);
} }
@ -355,37 +355,37 @@ if (software.nodegucaDoushio) {
const badNodes = ['HR', 'SECTION']; const badNodes = ['HR', 'SECTION'];
new MutationObserver(mutations => { new MutationObserver(mutations => {
mutations.forEach(mutation => { mutations.forEach(mutation => {
if (mutation.addedNodes.length <= 0) if (mutation.addedNodes.length <= 0)
return; // We only care if something post was added return; // We only care if something post was added
var firstAddedNode = mutation.addedNodes[0].nodeName; var firstAddedNode = mutation.addedNodes[0].nodeName;
// Enter a thread / change boards // Enter a thread / change boards
if (mutation.target.nodeName === 'THREADS') { if (mutation.target.nodeName === 'THREADS') {
if (badNodes.includes(firstAddedNode)) if (badNodes.includes(firstAddedNode))
return; // We are in the index and a post was added, handled properly further down return; // We are in the index and a post was added, handled properly further down
board_id = window.location.pathname.split('/')[1]; board_id = window.location.pathname.split('/')[1];
setTimeout(getPosts('section[id], article[id]'), 2000); setTimeout(getPosts('section[id], article[id]'), 2000);
resolveFlags(); resolveFlags();
init(); init();
} }
// We post // We post
if (firstAddedNode === 'HEADER') { if (firstAddedNode === 'HEADER') {
postFlags(mutation.target.id, postFunc) postFlags(mutation.target.id, postFunc)
} }
// Someone else posts // Someone else posts
if (firstAddedNode === 'ARTICLE') { if (firstAddedNode === 'ARTICLE') {
if (mutation.target.nodeName === 'BODY' || mutation.target.id === 'hover_overlay') if (mutation.target.nodeName === 'BODY' || mutation.target.id === 'hover_overlay')
return; // User is hovering over a post return; // User is hovering over a post
postNrs.push(mutation.addedNodes[0].id); postNrs.push(mutation.addedNodes[0].id);
setTimeout(resolveFlags, 1500); setTimeout(resolveFlags, 1500);
} }
}); });
}).observe(document.body, { childList: true, subtree: true }); }).observe(document.body, { childList: true, subtree: true });
} }

@ -11,11 +11,13 @@
:version "0.0.1" :version "0.0.1"
:serial t :serial t
:depends-on (:hunchentoot :depends-on (:hunchentoot
:hunchenhelpers
:cl-ppcre :cl-ppcre
:clsql :clsql
:jonathan) :jonathan)
:Components :Components
((:file "utils") ((:file "package")
(:file "utils")
(:file "db") (:file "db")
(:file "config") (:file "config")
(:file "main"))) (:file "main")))

@ -1,3 +1,5 @@
(in-package #:bantflags)
(defvar config (defvar config
'((boards "bant") '((boards "bant")
(staging-password "not implemented") (staging-password "not implemented")
@ -6,5 +8,7 @@
(www-root #p"/path/to/files/") (www-root #p"/path/to/files/")
(port 4242) (port 4242)
;; These can be a file or stream, make them nil to disable logging ;; These can be a file or stream, make them nil to disable logging
;; If the file can't be accessed, throws a weird error. See
;; README.org
(access-log *standard-output*) (access-log *standard-output*)
(error-log #p"/path/to/error/log/"))) (error-log #p"/path/to/error/log/")))

@ -2,6 +2,7 @@
;; This file is part of bantflags. ;; This file is part of bantflags.
;; bantflags is licensed under the GNU AGPL Version 3.0 or later. ;; bantflags is licensed under the GNU AGPL Version 3.0 or later.
;; see the LICENSE file or <https://www.gnu.org/licenses/> ;; see the LICENSE file or <https://www.gnu.org/licenses/>
(in-package #:bantflags)
;; Databases in common lisp are the fucking worst. ;; Databases in common lisp are the fucking worst.
;; Don't even bother. ;; Don't even bother.

@ -2,6 +2,7 @@
;; This file is part of bantflags. ;; This file is part of bantflags.
;; bantflags is licensed under the GNU AGPL Version 3.0 or later. ;; bantflags is licensed under the GNU AGPL Version 3.0 or later.
;; see the LICENSE file or <https://www.gnu.org/licenses/> ;; see the LICENSE file or <https://www.gnu.org/licenses/>
(in-package :bantflags)
(defun init () (defun init ()
(setf conn (conf 'db-conn)) (setf conn (conf 'db-conn))
@ -13,20 +14,23 @@
:port (cconf 'port) :port (cconf 'port)
:document-root (cconf 'www-root) :document-root (cconf 'www-root)
:access-log-destination (cconf 'access-log) :access-log-destination (cconf 'access-log)
:message-log-destination (cconf 'error-log))) :message-log-destination (cconf 'error-log))))
(hunchentoot:start +serb+))
(defun main () (defun main ()
(handler-case (init) (handler-case (init)
(error (c) (error (c)
(format t "Init fucked up, exiting ~a" c) (format t "Init fucked up, exiting ~a" c)
(return-from main))) (return-from main)))
(handler-case (hunchentoot:start +serb+)
(error (c)
(format t "couldn't start serb: ~a" c)
(return-from main)))
(loop (sleep 43200) (gc :full t))) (loop (sleep 43200) (gc :full t)))
(defmethod hunchentoot:acceptor-status-message (acceptor (http-status-code (eql 404)) &key) (defmethod hunchentoot:acceptor-status-message (acceptor (http-status-code (eql 404)) &key)
(format nil "")) ;; Empty 404 page (format nil "")) ;; Empty 404 page
(handle :post (api-post :uri "/api/post") @json (henh:handle :post (api-post :uri "/api/post") @json
(post_nr regions board version) (post_nr regions board version)
(multiple-value-bind (result msg) (insert-post-p post_nr (cl-ppcre:split "," regions) board) (multiple-value-bind (result msg) (insert-post-p post_nr (cl-ppcre:split "," regions) board)
(cond (cond
@ -35,12 +39,12 @@
(format nil "{\"~a\": [~{\"~a\"~^,~}]}~%" post_nr msg)) ;; This makes JSON (format nil "{\"~a\": [~{\"~a\"~^,~}]}~%" post_nr msg)) ;; This makes JSON
(t (format nil "{\"Error\": \"~a\"}~%" msg))))) (t (format nil "{\"Error\": \"~a\"}~%" msg)))))
(handle :post (api-get :uri "/api/get") @json (henh:handle :post (api-get :uri "/api/get") @json
(post_nrs board version) (post_nrs board version)
(if (get-posts-p (cl-ppcre:split "," post_nrs) board) (if (get-posts-p (cl-ppcre:split "," post_nrs) board)
(format nil "~a~%" (get-posts post_nrs board)) (format nil "~a~%" (get-posts post_nrs board))
(t (format nil "{[\"~a\"]}~%" "bad")))) (t (format nil "{[\"~a\"]}~%" "bad"))))
(handle :get (api-flags :uri "/api/flags") @plain (henh:handle :get (api-flags :uri "/api/flags") @plain
() ()
(format nil "~a~%" *flags-txt*)) (format nil "~a~%" *flags-txt*))

@ -0,0 +1,3 @@
(defpackage #:bantflags
(:use #:cl)
(:export :init))

@ -2,6 +2,7 @@
;; This file is part of bantflags. ;; This file is part of bantflags.
;; bantflags is licensed under the GNU AGPL Version 3.0 or later. ;; bantflags is licensed under the GNU AGPL Version 3.0 or later.
;; see the LICENSE file or <https://www.gnu.org/licenses/> ;; see the LICENSE file or <https://www.gnu.org/licenses/>
(in-package #:bantflags)
(defvar empty-flag '("empty, or there were errors. Re-set your flags.")) (defvar empty-flag '("empty, or there were errors. Re-set your flags."))
@ -26,7 +27,7 @@
do (setf (gethash (car flag) *flags*) id)) do (setf (gethash (car flag) *flags*) id))
;; We don't want users to select `empty-flag` ;; We don't want users to select `empty-flag`
(setf *flags-txt* (setf *flags-txt*
(cl-ppcre:regex-replace (concatenate 'string empty-flag "\\n") ;; newline (cl-ppcre:regex-replace (concatenate 'string (car empty-flag) "\\n") ;; newline
(format nil "~{~a~^~%~}" (mapcan (lambda (x) (cdr x)) flags)) (format nil "~{~a~^~%~}" (mapcan (lambda (x) (cdr x)) flags))
"")))) ""))))
@ -59,20 +60,6 @@
(every #'post-number-p post_nrs) (every #'post-number-p post_nrs)
(boardp board))) (boardp board)))
;; hunchentoot
(defmacro handle (method uri content-type params &body body)
"Creates an easy handles for a specific HTTP request method. If the
method provided sent from the client isn't correct, return 404 and
stop processing the request.
(handle :get (uri-fun :uri \"/path/to/page\"/) @content-type (args) (body))"
`(hunchentoot:define-easy-handler ,uri ,params
(unless (eq ,method (hunchentoot:request-method*))
(setf (hunchentoot:return-code*) hunchentoot:+http-not-found+)
(hunchentoot:abort-request-handler))
(setf (tbnl:content-type* tbnl:*reply*) ,content-type)
,@body))
;; Content types ;; Content types
(defvar @json "application/json") (defvar @json "application/json")
(defvar @plain "text/plain") (defvar @plain "text/plain")

Loading…
Cancel
Save