backend.js 123 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271
  1. #!/usr/bin/env node
  2. "use strict";
  3. const path = require("path");
  4. const envPaths = require("../server/node_modules/env-paths");
  5. const {cli} = require("../client/oasis_client");
  6. const fs = require("fs");
  7. const os = require('os');
  8. const promisesFs = require("fs").promises;
  9. const SSBconfig = require('../server/SSB_server.js');
  10. const moment = require('../server/node_modules/moment');
  11. const FileType = require('../server/node_modules/file-type');
  12. const ssbRef = require("../server/node_modules/ssb-ref");
  13. const defaultConfig = {};
  14. const defaultConfigFile = path.join(
  15. envPaths("oasis", { suffix: "" }).config,
  16. "/default.json"
  17. );
  18. let haveConfig;
  19. try {
  20. const defaultConfigOverride = fs.readFileSync(defaultConfigFile, "utf8");
  21. Object.entries(JSON.parse(defaultConfigOverride)).forEach(([key, value]) => {
  22. defaultConfig[key] = value;
  23. });
  24. haveConfig = true;
  25. } catch (e) {
  26. if (e.code === "ENOENT") {
  27. haveConfig = false;
  28. } else {
  29. console.log(`There was a problem loading ${defaultConfigFile}`);
  30. throw e;
  31. }
  32. }
  33. const config = cli(defaultConfig, defaultConfigFile);
  34. if (config.debug) {
  35. process.env.DEBUG = "oasis,oasis:*";
  36. }
  37. //AI
  38. const axiosMod = require('../server/node_modules/axios');
  39. const axios = axiosMod.default || axiosMod;
  40. const { spawn } = require('child_process');
  41. const { fieldsForSnippet, buildContext, clip, publishExchange, getBestTrainedAnswer } = require('../AI/buildAIContext.js');
  42. let aiStarted = false;
  43. function startAI() {
  44. if (aiStarted) return;
  45. aiStarted = true;
  46. const aiPath = path.resolve(__dirname, '../AI/ai_service.mjs');
  47. const aiProcess = spawn('node', [aiPath], {
  48. detached: true,
  49. stdio: 'ignore' // set 'inherit' for debug
  50. });
  51. aiProcess.unref();
  52. }
  53. //banking
  54. function readWalletMap() {
  55. const candidates = [
  56. path.join(__dirname, '..', 'configs', 'wallet-addresses.json'),
  57. path.join(process.cwd(), 'configs', 'wallet-addresses.json')
  58. ];
  59. for (const p of candidates) {
  60. try {
  61. if (fs.existsSync(p)) {
  62. const obj = JSON.parse(fs.readFileSync(p, 'utf8'));
  63. if (obj && typeof obj === 'object') return obj;
  64. }
  65. } catch {}
  66. }
  67. return {};
  68. }
  69. //custom styles
  70. const customStyleFile = path.join(
  71. envPaths("oasis", { suffix: "" }).config,
  72. "/custom-style.css"
  73. );
  74. let haveCustomStyle;
  75. try {
  76. fs.readFileSync(customStyleFile, "utf8");
  77. haveCustomStyle = true;
  78. } catch (e) {
  79. if (e.code === "ENOENT") {
  80. haveCustomStyle = false;
  81. } else {
  82. console.log(`There was a problem loading ${customStyleFile}`);
  83. throw e;
  84. }
  85. }
  86. const { get } = require("node:http");
  87. const debug = require("../server/node_modules/debug")("oasis");
  88. const log = (formatter, ...args) => {
  89. const isDebugEnabled = debug.enabled;
  90. debug.enabled = true;
  91. debug(formatter, ...args);
  92. debug.enabled = isDebugEnabled;
  93. };
  94. delete config._;
  95. delete config.$0;
  96. const { host } = config;
  97. const { port } = config;
  98. const url = `http://${host}:${port}`;
  99. debug("Current configuration: %O", config);
  100. debug(`You can save the above to ${defaultConfigFile} to make \
  101. these settings the default. See the readme for details.`);
  102. const { saveConfig, getConfig } = require('../configs/config-manager');
  103. const configPath = path.join(__dirname, '../configs/oasis-config.json');
  104. const oasisCheckPath = "/.well-known/oasis";
  105. process.on("uncaughtException", function (err) {
  106. if (err["code"] === "EADDRINUSE") {
  107. get(url + oasisCheckPath, (res) => {
  108. let rawData = "";
  109. res.on("data", (chunk) => {
  110. rawData += chunk;
  111. });
  112. res.on("end", () => {
  113. log(rawData);
  114. if (rawData === "oasis") {
  115. log(`Oasis is already running on host ${host} and port ${port}`);
  116. if (config.open === true) {
  117. log("Opening link to existing instance of Oasis");
  118. open(url);
  119. } else {
  120. log(
  121. "Not opening your browser because opening is disabled by your config"
  122. );
  123. }
  124. process.exit(0);
  125. } else {
  126. throw new Error(`Another server is already running at ${url}.
  127. It might be another copy of Oasis or another program on your computer.
  128. You can run Oasis on a different port number with this option:
  129. oasis --port ${config.port + 1}
  130. Alternatively, you can set the default port in ${defaultConfigFile} with:
  131. {
  132. "port": ${config.port + 1}
  133. }
  134. `);
  135. }
  136. });
  137. });
  138. } else {
  139. console.log("");
  140. console.log("Oasis traceback (share below content with devs to report!):");
  141. console.log("===========================================================");
  142. console.log(err);
  143. console.log("");
  144. }
  145. });
  146. process.argv = [];
  147. const http = require("../client/middleware");
  148. const {koaBody} = require("../server/node_modules/koa-body");
  149. const { nav, ul, li, a, form, button, div } = require("../server/node_modules/hyperaxe");
  150. const open = require("../server/node_modules/open");
  151. const pull = require("../server/node_modules/pull-stream");
  152. const koaRouter = require("../server/node_modules/@koa/router");
  153. const ssbMentions = require("../server/node_modules/ssb-mentions");
  154. const isSvg = require('../server/node_modules/is-svg');
  155. const { isFeed, isMsg, isBlob } = require("../server/node_modules/ssb-ref");
  156. const ssb = require("../client/gui");
  157. const router = new koaRouter();
  158. const extractMentions = async (text) => {
  159. const mentions = ssbMentions(text) || [];
  160. const resolvedMentions = await Promise.all(mentions.map(async (mention) => {
  161. const name = mention.name || await about.name(mention.link);
  162. return {
  163. link: mention.link,
  164. name: name || 'Anonymous',
  165. };
  166. }));
  167. return resolvedMentions;
  168. };
  169. const cooler = ssb({ offline: config.offline });
  170. // load core models (cooler)
  171. const models = require("../models/main_models");
  172. const { about, blob, friend, meta, post, vote } = models({
  173. cooler,
  174. isPublic: config.public,
  175. });
  176. const { handleBlobUpload } = require('../backend/blobHandler.js');
  177. // load plugin models (static)
  178. const exportmodeModel = require('../models/exportmode_model');
  179. const panicmodeModel = require('../models/panicmode_model');
  180. const cipherModel = require('../models/cipher_model');
  181. const legacyModel = require('../models/legacy_model');
  182. const walletModel = require('../models/wallet_model')
  183. // load plugin models (cooler)
  184. const pmModel = require('../models/pm_model')({ cooler, isPublic: config.public });
  185. const bookmarksModel = require("../models/bookmarking_model")({ cooler, isPublic: config.public });
  186. const opinionsModel = require('../models/opinions_model')({ cooler, isPublic: config.public });
  187. const eventsModel = require('../models/events_model')({ cooler, isPublic: config.public });
  188. const tasksModel = require('../models/tasks_model')({ cooler, isPublic: config.public });
  189. const votesModel = require('../models/votes_model')({ cooler, isPublic: config.public });
  190. const reportsModel = require('../models/reports_model')({ cooler, isPublic: config.public });
  191. const transfersModel = require('../models/transfers_model')({ cooler, isPublic: config.public });
  192. const tagsModel = require('../models/tags_model')({ cooler, isPublic: config.public });
  193. const cvModel = require('../models/cv_model')({ cooler, isPublic: config.public });
  194. const inhabitantsModel = require('../models/inhabitants_model')({ cooler, isPublic: config.public });
  195. const feedModel = require('../models/feed_model')({ cooler, isPublic: config.public });
  196. const imagesModel = require("../models/images_model")({ cooler, isPublic: config.public });
  197. const audiosModel = require("../models/audios_model")({ cooler, isPublic: config.public });
  198. const videosModel = require("../models/videos_model")({ cooler, isPublic: config.public });
  199. const documentsModel = require("../models/documents_model")({ cooler, isPublic: config.public });
  200. const agendaModel = require("../models/agenda_model")({ cooler, isPublic: config.public });
  201. const trendingModel = require('../models/trending_model')({ cooler, isPublic: config.public });
  202. const statsModel = require('../models/stats_model')({ cooler, isPublic: config.public });
  203. const tribesModel = require('../models/tribes_model')({ cooler, isPublic: config.public });
  204. const searchModel = require('../models/search_model')({ cooler, isPublic: config.public });
  205. const activityModel = require('../models/activity_model')({ cooler, isPublic: config.public });
  206. const pixeliaModel = require('../models/pixelia_model')({ cooler, isPublic: config.public });
  207. const marketModel = require('../models/market_model')({ cooler, isPublic: config.public });
  208. const forumModel = require('../models/forum_model')({ cooler, isPublic: config.public });
  209. const blockchainModel = require('../models/blockchain_model')({ cooler, isPublic: config.public });
  210. const jobsModel = require('../models/jobs_model')({ cooler, isPublic: config.public });
  211. const projectsModel = require("../models/projects_model")({ cooler, isPublic: config.public });
  212. const bankingModel = require("../models/banking_model")({ services: { cooler }, isPublic: config.public })
  213. // starting warmup
  214. about._startNameWarmup();
  215. async function renderBlobMarkdown(text, mentions = {}, myFeedId, myUsername) {
  216. if (!text) return '';
  217. const mentionByFeed = {};
  218. Object.values(mentions).forEach(arr => {
  219. arr.forEach(m => {
  220. mentionByFeed[m.feed] = m;
  221. });
  222. });
  223. text = text.replace(/\[@([^\]]+)\]\(([^)]+)\)/g, (_, name, id) => {
  224. return `<a class="mention" href="/author/${encodeURIComponent(id)}">@${myUsername}</a>`;
  225. });
  226. const mentionRegex = /@([A-Za-z0-9_\-\.+=\/]+\.ed25519)/g;
  227. const words = text.split(' ');
  228. text = (await Promise.all(
  229. words.map(async (word) => {
  230. const match = mentionRegex.exec(word);
  231. if (match && match[1]) {
  232. const feedId = match[1];
  233. if (feedId === myFeedId) {
  234. return `<a class="mention" href="/author/${encodeURIComponent(feedId)}">@${myUsername}</a>`;
  235. }
  236. }
  237. return word;
  238. })
  239. )).join(' ');
  240. text = text
  241. .replace(/!\[image:[^\]]+\]\(([^)]+)\)/g, (_, id) =>
  242. `<img src="/blob/${encodeURIComponent(id)}" alt="image" class="post-image" />`)
  243. .replace(/\[audio:[^\]]+\]\(([^)]+)\)/g, (_, id) =>
  244. `<audio controls class="post-audio" src="/blob/${encodeURIComponent(id)}"></audio>`)
  245. .replace(/\[video:[^\]]+\]\(([^)]+)\)/g, (_, id) =>
  246. `<video controls class="post-video" src="/blob/${encodeURIComponent(id)}"></video>`)
  247. .replace(/\[pdf:[^\]]+\]\(([^)]+)\)/g, (_, id) =>
  248. `<a class="post-pdf" href="/blob/${encodeURIComponent(id)}" target="_blank">PDF</a>`);
  249. return text;
  250. }
  251. let formattedTextCache = null;
  252. const ADDR_PATH = path.join(__dirname, "..", "configs", "wallet-addresses.json");
  253. function readAddrMap() {
  254. try { return JSON.parse(fs.readFileSync(ADDR_PATH, "utf8")); } catch { return {}; }
  255. }
  256. function writeAddrMap(map) {
  257. fs.mkdirSync(path.dirname(ADDR_PATH), { recursive: true });
  258. fs.writeFileSync(ADDR_PATH, JSON.stringify(map, null, 2));
  259. }
  260. const preparePreview = async function (ctx) {
  261. let text = String(ctx.request.body.text || "");
  262. const mentions = {};
  263. const rex = /(^|\s)(?!\[)@([a-zA-Z0-9\-/.=+]{3,})\b/g;
  264. let m;
  265. while ((m = rex.exec(text)) !== null) {
  266. const token = m[2];
  267. const key = token;
  268. let found = mentions[key] || [];
  269. if (/\.ed25519$/.test(token)) {
  270. const name = await about.name(token);
  271. const img = await about.image(token);
  272. found.push({
  273. feed: token,
  274. name,
  275. img,
  276. rel: { followsMe: false, following: false, blocking: false, me: false }
  277. });
  278. } else {
  279. const matches = about.named(token);
  280. for (const match of matches) {
  281. found.push(match);
  282. }
  283. }
  284. if (found.length > 0) {
  285. mentions[key] = found;
  286. }
  287. }
  288. Object.keys(mentions).forEach((key) => {
  289. let matches = mentions[key];
  290. const meaningful = matches.filter((m) => (m.rel?.followsMe || m.rel?.following) && !m.rel?.blocking);
  291. mentions[key] = meaningful.length > 0 ? meaningful : matches;
  292. });
  293. const replacer = (match, prefix, token) => {
  294. const matches = mentions[token];
  295. if (matches && matches.length === 1) {
  296. return `${prefix}[@${matches[0].name}](${matches[0].feed})`;
  297. }
  298. return match;
  299. };
  300. text = text.replace(rex, replacer);
  301. const blobMarkdown = await handleBlobUpload(ctx, "blob");
  302. if (blobMarkdown) {
  303. text += blobMarkdown;
  304. }
  305. const ssbClient = await cooler.open();
  306. const authorMeta = {
  307. id: ssbClient.id,
  308. name: await about.name(ssbClient.id),
  309. image: await about.image(ssbClient.id),
  310. };
  311. const renderedText = await renderBlobMarkdown(text, mentions, authorMeta.id, authorMeta.name);
  312. const hasBrTags = /<br\s*\/?>/.test(renderedText);
  313. const formattedText = formattedTextCache || (!hasBrTags ? renderedText.replace(/\n/g, '<br>') : renderedText);
  314. if (!formattedTextCache && !hasBrTags) {
  315. formattedTextCache = formattedText;
  316. }
  317. const contentWarning = ctx.request.body.contentWarning || '';
  318. let finalContent = formattedText;
  319. if (contentWarning && !finalContent.startsWith(contentWarning)) {
  320. finalContent = `<br>${finalContent}`;
  321. }
  322. return { authorMeta, text: renderedText, formattedText: finalContent, mentions };
  323. };
  324. // set koaMiddleware maxSize: 50 MiB (voted by community at: 09/04/2025)
  325. const megabyte = Math.pow(2, 20);
  326. const maxSize = 50 * megabyte;
  327. // koaMiddleware to manage files
  328. const homeDir = os.homedir();
  329. const blobsPath = path.join(homeDir, '.ssb', 'blobs', 'tmp');
  330. const gossipPath = path.join(homeDir, '.ssb', 'gossip.json');
  331. const unfollowedPath = path.join(homeDir, '.ssb', 'gossip_unfollowed.json');
  332. function ensureJSONFile(p, initial = []) {
  333. fs.mkdirSync(path.dirname(p), { recursive: true });
  334. if (!fs.existsSync(p)) fs.writeFileSync(p, JSON.stringify(initial, null, 2), 'utf8');
  335. }
  336. function readJSON(p) {
  337. ensureJSONFile(p, []);
  338. try { return JSON.parse(fs.readFileSync(p, 'utf8') || '[]') } catch { return [] }
  339. }
  340. function writeJSON(p, data) {
  341. fs.mkdirSync(path.dirname(p), { recursive: true });
  342. fs.writeFileSync(p, JSON.stringify(data, null, 2), 'utf8');
  343. }
  344. function canonicalKey(key) {
  345. let core = String(key).replace(/^@/, '').replace(/\.ed25519$/, '').replace(/-/g, '+').replace(/_/g, '/');
  346. if (!core.endsWith('=')) core += '=';
  347. return `@${core}.ed25519`;
  348. }
  349. function msAddrFrom(host, port, key) {
  350. const core = canonicalKey(key).replace(/^@/, '').replace(/\.ed25519$/, '');
  351. return `net:${host}:${Number(port) || 8008}~shs:${core}`;
  352. }
  353. ensureJSONFile(gossipPath, []);
  354. ensureJSONFile(unfollowedPath, []);
  355. const koaBodyMiddleware = koaBody({
  356. multipart: true,
  357. formidable: {
  358. uploadDir: blobsPath,
  359. keepExtensions: true,
  360. maxFieldsSize: maxSize,
  361. hash: 'sha256',
  362. },
  363. parsedMethods: ['POST'],
  364. });
  365. const resolveCommentComponents = async function (ctx) {
  366. let parentId;
  367. try {
  368. parentId = decodeURIComponent(ctx.params.message);
  369. } catch {
  370. parentId = ctx.params.message;
  371. }
  372. const parentMessage = await post.get(parentId);
  373. if (!parentMessage || !parentMessage.value) {
  374. throw new Error("Invalid parentMessage or missing 'value'");
  375. }
  376. const myFeedId = await meta.myFeedId();
  377. const hasRoot =
  378. typeof parentMessage?.value?.content?.root === "string" &&
  379. ssbRef.isMsg(parentMessage.value.content.root);
  380. const hasFork =
  381. typeof parentMessage?.value?.content?.fork === "string" &&
  382. ssbRef.isMsg(parentMessage.value.content.fork);
  383. const rootMessage = hasRoot
  384. ? hasFork
  385. ? parentMessage
  386. : await post.get(parentMessage.value.content.root)
  387. : parentMessage;
  388. const messages = await post.topicComments(rootMessage.key);
  389. messages.push(rootMessage);
  390. let contentWarning;
  391. if (ctx.request.body) {
  392. const rawContentWarning = String(ctx.request.body.contentWarning || "").trim();
  393. contentWarning = rawContentWarning.length > 0 ? rawContentWarning : undefined;
  394. }
  395. return { messages, myFeedId, parentMessage, contentWarning };
  396. };
  397. // import views (core)
  398. 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");
  399. // import views (modules)
  400. const { activityView } = require("../views/activity_view");
  401. const { cvView, createCVView } = require("../views/cv_view");
  402. const { indexingView } = require("../views/indexing_view");
  403. const { pixeliaView } = require("../views/pixelia_view");
  404. const { statsView } = require("../views/stats_view");
  405. const { tribesView, tribeDetailView, tribesInvitesView, tribeView, renderInvitePage } = require("../views/tribes_view");
  406. const { agendaView } = require("../views/agenda_view");
  407. const { documentView, singleDocumentView } = require("../views/document_view");
  408. const { inhabitantsView, inhabitantsProfileView } = require("../views/inhabitants_view");
  409. const { walletViewRender, walletView, walletHistoryView, walletReceiveView, walletSendFormView, walletSendConfirmView, walletSendResultView, walletErrorView } = require("../views/wallet_view");
  410. const { pmView } = require("../views/pm_view");
  411. const { tagsView } = require("../views/tags_view");
  412. const { videoView, singleVideoView } = require("../views/video_view");
  413. const { audioView, singleAudioView } = require("../views/audio_view");
  414. const { eventView, singleEventView } = require("../views/event_view");
  415. const { invitesView } = require("../views/invites_view");
  416. const { modulesView } = require("../views/modules_view");
  417. const { reportView, singleReportView } = require("../views/report_view");
  418. const { taskView, singleTaskView } = require("../views/task_view");
  419. const { voteView } = require("../views/vote_view");
  420. const { bookmarkView, singleBookmarkView } = require("../views/bookmark_view");
  421. const { feedView, feedCreateView } = require("../views/feed_view");
  422. const { legacyView } = require("../views/legacy_view");
  423. const { opinionsView } = require("../views/opinions_view");
  424. const { peersView } = require("../views/peers_view");
  425. const { searchView } = require("../views/search_view");
  426. const { transferView, singleTransferView } = require("../views/transfer_view");
  427. const { cipherView } = require("../views/cipher_view");
  428. const { imageView, singleImageView } = require("../views/image_view");
  429. const { settingsView } = require("../views/settings_view");
  430. const { trendingView } = require("../views/trending_view");
  431. const { marketView, singleMarketView } = require("../views/market_view");
  432. const { aiView } = require("../views/AI_view");
  433. const { forumView, singleForumView } = require("../views/forum_view");
  434. const { renderBlockchainView, renderSingleBlockView } = require("../views/blockchain_view");
  435. const { jobsView, singleJobsView, renderJobForm } = require("../views/jobs_view");
  436. const { projectsView, singleProjectView } = require("../views/projects_view")
  437. const { renderBankingView, renderSingleAllocationView, renderEpochView } = require("../views/banking_views")
  438. let sharp;
  439. try {
  440. sharp = require("sharp");
  441. } catch (e) {
  442. // Optional dependency
  443. }
  444. const readmePath = path.join(__dirname, "..", ".." ,"README.md");
  445. const packagePath = path.join(__dirname, "..", "server", "package.json");
  446. const readme = fs.readFileSync(readmePath, "utf8");
  447. const version = JSON.parse(fs.readFileSync(packagePath, "utf8")).version;
  448. const nullImageId = '&0000000000000000000000000000000000000000000=.sha256';
  449. const getAvatarUrl = (image) => {
  450. if (!image || image === nullImageId) {
  451. return '/assets/images/default-avatar.png';
  452. }
  453. return `/image/256/${encodeURIComponent(image)}`;
  454. };
  455. router
  456. .param("imageSize", (imageSize, ctx, next) => {
  457. const size = Number(imageSize);
  458. const isInteger = size % 1 === 0;
  459. const overMinSize = size > 2;
  460. const underMaxSize = size <= 256;
  461. ctx.assert(
  462. isInteger && overMinSize && underMaxSize,
  463. 400,
  464. "Invalid image size"
  465. );
  466. return next();
  467. })
  468. .param("blobId", (blobId, ctx, next) => {
  469. ctx.assert(ssbRef.isBlob(blobId), 400, "Invalid blob link");
  470. return next();
  471. })
  472. .param("message", (message, ctx, next) => {
  473. ctx.assert(ssbRef.isMsg(message), 400, "Invalid message link");
  474. return next();
  475. })
  476. .param("feed", (message, ctx, next) => {
  477. ctx.assert(ssbRef.isFeedId(message), 400, "Invalid feed link");
  478. return next();
  479. })
  480. //GET backend routes
  481. .get("/", async (ctx) => {
  482. ctx.redirect("/activity"); // default view when starting Oasis
  483. })
  484. .get("/robots.txt", (ctx) => {
  485. ctx.body = "User-agent: *\nDisallow: /";
  486. })
  487. .get(oasisCheckPath, (ctx) => {
  488. ctx.body = "oasis";
  489. })
  490. .get('/stats', async (ctx) => {
  491. const filter = ctx.query.filter || 'ALL';
  492. const stats = await statsModel.getStats(filter);
  493. const myId = SSBconfig.config.keys.id;
  494. const myAddress = await bankingModel.getUserAddress(myId);
  495. const addrRows = await bankingModel.listAddressesMerged();
  496. stats.banking = {
  497. myAddress: myAddress || null,
  498. totalAddresses: Array.isArray(addrRows) ? addrRows.length : 0
  499. };
  500. ctx.body = statsView(stats, filter);
  501. })
  502. .get("/public/popular/:period", async (ctx) => {
  503. const { period } = ctx.params;
  504. const popularMod = ctx.cookies.get("popularMod") || 'on';
  505. if (popularMod !== 'on') {
  506. ctx.redirect('/modules');
  507. return;
  508. }
  509. const i18n = require("../client/assets/translations/i18n");
  510. const lang = ctx.cookies.get('lang') || 'en';
  511. const translations = i18n[lang] || i18n['en'];
  512. const publicPopular = async ({ period }) => {
  513. const messages = await post.popular({ period });
  514. const prefix = nav(
  515. div({ class: "filters" },
  516. ul(
  517. li(
  518. form({ method: "GET", action: "/public/popular/day" },
  519. button({ type: "submit", class: "filter-btn" }, translations.day)
  520. )
  521. ),
  522. li(
  523. form({ method: "GET", action: "/public/popular/week" },
  524. button({ type: "submit", class: "filter-btn" }, translations.week)
  525. )
  526. ),
  527. li(
  528. form({ method: "GET", action: "/public/popular/month" },
  529. button({ type: "submit", class: "filter-btn" }, translations.month)
  530. )
  531. ),
  532. li(
  533. form({ method: "GET", action: "/public/popular/year" },
  534. button({ type: "submit", class: "filter-btn" }, translations.year)
  535. )
  536. )
  537. )
  538. )
  539. );
  540. return popularView({
  541. messages,
  542. prefix,
  543. });
  544. };
  545. ctx.body = await publicPopular({ period });
  546. })
  547. // modules
  548. .get("/modules", async (ctx) => {
  549. const configMods = getConfig().modules;
  550. const modules = [
  551. 'popular', 'topics', 'summaries', 'latest', 'threads', 'multiverse', 'invites', 'wallet',
  552. 'legacy', 'cipher', 'bookmarks', 'videos', 'docs', 'audios', 'tags', 'images', 'trending',
  553. 'events', 'tasks', 'market', 'tribes', 'governance', 'reports', 'opinions', 'transfers',
  554. 'feed', 'pixelia', 'agenda', 'ai', 'forum', 'jobs', 'projects', 'banking'
  555. ];
  556. const moduleStates = modules.reduce((acc, mod) => {
  557. acc[`${mod}Mod`] = configMods[`${mod}Mod`];
  558. return acc;
  559. }, {});
  560. ctx.body = modulesView(moduleStates);
  561. })
  562. // AI
  563. .get('/ai', async (ctx) => {
  564. const aiMod = ctx.cookies.get('aiMod') || 'on';
  565. if (aiMod !== 'on') {
  566. ctx.redirect('/modules');
  567. return;
  568. }
  569. startAI();
  570. const i18nAll = require('../client/assets/translations/i18n');
  571. const lang = ctx.cookies.get('lang') || 'en';
  572. const translations = i18nAll[lang] || i18nAll['en'];
  573. const { setLanguage } = require('../views/main_views');
  574. setLanguage(lang);
  575. const historyPath = path.join(__dirname, '..', '..', 'src', 'configs', 'AI-history.json');
  576. let chatHistory = [];
  577. try {
  578. const fileData = fs.readFileSync(historyPath, 'utf-8');
  579. chatHistory = JSON.parse(fileData);
  580. } catch {}
  581. const config = getConfig();
  582. const userPrompt = config.ai?.prompt?.trim() || '';
  583. ctx.body = aiView(chatHistory, userPrompt);
  584. })
  585. // pixelArt
  586. .get('/pixelia', async (ctx) => {
  587. const pixeliaMod = ctx.cookies.get("pixeliaMod") || 'on';
  588. if (pixeliaMod !== 'on') {
  589. ctx.redirect('/modules');
  590. return;
  591. }
  592. const pixelArt = await pixeliaModel.listPixels();
  593. ctx.body = pixeliaView(pixelArt);
  594. })
  595. // blockexplorer
  596. .get('/blockexplorer', async (ctx) => {
  597. const userId = SSBconfig.config.keys.id;
  598. const query = ctx.query;
  599. const filter = query.filter || 'recent';
  600. const blockchainData = await blockchainModel.listBlockchain(filter, userId);
  601. ctx.body = renderBlockchainView(blockchainData, filter, userId);
  602. })
  603. .get('/blockexplorer/block/:id', async (ctx) => {
  604. const blockId = ctx.params.id;
  605. const block = await blockchainModel.getBlockById(blockId);
  606. ctx.body = renderSingleBlockView(block);
  607. })
  608. .get("/public/latest", async (ctx) => {
  609. const latestMod = ctx.cookies.get("latestMod") || 'on';
  610. if (latestMod !== 'on') {
  611. ctx.redirect('/modules');
  612. return;
  613. }
  614. const messages = await post.latest();
  615. ctx.body = await latestView({ messages });
  616. })
  617. .get("/public/latest/extended", async (ctx) => {
  618. const extendedMod = ctx.cookies.get("extendedMod") || 'on';
  619. if (extendedMod !== 'on') {
  620. ctx.redirect('/modules');
  621. return;
  622. }
  623. const messages = await post.latestExtended();
  624. ctx.body = await extendedView({ messages });
  625. })
  626. .get("/public/latest/topics", async (ctx) => {
  627. const topicsMod = ctx.cookies.get("topicsMod") || 'on';
  628. if (topicsMod !== 'on') {
  629. ctx.redirect('/modules');
  630. return;
  631. }
  632. const messages = await post.latestTopics();
  633. const channels = await post.channels();
  634. const list = channels.map((c) => {
  635. return li(a({ href: `/hashtag/${c}` }, `#${c}`));
  636. });
  637. const prefix = nav(ul(list));
  638. ctx.body = await topicsView({ messages, prefix });
  639. })
  640. .get("/public/latest/summaries", async (ctx) => {
  641. const summariesMod = ctx.cookies.get("summariesMod") || 'on';
  642. if (summariesMod !== 'on') {
  643. ctx.redirect('/modules');
  644. return;
  645. }
  646. const messages = await post.latestSummaries();
  647. ctx.body = await summaryView({ messages });
  648. })
  649. .get("/public/latest/threads", async (ctx) => {
  650. const threadsMod = ctx.cookies.get("threadsMod") || 'on';
  651. if (threadsMod !== 'on') {
  652. ctx.redirect('/modules');
  653. return;
  654. }
  655. const messages = await post.latestThreads();
  656. ctx.body = await threadsView({ messages });
  657. })
  658. .get('/author/:feed', async (ctx) => {
  659. const feedId = decodeURIComponent(ctx.params.feed || '');
  660. const gt = Number(ctx.request.query.gt || -1);
  661. const lt = Number(ctx.request.query.lt || -1);
  662. if (lt > 0 && gt > 0 && gt >= lt) throw new Error('Given search range is empty');
  663. const description = await about.description(feedId);
  664. const name = await about.name(feedId);
  665. const image = await about.image(feedId);
  666. const messages = await post.fromPublicFeed(feedId, gt, lt);
  667. const firstPost = await post.firstBy(feedId);
  668. const lastPost = await post.latestBy(feedId);
  669. const relationship = await friend.getRelationship(feedId);
  670. const avatarUrl = getAvatarUrl(image);
  671. const ecoAddress = await bankingModel.getUserAddress(feedId);
  672. const { ecoValue, karmaScore } = await bankingModel.getBankingData(feedId);
  673. ctx.body = authorView({
  674. feedId,
  675. messages,
  676. firstPost,
  677. lastPost,
  678. name,
  679. description,
  680. avatarUrl,
  681. relationship,
  682. ecoAddress,
  683. karmaScore
  684. });
  685. })
  686. .get("/search", async (ctx) => {
  687. const query = ctx.query.query || '';
  688. if (!query) {
  689. return ctx.body = await searchView({ messages: [], query, types: [] });
  690. }
  691. const results = await searchModel.search({ query, types: [] });
  692. const groupedResults = Object.entries(results).reduce((acc, [type, msgs]) => {
  693. acc[type] = msgs.map(msg => {
  694. if (!msg.value || !msg.value.content) {
  695. return {};
  696. }
  697. return {
  698. ...msg,
  699. content: msg.value.content,
  700. author: msg.value.content.author || 'Unknown',
  701. };
  702. });
  703. return acc;
  704. }, {});
  705. ctx.body = await searchView({ results: groupedResults, query, types: [] });
  706. })
  707. .get('/images', async (ctx) => {
  708. const imagesMod = ctx.cookies.get("imagesMod") || 'on';
  709. if (imagesMod !== 'on') {
  710. ctx.redirect('/modules');
  711. return;
  712. }
  713. const filter = ctx.query.filter || 'all'
  714. const images = await imagesModel.listAll(filter);
  715. ctx.body = await imageView(images, filter, null);
  716. })
  717. .get('/images/edit/:id', async ctx => {
  718. const imageId = ctx.params.id;
  719. const images = await imagesModel.listAll('all');
  720. ctx.body = await imageView(images, 'edit', imageId);
  721. })
  722. .get('/images/:imageId', async ctx => {
  723. const imageId = ctx.params.imageId;
  724. const filter = ctx.query.filter || 'all';
  725. const image = await imagesModel.getImageById(imageId);
  726. ctx.body = await singleImageView(image, filter);
  727. })
  728. .get('/audios', async (ctx) => {
  729. const audiosMod = ctx.cookies.get("audiosMod") || 'on';
  730. if (audiosMod !== 'on') {
  731. ctx.redirect('/modules');
  732. return;
  733. }
  734. const filter = ctx.query.filter || 'all';
  735. const audios = await audiosModel.listAll(filter);
  736. ctx.body = await audioView(audios, filter, null);
  737. })
  738. .get('/audios/edit/:id', async (ctx) => {
  739. const audiosMod = ctx.cookies.get("audiosMod") || 'on';
  740. if (audiosMod !== 'on') {
  741. ctx.redirect('/modules');
  742. return;
  743. }
  744. const audio = await audiosModel.getAudioById(ctx.params.id);
  745. ctx.body = await audioView([audio], 'edit', ctx.params.id);
  746. })
  747. .get('/audios/:audioId', async ctx => {
  748. const audioId = ctx.params.audioId;
  749. const filter = ctx.query.filter || 'all';
  750. const audio = await audiosModel.getAudioById(audioId);
  751. ctx.body = await singleAudioView(audio, filter);
  752. })
  753. .get('/videos', async (ctx) => {
  754. const filter = ctx.query.filter || 'all';
  755. const videos = await videosModel.listAll(filter);
  756. ctx.body = await videoView(videos, filter, null);
  757. })
  758. .get('/videos/edit/:id', async (ctx) => {
  759. const video = await videosModel.getVideoById(ctx.params.id);
  760. ctx.body = await videoView([video], 'edit', ctx.params.id);
  761. })
  762. .get('/videos/:videoId', async ctx => {
  763. const videoId = ctx.params.videoId;
  764. const filter = ctx.query.filter || 'all';
  765. const video = await videosModel.getVideoById(videoId);
  766. ctx.body = await singleVideoView(video, filter);
  767. })
  768. .get('/documents', async (ctx) => {
  769. const filter = ctx.query.filter || 'all';
  770. const documents = await documentsModel.listAll(filter);
  771. ctx.body = await documentView(documents, filter, null);
  772. })
  773. .get('/documents/edit/:id', async (ctx) => {
  774. const document = await documentsModel.getDocumentById(ctx.params.id);
  775. ctx.body = await documentView([document], 'edit', ctx.params.id);
  776. })
  777. .get('/documents/:documentId', async ctx => {
  778. const documentId = ctx.params.documentId;
  779. const filter = ctx.query.filter || 'all';
  780. const document = await documentsModel.getDocumentById(documentId);
  781. ctx.body = await singleDocumentView(document, filter);
  782. })
  783. .get('/cv', async ctx => {
  784. const cv = await cvModel.getCVByUserId()
  785. ctx.body = await cvView(cv)
  786. })
  787. .get('/cv/create', async ctx => {
  788. ctx.body = await createCVView()
  789. })
  790. .get('/cv/edit/:id', async ctx => {
  791. const cv = await cvModel.getCVByUserId()
  792. ctx.body = await createCVView(cv, true)
  793. })
  794. .get('/pm', async ctx => {
  795. const { recipients = '', subject = '', quote = '', preview = '' } = ctx.query;
  796. const quoted = quote ? quote.split('\n').map(l => '> ' + l).join('\n') + '\n\n' : '';
  797. const showPreview = preview === '1';
  798. ctx.body = await pmView(recipients, subject, quoted, showPreview);
  799. })
  800. .get('/inbox', async ctx => {
  801. const inboxMod = ctx.cookies.get('inboxMod') || 'on';
  802. if (inboxMod !== 'on') { ctx.redirect('/modules'); return; }
  803. const messages = await pmModel.listAllPrivate();
  804. ctx.body = await privateView({ messages }, ctx.query.filter || undefined);
  805. })
  806. .get('/tags', async ctx => {
  807. const filter = ctx.query.filter || 'all'
  808. const tags = await tagsModel.listTags(filter)
  809. ctx.body = await tagsView(tags, filter)
  810. })
  811. .get('/reports', async ctx => {
  812. const filter = ctx.query.filter || 'all';
  813. const reports = await reportsModel.listAll(filter);
  814. ctx.body = await reportView(reports, filter, null);
  815. })
  816. .get('/reports/edit/:id', async ctx => {
  817. const report = await reportsModel.getReportById(ctx.params.id);
  818. ctx.body = await reportView([report], 'edit', ctx.params.id);
  819. })
  820. .get('/reports/:reportId', async ctx => {
  821. const reportId = ctx.params.reportId;
  822. const filter = ctx.query.filter || 'all';
  823. const report = await reportsModel.getReportById(reportId, filter);
  824. ctx.body = await singleReportView(report, filter);
  825. })
  826. .get('/trending', async (ctx) => {
  827. const filter = ctx.query.filter || 'RECENT';
  828. const trendingItems = await trendingModel.listTrending(filter);
  829. const items = trendingItems.filtered || [];
  830. const categories = trendingModel.categories;
  831. ctx.body = await trendingView(items, filter, categories);
  832. })
  833. .get('/agenda', async (ctx) => {
  834. const filter = ctx.query.filter || 'all';
  835. const data = await agendaModel.listAgenda(filter);
  836. ctx.body = await agendaView(data, filter);
  837. })
  838. .get("/hashtag/:hashtag", async (ctx) => {
  839. const { hashtag } = ctx.params;
  840. const messages = await post.fromHashtag(hashtag);
  841. ctx.body = await hashtagView({ hashtag, messages });
  842. })
  843. .get('/inhabitants', async (ctx) => {
  844. const filter = ctx.query.filter || 'all';
  845. const query = {
  846. search: ctx.query.search || ''
  847. };
  848. if (['CVs', 'MATCHSKILLS'].includes(filter)) {
  849. query.location = ctx.query.location || '';
  850. query.language = ctx.query.language || '';
  851. query.skills = ctx.query.skills || '';
  852. }
  853. const userId = SSBconfig.config.keys.id;
  854. const inhabitants = await inhabitantsModel.listInhabitants({
  855. filter,
  856. ...query
  857. });
  858. const [addresses, karmaList] = await Promise.all([
  859. bankingModel.listAddressesMerged(),
  860. Promise.all(
  861. inhabitants.map(async (u) => {
  862. try {
  863. const { karmaScore } = await bankingModel.getBankingData(u.id);
  864. return { id: u.id, karmaScore: typeof karmaScore === 'number' ? karmaScore : 0 };
  865. } catch {
  866. return { id: u.id, karmaScore: 0 };
  867. }
  868. })
  869. )
  870. ]);
  871. const addrMap = new Map(addresses.map(x => [x.id, x.address]));
  872. const karmaMap = new Map(karmaList.map(x => [x.id, x.karmaScore]));
  873. let enriched = inhabitants.map(u => ({
  874. ...u,
  875. ecoAddress: addrMap.get(u.id) || null,
  876. karmaScore: karmaMap.has(u.id)
  877. ? karmaMap.get(u.id)
  878. : (typeof u.karmaScore === 'number' ? u.karmaScore : 0)
  879. }));
  880. if (filter === 'TOP KARMA') {
  881. enriched = enriched.sort((a, b) => (b.karmaScore || 0) - (a.karmaScore || 0));
  882. }
  883. ctx.body = await inhabitantsView(enriched, filter, query, userId);
  884. })
  885. .get('/inhabitant/:id', async (ctx) => {
  886. const id = ctx.params.id;
  887. const about = await inhabitantsModel.getLatestAboutById(id);
  888. const cv = await inhabitantsModel.getCVByUserId(id);
  889. const feed = await inhabitantsModel.getFeedByUserId(id);
  890. const currentUserId = SSBconfig.config.keys.id;
  891. ctx.body = await inhabitantsProfileView({ about, cv, feed }, currentUserId);
  892. })
  893. .get('/tribes', async ctx => {
  894. const filter = ctx.query.filter || 'recent';
  895. const search = ctx.query.search || '';
  896. const tribes = await tribesModel.listAll();
  897. let filteredTribes = tribes;
  898. if (search) {
  899. filteredTribes = tribes.filter(tribe =>
  900. tribe.title.toLowerCase().includes(search.toLowerCase())
  901. );
  902. }
  903. ctx.body = await tribesView(filteredTribes, filter, null, ctx.query);
  904. })
  905. .get('/tribes/create', async ctx => {
  906. ctx.body = await tribesView([], 'create', null)
  907. })
  908. .get('/tribes/edit/:id', async ctx => {
  909. const tribe = await tribesModel.getTribeById(ctx.params.id)
  910. ctx.body = await tribesView([tribe], 'edit', ctx.params.id)
  911. })
  912. .get('/tribe/:tribeId', koaBody(), async ctx => {
  913. const tribeId = ctx.params.tribeId;
  914. const tribe = await tribesModel.getTribeById(tribeId);
  915. const userId = SSBconfig.config.keys.id;
  916. const query = ctx.query;
  917. if (!query.feedFilter) {
  918. query.feedFilter = 'TOP';
  919. }
  920. if (tribe.isAnonymous === false && !tribe.members.includes(userId)) {
  921. ctx.status = 403;
  922. ctx.body = { message: 'You cannot access to this tribe!' };
  923. return;
  924. }
  925. if (!tribe.members.includes(userId)) {
  926. ctx.status = 403;
  927. ctx.body = { message: 'You cannot access to this tribe!' };
  928. return;
  929. }
  930. ctx.body = await tribeView(tribe, userId, query);
  931. })
  932. .get('/activity', async ctx => {
  933. const filter = ctx.query.filter || 'recent';
  934. const userId = SSBconfig.config.keys.id;
  935. try { await bankingModel.ensureSelfAddressPublished(); } catch (_) {}
  936. try { await bankingModel.getUserEngagementScore(userId); } catch (_) {}
  937. const actions = await activityModel.listFeed(filter);
  938. ctx.body = activityView(actions, filter, userId);
  939. })
  940. .get("/profile", async (ctx) => {
  941. const myFeedId = await meta.myFeedId()
  942. const gt = Number(ctx.request.query["gt"] || -1)
  943. const lt = Number(ctx.request.query["lt"] || -1)
  944. if (lt > 0 && gt > 0 && gt >= lt) throw new Error("Given search range is empty")
  945. const description = await about.description(myFeedId)
  946. const name = await about.name(myFeedId)
  947. const image = await about.image(myFeedId)
  948. const messages = await post.fromPublicFeed(myFeedId, gt, lt)
  949. const firstPost = await post.firstBy(myFeedId)
  950. const lastPost = await post.latestBy(myFeedId)
  951. const avatarUrl = getAvatarUrl(image)
  952. const ecoAddress = await bankingModel.getUserAddress(myFeedId)
  953. const { karmaScore, ecoValue } = await bankingModel.getBankingData(myFeedId);
  954. ctx.body = await authorView({
  955. feedId: myFeedId,
  956. messages,
  957. firstPost,
  958. lastPost,
  959. name,
  960. description,
  961. avatarUrl,
  962. relationship: { me: true },
  963. ecoAddress,
  964. karmaScore
  965. })
  966. })
  967. .get("/profile/edit", async (ctx) => {
  968. const myFeedId = await meta.myFeedId();
  969. const description = await about.description(myFeedId);
  970. const name = await about.name(myFeedId);
  971. ctx.body = await editProfileView({
  972. name,
  973. description,
  974. });
  975. })
  976. .post("/profile/edit", koaBody({ multipart: true }), async (ctx) => {
  977. const name = String(ctx.request.body.name);
  978. const description = String(ctx.request.body.description);
  979. const image = await promisesFs.readFile(ctx.request.files.image.filepath);
  980. ctx.body = await post.publishProfileEdit({
  981. name,
  982. description,
  983. image,
  984. });
  985. ctx.redirect("/profile");
  986. })
  987. .get("/publish/custom", async (ctx) => {
  988. ctx.body = await publishCustomView();
  989. })
  990. .get("/json/:message", async (ctx) => {
  991. if (config.public) {
  992. throw new Error(
  993. "Sorry, many actions are unavailable when Oasis is running in public mode. Please run Oasis in the default mode and try again."
  994. );
  995. }
  996. const { message } = ctx.params;
  997. ctx.type = "application/json";
  998. const json = async (message) => {
  999. const json = await meta.get(message);
  1000. return JSON.stringify(json, null, 2);
  1001. };
  1002. ctx.body = await json(message);
  1003. })
  1004. .get("/blob/:blobId", async (ctx) => {
  1005. const { blobId } = ctx.params;
  1006. const id = blobId.startsWith('&') ? blobId : `&${blobId}`;
  1007. const buffer = await blob.getResolved({ blobId });
  1008. let fileType;
  1009. try {
  1010. fileType = await FileType.fromBuffer(buffer);
  1011. } catch {
  1012. fileType = null;
  1013. }
  1014. let mime = fileType?.mime || "application/octet-stream";
  1015. if (mime === "application/octet-stream" && buffer.slice(0, 4).toString() === "%PDF") {
  1016. mime = "application/pdf";
  1017. }
  1018. ctx.set("Content-Type", mime);
  1019. ctx.set("Content-Disposition", `inline; filename="${blobId}"`);
  1020. ctx.set("Cache-Control", "public, max-age=31536000, immutable");
  1021. ctx.body = buffer;
  1022. })
  1023. .get("/image/:imageSize/:blobId", async (ctx) => {
  1024. const { blobId, imageSize } = ctx.params;
  1025. const size = Number(imageSize);
  1026. const fallbackPixel = Buffer.from(
  1027. "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
  1028. "base64"
  1029. );
  1030. const fakeImage = () => {
  1031. if (typeof sharp !== "function") {
  1032. return Promise.resolve(fallbackPixel);
  1033. }
  1034. return sharp({
  1035. create: {
  1036. width: size,
  1037. height: size,
  1038. channels: 4,
  1039. background: { r: 0, g: 0, b: 0, alpha: 0.5 },
  1040. },
  1041. }).png().toBuffer();
  1042. };
  1043. try {
  1044. const buffer = await blob.getResolved({ blobId });
  1045. if (!buffer) {
  1046. ctx.set("Content-Type", "image/png");
  1047. ctx.body = await fakeImage();
  1048. return;
  1049. }
  1050. const fileType = await FileType.fromBuffer(buffer);
  1051. const mimeType = fileType?.mime || "application/octet-stream";
  1052. ctx.set("Content-Type", mimeType);
  1053. if (typeof sharp === "function") {
  1054. ctx.body = await sharp(buffer)
  1055. .resize(size, size)
  1056. .png()
  1057. .toBuffer();
  1058. } else {
  1059. ctx.body = buffer;
  1060. }
  1061. } catch (err) {
  1062. ctx.set("Content-Type", "image/png");
  1063. ctx.body = await fakeImage();
  1064. }
  1065. })
  1066. .get("/settings", async (ctx) => {
  1067. const theme = ctx.cookies.get("theme") || "Dark-SNH";
  1068. const config = getConfig();
  1069. const aiPrompt = config.ai?.prompt || "";
  1070. const pubWalletUrl = config.walletPub?.url || '';
  1071. const pubWalletUser = config.walletPub?.user || '';
  1072. const pubWalletPass = config.walletPub?.pass || '';
  1073. const getMeta = async ({ theme, aiPrompt, pubWalletUrl, pubWalletUser, pubWalletPass }) => {
  1074. return settingsView({
  1075. theme,
  1076. version: version.toString(),
  1077. aiPrompt,
  1078. pubWalletUrl,
  1079. pubWalletUser,
  1080. pubWalletPass
  1081. });
  1082. };
  1083. ctx.body = await getMeta({
  1084. theme,
  1085. aiPrompt,
  1086. pubWalletUrl,
  1087. pubWalletUser,
  1088. pubWalletPass
  1089. });
  1090. })
  1091. .get("/peers", async (ctx) => {
  1092. const { discoveredPeers, unknownPeers } = await meta.discovered();
  1093. const onlinePeers = await meta.onlinePeers();
  1094. ctx.body = await peersView({
  1095. onlinePeers,
  1096. discoveredPeers,
  1097. unknownPeers
  1098. });
  1099. })
  1100. .get("/invites", async (ctx) => {
  1101. const theme = ctx.cookies.get("theme") || config.theme;
  1102. const invitesMod = ctx.cookies.get("invitesMod") || 'on';
  1103. if (invitesMod !== 'on') {
  1104. ctx.redirect('/modules');
  1105. return;
  1106. }
  1107. const getMeta = async ({ theme }) => {
  1108. return invitesView({});
  1109. };
  1110. ctx.body = await getMeta({ theme });
  1111. })
  1112. .get("/likes/:feed", async (ctx) => {
  1113. const { feed } = ctx.params;
  1114. const likes = async ({ feed }) => {
  1115. const pendingMessages = post.likes({ feed });
  1116. const pendingName = about.name(feed);
  1117. return likesView({
  1118. messages: await pendingMessages,
  1119. feed,
  1120. name: await pendingName,
  1121. });
  1122. };
  1123. ctx.body = await likes({ feed });
  1124. })
  1125. .get("/mentions", async (ctx) => {
  1126. const { messages, myFeedId } = await post.mentionsMe();
  1127. ctx.body = await mentionsView({ messages, myFeedId });
  1128. })
  1129. .get('/opinions', async (ctx) => {
  1130. const filter = ctx.query.filter || 'RECENT';
  1131. const opinions = await opinionsModel.listOpinions(filter);
  1132. ctx.body = await opinionsView(opinions, filter);
  1133. })
  1134. .get('/feed', async ctx => {
  1135. const filter = ctx.query.filter || 'ALL';
  1136. const feeds = await feedModel.listFeeds(filter);
  1137. ctx.body = feedView(feeds, filter);
  1138. })
  1139. .get('/feed/create', async ctx => {
  1140. ctx.body = feedCreateView();
  1141. })
  1142. .get('/forum', async ctx => {
  1143. const forumMod = ctx.cookies.get("forumMod") || 'on';
  1144. if (forumMod !== 'on') {
  1145. ctx.redirect('/modules');
  1146. return;
  1147. }
  1148. const filter = ctx.query.filter || 'hot';
  1149. const forums = await forumModel.listAll(filter);
  1150. ctx.body = await forumView(forums, filter);
  1151. })
  1152. .get('/forum/:forumId', async ctx => {
  1153. const rawId = ctx.params.forumId
  1154. const msg = await forumModel.getMessageById(rawId)
  1155. const isReply = Boolean(msg.root)
  1156. const forumId = isReply ? msg.root : rawId
  1157. const highlightCommentId = isReply ? rawId : null
  1158. const forum = await forumModel.getForumById(forumId)
  1159. const messagesData = await forumModel.getMessagesByForumId(forumId)
  1160. ctx.body = await singleForumView(
  1161. forum,
  1162. messagesData,
  1163. ctx.query.filter,
  1164. highlightCommentId
  1165. )
  1166. })
  1167. .get('/legacy', async (ctx) => {
  1168. const legacyMod = ctx.cookies.get("legacyMod") || 'on';
  1169. if (legacyMod !== 'on') {
  1170. ctx.redirect('/modules');
  1171. return;
  1172. }
  1173. try {
  1174. ctx.body = await legacyView();
  1175. } catch (error) {
  1176. ctx.body = { error: error.message };
  1177. }
  1178. })
  1179. .get('/bookmarks', async (ctx) => {
  1180. const bookmarksMod = ctx.cookies.get("bookmarksMod") || 'on';
  1181. if (bookmarksMod !== 'on') {
  1182. ctx.redirect('/modules');
  1183. return;
  1184. }
  1185. const filter = ctx.query.filter || 'all';
  1186. const bookmarks = await bookmarksModel.listAll(null, filter);
  1187. ctx.body = await bookmarkView(bookmarks, filter, null);
  1188. })
  1189. .get('/bookmarks/edit/:id', async (ctx) => {
  1190. const bookmarksMod = ctx.cookies.get("bookmarksMod") || 'on';
  1191. if (bookmarksMod !== 'on') {
  1192. ctx.redirect('/modules');
  1193. return;
  1194. }
  1195. const bookmarkId = ctx.params.id;
  1196. const bookmark = await bookmarksModel.getBookmarkById(bookmarkId);
  1197. if (bookmark.opinions_inhabitants && bookmark.opinions_inhabitants.length > 0) {
  1198. ctx.flash = { message: "This bookmark has received votes and cannot be updated." };
  1199. ctx.redirect(`/bookmarks?filter=mine`);
  1200. }
  1201. ctx.body = await bookmarkView([bookmark], 'edit', bookmarkId);
  1202. })
  1203. .get('/bookmarks/:bookmarkId', async ctx => {
  1204. const bookmarkId = ctx.params.bookmarkId;
  1205. const filter = ctx.query.filter || 'all';
  1206. const bookmark = await bookmarksModel.getBookmarkById(bookmarkId);
  1207. ctx.body = await singleBookmarkView(bookmark, filter);
  1208. })
  1209. .get('/tasks', async ctx=>{
  1210. const filter = ctx.query.filter||'all';
  1211. const tasks = await tasksModel.listAll(filter);
  1212. ctx.body = await taskView(tasks,filter,null);
  1213. })
  1214. .get('/tasks/edit/:id', async ctx=>{
  1215. const id = ctx.params.id;
  1216. const task = await tasksModel.getTaskById(id);
  1217. ctx.body = await taskView(task,'edit',id);
  1218. })
  1219. .get('/tasks/:taskId', async ctx => {
  1220. const taskId = ctx.params.taskId;
  1221. const filter = ctx.query.filter || 'all';
  1222. const task = await tasksModel.getTaskById(taskId, filter);
  1223. ctx.body = await taskView([task], filter, taskId);
  1224. })
  1225. .get('/events', async (ctx) => {
  1226. const eventsMod = ctx.cookies.get("eventsMod") || 'on';
  1227. if (eventsMod !== 'on') {
  1228. ctx.redirect('/modules');
  1229. return;
  1230. }
  1231. const filter = ctx.query.filter || 'all';
  1232. const events = await eventsModel.listAll(null, filter);
  1233. ctx.body = await eventView(events, filter, null);
  1234. })
  1235. .get('/events/edit/:id', async (ctx) => {
  1236. const eventsMod = ctx.cookies.get("eventsMod") || 'on';
  1237. if (eventsMod !== 'on') {
  1238. ctx.redirect('/modules');
  1239. return;
  1240. }
  1241. const eventId = ctx.params.id;
  1242. const event = await eventsModel.getEventById(eventId);
  1243. ctx.body = await eventView([event], 'edit', eventId);
  1244. })
  1245. .get('/events/:eventId', async ctx => {
  1246. const eventId = ctx.params.eventId;
  1247. const filter = ctx.query.filter || 'all';
  1248. const event = await eventsModel.getEventById(eventId);
  1249. ctx.body = await singleEventView(event, filter);
  1250. })
  1251. .get('/votes', async ctx => {
  1252. const filter = ctx.query.filter || 'all';
  1253. const voteList = await votesModel.listAll(filter);
  1254. ctx.body = await voteView(voteList, filter, null);
  1255. })
  1256. .get('/votes/edit/:id', async ctx => {
  1257. const id = ctx.params.id;
  1258. const vote = await votesModel.getVoteById(id);
  1259. ctx.body = await voteView([vote], 'edit', id);
  1260. })
  1261. .get('/votes/:voteId', async ctx => {
  1262. const voteId = ctx.params.voteId;
  1263. const vote = await votesModel.getVoteById(voteId);
  1264. ctx.body = await voteView(vote);
  1265. })
  1266. .get('/market', async ctx => {
  1267. const marketMod = ctx.cookies.get("marketMod") || 'on';
  1268. if (marketMod !== 'on') {
  1269. ctx.redirect('/modules');
  1270. return;
  1271. }
  1272. const filter = ctx.query.filter || 'all';
  1273. const marketItems = await marketModel.listAllItems(filter);
  1274. ctx.body = await marketView(marketItems, filter, null);
  1275. })
  1276. .get('/market/edit/:id', async ctx => {
  1277. const id = ctx.params.id;
  1278. const marketItem = await marketModel.getItemById(id);
  1279. ctx.body = await marketView([marketItem], 'edit', marketItem);
  1280. })
  1281. .get('/market/:itemId', async ctx => {
  1282. const itemId = ctx.params.itemId;
  1283. const filter = ctx.query.filter || 'all';
  1284. const item = await marketModel.getItemById(itemId);
  1285. ctx.body = await singleMarketView(item, filter);
  1286. })
  1287. .get('/jobs', async (ctx) => {
  1288. const jobsMod = ctx.cookies.get("jobsMod") || 'on';
  1289. if (jobsMod !== 'on') {
  1290. ctx.redirect('/modules');
  1291. return;
  1292. }
  1293. const filter = ctx.query.filter || 'ALL';
  1294. const query = {
  1295. search: ctx.query.search || '',
  1296. };
  1297. if (filter === 'CV') {
  1298. query.location = ctx.query.location || '';
  1299. query.language = ctx.query.language || '';
  1300. query.skills = ctx.query.skills || '';
  1301. const inhabitants = await inhabitantsModel.listInhabitants({
  1302. filter: 'CVs',
  1303. ...query
  1304. });
  1305. ctx.body = await jobsView(inhabitants, filter, query);
  1306. return;
  1307. }
  1308. const jobs = await jobsModel.listJobs(filter, ctx.state.user?.id, query);
  1309. ctx.body = await jobsView(jobs, filter, query);
  1310. })
  1311. .get('/jobs/edit/:id', async (ctx) => {
  1312. const id = ctx.params.id;
  1313. const job = await jobsModel.getJobById(id);
  1314. ctx.body = await jobsView([job], 'EDIT');
  1315. })
  1316. .get('/jobs/:jobId', async (ctx) => {
  1317. const jobId = ctx.params.jobId;
  1318. const filter = ctx.query.filter || 'ALL';
  1319. const job = await jobsModel.getJobById(jobId);
  1320. ctx.body = await singleJobsView(job, filter);
  1321. })
  1322. .get('/projects', async (ctx) => {
  1323. const projectsMod = ctx.cookies.get("projectsMod") || 'on';
  1324. if (projectsMod !== 'on') { ctx.redirect('/modules'); return; }
  1325. const filter = ctx.query.filter || 'ALL';
  1326. if (filter === 'CREATE') {
  1327. ctx.body = await projectsView([], 'CREATE');
  1328. return;
  1329. }
  1330. const modelFilter = (filter === 'BACKERS') ? 'ALL' : filter;
  1331. let projects = await projectsModel.listProjects(modelFilter);
  1332. if (filter === 'MINE') {
  1333. const userId = SSBconfig.config.keys.id;
  1334. projects = projects.filter(project => project.author === userId);
  1335. }
  1336. ctx.body = await projectsView(projects, filter);
  1337. })
  1338. .get('/projects/edit/:id', async (ctx) => {
  1339. const id = ctx.params.id
  1340. const pr = await projectsModel.getProjectById(id)
  1341. ctx.body = await projectsView([pr], 'EDIT')
  1342. })
  1343. .get('/projects/:projectId', async (ctx) => {
  1344. const projectId = ctx.params.projectId
  1345. const filter = ctx.query.filter || 'ALL'
  1346. const project = await projectsModel.getProjectById(projectId)
  1347. ctx.body = await singleProjectView(project, filter)
  1348. })
  1349. .get("/banking", async (ctx) => {
  1350. const bankingMod = ctx.cookies.get("bankingMod") || 'on';
  1351. if (bankingMod !== 'on') {
  1352. ctx.redirect('/modules');
  1353. return;
  1354. }
  1355. const userId = SSBconfig.config.keys.id;
  1356. const query = ctx.query;
  1357. const filter = (query.filter || 'overview').toLowerCase();
  1358. const q = (query.q || '').trim();
  1359. const msg = (query.msg || '').trim();
  1360. await bankingModel.ensureSelfAddressPublished();
  1361. const data = await bankingModel.listBanking(filter, userId);
  1362. if (filter === 'addresses' && q) {
  1363. data.addresses = (data.addresses || []).filter(x =>
  1364. String(x.id).toLowerCase().includes(q.toLowerCase()) ||
  1365. String(x.address).toLowerCase().includes(q.toLowerCase())
  1366. );
  1367. data.search = q;
  1368. }
  1369. data.flash = msg || '';
  1370. const { ecoValue, inflationFactor, ecoInHours, currentSupply, isSynced } = await bankingModel.calculateEcoinValue();
  1371. data.exchange = {
  1372. ecoValue: ecoValue,
  1373. inflationFactor,
  1374. ecoInHours,
  1375. currentSupply: currentSupply,
  1376. totalSupply: 25500000,
  1377. isSynced: isSynced
  1378. };
  1379. ctx.body = renderBankingView(data, filter, userId);
  1380. })
  1381. .get("/banking/allocation/:id", async (ctx) => {
  1382. const userId = SSBconfig.config.keys.id;
  1383. const allocation = await bankingModel.getAllocationById(ctx.params.id);
  1384. ctx.body = renderSingleAllocationView(allocation, userId);
  1385. })
  1386. .get("/banking/epoch/:id", async (ctx) => {
  1387. const epoch = await bankingModel.getEpochById(ctx.params.id);
  1388. const allocations = await bankingModel.listEpochAllocations(ctx.params.id);
  1389. ctx.body = renderEpochView(epoch, allocations);
  1390. })
  1391. .get('/cipher', async (ctx) => {
  1392. const cipherMod = ctx.cookies.get("cipherMod") || 'on';
  1393. if (cipherMod !== 'on') {
  1394. ctx.redirect('/modules');
  1395. return;
  1396. }
  1397. try {
  1398. ctx.body = await cipherView();
  1399. } catch (error) {
  1400. ctx.body = { error: error.message };
  1401. }
  1402. })
  1403. .get("/thread/:message", async (ctx) => {
  1404. const { message } = ctx.params;
  1405. const thread = async (message) => {
  1406. const messages = await post.fromThread(message);
  1407. return threadView({ messages });
  1408. };
  1409. ctx.body = await thread(message);
  1410. })
  1411. .get("/subtopic/:message", async (ctx) => {
  1412. const { message } = ctx.params;
  1413. const rootMessage = await post.get(message);
  1414. const myFeedId = await meta.myFeedId();
  1415. debug("%O", rootMessage);
  1416. const messages = [rootMessage];
  1417. ctx.body = await subtopicView({ messages, myFeedId });
  1418. })
  1419. .get("/publish", async (ctx) => {
  1420. ctx.body = await publishView();
  1421. })
  1422. .get("/comment/:message", async (ctx) => {
  1423. const { messages, myFeedId, parentMessage } =
  1424. await resolveCommentComponents(ctx);
  1425. ctx.body = await commentView({ messages, myFeedId, parentMessage });
  1426. })
  1427. .get("/wallet", async (ctx) => {
  1428. const { url, user, pass } = getConfig().wallet;
  1429. const walletMod = ctx.cookies.get("walletMod") || 'on';
  1430. if (walletMod !== 'on') { ctx.redirect('/modules'); return; }
  1431. try {
  1432. const balance = await walletModel.getBalance(url, user, pass);
  1433. const address = await walletModel.getAddress(url, user, pass);
  1434. const userId = SSBconfig.config.keys.id;
  1435. if (address && typeof address === "string") {
  1436. const map = readAddrMap();
  1437. const was = map[userId];
  1438. if (was !== address) {
  1439. map[userId] = address;
  1440. writeAddrMap(map);
  1441. try { await publishActivity({ type: 'bankWallet', address }); } catch (e) {}
  1442. }
  1443. }
  1444. ctx.body = await walletView(balance, address);
  1445. } catch (error) {
  1446. ctx.body = await walletErrorView(error);
  1447. }
  1448. })
  1449. .get("/wallet/history", async (ctx) => {
  1450. const { url, user, pass } = getConfig().wallet;
  1451. try {
  1452. const balance = await walletModel.getBalance(url, user, pass);
  1453. const transactions = await walletModel.listTransactions(url, user, pass);
  1454. const address = await walletModel.getAddress(url, user, pass);
  1455. const userId = SSBconfig.config.keys.id;
  1456. if (address && typeof address === "string") {
  1457. const map = readAddrMap();
  1458. const was = map[userId];
  1459. if (was !== address) {
  1460. map[userId] = address;
  1461. writeAddrMap(map);
  1462. try { await publishActivity({ type: 'bankWallet', address }); } catch (e) {}
  1463. }
  1464. }
  1465. ctx.body = await walletHistoryView(balance, transactions, address);
  1466. } catch (error) {
  1467. ctx.body = await walletErrorView(error);
  1468. }
  1469. })
  1470. .get("/wallet/receive", async (ctx) => {
  1471. const { url, user, pass } = getConfig().wallet;
  1472. try {
  1473. const balance = await walletModel.getBalance(url, user, pass);
  1474. const address = await walletModel.getAddress(url, user, pass);
  1475. const userId = SSBconfig.config.keys.id;
  1476. if (address && typeof address === "string") {
  1477. const map = readAddrMap();
  1478. const was = map[userId];
  1479. if (was !== address) {
  1480. map[userId] = address;
  1481. writeAddrMap(map);
  1482. try { await publishActivity({ type: 'bankWallet', address }); } catch (e) {}
  1483. }
  1484. }
  1485. ctx.body = await walletReceiveView(balance, address);
  1486. } catch (error) {
  1487. ctx.body = await walletErrorView(error);
  1488. }
  1489. })
  1490. .get("/wallet/send", async (ctx) => {
  1491. const { url, user, pass, fee } = getConfig().wallet;
  1492. try {
  1493. const balance = await walletModel.getBalance(url, user, pass);
  1494. const address = await walletModel.getAddress(url, user, pass);
  1495. const userId = SSBconfig.config.keys.id;
  1496. if (address && typeof address === "string") {
  1497. const map = readAddrMap();
  1498. const was = map[userId];
  1499. if (was !== address) {
  1500. map[userId] = address;
  1501. writeAddrMap(map);
  1502. try { await publishActivity({ type: 'bankWallet', address }); } catch (e) {}
  1503. }
  1504. }
  1505. ctx.body = await walletSendFormView(balance, null, null, fee, null, address);
  1506. } catch (error) {
  1507. ctx.body = await walletErrorView(error);
  1508. }
  1509. })
  1510. .get('/transfers', async ctx => {
  1511. const filter = ctx.query.filter || 'all'
  1512. const list = await transfersModel.listAll(filter)
  1513. ctx.body = await transferView(list, filter, null)
  1514. })
  1515. .get('/transfers/edit/:id', async ctx => {
  1516. const tr = await transfersModel.getTransferById(ctx.params.id)
  1517. ctx.body = await transferView([tr], 'edit', ctx.params.id)
  1518. })
  1519. .get('/transfers/:transferId', async ctx => {
  1520. const transferId = ctx.params.transferId;
  1521. const filter = ctx.query.filter || 'all';
  1522. const transfer = await transfersModel.getTransferById(transferId);
  1523. ctx.body = await singleTransferView(transfer, filter);
  1524. })
  1525. //POST backend routes
  1526. .post('/ai', koaBody(), async (ctx) => {
  1527. const { input } = ctx.request.body;
  1528. if (!input) {
  1529. ctx.status = 400;
  1530. ctx.body = { error: 'No input provided' };
  1531. return;
  1532. }
  1533. startAI();
  1534. const i18nAll = require('../client/assets/translations/i18n');
  1535. const lang = ctx.cookies.get('lang') || 'en';
  1536. const translations = i18nAll[lang] || i18nAll['en'];
  1537. const { setLanguage } = require('../views/main_views');
  1538. setLanguage(lang);
  1539. const historyPath = path.join(__dirname, '..', '..', 'src', 'configs', 'AI-history.json');
  1540. let chatHistory = [];
  1541. try {
  1542. const fileData = fs.readFileSync(historyPath, 'utf-8');
  1543. chatHistory = JSON.parse(fileData);
  1544. } catch {
  1545. chatHistory = [];
  1546. }
  1547. const config = getConfig();
  1548. const userPrompt = config.ai?.prompt?.trim() || 'Provide an informative and precise response.';
  1549. try {
  1550. let aiResponse = '';
  1551. let snippets = [];
  1552. const trained = await getBestTrainedAnswer(input);
  1553. if (trained && trained.answer) {
  1554. aiResponse = trained.answer;
  1555. snippets = Array.isArray(trained.ctx) ? trained.ctx : [];
  1556. } else {
  1557. const response = await axios.post('http://localhost:4001/ai', { input });
  1558. aiResponse = response.data.answer;
  1559. snippets = Array.isArray(response.data.snippets) ? response.data.snippets : [];
  1560. }
  1561. chatHistory.unshift({
  1562. prompt: userPrompt,
  1563. question: input,
  1564. answer: aiResponse,
  1565. timestamp: Date.now(),
  1566. trainStatus: 'pending',
  1567. snippets
  1568. });
  1569. } catch (e) {
  1570. chatHistory.unshift({
  1571. prompt: userPrompt,
  1572. question: input,
  1573. answer: translations.aiServerError || 'The AI could not answer. Please try again.',
  1574. timestamp: Date.now(),
  1575. trainStatus: 'rejected',
  1576. snippets: []
  1577. });
  1578. }
  1579. chatHistory = chatHistory.slice(0, 20);
  1580. fs.writeFileSync(historyPath, JSON.stringify(chatHistory, null, 2), 'utf-8');
  1581. ctx.body = aiView(chatHistory, userPrompt);
  1582. })
  1583. .post('/ai/approve', koaBody(), async (ctx) => {
  1584. const ts = String(ctx.request.body.ts || '');
  1585. const custom = String(ctx.request.body.custom || '').trim();
  1586. const historyPath = path.join(__dirname, '..', '..', 'src', 'configs', 'AI-history.json');
  1587. let chatHistory = [];
  1588. try {
  1589. const fileData = fs.readFileSync(historyPath, 'utf-8');
  1590. chatHistory = JSON.parse(fileData);
  1591. } catch {
  1592. chatHistory = [];
  1593. }
  1594. const item = chatHistory.find(e => String(e.timestamp) === ts);
  1595. if (item) {
  1596. try {
  1597. if (custom) item.answer = custom;
  1598. item.type = 'aiExchange';
  1599. let snippets = fieldsForSnippet('aiExchange', item);
  1600. if (snippets.length === 0) {
  1601. const context = await buildContext();
  1602. snippets = [context];
  1603. } else {
  1604. snippets = snippets.map(snippet => clip(snippet, 200));
  1605. }
  1606. await publishExchange({
  1607. q: item.question,
  1608. a: item.answer,
  1609. ctx: snippets,
  1610. tokens: {}
  1611. });
  1612. item.trainStatus = 'approved';
  1613. } catch {
  1614. item.trainStatus = 'failed';
  1615. }
  1616. fs.writeFileSync(historyPath, JSON.stringify(chatHistory, null, 2), 'utf-8');
  1617. }
  1618. const config = getConfig();
  1619. const userPrompt = config.ai?.prompt?.trim() || '';
  1620. ctx.body = aiView(chatHistory, userPrompt);
  1621. })
  1622. .post('/ai/reject', koaBody(), async (ctx) => {
  1623. const i18nAll = require('../client/assets/translations/i18n');
  1624. const lang = ctx.cookies.get('lang') || 'en';
  1625. const { setLanguage } = require('../views/main_views');
  1626. setLanguage(lang);
  1627. const ts = String(ctx.request.body.ts || '');
  1628. const historyPath = path.join(__dirname, '..', '..', 'src', 'configs', 'AI-history.json');
  1629. let chatHistory = [];
  1630. try {
  1631. const fileData = fs.readFileSync(historyPath, 'utf-8');
  1632. chatHistory = JSON.parse(fileData);
  1633. } catch {
  1634. chatHistory = [];
  1635. }
  1636. const item = chatHistory.find(e => String(e.timestamp) === ts);
  1637. if (item) {
  1638. item.trainStatus = 'rejected';
  1639. fs.writeFileSync(historyPath, JSON.stringify(chatHistory, null, 2), 'utf-8');
  1640. }
  1641. const config = getConfig();
  1642. const userPrompt = config.ai?.prompt?.trim() || '';
  1643. ctx.body = aiView(chatHistory, userPrompt);
  1644. })
  1645. .post('/ai/clear', async (ctx) => {
  1646. const i18nAll = require('../client/assets/translations/i18n');
  1647. const lang = ctx.cookies.get('lang') || 'en';
  1648. const { setLanguage } = require('../views/main_views');
  1649. setLanguage(lang);
  1650. const historyPath = path.join(__dirname, '..', '..', 'src', 'configs', 'AI-history.json');
  1651. fs.writeFileSync(historyPath, '[]', 'utf-8');
  1652. const config = getConfig();
  1653. const userPrompt = config.ai?.prompt?.trim() || '';
  1654. ctx.body = aiView([], userPrompt);
  1655. })
  1656. .post('/pixelia/paint', koaBody(), async (ctx) => {
  1657. const { x, y, color } = ctx.request.body;
  1658. if (x < 1 || x > 50 || y < 1 || y > 200) {
  1659. const errorMessage = 'Coordinates are wrong!';
  1660. const pixelArt = await pixeliaModel.listPixels();
  1661. ctx.body = pixeliaView(pixelArt, errorMessage);
  1662. return;
  1663. }
  1664. await pixeliaModel.paintPixel(x, y, color);
  1665. ctx.redirect('/pixelia');
  1666. })
  1667. .post('/pm', koaBody(), async ctx => {
  1668. const { recipients, subject, text } = ctx.request.body;
  1669. const recipientsArr = (recipients || '').split(',').map(s => s.trim()).filter(Boolean);
  1670. await pmModel.sendMessage(recipientsArr, subject, text);
  1671. ctx.redirect('/inbox?filter=sent');
  1672. })
  1673. .post('/pm/preview', koaBody(), async ctx => {
  1674. const { recipients = '', subject = '', text = '' } = ctx.request.body;
  1675. ctx.body = await pmView(recipients, subject, text, true);
  1676. })
  1677. .post('/inbox/delete/:id', koaBody(), async ctx => {
  1678. const { id } = ctx.params;
  1679. await pmModel.deleteMessageById(id);
  1680. ctx.redirect('/inbox');
  1681. })
  1682. .post('/inbox/delete/:id', koaBody(), async ctx => {
  1683. const { id } = ctx.params;
  1684. await pmModel.deleteMessageById(id);
  1685. ctx.redirect('/inbox');
  1686. })
  1687. .post("/search", koaBody(), async (ctx) => {
  1688. const body = ctx.request.body;
  1689. const query = body.query || "";
  1690. let types = body.type || [];
  1691. if (typeof types === "string") types = [types];
  1692. if (!Array.isArray(types)) types = [];
  1693. if (!query) {
  1694. return ctx.body = await searchView({ messages: [], query, types });
  1695. }
  1696. const results = await searchModel.search({ query, types });
  1697. const groupedResults = Object.entries(results).reduce((acc, [type, msgs]) => {
  1698. acc[type] = msgs.map(msg => {
  1699. if (!msg.value || !msg.value.content) {
  1700. return {};
  1701. }
  1702. return {
  1703. ...msg,
  1704. content: msg.value.content,
  1705. author: msg.value.content.author || 'Unknown',
  1706. };
  1707. });
  1708. return acc;
  1709. }, {});
  1710. ctx.body = await searchView({ results: groupedResults, query, types });
  1711. })
  1712. .post("/subtopic/preview/:message",
  1713. koaBody({ multipart: true }),
  1714. async (ctx) => {
  1715. const { message } = ctx.params;
  1716. const rootMessage = await post.get(message);
  1717. const myFeedId = await meta.myFeedId();
  1718. const rawContentWarning = String(ctx.request.body.contentWarning).trim();
  1719. const contentWarning =
  1720. rawContentWarning.length > 0 ? rawContentWarning : undefined;
  1721. const messages = [rootMessage];
  1722. const previewData = await preparePreview(ctx);
  1723. ctx.body = await previewSubtopicView({
  1724. messages,
  1725. myFeedId,
  1726. previewData,
  1727. contentWarning,
  1728. });
  1729. }
  1730. )
  1731. .post("/subtopic/:message", koaBody(), async (ctx) => {
  1732. const { message } = ctx.params;
  1733. const text = String(ctx.request.body.text);
  1734. const rawContentWarning = String(ctx.request.body.contentWarning).trim();
  1735. const contentWarning =
  1736. rawContentWarning.length > 0 ? rawContentWarning : undefined;
  1737. const publishSubtopic = async ({ message, text }) => {
  1738. const mentions = extractMentions(text);
  1739. const parent = await post.get(message);
  1740. return post.subtopic({
  1741. parent,
  1742. message: { text, mentions, contentWarning },
  1743. });
  1744. };
  1745. ctx.body = await publishSubtopic({ message, text });
  1746. ctx.redirect(`/thread/${encodeURIComponent(message)}`);
  1747. })
  1748. .post("/comment/preview/:message", koaBody({ multipart: true }), async (ctx) => {
  1749. const { messages, contentWarning, myFeedId, parentMessage } = await resolveCommentComponents(ctx);
  1750. const previewData = await preparePreview(ctx);
  1751. ctx.body = await previewCommentView({
  1752. messages,
  1753. myFeedId,
  1754. contentWarning,
  1755. previewData,
  1756. parentMessage,
  1757. });
  1758. })
  1759. .post("/comment/:message", koaBody(), async (ctx) => {
  1760. let decodedMessage;
  1761. try {
  1762. decodedMessage = decodeURIComponent(ctx.params.message);
  1763. } catch {
  1764. decodedMessage = ctx.params.message;
  1765. }
  1766. const text = String(ctx.request.body.text);
  1767. const rawContentWarning = String(ctx.request.body.contentWarning);
  1768. const contentWarning =
  1769. rawContentWarning.length > 0 ? rawContentWarning : undefined;
  1770. let mentions = extractMentions(text);
  1771. if (!Array.isArray(mentions)) mentions = [];
  1772. const parent = await meta.get(decodedMessage);
  1773. ctx.body = await post.comment({
  1774. parent,
  1775. message: {
  1776. text,
  1777. mentions,
  1778. contentWarning
  1779. },
  1780. });
  1781. ctx.redirect(`/thread/${encodeURIComponent(parent.key)}`);
  1782. })
  1783. .post("/publish/preview", koaBody({multipart: true, formidable: { multiples: false }, urlencoded: true }), async (ctx) => {
  1784. const rawContentWarning = ctx.request.body.contentWarning?.toString().trim() || "";
  1785. const contentWarning = rawContentWarning.length > 0 ? rawContentWarning : undefined;
  1786. const previewData = await preparePreview(ctx);
  1787. ctx.body = await previewView({ previewData, contentWarning });
  1788. })
  1789. .post("/publish", koaBody({ multipart: true, urlencoded: true, formidable: { multiples: false } }), async (ctx) => {
  1790. const text = ctx.request.body.text?.toString().trim() || "";
  1791. const rawContentWarning = ctx.request.body.contentWarning?.toString().trim() || "";
  1792. const contentWarning = rawContentWarning.length > 0 ? rawContentWarning : undefined;
  1793. let mentions = [];
  1794. try {
  1795. mentions = JSON.parse(ctx.request.body.mentions || "[]");
  1796. } catch (e) {
  1797. mentions = await extractMentions(text);
  1798. }
  1799. await post.root({ text, mentions, contentWarning });
  1800. ctx.redirect("/public/latest");
  1801. })
  1802. .post("/publish/custom", koaBody(), async (ctx) => {
  1803. const text = String(ctx.request.body.text);
  1804. const obj = JSON.parse(text);
  1805. ctx.body = await post.publishCustom(obj);
  1806. ctx.redirect(`/public/latest`);
  1807. })
  1808. .post("/follow/:feed", koaBody(), async (ctx) => {
  1809. const { feed } = ctx.params;
  1810. const referer = new URL(ctx.request.header.referer);
  1811. ctx.body = await friend.follow(feed);
  1812. ctx.redirect(referer.href);
  1813. })
  1814. .post("/unfollow/:feed", koaBody(), async (ctx) => {
  1815. const { feed } = ctx.params;
  1816. const referer = new URL(ctx.request.header.referer);
  1817. ctx.body = await friend.unfollow(feed);
  1818. ctx.redirect(referer.href);
  1819. })
  1820. .post("/block/:feed", koaBody(), async (ctx) => {
  1821. const { feed } = ctx.params;
  1822. const referer = new URL(ctx.request.header.referer);
  1823. ctx.body = await friend.block(feed);
  1824. ctx.redirect(referer.href);
  1825. })
  1826. .post("/unblock/:feed", koaBody(), async (ctx) => {
  1827. const { feed } = ctx.params;
  1828. const referer = new URL(ctx.request.header.referer);
  1829. ctx.body = await friend.unblock(feed);
  1830. ctx.redirect(referer.href);
  1831. })
  1832. .post("/like/:message", koaBody(), async (ctx) => {
  1833. const { message } = ctx.params;
  1834. const messageKey = message;
  1835. const voteValue = Number(ctx.request.body.voteValue);
  1836. const encoded = {
  1837. message: encodeURIComponent(message),
  1838. };
  1839. const referer = new URL(ctx.request.header.referer);
  1840. referer.hash = `centered-footer-${encoded.message}`;
  1841. const like = async ({ messageKey, voteValue }) => {
  1842. const value = Number(voteValue);
  1843. const message = await post.get(messageKey);
  1844. const isPrivate = message.value.meta.private === true;
  1845. const messageRecipients = isPrivate ? message.value.content.recps : [];
  1846. const normalized = messageRecipients.map((recipient) => {
  1847. if (typeof recipient === "string") {
  1848. return recipient;
  1849. }
  1850. if (typeof recipient.link === "string") {
  1851. return recipient.link;
  1852. }
  1853. return null;
  1854. });
  1855. const recipients = normalized.length > 0 ? normalized : undefined;
  1856. return vote.publish({ messageKey, value, recps: recipients });
  1857. };
  1858. ctx.body = await like({ messageKey, voteValue });
  1859. ctx.redirect(referer.href);
  1860. })
  1861. .post('/forum/create', koaBody(), async ctx => {
  1862. const { category, title, text } = ctx.request.body;
  1863. await forumModel.createForum(category, title, text);
  1864. ctx.redirect('/forum');
  1865. })
  1866. .post('/forum/:id/message', koaBody(), async ctx => {
  1867. const forumId = ctx.params.id;
  1868. const { message, parentId } = ctx.request.body;
  1869. const userId = SSBconfig.config.keys.id;
  1870. const newMessage = { text: message, author: userId, timestamp: new Date().toISOString() };
  1871. await forumModel.addMessageToForum(forumId, newMessage, parentId);
  1872. ctx.redirect(`/forum/${encodeURIComponent(forumId)}`);
  1873. })
  1874. .post('/forum/:forumId/vote', koaBody(), async ctx => {
  1875. const { forumId } = ctx.params;
  1876. const { target, value } = ctx.request.body;
  1877. await forumModel.voteContent(target, parseInt(value, 10));
  1878. const back = ctx.get('referer') || `/forum/${encodeURIComponent(forumId)}`;
  1879. ctx.redirect(back);
  1880. })
  1881. .post('/forum/delete/:id', koaBody(), async ctx => {
  1882. await forumModel.deleteForumById(ctx.params.id);
  1883. ctx.redirect('/forum');
  1884. })
  1885. .post('/legacy/export', koaBody(), async (ctx) => {
  1886. const password = ctx.request.body.password;
  1887. if (!password || password.length < 32) {
  1888. ctx.redirect('/legacy');
  1889. return;
  1890. }
  1891. try {
  1892. const encryptedFilePath = await legacyModel.exportData({ password });
  1893. ctx.body = {
  1894. message: 'Data exported successfully!',
  1895. file: encryptedFilePath
  1896. };
  1897. ctx.redirect('/legacy');
  1898. } catch (error) {
  1899. ctx.status = 500;
  1900. ctx.body = { error: `Error: ${error.message}` };
  1901. ctx.redirect('/legacy');
  1902. }
  1903. })
  1904. .post('/legacy/import', koaBody({
  1905. multipart: true,
  1906. formidable: {
  1907. keepExtensions: true,
  1908. uploadDir: '/tmp',
  1909. }
  1910. }), async (ctx) => {
  1911. const uploadedFile = ctx.request.files?.uploadedFile;
  1912. const password = ctx.request.body.importPassword;
  1913. if (!uploadedFile) {
  1914. ctx.body = { error: 'No file uploaded' };
  1915. ctx.redirect('/legacy');
  1916. return;
  1917. }
  1918. if (!password || password.length < 32) {
  1919. ctx.body = { error: 'Password is too short or missing.' };
  1920. ctx.redirect('/legacy');
  1921. return;
  1922. }
  1923. try {
  1924. await legacyModel.importData({ filePath: uploadedFile.filepath, password });
  1925. ctx.body = { message: 'Data imported successfully!' };
  1926. ctx.redirect('/legacy');
  1927. } catch (error) {
  1928. ctx.body = { error: error.message };
  1929. ctx.redirect('/legacy');
  1930. }
  1931. })
  1932. .post('/trending/:contentId/:category', async (ctx) => {
  1933. const { contentId, category } = ctx.params;
  1934. const voterId = SSBconfig?.keys?.id;
  1935. const target = await trendingModel.getMessageById(contentId);
  1936. if (target?.content?.opinions_inhabitants?.includes(voterId)) {
  1937. ctx.flash = { message: 'You have already opined.' };
  1938. ctx.redirect('/trending');
  1939. return;
  1940. }
  1941. await trendingModel.createVote(contentId, category);
  1942. ctx.redirect('/trending');
  1943. })
  1944. .post('/opinions/:contentId/:category', async (ctx) => {
  1945. const { contentId, category } = ctx.params;
  1946. const voterId = SSBconfig?.keys?.id;
  1947. const target = await opinionsModel.getMessageById(contentId);
  1948. if (target?.content?.opinions_inhabitants?.includes(voterId)) {
  1949. ctx.flash = { message: 'You have already opined.' };
  1950. ctx.redirect('/opinions');
  1951. return;
  1952. }
  1953. await opinionsModel.createVote(contentId, category);
  1954. ctx.redirect('/opinions');
  1955. })
  1956. .post('/agenda/discard/:itemId', async (ctx) => {
  1957. const { itemId } = ctx.params;
  1958. await agendaModel.discardItem(itemId);
  1959. ctx.redirect('/agenda');
  1960. })
  1961. .post('/agenda/restore/:itemId', async (ctx) => {
  1962. const { itemId } = ctx.params;
  1963. await agendaModel.restoreItem(itemId);
  1964. ctx.redirect('/agenda?filter=discarded');
  1965. })
  1966. .post('/feed/create', koaBody(), async ctx => {
  1967. const { text } = ctx.request.body || {};
  1968. await feedModel.createFeed(text.trim());
  1969. ctx.redirect('/feed');
  1970. })
  1971. .post('/feed/opinions/:feedId/:category', async ctx => {
  1972. const { feedId, category } = ctx.params;
  1973. await opinionsModel.createVote(feedId, category);
  1974. ctx.redirect('/feed');
  1975. })
  1976. .post('/feed/refeed/:id', koaBody(), async ctx => {
  1977. await feedModel.createRefeed(ctx.params.id);
  1978. ctx.redirect('/feed');
  1979. })
  1980. .post('/bookmarks/create', koaBody(), async (ctx) => {
  1981. const { url, tags, description, category, lastVisit } = ctx.request.body;
  1982. const formattedLastVisit = lastVisit ? moment(lastVisit).isBefore(moment()) ? moment(lastVisit).toISOString() : moment().toISOString() : moment().toISOString();
  1983. await bookmarksModel.createBookmark(url, tags, description, category, formattedLastVisit);
  1984. ctx.redirect('/bookmarks');
  1985. })
  1986. .post('/bookmarks/update/:id', koaBody(), async (ctx) => {
  1987. const { url, tags, description, category, lastVisit } = ctx.request.body;
  1988. const bookmarkId = ctx.params.id;
  1989. const formattedLastVisit = lastVisit
  1990. ? moment(lastVisit).isBefore(moment())
  1991. ? moment(lastVisit).toISOString()
  1992. : moment().toISOString()
  1993. : moment().toISOString();
  1994. const bookmark = await bookmarksModel.getBookmarkById(bookmarkId);
  1995. if (bookmark.opinions_inhabitants && bookmark.opinions_inhabitants.length > 0) {
  1996. ctx.flash = { message: "This bookmark has received votes and cannot be updated." };
  1997. ctx.redirect(`/bookmarks?filter=mine`);
  1998. }
  1999. await bookmarksModel.updateBookmarkById(bookmarkId, {
  2000. url,
  2001. tags,
  2002. description,
  2003. category,
  2004. lastVisit: formattedLastVisit,
  2005. createdAt: bookmark.createdAt,
  2006. author: bookmark.author,
  2007. });
  2008. ctx.redirect('/bookmarks?filter=mine');
  2009. })
  2010. .post('/bookmarks/delete/:id', koaBody(), async (ctx) => {
  2011. const bookmarkId = ctx.params.id;
  2012. await bookmarksModel.deleteBookmarkById(bookmarkId);
  2013. ctx.redirect('/bookmarks?filter=mine');
  2014. })
  2015. .post('/bookmarks/opinions/:bookmarkId/:category', async (ctx) => {
  2016. const { bookmarkId, category } = ctx.params;
  2017. const voterId = SSBconfig?.keys?.id;
  2018. const bookmark = await bookmarksModel.getBookmarkById(bookmarkId);
  2019. if (bookmark.opinions_inhabitants && bookmark.opinions_inhabitants.includes(voterId)) {
  2020. ctx.flash = { message: "You have already opined." };
  2021. ctx.redirect('/bookmarks');
  2022. return;
  2023. }
  2024. await opinionsModel.createVote(bookmarkId, category, 'bookmark');
  2025. ctx.redirect('/bookmarks');
  2026. })
  2027. .post('/images/create', koaBody({ multipart: true }), async ctx => {
  2028. const blob = await handleBlobUpload(ctx, 'image');
  2029. const { tags, title, description, meme } = ctx.request.body;
  2030. await imagesModel.createImage(blob, tags, title, description, meme);
  2031. ctx.redirect('/images');
  2032. })
  2033. .post('/images/update/:id', koaBody({ multipart: true }), async ctx => {
  2034. const { tags, title, description, meme } = ctx.request.body;
  2035. const blob = ctx.request.files?.image ? await handleBlobUpload(ctx, 'image') : null;
  2036. const match = blob?.match(/\(([^)]+)\)/);
  2037. const blobId = match ? match[1] : blob;
  2038. await imagesModel.updateImageById(ctx.params.id, blobId, tags, title, description, meme);
  2039. ctx.redirect('/images?filter=mine');
  2040. })
  2041. .post('/images/delete/:id', koaBody(), async ctx => {
  2042. await imagesModel.deleteImageById(ctx.params.id);
  2043. ctx.redirect('/images?filter=mine');
  2044. })
  2045. .post('/images/opinions/:imageId/:category', async ctx => {
  2046. const { imageId, category } = ctx.params;
  2047. const voterId = SSBconfig?.keys?.id;
  2048. const image = await imagesModel.getImageById(imageId);
  2049. if (image.opinions_inhabitants && image.opinions_inhabitants.includes(voterId)) {
  2050. ctx.flash = { message: "You have already opined." };
  2051. ctx.redirect('/images');
  2052. return;
  2053. }
  2054. await imagesModel.createOpinion(imageId, category, 'image');
  2055. ctx.redirect('/images');
  2056. })
  2057. .post('/audios/create', koaBody({ multipart: true }), async (ctx) => {
  2058. const audioBlob = await handleBlobUpload(ctx, 'audio');
  2059. const { tags, title, description } = ctx.request.body;
  2060. await audiosModel.createAudio(audioBlob, tags, title, description);
  2061. ctx.redirect('/audios');
  2062. })
  2063. .post('/audios/update/:id', koaBody({ multipart: true }), async (ctx) => {
  2064. const { tags, title, description } = ctx.request.body;
  2065. const blob = ctx.request.files?.audio ? await handleBlobUpload(ctx, 'audio') : null;
  2066. await audiosModel.updateAudioById(ctx.params.id, blob, tags, title, description);
  2067. ctx.redirect('/audios?filter=mine');
  2068. })
  2069. .post('/audios/delete/:id', koaBody(), async (ctx) => {
  2070. await audiosModel.deleteAudioById(ctx.params.id);
  2071. ctx.redirect('/audios?filter=mine');
  2072. })
  2073. .post('/audios/opinions/:audioId/:category', async (ctx) => {
  2074. const { audioId, category } = ctx.params;
  2075. const voterId = SSBconfig?.keys?.id;
  2076. const audio = await audiosModel.getAudioById(audioId);
  2077. if (audio.opinions_inhabitants?.includes(voterId)) {
  2078. ctx.flash = { message: "You have already opined." };
  2079. ctx.redirect('/audios');
  2080. return;
  2081. }
  2082. await audiosModel.createOpinion(audioId, category);
  2083. ctx.redirect('/audios');
  2084. })
  2085. .post('/videos/create', koaBody({ multipart: true }), async (ctx) => {
  2086. const videoBlob = await handleBlobUpload(ctx, 'video');
  2087. const { tags, title, description } = ctx.request.body;
  2088. await videosModel.createVideo(videoBlob, tags, title, description);
  2089. ctx.redirect('/videos');
  2090. })
  2091. .post('/videos/update/:id', koaBody({ multipart: true }), async (ctx) => {
  2092. const { tags, title, description } = ctx.request.body;
  2093. const blob = ctx.request.files?.video ? await handleBlobUpload(ctx, 'video') : null;
  2094. await videosModel.updateVideoById(ctx.params.id, blob, tags, title, description);
  2095. ctx.redirect('/videos?filter=mine');
  2096. })
  2097. .post('/videos/delete/:id', koaBody(), async (ctx) => {
  2098. await videosModel.deleteVideoById(ctx.params.id);
  2099. ctx.redirect('/videos?filter=mine');
  2100. })
  2101. .post('/videos/opinions/:videoId/:category', async (ctx) => {
  2102. const { videoId, category } = ctx.params;
  2103. const voterId = SSBconfig?.keys?.id;
  2104. const video = await videosModel.getVideoById(videoId);
  2105. if (video.opinions_inhabitants?.includes(voterId)) {
  2106. ctx.flash = { message: "You have already opined." };
  2107. ctx.redirect('/videos');
  2108. return;
  2109. }
  2110. await videosModel.createOpinion(videoId, category);
  2111. ctx.redirect('/videos');
  2112. })
  2113. .post('/documents/create', koaBody({ multipart: true }), async (ctx) => {
  2114. const docBlob = await handleBlobUpload(ctx, 'document');
  2115. const { tags, title, description } = ctx.request.body;
  2116. await documentsModel.createDocument(docBlob, tags, title, description);
  2117. ctx.redirect('/documents');
  2118. })
  2119. .post('/documents/update/:id', koaBody({ multipart: true }), async (ctx) => {
  2120. const { tags, title, description } = ctx.request.body;
  2121. const blob = ctx.request.files?.document ? await handleBlobUpload(ctx, 'document') : null;
  2122. await documentsModel.updateDocumentById(ctx.params.id, blob, tags, title, description);
  2123. ctx.redirect('/documents?filter=mine');
  2124. })
  2125. .post('/documents/delete/:id', koaBody(), async (ctx) => {
  2126. await documentsModel.deleteDocumentById(ctx.params.id);
  2127. ctx.redirect('/documents?filter=mine');
  2128. })
  2129. .post('/documents/opinions/:documentId/:category', async (ctx) => {
  2130. const { documentId, category } = ctx.params;
  2131. const voterId = SSBconfig?.keys?.id;
  2132. const document = await documentsModel.getDocumentById(documentId);
  2133. if (document.opinions_inhabitants?.includes(voterId)) {
  2134. ctx.flash = { message: "You have already opined." };
  2135. ctx.redirect('/documents');
  2136. return;
  2137. }
  2138. await documentsModel.createOpinion(documentId, category);
  2139. ctx.redirect('/documents');
  2140. })
  2141. .post('/cv/upload', koaBody({ multipart: true }), async ctx => {
  2142. const photoUrl = await handleBlobUpload(ctx, 'image')
  2143. await cvModel.createCV(ctx.request.body, photoUrl)
  2144. ctx.redirect('/cv')
  2145. })
  2146. .post('/cv/update/:id', koaBody({ multipart: true }), async ctx => {
  2147. const photoUrl = await handleBlobUpload(ctx, 'image')
  2148. await cvModel.updateCV(ctx.params.id, ctx.request.body, photoUrl)
  2149. ctx.redirect('/cv')
  2150. })
  2151. .post('/cv/delete/:id', async ctx => {
  2152. await cvModel.deleteCVById(ctx.params.id)
  2153. ctx.redirect('/cv')
  2154. })
  2155. .post('/cipher/encrypt', koaBody(), async (ctx) => {
  2156. const { text, password } = ctx.request.body;
  2157. if (password.length < 32) {
  2158. ctx.body = { error: 'Password is too short or missing.' };
  2159. ctx.redirect('/cipher');
  2160. return;
  2161. }
  2162. const { encryptedText, iv, salt, authTag } = cipherModel.encryptData(text, password);
  2163. const view = await cipherView(encryptedText, "", iv, password);
  2164. ctx.body = view;
  2165. })
  2166. .post('/cipher/decrypt', koaBody(), async (ctx) => {
  2167. const { encryptedText, password } = ctx.request.body;
  2168. if (password.length < 32) {
  2169. ctx.body = { error: 'Password is too short or missing.' };
  2170. ctx.redirect('/cipher');
  2171. return;
  2172. }
  2173. const decryptedText = cipherModel.decryptData(encryptedText, password);
  2174. const view = await cipherView("", decryptedText, "", password);
  2175. ctx.body = view;
  2176. })
  2177. .post('/tribes/create', koaBody({ multipart: true }), async ctx => {
  2178. const { title, description, location, tags, isLARP, isAnonymous, inviteMode } = ctx.request.body;
  2179. // Block L.A.R.P. creation
  2180. if (isLARP === 'true' || isLARP === true) {
  2181. ctx.status = 400;
  2182. ctx.body = { error: "L.A.R.P. tribes cannot be created." };
  2183. return;
  2184. }
  2185. const image = await handleBlobUpload(ctx, 'image');
  2186. await tribesModel.createTribe(
  2187. title,
  2188. description,
  2189. image,
  2190. location,
  2191. tags,
  2192. isLARP === 'true',
  2193. isAnonymous === 'true',
  2194. inviteMode
  2195. );
  2196. ctx.redirect('/tribes');
  2197. })
  2198. .post('/tribes/update/:id', koaBody({ multipart: true }), async ctx => {
  2199. const { title, description, location, isLARP, isAnonymous, inviteMode, tags } = ctx.request.body;
  2200. // Block L.A.R.P. creation
  2201. if (isLARP === 'true' || isLARP === true) {
  2202. ctx.status = 400;
  2203. ctx.body = { error: "L.A.R.P. tribes cannot be updated." };
  2204. return;
  2205. }
  2206. const parsedTags = tags ? tags.split(',').map(t => t.trim()).filter(Boolean) : [];
  2207. const image = await handleBlobUpload(ctx, 'image');
  2208. await tribesModel.updateTribeById(ctx.params.id, {
  2209. title,
  2210. description,
  2211. image,
  2212. location,
  2213. tags: parsedTags,
  2214. isLARP: isLARP === 'true',
  2215. isAnonymous: isAnonymous === 'true',
  2216. inviteMode
  2217. });
  2218. ctx.redirect('/tribes?filter=mine');
  2219. })
  2220. .post('/tribes/delete/:id', async ctx => {
  2221. await tribesModel.deleteTribeById(ctx.params.id)
  2222. ctx.redirect('/tribes?filter=mine')
  2223. })
  2224. .post('/tribes/generate-invite', koaBody(), async ctx => {
  2225. const { tribeId } = ctx.request.body;
  2226. const inviteCode = await tribesModel.generateInvite(tribeId);
  2227. ctx.body = await renderInvitePage(inviteCode);
  2228. })
  2229. .post('/tribes/join-code', koaBody(), async ctx => {
  2230. const { inviteCode } = ctx.request.body
  2231. await tribesModel.joinByInvite(inviteCode)
  2232. ctx.redirect('/tribes?filter=membership')
  2233. })
  2234. .post('/tribes/leave/:id', koaBody(), async ctx => {
  2235. await tribesModel.leaveTribe(ctx.params.id)
  2236. ctx.redirect('/tribes?filter=membership')
  2237. })
  2238. .post('/tribes/:id/message', koaBody(), async ctx => {
  2239. const tribeId = ctx.params.id;
  2240. const message = ctx.request.body.message;
  2241. await tribesModel.postMessage(tribeId, message);
  2242. ctx.redirect(ctx.headers.referer);
  2243. })
  2244. .post('/tribes/:id/refeed/:msgId', koaBody(), async ctx => {
  2245. const tribeId = ctx.params.id;
  2246. const msgId = ctx.params.msgId;
  2247. await tribesModel.refeed(tribeId, msgId);
  2248. ctx.redirect(ctx.headers.referer);
  2249. })
  2250. .post('/tribe/:id/message', koaBody(), async ctx => {
  2251. const tribeId = ctx.params.id;
  2252. const message = ctx.request.body.message;
  2253. await tribesModel.postMessage(tribeId, message);
  2254. ctx.redirect('/tribes?filter=mine')
  2255. })
  2256. .post('/panic/remove', koaBody(), async (ctx) => {
  2257. const { exec } = require('child_process');
  2258. try {
  2259. await panicmodeModel.removeSSB();
  2260. ctx.body = {
  2261. message: 'Your blockchain has been succesfully deleted!'
  2262. };
  2263. exec('pkill -f "node SSB_server.js start"');
  2264. setTimeout(() => {
  2265. process.exit(0);
  2266. }, 1000);
  2267. } catch (error) {
  2268. ctx.body = {
  2269. error: 'Error deleting your blockchain: ' + error.message
  2270. };
  2271. }
  2272. })
  2273. .post('/export/create', async (ctx) => {
  2274. try {
  2275. const outputPath = path.join(os.homedir(), 'ssb_exported.zip');
  2276. await exportmodeModel.exportSSB(outputPath);
  2277. ctx.set('Content-Type', 'application/zip');
  2278. ctx.set('Content-Disposition', `attachment; filename=ssb_exported.zip`);
  2279. ctx.body = fs.createReadStream(outputPath);
  2280. ctx.res.on('finish', () => {
  2281. fs.unlinkSync(outputPath);
  2282. });
  2283. } catch (error) {
  2284. ctx.body = {
  2285. error: 'Error exporting your blockchain: ' + error.message
  2286. };
  2287. }
  2288. })
  2289. .post('/tasks/create', koaBody(), async (ctx) => {
  2290. const { title, description, startTime, endTime, priority, location, tags, isPublic } = ctx.request.body;
  2291. await tasksModel.createTask(title, description, startTime, endTime, priority, location, tags, isPublic);
  2292. ctx.redirect('/tasks?filter=mine');
  2293. })
  2294. .post('/tasks/update/:id', koaBody(), async (ctx) => {
  2295. const { title, description, startTime, endTime, priority, location, tags, isPublic } = ctx.request.body;
  2296. const taskId = ctx.params.id;
  2297. const parsedTags = Array.isArray(tags)
  2298. ? tags.filter(Boolean)
  2299. : (typeof tags === 'string' ? tags.split(',').map(t => t.trim()).filter(Boolean) : []);
  2300. await tasksModel.updateTaskById(taskId, {
  2301. title,
  2302. description,
  2303. startTime,
  2304. endTime,
  2305. priority,
  2306. location,
  2307. tags: parsedTags,
  2308. isPublic
  2309. });
  2310. ctx.redirect('/tasks?filter=mine');
  2311. })
  2312. .post('/tasks/assign/:id', koaBody(), async (ctx) => {
  2313. const taskId = ctx.params.id;
  2314. await tasksModel.toggleAssignee(taskId);
  2315. ctx.redirect('/tasks');
  2316. })
  2317. .post('/tasks/delete/:id', koaBody(), async (ctx) => {
  2318. const taskId = ctx.params.id;
  2319. await tasksModel.deleteTaskById(taskId);
  2320. ctx.redirect('/tasks?filter=mine');
  2321. })
  2322. .post('/tasks/status/:id', koaBody(), async (ctx) => {
  2323. const taskId = ctx.params.id;
  2324. const { status } = ctx.request.body;
  2325. await tasksModel.updateTaskStatus(taskId, status);
  2326. ctx.redirect('/tasks?filter=mine');
  2327. })
  2328. .post('/reports/create', koaBody({ multipart: true }), async ctx => {
  2329. const { title, description, category, tags, severity } = ctx.request.body;
  2330. const image = await handleBlobUpload(ctx, 'image');
  2331. await reportsModel.createReport(title, description, category, image, tags, severity);
  2332. ctx.redirect('/reports');
  2333. })
  2334. .post('/reports/update/:id', koaBody({ multipart: true }), async ctx => {
  2335. const { title, description, category, tags, severity } = ctx.request.body;
  2336. const image = await handleBlobUpload(ctx, 'image');
  2337. await reportsModel.updateReportById(ctx.params.id, {
  2338. title, description, category, image, tags, severity
  2339. });
  2340. ctx.redirect('/reports?filter=mine');
  2341. })
  2342. .post('/reports/delete/:id', async ctx => {
  2343. await reportsModel.deleteReportById(ctx.params.id);
  2344. ctx.redirect('/reports?filter=mine');
  2345. })
  2346. .post('/reports/confirm/:id', async ctx => {
  2347. await reportsModel.confirmReportById(ctx.params.id);
  2348. ctx.redirect('/reports');
  2349. })
  2350. .post('/reports/status/:id', koaBody(), async (ctx) => {
  2351. const reportId = ctx.params.id;
  2352. const { status } = ctx.request.body;
  2353. await reportsModel.updateReportById(reportId, { status });
  2354. ctx.redirect('/reports?filter=mine');
  2355. })
  2356. .post('/events/create', koaBody(), async (ctx) => {
  2357. const { title, description, date, location, price, url, attendees, tags, isPublic } = ctx.request.body;
  2358. await eventsModel.createEvent(title, description, date, location, price, url, attendees, tags, isPublic);
  2359. ctx.redirect('/events?filter=mine');
  2360. })
  2361. .post('/events/update/:id', koaBody(), async (ctx) => {
  2362. const { title, description, date, location, price, url, attendees, tags, isPublic } = ctx.request.body;
  2363. const eventId = ctx.params.id;
  2364. const event = await eventsModel.getEventById(eventId);
  2365. await eventsModel.updateEventById(eventId, {
  2366. title,
  2367. description,
  2368. date,
  2369. location,
  2370. price,
  2371. url,
  2372. attendees,
  2373. tags,
  2374. isPublic,
  2375. createdAt: event.createdAt,
  2376. organizer: event.organizer,
  2377. });
  2378. ctx.redirect('/events?filter=mine');
  2379. })
  2380. .post('/events/attend/:id', koaBody(), async (ctx) => {
  2381. const eventId = ctx.params.id;
  2382. await eventsModel.toggleAttendee(eventId);
  2383. ctx.redirect('/events');
  2384. })
  2385. .post('/events/delete/:id', koaBody(), async (ctx) => {
  2386. const eventId = ctx.params.id;
  2387. await eventsModel.deleteEventById(eventId);
  2388. ctx.redirect('/events?filter=mine');
  2389. })
  2390. .post('/votes/create', koaBody(), async ctx => {
  2391. const { question, deadline, options, tags = '' } = ctx.request.body;
  2392. const defaultOptions = ['YES', 'NO', 'ABSTENTION', 'CONFUSED', 'FOLLOW_MAJORITY', 'NOT_INTERESTED'];
  2393. const parsedOptions = options
  2394. ? options.split(',').map(o => o.trim()).filter(Boolean)
  2395. : defaultOptions;
  2396. const parsedTags = tags.split(',').map(t => t.trim()).filter(Boolean);
  2397. await votesModel.createVote(question, deadline, parsedOptions, parsedTags);
  2398. ctx.redirect('/votes');
  2399. })
  2400. .post('/votes/update/:id', koaBody(), async ctx => {
  2401. const id = ctx.params.id;
  2402. const { question, deadline, options, tags = '' } = ctx.request.body;
  2403. const parsedOptions = options
  2404. ? options.split(',').map(o => o.trim()).filter(Boolean)
  2405. : undefined;
  2406. const parsedTags = tags ? tags.split(',').map(t => t.trim()).filter(Boolean) : [];
  2407. await votesModel.updateVoteById(id, { question, deadline, options: parsedOptions, tags: parsedTags });
  2408. ctx.redirect('/votes?filter=mine');
  2409. })
  2410. .post('/votes/delete/:id', koaBody(), async ctx => {
  2411. const id = ctx.params.id;
  2412. await votesModel.deleteVoteById(id);
  2413. ctx.redirect('/votes?filter=mine');
  2414. })
  2415. .post('/votes/vote/:id', koaBody(), async ctx => {
  2416. const id = ctx.params.id;
  2417. const { choice } = ctx.request.body;
  2418. await votesModel.voteOnVote(id, choice);
  2419. ctx.redirect('/votes?filter=open');
  2420. })
  2421. .post('/votes/opinions/:voteId/:category', async (ctx) => {
  2422. const { voteId, category } = ctx.params;
  2423. const voterId = SSBconfig?.keys?.id;
  2424. const vote = await votesModel.getVoteById(voteId);
  2425. if (vote.opinions_inhabitants && vote.opinions_inhabitants.includes(voterId)) {
  2426. ctx.flash = { message: "You have already opined." };
  2427. ctx.redirect('/votes');
  2428. return;
  2429. }
  2430. await votesModel.createOpinion(voteId, category);
  2431. ctx.redirect('/votes');
  2432. })
  2433. .post('/market/create', koaBody({ multipart: true }), async ctx => {
  2434. const { item_type, title, description, price, tags, item_status, deadline, includesShipping, stock } = ctx.request.body;
  2435. const image = await handleBlobUpload(ctx, 'image');
  2436. if (!stock || stock <= 0) {
  2437. ctx.throw(400, 'Stock must be a positive number.');
  2438. }
  2439. await marketModel.createItem(item_type, title, description, image, price, tags, item_status, deadline, includesShipping, stock);
  2440. ctx.redirect('/market');
  2441. })
  2442. .post('/market/update/:id', koaBody({ multipart: true }), async ctx => {
  2443. const id = ctx.params.id;
  2444. const { item_type, title, description, price, tags = '', item_status, deadline, includesShipping, stock } = ctx.request.body;
  2445. const parsedTags = tags.split(',').map(t => t.trim()).filter(Boolean);
  2446. if (stock < 0) {
  2447. ctx.throw(400, 'Stock cannot be negative.');
  2448. }
  2449. const updatedData = {
  2450. item_type,
  2451. title,
  2452. description,
  2453. price,
  2454. item_status,
  2455. deadline,
  2456. includesShipping,
  2457. tags: parsedTags,
  2458. stock
  2459. };
  2460. const image = await handleBlobUpload(ctx, 'image');
  2461. updatedData.image = image;
  2462. await marketModel.updateItemById(id, updatedData);
  2463. ctx.redirect('/market?filter=mine');
  2464. })
  2465. .post('/market/delete/:id', koaBody(), async ctx => {
  2466. const id = ctx.params.id;
  2467. await marketModel.deleteItemById(id);
  2468. ctx.redirect('/market?filter=mine');
  2469. })
  2470. .post('/market/sold/:id', koaBody(), async ctx => {
  2471. const id = ctx.params.id;
  2472. const marketItem = await marketModel.getItemById(id);
  2473. if (marketItem.stock <= 0) {
  2474. ctx.throw(400, 'No stock left to mark as sold.');
  2475. }
  2476. if (marketItem.status !== 'SOLD') {
  2477. await marketModel.setItemAsSold(id);
  2478. await marketModel.decrementStock(id);
  2479. }
  2480. ctx.redirect('/market?filter=mine');
  2481. })
  2482. .post('/market/buy/:id', koaBody(), async ctx => {
  2483. const id = ctx.params.id;
  2484. const marketItem = await marketModel.getItemById(id);
  2485. if (marketItem.item_type === 'exchange') {
  2486. if (marketItem.status !== 'SOLD') {
  2487. const buyerId = SSBconfig.config.keys.id;
  2488. const { price, title, seller } = marketItem;
  2489. const subject = `MARKET_SOLD`;
  2490. const text = `item "${title}" has been sold -> /market/${id} OASIS ID: ${buyerId} for: $${price}`;
  2491. await pmModel.sendMessage([seller], subject, text);
  2492. await marketModel.setItemAsSold(id);
  2493. }
  2494. }
  2495. await marketModel.decrementStock(id);
  2496. ctx.redirect('/inbox?filter=sent');
  2497. })
  2498. .post('/market/bid/:id', koaBody(), async ctx => {
  2499. const id = ctx.params.id;
  2500. const userId = SSBconfig.config.keys.id;
  2501. const { bidAmount } = ctx.request.body;
  2502. const marketItem = await marketModel.getItemById(id);
  2503. await marketModel.addBidToAuction(id, userId, bidAmount);
  2504. if (marketItem.stock > 0 && marketItem.status === 'SOLD') {
  2505. await marketModel.decrementStock(id);
  2506. }
  2507. ctx.redirect('/market?filter=auctions');
  2508. })
  2509. .post('/jobs/create', koaBody({ multipart: true }), async (ctx) => {
  2510. const {
  2511. job_type,
  2512. title,
  2513. description,
  2514. requirements,
  2515. languages,
  2516. job_time,
  2517. tasks,
  2518. location,
  2519. vacants,
  2520. salary
  2521. } = ctx.request.body;
  2522. const imageBlob = ctx.request.files?.image
  2523. ? await handleBlobUpload(ctx, 'image')
  2524. : null;
  2525. await jobsModel.createJob({
  2526. job_type,
  2527. title,
  2528. description,
  2529. requirements,
  2530. languages,
  2531. job_time,
  2532. tasks,
  2533. location,
  2534. vacants: vacants ? parseInt(vacants, 10) : 1,
  2535. salary: salary != null ? parseFloat(salary) : 0,
  2536. image: imageBlob
  2537. });
  2538. ctx.redirect('/jobs?filter=MINE');
  2539. })
  2540. .post('/jobs/update/:id', koaBody({ multipart: true }), async (ctx) => {
  2541. const id = ctx.params.id;
  2542. const {
  2543. job_type,
  2544. title,
  2545. description,
  2546. requirements,
  2547. languages,
  2548. job_time,
  2549. tasks,
  2550. location,
  2551. vacants,
  2552. salary
  2553. } = ctx.request.body;
  2554. const imageBlob = ctx.request.files?.image
  2555. ? await handleBlobUpload(ctx, 'image')
  2556. : undefined;
  2557. await jobsModel.updateJob(id, {
  2558. job_type,
  2559. title,
  2560. description,
  2561. requirements,
  2562. languages,
  2563. job_time,
  2564. tasks,
  2565. location,
  2566. vacants: vacants ? parseInt(vacants, 10) : undefined,
  2567. salary: salary != null && salary !== '' ? parseFloat(salary) : undefined,
  2568. image: imageBlob
  2569. });
  2570. ctx.redirect('/jobs?filter=MINE');
  2571. })
  2572. .post('/jobs/delete/:id', koaBody(), async (ctx) => {
  2573. const id = ctx.params.id;
  2574. await jobsModel.deleteJob(id);
  2575. ctx.redirect('/jobs?filter=MINE');
  2576. })
  2577. .post('/jobs/status/:id', koaBody(), async (ctx) => {
  2578. const id = ctx.params.id;
  2579. const { status } = ctx.request.body;
  2580. await jobsModel.updateJobStatus(id, String(status).toUpperCase());
  2581. ctx.redirect('/jobs?filter=MINE');
  2582. })
  2583. .post('/jobs/subscribe/:id', koaBody(), async (ctx) => {
  2584. const rawId = ctx.params.id;
  2585. const userId = SSBconfig.config.keys.id;
  2586. const latestId = await jobsModel.getJobTipId(rawId);
  2587. const job = await jobsModel.getJobById(latestId);
  2588. const subs = Array.isArray(job.subscribers) ? job.subscribers.slice() : [];
  2589. if (!subs.includes(userId)) subs.push(userId);
  2590. await jobsModel.updateJob(latestId, { subscribers: subs });
  2591. const subject = 'JOB_SUBSCRIBED';
  2592. const title = job.title || '';
  2593. const text = `has subscribed to your job offer "${title}" -> /jobs/${latestId}`;
  2594. await pmModel.sendMessage([job.author], subject, text);
  2595. ctx.redirect('/jobs');
  2596. })
  2597. .post('/jobs/unsubscribe/:id', koaBody(), async (ctx) => {
  2598. const rawId = ctx.params.id;
  2599. const userId = SSBconfig.config.keys.id;
  2600. const latestId = await jobsModel.getJobTipId(rawId);
  2601. const job = await jobsModel.getJobById(latestId);
  2602. const subs = Array.isArray(job.subscribers) ? job.subscribers.slice() : [];
  2603. const next = subs.filter(uid => uid !== userId);
  2604. await jobsModel.updateJob(latestId, { subscribers: next });
  2605. const subject = 'JOB_UNSUBSCRIBED';
  2606. const title = job.title || '';
  2607. const text = `has unsubscribed from your job offer "${title}" -> /jobs/${latestId}`;
  2608. await pmModel.sendMessage([job.author], subject, text);
  2609. ctx.redirect('/jobs');
  2610. })
  2611. .post('/projects/create', koaBody({ multipart: true }), async (ctx) => {
  2612. const b = ctx.request.body || {};
  2613. const imageBlob = ctx.request.files?.image ? await handleBlobUpload(ctx, 'image') : null;
  2614. const bounties =
  2615. b.bountiesInput
  2616. ? b.bountiesInput.split('\n').filter(Boolean).map(l => {
  2617. const [t, a, d] = l.split('|');
  2618. return { title: (t || '').trim(), amount: parseFloat(a || 0) || 0, description: (d || '').trim(), milestoneIndex: null };
  2619. })
  2620. : [];
  2621. await projectsModel.createProject({
  2622. ...b,
  2623. title: b.title,
  2624. description: b.description,
  2625. goal: b.goal != null && b.goal !== '' ? parseFloat(b.goal) : 0,
  2626. deadline: b.deadline ? new Date(b.deadline).toISOString() : null,
  2627. progress: b.progress != null && b.progress !== '' ? parseInt(b.progress, 10) : 0,
  2628. bounties,
  2629. image: imageBlob
  2630. });
  2631. ctx.redirect('/projects?filter=MINE');
  2632. })
  2633. .post('/projects/update/:id', koaBody({ multipart: true }), async (ctx) => {
  2634. const id = ctx.params.id;
  2635. const b = ctx.request.body || {};
  2636. const imageBlob = ctx.request.files?.image ? await handleBlobUpload(ctx, 'image') : undefined;
  2637. await projectsModel.updateProject(id, {
  2638. title: b.title,
  2639. description: b.description,
  2640. goal: b.goal !== '' && b.goal != null ? parseFloat(b.goal) : undefined,
  2641. deadline: b.deadline ? new Date(b.deadline).toISOString() : undefined,
  2642. progress: b.progress !== '' && b.progress != null ? parseInt(b.progress, 10) : undefined,
  2643. bounties: b.bountiesInput !== undefined
  2644. ? b.bountiesInput.split('\n').filter(Boolean).map(l => {
  2645. const [t, a, d] = l.split('|');
  2646. return { title: (t || '').trim(), amount: parseFloat(a || 0) || 0, description: (d || '').trim(), milestoneIndex: null };
  2647. })
  2648. : undefined,
  2649. image: imageBlob
  2650. });
  2651. ctx.redirect('/projects?filter=MINE');
  2652. })
  2653. .post('/projects/delete/:id', koaBody(), async (ctx) => {
  2654. await projectsModel.deleteProject(ctx.params.id);
  2655. ctx.redirect('/projects?filter=MINE');
  2656. })
  2657. .post('/projects/status/:id', koaBody(), async (ctx) => {
  2658. await projectsModel.updateProjectStatus(ctx.params.id, String(ctx.request.body.status || '').toUpperCase());
  2659. ctx.redirect('/projects?filter=MINE');
  2660. })
  2661. .post('/projects/progress/:id', koaBody(), async (ctx) => {
  2662. const { progress } = ctx.request.body;
  2663. await projectsModel.updateProjectProgress(ctx.params.id, progress);
  2664. ctx.redirect(`/projects/${encodeURIComponent(ctx.params.id)}`);
  2665. })
  2666. .post('/projects/pledge/:id', koaBody(), async (ctx) => {
  2667. const rawId = ctx.params.id;
  2668. const latestId = await projectsModel.getProjectTipId(rawId);
  2669. const { amount, milestoneOrBounty = '' } = ctx.request.body;
  2670. const pledgeAmount = parseFloat(amount);
  2671. if (isNaN(pledgeAmount) || pledgeAmount <= 0) ctx.throw(400, 'Invalid amount');
  2672. const userId = SSBconfig.config.keys.id;
  2673. const project = await projectsModel.getProjectById(latestId);
  2674. if (project.author === userId) ctx.throw(403, 'Authors cannot pledge to their own project');
  2675. let milestoneIndex = null;
  2676. let bountyIndex = null;
  2677. if (milestoneOrBounty.startsWith('milestone:')) {
  2678. milestoneIndex = parseInt(milestoneOrBounty.split(':')[1], 10);
  2679. } else if (milestoneOrBounty.startsWith('bounty:')) {
  2680. bountyIndex = parseInt(milestoneOrBounty.split(':')[1], 10);
  2681. }
  2682. const deadlineISO = require('../server/node_modules/moment')().add(14, 'days').toISOString();
  2683. const tags = ['backer-pledge', `project:${latestId}`];
  2684. const transfer = await transfersModel.createTransfer(
  2685. project.author,
  2686. 'Project Pledge',
  2687. pledgeAmount,
  2688. deadlineISO,
  2689. tags
  2690. );
  2691. const transferId = transfer.key || transfer.id;
  2692. const backers = Array.isArray(project.backers) ? project.backers.slice() : [];
  2693. backers.push({
  2694. userId,
  2695. amount: pledgeAmount,
  2696. at: new Date().toISOString(),
  2697. transferId,
  2698. confirmed: false,
  2699. milestoneIndex,
  2700. bountyIndex
  2701. });
  2702. const pledged = (parseFloat(project.pledged || 0) || 0) + pledgeAmount;
  2703. const goalProgress = project.goal ? (pledged / parseFloat(project.goal)) * 100 : 0;
  2704. await projectsModel.updateProject(latestId, { backers, pledged, progress: goalProgress });
  2705. const subject = 'PROJECT_PLEDGE';
  2706. const title = project.title || '';
  2707. const text = `has pledged ${pledgeAmount} ECO to your project "${title}" -> /projects/${latestId}`;
  2708. await pmModel.sendMessage([project.author], subject, text);
  2709. ctx.redirect(`/projects/${encodeURIComponent(latestId)}`);
  2710. })
  2711. .post('/projects/confirm-transfer/:id', koaBody(), async (ctx) => {
  2712. const transferId = ctx.params.id;
  2713. const userId = SSBconfig.config.keys.id;
  2714. const transfer = await transfersModel.getTransferById(transferId);
  2715. if (transfer.to !== userId) ctx.throw(403, 'Unauthorized action');
  2716. const tagProject = (transfer.tags || []).find(t => String(t).startsWith('project:'));
  2717. if (!tagProject) ctx.throw(400, 'Missing project tag on transfer');
  2718. const projectId = tagProject.split(':')[1];
  2719. await transfersModel.confirmTransferById(transferId);
  2720. const project = await projectsModel.getProjectById(projectId);
  2721. const backers = Array.isArray(project.backers) ? project.backers.slice() : [];
  2722. const idx = backers.findIndex(b => b.transferId === transferId);
  2723. if (idx !== -1) backers[idx].confirmed = true;
  2724. const goalProgress = project.goal ? (parseFloat(project.pledged || 0) / parseFloat(project.goal)) * 100 : 0;
  2725. await projectsModel.updateProject(projectId, { backers, progress: goalProgress });
  2726. ctx.redirect(`/projects/${encodeURIComponent(projectId)}`);
  2727. })
  2728. .post('/projects/follow/:id', koaBody(), async (ctx) => {
  2729. const userId = SSBconfig.config.keys.id;
  2730. const rawId = ctx.params.id;
  2731. const latestId = await projectsModel.getProjectTipId(rawId);
  2732. const project = await projectsModel.getProjectById(latestId);
  2733. await projectsModel.followProject(rawId, userId);
  2734. const subject = 'PROJECT_FOLLOWED';
  2735. const title = project.title || '';
  2736. const text = `has followed your project "${title}" -> /projects/${latestId}`;
  2737. await pmModel.sendMessage([project.author], subject, text);
  2738. ctx.redirect('/projects');
  2739. })
  2740. .post('/projects/unfollow/:id', koaBody(), async (ctx) => {
  2741. const userId = SSBconfig.config.keys.id;
  2742. const rawId = ctx.params.id;
  2743. const latestId = await projectsModel.getProjectTipId(rawId);
  2744. const project = await projectsModel.getProjectById(latestId);
  2745. await projectsModel.unfollowProject(rawId, userId);
  2746. const subject = 'PROJECT_UNFOLLOWED';
  2747. const title = project.title || '';
  2748. const text = `has unfollowed your project "${title}" -> /projects/${latestId}`;
  2749. await pmModel.sendMessage([project.author], subject, text);
  2750. ctx.redirect('/projects');
  2751. })
  2752. .post('/projects/milestones/add/:id', koaBody(), async (ctx) => {
  2753. const { title, description, targetPercent, dueDate } = ctx.request.body;
  2754. await projectsModel.addMilestone(ctx.params.id, {
  2755. title,
  2756. description: description || '',
  2757. targetPercent: targetPercent != null && targetPercent !== '' ? parseInt(targetPercent, 10) : 0,
  2758. dueDate: dueDate ? new Date(dueDate).toISOString() : null
  2759. });
  2760. ctx.redirect(`/projects/${encodeURIComponent(ctx.params.id)}`);
  2761. })
  2762. .post('/projects/milestones/update/:id/:index', koaBody(), async (ctx) => {
  2763. const { title, description, targetPercent, dueDate, done } = ctx.request.body;
  2764. await projectsModel.updateMilestone(
  2765. ctx.params.id,
  2766. parseInt(ctx.params.index, 10),
  2767. {
  2768. title,
  2769. ...(description !== undefined ? { description } : {}),
  2770. targetPercent: targetPercent !== undefined && targetPercent !== '' ? parseInt(targetPercent, 10) : undefined,
  2771. dueDate: dueDate !== undefined ? (dueDate ? new Date(dueDate).toISOString() : null) : undefined,
  2772. done: done !== undefined ? !!done : undefined
  2773. }
  2774. );
  2775. ctx.redirect(`/projects/${encodeURIComponent(ctx.params.id)}`);
  2776. })
  2777. .post('/projects/milestones/complete/:id/:index', koaBody(), async (ctx) => {
  2778. const userId = SSBconfig.config.keys.id;
  2779. await projectsModel.completeMilestone(ctx.params.id, parseInt(ctx.params.index, 10), userId);
  2780. ctx.redirect(`/projects/${encodeURIComponent(ctx.params.id)}`);
  2781. })
  2782. .post('/projects/bounties/add/:id', koaBody(), async (ctx) => {
  2783. const { title, amount, description, milestoneIndex } = ctx.request.body;
  2784. await projectsModel.addBounty(ctx.params.id, {
  2785. title,
  2786. amount,
  2787. description,
  2788. milestoneIndex: (milestoneIndex === '' || milestoneIndex === undefined) ? null : parseInt(milestoneIndex, 10)
  2789. });
  2790. ctx.redirect(`/projects/${encodeURIComponent(ctx.params.id)}`);
  2791. })
  2792. .post('/projects/bounties/update/:id/:index', koaBody(), async (ctx) => {
  2793. const { title, amount, description, milestoneIndex, done } = ctx.request.body;
  2794. await projectsModel.updateBounty(
  2795. ctx.params.id,
  2796. parseInt(ctx.params.index, 10),
  2797. {
  2798. title: title !== undefined ? title : undefined,
  2799. amount: amount !== undefined && amount !== '' ? parseFloat(amount) : undefined,
  2800. description: description !== undefined ? description : undefined,
  2801. milestoneIndex: milestoneIndex !== undefined ? (milestoneIndex === '' ? null : parseInt(milestoneIndex, 10)) : undefined,
  2802. done: done !== undefined ? !!done : undefined
  2803. }
  2804. );
  2805. ctx.redirect(`/projects/${encodeURIComponent(ctx.params.id)}`);
  2806. })
  2807. .post('/projects/bounties/claim/:id/:index', koaBody(), async (ctx) => {
  2808. const userId = SSBconfig.config.keys.id;
  2809. await projectsModel.claimBounty(ctx.params.id, parseInt(ctx.params.index, 10), userId);
  2810. ctx.redirect(`/projects/${encodeURIComponent(ctx.params.id)}`);
  2811. })
  2812. .post('/projects/bounties/complete/:id/:index', koaBody(), async (ctx) => {
  2813. const userId = SSBconfig.config.keys.id;
  2814. await projectsModel.completeBounty(ctx.params.id, parseInt(ctx.params.index, 10), userId);
  2815. ctx.redirect(`/projects/${encodeURIComponent(ctx.params.id)}`);
  2816. })
  2817. .post("/banking/claim/:id", koaBody(), async (ctx) => {
  2818. const userId = SSBconfig.config.keys.id;
  2819. const allocationId = ctx.params.id;
  2820. const allocation = await bankingModel.getAllocationById(allocationId);
  2821. if (!allocation) {
  2822. ctx.body = { error: i18n.errorNoAllocation };
  2823. return;
  2824. }
  2825. if (allocation.to !== userId || allocation.status !== "UNCONFIRMED") {
  2826. ctx.body = { error: i18n.errorInvalidClaim };
  2827. return;
  2828. }
  2829. const pubWalletConfig = getConfig().walletPub;
  2830. const { url, user, pass } = pubWalletConfig;
  2831. const { txid } = await bankingModel.claimAllocation({
  2832. transferId: allocationId,
  2833. claimerId: userId,
  2834. pubWalletUrl: url,
  2835. pubWalletUser: user,
  2836. pubWalletPass: pass,
  2837. });
  2838. await bankingModel.updateAllocationStatus(allocationId, "CLOSED", txid);
  2839. await bankingModel.publishBankClaim({
  2840. amount: allocation.amount,
  2841. epochId: allocation.epochId,
  2842. allocationId: allocation.id,
  2843. txid,
  2844. });
  2845. ctx.redirect(`/banking?claimed=${encodeURIComponent(txid)}`);
  2846. })
  2847. .post("/banking/simulate", koaBody(), async (ctx) => {
  2848. const epochId = ctx.request.body?.epochId || undefined;
  2849. const rules = ctx.request.body?.rules || undefined;
  2850. const { epoch, allocations } = await bankingModel.computeEpoch({ epochId: epochId || undefined, rules });
  2851. ctx.body = { epoch, allocations };
  2852. })
  2853. .post("/banking/run", koaBody(), async (ctx) => {
  2854. const epochId = ctx.request.body?.epochId || undefined;
  2855. const rules = ctx.request.body?.rules || undefined;
  2856. const { epoch, allocations } = await bankingModel.executeEpoch({ epochId: epochId || undefined, rules });
  2857. ctx.body = { epoch, allocations };
  2858. })
  2859. .post("/banking/addresses", koaBody(), async (ctx) => {
  2860. const userId = (ctx.request.body?.userId || "").trim();
  2861. const address = (ctx.request.body?.address || "").trim();
  2862. const res = await bankingModel.addAddress({ userId, address });
  2863. ctx.redirect(`/banking?filter=addresses&msg=${encodeURIComponent(res.status)}`);
  2864. })
  2865. .post("/banking/addresses/delete", koaBody(), async (ctx) => {
  2866. const userId = (ctx.request.body?.userId || "").trim();
  2867. const res = await bankingModel.removeAddress({ userId });
  2868. ctx.redirect(`/banking?filter=addresses&msg=${encodeURIComponent(res.status)}`);
  2869. })
  2870. // UPDATE OASIS
  2871. .post("/update", koaBody(), async (ctx) => {
  2872. const util = require("node:util");
  2873. const exec = util.promisify(require("node:child_process").exec);
  2874. async function updateTool() {
  2875. const { stdout, stderr } = await exec("git reset --hard && git pull");
  2876. console.log("oasis@version: updating Oasis...");
  2877. console.log(stdout);
  2878. console.log(stderr);
  2879. const { stdout: shOut, stderr: shErr } = await exec("sh install.sh");
  2880. console.log("oasis@version: running install.sh...");
  2881. console.log(shOut);
  2882. console.error(shErr);
  2883. }
  2884. await updateTool();
  2885. const referer = new URL(ctx.request.header.referer);
  2886. ctx.redirect(referer.href);
  2887. })
  2888. .post("/settings/theme", koaBody(), async (ctx) => {
  2889. const theme = String(ctx.request.body.theme || "").trim();
  2890. const currentConfig = getConfig();
  2891. currentConfig.themes.current = theme || "Dark-SNH";
  2892. fs.writeFileSync(configPath, JSON.stringify(currentConfig, null, 2));
  2893. ctx.cookies.set("theme", currentConfig.themes.current);
  2894. ctx.redirect("/settings");
  2895. })
  2896. .post("/language", koaBody(), async (ctx) => {
  2897. const language = String(ctx.request.body.language);
  2898. ctx.cookies.set("language", language);
  2899. const referer = new URL(ctx.request.header.referer);
  2900. ctx.redirect(referer.href);
  2901. })
  2902. .post("/settings/conn/start", koaBody(), async (ctx) => {
  2903. await meta.connStart();
  2904. ctx.redirect("/peers");
  2905. })
  2906. .post("/settings/conn/stop", koaBody(), async (ctx) => {
  2907. await meta.connStop();
  2908. ctx.redirect("/peers");
  2909. })
  2910. .post("/settings/conn/sync", koaBody(), async (ctx) => {
  2911. await meta.sync();
  2912. ctx.redirect("/peers");
  2913. })
  2914. .post("/settings/conn/restart", koaBody(), async (ctx) => {
  2915. await meta.connRestart();
  2916. ctx.redirect("/peers");
  2917. })
  2918. .post("/settings/invite/accept", koaBody(), async (ctx) => {
  2919. const invite = String(ctx.request.body.invite);
  2920. await meta.acceptInvite(invite);
  2921. ctx.redirect("/invites");
  2922. })
  2923. .post("/settings/invite/unfollow", koaBody(), async (ctx) => {
  2924. const { key } = ctx.request.body || {};
  2925. if (!key) { ctx.redirect("/invites"); return; }
  2926. const pubs = readJSON(gossipPath);
  2927. const idx = pubs.findIndex(x => x && canonicalKey(x.key) === canonicalKey(key));
  2928. let removed = null;
  2929. if (idx >= 0) {
  2930. removed = pubs.splice(idx, 1)[0];
  2931. writeJSON(gossipPath, pubs);
  2932. }
  2933. const ssb = await cooler.open();
  2934. let addr = null;
  2935. if (removed && removed.host) addr = msAddrFrom(removed.host, removed.port, removed.key);
  2936. if (addr) {
  2937. try { await new Promise(res => ssb.conn.disconnect(addr, res)); } catch {}
  2938. try { ssb.conn.forget(addr); } catch {}
  2939. }
  2940. try {
  2941. await new Promise((resolve, reject) => {
  2942. ssb.publish({ type: "contact", contact: canonicalKey(key), following: false, blocking: true }, (err) => err ? reject(err) : resolve());
  2943. });
  2944. } catch {}
  2945. const unf = readJSON(unfollowedPath);
  2946. if (removed && !unf.find(x => x && canonicalKey(x.key) === canonicalKey(removed.key))) {
  2947. unf.push(removed);
  2948. writeJSON(unfollowedPath, unf);
  2949. } else if (!removed && !unf.find(x => x && canonicalKey(x.key) === canonicalKey(key))) {
  2950. unf.push({ key: canonicalKey(key) });
  2951. writeJSON(unfollowedPath, unf);
  2952. }
  2953. ctx.redirect("/invites");
  2954. })
  2955. .post("/settings/invite/follow", koaBody(), async (ctx) => {
  2956. const { key, host, port } = ctx.request.body || {};
  2957. if (!key || !host) { ctx.redirect("/invites"); return; }
  2958. const isInErrorState = (host) => {
  2959. const pubs = readJSON(gossipPath);
  2960. const pub = pubs.find(p => p.host === host);
  2961. return pub && pub.error;
  2962. };
  2963. if (isInErrorState(host)) {
  2964. ctx.redirect("/invites");
  2965. return;
  2966. }
  2967. const ssb = await cooler.open();
  2968. const unf = readJSON(unfollowedPath);
  2969. const kcanon = canonicalKey(key);
  2970. const saved = unf.find(x => x && canonicalKey(x.key) === kcanon);
  2971. const rec = saved || { host, port: Number(port) || 8008, key: kcanon };
  2972. const pubs = readJSON(gossipPath);
  2973. if (!pubs.find(x => x && canonicalKey(x.key) === kcanon)) {
  2974. pubs.push({ host: rec.host, port: Number(rec.port) || 8008, key: kcanon });
  2975. writeJSON(gossipPath, pubs);
  2976. }
  2977. const addr = msAddrFrom(rec.host, rec.port, kcanon);
  2978. try { ssb.conn.remember(addr, { type: "pub", autoconnect: true, key: kcanon }); } catch {}
  2979. try { await new Promise(res => ssb.conn.connect(addr, { type: "pub" }, res)); } catch {}
  2980. try {
  2981. await new Promise((resolve, reject) => {
  2982. ssb.publish({ type: "contact", contact: kcanon, blocking: false }, (err) => err ? reject(err) : resolve());
  2983. });
  2984. } catch {}
  2985. const nextUnf = unf.filter(x => !(x && canonicalKey(x.key) === kcanon));
  2986. writeJSON(unfollowedPath, nextUnf);
  2987. ctx.redirect("/invites");
  2988. })
  2989. .post("/settings/ssb-logstream", koaBody(), async (ctx) => {
  2990. const logLimit = parseInt(ctx.request.body.ssb_log_limit, 10);
  2991. if (!isNaN(logLimit) && logLimit > 0 && logLimit <= 100000) {
  2992. const configData = fs.readFileSync(configPath, 'utf8');
  2993. const config = JSON.parse(configData);
  2994. if (!config.ssbLogStream) config.ssbLogStream = {};
  2995. config.ssbLogStream.limit = logLimit;
  2996. fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
  2997. }
  2998. ctx.redirect("/settings");
  2999. })
  3000. .post("/settings/rebuild", async (ctx) => {
  3001. meta.rebuild();
  3002. ctx.redirect("/settings");
  3003. })
  3004. .post("/save-modules", koaBody(), async (ctx) => {
  3005. const modules = [
  3006. 'popular', 'topics', 'summaries', 'latest', 'threads', 'multiverse', 'invites', 'wallet',
  3007. 'legacy', 'cipher', 'bookmarks', 'videos', 'docs', 'audios', 'tags', 'images', 'trending',
  3008. 'events', 'tasks', 'market', 'tribes', 'governance', 'reports', 'opinions', 'transfers',
  3009. 'feed', 'pixelia', 'agenda', 'ai', 'forum', 'jobs', 'projects', 'banking'
  3010. ];
  3011. const currentConfig = getConfig();
  3012. modules.forEach(mod => {
  3013. const modKey = `${mod}Mod`;
  3014. const formKey = `${mod}Form`;
  3015. const modValue = ctx.request.body[formKey] === 'on' ? 'on' : 'off';
  3016. currentConfig.modules[modKey] = modValue;
  3017. });
  3018. saveConfig(currentConfig);
  3019. ctx.redirect(`/modules`);
  3020. })
  3021. .post("/settings/ai", koaBody(), async (ctx) => {
  3022. const aiPrompt = String(ctx.request.body.ai_prompt || "").trim();
  3023. if (aiPrompt.length > 128) {
  3024. ctx.status = 400;
  3025. ctx.body = "Prompt too long. Must be 128 characters or fewer.";
  3026. return;
  3027. }
  3028. const currentConfig = getConfig();
  3029. currentConfig.ai = currentConfig.ai || {};
  3030. currentConfig.ai.prompt = aiPrompt;
  3031. saveConfig(currentConfig);
  3032. const referer = new URL(ctx.request.header.referer);
  3033. ctx.redirect("/settings");
  3034. })
  3035. .post("/settings/pub-wallet", koaBody(), async (ctx) => {
  3036. const walletUrl = String(ctx.request.body.wallet_url || "").trim();
  3037. const walletUser = String(ctx.request.body.wallet_user || "").trim();
  3038. const walletPass = String(ctx.request.body.wallet_pass || "").trim();
  3039. const currentConfig = getConfig();
  3040. currentConfig.walletPub = {
  3041. url: walletUrl,
  3042. user: walletUser,
  3043. pass: walletPass
  3044. };
  3045. fs.writeFileSync(configPath, JSON.stringify(currentConfig, null, 2));
  3046. ctx.redirect("/settings");
  3047. })
  3048. .post('/transfers/create',
  3049. koaBody(),
  3050. async ctx => {
  3051. const { to, concept, amount, deadline, tags } = ctx.request.body
  3052. await transfersModel.createTransfer(to, concept, amount, deadline, tags)
  3053. ctx.redirect('/transfers')
  3054. })
  3055. .post('/transfers/update/:id',
  3056. koaBody(),
  3057. async ctx => {
  3058. const { to, concept, amount, deadline, tags } = ctx.request.body
  3059. await transfersModel.updateTransferById(
  3060. ctx.params.id, to, concept, amount, deadline, tags
  3061. )
  3062. ctx.redirect('/transfers?filter=mine')
  3063. })
  3064. .post('/transfers/confirm/:id', async ctx => {
  3065. await transfersModel.confirmTransferById(ctx.params.id)
  3066. ctx.redirect('/transfers')
  3067. })
  3068. .post('/transfers/delete/:id', async ctx => {
  3069. await transfersModel.deleteTransferById(ctx.params.id)
  3070. ctx.redirect('/transfers?filter=mine')
  3071. })
  3072. .post('/transfers/opinions/:transferId/:category', async ctx => {
  3073. const { transferId, category } = ctx.params
  3074. const voterId = SSBconfig?.keys?.id;
  3075. const t = await transfersModel.getTransferById(transferId)
  3076. if (t.opinions_inhabitants.includes(voterId)) {
  3077. ctx.flash = { message: 'You have already opined.' }
  3078. ctx.redirect('/transfers')
  3079. return
  3080. }
  3081. await opinionsModel.createVote(transferId, category, 'transfer')
  3082. ctx.redirect('/transfers')
  3083. })
  3084. .post("/settings/wallet", koaBody(), async (ctx) => {
  3085. const url = String(ctx.request.body.wallet_url);
  3086. const user = String(ctx.request.body.wallet_user);
  3087. const pass = String(ctx.request.body.wallet_pass);
  3088. const fee = String(ctx.request.body.wallet_fee);
  3089. const currentConfig = getConfig();
  3090. if (url) currentConfig.wallet.url = url;
  3091. if (user) currentConfig.wallet.user = user;
  3092. if (pass) currentConfig.wallet.pass = pass;
  3093. if (fee) currentConfig.wallet.fee = fee;
  3094. saveConfig(currentConfig);
  3095. const res = await bankingModel.ensureSelfAddressPublished();
  3096. ctx.redirect(`/banking?filter=addresses&msg=${encodeURIComponent(res.status)}`);
  3097. })
  3098. .post("/wallet/send", koaBody(), async (ctx) => {
  3099. const action = String(ctx.request.body.action);
  3100. const destination = String(ctx.request.body.destination);
  3101. const amount = Number(ctx.request.body.amount);
  3102. const fee = Number(ctx.request.body.fee);
  3103. const { url, user, pass } = getConfig().wallet;
  3104. let balance = null
  3105. try {
  3106. balance = await walletModel.getBalance(url, user, pass);
  3107. } catch (error) {
  3108. ctx.body = await walletErrorView(error);
  3109. }
  3110. switch (action) {
  3111. case 'confirm':
  3112. const validation = await walletModel.validateSend(url, user, pass, destination, amount, fee);
  3113. if (validation.isValid) {
  3114. try {
  3115. ctx.body = await walletSendConfirmView(balance, destination, amount, fee);
  3116. } catch (error) {
  3117. ctx.body = await walletErrorView(error);
  3118. }
  3119. } else {
  3120. try {
  3121. const statusMessages = {
  3122. type: 'error',
  3123. title: 'validation_errors',
  3124. messages: validation.errors,
  3125. }
  3126. ctx.body = await walletSendFormView(balance, destination, amount, fee, statusMessages);
  3127. } catch (error) {
  3128. ctx.body = await walletErrorView(error);
  3129. }
  3130. }
  3131. break;
  3132. case 'send':
  3133. try {
  3134. const txId = await walletModel.sendToAddress(url, user, pass, destination, amount);
  3135. ctx.body = await walletSendResultView(balance, destination, amount, txId);
  3136. } catch (error) {
  3137. ctx.body = await walletErrorView(error);
  3138. }
  3139. break;
  3140. }
  3141. });
  3142. const routes = router.routes();
  3143. const middleware = [
  3144. async (ctx, next) => {
  3145. if (config.public && ctx.method !== "GET") {
  3146. throw new Error(
  3147. "Sorry, many actions are unavailable when Oasis is running in public mode. Please run Oasis in the default mode and try again."
  3148. );
  3149. }
  3150. await next();
  3151. },
  3152. async (ctx, next) => {
  3153. const selectedLanguage = ctx.cookies.get("language") || "en";
  3154. setLanguage(selectedLanguage);
  3155. await next();
  3156. },
  3157. async (ctx, next) => {
  3158. const ssb = await cooler.open();
  3159. const status = await ssb.status();
  3160. const values = Object.values(status.sync.plugins);
  3161. const totalCurrent = Object.values(status.sync.plugins).reduce(
  3162. (acc, cur) => acc + cur,
  3163. 0
  3164. );
  3165. const totalTarget = status.sync.since * values.length;
  3166. const left = totalTarget - totalCurrent;
  3167. const percent = Math.floor((totalCurrent / totalTarget) * 1000) / 10;
  3168. const megabyte = 1024 * 1024;
  3169. if (left > megabyte) {
  3170. ctx.response.body = indexingView({ percent });
  3171. } else {
  3172. try {
  3173. await next();
  3174. } catch (err) {
  3175. ctx.status = err.status || 500;
  3176. ctx.body = { message: err.message || 'Internal Server Error' };
  3177. }
  3178. }
  3179. },
  3180. routes,
  3181. ];
  3182. const { allowHost } = config;
  3183. const app = http({ host, port, middleware, allowHost });
  3184. app._close = () => {
  3185. nameWarmup.close();
  3186. cooler.close();
  3187. };
  3188. module.exports = app;
  3189. if (config.open === true) {
  3190. open(url);
  3191. }