| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268 |
- #!/usr/bin/env node
- "use strict";
- const path = require("path");
- const fs = require("fs");
- const promisesFs = fs.promises;
- const os = require('os');
- const envPaths = require("../server/node_modules/env-paths");
- const {cli} = require("../client/oasis_client");
- const SSBconfig = require('../server/SSB_server.js');
- const moment = require('../server/node_modules/moment');
- const FileType = require('../server/node_modules/file-type');
- const ssbRef = require("../server/node_modules/ssb-ref");
- const defaultConfig = {};
- const defaultConfigFile = path.join(envPaths("oasis", { suffix: "" }).config, "/default.json");
- let haveConfig = false;
- try {
- Object.assign(defaultConfig, JSON.parse(fs.readFileSync(defaultConfigFile, "utf8")));
- haveConfig = true;
- } catch (e) { if (e.code !== "ENOENT") { console.log(`Problem loading ${defaultConfigFile}`); throw e; } }
- const config = cli(defaultConfig, defaultConfigFile);
- if (config.debug) {
- process.env.DEBUG = "oasis,oasis:*";
- }
- const axiosMod = require('../server/node_modules/axios');
- const axios = axiosMod.default || axiosMod;
- const { spawn } = require('child_process');
- const { fieldsForSnippet, buildContext, clip, publishExchange, getBestTrainedAnswer } = require('../AI/buildAIContext.js');
- let aiStarted = false;
- function startAI() {
- if (aiStarted) return;
- aiStarted = true;
- const aiProcess = spawn('node', [path.resolve(__dirname, '../AI/ai_service.mjs')], { detached: true, stdio: 'ignore' });
- aiProcess.unref();
- }
- const ADDR_PATH = path.join(__dirname, '..', 'configs', 'wallet-addresses.json');
- const readAddrMap = () => { try { return JSON.parse(fs.readFileSync(ADDR_PATH, 'utf8')); } catch { return {}; } };
- const writeAddrMap = (map) => { fs.mkdirSync(path.dirname(ADDR_PATH), { recursive: true }); fs.writeFileSync(ADDR_PATH, JSON.stringify(map, null, 2)); };
- //parliament model
- let electionInFlight = null;
- const ensureTerm = async () => {
- const cur = await parliamentModel.getCurrentTerm().catch(() => null);
- if (cur) return cur;
- if (electionInFlight) return electionInFlight;
- electionInFlight = parliamentModel.resolveElection().catch(() => null).finally(() => { electionInFlight = null; });
- return electionInFlight;
- };
- let sweepInFlight = null;
- const runSweepOnce = async () => {
- if (sweepInFlight) return sweepInFlight;
- sweepInFlight = parliamentModel.sweepProposals().catch(() => {}).finally(() => { sweepInFlight = null; });
- return sweepInFlight;
- };
- async function buildState(filter) {
- const f = (filter || 'government').toLowerCase();
- await ensureTerm();
- await runSweepOnce();
- const [govCard, candidatures, proposals, canPropose, laws, historical] = await Promise.all([
- parliamentModel.getGovernmentCard(),
- parliamentModel.listCandidatures('OPEN'),
- parliamentModel.listProposalsCurrent(),
- parliamentModel.canPropose(),
- parliamentModel.listLaws(),
- parliamentModel.listHistorical()
- ]);
- return { filter: f, governmentCard: govCard, candidatures, proposals, canPropose, laws, historical };
- }
- function pickLeader(cands = []) {
- if (!cands.length) return null;
- return [...cands].sort((a, b) => {
- const d = (x, y) => y - x;
- return d(Number(a.votes||0), Number(b.votes||0)) || d(Number(a.karma||0), Number(b.karma||0)) ||
- (Number(a.profileSince||0) - Number(b.profileSince||0)) ||
- (new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) ||
- String(a.targetId).localeCompare(String(b.targetId));
- })[0];
- }
- async function buildLeaderMeta(leader) {
- if (!leader) return null;
- if (leader.targetType === 'inhabitant') {
- let name = null, image = null, description = null;
- try { name = about?.name && await about.name(leader.targetId); } catch {}
- try { image = about?.image && await about.image(leader.targetId); } catch {}
- try { description = about?.description && await about.description(leader.targetId); } catch {}
- const imgId = typeof image === 'string' ? image : image?.link || image?.url || null;
- return { isTribe: false, name: name || leader.targetId, avatarUrl: imgId ? `/image/256/${encodeURIComponent(imgId)}` : '/assets/images/default-avatar.png', bio: typeof description === 'string' ? description : '' };
- }
- let tribe = null;
- try { tribe = await tribesModel.getTribeById(leader.targetId); } catch {}
- const imgId = tribe?.image || null;
- return { isTribe: true, name: leader.targetTitle || tribe?.title || tribe?.name || leader.targetId, avatarUrl: imgId ? `/image/256/${encodeURIComponent(imgId)}` : '/assets/images/default-tribe.png', bio: tribe?.description || '' };
- }
- const safeArr = v => Array.isArray(v) ? v : [];
- const safeText = v => String(v || '').trim();
- const safeReturnTo = (ctx, fb, ap) => { const rt = ctx.request?.body?.returnTo || ctx.query?.returnTo; return typeof rt === 'string' && ap?.some(p => rt.startsWith(p)) ? rt : fb; };
- // anti-injections
- const { stripDangerousTags, sanitizeHtml } = require('./sanitizeHtml');
- const sharedState = require('../configs/shared-state');
- module.exports = stripDangerousTags;
- const sanitizeMsgText = (msg) => {
- if (!msg?.value?.content) return msg;
- const c = msg.value.content;
- if (typeof c.text === 'string') c.text = stripDangerousTags(c.text);
- if (typeof c.description === 'string') c.description = stripDangerousTags(c.description);
- if (typeof c.title === 'string') c.title = stripDangerousTags(c.title);
- if (typeof c.contentWarning === 'string') c.contentWarning = stripDangerousTags(c.contentWarning);
- return msg;
- };
- const sanitizeMessages = (msgs) => Array.isArray(msgs) ? msgs.map(sanitizeMsgText) : msgs;
- const parseBool01 = v => String(Array.isArray(v) ? v[v.length - 1] : v || '') === '1';
- const checkMod = (ctx, mod) => {
- const cfg = getConfig();
- const serverValue = cfg.modules?.[mod];
- if (serverValue === 'off') return false;
- const cookieValue = ctx.cookies.get(mod);
- if (cookieValue) return cookieValue === 'on';
- return serverValue === 'on' || serverValue === undefined;
- };
- const getViewerId = () => SSBconfig?.config?.keys?.id || SSBconfig?.keys?.id;
- const refreshInboxCount = async (messagesOpt) => {
- const messages = messagesOpt || await pmModel.listAllPrivate();
- const userId = getViewerId();
- const isToUser = m => Array.isArray(m?.value?.content?.to) && m.value.content.to.includes(userId);
- const filtered = messages.filter(m => m && m.key && m.value && m.value.content && m.value.content.type === 'post' && m.value.content.private === true);
- sharedState.setInboxCount(filtered.filter(isToUser).length);
- };
- const mediaFavorites = require("./media-favorites.js");
- const customStyleFile = path.join(envPaths("oasis", { suffix: "" }).config, "/custom-style.css");
- let haveCustomStyle = false;
- try { fs.readFileSync(customStyleFile, "utf8"); haveCustomStyle = true; } catch (e) { if (e.code !== "ENOENT") { console.log(`Problem loading ${customStyleFile}`); throw e; } }
- const { get } = require("node:http");
- const debug = require("../server/node_modules/debug")("oasis");
- const log = (formatter, ...args) => {
- const isDebugEnabled = debug.enabled;
- debug.enabled = true;
- debug(formatter, ...args);
- debug.enabled = isDebugEnabled;
- };
- delete config._;
- delete config.$0;
- const { host } = config;
- const { port } = config;
- const url = `http://${host}:${port}`;
- debug("Current configuration: %O", config);
- debug(`You can save the above to ${defaultConfigFile} to make \
- these settings the default. See the readme for details.`);
- const { saveConfig, getConfig } = require('../configs/config-manager');
- const configPath = path.join(__dirname, '../configs/oasis-config.json');
- const oasisCheckPath = "/.well-known/oasis";
- process.on("uncaughtException", function (err) {
- if (err["code"] === "EADDRINUSE") {
- get(url + oasisCheckPath, (res) => {
- let rawData = "";
- res.on("data", (chunk) => {
- rawData += chunk;
- });
- res.on("end", () => {
- log(rawData);
- if (rawData === "oasis") {
- log(`Oasis is already running on host ${host} and port ${port}`);
- if (config.open === true) {
- log("Opening link to existing instance of Oasis");
- open(url);
- } else {
- log(
- "Not opening your browser because opening is disabled by your config"
- );
- }
- process.exit(0);
- } else {
- throw new Error(`Another server is already running at ${url}.
- It might be another copy of Oasis or another program on your computer.
- You can run Oasis on a different port number with this option:
- oasis --port ${config.port + 1}
- Alternatively, you can set the default port in ${defaultConfigFile} with:
- {
- "port": ${config.port + 1}
- }
- `);
- }
- });
- });
- } else {
- console.log("");
- console.log("Oasis traceback (share below content with devs to report!):");
- console.log("===========================================================");
- console.log(err);
- console.log("");
- }
- });
- process.argv = [];
- const http = require("../client/middleware");
- const {koaBody} = require("../server/node_modules/koa-body");
- const { nav, ul, li, a, form, button, div, section, h2, p } = require("../server/node_modules/hyperaxe");
- const open = require("../server/node_modules/open");
- const pull = require("../server/node_modules/pull-stream");
- const koaRouter = require("../server/node_modules/@koa/router");
- const ssbMentions = require("../server/node_modules/ssb-mentions");
- const isSvg = require('../server/node_modules/is-svg');
- const { isFeed, isMsg, isBlob } = require("../server/node_modules/ssb-ref");
- const ssb = require("../client/gui");
- const router = new koaRouter();
- const extractMentions = async (text) => {
- const mentions = ssbMentions(text) || [];
- const resolvedMentions = await Promise.all(mentions.map(async (mention) => {
- const name = mention.name || await about.name(mention.link);
- return {
- link: mention.link,
- name: name || 'Anonymous',
- };
- }));
- return resolvedMentions;
- };
- const cooler = ssb({ offline: config.offline });
- const models = require("../models/main_models");
- const { about, blob, friend, meta, post, vote } = models({
- cooler,
- isPublic: config.public,
- });
- const { handleBlobUpload, serveBlob, FileTooLargeError } = require('../backend/blobHandler.js');
- const extractBlobId = (md) => md ? (md.match(/\((&[^)]+)\)/)?.[1] ?? null) : null;
- const exportmodeModel = require('../models/exportmode_model');
- const panicmodeModel = require('../models/panicmode_model');
- const cipherModel = require('../models/cipher_model');
- const legacyModel = require('../models/legacy_model');
- const walletModel = require('../models/wallet_model')
- const pmModel = require('../models/pm_model')({ cooler, isPublic: config.public });
- const bookmarksModel = require("../models/bookmarking_model")({ cooler, isPublic: config.public });
- const opinionsModel = require('../models/opinions_model')({ cooler, isPublic: config.public });
- const eventsModel = require('../models/events_model')({ cooler, isPublic: config.public });
- const tasksModel = require('../models/tasks_model')({ cooler, isPublic: config.public });
- const votesModel = require('../models/votes_model')({ cooler, isPublic: config.public });
- const reportsModel = require('../models/reports_model')({ cooler, isPublic: config.public });
- const transfersModel = require('../models/transfers_model')({ cooler, isPublic: config.public });
- const tagsModel = require('../models/tags_model')({ cooler, isPublic: config.public });
- const cvModel = require('../models/cv_model')({ cooler, isPublic: config.public });
- const inhabitantsModel = require('../models/inhabitants_model')({ cooler, isPublic: config.public });
- const feedModel = require('../models/feed_model')({ cooler, isPublic: config.public });
- const imagesModel = require("../models/images_model")({ cooler, isPublic: config.public });
- const audiosModel = require("../models/audios_model")({ cooler, isPublic: config.public });
- const videosModel = require("../models/videos_model")({ cooler, isPublic: config.public });
- const documentsModel = require("../models/documents_model")({ cooler, isPublic: config.public });
- const agendaModel = require("../models/agenda_model")({ cooler, isPublic: config.public });
- const trendingModel = require('../models/trending_model')({ cooler, isPublic: config.public });
- const statsModel = require('../models/stats_model')({ cooler, isPublic: config.public });
- const tribesModel = require('../models/tribes_model')({ cooler, isPublic: config.public });
- const tribesContentModel = require('../models/tribes_content_model')({ cooler, isPublic: config.public });
- const searchModel = require('../models/search_model')({ cooler, isPublic: config.public });
- const activityModel = require('../models/activity_model')({ cooler, isPublic: config.public });
- const pixeliaModel = require('../models/pixelia_model')({ cooler, isPublic: config.public });
- const marketModel = require('../models/market_model')({ cooler, isPublic: config.public });
- const forumModel = require('../models/forum_model')({ cooler, isPublic: config.public });
- const blockchainModel = require('../models/blockchain_model')({ cooler, isPublic: config.public });
- const jobsModel = require('../models/jobs_model')({ cooler, isPublic: config.public });
- const projectsModel = require("../models/projects_model")({ cooler, isPublic: config.public });
- const bankingModel = require("../models/banking_model")({ services: { cooler }, isPublic: config.public });
- const favoritesModel = require("../models/favorites_model")({services: { cooler }, audiosModel, bookmarksModel, documentsModel, imagesModel, videosModel });
- const parliamentModel = require('../models/parliament_model')({ cooler, services: { tribes: tribesModel, votes: votesModel, inhabitants: inhabitantsModel, banking: bankingModel } });
- const courtsModel = require('../models/courts_model')({ cooler, services: { votes: votesModel, inhabitants: inhabitantsModel, tribes: tribesModel, banking: bankingModel } });
- const getVoteComments = async (voteId) => {
- const raw = await post.topicComments(voteId);
- return (raw || []).filter(c => c?.value?.content?.type === 'post' && c.value.content.root === voteId)
- .sort((a, b) => (a?.value?.timestamp || 0) - (b?.value?.timestamp || 0));
- };
- const enrichWithComments = async (items, idKey = 'id') => {
- await Promise.all(items.map(async x => { x.commentCount = (await getVoteComments(x[idKey] || x.key || x.rootId)).length; }));
- return items;
- };
- const withCount = (item, comments) => ({ ...item, commentCount: comments.length });
- const mediaResolvers = {
- images: id => imagesModel.resolveRootId(id),
- audios: id => audiosModel.resolveRootId(id),
- videos: id => videosModel.resolveRootId(id),
- documents: id => documentsModel.resolveRootId(id),
- bookmarks: id => bookmarksModel.resolveRootId(id)
- };
- const mediaModCheck = { images: 'imagesMod', audios: 'audiosMod', videos: 'videosMod', documents: 'documentsMod', bookmarks: 'bookmarksMod', market: 'marketMod', jobs: 'jobsMod', projects: 'projectsMod' };
- const favAction = async (ctx, kind, action) => {
- if (!checkMod(ctx, mediaModCheck[kind])) { ctx.redirect('/modules'); return; }
- const rootId = await mediaResolvers[kind](ctx.params.id);
- await mediaFavorites[action + 'Favorite'](kind, rootId);
- ctx.redirect(safeReturnTo(ctx, `/${kind}`, [`/${kind}`]));
- };
- const commentAction = async (ctx, kind, idParam) => {
- const modKey = mediaModCheck[kind];
- if (modKey && !checkMod(ctx, modKey)) { ctx.redirect('/modules'); return; }
- const itemId = ctx.params[idParam];
- let text = stripDangerousTags((ctx.request.body.text || '').trim());
- const rt = safeReturnTo(ctx, `/${kind}/${encodeURIComponent(itemId)}`, [`/${kind}`]);
- const blobMarkdown = await handleBlobUpload(ctx, 'blob');
- if (blobMarkdown) text += blobMarkdown;
- if (!text) { ctx.redirect(rt); return; }
- await post.publish({ text, root: itemId, dest: itemId });
- ctx.redirect(rt);
- };
- const opinionModels = { images: imagesModel, audios: audiosModel, videos: videosModel, documents: documentsModel, bookmarks: bookmarksModel };
- const deleteModels = { images: imagesModel, audios: audiosModel, videos: videosModel, documents: documentsModel, bookmarks: bookmarksModel };
- const opinionAction = async (ctx, kind, idParam) => {
- const modKey = mediaModCheck[kind];
- if (modKey && !checkMod(ctx, modKey)) { ctx.redirect('/modules'); return; }
- await opinionModels[kind].createOpinion(ctx.params[idParam], ctx.params.category);
- ctx.redirect(safeReturnTo(ctx, `/${kind}`, [`/${kind}`]));
- };
- const deleteAction = async (ctx, kind, deleteFn = 'delete' + kind.charAt(0).toUpperCase() + kind.slice(1, -1) + 'ById') => {
- const modKey = mediaModCheck[kind];
- if (modKey && !checkMod(ctx, modKey)) { ctx.redirect('/modules'); return; }
- await deleteModels[kind][deleteFn](ctx.params.id);
- ctx.redirect(safeReturnTo(ctx, `/${kind}?filter=mine`, [`/${kind}`]));
- };
- const mediaCreateModels = { audios: audiosModel, videos: videosModel };
- const mediaCreateAction = async (ctx, kind) => {
- const modKey = mediaModCheck[kind];
- if (modKey && !checkMod(ctx, modKey)) { ctx.redirect('/modules'); return; }
- const blob = await handleBlobUpload(ctx, kind.slice(0, -1));
- const { tags, title, description } = ctx.request.body;
- await mediaCreateModels[kind][`create${kind.charAt(0).toUpperCase()}${kind.slice(1, -1)}`](blob, stripDangerousTags(tags), stripDangerousTags(title), stripDangerousTags(description));
- ctx.redirect(safeReturnTo(ctx, `/${kind}?filter=all`, [`/${kind}`]));
- };
- const mediaUpdateAction = async (ctx, kind) => {
- const modKey = mediaModCheck[kind];
- if (modKey && !checkMod(ctx, modKey)) { ctx.redirect('/modules'); return; }
- const { tags, title, description } = ctx.request.body;
- const singular = kind.slice(0, -1);
- const blob = ctx.request.files?.[singular] ? await handleBlobUpload(ctx, singular) : null;
- await mediaCreateModels[kind][`update${kind.charAt(0).toUpperCase()}${kind.slice(1, -1)}ById`](ctx.params.id, blob, stripDangerousTags(tags), stripDangerousTags(title), stripDangerousTags(description));
- ctx.redirect(safeReturnTo(ctx, `/${kind}?filter=mine`, [`/${kind}`]));
- };
- const qf = (ctx, def = 'all') => ctx.query.filter || def;
- const qp = (ctx, def = 1) => Math.max(1, parseInt(ctx.query.page) || def);
- about._startNameWarmup();
- async function renderBlobMarkdown(text, mentions = {}, myFeedId, myUsername) {
- if (!text) return '';
- const mentionByFeed = {};
- Object.values(mentions).forEach(arr => {
- arr.forEach(m => {
- mentionByFeed[m.feed] = m;
- });
- });
- text = text.replace(/\[@([^\]]+)\]\(([^)]+)\)/g, (_, name, id) => {
- return `<a class="mention" href="/author/${encodeURIComponent(id)}">@${name}</a>`;
- });
- const words = text.split(' ');
- text = (await Promise.all(
- words.map(async (word) => {
- const match = /@([A-Za-z0-9_\-\.+=\/]+\.ed25519)/.exec(word);
- if (match && match[1]) {
- const feedId = match[1];
- const feedWithAt = feedId.startsWith('@') ? feedId : `@${feedId}`;
- let resolvedName;
- if (feedId === myFeedId || feedWithAt === myFeedId) {
- resolvedName = myUsername;
- } else {
- try { resolvedName = await about.name(feedWithAt); } catch { resolvedName = feedId.slice(0, 8); }
- }
- return word.replace(match[0], `<a class="mention" href="/author/${encodeURIComponent(feedWithAt)}">@${resolvedName}</a>`);
- }
- return word;
- })
- )).join(' ');
- text = text
- .replace(/!\[image:[^\]]+\]\(([^)]+)\)/g, (_, id) =>
- `<img src="/blob/${encodeURIComponent(id)}" alt="image" class="post-image" />`)
- .replace(/\[audio:[^\]]+\]\(([^)]+)\)/g, (_, id) =>
- `<audio controls class="post-audio" src="/blob/${encodeURIComponent(id)}"></audio>`)
- .replace(/\[video:[^\]]+\]\(([^)]+)\)/g, (_, id) =>
- `<video controls class="post-video" src="/blob/${encodeURIComponent(id)}"></video>`)
- .replace(/\[pdf:([^\]]*)\]\(([^)]+)\)/g, (_, name, id) => {
- const { i18n } = require("../views/main_views");
- return `<a class="post-pdf" href="/blob/${encodeURIComponent(id)}" target="_blank">${name || (i18n && i18n.pdfFallbackLabel) || 'PDF'}</a>`;
- });
- return text;
- }
- async function resolveMentionText(text) {
- if (!text || typeof text !== 'string') return text;
- const mentionRe = /@([A-Za-z0-9_\-\.+=\/]+\.ed25519)/g;
- const matches = [...text.matchAll(mentionRe)];
- if (!matches.length) return text;
- const seen = new Map();
- for (const m of matches) {
- const raw = m[1];
- const feed = raw.startsWith('@') ? raw : `@${raw}`;
- if (seen.has(feed)) continue;
- let name;
- try { name = await about.name(feed); } catch { name = feed.slice(1, 9); }
- seen.set(feed, name);
- }
- return text.replace(mentionRe, (full, id) => {
- const feed = id.startsWith('@') ? id : `@${id}`;
- const name = seen.get(feed) || feed.slice(1, 9);
- return `[@${name}](${feed})`;
- });
- }
- const preparePreview = async function (ctx) {
- let text = String(ctx.request.body.text || "")
- const contentWarning = stripDangerousTags(String(ctx.request.body.contentWarning || ""))
- const ensureAt = (id) => {
- const s = String(id || "")
- if (!s) return ""
- return s.startsWith("@") ? s : `@${s.replace(/^@+/, "")}`
- }
- const stripAt = (id) => String(id || "").replace(/^@+/, "")
- const norm = (s) => String(s || "").trim().toLowerCase()
- const ssbClient = await cooler.open()
- const authorMeta = {
- id: ssbClient.id,
- name: await about.name(ssbClient.id),
- image: await about.image(ssbClient.id),
- }
- const myId = String(authorMeta.id)
- text = text.replace(
- /\[@([^\]]+)\]\s*\(\s*@?([^) \t\r\n]+\.ed25519)\s*\)/g,
- (_m, label, feed) => `[@${label}](@${stripAt(feed)})`
- )
- const mentions = {}
- const normalizeMatch = (m) => {
- const feed = ensureAt(m?.feed || m?.link || m?.id || "")
- const name = String(m?.name || "")
- const img = m?.img || m?.image || null
- const rel = m?.rel || {}
- return { ...m, feed, name, img, rel }
- }
- const pushUnique = (key, arr) => {
- const prev = Array.isArray(mentions[key]) ? mentions[key] : []
- const seen = new Set(prev.map((x) => String(x?.feed || "")))
- const out = prev.slice()
- for (const x of arr) {
- const f = String(x?.feed || "")
- if (!f) continue
- if (seen.has(f)) continue
- seen.add(f)
- out.push(x)
- }
- if (out.length) mentions[key] = out
- }
- const chooseByPhrase = (matches, phrase) => {
- const p = norm(phrase)
- const exact = matches.filter((mm) => norm(mm.name) === p)
- if (exact.length) return exact
- const starts = matches.filter((mm) => norm(mm.name).startsWith(p))
- if (starts.length) return starts
- const incl = matches.filter((mm) => norm(mm.name).includes(p))
- if (incl.length) return incl
- return null
- }
- const rex = /(^|\s)(?!\[)@([a-zA-Z0-9\-/.=+]{3,})(?:\s+([a-zA-Z0-9][a-zA-Z0-9\-/.=+]{1,}))?(?:\s+([a-zA-Z0-9][a-zA-Z0-9\-/.=+]{1,}))?\b/g
- let m
- while ((m = rex.exec(text)) !== null) {
- const w1 = m[2]
- const w2 = m[3]
- const w3 = m[4]
- if (/\.ed25519$/.test(w1)) {
- const feed = ensureAt(w1)
- const [name, img, rel] = await Promise.all([
- about.name(feed),
- about.image(feed),
- friend.getRelationship(feed).catch(() => ({ followsMe: false, following: false, blocking: false, me: false }))
- ])
- pushUnique(w1, [{ feed, name, img, rel }])
- continue
- }
- const phrase1 = w1
- const phrase2 = w2 ? `${w1} ${w2}` : null
- const phrase3 = w3 ? `${w1} ${w2 ? w2 : ""} ${w3}`.replace(/\s+/g, " ").trim() : null
- const matchesRaw = about.named(w1) || []
- const matchesAll = matchesRaw.map(normalizeMatch)
- const matches = matchesAll.filter((mm) => String(mm.feed) !== myId && !mm?.rel?.me)
- let chosenKey = phrase1
- let chosenMatches = matches
- if (phrase3) {
- const best3 = chooseByPhrase(matches, phrase3)
- if (best3 && best3.length) {
- chosenKey = phrase3
- chosenMatches = best3
- } else if (phrase2) {
- const best2 = chooseByPhrase(matches, phrase2)
- if (best2 && best2.length) {
- chosenKey = phrase2
- chosenMatches = best2
- }
- }
- } else if (phrase2) {
- const best2 = chooseByPhrase(matches, phrase2)
- if (best2 && best2.length) {
- chosenKey = phrase2
- chosenMatches = best2
- }
- }
- if (chosenMatches.length > 0) {
- pushUnique(chosenKey, chosenMatches)
- }
- }
- Object.keys(mentions).forEach((key) => {
- const matches = Array.isArray(mentions[key]) ? mentions[key] : []
- const meaningful = matches.filter((mm) => (mm?.rel?.followsMe || mm?.rel?.following) && !mm?.rel?.blocking && String(mm?.feed || "") !== myId && !mm?.rel?.me)
- mentions[key] = meaningful.length > 0 ? meaningful : matches
- })
- const rexReplace = /(^|\s)(?!\[)@([a-zA-Z0-9\-/.=+]{3,})(?:\s+([a-zA-Z0-9][a-zA-Z0-9\-/.=+]{1,}))?(?:\s+([a-zA-Z0-9][a-zA-Z0-9\-/.=+]{1,}))?\b/g
- const replacer = (match, prefix, w1, w2, w3) => {
- const phrase1 = w1
- const phrase2 = w2 ? `${w1} ${w2}` : null
- const phrase3 = w3 ? `${w1} ${w2 ? w2 : ""} ${w3}`.replace(/\s+/g, " ").trim() : null
- const tryKey = (k) => {
- const arr = mentions[k]
- if (arr && arr.length === 1) {
- return `${prefix}[@${arr[0].name}](${ensureAt(arr[0].feed)})`
- }
- return null
- }
- if (/\.ed25519$/.test(w1)) {
- const arr = mentions[w1]
- if (arr && arr.length === 1) return `${prefix}[@${arr[0].name}](${ensureAt(arr[0].feed)})`
- return match
- }
- const r3 = phrase3 ? tryKey(phrase3) : null
- if (r3) return r3
- const r2 = phrase2 ? tryKey(phrase2) : null
- if (r2) return r2
- const r1 = tryKey(phrase1)
- if (r1) return r1
- return match
- }
- text = text.replace(rexReplace, replacer)
- const blobMarkdown = await handleBlobUpload(ctx, "blob")
- if (blobMarkdown) {
- text += blobMarkdown
- }
- const renderedText = await renderBlobMarkdown(
- text,
- mentions,
- authorMeta.id,
- authorMeta.name
- )
- const hasBrTags = /<br\s*\/?>/i.test(renderedText)
- const hasBlockTags = /<(p|div|ul|ol|li|pre|blockquote|h[1-6]|table|tr|td|th|section|article)\b/i.test(renderedText)
- let formattedText = renderedText
- if (!hasBrTags && !hasBlockTags && /[\r\n]/.test(renderedText)) {
- formattedText = renderedText.replace(/\r\n|\r|\n/g, "<br>")
- }
- return { authorMeta, text, formattedText, mentions, contentWarning }
- }
- const megabyte = Math.pow(2, 20);
- const maxSize = 50 * megabyte;
- const homeDir = os.homedir();
- const blobsPath = path.join(homeDir, '.ssb', 'blobs', 'tmp');
- const gossipPath = path.join(homeDir, '.ssb', 'gossip.json');
- const unfollowedPath = path.join(homeDir, '.ssb', 'gossip_unfollowed.json');
- const ensureJSONFile = (p, init = []) => { fs.mkdirSync(path.dirname(p), { recursive: true }); if (!fs.existsSync(p)) fs.writeFileSync(p, JSON.stringify(init, null, 2), 'utf8'); };
- const readJSON = p => { ensureJSONFile(p, []); try { return JSON.parse(fs.readFileSync(p, 'utf8') || '[]'); } catch { return []; } };
- const writeJSON = (p, d) => { fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, JSON.stringify(d, null, 2), 'utf8'); };
- const canonicalKey = k => { let c = String(k).replace(/^@/, '').replace(/\.ed25519$/, '').replace(/-/g, '+').replace(/_/g, '/'); if (!c.endsWith('=')) c += '='; return `@${c}.ed25519`; };
- const msAddrFrom = (h, p, k) => `net:${h}:${Number(p) || 8008}~shs:${canonicalKey(k).slice(1, -9)}`;
- ensureJSONFile(gossipPath, []);
- ensureJSONFile(unfollowedPath, []);
- const koaBodyMiddleware = koaBody({
- multipart: true,
- formidable: {
- uploadDir: blobsPath,
- keepExtensions: true,
- maxFieldsSize: maxSize,
- maxFileSize: maxSize,
- hash: 'sha256',
- },
- parsedMethods: ['POST'],
- });
- const resolveCommentComponents = async function (ctx) {
- let parentId;
- try {
- parentId = decodeURIComponent(ctx.params.message);
- } catch {
- parentId = ctx.params.message;
- }
- const parentMessage = await post.get(parentId);
- if (!parentMessage || !parentMessage.value) {
- throw new Error("Invalid parentMessage or missing 'value'");
- }
- const myFeedId = await meta.myFeedId();
- const hasRoot =
- typeof parentMessage?.value?.content?.root === "string" &&
- ssbRef.isMsg(parentMessage.value.content.root);
- const hasFork =
- typeof parentMessage?.value?.content?.fork === "string" &&
- ssbRef.isMsg(parentMessage.value.content.fork);
- const rootMessage = hasRoot
- ? hasFork
- ? parentMessage
- : await post.get(parentMessage.value.content.root)
- : parentMessage;
- const messages = await post.topicComments(rootMessage.key);
- messages.push(rootMessage);
- let contentWarning;
- if (ctx.request.body) {
- const rawContentWarning = stripDangerousTags(String(ctx.request.body.contentWarning || "").trim());
- contentWarning = rawContentWarning.length > 0 ? rawContentWarning : undefined;
- }
- return { messages, myFeedId, parentMessage, contentWarning };
- };
- const { authorView, previewCommentView, commentView, editProfileView, extendedView, latestView, likesView, threadView, hashtagView, mentionsView, popularView, previewView, privateView, publishCustomView, publishView, previewSubtopicView, subtopicView, imageSearchView, setLanguage, topicsView, summaryView, threadsView } = require("../views/main_views");
- const { activityView } = require("../views/activity_view");
- const { cvView, createCVView } = require("../views/cv_view");
- const { indexingView } = require("../views/indexing_view");
- const { pixeliaView } = require("../views/pixelia_view");
- const { statsView } = require("../views/stats_view");
- const { tribesView, tribeView, renderInvitePage } = require("../views/tribes_view");
- const { agendaView } = require("../views/agenda_view");
- const { documentView, singleDocumentView } = require("../views/document_view");
- const { inhabitantsView, inhabitantsProfileView } = require("../views/inhabitants_view");
- const { walletViewRender, walletView, walletHistoryView, walletReceiveView, walletSendFormView, walletSendConfirmView, walletSendResultView, walletErrorView } = require("../views/wallet_view");
- const { pmView } = require("../views/pm_view");
- const { tagsView } = require("../views/tags_view");
- const { videoView, singleVideoView } = require("../views/video_view");
- const { audioView, singleAudioView } = require("../views/audio_view");
- const { eventView, singleEventView } = require("../views/event_view");
- const { invitesView } = require("../views/invites_view");
- const { modulesView } = require("../views/modules_view");
- const { reportView, singleReportView } = require("../views/report_view");
- const { taskView, singleTaskView } = require("../views/task_view");
- const { voteView } = require("../views/vote_view");
- const { bookmarkView, singleBookmarkView } = require("../views/bookmark_view");
- const { feedView, feedCreateView, singleFeedView } = require("../views/feed_view");
- const { legacyView } = require("../views/legacy_view");
- const { opinionsView } = require("../views/opinions_view");
- const { peersView } = require("../views/peers_view");
- const { searchView } = require("../views/search_view");
- const { transferView, singleTransferView } = require("../views/transfer_view");
- const { cipherView } = require("../views/cipher_view");
- const { imageView, singleImageView } = require("../views/image_view");
- const { settingsView } = require("../views/settings_view");
- const { trendingView } = require("../views/trending_view");
- const { marketView, singleMarketView } = require("../views/market_view");
- const { aiView } = require("../views/AI_view");
- const { forumView, singleForumView } = require("../views/forum_view");
- const { renderBlockchainView, renderSingleBlockView } = require("../views/blockchain_view");
- const { jobsView, singleJobsView, renderJobForm } = require("../views/jobs_view");
- const { projectsView, singleProjectView } = require("../views/projects_view")
- const { renderBankingView, renderSingleAllocationView, renderEpochView } = require("../views/banking_views")
- const { favoritesView } = require("../views/favorites_view");
- const { parliamentView } = require("../views/parliament_view");
- const { courtsView, courtsCaseView } = require('../views/courts_view');
- let sharp;
- try {
- sharp = require("sharp");
- } catch (e) {
- }
- const readmePath = path.join(__dirname, "..", ".." ,"README.md");
- const packagePath = path.join(__dirname, "..", "server", "package.json");
- const readme = fs.readFileSync(readmePath, "utf8");
- const version = JSON.parse(fs.readFileSync(packagePath, "utf8")).version;
- const nullImageId = '&0000000000000000000000000000000000000000000=.sha256';
- const getAvatarUrl = img => !img || img === nullImageId ? '/assets/images/default-avatar.png' : `/image/256/${encodeURIComponent(img)}`;
- const MAX_TITLE_LENGTH = 150;
- const MAX_TEXT_LENGTH = 8000;
- const parseSizeMB = (s) => { if (!s) return 0; const m = String(s).match(/([\d.]+)\s*(GB|MB|KB|B)/i); if (!m) return 0; const v = parseFloat(m[1]), u = m[2].toUpperCase(); return u === 'GB' ? v * 1024 : u === 'MB' ? v : u === 'KB' ? v / 1024 : v / (1024 * 1024); };
- const tooLong = (ctx, value, max, label) => {
- if (value && value.length > max) {
- ctx.status = 400;
- ctx.body = `${label} too long (max ${max})`;
- return true;
- }
- return false;
- };
- router
- .param("imageSize", (imageSize, ctx, next) => {
- const size = Number(imageSize);
- const isInteger = size % 1 === 0;
- const overMinSize = size > 2;
- const underMaxSize = size <= 256;
- ctx.assert(
- isInteger && overMinSize && underMaxSize,
- 400,
- "Invalid image size"
- );
- return next();
- })
- .param("blobId", (blobId, ctx, next) => {
- ctx.assert(ssbRef.isBlob(blobId), 400, "Invalid blob link");
- return next();
- })
- .param("message", (message, ctx, next) => {
- ctx.assert(ssbRef.isMsg(message), 400, "Invalid message link");
- return next();
- })
- .param("feed", (message, ctx, next) => {
- ctx.assert(ssbRef.isFeedId(message), 400, "Invalid feed link");
- return next();
- })
- .get("/", async (ctx) => {
- const currentConfig = getConfig();
- const homePage = currentConfig.homePage || "activity";
- ctx.redirect(`/${homePage}`);
- })
- .get("/robots.txt", (ctx) => {
- ctx.body = "User-agent: *\nDisallow: /";
- })
- .get(oasisCheckPath, (ctx) => {
- ctx.body = "oasis";
- })
- .get('/stats', async (ctx) => {
- const filter = qf(ctx, 'ALL'), stats = await statsModel.getStats(filter);
- const myId = getViewerId();
- const myAddress = await bankingModel.getUserAddress(myId);
- const addrRows = await bankingModel.listAddressesMerged();
- stats.banking = {
- myAddress: myAddress || null,
- totalAddresses: Array.isArray(addrRows) ? addrRows.length : 0
- };
- const totalMB = parseSizeMB(stats.statsBlobsSize) + parseSizeMB(stats.statsBlockchainSize);
- const hcT = parseFloat((totalMB * 0.0002 * 475).toFixed(2));
- const inhabitants = stats.usersKPIs?.totalInhabitants || stats.inhabitants || 1;
- const hcH = inhabitants > 0 ? parseFloat((hcT / inhabitants).toFixed(2)) : 0;
- sharedState.setCarbonHcT(hcT);
- sharedState.setCarbonHcH(hcH);
- ctx.body = statsView(stats, filter);
- })
- .get("/public/popular/:period", async (ctx) => {
- if (!checkMod(ctx, 'popularMod')) return ctx.redirect('/modules');
- const i18n = require("../client/assets/translations/i18n"), lang = ctx.cookies.get('language') || getConfig().language || 'en', t = i18n[lang] || i18n['en'];
- const messages = sanitizeMessages(await post.popular({ period: ctx.params.period }));
- ctx.body = await popularView({ messages, prefix: nav(div({ class: "filters" }, ul(['day','week','month','year'].map(p => li(form({ method: "GET", action: `/public/popular/${p}` }, button({ type: "submit", class: "filter-btn" }, t[p]))))))) });
- })
- .get("/modules", async (ctx) => {
- const modules = ['popular', 'topics', 'summaries', 'latest', 'threads', 'multiverse', 'invites', 'wallet', 'legacy', 'cipher', 'bookmarks', 'videos', 'docs', 'audios', 'tags', 'images', 'trending', 'events', 'tasks', 'market', 'tribes', 'votes', 'reports', 'opinions', 'transfers', 'feed', 'pixelia', 'agenda', 'favorites', 'ai', 'forum', 'jobs', 'projects', 'banking', 'parliament', 'courts'];
- const cfg = getConfig().modules;
- ctx.body = modulesView(modules.reduce((acc, m) => { acc[`${m}Mod`] = cfg[`${m}Mod`]; return acc; }, {}));
- })
- .get('/ai', async (ctx) => {
- if (!checkMod(ctx, 'aiMod')) return ctx.redirect('/modules');
- startAI();
- const lang = ctx.cookies.get('language') || getConfig().language || 'en', historyPath = path.join(__dirname, '..', '..', 'src', 'configs', 'AI-history.json');
- require('../views/main_views').setLanguage(lang);
- let chatHistory = []; try { chatHistory = JSON.parse(fs.readFileSync(historyPath, 'utf-8')); } catch {}
- ctx.body = aiView(chatHistory, getConfig().ai?.prompt?.trim() || '');
- })
- .get('/pixelia', async (ctx) => {
- if (!checkMod(ctx, 'pixeliaMod')) { ctx.redirect('/modules'); return; }
- const pixelArt = await pixeliaModel.listPixels();
- ctx.body = pixeliaView(pixelArt);
- })
- .get('/blockexplorer', async (ctx) => {
- const userId = getViewerId();
- const query = ctx.query || {};
- const search = {
- id: query.id || '',
- author: query.author || '',
- from: query.from || '',
- to: query.to || ''
- };
- const searchActive = Object.values(search).some(v => String(v || '').trim().length > 0);
- let filter = query.filter || 'recent';
- if (searchActive && String(filter).toLowerCase() === 'recent') filter = 'all';
- const blockchainData = await blockchainModel.listBlockchain(filter, userId, search);
- ctx.body = renderBlockchainView(blockchainData, filter, userId, search);
- })
- .get('/blockexplorer/block/:id', async (ctx) => {
- const userId = getViewerId();
- const query = ctx.query || {};
- const search = {
- id: query.id || '',
- author: query.author || '',
- from: query.from || '',
- to: query.to || ''
- };
- const searchActive = Object.values(search).some(v => String(v || '').trim().length > 0);
- let filter = query.filter || 'recent';
- if (searchActive && String(filter).toLowerCase() === 'recent') filter = 'all';
- const blockId = ctx.params.id;
- const block = await blockchainModel.getBlockById(blockId);
- const viewMode = query.view || 'block';
- ctx.body = renderSingleBlockView(block, filter, userId, search, viewMode);
- })
- .get("/public/latest", async (ctx) => {
- if (!checkMod(ctx, 'latestMod')) { ctx.redirect('/modules'); return; }
- const messages = sanitizeMessages(await post.latest());
- ctx.body = await latestView({ messages });
- })
- .get("/public/latest/extended", async (ctx) => {
- if (!checkMod(ctx, 'extendedMod')) { ctx.redirect('/modules'); return; }
- const messages = sanitizeMessages(await post.latestExtended());
- ctx.body = await extendedView({ messages });
- })
- .get("/public/latest/topics", async (ctx) => {
- if (!checkMod(ctx, 'topicsMod')) { ctx.redirect('/modules'); return; }
- const messages = sanitizeMessages(await post.latestTopics());
- const channels = await post.channels();
- const list = channels.map((c) => {
- return li(a({ href: `/hashtag/${c}` }, `#${c}`));
- });
- const prefix = nav(ul(list));
- ctx.body = await topicsView({ messages, prefix });
- })
- .get("/public/latest/summaries", async (ctx) => {
- if (!checkMod(ctx, 'summariesMod')) { ctx.redirect('/modules'); return; }
- const messages = sanitizeMessages(await post.latestSummaries());
- ctx.body = await summaryView({ messages });
- })
- .get("/public/latest/threads", async (ctx) => {
- if (!checkMod(ctx, 'threadsMod')) { ctx.redirect('/modules'); return; }
- const messages = sanitizeMessages(await post.latestThreads());
- ctx.body = await threadsView({ messages });
- })
- .get('/author/:feed', async (ctx) => {
- const feedId = decodeURIComponent(ctx.params.feed || ''), gt = Number(ctx.request.query.gt || -1), lt = Number(ctx.request.query.lt || -1);
- if (lt > 0 && gt > 0 && gt >= lt) throw new Error('Given search range is empty');
- const [description, name, image, messages, firstPost, lastPost, relationship, ecoAddress, bankData] = await Promise.all([
- about.description(feedId), about.name(feedId), about.image(feedId), post.fromPublicFeed(feedId, gt, lt),
- post.firstBy(feedId), post.latestBy(feedId), friend.getRelationship(feedId), bankingModel.getUserAddress(feedId), bankingModel.getBankingData(feedId)
- ]);
- const sanitizedMsgs = sanitizeMessages(messages);
- const normTs = t => { const n = Number(t || 0); return !isFinite(n) || n <= 0 ? 0 : n < 1e12 ? n * 1000 : n; };
- const pull = require('../server/node_modules/pull-stream'), ssb = await require('../client/gui')({ offline: require('../server/ssb_config').offline }).open();
- const latestFromStream = await new Promise(res => pull(ssb.createUserStream({ id: feedId, reverse: true }), pull.filter(m => m?.value?.content?.type !== 'tombstone'), pull.take(1), pull.collect((err, arr) => res(!err && arr?.[0] ? normTs(arr[0].value?.timestamp || arr[0].timestamp) : 0))));
- const days = latestFromStream ? (Date.now() - latestFromStream) / 86400000 : Infinity;
- ctx.body = await authorView({ feedId, messages: sanitizedMsgs, firstPost, lastPost, name, description, avatarUrl: getAvatarUrl(image), relationship, ecoAddress, karmaScore: bankData.karmaScore, lastActivityBucket: days < 14 ? 'green' : days < 182.5 ? 'orange' : 'red' });
- })
- .get("/search", async (ctx) => {
- const query = ctx.query.query || '';
- if (!query) return ctx.body = await searchView({ messages: [], query, types: [] });
- const results = await searchModel.search({ query, types: [] });
- ctx.body = await searchView({ results: Object.entries(results).reduce((acc, [type, msgs]) => {
- acc[type] = msgs.map(msg => (!msg.value?.content) ? {} : { ...msg, content: msg.value.content, author: msg.value.content.author || 'Unknown' });
- return acc;
- }, {}), query, types: [] });
- })
- .get("/images", async (ctx) => {
- if (!checkMod(ctx, 'imagesMod')) { ctx.redirect('/modules'); return; }
- const { filter = 'all', q = '', sort = 'recent' } = ctx.query;
- const items = await imagesModel.listAll({ filter: filter === 'favorites' ? 'all' : filter, q, sort, viewerId: getViewerId() });
- const fav = await mediaFavorites.getFavoriteSet('images');
- let enriched = items.map(x => ({ ...x, isFavorite: fav.has(String(x.rootId || x.key)) }));
- if (filter === 'favorites') enriched = enriched.filter(x => x.isFavorite);
- await Promise.all(enriched.map(async x => { x.commentCount = (await getVoteComments(x.key)).length; }));
- ctx.body = await imageView(enriched, filter, null, { q, sort });
- })
- .get("/images/edit/:id", async (ctx) => {
- if (!checkMod(ctx, 'imagesMod')) { ctx.redirect('/modules'); return; }
- const img = await imagesModel.getImageById(ctx.params.id, getViewerId());
- const fav = await mediaFavorites.getFavoriteSet('images');
- ctx.body = await imageView([{ ...img, isFavorite: fav.has(String(img.rootId || img.key)) }], 'edit', img.key, { returnTo: ctx.query.returnTo || '' });
- })
- .get("/images/:imageId", async (ctx) => {
- if (!checkMod(ctx, 'imagesMod')) { ctx.redirect('/modules'); return; }
- const { imageId } = ctx.params; const { filter = 'all', q = '', sort = 'recent' } = ctx.query;
- const img = await imagesModel.getImageById(imageId, getViewerId());
- const fav = await mediaFavorites.getFavoriteSet('images');
- const comments = await getVoteComments(img.key);
- ctx.body = await singleImageView({ ...img, isFavorite: fav.has(String(img.rootId || img.key)), commentCount: comments.length }, filter, comments, { q, sort, returnTo: safeReturnTo(ctx, `/images?filter=${encodeURIComponent(filter)}`, ['/images']) });
- })
- .get("/audios", async (ctx) => {
- if (!checkMod(ctx, 'audiosMod')) { ctx.redirect('/modules'); return; }
- const { filter = 'all', q = '', sort = 'recent' } = ctx.query;
- const items = await audiosModel.listAll({ filter: filter === 'favorites' ? 'all' : filter, q, sort, viewerId: getViewerId() });
- const fav = await mediaFavorites.getFavoriteSet('audios');
- let enriched = items.map(x => ({ ...x, isFavorite: fav.has(String(x.rootId || x.key)) }));
- if (filter === 'favorites') enriched = enriched.filter(x => x.isFavorite);
- await Promise.all(enriched.map(async x => { x.commentCount = (await getVoteComments(x.key)).length; }));
- ctx.body = await audioView(enriched, filter, null, { q, sort });
- })
- .get("/audios/edit/:id", async (ctx) => {
- if (!checkMod(ctx, 'audiosMod')) { ctx.redirect('/modules'); return; }
- const audio = await audiosModel.getAudioById(ctx.params.id, getViewerId());
- const fav = await mediaFavorites.getFavoriteSet('audios');
- ctx.body = await audioView([{ ...audio, isFavorite: fav.has(String(audio.rootId || audio.key)) }], 'edit', audio.key, { returnTo: ctx.query.returnTo || '' });
- })
- .get("/audios/:audioId", async (ctx) => {
- if (!checkMod(ctx, 'audiosMod')) { ctx.redirect('/modules'); return; }
- const { audioId } = ctx.params; const { filter = 'all', q = '', sort = 'recent' } = ctx.query;
- const audio = await audiosModel.getAudioById(audioId, getViewerId());
- const fav = await mediaFavorites.getFavoriteSet('audios');
- const comments = await getVoteComments(audio.key);
- ctx.body = await singleAudioView({ ...audio, isFavorite: fav.has(String(audio.rootId || audio.key)), commentCount: comments.length }, filter, comments, { q, sort, returnTo: safeReturnTo(ctx, `/audios?filter=${encodeURIComponent(filter)}`, ['/audios']) });
- })
- .get("/videos", async (ctx) => {
- if (!checkMod(ctx, 'videosMod')) { ctx.redirect('/modules'); return; }
- const { filter = 'all', q = '', sort = 'recent' } = ctx.query;
- const items = await videosModel.listAll({ filter: filter === 'favorites' ? 'all' : filter, q, sort, viewerId: getViewerId() });
- const fav = await mediaFavorites.getFavoriteSet('videos');
- let enriched = items.map(x => ({ ...x, isFavorite: fav.has(String(x.rootId || x.key)) }));
- if (filter === 'favorites') enriched = enriched.filter(x => x.isFavorite);
- await Promise.all(enriched.map(async x => { x.commentCount = (await getVoteComments(x.key)).length; }));
- ctx.body = await videoView(enriched, filter, null, { q, sort });
- })
- .get("/videos/edit/:id", async (ctx) => {
- if (!checkMod(ctx, 'videosMod')) { ctx.redirect('/modules'); return; }
- const video = await videosModel.getVideoById(ctx.params.id, getViewerId());
- const fav = await mediaFavorites.getFavoriteSet('videos');
- ctx.body = await videoView([{ ...video, isFavorite: fav.has(String(video.rootId || video.key)) }], 'edit', video.key, { returnTo: ctx.query.returnTo || '' });
- })
- .get("/videos/:videoId", async (ctx) => {
- if (!checkMod(ctx, 'videosMod')) { ctx.redirect('/modules'); return; }
- const { videoId } = ctx.params; const { filter = 'all', q = '', sort = 'recent' } = ctx.query;
- const video = await videosModel.getVideoById(videoId, getViewerId());
- const fav = await mediaFavorites.getFavoriteSet('videos');
- const comments = await getVoteComments(video.key);
- ctx.body = await singleVideoView({ ...video, isFavorite: fav.has(String(video.rootId || video.key)), commentCount: comments.length }, filter, comments, { q, sort, returnTo: safeReturnTo(ctx, `/videos?filter=${encodeURIComponent(filter)}`, ['/videos']) });
- })
- .get("/documents", async (ctx) => {
- const { filter = 'all', q = '', sort = 'recent' } = ctx.query;
- const items = await documentsModel.listAll({ filter, q, sort });
- await Promise.all(items.map(async x => { x.commentCount = (await getVoteComments(x.rootId || x.key)).length; }));
- ctx.body = await documentView(items, filter, null, { q, sort });
- })
- .get("/documents/edit/:id", async (ctx) => {
- const doc = await documentsModel.getDocumentById(ctx.params.id);
- ctx.body = await documentView([doc], 'edit', doc.key, { returnTo: ctx.query.returnTo || '' });
- })
- .get("/documents/:documentId", async (ctx) => {
- const { filter = "all", q = "", sort = "recent" } = ctx.query;
- const document = await documentsModel.getDocumentById(ctx.params.documentId);
- const comments = await getVoteComments(document.rootId || document.key);
- ctx.body = await singleDocumentView(withCount(document, comments), filter, comments, {
- q, sort,
- returnTo: safeReturnTo(ctx, `/documents/${encodeURIComponent(document.key)}?filter=${encodeURIComponent(filter)}${q ? `&q=${encodeURIComponent(q)}` : ""}${sort ? `&sort=${encodeURIComponent(sort)}` : ""}`, ["/documents"])
- });
- })
- .get('/cv', async ctx => {
- const cv = await cvModel.getCVByUserId()
- ctx.body = await cvView(cv)
- })
- .get('/cv/create', async ctx => {
- ctx.body = await createCVView()
- })
- .get('/cv/edit/:id', async ctx => {
- const cv = await cvModel.getCVByUserId()
- ctx.body = await createCVView(cv, true)
- })
- .get('/pm', async ctx => {
- const { recipients = '', subject = '', quote = '', preview = '' } = ctx.query;
- const quoted = quote ? quote.split('\n').map(l => '> ' + l).join('\n') + '\n\n' : '';
- const showPreview = preview === '1';
- ctx.body = await pmView(recipients, subject, quoted, showPreview);
- })
- .get('/inbox', async ctx => {
- if (!checkMod(ctx, 'inboxMod')) { ctx.redirect('/modules'); return; }
- const messages = sanitizeMessages(await pmModel.listAllPrivate());
- await refreshInboxCount(messages);
- ctx.body = await privateView({ messages }, ctx.query.filter || undefined);
- })
- .get('/tags', async ctx => {
- const filter = qf(ctx), tags = await tagsModel.listTags(filter);
- ctx.body = await tagsView(tags, filter);
- })
- .get('/reports', async ctx => {
- const filter = qf(ctx), reports = await enrichWithComments(await reportsModel.listAll());
- ctx.body = await reportView(reports, filter, null, ctx.query.category || '');
- })
- .get('/reports/edit/:id', async ctx => {
- const report = await reportsModel.getReportById(ctx.params.id);
- ctx.body = await reportView([report], 'edit', ctx.params.id);
- })
- .get('/reports/:reportId', async ctx => {
- const { reportId } = ctx.params, filter = qf(ctx), report = await reportsModel.getReportById(reportId);
- const comments = await getVoteComments(reportId);
- ctx.body = await singleReportView(withCount(report, comments), filter, comments);
- })
- .get('/trending', async (ctx) => {
- const filter = qf(ctx, 'RECENT'), { filtered = [] } = await trendingModel.listTrending(filter);
- ctx.body = await trendingView(filtered, filter, trendingModel.categories);
- })
- .get('/agenda', async (ctx) => {
- const filter = qf(ctx), data = await agendaModel.listAgenda(filter);
- ctx.body = await agendaView(data, filter);
- })
- .get("/hashtag/:hashtag", async (ctx) => {
- const { hashtag } = ctx.params;
- const messages = sanitizeMessages(await post.fromHashtag(hashtag));
- ctx.body = await hashtagView({ hashtag, messages });
- })
- .get('/inhabitants', async (ctx) => {
- const filter = qf(ctx);
- const query = { search: ctx.query.search || '' };
- const userId = getViewerId();
- if (['CVs', 'MATCHSKILLS'].includes(filter)) {
- Object.assign(query, {
- location: ctx.query.location || '',
- language: ctx.query.language || '',
- skills: ctx.query.skills || ''
- });
- }
- const inhabitants = await inhabitantsModel.listInhabitants({ filter, ...query });
- const [addresses, karmaList] = await Promise.all([
- bankingModel.listAddressesMerged(),
- Promise.all(
- inhabitants.map(async (u) => {
- try {
- const bank = await bankingModel.getBankingData(u.id);
- return { id: u.id, karmaScore: bank?.karmaScore || 0 };
- } catch {
- return { id: u.id, karmaScore: 0 };
- }
- })
- )
- ]);
- const activityList = await Promise.all(
- inhabitants.map(async (u) => {
- try {
- const ts = await inhabitantsModel.getLastActivityTimestampByUserId(u.id);
- const { bucket } = inhabitantsModel.bucketLastActivity(ts || null);
- return { id: u.id, lastActivityBucket: bucket };
- } catch {
- return { id: u.id, lastActivityBucket: 'red' };
- }
- })
- );
- const addrMap = new Map(addresses.map(x => [x.id, x.address]));
- const karmaMap = new Map(karmaList.map(x => [x.id, x.karmaScore]));
- const activityMap = new Map(activityList.map(x => [x.id, x.lastActivityBucket]));
- let enriched = inhabitants.map(u => ({
- ...u,
- ecoAddress: addrMap.get(u.id) || null,
- karmaScore:
- karmaMap.get(u.id) ??
- (typeof u.karmaScore === 'number' ? u.karmaScore : 0),
- lastActivityBucket: activityMap.get(u.id)
- }));
- if (filter === 'TOP KARMA') {
- enriched = enriched.sort((a, b) => (b.karmaScore || 0) - (a.karmaScore || 0));
- }
- if (filter === 'TOP ACTIVITY') {
- const order = { green: 0, orange: 1, red: 2 };
- enriched = enriched.sort(
- (a, b) => (order[a.lastActivityBucket] ?? 3) - (order[b.lastActivityBucket] ?? 3)
- );
- }
- ctx.body = await inhabitantsView(enriched, filter, query, userId);
- })
- .get('/inhabitant/:id', async (ctx) => {
- const id = ctx.params.id;
- const [about, cv, feed, photo, bank, lastTs] = await Promise.all([
- inhabitantsModel.getLatestAboutById(id),
- inhabitantsModel.getCVByUserId(id),
- inhabitantsModel.getFeedByUserId(id),
- inhabitantsModel.getPhotoUrlByUserId(id, 256),
- bankingModel.getBankingData(id).catch(() => ({ karmaScore: 0 })),
- inhabitantsModel.getLastActivityTimestampByUserId(id).catch(() => null)
- ]);
- const bucketInfo = inhabitantsModel.bucketLastActivity(lastTs || null);
- const currentUserId = getViewerId();
- const karmaScore = bank && typeof bank.karmaScore === 'number' ? bank.karmaScore : 0;
- ctx.body = await inhabitantsProfileView({ about, cv, feed, photo, karmaScore, lastActivityBucket: bucketInfo.bucket, viewedId: id }, currentUserId);
- })
- .get('/parliament', async (ctx) => {
- if (!checkMod(ctx, 'parliamentMod')) return ctx.redirect('/modules');
- const filter = (ctx.query.filter || 'government').toLowerCase();
- await ensureTerm();
- await runSweepOnce();
- const [governmentCardRaw, candidatures, proposals, futureLaws, canPropose, laws, historical, leaders, revocations, futureRevocations, revocationsEnactedCount, inhabitantsAll] = await Promise.all([
- parliamentModel.getGovernmentCard(),
- parliamentModel.listCandidatures('OPEN'),
- parliamentModel.listProposalsCurrent(),
- parliamentModel.listFutureLawsCurrent(),
- parliamentModel.canPropose(),
- parliamentModel.listLaws(),
- parliamentModel.listHistorical(),
- parliamentModel.listLeaders(),
- parliamentModel.listRevocationsCurrent(),
- parliamentModel.listFutureRevocationsCurrent(),
- parliamentModel.countRevocationsEnacted(),
- inhabitantsModel.listInhabitants({ filter: 'all', includeInactive: true })
- ]);
- const inhabitantsTotal = Array.isArray(inhabitantsAll) ? inhabitantsAll.length : 0;
- const governmentCard = governmentCardRaw ? { ...governmentCardRaw, inhabitantsTotal } : null;
- const leader = pickLeader(candidatures || []);
- const getActorMeta = async (type, id) => (type === 'tribe' || type === 'inhabitant') ? parliamentModel.getActorMeta({ targetType: type, targetId: id }) : null;
- const leaderMeta = leader ? await getActorMeta(leader.targetType || leader.powerType || 'inhabitant', leader.targetId || leader.powerId) : null;
- const powerMeta = governmentCard ? await getActorMeta(governmentCard.powerType, governmentCard.powerId) : null;
- const buildMetas = async (items, limit) => {
- const m = {};
- for (const g of (items || []).slice(0, limit)) {
- if (g.powerType === 'tribe' || g.powerType === 'inhabitant') {
- const k = `${g.powerType}:${g.powerId}`;
- if (!m[k]) m[k] = await getActorMeta(g.powerType, g.powerId);
- }
- }
- return m;
- };
- const [historicalMetas, leadersMetas] = await Promise.all([buildMetas(historical, 12), buildMetas(leaders, 20)]);
- ctx.body = await parliamentView({
- filter,
- inhabitantsTotal,
- governmentCard,
- candidatures,
- proposals,
- futureLaws,
- canPropose,
- laws,
- historical,
- leaders,
- leaderMeta,
- powerMeta,
- historicalMetas,
- leadersMetas,
- revocations,
- futureRevocations,
- revocationsEnactedCount
- });
- })
- .get('/courts', async (ctx) => {
- if (!checkMod(ctx, 'courtsMod')) return ctx.redirect('/modules');
- const filter = String(ctx.query.filter || 'cases').toLowerCase(), search = String(ctx.query.search || '').trim();
- const currentUserId = await courtsModel.getCurrentUserId();
- const state = { filter, search, cases: [], myCases: [], trials: [], history: [], nominations: [], userId: currentUserId };
- const searchFilter = (items) => !search ? items : items.filter(c => [c.title, c.description].some(s => String(s || '').toLowerCase().includes(search.toLowerCase())));
- if (filter === 'cases') state.cases = searchFilter((await courtsModel.listCases('open')).map(c => ({ ...c, respondent: c.respondentId || c.respondent })));
- if (filter === 'mycases' || filter === 'actions') {
- let myCases = searchFilter(await courtsModel.listCasesForUser(currentUserId));
- if (filter === 'actions') myCases = myCases.filter(c => {
- const s = String(c.status || '').toUpperCase(), m = String(c.method || '').toUpperCase(), id = String(currentUserId || '');
- const roles = { a: !!c.isAccuser, r: !!c.isRespondent, m: !!c.isMediator, j: !!c.isJudge, d: !!c.isDictator };
- const open = s === 'OPEN' || s === 'IN_PROGRESS';
- return (roles.r && open) || (m === 'JUDGE' && !c.judgeId && (roles.a || roles.r) && open) || ((roles.j || roles.d || roles.m) && s === 'OPEN') || ((roles.a || roles.r || roles.m) && m === 'MEDIATION' && open) || ((roles.a || roles.r || roles.m || roles.j || roles.d) && open);
- });
- state.myCases = myCases;
- }
- if (filter === 'judges') state.nominations = (await courtsModel.listNominations()) || [];
- if (filter === 'history') {
- const id = String(currentUserId || '');
- state.history = searchFilter((await courtsModel.listCases('history')).map(c => {
- const ma = Array.isArray(c.mediatorsAccuser) ? c.mediatorsAccuser : [], mr = Array.isArray(c.mediatorsRespondent) ? c.mediatorsRespondent : [];
- return { ...c, respondent: c.respondentId || c.respondent, mine: [c.accuser, c.respondentId, c.judgeId].map(String).includes(id) || ma.includes(id) || mr.includes(id), publicDetails: c.publicPrefAccuser && c.publicPrefRespondent, decidedAt: c.verdictAt || c.closedAt || c.decidedAt };
- }));
- }
- ctx.body = await courtsView(state);
- })
- .get('/courts/cases/:id', async (ctx) => {
- if (!checkMod(ctx, 'courtsMod')) return ctx.redirect('/modules');
- ctx.body = await courtsCaseView({ caseData: await courtsModel.getCaseDetails({ caseId: ctx.params.id }).catch(() => null) });
- })
- .get('/tribes', async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- const filter = qf(ctx), search = ctx.query.search || '', tribes = await tribesModel.listAll();
- const filteredTribes = search ? tribes.filter(t => t.title.toLowerCase().includes(search.toLowerCase())) : tribes;
- ctx.body = await tribesView(filteredTribes, filter, null, ctx.query, tribes);
- })
- .get('/tribes/create', async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- ctx.body = await tribesView([], 'create', null)
- })
- .get('/tribes/edit/:id', async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- const tribe = await tribesModel.getTribeById(ctx.params.id)
- ctx.body = await tribesView([tribe], 'edit', ctx.params.id)
- })
- .get('/tribe/:tribeId', async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- const listByTribeAllChain = async (tribeId, contentType) => {
- const chainIds = await tribesModel.getChainIds(tribeId).catch(() => [tribeId]);
- const results = await Promise.all(chainIds.map(id => tribesContentModel.listByTribe(id, contentType).catch(() => [])));
- const seen = new Set();
- return results.flat().filter(item => { const k = item.id || item.key; if (seen.has(k)) return false; seen.add(k); return true; });
- };
- const tribe = await tribesModel.getTribeById(ctx.params.tribeId);
- const uid = getViewerId();
- const query = { feedFilter: 'TOP', ...ctx.query };
- if (!tribe.members.includes(uid)) {
- ctx.redirect('/tribes');
- return;
- }
- const section = ctx.query.section || 'activity';
- const contentTypeMap = { events: 'event', tasks: 'task', reports: 'report', votations: 'votation', market: 'market', jobs: 'job', projects: 'project', media: 'media' };
- const mediaSections = { 'media-audio': 'media', 'media-video': 'media', 'media-images': 'media', 'media-documents': 'media', 'media-bookmarks': 'media', 'images': 'media', 'audios': 'media', 'videos': 'media', 'documents': 'media', 'bookmarks': 'media' };
- let sectionData = null;
- if (section === 'inhabitants') {
- const allInhabitants = await inhabitantsModel.listInhabitants({ filter: 'all', includeInactive: true });
- sectionData = allInhabitants.filter(u => tribe.members.includes(u.id));
- } else if (section === 'feed') {
- sectionData = await listByTribeAllChain(tribe.id, 'feed').catch(() => []);
- } else if (section === 'forum') {
- const forums = await listByTribeAllChain(tribe.id, 'forum');
- const replies = await listByTribeAllChain(tribe.id, 'forum-reply');
- sectionData = [...forums, ...replies];
- } else if (section === 'subtribes') {
- sectionData = await tribesModel.listSubTribes(tribe.id);
- } else if (mediaSections[section]) {
- sectionData = await listByTribeAllChain(tribe.id, 'media');
- } else if (contentTypeMap[section]) {
- sectionData = await listByTribeAllChain(tribe.id, contentTypeMap[section]);
- } else if (section === 'activity') {
- const allContent = await listByTribeAllChain(tribe.id, null);
- const subTribes = await tribesModel.listSubTribes(tribe.id);
- const subContent = [];
- for (const st of subTribes) {
- const stItems = await listByTribeAllChain(st.id, null).catch(() => []);
- subContent.push(...stItems.map(item => ({ ...item, tribeName: st.title })));
- }
- const combined = [...allContent, ...subContent];
- const allInhabitants = await inhabitantsModel.listInhabitants({ filter: 'all', includeInactive: true });
- const allMembers = [...new Set([...tribe.members, ...subTribes.flatMap(st => st.members || [])])];
- const memberMap = new Map(allInhabitants.filter(u => allMembers.includes(u.id)).map(u => [u.id, u]));
- const activities = combined.map(item => ({ ...item, authorName: memberMap.get(item.author)?.name || item.author, timestamp: Date.parse(item.createdAt) || item._ts || 0 })).sort((a, b) => b.timestamp - a.timestamp);
- sectionData = { activities, memberMap };
- } else if (section === 'trending') {
- const allContent = await listByTribeAllChain(tribe.id, null);
- const period = ctx.query.period || 'all';
- let items = allContent.filter(i => i.contentType !== 'forum-reply' && i.contentType !== 'pixelia');
- if (period === 'day') items = items.filter(i => (Date.parse(i.createdAt) || i._ts || 0) >= Date.now() - 86400000);
- else if (period === 'week') items = items.filter(i => (Date.parse(i.createdAt) || i._ts || 0) >= Date.now() - 7 * 86400000);
- items.sort((a, b) => {
- const score = i => (i.refeeds || 0) + (Array.isArray(i.attendees) ? i.attendees.length : 0) + Object.values(i.votes || {}).reduce((s, arr) => s + (Array.isArray(arr) ? arr.length : 0), 0) + (Array.isArray(i.assignees) ? i.assignees.length : 0) + (Array.isArray(i.opinions_inhabitants) ? i.opinions_inhabitants.length : 0);
- return score(b) - score(a);
- });
- sectionData = { items, period };
- } else if (section === 'tags') {
- const allContent = await listByTribeAllChain(tribe.id, null);
- const tagMap = new Map();
- for (const item of allContent) {
- for (const tag of (item.tags || []).filter(Boolean)) {
- const lower = tag.toLowerCase().trim();
- if (!lower) continue;
- if (!tagMap.has(lower)) tagMap.set(lower, { tag: lower, count: 0, items: [] });
- const entry = tagMap.get(lower);
- entry.count++;
- entry.items.push(item);
- }
- }
- const selectedTag = (ctx.query.tag || '').toLowerCase().trim();
- sectionData = { tags: [...tagMap.values()].sort((a, b) => b.count - a.count), selectedTag, filteredItems: selectedTag && tagMap.has(selectedTag) ? tagMap.get(selectedTag).items : [] };
- } else if (section === 'search') {
- const sq = (ctx.query.q || '').trim().toLowerCase();
- let results = [];
- if (sq.length >= 2) {
- const allContent = await listByTribeAllChain(tribe.id, null);
- results = allContent.filter(item => (item.title || '').toLowerCase().includes(sq) || (item.description || '').toLowerCase().includes(sq) || (item.tags || []).join(' ').toLowerCase().includes(sq));
- }
- sectionData = { query: ctx.query.q || '', results };
- } else if (section === 'opinions') {
- const allContent = await listByTribeAllChain(tribe.id, null);
- const opinionated = allContent.filter(i => i.opinions && Object.keys(i.opinions).length > 0).sort((a, b) => {
- const sum = o => Object.values(o.opinions || {}).reduce((s, n) => s + n, 0);
- return sum(b) - sum(a);
- });
- sectionData = { items: allContent.filter(i => i.contentType !== 'forum-reply' && i.contentType !== 'pixelia'), opinionated };
- } else if (section === 'pixelia') {
- const pixels = await listByTribeAllChain(tribe.id, 'pixelia');
- const coordMap = new Map();
- for (const px of pixels) { const existing = coordMap.get(px.title); if (!existing || (Date.parse(px.createdAt) || 0) > (Date.parse(existing.createdAt) || 0)) coordMap.set(px.title, px); }
- sectionData = { pixels: [...coordMap.values()] };
- } else if (section === 'overview') {
- const events = await listByTribeAllChain(tribe.id, 'event').catch(() => []);
- const tasks = await listByTribeAllChain(tribe.id, 'task').catch(() => []);
- const feed = await listByTribeAllChain(tribe.id, 'feed').catch(() => []);
- sectionData = { events, tasks, feed };
- }
- const subTribes = await tribesModel.listSubTribes(tribe.id);
- tribe.subTribes = subTribes;
- if (tribe.parentTribeId) {
- try { tribe.parentTribe = await tribesModel.getTribeById(tribe.parentTribeId); } catch (_) {}
- }
- const resolveItemMentions = async (items) => {
- if (!Array.isArray(items)) return items;
- for (const item of items) {
- if (item.description) item.description = await resolveMentionText(item.description);
- }
- return items;
- };
- if (Array.isArray(sectionData)) {
- await resolveItemMentions(sectionData);
- } else if (sectionData && typeof sectionData === 'object') {
- if (sectionData.activities) await resolveItemMentions(sectionData.activities);
- if (sectionData.items) await resolveItemMentions(sectionData.items);
- if (sectionData.results) await resolveItemMentions(sectionData.results);
- if (sectionData.events) await resolveItemMentions(sectionData.events);
- if (sectionData.tasks) await resolveItemMentions(sectionData.tasks);
- if (sectionData.feed) await resolveItemMentions(sectionData.feed);
- }
- ctx.body = await tribeView(tribe, uid, query, section, sectionData);
- })
- .get('/activity', async ctx => {
- const filter = qf(ctx, 'recent'), userId = getViewerId();
- const q = String((ctx.query && ctx.query.q) || '');
- try { await bankingModel.ensureSelfAddressPublished(); } catch (_) {}
- try { await bankingModel.getUserEngagementScore(userId); } catch (_) {}
- const allActions = await activityModel.listFeed('all');
- ctx.body = activityView(allActions, filter, userId, q);
- })
- .get("/profile", async (ctx) => {
- const myFeedId = await meta.myFeedId(), gt = Number(ctx.request.query.gt || -1), lt = Number(ctx.request.query.lt || -1);
- if (lt > 0 && gt > 0 && gt >= lt) throw new Error("Given search range is empty");
- const [description, name, image, messages, firstPost, lastPost, ecoAddress, bankData] = await Promise.all([
- about.description(myFeedId), about.name(myFeedId), about.image(myFeedId), post.fromPublicFeed(myFeedId, gt, lt),
- post.firstBy(myFeedId), post.latestBy(myFeedId), bankingModel.getUserAddress(myFeedId), bankingModel.getBankingData(myFeedId)
- ]);
- const normTs = t => { const n = Number(t || 0); return !isFinite(n) || n <= 0 ? 0 : n < 1e12 ? n * 1000 : n; };
- const pickTs = obj => { if (!obj) return 0; const v = obj.value || obj; return normTs(v.timestamp || v.ts || v.time || v.meta?.timestamp || 0); };
- let lastActivityTs = Math.max(Array.isArray(messages) && messages.length ? Math.max(...messages.map(pickTs)) : 0, pickTs(lastPost), pickTs(firstPost));
- if (!lastActivityTs) {
- const pull = require("../server/node_modules/pull-stream"), ssb = await require("../client/gui")({ offline: require("../server/ssb_config").offline }).open();
- lastActivityTs = await new Promise(res => pull(ssb.createUserStream({ id: myFeedId, reverse: true }), pull.filter(m => m?.value?.content?.type !== "tombstone"), pull.take(1), pull.collect((err, arr) => res(!err && arr?.[0] ? normTs(arr[0].value?.timestamp || arr[0].timestamp) : 0))));
- }
- const days = lastActivityTs ? (Date.now() - lastActivityTs) / 86400000 : Infinity;
- ctx.body = await authorView({ feedId: myFeedId, messages: sanitizeMessages(messages), firstPost, lastPost, name, description, avatarUrl: getAvatarUrl(image), relationship: { me: true }, ecoAddress, karmaScore: bankData.karmaScore, lastActivityBucket: days < 14 ? "green" : days < 182.5 ? "orange" : "red" });
- })
- .get("/profile/edit", async (ctx) => {
- const myFeedId = await meta.myFeedId();
- ctx.body = await editProfileView({ name: await about.name(myFeedId), description: await about.description(myFeedId) });
- })
- .post("/profile/edit", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => {
- const imageFile = ctx.request.files?.image;
- const mime = imageFile?.mimetype || imageFile?.type || '';
- const isImage = mime.startsWith('image/');
- const imageData = isImage && imageFile?.filepath ? await promisesFs.readFile(imageFile.filepath).catch(() => undefined) : undefined;
- await post.publishProfileEdit({
- name: stripDangerousTags(String(ctx.request.body?.name || '')),
- description: stripDangerousTags(String(ctx.request.body?.description || '')),
- image: imageData
- });
- ctx.redirect("/profile");
- })
- .get("/publish/custom", async (ctx) => {
- ctx.body = await publishCustomView();
- })
- .get("/json/:message", async (ctx) => {
- if (config.public) {
- throw new Error(
- "Sorry, many actions are unavailable when Oasis is running in public mode. Please run Oasis in the default mode and try again."
- );
- }
- const { message } = ctx.params;
- ctx.type = "application/json";
- const json = async (message) => {
- const json = await meta.get(message);
- return JSON.stringify(json, null, 2);
- };
- ctx.body = await json(message);
- })
- .get("/blob/:blobId", serveBlob)
- .get("/image/:imageSize/:blobId", async (ctx) => {
- const { blobId, imageSize } = ctx.params;
- const size = Number(imageSize);
- const fallbackPixel = Buffer.from(
- "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
- "base64"
- );
- const fakeImage = () => {
- if (typeof sharp !== "function") {
- return Promise.resolve(fallbackPixel);
- }
- return sharp({
- create: {
- width: size,
- height: size,
- channels: 4,
- background: { r: 0, g: 0, b: 0, alpha: 0.5 },
- },
- }).png().toBuffer();
- };
- try {
- const buffer = await blob.getResolved({ blobId });
- if (!buffer) {
- ctx.set("Content-Type", "image/png");
- ctx.body = await fakeImage();
- return;
- }
- const fileType = await FileType.fromBuffer(buffer);
- const mimeType = fileType?.mime || "application/octet-stream";
- ctx.set("Content-Type", mimeType);
- if (typeof sharp === "function") {
- ctx.body = await sharp(buffer)
- .resize(size, size)
- .png()
- .toBuffer();
- } else {
- ctx.body = buffer;
- }
- } catch (err) {
- ctx.set("Content-Type", "image/png");
- ctx.body = await fakeImage();
- }
- })
- .get("/settings", async (ctx) => {
- const cfg = getConfig(), theme = ctx.cookies.get("theme") || "Dark-SNH";
- ctx.body = await settingsView({ theme, version: version.toString(), aiPrompt: cfg.ai?.prompt || "", pubWalletUrl: cfg.walletPub?.url || '', pubWalletUser: cfg.walletPub?.user || '', pubWalletPass: cfg.walletPub?.pass || '' });
- })
- .get("/peers", async (ctx) => {
- const { discoveredPeers, unknownPeers } = await meta.discovered();
- ctx.body = await peersView({ onlinePeers: await meta.onlinePeers(), discoveredPeers, unknownPeers });
- })
- .get("/invites", async (ctx) => {
- if (!checkMod(ctx, 'invitesMod')) return ctx.redirect('/modules');
- ctx.body = await invitesView({});
- })
- .get("/likes/:feed", async (ctx) => {
- const { feed } = ctx.params;
- ctx.body = await likesView({ messages: await post.likes({ feed }), feed, name: await about.name(feed) });
- })
- .get("/mentions", async (ctx) => {
- const { messages, myFeedId } = await post.mentionsMe();
- const tribeMentions = [];
- try {
- const allTribes = await tribesModel.listAll();
- const myTribes = allTribes.filter(t => t.members.includes(myFeedId));
- for (const t of myTribes) {
- const items = await tribesContentModel.listByTribe(t.id, null).catch(() => []);
- for (const item of items) {
- const text = (item.description || '') + ' ' + (item.title || '');
- if (text.includes(myFeedId) || text.includes(myFeedId.slice(1))) {
- tribeMentions.push({
- key: item.id,
- value: {
- author: item.author,
- timestamp: Date.parse(item.createdAt) || item._ts || Date.now(),
- content: {
- type: 'tribe-content',
- text: item.description || item.title || '',
- tribeId: t.id,
- tribeName: t.title,
- contentType: item.contentType,
- mentions: { _self: [{ link: myFeedId }] }
- }
- }
- });
- }
- }
- }
- } catch (_) {}
- const combined = [...(Array.isArray(messages) ? messages : []), ...tribeMentions];
- for (const msg of combined) {
- if (!msg.value) continue;
- const authorId = msg.value.author;
- if (authorId) {
- if (!msg.value.meta) msg.value.meta = {};
- if (!msg.value.meta.author) msg.value.meta.author = {};
- if (!msg.value.meta.author.name) {
- try { msg.value.meta.author.name = await about.name(authorId); } catch (_) {}
- }
- }
- }
- ctx.body = await mentionsView({ messages: combined, myFeedId });
- })
- .get('/opinions', async (ctx) => {
- const filter = qf(ctx, 'RECENT'), opinions = await opinionsModel.listOpinions(filter);
- ctx.body = await opinionsView(opinions, filter);
- })
- .get("/feed", async (ctx) => {
- const filter = String(ctx.query.filter || "ALL").toUpperCase();
- const q = typeof ctx.query.q === "string" ? ctx.query.q : "";
- const tag = typeof ctx.query.tag === "string" ? ctx.query.tag : "";
- const msg = typeof ctx.query.msg === "string" ? ctx.query.msg : "";
- const feeds = await feedModel.listFeeds({ filter, q, tag });
- ctx.body = feedView(feeds, { filter, q, tag, msg });
- })
- .get("/feed/create", async (ctx) => {
- const q = typeof ctx.query.q === "string" ? ctx.query.q : "";
- const tag = typeof ctx.query.tag === "string" ? ctx.query.tag : "";
- ctx.body = feedCreateView({ q, tag });
- })
- .get("/feed/:feedId", async (ctx) => {
- const feed = await feedModel.getFeedById(ctx.params.feedId);
- if (!feed) { ctx.redirect('/feed'); return; }
- const comments = await feedModel.getComments(ctx.params.feedId).catch(() => []);
- ctx.body = singleFeedView(feed, comments);
- })
- .get('/forum', async ctx => {
- if (!checkMod(ctx, 'forumMod')) { ctx.redirect('/modules'); return; }
- const filter = qf(ctx, 'recent'), forums = await forumModel.listAll(filter);
- ctx.body = await forumView(forums, filter);
- })
- .get('/forum/:forumId', async ctx => {
- const msg = await forumModel.getMessageById(ctx.params.forumId), isReply = Boolean(msg.root), forumId = isReply ? msg.root : ctx.params.forumId;
- ctx.body = await singleForumView(await forumModel.getForumById(forumId), await forumModel.getMessagesByForumId(forumId), ctx.query.filter, isReply ? ctx.params.forumId : null);
- })
- .get('/legacy', async (ctx) => {
- if (!checkMod(ctx, 'legacyMod')) return ctx.redirect('/modules');
- try { ctx.body = await legacyView(); } catch (error) { ctx.body = { error: error.message }; }
- })
- .get('/bookmarks', async (ctx) => {
- if (!checkMod(ctx, 'bookmarksMod')) return ctx.redirect('/modules');
- const filter = qf(ctx), q = ctx.query.q || '', sort = ctx.query.sort || 'recent', viewerId = getViewerId();
- const favs = await mediaFavorites.getFavoriteSet("bookmarks");
- let bookmarks = (await bookmarksModel.listAll({ viewerId, filter: filter === "favorites" ? "all" : filter, q, sort })).map(b => ({ ...b, isFavorite: favs.has(String(b.rootId || b.id)) }));
- if (filter === "favorites") bookmarks = bookmarks.filter(b => b.isFavorite);
- await enrichWithComments(bookmarks, 'rootId');
- ctx.body = await bookmarkView(bookmarks, filter, null, { q, sort });
- })
- .get("/bookmarks/edit/:id", async (ctx) => {
- if (!checkMod(ctx, 'bookmarksMod')) return ctx.redirect('/modules');
- const bookmark = await bookmarksModel.getBookmarkById(ctx.params.id, getViewerId()), favs = await mediaFavorites.getFavoritesSet("bookmarks");
- ctx.body = await bookmarkView([{ ...bookmark, isFav: favs.has(String(bookmark.rootId || bookmark.id)) }], "edit", bookmark.id, { returnTo: ctx.query.returnTo || "" });
- })
- .get('/bookmarks/:bookmarkId', async (ctx) => {
- if (!checkMod(ctx, 'bookmarksMod')) return ctx.redirect('/modules');
- const filter = qf(ctx), q = ctx.query.q || '', sort = ctx.query.sort || 'recent', favs = await mediaFavorites.getFavoriteSet("bookmarks");
- const bookmark = await bookmarksModel.getBookmarkById(ctx.params.bookmarkId), root = bookmark.rootId || bookmark.id, comments = await getVoteComments(root);
- ctx.body = await singleBookmarkView({ ...bookmark, commentCount: comments.length, isFavorite: favs.has(String(root)) }, filter, comments, { q, sort, returnTo: safeReturnTo(ctx, `/bookmarks?filter=${encodeURIComponent(filter)}`, ['/bookmarks']) });
- })
- .get('/tasks', async ctx => {
- const filter = qf(ctx), tasks = await enrichWithComments(await tasksModel.listAll());
- ctx.body = await taskView(tasks, filter, null, ctx.query.returnTo);
- })
- .get('/tasks/edit/:id', async ctx => {
- const id = ctx.params.id;
- const task = await tasksModel.getTaskById(id);
- ctx.body = await taskView(task, 'edit', id, ctx.query.returnTo);
- })
- .get('/tasks/:taskId', async ctx => {
- const { taskId } = ctx.params, filter = qf(ctx), task = await tasksModel.getTaskById(taskId);
- const comments = await getVoteComments(taskId);
- ctx.body = await singleTaskView(withCount(task, comments), filter, comments);
- })
- .get('/events', async (ctx) => {
- if (!checkMod(ctx, 'eventsMod')) { ctx.redirect('/modules'); return; }
- const filter = qf(ctx), events = await enrichWithComments(await eventsModel.listAll(null, filter));
- ctx.body = await eventView(events, filter, null, ctx.query.returnTo);
- })
- .get('/events/edit/:id', async (ctx) => {
- if (!checkMod(ctx, 'eventsMod')) { ctx.redirect('/modules'); return; }
- const eventId = ctx.params.id;
- const event = await eventsModel.getEventById(eventId);
- ctx.body = await eventView([event], 'edit', eventId, ctx.query.returnTo);
- })
- .get('/events/:eventId', async ctx => {
- const { eventId } = ctx.params, filter = qf(ctx), event = await eventsModel.getEventById(eventId);
- const comments = await getVoteComments(eventId);
- ctx.body = await singleEventView(withCount(event, comments), filter, comments);
- })
- .get('/votes', async ctx => {
- const filter = qf(ctx), voteList = await enrichWithComments(await votesModel.listAll(filter));
- ctx.body = await voteView(voteList, filter, null, [], filter);
- })
- .get('/votes/edit/:id', async ctx => {
- const id = ctx.params.id;
- const activeFilter = (ctx.query.filter || 'mine');
- const voteData = await votesModel.getVoteById(id);
- ctx.body = await voteView([voteData], 'edit', id, [], activeFilter);
- })
- .get('/votes/:voteId', async ctx => {
- const { voteId } = ctx.params, filter = qf(ctx), voteData = await votesModel.getVoteById(voteId);
- const comments = await getVoteComments(voteId);
- ctx.body = await voteView([withCount(voteData, comments)], 'detail', voteId, comments, filter);
- })
- .get("/market", async (ctx) => {
- if (!checkMod(ctx, 'marketMod')) { ctx.redirect('/modules'); return; }
- const filter = qf(ctx), q = ctx.query.q || "", minPrice = ctx.query.minPrice ?? "", maxPrice = ctx.query.maxPrice ?? "", sort = ctx.query.sort || "recent";
- let marketItems = await marketModel.listAllItems("all");
- await marketModel.checkAuctionItemsStatus(marketItems);
- marketItems = await marketModel.listAllItems("all");
- await enrichWithComments(marketItems);
- ctx.body = await marketView(marketItems, filter, null, { q, minPrice, maxPrice, sort });
- })
- .get("/market/edit/:id", async (ctx) => {
- if (!checkMod(ctx, 'marketMod')) { ctx.redirect('/modules'); return; }
- const id = ctx.params.id
- let marketItem = await marketModel.getItemById(id)
- if (!marketItem) ctx.throw(404, "Item not found")
- await marketModel.checkAuctionItemsStatus([marketItem])
- marketItem = await marketModel.getItemById(id)
- if (!marketItem) ctx.throw(404, "Item not found")
- ctx.body = await marketView([marketItem], "edit", marketItem, { q: "", minPrice: "", maxPrice: "", sort: "recent" })
- })
- .get("/market/:itemId", async (ctx) => {
- if (!checkMod(ctx, 'marketMod')) { ctx.redirect('/modules'); return; }
- const { itemId } = ctx.params, filter = qf(ctx), q = ctx.query.q || "", minPrice = ctx.query.minPrice ?? "", maxPrice = ctx.query.maxPrice ?? "", sort = ctx.query.sort || "recent";
- let item = await marketModel.getItemById(itemId)
- if (!item) ctx.throw(404, "Item not found")
- await marketModel.checkAuctionItemsStatus([item])
- item = await marketModel.getItemById(itemId)
- if (!item) ctx.throw(404, "Item not found")
- const comments = await getVoteComments(itemId)
- const returnTo = (() => {
- const params = []
- if (filter) params.push(`filter=${encodeURIComponent(filter)}`)
- if (q) params.push(`q=${encodeURIComponent(q)}`)
- if (minPrice !== "" && minPrice != null) params.push(`minPrice=${encodeURIComponent(String(minPrice))}`)
- if (maxPrice !== "" && maxPrice != null) params.push(`maxPrice=${encodeURIComponent(String(maxPrice))}`)
- if (sort) params.push(`sort=${encodeURIComponent(sort)}`)
- return `/market${params.length ? `?${params.join("&")}` : ""}`
- })()
- ctx.body = await singleMarketView(withCount(item, comments), filter, comments, { q, minPrice, maxPrice, sort, returnTo })
- })
- .get('/jobs', async (ctx) => {
- if (!checkMod(ctx, 'jobsMod')) { ctx.redirect('/modules'); return; }
- let filter = String(ctx.query.filter || 'ALL').toUpperCase()
- if (filter === 'FAVS' || filter === 'NEEDS') filter = 'ALL'
- const query = {
- search: ctx.query.search || '',
- minSalary: ctx.query.minSalary ?? '',
- maxSalary: ctx.query.maxSalary ?? '',
- sort: ctx.query.sort || 'recent'
- }
- if (filter === 'CREATE') {
- ctx.body = await jobsView([], 'CREATE', query)
- return
- }
- if (filter === 'CV') {
- query.location = ctx.query.location || ''
- query.language = ctx.query.language || ''
- query.skills = ctx.query.skills || ''
- const inhabitants = await inhabitantsModel.listInhabitants({
- filter: 'CVs',
- ...query
- })
- ctx.body = await jobsView(inhabitants, filter, query)
- return
- }
- const viewerId = getViewerId()
- const jobs = await jobsModel.listJobs(filter, viewerId, query)
- await enrichWithComments(jobs)
- ctx.body = await jobsView(jobs, filter, query)
- })
- .get('/jobs/edit/:id', async (ctx) => {
- if (!checkMod(ctx, 'jobsMod')) { ctx.redirect('/modules'); return; }
- const id = ctx.params.id
- const viewerId = getViewerId()
- const job = await jobsModel.getJobById(id, viewerId)
- ctx.body = await jobsView([job], 'EDIT', {})
- })
- .get('/jobs/:jobId', async (ctx) => {
- if (!checkMod(ctx, 'jobsMod')) { ctx.redirect('/modules'); return; }
- const jobId = ctx.params.jobId
- let filter = String(ctx.query.filter || 'ALL').toUpperCase()
- if (filter === 'FAVS' || filter === 'NEEDS') filter = 'ALL'
- const viewerId = getViewerId()
- const params = {
- search: ctx.query.search || '',
- minSalary: ctx.query.minSalary ?? '',
- maxSalary: ctx.query.maxSalary ?? '',
- sort: ctx.query.sort || 'recent',
- returnTo: safeReturnTo(ctx, `/jobs?filter=${encodeURIComponent(filter)}`, ['/jobs'])
- }
- const job = await jobsModel.getJobById(jobId, viewerId)
- const comments = await getVoteComments(jobId)
- ctx.body = await singleJobsView(withCount(job, comments), filter, comments, params)
- })
- .get("/projects", async (ctx) => {
- if (!checkMod(ctx, 'projectsMod')) { ctx.redirect('/modules'); return; }
- const filter = String(ctx.query.filter || "ALL").toUpperCase()
- if (filter === "CREATE") {
- ctx.body = await projectsView([], "CREATE")
- return
- }
- const modelFilter = filter === "BACKERS" ? "ALL" : filter
- const projects = await projectsModel.listProjects(modelFilter)
- await enrichWithComments(projects)
- ctx.body = await projectsView(projects, filter)
- })
- .get("/projects/edit/:id", async (ctx) => {
- if (!checkMod(ctx, 'projectsMod')) { ctx.redirect('/modules'); return; }
- const id = ctx.params.id
- const pr = await projectsModel.getProjectById(id)
- ctx.body = await projectsView([pr], "EDIT")
- })
- .get("/projects/:projectId", async (ctx) => {
- if (!checkMod(ctx, 'projectsMod')) { ctx.redirect('/modules'); return; }
- const projectId = ctx.params.projectId
- const filter = String(ctx.query.filter || "ALL").toUpperCase()
- const project = await projectsModel.getProjectById(projectId)
- const comments = await getVoteComments(projectId)
- ctx.body = await singleProjectView(withCount(project, comments), filter, comments)
- })
- .get("/banking", async (ctx) => {
- if (!checkMod(ctx, 'bankingMod')) { ctx.redirect('/modules'); return; }
- const userId = getViewerId();
- const query = ctx.query;
- const filter = (query.filter || 'overview').toLowerCase();
- const q = (query.q || '').trim();
- const msg = (query.msg || '').trim();
- await bankingModel.ensureSelfAddressPublished();
- const data = await bankingModel.listBanking(filter, userId);
- if (filter === 'addresses' && q) {
- data.addresses = (data.addresses || []).filter(x =>
- String(x.id).toLowerCase().includes(q.toLowerCase()) ||
- String(x.address).toLowerCase().includes(q.toLowerCase())
- );
- data.search = q;
- }
- data.flash = msg || '';
- const { ecoValue, inflationFactor, ecoInHours, currentSupply, isSynced } = await bankingModel.calculateEcoinValue();
- data.exchange = {
- ecoValue: ecoValue,
- inflationFactor,
- ecoInHours,
- currentSupply: currentSupply,
- totalSupply: 25500000,
- isSynced: isSynced
- };
- ctx.body = renderBankingView(data, filter, userId);
- })
- .get("/banking/allocation/:id", async (ctx) => {
- const userId = getViewerId();
- const allocation = await bankingModel.getAllocationById(ctx.params.id);
- ctx.body = renderSingleAllocationView(allocation, userId);
- })
- .get("/banking/epoch/:id", async (ctx) => {
- const epoch = await bankingModel.getEpochById(ctx.params.id);
- const allocations = await bankingModel.listEpochAllocations(ctx.params.id);
- ctx.body = renderEpochView(epoch, allocations);
- })
- .get("/favorites", async (ctx) => {
- const filter = qf(ctx), data = await favoritesModel.listAll({ filter });
- ctx.body = await favoritesView(data.items, filter, data.counts);
- })
- .get('/cipher', async (ctx) => {
- if (!checkMod(ctx, 'cipherMod')) { ctx.redirect('/modules'); return; }
- try {
- ctx.body = await cipherView();
- } catch (error) {
- ctx.body = { error: error.message };
- }
- })
- .get("/thread/:message", async (ctx) => {
- const { message } = ctx.params;
- const thread = async (message) => {
- const messages = await post.fromThread(message);
- return threadView({ messages });
- };
- ctx.body = await thread(message);
- })
- .get("/subtopic/:message", async (ctx) => {
- const { message } = ctx.params;
- const rootMessage = await post.get(message);
- const myFeedId = await meta.myFeedId();
- debug("%O", rootMessage);
- const messages = [rootMessage];
- ctx.body = await subtopicView({ messages, myFeedId });
- })
- .get("/publish", async (ctx) => {
- ctx.body = await publishView();
- })
- .get("/comment/:message", async (ctx) => {
- const { messages, myFeedId, parentMessage } =
- await resolveCommentComponents(ctx);
- ctx.body = await commentView({ messages, myFeedId, parentMessage });
- })
- .get("/wallet", async (ctx) => {
- const { url, user, pass } = getConfig().wallet;
- if (!checkMod(ctx, 'walletMod')) { ctx.redirect('/modules'); return; }
- try {
- const balance = await walletModel.getBalance(url, user, pass);
- const address = await walletModel.getAddress(url, user, pass);
- const userId = getViewerId();
- if (address && typeof address === "string") {
- const map = readAddrMap();
- const was = map[userId];
- if (was !== address) {
- map[userId] = address;
- writeAddrMap(map);
- try { await publishActivity({ type: 'bankWallet', address }); } catch (e) {}
- }
- }
- ctx.body = await walletView(balance, address);
- } catch (error) {
- ctx.body = await walletErrorView(error);
- }
- })
- .get("/wallet/history", async (ctx) => {
- const { url, user, pass } = getConfig().wallet;
- try {
- const balance = await walletModel.getBalance(url, user, pass);
- const transactions = await walletModel.listTransactions(url, user, pass);
- const address = await walletModel.getAddress(url, user, pass);
- const userId = getViewerId();
- if (address && typeof address === "string") {
- const map = readAddrMap();
- const was = map[userId];
- if (was !== address) {
- map[userId] = address;
- writeAddrMap(map);
- try { await publishActivity({ type: 'bankWallet', address }); } catch (e) {}
- }
- }
- ctx.body = await walletHistoryView(balance, transactions, address);
- } catch (error) {
- ctx.body = await walletErrorView(error);
- }
- })
- .get("/wallet/receive", async (ctx) => {
- const { url, user, pass } = getConfig().wallet;
- try {
- const balance = await walletModel.getBalance(url, user, pass);
- const address = await walletModel.getAddress(url, user, pass);
- const userId = getViewerId();
- if (address && typeof address === "string") {
- const map = readAddrMap();
- const was = map[userId];
- if (was !== address) {
- map[userId] = address;
- writeAddrMap(map);
- try { await publishActivity({ type: 'bankWallet', address }); } catch (e) {}
- }
- }
- ctx.body = await walletReceiveView(balance, address);
- } catch (error) {
- ctx.body = await walletErrorView(error);
- }
- })
- .get("/wallet/send", async (ctx) => {
- const { url, user, pass, fee } = getConfig().wallet;
- try {
- const balance = await walletModel.getBalance(url, user, pass);
- const address = await walletModel.getAddress(url, user, pass);
- const userId = getViewerId();
- if (address && typeof address === "string") {
- const map = readAddrMap();
- const was = map[userId];
- if (was !== address) {
- map[userId] = address;
- writeAddrMap(map);
- try { await publishActivity({ type: 'bankWallet', address }); } catch (e) {}
- }
- }
- ctx.body = await walletSendFormView(balance, null, null, fee, null, address);
- } catch (error) {
- ctx.body = await walletErrorView(error);
- }
- })
- .get('/transfers', async ctx => {
- if (!checkMod(ctx, 'transfersMod')) { ctx.redirect('/modules'); return; }
- let filter = ctx.query.filter || 'all'; if (filter === 'favs') filter = 'all';
- const list = await transfersModel.listAll(filter, getViewerId());
- ctx.body = await transferView(list, filter, null, { q: ctx.query.q || '', minAmount: ctx.query.minAmount ?? '', maxAmount: ctx.query.maxAmount ?? '', sort: ctx.query.sort || 'recent' });
- })
- .get('/transfers/edit/:id', async ctx => {
- if (!checkMod(ctx, 'transfersMod')) { ctx.redirect('/modules'); return; }
- const tr = await transfersModel.getTransferById(ctx.params.id, getViewerId());
- ctx.body = await transferView([tr], 'edit', ctx.params.id, {});
- })
- .get('/transfers/:transferId', async ctx => {
- if (!checkMod(ctx, 'transfersMod')) { ctx.redirect('/modules'); return; }
- let filter = ctx.query.filter || 'all'; if (filter === 'favs') filter = 'all';
- const transfer = await transfersModel.getTransferById(ctx.params.transferId, getViewerId());
- ctx.body = await singleTransferView(transfer, filter, { q: ctx.query.q || '', minAmount: ctx.query.minAmount ?? '', maxAmount: ctx.query.maxAmount ?? '', sort: ctx.query.sort || 'recent', returnTo: safeReturnTo(ctx, `/transfers?filter=${encodeURIComponent(filter)}`, ['/transfers']) });
- })
- .post('/ai', koaBody(), async (ctx) => {
- const { input } = ctx.request.body;
- if (!input) {
- ctx.status = 400;
- ctx.body = { error: 'No input provided' };
- return;
- }
- startAI();
- const i18nAll = require('../client/assets/translations/i18n');
- const lang = ctx.cookies.get('language') || getConfig().language || 'en';
- const translations = i18nAll[lang] || i18nAll['en'];
- const { setLanguage } = require('../views/main_views');
- setLanguage(lang);
- const historyPath = path.join(__dirname, '..', '..', 'src', 'configs', 'AI-history.json');
- let chatHistory = [];
- try {
- const fileData = fs.readFileSync(historyPath, 'utf-8');
- chatHistory = JSON.parse(fileData);
- } catch {
- chatHistory = [];
- }
- const config = getConfig();
- const userPrompt = config.ai?.prompt?.trim() || 'Provide an informative and precise response.';
- try {
- let aiResponse = '';
- let snippets = [];
- const trained = await getBestTrainedAnswer(input);
- if (trained && trained.answer) {
- aiResponse = trained.answer;
- snippets = Array.isArray(trained.ctx) ? trained.ctx : [];
- } else {
- const response = await axios.post('http://localhost:4001/ai', { input });
- aiResponse = response.data.answer;
- snippets = Array.isArray(response.data.snippets) ? response.data.snippets : [];
- }
- chatHistory.unshift({
- prompt: userPrompt,
- question: input,
- answer: aiResponse,
- timestamp: Date.now(),
- trainStatus: 'pending',
- snippets
- });
- } catch (e) {
- chatHistory.unshift({
- prompt: userPrompt,
- question: input,
- answer: translations.aiServerError || 'The AI could not answer. Please try again.',
- timestamp: Date.now(),
- trainStatus: 'rejected',
- snippets: []
- });
- }
- chatHistory = chatHistory.slice(0, 20);
- fs.writeFileSync(historyPath, JSON.stringify(chatHistory, null, 2), 'utf-8');
- ctx.body = aiView(chatHistory, userPrompt);
- })
- .post('/ai/approve', koaBody(), async (ctx) => {
- const ts = String(ctx.request.body.ts || '');
- const custom = String(ctx.request.body.custom || '').trim();
- const historyPath = path.join(__dirname, '..', '..', 'src', 'configs', 'AI-history.json');
- let chatHistory = [];
- try {
- const fileData = fs.readFileSync(historyPath, 'utf-8');
- chatHistory = JSON.parse(fileData);
- } catch {
- chatHistory = [];
- }
- const item = chatHistory.find(e => String(e.timestamp) === ts);
- if (item) {
- try {
- if (custom) item.answer = stripDangerousTags(custom);
- item.type = 'aiExchange';
- let snippets = fieldsForSnippet('aiExchange', item);
- if (snippets.length === 0) {
- const context = await buildContext();
- snippets = [context];
- } else {
- snippets = snippets.map(snippet => clip(snippet, 200));
- }
- await publishExchange({
- q: item.question,
- a: item.answer,
- ctx: snippets,
- tokens: {}
- });
- item.trainStatus = 'approved';
- } catch {
- item.trainStatus = 'failed';
- }
- fs.writeFileSync(historyPath, JSON.stringify(chatHistory, null, 2), 'utf-8');
- }
- const config = getConfig();
- const userPrompt = config.ai?.prompt?.trim() || '';
- ctx.body = aiView(chatHistory, userPrompt);
- })
- .post('/ai/reject', koaBody(), async (ctx) => {
- const i18nAll = require('../client/assets/translations/i18n');
- const lang = ctx.cookies.get('language') || getConfig().language || 'en';
- const { setLanguage } = require('../views/main_views');
- setLanguage(lang);
- const ts = String(ctx.request.body.ts || '');
- const historyPath = path.join(__dirname, '..', '..', 'src', 'configs', 'AI-history.json');
- let chatHistory = [];
- try {
- const fileData = fs.readFileSync(historyPath, 'utf-8');
- chatHistory = JSON.parse(fileData);
- } catch {
- chatHistory = [];
- }
- const item = chatHistory.find(e => String(e.timestamp) === ts);
- if (item) {
- item.trainStatus = 'rejected';
- fs.writeFileSync(historyPath, JSON.stringify(chatHistory, null, 2), 'utf-8');
- }
- const config = getConfig();
- const userPrompt = config.ai?.prompt?.trim() || '';
- ctx.body = aiView(chatHistory, userPrompt);
- })
- .post('/ai/clear', async (ctx) => {
- const i18nAll = require('../client/assets/translations/i18n');
- const lang = ctx.cookies.get('language') || getConfig().language || 'en';
- const { setLanguage } = require('../views/main_views');
- setLanguage(lang);
- const historyPath = path.join(__dirname, '..', '..', 'src', 'configs', 'AI-history.json');
- fs.writeFileSync(historyPath, '[]', 'utf-8');
- const config = getConfig();
- const userPrompt = config.ai?.prompt?.trim() || '';
- ctx.body = aiView([], userPrompt);
- })
- .post('/pixelia/paint', koaBody(), async (ctx) => {
- const x = Number(ctx.request.body.x), y = Number(ctx.request.body.y), color = ctx.request.body.color;
- if (!Number.isFinite(x) || !Number.isFinite(y) || x < 1 || x > 50 || y < 1 || y > 200) {
- const errorMessage = 'Coordinates are wrong!';
- const pixelArt = await pixeliaModel.listPixels();
- ctx.body = pixeliaView(pixelArt, errorMessage);
- return;
- }
- await pixeliaModel.paintPixel(x, y, color);
- ctx.redirect('/pixelia');
- })
- .post('/pm', koaBody(), async ctx => {
- const { recipients, subject, text } = ctx.request.body;
- const recipientsArr = (recipients || '').split(',').map(s => s.trim()).filter(Boolean);
- await pmModel.sendMessage(recipientsArr, subject, text);
- await refreshInboxCount();
- ctx.redirect('/inbox?filter=sent');
- })
- .post('/pm/preview', koaBody(), async ctx => {
- const { recipients = '', subject = '', text = '' } = ctx.request.body;
- ctx.body = await pmView(recipients, subject, text, true);
- })
- .post('/inbox/delete/:id', koaBody(), async ctx => {
- await pmModel.deleteMessageById(ctx.params.id);
- await refreshInboxCount();
- ctx.redirect('/inbox');
- })
- .post("/search", koaBody(), async (ctx) => {
- const b = ctx.request.body, query = b.query || "";
- let types = b.type || [];
- if (typeof types === "string") types = [types];
- if (!Array.isArray(types)) types = [];
- if (!query) return ctx.body = await searchView({ messages: [], query, types });
- const results = await searchModel.search({ query, types });
- ctx.body = await searchView({ results: Object.entries(results).reduce((acc, [type, msgs]) => {
- acc[type] = msgs.map(msg => (!msg.value?.content) ? {} : { ...msg, content: msg.value.content, author: msg.value.content.author || 'Unknown' });
- return acc;
- }, {}), query, types });
- })
- .post("/subtopic/preview/:message",
- koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }),
- async (ctx) => {
- const { message } = ctx.params;
- const rootMessage = await post.get(message);
- const myFeedId = await meta.myFeedId();
- const rawContentWarning = stripDangerousTags(String(ctx.request.body.contentWarning).trim());
- const contentWarning =
- rawContentWarning.length > 0 ? rawContentWarning : undefined;
- const messages = [rootMessage];
- const previewData = await preparePreview(ctx);
- ctx.body = await previewSubtopicView({
- messages,
- myFeedId,
- previewData,
- contentWarning,
- });
- }
- )
- .post("/subtopic/:message", koaBody(), async (ctx) => {
- const { message } = ctx.params;
- const text = stripDangerousTags(String(ctx.request.body.text));
- const rawContentWarning = stripDangerousTags(String(ctx.request.body.contentWarning).trim());
- const contentWarning =
- rawContentWarning.length > 0 ? rawContentWarning : undefined;
- const publishSubtopic = async ({ message, text }) => {
- const mentions = extractMentions(text);
- const parent = await post.get(message);
- return post.subtopic({
- parent,
- message: { text, mentions, contentWarning },
- });
- };
- ctx.body = await publishSubtopic({ message, text });
- ctx.redirect(`/thread/${encodeURIComponent(message)}`);
- })
- .post("/comment/preview/:message", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => {
- const { messages, contentWarning, myFeedId, parentMessage } = await resolveCommentComponents(ctx);
- const previewData = await preparePreview(ctx);
- ctx.body = await previewCommentView({
- messages,
- myFeedId,
- contentWarning,
- previewData,
- parentMessage,
- });
- })
- .post("/comment/:message", koaBody(), async (ctx) => {
- let decodedMessage;
- try {
- decodedMessage = decodeURIComponent(ctx.params.message);
- } catch {
- decodedMessage = ctx.params.message;
- }
- const text = stripDangerousTags(String(ctx.request.body.text));
- const rawContentWarning = stripDangerousTags(String(ctx.request.body.contentWarning));
- const contentWarning =
- rawContentWarning.length > 0 ? rawContentWarning : undefined;
- let mentions = extractMentions(text);
- if (!Array.isArray(mentions)) mentions = [];
- const parent = await meta.get(decodedMessage);
- ctx.body = await post.comment({
- parent,
- message: {
- text,
- mentions,
- contentWarning
- },
- });
- ctx.redirect(`/thread/${encodeURIComponent(parent.key)}`);
- })
- .post("/publish/preview", koaBody({multipart: true, formidable: { multiples: false, maxFileSize: maxSize }, urlencoded: true }), async (ctx) => {
- const cw = stripDangerousTags(ctx.request.body.contentWarning?.toString().trim() || "");
- ctx.body = await previewView({ previewData: await preparePreview(ctx), contentWarning: cw.length > 0 ? cw : undefined });
- })
- .post("/publish", koaBody({ multipart: true, urlencoded: true, formidable: { multiples: false, maxFileSize: maxSize } }), async (ctx) => {
- const b = ctx.request.body, text = stripDangerousTags(b.text?.toString().trim() || ""), cw = stripDangerousTags(b.contentWarning?.toString().trim() || "");
- let mentions = [];
- try { mentions = JSON.parse(b.mentions || "[]"); } catch { mentions = await extractMentions(text); }
- await post.root({ text, mentions, contentWarning: cw.length > 0 ? cw : undefined });
- ctx.redirect("/public/latest");
- })
- .post("/publish/custom", koaBody(), async (ctx) => {
- const text = String(ctx.request.body.text);
- const obj = JSON.parse(text);
- ctx.body = await post.publishCustom(obj);
- ctx.redirect(`/public/latest`);
- })
- .post("/follow/:feed", koaBody(), async (ctx) => {
- ctx.body = await friend.follow(ctx.params.feed);
- ctx.redirect(new URL(ctx.request.header.referer).href);
- })
- .post("/unfollow/:feed", koaBody(), async (ctx) => {
- ctx.body = await friend.unfollow(ctx.params.feed);
- ctx.redirect(new URL(ctx.request.header.referer).href);
- })
- .post("/block/:feed", koaBody(), async (ctx) => {
- ctx.body = await friend.block(ctx.params.feed);
- ctx.redirect(new URL(ctx.request.header.referer).href);
- })
- .post("/unblock/:feed", koaBody(), async (ctx) => {
- ctx.body = await friend.unblock(ctx.params.feed);
- ctx.redirect(new URL(ctx.request.header.referer).href);
- })
- .post("/like/:message", koaBody(), async (ctx) => {
- const { message } = ctx.params, voteValue = Number(ctx.request.body.voteValue);
- const referer = new URL(ctx.request.header.referer);
- referer.hash = `centered-footer-${encodeURIComponent(message)}`;
- const msgData = await post.get(message);
- const isPrivate = msgData.value.meta.private === true;
- const normalized = (isPrivate ? msgData.value.content.recps : []).map(r => typeof r === 'string' ? r : r?.link).filter(Boolean);
- ctx.body = await vote.publish({ messageKey: message, value: voteValue, recps: normalized.length ? normalized : undefined });
- ctx.redirect(referer.href);
- })
- .post('/forum/create', koaBody(), async ctx => {
- const { category, title, text } = ctx.request.body;
- await forumModel.createForum(category, stripDangerousTags(title), stripDangerousTags(text));
- ctx.redirect('/forum');
- })
- .post('/forum/:id/message', koaBody(), async ctx => {
- const { message, parentId } = ctx.request.body;
- const cleanedMsg = stripDangerousTags(message);
- const mentions = await extractMentions(cleanedMsg);
- await forumModel.addMessageToForum(ctx.params.id, { text: cleanedMsg, author: getViewerId(), timestamp: new Date().toISOString(), mentions: mentions.length > 0 ? mentions : undefined }, parentId);
- ctx.redirect(`/forum/${encodeURIComponent(ctx.params.id)}`);
- })
- .post('/forum/:forumId/vote', koaBody(), async ctx => {
- await forumModel.voteContent(ctx.request.body.target, parseInt(ctx.request.body.value, 10));
- ctx.redirect(ctx.get('referer') || `/forum/${encodeURIComponent(ctx.params.forumId)}`);
- })
- .post('/forum/delete/:id', koaBody(), async ctx => {
- await forumModel.deleteForumById(ctx.params.id);
- ctx.redirect('/forum');
- })
- .post('/legacy/export', koaBody(), async (ctx) => {
- const pw = ctx.request.body.password;
- if (!pw || pw.length < 32) return ctx.redirect('/legacy');
- try {
- ctx.body = { message: 'Data exported successfully!', file: await legacyModel.exportData({ password: pw }) };
- ctx.redirect('/legacy');
- } catch (error) { ctx.status = 500; ctx.body = { error: `Error: ${error.message}` }; ctx.redirect('/legacy'); }
- })
- .post('/legacy/import', koaBody({
- multipart: true,
- formidable: {
- keepExtensions: true,
- uploadDir: '/tmp',
- }
- }), async (ctx) => {
- const uploadedFile = ctx.request.files?.uploadedFile, pw = ctx.request.body.importPassword;
- if (!uploadedFile) { ctx.body = { error: 'No file uploaded' }; return ctx.redirect('/legacy'); }
- if (!pw || pw.length < 32) { ctx.body = { error: 'Password is too short or missing.' }; return ctx.redirect('/legacy'); }
- try {
- await legacyModel.importData({ filePath: uploadedFile.filepath, password: pw });
- ctx.body = { message: 'Data imported successfully!' };
- ctx.redirect('/legacy');
- } catch (error) { ctx.body = { error: error.message }; ctx.redirect('/legacy'); }
- })
- .post('/trending/:contentId/:category', async (ctx) => {
- const { contentId, category } = ctx.params, voterId = SSBconfig?.keys?.id;
- if ((await trendingModel.getMessageById(contentId))?.content?.opinions_inhabitants?.includes(voterId)) {
- ctx.flash = { message: 'You have already opined.' }; return ctx.redirect('/trending');
- }
- await trendingModel.createVote(contentId, category); ctx.redirect('/trending');
- })
- .post('/opinions/:contentId/:category', async (ctx) => {
- const { contentId, category } = ctx.params, voterId = SSBconfig?.keys?.id;
- if ((await opinionsModel.getMessageById(contentId))?.content?.opinions_inhabitants?.includes(voterId)) {
- ctx.flash = { message: 'You have already opined.' }; return ctx.redirect('/opinions');
- }
- await opinionsModel.createVote(contentId, category); ctx.redirect('/opinions');
- })
- .post('/agenda/discard/:itemId', async (ctx) => {
- await agendaModel.discardItem(ctx.params.itemId); ctx.redirect('/agenda');
- })
- .post('/agenda/restore/:itemId', async (ctx) => {
- await agendaModel.restoreItem(ctx.params.itemId); ctx.redirect('/agenda?filter=discarded');
- })
- .post("/feed/create", koaBody(), async (ctx) => {
- const text = ctx.request.body?.text != null ? stripDangerousTags(String(ctx.request.body.text)) : "";
- const mentions = await extractMentions(text);
- await feedModel.createFeed(text, mentions);
- ctx.redirect("/feed?filter=ALL&msg=feedPublished");
- })
- .post("/feed/opinions/:feedId/:category", async (ctx) => {
- const { feedId, category } = ctx.params;
- try {
- await feedModel.addOpinion(feedId, category);
- } catch { /* already voted or invalid — ignore */ }
- ctx.redirect(ctx.get("Referer") || "/feed");
- })
- .post("/feed/refeed/:id", koaBody(), async (ctx) => {
- await feedModel.createRefeed(ctx.params.id);
- ctx.redirect(ctx.get("Referer") || "/feed");
- })
- .post("/feed/:feedId/comments", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => {
- const text = ctx.request.body?.text != null ? stripDangerousTags(String(ctx.request.body.text)) : "";
- const imageMarkdown = ctx.request.files?.blob ? await handleBlobUpload(ctx, 'blob') : null;
- const fullText = imageMarkdown ? (text ? text + '\n' : '') + imageMarkdown : text;
- await feedModel.addComment(ctx.params.feedId, fullText);
- ctx.redirect(`/feed/${encodeURIComponent(ctx.params.feedId)}`);
- })
- .post("/bookmarks/create", koaBody(), async (ctx) => {
- if (!checkMod(ctx, 'bookmarksMod')) { ctx.redirect('/modules'); return; }
- const b = ctx.request.body;
- await bookmarksModel.createBookmark(stripDangerousTags(b.url), b.tags, stripDangerousTags(b.description), b.category, b.lastVisit);
- ctx.redirect(safeReturnTo(ctx, '/bookmarks?filter=all', ['/bookmarks']));
- })
- .post("/bookmarks/update/:id", koaBody(), async (ctx) => {
- if (!checkMod(ctx, 'bookmarksMod')) { ctx.redirect('/modules'); return; }
- const b = ctx.request.body;
- await bookmarksModel.updateBookmarkById(ctx.params.id, { url: stripDangerousTags(b.url), tags: b.tags, description: stripDangerousTags(b.description), category: b.category, lastVisit: b.lastVisit });
- ctx.redirect(safeReturnTo(ctx, '/bookmarks?filter=mine', ['/bookmarks']));
- })
- .post("/bookmarks/delete/:id", koaBody(), async ctx => deleteAction(ctx, 'bookmarks'))
- .post("/bookmarks/opinions/:bookmarkId/:category", koaBody(), async ctx => opinionAction(ctx, 'bookmarks', 'bookmarkId'))
- .post("/bookmarks/favorites/add/:id", koaBody(), async ctx => favAction(ctx, 'bookmarks', 'add'))
- .post("/bookmarks/favorites/remove/:id", koaBody(), async ctx => favAction(ctx, 'bookmarks', 'remove'))
- .post("/bookmarks/:bookmarkId/comments", koaBodyMiddleware, async ctx => commentAction(ctx, 'bookmarks', 'bookmarkId'))
- .post("/images/create", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => {
- if (!checkMod(ctx, 'imagesMod')) { ctx.redirect('/modules'); return; }
- const blob = await handleBlobUpload(ctx, 'image'), b = ctx.request.body;
- await imagesModel.createImage(blob, b.tags, stripDangerousTags(b.title), stripDangerousTags(b.description), parseBool01(b.meme));
- ctx.redirect(safeReturnTo(ctx, '/images?filter=all', ['/images']));
- })
- .post("/images/update/:id", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => {
- if (!checkMod(ctx, 'imagesMod')) { ctx.redirect('/modules'); return; }
- const b = ctx.request.body, blob = ctx.request.files?.image ? await handleBlobUpload(ctx, 'image') : null;
- await imagesModel.updateImageById(ctx.params.id, blob, b.tags, stripDangerousTags(b.title), stripDangerousTags(b.description), parseBool01(b.meme));
- ctx.redirect(safeReturnTo(ctx, '/images?filter=mine', ['/images']));
- })
- .post("/images/delete/:id", koaBody(), async ctx => deleteAction(ctx, 'images'))
- .post("/images/opinions/:imageId/:category", koaBody(), async ctx => opinionAction(ctx, 'images', 'imageId'))
- .post("/images/favorites/add/:id", koaBody(), async ctx => favAction(ctx, 'images', 'add'))
- .post("/images/favorites/remove/:id", koaBody(), async ctx => favAction(ctx, 'images', 'remove'))
- .post("/images/:imageId/comments", koaBodyMiddleware, async ctx => commentAction(ctx, 'images', 'imageId'))
- .post("/audios/create", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async ctx => mediaCreateAction(ctx, 'audios'))
- .post("/audios/update/:id", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async ctx => mediaUpdateAction(ctx, 'audios'))
- .post("/audios/delete/:id", koaBody(), async ctx => deleteAction(ctx, 'audios'))
- .post("/audios/opinions/:audioId/:category", koaBody(), async ctx => opinionAction(ctx, 'audios', 'audioId'))
- .post("/audios/favorites/add/:id", koaBody(), async ctx => favAction(ctx, 'audios', 'add'))
- .post("/audios/favorites/remove/:id", koaBody(), async ctx => favAction(ctx, 'audios', 'remove'))
- .post("/audios/:audioId/comments", koaBodyMiddleware, async ctx => commentAction(ctx, 'audios', 'audioId'))
- .post("/videos/create", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async ctx => mediaCreateAction(ctx, 'videos'))
- .post("/videos/update/:id", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async ctx => mediaUpdateAction(ctx, 'videos'))
- .post("/videos/delete/:id", koaBody(), async ctx => deleteAction(ctx, 'videos'))
- .post("/videos/opinions/:videoId/:category", koaBody(), async ctx => opinionAction(ctx, 'videos', 'videoId'))
- .post("/videos/favorites/add/:id", koaBody(), async ctx => favAction(ctx, 'videos', 'add'))
- .post("/videos/favorites/remove/:id", koaBody(), async ctx => favAction(ctx, 'videos', 'remove'))
- .post("/videos/:videoId/comments", koaBodyMiddleware, async ctx => commentAction(ctx, 'videos', 'videoId'))
- .post("/documents/create", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => {
- const docBlob = await handleBlobUpload(ctx, "document"), b = ctx.request.body;
- await documentsModel.createDocument(docBlob, b.tags, stripDangerousTags(b.title), stripDangerousTags(b.description));
- ctx.redirect(safeReturnTo(ctx, "/documents?filter=all", ["/documents"]));
- })
- .post("/documents/update/:id", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => {
- const b = ctx.request.body, blob = ctx.request.files?.document ? await handleBlobUpload(ctx, "document") : null;
- await documentsModel.updateDocumentById(ctx.params.id, blob, b.tags, stripDangerousTags(b.title), stripDangerousTags(b.description));
- ctx.redirect(safeReturnTo(ctx, "/documents?filter=mine", ["/documents"]));
- })
- .post("/documents/delete/:id", koaBody(), async ctx => deleteAction(ctx, 'documents'))
- .post("/documents/opinions/:documentId/:category", koaBody(), async ctx => opinionAction(ctx, 'documents', 'documentId'))
- .post("/documents/favorites/add/:id", koaBody(), async ctx => favAction(ctx, 'documents', 'add'))
- .post("/documents/favorites/remove/:id", koaBody(), async ctx => favAction(ctx, 'documents', 'remove'))
- .post("/documents/:documentId/comments", koaBodyMiddleware, async ctx => commentAction(ctx, 'documents', 'documentId'))
- .post('/cv/upload', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async ctx => {
- const photoUrl = await handleBlobUpload(ctx, 'image')
- await cvModel.createCV(ctx.request.body, photoUrl)
- ctx.redirect('/cv')
- })
- .post('/cv/update/:id', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async ctx => {
- const photoUrl = await handleBlobUpload(ctx, 'image')
- await cvModel.updateCV(ctx.params.id, ctx.request.body, photoUrl)
- ctx.redirect('/cv')
- })
- .post('/cv/delete/:id', async ctx => {
- await cvModel.deleteCVById(ctx.params.id)
- ctx.redirect('/cv')
- })
- .post('/cipher/encrypt', koaBody(), async (ctx) => {
- const { text, password } = ctx.request.body;
- if (password.length < 32) { ctx.body = { error: 'Password is too short or missing.' }; return ctx.redirect('/cipher'); }
- const { encryptedText, iv } = cipherModel.encryptData(text, password);
- ctx.body = await cipherView(encryptedText, "", iv, password);
- })
- .post('/cipher/decrypt', koaBody(), async (ctx) => {
- const { encryptedText, password } = ctx.request.body;
- if (password.length < 32) { ctx.body = { error: 'Password is too short or missing.' }; return ctx.redirect('/cipher'); }
- ctx.body = await cipherView("", cipherModel.decryptData(encryptedText, password), "", password);
- })
- .post('/tribes/create', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- const b = ctx.request.body;
- if (tooLong(ctx, b.title, MAX_TITLE_LENGTH, 'Title') || tooLong(ctx, b.description, MAX_TEXT_LENGTH, 'Description')) return;
- if (!['strict', 'open'].includes(b.inviteMode)) { ctx.redirect('/tribes'); return; }
- const image = await handleBlobUpload(ctx, 'image');
- await tribesModel.createTribe(stripDangerousTags(b.title), stripDangerousTags(b.description), image, stripDangerousTags(b.location), b.tags, b.isLARP === 'true', b.isAnonymous === 'true', b.inviteMode);
- ctx.redirect('/tribes');
- })
- .post('/tribe/:id/subtribes/create', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- const parentTribe = await tribesModel.getTribeById(ctx.params.id);
- const viewerId = getViewerId();
- const canCreate = parentTribe.inviteMode === 'open'
- ? parentTribe.members.includes(viewerId)
- : parentTribe.author === viewerId;
- if (!canCreate) { ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=subtribes`); return; }
- const b = ctx.request.body;
- if (tooLong(ctx, b.title, MAX_TITLE_LENGTH, 'Title') || tooLong(ctx, b.description, MAX_TEXT_LENGTH, 'Description')) return;
- const image = await handleBlobUpload(ctx, 'image');
- await tribesModel.createTribe(stripDangerousTags(b.title), stripDangerousTags(b.description), image, stripDangerousTags(b.location), b.tags, b.isLARP === 'true', b.isAnonymous === 'true', b.inviteMode || 'open', ctx.params.id);
- ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=subtribes`);
- })
- .post('/tribes/update/:id', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- const tribe = await tribesModel.getTribeById(ctx.params.id);
- if (tribe.author !== getViewerId()) { ctx.status = 403; ctx.redirect('/tribes'); return; }
- const b = ctx.request.body;
- if (tooLong(ctx, b.title, MAX_TITLE_LENGTH, 'Title') || tooLong(ctx, b.description, MAX_TEXT_LENGTH, 'Description')) return;
- if (b.inviteMode && !['strict', 'open'].includes(b.inviteMode)) { ctx.redirect('/tribes'); return; }
- const tags = b.tags ? b.tags.split(',').map(t => t.trim()).filter(Boolean) : [];
- await tribesModel.updateTribeById(ctx.params.id, { title: stripDangerousTags(b.title), description: stripDangerousTags(b.description), image: await handleBlobUpload(ctx, 'image'), location: stripDangerousTags(b.location), tags, isLARP: b.isLARP === 'true', isAnonymous: b.isAnonymous === 'true', inviteMode: b.inviteMode || tribe.inviteMode, status: b.status || tribe.status || 'OPEN' });
- ctx.redirect('/tribes?filter=mine');
- })
- .post('/tribes/delete/:id', async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- const tribe = await tribesModel.getTribeById(ctx.params.id);
- if (tribe.author !== getViewerId()) { ctx.status = 403; ctx.redirect('/tribes'); return; }
- await tribesModel.deleteTribeById(ctx.params.id)
- ctx.redirect('/tribes?filter=mine')
- })
- .post('/tribes/generate-invite', koaBody(), async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- ctx.body = await renderInvitePage(await tribesModel.generateInvite(ctx.request.body.tribeId));
- })
- .post('/tribes/join-code', koaBody(), async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- await tribesModel.joinByInvite(ctx.request.body.inviteCode)
- ctx.redirect('/tribes?filter=membership')
- })
- .post('/tribes/leave/:id', koaBody(), async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- await tribesModel.leaveTribe(ctx.params.id)
- ctx.redirect('/tribes?filter=membership')
- })
- .post('/tribe/:id/message', koaBody(), async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- const tribe = await tribesModel.getTribeById(ctx.params.id);
- const uid = getViewerId();
- if (!tribe.members.includes(uid)) { ctx.status = 403; ctx.redirect('/tribes'); return; }
- if (tooLong(ctx, ctx.request.body.message, MAX_TEXT_LENGTH, 'Text')) return;
- const message = stripDangerousTags((ctx.request.body.message || '').trim());
- if (!message || message.length === 0 || message.length > 280) { ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=feed`); return; }
- await tribesContentModel.create(tribe.id, 'feed', { description: await resolveMentionText(message) });
- ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=feed&sent=1`);
- })
- .post('/tribe/:id/refeed/:msgId', koaBody(), async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- const tribe = await tribesModel.getTribeById(ctx.params.id);
- const uid = getViewerId();
- if (!tribe.members.includes(uid)) { ctx.status = 403; ctx.redirect('/tribes'); return; }
- await tribesContentModel.toggleRefeed(ctx.params.msgId);
- ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=feed`);
- })
- .post('/tribe/:id/events/create', koaBody(), async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- const tribe = await tribesModel.getTribeById(ctx.params.id);
- if (!tribe.members.includes(getViewerId())) { ctx.status = 403; ctx.redirect('/tribes'); return; }
- const b = ctx.request.body;
- if (tooLong(ctx, b.title, MAX_TITLE_LENGTH, 'Title') || tooLong(ctx, b.description, MAX_TEXT_LENGTH, 'Description')) return;
- if (b.date && b.date < new Date().toISOString().split('T')[0]) { ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=events&action=create`); return; }
- await tribesContentModel.create(tribe.id, 'event', { title: stripDangerousTags(b.title), description: await resolveMentionText(stripDangerousTags(b.description)), date: b.date, location: stripDangerousTags(b.location), attendees: [getViewerId()] });
- ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=events`);
- })
- .post('/tribe/:id/events/attend/:eventId', koaBody(), async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- const tribe = await tribesModel.getTribeById(ctx.params.id);
- if (!tribe.members.includes(getViewerId())) { ctx.status = 403; ctx.redirect('/tribes'); return; }
- await tribesContentModel.toggleAttendee(ctx.params.eventId);
- ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=events`);
- })
- .post('/tribe/:id/tasks/create', koaBody(), async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- const tribe = await tribesModel.getTribeById(ctx.params.id);
- if (!tribe.members.includes(getViewerId())) { ctx.status = 403; ctx.redirect('/tribes'); return; }
- const b = ctx.request.body;
- if (tooLong(ctx, b.title, MAX_TITLE_LENGTH, 'Title') || tooLong(ctx, b.description, MAX_TEXT_LENGTH, 'Description')) return;
- if (b.deadline && b.deadline < new Date().toISOString().split('T')[0]) { ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=tasks&action=create`); return; }
- await tribesContentModel.create(tribe.id, 'task', { title: stripDangerousTags(b.title), description: await resolveMentionText(stripDangerousTags(b.description)), priority: b.priority, deadline: b.deadline, assignees: [] });
- ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=tasks`);
- })
- .post('/tribe/:id/tasks/assign/:taskId', koaBody(), async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- const tribe = await tribesModel.getTribeById(ctx.params.id);
- if (!tribe.members.includes(getViewerId())) { ctx.status = 403; ctx.redirect('/tribes'); return; }
- await tribesContentModel.toggleAssignee(ctx.params.taskId);
- ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=tasks`);
- })
- .post('/tribe/:id/tasks/status/:taskId', koaBody(), async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- const tribe = await tribesModel.getTribeById(ctx.params.id);
- if (!tribe.members.includes(getViewerId())) { ctx.status = 403; ctx.redirect('/tribes'); return; }
- const item = await tribesContentModel.getById(ctx.params.taskId);
- if (!item || item.author !== getViewerId()) { ctx.status = 403; ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=tasks`); return; }
- await tribesContentModel.updateStatus(ctx.params.taskId, ctx.request.body.status);
- ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=tasks`);
- })
- .post('/tribe/:id/votations/create', koaBody(), async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- const tribe = await tribesModel.getTribeById(ctx.params.id);
- if (!tribe.members.includes(getViewerId())) { ctx.status = 403; ctx.redirect('/tribes'); return; }
- const b = ctx.request.body;
- if (tooLong(ctx, b.title, MAX_TITLE_LENGTH, 'Title') || tooLong(ctx, b.description, MAX_TEXT_LENGTH, 'Description')) return;
- if (b.deadline && b.deadline < new Date().toISOString().split('T')[0]) { ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=votations&action=create`); return; }
- const options = [b.option1, b.option2, b.option3, b.option4].filter(Boolean).map(o => stripDangerousTags(o));
- await tribesContentModel.create(tribe.id, 'votation', { title: stripDangerousTags(b.title), description: await resolveMentionText(stripDangerousTags(b.description)), deadline: b.deadline, options, votes: {} });
- ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=votations`);
- })
- .post('/tribe/:id/votations/:voteId/vote', koaBody(), async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- const tribe = await tribesModel.getTribeById(ctx.params.id);
- if (!tribe.members.includes(getViewerId())) { ctx.status = 403; ctx.redirect('/tribes'); return; }
- await tribesContentModel.castVote(ctx.params.voteId, parseInt(ctx.request.body.optionIndex, 10));
- ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=votations`);
- })
- .post('/tribe/:id/votations/close/:voteId', koaBody(), async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- const tribe = await tribesModel.getTribeById(ctx.params.id);
- if (!tribe.members.includes(getViewerId())) { ctx.status = 403; ctx.redirect('/tribes'); return; }
- const votation = await tribesContentModel.getById(ctx.params.voteId);
- if (!votation || votation.author !== getViewerId()) { ctx.status = 403; ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=votations`); return; }
- await tribesContentModel.updateStatus(ctx.params.voteId, 'CLOSED');
- ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=votations`);
- })
- .post('/tribe/:id/forum/create', koaBody(), async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- const tribe = await tribesModel.getTribeById(ctx.params.id);
- if (!tribe.members.includes(getViewerId())) { ctx.status = 403; ctx.redirect('/tribes'); return; }
- const b = ctx.request.body;
- if (tooLong(ctx, b.title, MAX_TITLE_LENGTH, 'Title') || tooLong(ctx, b.description, MAX_TEXT_LENGTH, 'Description')) return;
- await tribesContentModel.create(tribe.id, 'forum', { title: stripDangerousTags(b.title), description: await resolveMentionText(stripDangerousTags(b.description)), category: b.category });
- ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=forum`);
- })
- .post('/tribe/:id/forum/:forumId/reply', koaBody(), async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- const tribe = await tribesModel.getTribeById(ctx.params.id);
- if (!tribe.members.includes(getViewerId())) { ctx.status = 403; ctx.redirect('/tribes'); return; }
- const b = ctx.request.body;
- if (tooLong(ctx, b.description, MAX_TEXT_LENGTH, 'Description')) return;
- await tribesContentModel.create(tribe.id, 'forum-reply', { description: await resolveMentionText(stripDangerousTags(b.description)), parentId: ctx.params.forumId });
- ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=forum&thread=${encodeURIComponent(ctx.params.forumId)}`);
- })
- .post('/tribe/:id/forum/:forumId/refeed', koaBody(), async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- const tribe = await tribesModel.getTribeById(ctx.params.id);
- const uid = getViewerId();
- if (!tribe.members.includes(uid)) { ctx.status = 403; ctx.redirect('/tribes'); return; }
- await tribesContentModel.toggleRefeed(ctx.params.forumId);
- const thread = ctx.query.thread || '';
- ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=forum${thread ? '&thread=' + encodeURIComponent(thread) : ''}`);
- })
- .post('/tribe/:id/media/upload', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- const tribe = await tribesModel.getTribeById(ctx.params.id);
- if (!tribe.members.includes(getViewerId())) { ctx.status = 403; ctx.redirect('/tribes'); return; }
- const b = ctx.request.body;
- if (tooLong(ctx, b.title, MAX_TITLE_LENGTH, 'Title') || tooLong(ctx, b.description, MAX_TEXT_LENGTH, 'Description')) return;
- const returnSection = b.returnSection || 'media';
- const mediaType = b.mediaType || 'image';
- let blobRef = null;
- if (mediaType === 'bookmark') {
- const url = stripDangerousTags(b.url || '');
- await tribesContentModel.create(tribe.id, 'media', { title: stripDangerousTags(b.title), description: stripDangerousTags(b.description), mediaType: 'bookmark', url });
- } else {
- const blobMarkdownMedia = await handleBlobUpload(ctx, 'media');
- blobRef = blobMarkdownMedia ? ((blobMarkdownMedia.match(/\((&[^)]+)\)/) || [])[1] || blobMarkdownMedia) : null;
- await tribesContentModel.create(tribe.id, 'media', { title: stripDangerousTags(b.title), description: stripDangerousTags(b.description), mediaType, image: blobRef });
- }
- ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=${returnSection}`);
- })
- .post('/tribe/:id/content/delete/:contentId', koaBody(), async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- const tribeRedirect = `/tribe/${encodeURIComponent(ctx.params.id)}`;
- const item = await tribesContentModel.getById(ctx.params.contentId);
- if (!item || item.author !== getViewerId() || item.tribeId !== ctx.params.id) { ctx.status = 403; ctx.redirect(tribeRedirect); return; }
- await tribesContentModel.deleteById(ctx.params.contentId);
- ctx.redirect(tribeRedirect);
- })
- .post('/tribe/:id/content/:contentId/opinion/:category', koaBody(), async ctx => {
- if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; }
- const tribe = await tribesModel.getTribeById(ctx.params.id);
- if (!tribe.members.includes(getViewerId())) { ctx.status = 403; ctx.redirect('/tribes'); return; }
- const item = await tribesContentModel.getById(ctx.params.contentId);
- if (!item || item.tribeId !== ctx.params.id) { ctx.status = 404; ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=opinions`); return; }
- try {
- await tribesContentModel.castOpinion(ctx.params.contentId, ctx.params.category);
- } catch (_) {}
- ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=opinions`);
- })
- .post('/panic/remove', koaBody(), async (ctx) => {
- const { exec } = require('child_process');
- try {
- await panicmodeModel.removeSSB();
- ctx.body = { message: 'Your blockchain has been succesfully deleted!' };
- exec('pkill -f "node SSB_server.js start"');
- setTimeout(() => process.exit(0), 1000);
- } catch (error) { ctx.body = { error: 'Error deleting your blockchain: ' + error.message }; }
- })
- .post('/export/create', async (ctx) => {
- try {
- const outputPath = path.join(os.homedir(), 'ssb_exported.zip');
- await exportmodeModel.exportSSB(outputPath);
- ctx.set('Content-Type', 'application/zip');
- ctx.set('Content-Disposition', `attachment; filename=ssb_exported.zip`);
- ctx.body = fs.createReadStream(outputPath);
- ctx.res.on('finish', () => fs.unlinkSync(outputPath));
- } catch (error) { ctx.body = { error: 'Error exporting your blockchain: ' + error.message }; }
- })
- .post('/tasks/create', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async ctx => {
- const b = ctx.request.body;
- const imageMarkdown = ctx.request.files?.image ? await handleBlobUpload(ctx, 'image') : null;
- let desc = stripDangerousTags(b.description);
- if (imageMarkdown) desc = (desc ? desc + '\n' : '') + imageMarkdown;
- await tasksModel.createTask(stripDangerousTags(b.title), desc, b.startTime, b.endTime, b.priority, stripDangerousTags(b.location), b.tags, b.isPublic);
- ctx.redirect(safeReturnTo(ctx, '/tasks?filter=mine', ['/tasks']));
- })
- .post('/tasks/update/:id', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async ctx => {
- const b = ctx.request.body, tags = Array.isArray(b.tags) ? b.tags.filter(Boolean) : (typeof b.tags === 'string' ? b.tags.split(',').map(t => t.trim()).filter(Boolean) : []);
- const imageMarkdown = ctx.request.files?.image ? await handleBlobUpload(ctx, 'image') : null;
- let desc = stripDangerousTags(b.description);
- if (imageMarkdown) desc = (desc ? desc + '\n' : '') + imageMarkdown;
- await tasksModel.updateTaskById(ctx.params.id, { title: stripDangerousTags(b.title), description: desc, startTime: b.startTime, endTime: b.endTime, priority: b.priority, location: stripDangerousTags(b.location), tags, isPublic: b.isPublic });
- ctx.redirect(safeReturnTo(ctx, '/tasks?filter=mine', ['/tasks']));
- })
- .post('/tasks/assign/:id', koaBody(), async ctx => {
- await tasksModel.toggleAssignee(ctx.params.id);
- ctx.redirect(safeReturnTo(ctx, '/tasks', ['/tasks']));
- })
- .post('/tasks/delete/:id', koaBody(), async ctx => {
- await tasksModel.deleteTaskById(ctx.params.id);
- ctx.redirect(safeReturnTo(ctx, '/tasks?filter=mine', ['/tasks']));
- })
- .post('/tasks/status/:id', koaBody(), async ctx => {
- await tasksModel.updateTaskStatus(ctx.params.id, ctx.request.body.status);
- ctx.redirect(safeReturnTo(ctx, '/tasks?filter=mine', ['/tasks']));
- })
- .post('/tasks/:taskId/comments', koaBodyMiddleware, async ctx => commentAction(ctx, 'tasks', 'taskId'))
- .post('/reports/create', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async ctx => {
- const b = ctx.request.body, image = await handleBlobUpload(ctx, 'image');
- await reportsModel.createReport(stripDangerousTags(b.title), stripDangerousTags(b.description), b.category, image, b.tags, b.severity, {
- stepsToReproduce: stripDangerousTags(b.stepsToReproduce), expectedBehavior: stripDangerousTags(b.expectedBehavior), actualBehavior: stripDangerousTags(b.actualBehavior), environment: stripDangerousTags(b.environment), reproduceRate: b.reproduceRate,
- problemStatement: stripDangerousTags(b.problemStatement), userStory: stripDangerousTags(b.userStory), acceptanceCriteria: stripDangerousTags(b.acceptanceCriteria),
- whatHappened: stripDangerousTags(b.whatHappened), reportedUser: b.reportedUser, evidenceLinks: stripDangerousTags(b.evidenceLinks),
- contentLocation: stripDangerousTags(b.contentLocation), whyInappropriate: stripDangerousTags(b.whyInappropriate), requestedAction: stripDangerousTags(b.requestedAction)
- });
- ctx.redirect('/reports');
- })
- .post('/reports/update/:id', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async ctx => {
- const b = ctx.request.body, image = await handleBlobUpload(ctx, 'image');
- await reportsModel.updateReportById(ctx.params.id, {
- title: b.title, description: b.description, category: b.category, image, tags: b.tags, severity: b.severity,
- template: {
- stepsToReproduce: b.stepsToReproduce, expectedBehavior: b.expectedBehavior, actualBehavior: b.actualBehavior, environment: b.environment, reproduceRate: b.reproduceRate,
- problemStatement: b.problemStatement, userStory: b.userStory, acceptanceCriteria: b.acceptanceCriteria,
- whatHappened: b.whatHappened, reportedUser: b.reportedUser, evidenceLinks: b.evidenceLinks,
- contentLocation: b.contentLocation, whyInappropriate: b.whyInappropriate, requestedAction: b.requestedAction
- }
- });
- ctx.redirect('/reports?filter=mine');
- })
- .post('/reports/delete/:id', async ctx => {
- await reportsModel.deleteReportById(ctx.params.id);
- ctx.redirect('/reports?filter=mine');
- })
- .post('/reports/confirm/:id', async ctx => {
- await reportsModel.confirmReportById(ctx.params.id);
- ctx.redirect('/reports');
- })
- .post('/reports/status/:id', koaBody(), async ctx => {
- await reportsModel.updateReportById(ctx.params.id, { status: ctx.request.body.status });
- ctx.redirect('/reports?filter=mine');
- })
- .post('/reports/:reportId/comments', koaBodyMiddleware, async ctx => commentAction(ctx, 'reports', 'reportId'))
- .post('/events/create', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => {
- const b = ctx.request.body;
- const imageMarkdown = ctx.request.files?.image ? await handleBlobUpload(ctx, 'image') : null;
- let desc = stripDangerousTags(b.description);
- if (imageMarkdown) desc = (desc ? desc + '\n' : '') + imageMarkdown;
- await eventsModel.createEvent(stripDangerousTags(b.title), desc, b.date, stripDangerousTags(b.location), b.price, b.url, b.attendees || [], b.tags, b.isPublic);
- ctx.redirect(safeReturnTo(ctx, '/events?filter=mine', ['/events']));
- })
- .post('/events/update/:id', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => {
- const b = ctx.request.body, existing = await eventsModel.getEventById(ctx.params.id);
- const imageMarkdown = ctx.request.files?.image ? await handleBlobUpload(ctx, 'image') : null;
- let desc = stripDangerousTags(b.description);
- if (imageMarkdown) desc = (desc ? desc + '\n' : '') + imageMarkdown;
- await eventsModel.updateEventById(ctx.params.id, { title: stripDangerousTags(b.title), description: desc, date: b.date, location: stripDangerousTags(b.location), price: b.price, url: b.url, attendees: b.attendees, tags: b.tags, isPublic: b.isPublic, createdAt: existing.createdAt, organizer: existing.organizer });
- ctx.redirect(safeReturnTo(ctx, '/events?filter=mine', ['/events']));
- })
- .post('/events/attend/:id', koaBody(), async ctx => {
- await eventsModel.toggleAttendee(ctx.params.id);
- ctx.redirect(safeReturnTo(ctx, '/events', ['/events']));
- })
- .post('/events/delete/:id', koaBody(), async ctx => {
- await eventsModel.deleteEventById(ctx.params.id);
- ctx.redirect(safeReturnTo(ctx, '/events?filter=mine', ['/events']));
- })
- .post('/events/:eventId/comments', koaBodyMiddleware, async ctx => commentAction(ctx, 'events', 'eventId'))
- .post('/votes/create', koaBody(), async ctx => {
- const b = ctx.request.body, defaultOptions = ['YES', 'NO', 'ABSTENTION', 'CONFUSED', 'FOLLOW_MAJORITY', 'NOT_INTERESTED'];
- const parsedOptions = b.options ? b.options.split(',').map(o => o.trim()).filter(Boolean) : defaultOptions;
- await votesModel.createVote(stripDangerousTags(b.question), b.deadline, parsedOptions, String(b.tags || '').split(',').map(t => t.trim()).filter(Boolean));
- ctx.redirect(safeReturnTo(ctx, '/votes?filter=mine', ['/votes']));
- })
- .post('/votes/update/:id', koaBody(), async ctx => {
- const b = ctx.request.body, parsedOptions = b.options ? b.options.split(',').map(o => o.trim()).filter(Boolean) : undefined;
- await votesModel.updateVoteById(ctx.params.id, { question: stripDangerousTags(b.question), deadline: b.deadline, options: parsedOptions, tags: b.tags ? b.tags.split(',').map(t => t.trim()).filter(Boolean) : [] });
- ctx.redirect(safeReturnTo(ctx, '/votes?filter=mine', ['/votes']));
- })
- .post('/votes/delete/:id', koaBody(), async ctx => {
- await votesModel.deleteVoteById(ctx.params.id);
- ctx.redirect(safeReturnTo(ctx, '/votes?filter=mine', ['/votes']));
- })
- .post('/votes/vote/:id', koaBody(), async ctx => {
- await votesModel.voteOnVote(ctx.params.id, ctx.request.body.choice);
- ctx.redirect(safeReturnTo(ctx, '/votes?filter=open', ['/votes']));
- })
- .post('/votes/opinions/:voteId/:category', koaBody(), async ctx => {
- try { await votesModel.createOpinion(ctx.params.voteId, ctx.params.category); }
- catch (e) { if (!/already/i.test(String(e?.message || ''))) throw e; ctx.flash = { message: "You have already opined." }; }
- ctx.redirect(safeReturnTo(ctx, '/votes', ['/votes']));
- })
- .post('/votes/:voteId/comments', koaBodyMiddleware, async ctx => commentAction(ctx, 'votes', 'voteId'))
- .post('/parliament/candidatures/propose', koaBody(), async (ctx) => {
- const b = ctx.request.body || {}, id = String(b.candidateId || '').trim(), m = String(b.method || '').trim().toUpperCase();
- if (!id) ctx.throw(400, 'Candidate is required.');
- if (!new Set(['DEMOCRACY','MAJORITY','MINORITY','DICTATORSHIP','KARMATOCRACY']).has(m)) ctx.throw(400, 'Invalid method.');
- await parliamentModel.proposeCandidature({ candidateId: id, method: m }).catch(e => ctx.throw(400, String(e?.message || e)));
- ctx.redirect('/parliament?filter=candidatures');
- })
- .post('/parliament/candidatures/:id/vote', koaBody(), async (ctx) => {
- await parliamentModel.voteCandidature(ctx.params.id).catch(e => ctx.throw(400, String(e?.message || e)));
- ctx.redirect('/parliament?filter=candidatures');
- })
- .post('/parliament/proposals/create', koaBody(), async (ctx) => {
- const b = ctx.request.body || {}, t = String(b.title || '').trim(), d = String(b.description || '').trim();
- if (!t) ctx.throw(400, 'Title is required.');
- if (d.length > 1000) ctx.throw(400, 'Description must be ≤ 1000 chars.');
- await parliamentModel.createProposal({ title: stripDangerousTags(t), description: stripDangerousTags(d) }).catch(e => ctx.throw(400, String(e?.message || e)));
- ctx.redirect('/parliament?filter=proposals');
- })
- .post('/parliament/proposals/close/:id', koaBody(), async (ctx) => {
- await parliamentModel.closeProposal(ctx.params.id).catch(e => ctx.throw(400, String(e?.message || e)));
- ctx.redirect('/parliament?filter=proposals');
- })
- .post('/parliament/resolve', koaBody(), async (ctx) => {
- await ensureTerm();
- ctx.redirect('/parliament?filter=government');
- })
- .post('/parliament/revocations/create', koaBody(), async (ctx) => {
- const b = ctx.request.body || {}, rawLawId = Array.isArray(b.lawId) ? b.lawId[0] : (b.lawId ?? b['lawId[]'] ?? b.law_id ?? '');
- const lawId = String(rawLawId || '').trim();
- if (!lawId) ctx.throw(400, 'Law required');
- await parliamentModel.createRevocation({ lawId, title: b.title, reasons: b.reasons });
- ctx.redirect('/parliament?filter=revocations');
- })
- .post('/courts/cases/create', koaBody(), async (ctx) => {
- const b = ctx.request.body || {}, titleSuffix = String(b.titleSuffix || '').trim(), titlePreset = String(b.titlePreset || '').trim();
- const respondent = String(b.respondentId || '').trim(), method = String(b.method || '').trim().toUpperCase();
- if (!titleSuffix && !titlePreset) { ctx.flash = { message: 'Title is required.' }; return ctx.redirect('/courts?filter=cases'); }
- if (!respondent) { ctx.flash = { message: 'Accused / Respondent is required.' }; return ctx.redirect('/courts?filter=cases'); }
- if (!/^@[A-Za-z0-9+/]+=*\.ed25519$/.test(respondent)) { ctx.flash = { message: 'Invalid respondent ID. Must be a valid SSB ID (@...ed25519).' }; return ctx.redirect('/courts?filter=cases'); }
- if (!new Set(['JUDGE','DICTATOR','POPULAR','MEDIATION','KARMATOCRACY']).has(method)) { ctx.flash = { message: 'Invalid resolution method.' }; return ctx.redirect('/courts?filter=cases'); }
- try { await courtsModel.openCase({ titleBase: [titlePreset, titleSuffix].filter(Boolean).join(' - '), respondentInput: respondent, method }); }
- catch (e) { ctx.flash = { message: String(e?.message || e) }; }
- ctx.redirect('/courts?filter=mycases');
- })
- .post('/courts/cases/:id/evidence/add', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => {
- const caseId = ctx.params.id, b = ctx.request.body || {};
- if (!caseId) { ctx.flash = { message: 'Case not found.' }; return ctx.redirect('/courts?filter=cases'); }
- try { await courtsModel.addEvidence({ caseId, text: stripDangerousTags(String(b.text || '')), link: String(b.link || ''), imageMarkdown: ctx.request.files?.image ? await handleBlobUpload(ctx, 'image') : null }); }
- catch (e) { ctx.flash = { message: String(e?.message || e) }; }
- ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
- })
- .post('/courts/cases/:id/answer', koaBody(), async (ctx) => {
- const caseId = ctx.params.id, b = ctx.request.body || {}, answer = String(b.answer || ''), stance = String(b.stance || '').toUpperCase();
- if (!caseId) { ctx.flash = { message: 'Case not found.' }; return ctx.redirect('/courts?filter=cases'); }
- if (!answer) { ctx.flash = { message: 'Response brief is required.' }; return ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`); }
- if (!new Set(['DENY','ADMIT','PARTIAL']).has(stance)) { ctx.flash = { message: 'Invalid stance.' }; return ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`); }
- try { await courtsModel.answerCase({ caseId, stance, text: stripDangerousTags(answer) }); } catch (e) { ctx.flash = { message: String(e?.message || e) }; }
- ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
- })
- .post('/courts/cases/:id/decide', koaBody(), async (ctx) => {
- const caseId = ctx.params.id, b = ctx.request.body || {}, result = String(b.outcome || '').trim(), orders = String(b.orders || '');
- if (!caseId) { ctx.flash = { message: 'Case not found.' }; return ctx.redirect('/courts?filter=cases'); }
- if (!result) { ctx.flash = { message: 'Result is required.' }; return ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`); }
- try { await courtsModel.issueVerdict({ caseId, result, orders }); } catch (e) { ctx.flash = { message: String(e?.message || e) }; }
- ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
- })
- .post('/courts/cases/:id/settlements/propose', koaBody(), async (ctx) => {
- const caseId = ctx.params.id, terms = String(ctx.request.body?.terms || '');
- if (!caseId) { ctx.flash = { message: 'Case not found.' }; return ctx.redirect('/courts?filter=cases'); }
- if (!terms) { ctx.flash = { message: 'Terms are required.' }; return ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`); }
- try { await courtsModel.proposeSettlement({ caseId, terms }); } catch (e) { ctx.flash = { message: String(e?.message || e) }; }
- ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
- })
- .post('/courts/cases/:id/settlements/accept', koaBody(), async (ctx) => {
- const caseId = ctx.params.id;
- if (!caseId) { ctx.flash = { message: 'Case not found.' }; return ctx.redirect('/courts?filter=cases'); }
- try { await courtsModel.acceptSettlement({ caseId }); } catch (e) { ctx.flash = { message: String(e?.message || e) }; }
- ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
- })
- .post('/courts/cases/:id/mediators/accuser', koaBody(), async (ctx) => {
- const caseId = ctx.params.id, mediators = String(ctx.request.body?.mediators || '').split(',').map(s => s.trim()).filter(Boolean);
- const uid = ctx.state?.user?.id;
- if (!caseId) { ctx.flash = { message: 'Case not found.' }; return ctx.redirect('/courts?filter=cases'); }
- if (!mediators.length) { ctx.flash = { message: 'At least one mediator is required.' }; return ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`); }
- if (uid && mediators.includes(uid)) { ctx.flash = { message: 'You cannot appoint yourself as mediator.' }; return ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`); }
- try { await courtsModel.setMediators({ caseId, side: 'accuser', mediators }); } catch (e) { ctx.flash = { message: String(e?.message || e) }; }
- ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
- })
- .post('/courts/cases/:id/mediators/respondent', koaBody(), async (ctx) => {
- const caseId = ctx.params.id, mediators = String(ctx.request.body?.mediators || '').split(',').map(s => s.trim()).filter(Boolean);
- const uid = ctx.state?.user?.id;
- if (!caseId) { ctx.flash = { message: 'Case not found.' }; return ctx.redirect('/courts?filter=cases'); }
- if (!mediators.length) { ctx.flash = { message: 'At least one mediator is required.' }; return ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`); }
- if (uid && mediators.includes(uid)) { ctx.flash = { message: 'You cannot appoint yourself as mediator.' }; return ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`); }
- try { await courtsModel.setMediators({ caseId, side: 'respondent', mediators }); } catch (e) { ctx.flash = { message: String(e?.message || e) }; }
- ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
- })
- .post('/courts/cases/:id/judge', koaBody(), async (ctx) => {
- const caseId = ctx.params.id, judgeId = String(ctx.request.body?.judgeId || '').trim(), uid = ctx.state?.user?.id;
- if (!caseId) { ctx.flash = { message: 'Case not found.' }; return ctx.redirect('/courts?filter=cases'); }
- if (!judgeId) { ctx.flash = { message: 'Judge is required.' }; return ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`); }
- if (uid && judgeId === uid) { ctx.flash = { message: 'You cannot assign yourself as judge.' }; return ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`); }
- try { await courtsModel.assignJudge({ caseId, judgeId }); } catch (e) { ctx.flash = { message: String(e?.message || e) }; }
- ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
- })
- .post('/courts/cases/:id/public', koaBody(), async (ctx) => {
- const caseId = ctx.params.id, pref = String(ctx.request.body?.preference || '').toUpperCase();
- if (!caseId) { ctx.flash = { message: 'Case not found.' }; return ctx.redirect('/courts?filter=cases'); }
- if (pref !== 'YES' && pref !== 'NO') { ctx.flash = { message: 'Invalid visibility preference.' }; return ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`); }
- try { await courtsModel.setPublicPreference({ caseId, preference: pref === 'YES' }); } catch (e) { ctx.flash = { message: String(e?.message || e) }; }
- ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
- })
- .post('/courts/cases/:id/openVote', koaBody(), async (ctx) => {
- const caseId = ctx.params.id;
- if (!caseId) { ctx.flash = { message: 'Case not found.' }; return ctx.redirect('/courts?filter=cases'); }
- try { await courtsModel.openPopularVote({ caseId }); } catch (e) { ctx.flash = { message: String(e?.message || e) }; }
- ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
- })
- .post('/courts/judges/nominate', koaBody(), async (ctx) => {
- const judgeId = String(ctx.request.body?.judgeId || '').trim();
- if (!judgeId) { ctx.flash = { message: 'Judge is required.' }; return ctx.redirect('/courts?filter=judges'); }
- try { await courtsModel.nominateJudge({ judgeId }); } catch (e) { ctx.flash = { message: String(e?.message || e) }; }
- ctx.redirect('/courts?filter=judges');
- })
- .post('/courts/judges/:id/vote', koaBody(), async (ctx) => {
- if (!ctx.params.id) { ctx.flash = { message: 'Nomination not found.' }; return ctx.redirect('/courts?filter=judges'); }
- try { await courtsModel.voteNomination(ctx.params.id); } catch (e) { ctx.flash = { message: String(e?.message || e) }; }
- ctx.redirect('/courts?filter=judges');
- })
- .post("/market/create", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => {
- if (!checkMod(ctx, 'marketMod')) { ctx.redirect('/modules'); return; }
- const b = ctx.request.body, image = await handleBlobUpload(ctx, "image"), parsedStock = parseInt(String(b.stock || "0"), 10);
- if (!parsedStock || parsedStock <= 0) ctx.throw(400, "Stock must be a positive number.");
- const pickLast = v => Array.isArray(v) ? v[v.length - 1] : v, shpVal = pickLast(b.includesShipping);
- await marketModel.createItem(b.item_type, stripDangerousTags(b.title), stripDangerousTags(b.description), image, b.price, b.tags, b.item_status, b.deadline, shpVal === "1" || shpVal === "on" || shpVal === true || shpVal === "true", parsedStock);
- ctx.redirect(safeReturnTo(ctx, "/market", ["/market"]));
- })
- .post("/market/update/:id", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => {
- if (!checkMod(ctx, 'marketMod')) { ctx.redirect('/modules'); return; }
- const b = ctx.request.body, parsedStock = parseInt(String(b.stock || "0"), 10);
- if (parsedStock < 0) ctx.throw(400, "Stock cannot be negative.");
- const pickLast = v => Array.isArray(v) ? v[v.length - 1] : v, shpVal = pickLast(b.includesShipping);
- const updatedData = { item_type: b.item_type, title: stripDangerousTags(b.title), description: stripDangerousTags(b.description), price: b.price, item_status: b.item_status, deadline: b.deadline, includesShipping: shpVal === "1" || shpVal === "on" || shpVal === true || shpVal === "true", tags: String(b.tags || "").split(",").map(t => t.trim()).filter(Boolean), stock: parsedStock };
- const image = await handleBlobUpload(ctx, "image");
- if (image) updatedData.image = image;
- await marketModel.updateItemById(ctx.params.id, updatedData);
- ctx.redirect(safeReturnTo(ctx, "/market?filter=mine", ["/market"]));
- })
- .post("/market/delete/:id", koaBody(), async ctx => {
- if (!checkMod(ctx, 'marketMod')) { ctx.redirect('/modules'); return; }
- await marketModel.deleteItemById(ctx.params.id)
- ctx.redirect(safeReturnTo(ctx, "/market?filter=mine", ["/market"]))
- })
- .post("/market/sold/:id", koaBody(), async (ctx) => {
- if (!checkMod(ctx, 'marketMod')) { ctx.redirect('/modules'); return; }
- const item = await marketModel.getItemById(ctx.params.id);
- if (!item) ctx.throw(404, "Item not found");
- if (Number(item.stock || 0) <= 0) ctx.throw(400, "No stock left to mark as sold.");
- if (item.status !== "SOLD") { await marketModel.setItemAsSold(ctx.params.id); await marketModel.decrementStock(ctx.params.id); }
- ctx.redirect(safeReturnTo(ctx, "/market?filter=mine", ["/market"]));
- })
- .post("/market/buy/:id", koaBody(), async (ctx) => {
- if (!checkMod(ctx, 'marketMod')) { ctx.redirect('/modules'); return; }
- const item = await marketModel.getItemById(ctx.params.id);
- if (!item) ctx.throw(404, "Item not found");
- if (item.item_type === "exchange" && item.status !== "SOLD") {
- await pmModel.sendMessage([item.seller], "MARKET_SOLD", `item "${item.title}" has been sold -> /market/${ctx.params.id} OASIS ID: ${getViewerId()} for: ${item.price} ECO`);
- await marketModel.setItemAsSold(ctx.params.id);
- } else await marketModel.decrementStock(ctx.params.id);
- ctx.redirect(safeReturnTo(ctx, "/inbox?filter=sent", ["/inbox", "/market"]));
- })
- .post("/market/status/:id", koaBody(), async (ctx) => {
- if (!checkMod(ctx, 'marketMod')) { ctx.redirect('/modules'); return; }
- const desired = String(ctx.request.body.status || "").toUpperCase().replace(/_/g, " ").replace(/\s+/g, " ").trim();
- if (!["FOR SALE", "SOLD", "DISCARDED"].includes(desired)) ctx.throw(400, "Invalid status.");
- const item = await marketModel.getItemById(ctx.params.id);
- if (!item) ctx.throw(404, "Item not found");
- const cur = String(item.status || "").toUpperCase().replace(/\s+/g, " ").trim();
- if (cur !== "SOLD" && cur !== "DISCARDED" && desired !== cur && desired !== "FOR SALE") {
- if (desired === "SOLD") {
- if (Number(item.stock || 0) <= 0) ctx.throw(400, "No stock left to mark as sold.");
- await marketModel.setItemAsSold(ctx.params.id); await marketModel.decrementStock(ctx.params.id);
- } else if (desired === "DISCARDED") await marketModel.updateItemById(ctx.params.id, { status: "DISCARDED", stock: 0 });
- }
- ctx.redirect(safeReturnTo(ctx, "/market?filter=mine", ["/market"]));
- })
- .post("/market/bid/:id", koaBody(), async ctx => {
- if (!checkMod(ctx, 'marketMod')) { ctx.redirect('/modules'); return; }
- await marketModel.addBidToAuction(ctx.params.id, getViewerId(), ctx.request.body.bidAmount)
- ctx.redirect(safeReturnTo(ctx, "/market?filter=auctions", ["/market"]))
- })
- .post("/market/:itemId/comments", koaBodyMiddleware, async ctx => commentAction(ctx, 'market', 'itemId'))
- .post('/jobs/create', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => {
- if (!checkMod(ctx, 'jobsMod')) { ctx.redirect('/modules'); return; }
- const b = ctx.request.body, imageBlob = ctx.request.files?.image ? await handleBlobUpload(ctx, 'image') : null;
- await jobsModel.createJob({ job_type: b.job_type, title: b.title, description: b.description, requirements: b.requirements, languages: b.languages, job_time: b.job_time, tasks: b.tasks, location: b.location, vacants: b.vacants ? parseInt(b.vacants, 10) : 1, salary: b.salary != null && b.salary !== '' ? parseFloat(String(b.salary).replace(',', '.')) : 0, tags: b.tags, image: imageBlob });
- ctx.redirect(safeReturnTo(ctx, '/jobs?filter=MINE', ['/jobs']));
- })
- .post('/jobs/update/:id', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => {
- if (!checkMod(ctx, 'jobsMod')) { ctx.redirect('/modules'); return; }
- const b = ctx.request.body, imageBlob = ctx.request.files?.image ? await handleBlobUpload(ctx, 'image') : undefined;
- const patch = { job_type: b.job_type, title: b.title, description: b.description, requirements: b.requirements, languages: b.languages, job_time: b.job_time, tasks: b.tasks, location: b.location, tags: b.tags };
- if (b.vacants !== undefined && b.vacants !== '') patch.vacants = parseInt(b.vacants, 10);
- if (b.salary !== undefined && b.salary !== '') patch.salary = parseFloat(String(b.salary).replace(',', '.'));
- if (imageBlob !== undefined) patch.image = imageBlob;
- await jobsModel.updateJob(ctx.params.id, patch);
- ctx.redirect(safeReturnTo(ctx, '/jobs?filter=MINE', ['/jobs']));
- })
- .post('/jobs/delete/:id', koaBody(), async ctx => {
- if (!checkMod(ctx, 'jobsMod')) { ctx.redirect('/modules'); return; }
- await jobsModel.deleteJob(ctx.params.id)
- ctx.redirect(safeReturnTo(ctx, '/jobs?filter=MINE', ['/jobs']))
- })
- .post('/jobs/status/:id', koaBody(), async ctx => {
- if (!checkMod(ctx, 'jobsMod')) { ctx.redirect('/modules'); return; }
- await jobsModel.updateJobStatus(ctx.params.id, String(ctx.request.body.status).toUpperCase())
- ctx.redirect(safeReturnTo(ctx, '/jobs?filter=MINE', ['/jobs']))
- })
- .post('/jobs/subscribe/:id', koaBody(), async (ctx) => {
- if (!checkMod(ctx, 'jobsMod')) { ctx.redirect('/modules'); return; }
- const userId = getViewerId(), job = await jobsModel.getJobById(ctx.params.id, userId);
- await jobsModel.subscribeToJob(ctx.params.id, userId);
- await pmModel.sendMessage([job.author], 'JOB_SUBSCRIBED', `has subscribed to your job offer "${job.title || ''}" -> /jobs/${encodeURIComponent(job.id)}`);
- ctx.redirect(safeReturnTo(ctx, '/jobs', ['/jobs']));
- })
- .post('/jobs/unsubscribe/:id', koaBody(), async (ctx) => {
- if (!checkMod(ctx, 'jobsMod')) { ctx.redirect('/modules'); return; }
- const userId = getViewerId(), job = await jobsModel.getJobById(ctx.params.id, userId);
- await jobsModel.unsubscribeFromJob(ctx.params.id, userId);
- await pmModel.sendMessage([job.author], 'JOB_UNSUBSCRIBED', `has unsubscribed from your job offer "${job.title || ''}" -> /jobs/${encodeURIComponent(job.id)}`);
- ctx.redirect(safeReturnTo(ctx, '/jobs', ['/jobs']));
- })
- .post('/jobs/:jobId/comments', koaBodyMiddleware, async ctx => commentAction(ctx, 'jobs', 'jobId'))
- .post("/projects/create", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => {
- if (!checkMod(ctx, 'projectsMod')) { ctx.redirect('/modules'); return; }
- const b = ctx.request.body || {}, image = ctx.request.files?.image ? await handleBlobUpload(ctx, "image") : null;
- const bounties = b.bountiesInput ? String(b.bountiesInput).split("\n").filter(Boolean).map(l => { const [t,a,d] = String(l).split("|"); return { title: String(t||"").trim(), amount: parseFloat(a||0)||0, description: String(d||"").trim(), milestoneIndex: null }; }) : [];
- await projectsModel.createProject({ title: b.title, description: b.description, goal: b.goal != null && b.goal !== "" ? parseFloat(b.goal) : 0, deadline: b.deadline ? new Date(b.deadline).toISOString() : null, progress: b.progress != null && b.progress !== "" ? parseInt(b.progress,10) : 0, bounties, image, milestoneTitle: b.milestoneTitle, milestoneDescription: b.milestoneDescription, milestoneTargetPercent: b.milestoneTargetPercent, milestoneDueDate: b.milestoneDueDate });
- ctx.redirect(safeReturnTo(ctx, "/projects?filter=MINE", ["/projects"]));
- })
- .post("/projects/update/:id", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => {
- if (!checkMod(ctx, 'projectsMod')) { ctx.redirect('/modules'); return; }
- const id = await projectsModel.getProjectTipId(ctx.params.id), b = ctx.request.body || {};
- const image = ctx.request.files?.image ? await handleBlobUpload(ctx, "image") : undefined;
- const bounties = b.bountiesInput !== undefined ? String(b.bountiesInput).split("\n").filter(Boolean).map(l => { const [t,a,d] = String(l).split("|"); return { title: String(t||"").trim(), amount: parseFloat(a||0)||0, description: String(d||"").trim(), milestoneIndex: null }; }) : undefined;
- await projectsModel.updateProject(id, { title: b.title, description: b.description, goal: b.goal !== "" && b.goal != null ? parseFloat(b.goal) : undefined, deadline: b.deadline ? new Date(b.deadline).toISOString() : undefined, progress: b.progress !== "" && b.progress != null ? parseInt(b.progress,10) : undefined, bounties, image });
- ctx.redirect(safeReturnTo(ctx, "/projects?filter=MINE", ["/projects"]));
- })
- .post("/projects/delete/:id", koaBody(), async (ctx) => {
- if (!checkMod(ctx, 'projectsMod')) { ctx.redirect('/modules'); return; }
- await projectsModel.deleteProject(await projectsModel.getProjectTipId(ctx.params.id));
- ctx.redirect(safeReturnTo(ctx, "/projects?filter=MINE", ["/projects"]));
- })
- .post("/projects/status/:id", koaBody(), async (ctx) => {
- if (!checkMod(ctx, 'projectsMod')) { ctx.redirect('/modules'); return; }
- const id = await projectsModel.getProjectTipId(ctx.params.id);
- await projectsModel.updateProjectStatus(id, String(ctx.request.body?.status || "").toUpperCase());
- ctx.redirect(safeReturnTo(ctx, `/projects/${encodeURIComponent(id)}`, ["/projects"]));
- })
- .post("/projects/progress/:id", koaBody(), async (ctx) => {
- if (!checkMod(ctx, 'projectsMod')) { ctx.redirect('/modules'); return; }
- const id = await projectsModel.getProjectTipId(ctx.params.id);
- await projectsModel.updateProjectProgress(id, ctx.request.body?.progress);
- ctx.redirect(safeReturnTo(ctx, `/projects/${encodeURIComponent(id)}`, ["/projects"]));
- })
- .post("/projects/pledge/:id", koaBody(), async (ctx) => {
- if (!checkMod(ctx, 'projectsMod')) { ctx.redirect('/modules'); return; }
- const latestId = await projectsModel.getProjectTipId(ctx.params.id), b = ctx.request.body || {};
- const pledgeAmount = parseFloat(b.amount), uid = getViewerId();
- if (isNaN(pledgeAmount) || pledgeAmount <= 0) ctx.throw(400, "Invalid amount");
- const project = await projectsModel.getProjectById(latestId);
- if (String(project.status || "ACTIVE").toUpperCase() !== "ACTIVE") ctx.throw(400, "Project is not active");
- if (project.deadline && moment(project.deadline).isValid() && moment(project.deadline).isBefore(moment())) ctx.throw(400, "Project deadline passed");
- if (project.author === uid) ctx.throw(403, "Authors cannot pledge to their own project");
- let milestoneIndex = null, bountyIndex = null, mob = b.milestoneOrBounty || "";
- if (String(mob).startsWith("milestone:")) milestoneIndex = parseInt(String(mob).split(":")[1], 10);
- else if (String(mob).startsWith("bounty:")) bountyIndex = parseInt(String(mob).split(":")[1], 10);
- const transfer = await transfersModel.createTransfer(project.author, "Project Pledge", pledgeAmount, moment().add(14, "days").toISOString(), ["backer-pledge", `project:${latestId}`]);
- const backers = [...(project.backers || []), { userId: uid, amount: pledgeAmount, at: new Date().toISOString(), transferId: transfer.key || transfer.id, confirmed: false, milestoneIndex, bountyIndex }];
- const pledged = (parseFloat(project.pledged || 0) || 0) + pledgeAmount;
- await projectsModel.updateProject(latestId, { backers, pledged, progress: project.goal ? (pledged / parseFloat(project.goal)) * 100 : 0 });
- await pmModel.sendMessage([project.author], "PROJECT_PLEDGE", `has pledged ${pledgeAmount} ECO to your project "${project.title || ''}" -> /projects/${latestId}`);
- ctx.redirect(safeReturnTo(ctx, `/projects/${encodeURIComponent(latestId)}`, ["/projects"]));
- })
- .post("/projects/confirm-transfer/:id", koaBody(), async (ctx) => {
- if (!checkMod(ctx, 'projectsMod')) { ctx.redirect('/modules'); return; }
- const uid = getViewerId(), transfer = await transfersModel.getTransferById(ctx.params.id);
- if (transfer.to !== uid) ctx.throw(403, "Unauthorized action");
- const tagProject = (Array.isArray(transfer.tags) ? transfer.tags : []).find(t => String(t).startsWith("project:"));
- if (!tagProject) ctx.throw(400, "Missing project tag on transfer");
- const projectId = String(tagProject).split(":")[1];
- await transfersModel.confirmTransferById(ctx.params.id);
- const project = await projectsModel.getProjectById(projectId), backers = [...(project.backers || [])];
- const idx = backers.findIndex(b => b?.transferId === ctx.params.id);
- if (idx !== -1) backers[idx].confirmed = true;
- await projectsModel.updateProject(projectId, { backers, progress: project.goal ? (parseFloat(project.pledged || 0) / parseFloat(project.goal)) * 100 : 0 });
- ctx.redirect(safeReturnTo(ctx, `/projects/${encodeURIComponent(projectId)}`, ["/projects", "/transfers"]));
- })
- .post("/projects/follow/:id", koaBody(), async (ctx) => {
- if (!checkMod(ctx, 'projectsMod')) { ctx.redirect('/modules'); return; }
- const latestId = await projectsModel.getProjectTipId(ctx.params.id), project = await projectsModel.getProjectById(latestId);
- await projectsModel.followProject(ctx.params.id, getViewerId());
- await pmModel.sendMessage([project.author], "PROJECT_FOLLOWED", `has followed your project "${project.title || ''}" -> /projects/${latestId}`);
- ctx.redirect(safeReturnTo(ctx, "/projects", ["/projects"]));
- })
- .post("/projects/unfollow/:id", koaBody(), async (ctx) => {
- if (!checkMod(ctx, 'projectsMod')) { ctx.redirect('/modules'); return; }
- const latestId = await projectsModel.getProjectTipId(ctx.params.id), project = await projectsModel.getProjectById(latestId);
- await projectsModel.unfollowProject(ctx.params.id, getViewerId());
- await pmModel.sendMessage([project.author], "PROJECT_UNFOLLOWED", `has unfollowed your project "${project.title || ''}" -> /projects/${latestId}`);
- ctx.redirect(safeReturnTo(ctx, "/projects", ["/projects"]));
- })
- .post("/projects/milestones/add/:id", koaBody(), async (ctx) => {
- if (!checkMod(ctx, 'projectsMod')) { ctx.redirect('/modules'); return; }
- const id = await projectsModel.getProjectTipId(ctx.params.id), b = ctx.request.body || {};
- await projectsModel.addMilestone(id, { title: b.title, description: b.description || "", targetPercent: b.targetPercent != null && b.targetPercent !== "" ? parseInt(b.targetPercent, 10) : 0, dueDate: b.dueDate ? new Date(b.dueDate).toISOString() : null });
- ctx.redirect(safeReturnTo(ctx, `/projects/${encodeURIComponent(id)}`, ["/projects"]));
- })
- .post("/projects/milestones/update/:id/:index", koaBody(), async (ctx) => {
- if (!checkMod(ctx, 'projectsMod')) { ctx.redirect('/modules'); return; }
- const id = await projectsModel.getProjectTipId(ctx.params.id), idx = parseInt(ctx.params.index, 10), b = ctx.request.body || {};
- const patch = { title: b.title, ...(b.description !== undefined ? { description: b.description } : {}), ...(b.targetPercent !== undefined && b.targetPercent !== "" ? { targetPercent: parseInt(b.targetPercent, 10) } : {}), ...(b.dueDate !== undefined ? { dueDate: b.dueDate ? new Date(b.dueDate).toISOString() : null } : {}), ...(b.done !== undefined ? { done: !!b.done } : {}) };
- await projectsModel.updateMilestone(id, idx, patch);
- ctx.redirect(safeReturnTo(ctx, `/projects/${encodeURIComponent(id)}`, ["/projects"]));
- })
- .post("/projects/milestones/complete/:id/:index", koaBody(), async (ctx) => {
- if (!checkMod(ctx, 'projectsMod')) { ctx.redirect('/modules'); return; }
- const id = await projectsModel.getProjectTipId(ctx.params.id);
- await projectsModel.completeMilestone(id, parseInt(ctx.params.index, 10), getViewerId());
- ctx.redirect(safeReturnTo(ctx, `/projects/${encodeURIComponent(id)}`, ["/projects"]));
- })
- .post("/projects/bounties/add/:id", koaBody(), async (ctx) => {
- if (!checkMod(ctx, 'projectsMod')) { ctx.redirect('/modules'); return; }
- const id = await projectsModel.getProjectTipId(ctx.params.id), b = ctx.request.body || {};
- await projectsModel.addBounty(id, { title: b.title, amount: b.amount, description: b.description, milestoneIndex: b.milestoneIndex === "" || b.milestoneIndex === undefined ? null : parseInt(b.milestoneIndex, 10) });
- ctx.redirect(safeReturnTo(ctx, `/projects/${encodeURIComponent(id)}`, ["/projects"]));
- })
- .post("/projects/bounties/update/:id/:index", koaBody(), async (ctx) => {
- if (!checkMod(ctx, 'projectsMod')) { ctx.redirect('/modules'); return; }
- const id = await projectsModel.getProjectTipId(ctx.params.id), idx = parseInt(ctx.params.index, 10), b = ctx.request.body || {};
- const patch = { ...(b.title !== undefined ? { title: b.title } : {}), ...(b.amount !== undefined && b.amount !== "" ? { amount: parseFloat(b.amount) } : {}), ...(b.description !== undefined ? { description: b.description } : {}), ...(b.milestoneIndex !== undefined ? { milestoneIndex: b.milestoneIndex === "" ? null : parseInt(b.milestoneIndex, 10) } : {}), ...(b.done !== undefined ? { done: !!b.done } : {}) };
- await projectsModel.updateBounty(id, idx, patch);
- ctx.redirect(safeReturnTo(ctx, `/projects/${encodeURIComponent(id)}`, ["/projects"]));
- })
- .post("/projects/bounties/claim/:id/:index", koaBody(), async (ctx) => {
- if (!checkMod(ctx, 'projectsMod')) { ctx.redirect('/modules'); return; }
- const id = await projectsModel.getProjectTipId(ctx.params.id);
- await projectsModel.claimBounty(id, parseInt(ctx.params.index, 10), getViewerId());
- ctx.redirect(safeReturnTo(ctx, `/projects/${encodeURIComponent(id)}`, ["/projects"]));
- })
- .post("/projects/bounties/complete/:id/:index", koaBody(), async (ctx) => {
- if (!checkMod(ctx, 'projectsMod')) { ctx.redirect('/modules'); return; }
- const id = await projectsModel.getProjectTipId(ctx.params.id);
- await projectsModel.completeBounty(id, parseInt(ctx.params.index, 10), getViewerId());
- ctx.redirect(safeReturnTo(ctx, `/projects/${encodeURIComponent(id)}`, ["/projects"]));
- })
- .post("/projects/:projectId/comments", koaBodyMiddleware, async ctx => commentAction(ctx, 'projects', 'projectId'))
- .post("/banking/claim/:id", koaBody(), async (ctx) => {
- const userId = getViewerId(), allocation = await bankingModel.getAllocationById(ctx.params.id);
- if (!allocation) { ctx.body = { error: i18n.errorNoAllocation }; return; }
- if (allocation.to !== userId || allocation.status !== "UNCONFIRMED") { ctx.body = { error: i18n.errorInvalidClaim }; return; }
- const { url, user, pass } = getConfig().walletPub;
- const { txid } = await bankingModel.claimAllocation({ transferId: ctx.params.id, claimerId: userId, pubWalletUrl: url, pubWalletUser: user, pubWalletPass: pass });
- await bankingModel.updateAllocationStatus(ctx.params.id, "CLOSED", txid);
- await bankingModel.publishBankClaim({ amount: allocation.amount, epochId: allocation.epochId, allocationId: allocation.id, txid });
- ctx.redirect(`/banking?claimed=${encodeURIComponent(txid)}`);
- })
- .post("/banking/simulate", koaBody(), async (ctx) => {
- const { epochId, rules } = ctx.request.body || {};
- ctx.body = await bankingModel.computeEpoch({ epochId, rules });
- })
- .post("/banking/run", koaBody(), async (ctx) => {
- const { epochId, rules } = ctx.request.body || {};
- ctx.body = await bankingModel.executeEpoch({ epochId, rules });
- })
- .post("/banking/addresses", koaBody(), async (ctx) => {
- const b = ctx.request.body || {}, res = await bankingModel.addAddress({ userId: (b.userId || "").trim(), address: (b.address || "").trim() });
- ctx.redirect(`/banking?filter=addresses&msg=${encodeURIComponent(res.status)}`);
- })
- .post("/banking/addresses/delete", koaBody(), async (ctx) => {
- const res = await bankingModel.removeAddress({ userId: ((ctx.request.body?.userId) || "").trim() });
- ctx.redirect(`/banking?filter=addresses&msg=${encodeURIComponent(res.status)}`);
- })
- .post("/favorites/remove/:kind/:id", koaBody(), async (ctx) => {
- await favoritesModel.removeFavorite(ctx.params.kind, ctx.params.id);
- const fallback = `/favorites?filter=${encodeURIComponent(ctx.query.filter || "all")}`;
- ctx.redirect(safeReturnTo(ctx, fallback, ["/favorites"]));
- })
- .post("/update", koaBody(), async (ctx) => {
- const exec = require("node:util").promisify(require("node:child_process").exec);
- const { stdout, stderr } = await exec("git reset --hard && git pull");
- console.log("oasis@version: updating Oasis...", stdout, stderr);
- const { stdout: shOut, stderr: shErr } = await exec("sh install.sh");
- console.log("oasis@version: running install.sh...", shOut, shErr);
- ctx.redirect(new URL(ctx.request.header.referer).href);
- })
- .post("/settings/theme", koaBody(), async (ctx) => {
- const theme = String(ctx.request.body.theme || "").trim(), cfg = getConfig();
- cfg.themes.current = theme || "Dark-SNH";
- fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
- ctx.cookies.set("theme", cfg.themes.current, { httpOnly: true, sameSite: 'strict' });
- ctx.redirect("/settings");
- })
- .post("/language", koaBody(), async (ctx) => {
- const lang = String(ctx.request.body.language || "en");
- const cfg = getConfig();
- cfg.language = lang;
- fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
- ctx.cookies.set("language", lang, { maxAge: 365 * 24 * 60 * 60 * 1000, httpOnly: true, sameSite: 'strict' });
- ctx.redirect(new URL(ctx.request.header.referer).href);
- })
- .post("/settings/conn/start", koaBody(), async ctx => { await meta.connStart(); ctx.redirect("/peers"); })
- .post("/settings/conn/stop", koaBody(), async ctx => { await meta.connStop(); ctx.redirect("/peers"); })
- .post("/settings/conn/sync", koaBody(), async ctx => { await meta.sync(); ctx.redirect("/peers"); })
- .post("/settings/conn/restart", koaBody(), async ctx => { await meta.connRestart(); ctx.redirect("/peers"); })
- .post("/settings/invite/accept", koaBody(), async ctx => { await meta.acceptInvite(String(ctx.request.body.invite)); ctx.redirect("/invites"); })
- .post("/settings/invite/unfollow", koaBody(), async (ctx) => {
- const { key } = ctx.request.body || {};
- if (!key) return ctx.redirect("/invites");
- const pubs = readJSON(gossipPath), kcanon = canonicalKey(key);
- const idx = pubs.findIndex(x => x && canonicalKey(x.key) === kcanon);
- const removed = idx >= 0 ? (pubs.splice(idx, 1)[0], writeJSON(gossipPath, pubs), pubs[idx-1] !== undefined ? pubs.splice(idx,1)[0] : null) : null;
- const ssb = await cooler.open(), addr = removed?.host ? msAddrFrom(removed.host, removed.port, removed.key) : null;
- if (addr) { try { await new Promise(res => ssb.conn.disconnect(addr, res)); } catch {} try { ssb.conn.forget(addr); } catch {} }
- try { await new Promise((res, rej) => ssb.publish({ type: "contact", contact: kcanon, following: false, blocking: true }, e => e ? rej(e) : res())); } catch {}
- const unf = readJSON(unfollowedPath);
- if (!unf.find(x => x && canonicalKey(x.key) === kcanon)) { unf.push(removed || { key: kcanon }); writeJSON(unfollowedPath, unf); }
- ctx.redirect("/invites");
- })
- .post("/settings/invite/follow", koaBody(), async (ctx) => {
- const { key, host, port } = ctx.request.body || {};
- if (!key || !host) return ctx.redirect("/invites");
- const pubs = readJSON(gossipPath), kcanon = canonicalKey(key);
- if (pubs.find(p => p.host === host)?.error) return ctx.redirect("/invites");
- const ssb = await cooler.open(), unf = readJSON(unfollowedPath);
- const rec = unf.find(x => x && canonicalKey(x.key) === kcanon) || { host, port: Number(port) || 8008, key: kcanon };
- if (!pubs.find(x => x && canonicalKey(x.key) === kcanon)) { pubs.push({ host: rec.host, port: Number(rec.port) || 8008, key: kcanon }); writeJSON(gossipPath, pubs); }
- const addr = msAddrFrom(rec.host, rec.port, kcanon);
- try { ssb.conn.remember(addr, { type: "pub", autoconnect: true, key: kcanon }); } catch {}
- try { await new Promise(res => ssb.conn.connect(addr, { type: "pub" }, res)); } catch {}
- try { await new Promise((res, rej) => ssb.publish({ type: "contact", contact: kcanon, blocking: false }, e => e ? rej(e) : res())); } catch {}
- writeJSON(unfollowedPath, unf.filter(x => !(x && canonicalKey(x.key) === kcanon)));
- ctx.redirect("/invites");
- })
- .post("/peers/connect", koaBody(), async (ctx) => {
- const { key, host, port } = ctx.request.body || {};
- if (!key || !host) return ctx.redirect("/peers?err=missing");
- const hostStr = String(host).trim().toLowerCase();
- const isIPv4 = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostStr);
- const isHostname = /^[a-z0-9]([a-z0-9\-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9\-]*[a-z0-9])?)*$/.test(hostStr);
- if ((!isIPv4 && !isHostname) || hostStr.length > 253) return ctx.redirect("/peers?err=invalidHost");
- if (isIPv4 && hostStr.split('.').some(o => Number(o) > 255)) return ctx.redirect("/peers?err=invalidHost");
- const prt = Number(port) || 8008;
- if (!Number.isInteger(prt) || prt < 1 || prt > 65535) return ctx.redirect("/peers?err=invalidPort");
- const keyStr = String(key).trim();
- if (!/^@[A-Za-z0-9+/_\-]{43}=\.ed25519$/.test(keyStr)) return ctx.redirect("/peers?err=invalidKey");
- const kcanon = canonicalKey(keyStr);
- const pubs = readJSON(gossipPath);
- if (!pubs.find(x => x && canonicalKey(x.key) === kcanon)) {
- pubs.push({ host: hostStr, port: prt, key: kcanon });
- writeJSON(gossipPath, pubs);
- }
- const ssb = await cooler.open();
- const addr = msAddrFrom(hostStr, prt, kcanon);
- try { ssb.conn.remember(addr, { type: "peer", autoconnect: true, key: kcanon }); } catch {}
- try { await new Promise(res => ssb.conn.connect(addr, { type: "peer" }, res)); } catch {}
- try { await new Promise((res, rej) => ssb.publish({ type: "contact", contact: kcanon, following: true }, e => e ? rej(e) : res())); } catch {}
- const unf = readJSON(unfollowedPath);
- writeJSON(unfollowedPath, unf.filter(x => !(x && canonicalKey(x.key) === kcanon)));
- ctx.redirect("/peers");
- })
- .post("/settings/ssb-logstream", koaBody(), async (ctx) => {
- const logLimit = parseInt(ctx.request.body.ssb_log_limit, 10);
- if (!isNaN(logLimit) && logLimit > 0 && logLimit <= 100000) {
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
- config.ssbLogStream = { ...(config.ssbLogStream || {}), limit: logLimit };
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
- }
- ctx.redirect("/settings");
- })
- .post("/settings/home-page", koaBody(), async (ctx) => {
- const cfg = getConfig();
- cfg.homePage = String(ctx.request.body.homePage || "").trim() || "activity";
- saveConfig(cfg);
- ctx.redirect("/settings");
- })
- .post("/settings/rebuild", async ctx => { meta.rebuild(); ctx.redirect("/settings"); })
- .post("/modules/preset", koaBody(), async (ctx) => {
- const ALL_MODULES = ['popular', 'topics', 'summaries', 'latest', 'threads', 'multiverse', 'invites', 'wallet', 'legacy', 'cipher', 'bookmarks', 'videos', 'docs', 'audios', 'tags', 'images', 'trending', 'events', 'tasks', 'market', 'tribes', 'votes', 'reports', 'opinions', 'transfers', 'feed', 'pixelia', 'agenda', 'favorites', 'ai', 'forum', 'jobs', 'projects', 'banking', 'parliament', 'courts'];
- const PRESETS = {
- minimal: ['feed', 'forum', 'images', 'videos', 'audios', 'bookmarks', 'tags', 'trending', 'popular', 'latest', 'threads', 'opinions', 'cipher', 'legacy'],
- social: ['agenda', 'audios', 'bookmarks', 'cipher', 'courts', 'docs', 'events', 'favorites', 'feed', 'forum', 'images', 'invites', 'legacy', 'multiverse', 'opinions', 'parliament', 'pixelia', 'projects', 'reports', 'tags', 'tasks', 'threads', 'trending', 'tribes', 'videos', 'votes'],
- economy: ['agenda', 'audios', 'bookmarks', 'cipher', 'courts', 'docs', 'events', 'favorites', 'feed', 'forum', 'images', 'invites', 'legacy', 'multiverse', 'opinions', 'parliament', 'pixelia', 'projects', 'reports', 'tags', 'tasks', 'threads', 'trending', 'tribes', 'videos', 'votes', 'banking', 'wallet', 'transfers', 'market', 'jobs'],
- full: ALL_MODULES
- };
- const preset = String(ctx.request.body.preset || '');
- const enabledMods = PRESETS[preset];
- if (!enabledMods) { ctx.redirect('/modules'); return; }
- const cfg = getConfig();
- ALL_MODULES.forEach(mod => cfg.modules[`${mod}Mod`] = enabledMods.includes(mod) ? 'on' : 'off');
- saveConfig(cfg);
- ctx.redirect('/modules');
- })
- .post("/save-modules", koaBody(), async (ctx) => {
- const modules = ['popular', 'topics', 'summaries', 'latest', 'threads', 'multiverse', 'invites', 'wallet', 'legacy', 'cipher', 'bookmarks', 'videos', 'docs', 'audios', 'tags', 'images', 'trending', 'events', 'tasks', 'market', 'tribes', 'votes', 'reports', 'opinions', 'transfers', 'feed', 'pixelia', 'agenda', 'favorites', 'ai', 'forum', 'jobs', 'projects', 'banking', 'parliament', 'courts'];
- const cfg = getConfig();
- modules.forEach(mod => cfg.modules[`${mod}Mod`] = ctx.request.body[`${mod}Form`] === 'on' ? 'on' : 'off');
- saveConfig(cfg);
- ctx.redirect(`/modules`);
- })
- .post("/settings/ai", koaBody(), async (ctx) => {
- const aiPrompt = String(ctx.request.body.ai_prompt || "").trim();
- if (aiPrompt.length > 128) { ctx.status = 400; ctx.body = "Prompt too long. Must be 128 characters or fewer."; return; }
- const cfg = getConfig();
- cfg.ai = { ...(cfg.ai || {}), prompt: aiPrompt };
- saveConfig(cfg);
- ctx.redirect("/settings");
- })
- .post("/settings/pub-wallet", koaBody(), async (ctx) => {
- const b = ctx.request.body, cfg = getConfig();
- cfg.walletPub = { url: String(b.wallet_url || "").trim(), user: String(b.wallet_user || "").trim(), pass: String(b.wallet_pass || "").trim() };
- fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
- ctx.redirect("/settings");
- })
- .post('/transfers/create', koaBody(), async ctx => {
- if (!checkMod(ctx, 'transfersMod')) { ctx.redirect('/modules'); return; }
- const b = ctx.request.body;
- await transfersModel.createTransfer(b.to, b.concept, b.amount, b.deadline, b.tags);
- ctx.redirect(safeReturnTo(ctx, '/transfers?filter=all', ['/transfers']));
- })
- .post('/transfers/update/:id', koaBody(), async ctx => {
- if (!checkMod(ctx, 'transfersMod')) { ctx.redirect('/modules'); return; }
- const b = ctx.request.body;
- await transfersModel.updateTransferById(ctx.params.id, b.to, b.concept, b.amount, b.deadline, b.tags);
- ctx.redirect(safeReturnTo(ctx, '/transfers?filter=mine', ['/transfers']));
- })
- .post('/transfers/confirm/:id', koaBody(), async ctx => {
- if (!checkMod(ctx, 'transfersMod')) { ctx.redirect('/modules'); return; }
- await transfersModel.confirmTransferById(ctx.params.id);
- ctx.redirect(safeReturnTo(ctx, '/transfers', ['/transfers']));
- })
- .post('/transfers/delete/:id', koaBody(), async ctx => {
- if (!checkMod(ctx, 'transfersMod')) { ctx.redirect('/modules'); return; }
- await transfersModel.deleteTransferById(ctx.params.id);
- ctx.redirect(safeReturnTo(ctx, '/transfers?filter=mine', ['/transfers']));
- })
- .post('/transfers/opinions/:transferId/:category', koaBody(), async ctx => {
- if (!checkMod(ctx, 'transfersMod')) { ctx.redirect('/modules'); return; }
- await transfersModel.createOpinion(ctx.params.transferId, ctx.params.category);
- ctx.redirect(safeReturnTo(ctx, '/transfers', ['/transfers']));
- })
- .post("/settings/wallet", koaBody(), async (ctx) => {
- const b = ctx.request.body, cfg = getConfig();
- if (b.wallet_url) cfg.wallet.url = String(b.wallet_url);
- if (b.wallet_user) cfg.wallet.user = String(b.wallet_user);
- if (b.wallet_pass) cfg.wallet.pass = String(b.wallet_pass);
- if (b.wallet_fee) cfg.wallet.fee = String(b.wallet_fee);
- saveConfig(cfg);
- const res = await bankingModel.ensureSelfAddressPublished();
- ctx.redirect(`/banking?filter=addresses&msg=${encodeURIComponent(res.status)}`);
- })
- .post("/wallet/send", koaBody(), async (ctx) => {
- const b = ctx.request.body, action = String(b.action), dest = String(b.destination), amt = Number(b.amount), fee = Number(b.fee);
- const { url, user, pass } = getConfig().wallet;
- let balance = null;
- try { balance = await walletModel.getBalance(url, user, pass); } catch (error) { ctx.body = await walletErrorView(error); return; }
- if (action === 'confirm') {
- const v = await walletModel.validateSend(url, user, pass, dest, amt, fee);
- try { ctx.body = v.isValid ? await walletSendConfirmView(balance, dest, amt, fee) : await walletSendFormView(balance, dest, amt, fee, { type: 'error', title: 'validation_errors', messages: v.errors }); }
- catch (error) { ctx.body = await walletErrorView(error); }
- } else if (action === 'send') {
- try { ctx.body = await walletSendResultView(balance, dest, amt, await walletModel.sendToAddress(url, user, pass, dest, amt)); }
- catch (error) { ctx.body = await walletErrorView(error); }
- }
- });
- const routes = router.routes();
- const middleware = [
- async (ctx, next) => {
- if (config.public && ctx.method !== "GET") throw new Error("Sorry, many actions are unavailable when Oasis is running in public mode. Please run Oasis in the default mode and try again.");
- await next();
- },
- async (ctx, next) => { setLanguage(ctx.cookies.get("language") || getConfig().language || "en"); await next(); },
- async (ctx, next) => {
- const ssb = await cooler.open(), status = await ssb.status(), values = Object.values(status.sync.plugins);
- const totalCurrent = values.reduce((acc, cur) => acc + cur, 0), totalTarget = status.sync.since * values.length;
- if (totalTarget - totalCurrent > 1024 * 1024) ctx.response.body = indexingView({ percent: Math.floor((totalCurrent / totalTarget) * 1000) / 10 });
- else { try { await next(); } catch (err) {
- if (err.name === 'FileTooLargeError' || (err.message && err.message.includes('maxFileSize'))) {
- const { template, i18n } = require('../views/main_views');
- const referer = ctx.get('referer') || '/';
- ctx.status = 413;
- ctx.body = template(
- i18n.fileTooLargeTitle,
- section(
- div({ class: 'tags-header' },
- h2(i18n.fileTooLargeTitle),
- p(i18n.fileTooLargeMessage),
- p(a({ href: referer, class: 'filter-btn', style: 'display:inline-block;text-decoration:none;margin-top:16px;' }, i18n.goBack))
- )
- )
- );
- } else {
- ctx.status = err.status || 500; ctx.body = { message: err.message || 'Internal Server Error' };
- }
- } }
- },
- async (ctx, next) => {
- if (!ctx.path.startsWith('/assets/') && !ctx.path.startsWith('/image/') && !ctx.path.startsWith('/blob/')) {
- const now = Date.now();
- if (now - sharedState.getLastRefresh() > 60000) {
- sharedState.setLastRefresh(now);
- try {
- const stats = await statsModel.getStats('ALL');
- const totalMB = parseSizeMB(stats.statsBlobsSize) + parseSizeMB(stats.statsBlockchainSize);
- const hcT = parseFloat((totalMB * 0.0002 * 475).toFixed(2));
- const inhabitants = stats.usersKPIs?.totalInhabitants || stats.inhabitants || 1;
- const hcH = inhabitants > 0 ? parseFloat((hcT / inhabitants).toFixed(2)) : 0;
- sharedState.setCarbonHcT(hcT);
- sharedState.setCarbonHcH(hcH);
- } catch (_) {}
- try { await refreshInboxCount(); } catch (_) {}
- }
- }
- await next();
- },
- routes,
- ];
- const app = http({ host, port, middleware, allowHost: config.allowHost });
- app._close = () => { nameWarmup.close(); cooler.close(); };
- module.exports = app;
- if (config.open === true) open(url);
|