backend.js 89 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469
  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 { spawn } = require('child_process');
  39. function startAI() {
  40. const aiPath = path.resolve(__dirname, '../AI/ai_service.mjs');
  41. const aiProcess = spawn('node', [aiPath], {
  42. detached: false,
  43. stdio: 'ignore', //inherit for debug
  44. });
  45. aiProcess.unref();
  46. }
  47. const customStyleFile = path.join(
  48. envPaths("oasis", { suffix: "" }).config,
  49. "/custom-style.css"
  50. );
  51. let haveCustomStyle;
  52. try {
  53. fs.readFileSync(customStyleFile, "utf8");
  54. haveCustomStyle = true;
  55. } catch (e) {
  56. if (e.code === "ENOENT") {
  57. haveCustomStyle = false;
  58. } else {
  59. console.log(`There was a problem loading ${customStyleFile}`);
  60. throw e;
  61. }
  62. }
  63. const { get } = require("node:http");
  64. const debug = require("../server/node_modules/debug")("oasis");
  65. const log = (formatter, ...args) => {
  66. const isDebugEnabled = debug.enabled;
  67. debug.enabled = true;
  68. debug(formatter, ...args);
  69. debug.enabled = isDebugEnabled;
  70. };
  71. delete config._;
  72. delete config.$0;
  73. const { host } = config;
  74. const { port } = config;
  75. const url = `http://${host}:${port}`;
  76. debug("Current configuration: %O", config);
  77. debug(`You can save the above to ${defaultConfigFile} to make \
  78. these settings the default. See the readme for details.`);
  79. const { saveConfig, getConfig } = require('../configs/config-manager');
  80. const oasisCheckPath = "/.well-known/oasis";
  81. process.on("uncaughtException", function (err) {
  82. if (err["code"] === "EADDRINUSE") {
  83. get(url + oasisCheckPath, (res) => {
  84. let rawData = "";
  85. res.on("data", (chunk) => {
  86. rawData += chunk;
  87. });
  88. res.on("end", () => {
  89. log(rawData);
  90. if (rawData === "oasis") {
  91. log(`Oasis is already running on host ${host} and port ${port}`);
  92. if (config.open === true) {
  93. log("Opening link to existing instance of Oasis");
  94. open(url);
  95. } else {
  96. log(
  97. "Not opening your browser because opening is disabled by your config"
  98. );
  99. }
  100. process.exit(0);
  101. } else {
  102. throw new Error(`Another server is already running at ${url}.
  103. It might be another copy of Oasis or another program on your computer.
  104. You can run Oasis on a different port number with this option:
  105. oasis --port ${config.port + 1}
  106. Alternatively, you can set the default port in ${defaultConfigFile} with:
  107. {
  108. "port": ${config.port + 1}
  109. }
  110. `);
  111. }
  112. });
  113. });
  114. } else {
  115. console.log("");
  116. console.log("Oasis traceback (share below content with devs to report!):");
  117. console.log("===========================================================");
  118. console.log(err);
  119. console.log("");
  120. }
  121. });
  122. process.argv = [];
  123. const http = require("../client/middleware");
  124. const {koaBody} = require("../server/node_modules/koa-body");
  125. const { nav, ul, li, a, form, button, div } = require("../server/node_modules/hyperaxe");
  126. const open = require("../server/node_modules/open");
  127. const pull = require("../server/node_modules/pull-stream");
  128. const koaRouter = require("../server/node_modules/@koa/router");
  129. const ssbMentions = require("../server/node_modules/ssb-mentions");
  130. const isSvg = require('../server/node_modules/is-svg');
  131. const { isFeed, isMsg, isBlob } = require("../server/node_modules/ssb-ref");
  132. const ssb = require("../client/gui");
  133. const router = new koaRouter();
  134. const extractMentions = async (text) => {
  135. const mentions = ssbMentions(text) || [];
  136. const resolvedMentions = await Promise.all(mentions.map(async (mention) => {
  137. const name = mention.name || await about.name(mention.link);
  138. return {
  139. link: mention.link,
  140. name: name || 'Anonymous',
  141. };
  142. }));
  143. return resolvedMentions;
  144. };
  145. const cooler = ssb({ offline: config.offline });
  146. // load core models (cooler)
  147. const models = require("../models/main_models");
  148. const { about, blob, friend, meta, post, vote } = models({
  149. cooler,
  150. isPublic: config.public,
  151. });
  152. const { handleBlobUpload } = require('../backend/blobHandler.js');
  153. // load plugin models (static)
  154. const exportmodeModel = require('../models/exportmode_model');
  155. const panicmodeModel = require('../models/panicmode_model');
  156. const cipherModel = require('../models/cipher_model');
  157. const legacyModel = require('../models/legacy_model');
  158. const walletModel = require('../models/wallet_model')
  159. // load plugin models (cooler)
  160. const pmModel = require('../models/privatemessages_model')({ cooler, isPublic: config.public });
  161. const bookmarksModel = require("../models/bookmarking_model")({ cooler, isPublic: config.public });
  162. const opinionsModel = require('../models/opinions_model')({ cooler, isPublic: config.public });
  163. const eventsModel = require('../models/events_model')({ cooler, isPublic: config.public });
  164. const tasksModel = require('../models/tasks_model')({ cooler, isPublic: config.public });
  165. const votesModel = require('../models/votes_model')({ cooler, isPublic: config.public });
  166. const reportsModel = require('../models/reports_model')({ cooler, isPublic: config.public });
  167. const transfersModel = require('../models/transfers_model')({ cooler, isPublic: config.public });
  168. const tagsModel = require('../models/tags_model')({ cooler, isPublic: config.public });
  169. const cvModel = require('../models/cv_model')({ cooler, isPublic: config.public });
  170. const inhabitantsModel = require('../models/inhabitants_model')({ cooler, isPublic: config.public });
  171. const feedModel = require('../models/feed_model')({ cooler, isPublic: config.public });
  172. const imagesModel = require("../models/images_model")({ cooler, isPublic: config.public });
  173. const audiosModel = require("../models/audios_model")({ cooler, isPublic: config.public });
  174. const videosModel = require("../models/videos_model")({ cooler, isPublic: config.public });
  175. const documentsModel = require("../models/documents_model")({ cooler, isPublic: config.public });
  176. const agendaModel = require("../models/agenda_model")({ cooler, isPublic: config.public });
  177. const trendingModel = require('../models/trending_model')({ cooler, isPublic: config.public });
  178. const statsModel = require('../models/stats_model')({ cooler, isPublic: config.public });
  179. const tribesModel = require('../models/tribes_model')({ cooler, isPublic: config.public });
  180. const searchModel = require('../models/search_model')({ cooler, isPublic: config.public });
  181. const activityModel = require('../models/activity_model')({ cooler, isPublic: config.public });
  182. const pixeliaModel = require('../models/pixelia_model')({ cooler, isPublic: config.public });
  183. const marketModel = require('../models/market_model')({ cooler, isPublic: config.public });
  184. const forumModel = require('../models/forum_model')({ cooler, isPublic: config.public });
  185. // starting warmup
  186. about._startNameWarmup();
  187. async function renderBlobMarkdown(text, mentions = {}, myFeedId, myUsername) {
  188. if (!text) return '';
  189. const mentionByFeed = {};
  190. Object.values(mentions).forEach(arr => {
  191. arr.forEach(m => {
  192. mentionByFeed[m.feed] = m;
  193. });
  194. });
  195. text = text.replace(/\[@([^\]]+)\]\(([^)]+)\)/g, (_, name, id) => {
  196. return `<a class="mention" href="/author/${encodeURIComponent(id)}">@${myUsername}</a>`;
  197. });
  198. const mentionRegex = /@([A-Za-z0-9_\-\.+=\/]+\.ed25519)/g;
  199. const words = text.split(' ');
  200. text = (await Promise.all(
  201. words.map(async (word) => {
  202. const match = mentionRegex.exec(word);
  203. if (match && match[1]) {
  204. const feedId = match[1];
  205. if (feedId === myFeedId) {
  206. return `<a class="mention" href="/author/${encodeURIComponent(feedId)}">@${myUsername}</a>`;
  207. }
  208. }
  209. return word;
  210. })
  211. )).join(' ');
  212. text = text
  213. .replace(/!\[image:[^\]]+\]\(([^)]+)\)/g, (_, id) =>
  214. `<img src="/blob/${encodeURIComponent(id)}" alt="image" class="post-image" />`)
  215. .replace(/\[audio:[^\]]+\]\(([^)]+)\)/g, (_, id) =>
  216. `<audio controls class="post-audio" src="/blob/${encodeURIComponent(id)}"></audio>`)
  217. .replace(/\[video:[^\]]+\]\(([^)]+)\)/g, (_, id) =>
  218. `<video controls class="post-video" src="/blob/${encodeURIComponent(id)}"></video>`)
  219. .replace(/\[pdf:[^\]]+\]\(([^)]+)\)/g, (_, id) =>
  220. `<a class="post-pdf" href="/blob/${encodeURIComponent(id)}" target="_blank">PDF</a>`);
  221. return text;
  222. }
  223. let formattedTextCache = null;
  224. const preparePreview = async function (ctx) {
  225. let text = String(ctx.request.body.text || "");
  226. const mentions = {};
  227. const rex = /(^|\s)(?!\[)@([a-zA-Z0-9\-/.=+]{3,})\b/g;
  228. let m;
  229. while ((m = rex.exec(text)) !== null) {
  230. const token = m[2];
  231. const key = token;
  232. let found = mentions[key] || [];
  233. if (/\.ed25519$/.test(token)) {
  234. const name = await about.name(token);
  235. const img = await about.image(token);
  236. found.push({
  237. feed: token,
  238. name,
  239. img,
  240. rel: { followsMe: false, following: false, blocking: false, me: false }
  241. });
  242. } else {
  243. const matches = about.named(token);
  244. for (const match of matches) {
  245. found.push(match);
  246. }
  247. }
  248. if (found.length > 0) {
  249. mentions[key] = found;
  250. }
  251. }
  252. Object.keys(mentions).forEach((key) => {
  253. let matches = mentions[key];
  254. const meaningful = matches.filter((m) => (m.rel?.followsMe || m.rel?.following) && !m.rel?.blocking);
  255. mentions[key] = meaningful.length > 0 ? meaningful : matches;
  256. });
  257. const replacer = (match, prefix, token) => {
  258. const matches = mentions[token];
  259. if (matches && matches.length === 1) {
  260. return `${prefix}[@${matches[0].name}](${matches[0].feed})`;
  261. }
  262. return match;
  263. };
  264. text = text.replace(rex, replacer);
  265. const blobMarkdown = await handleBlobUpload(ctx, "blob");
  266. if (blobMarkdown) {
  267. text += blobMarkdown;
  268. }
  269. const ssbClient = await cooler.open();
  270. const authorMeta = {
  271. id: ssbClient.id,
  272. name: await about.name(ssbClient.id),
  273. image: await about.image(ssbClient.id),
  274. };
  275. const renderedText = await renderBlobMarkdown(text, mentions, authorMeta.id, authorMeta.name);
  276. const hasBrTags = /<br\s*\/?>/.test(renderedText);
  277. const formattedText = formattedTextCache || (!hasBrTags ? renderedText.replace(/\n/g, '<br>') : renderedText);
  278. if (!formattedTextCache && !hasBrTags) {
  279. formattedTextCache = formattedText;
  280. }
  281. const contentWarning = ctx.request.body.contentWarning || '';
  282. let finalContent = formattedText;
  283. if (contentWarning && !finalContent.startsWith(contentWarning)) {
  284. finalContent = `<br>${finalContent}`;
  285. }
  286. return { authorMeta, text: renderedText, formattedText: finalContent, mentions };
  287. };
  288. // set koaMiddleware maxSize: 50 MiB (voted by community at: 09/04/2025)
  289. const megabyte = Math.pow(2, 20);
  290. const maxSize = 50 * megabyte;
  291. // koaMiddleware to manage files
  292. const homeDir = os.homedir();
  293. const blobsPath = path.join(homeDir, '.ssb', 'blobs', 'tmp');
  294. const koaBodyMiddleware = koaBody({
  295. multipart: true,
  296. formidable: {
  297. uploadDir: blobsPath,
  298. keepExtensions: true,
  299. maxFieldsSize: maxSize,
  300. hash: 'sha256',
  301. },
  302. parsedMethods: ['POST'],
  303. });
  304. const resolveCommentComponents = async function (ctx) {
  305. let parentId;
  306. try {
  307. parentId = decodeURIComponent(ctx.params.message);
  308. } catch {
  309. parentId = ctx.params.message;
  310. }
  311. const parentMessage = await post.get(parentId);
  312. if (!parentMessage || !parentMessage.value) {
  313. throw new Error("Invalid parentMessage or missing 'value'");
  314. }
  315. const myFeedId = await meta.myFeedId();
  316. const hasRoot =
  317. typeof parentMessage?.value?.content?.root === "string" &&
  318. ssbRef.isMsg(parentMessage.value.content.root);
  319. const hasFork =
  320. typeof parentMessage?.value?.content?.fork === "string" &&
  321. ssbRef.isMsg(parentMessage.value.content.fork);
  322. const rootMessage = hasRoot
  323. ? hasFork
  324. ? parentMessage
  325. : await post.get(parentMessage.value.content.root)
  326. : parentMessage;
  327. const messages = await post.topicComments(rootMessage.key);
  328. messages.push(rootMessage);
  329. let contentWarning;
  330. if (ctx.request.body) {
  331. const rawContentWarning = String(ctx.request.body.contentWarning || "").trim();
  332. contentWarning = rawContentWarning.length > 0 ? rawContentWarning : undefined;
  333. }
  334. return { messages, myFeedId, parentMessage, contentWarning };
  335. };
  336. // import views (core)
  337. 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");
  338. // import views (modules)
  339. const { activityView } = require("../views/activity_view");
  340. const { cvView, createCVView } = require("../views/cv_view");
  341. const { indexingView } = require("../views/indexing_view");
  342. const { pixeliaView } = require("../views/pixelia_view");
  343. const { statsView } = require("../views/stats_view");
  344. const { tribesView, tribeDetailView, tribesInvitesView, tribeView, renderInvitePage } = require("../views/tribes_view");
  345. const { agendaView } = require("../views/agenda_view");
  346. const { documentView, singleDocumentView } = require("../views/document_view");
  347. const { inhabitantsView, inhabitantsProfileView } = require("../views/inhabitants_view");
  348. const { walletViewRender, walletView, walletHistoryView, walletReceiveView, walletSendFormView, walletSendConfirmView, walletSendResultView, walletErrorView } = require("../views/wallet_view");
  349. const { pmView } = require("../views/pm_view");
  350. const { tagsView } = require("../views/tags_view");
  351. const { videoView, singleVideoView } = require("../views/video_view");
  352. const { audioView, singleAudioView } = require("../views/audio_view");
  353. const { eventView, singleEventView } = require("../views/event_view");
  354. const { invitesView } = require("../views/invites_view");
  355. const { modulesView } = require("../views/modules_view");
  356. const { reportView, singleReportView } = require("../views/report_view");
  357. const { taskView, singleTaskView } = require("../views/task_view");
  358. const { voteView } = require("../views/vote_view");
  359. const { bookmarkView, singleBookmarkView } = require("../views/bookmark_view");
  360. const { feedView, feedCreateView } = require("../views/feed_view");
  361. const { legacyView } = require("../views/legacy_view");
  362. const { opinionsView } = require("../views/opinions_view");
  363. const { peersView } = require("../views/peers_view");
  364. const { searchView } = require("../views/search_view");
  365. const { transferView, singleTransferView } = require("../views/transfer_view");
  366. const { cipherView } = require("../views/cipher_view");
  367. const { imageView, singleImageView } = require("../views/image_view");
  368. const { settingsView } = require("../views/settings_view");
  369. const { trendingView } = require("../views/trending_view");
  370. const { marketView, singleMarketView } = require("../views/market_view");
  371. const { aiView } = require("../views/AI_view");
  372. const { forumView, singleForumView } = require("../views/forum_view");
  373. let sharp;
  374. try {
  375. sharp = require("sharp");
  376. } catch (e) {
  377. // Optional dependency
  378. }
  379. const readmePath = path.join(__dirname, "..", ".." ,"README.md");
  380. const packagePath = path.join(__dirname, "..", "server", "package.json");
  381. const readme = fs.readFileSync(readmePath, "utf8");
  382. const version = JSON.parse(fs.readFileSync(packagePath, "utf8")).version;
  383. const nullImageId = '&0000000000000000000000000000000000000000000=.sha256';
  384. const getAvatarUrl = (image) => {
  385. if (!image || image === nullImageId) {
  386. return '/assets/images/default-avatar.png';
  387. }
  388. return `/image/256/${encodeURIComponent(image)}`;
  389. };
  390. router
  391. .param("imageSize", (imageSize, ctx, next) => {
  392. const size = Number(imageSize);
  393. const isInteger = size % 1 === 0;
  394. const overMinSize = size > 2;
  395. const underMaxSize = size <= 256;
  396. ctx.assert(
  397. isInteger && overMinSize && underMaxSize,
  398. 400,
  399. "Invalid image size"
  400. );
  401. return next();
  402. })
  403. .param("blobId", (blobId, ctx, next) => {
  404. ctx.assert(ssbRef.isBlob(blobId), 400, "Invalid blob link");
  405. return next();
  406. })
  407. .param("message", (message, ctx, next) => {
  408. ctx.assert(ssbRef.isMsg(message), 400, "Invalid message link");
  409. return next();
  410. })
  411. .param("feed", (message, ctx, next) => {
  412. ctx.assert(ssbRef.isFeedId(message), 400, "Invalid feed link");
  413. return next();
  414. })
  415. //GET backend routes
  416. .get("/", async (ctx) => {
  417. ctx.redirect("/activity"); // default view when starting Oasis
  418. })
  419. .get("/robots.txt", (ctx) => {
  420. ctx.body = "User-agent: *\nDisallow: /";
  421. })
  422. .get(oasisCheckPath, (ctx) => {
  423. ctx.body = "oasis";
  424. })
  425. .get('/stats', async ctx => {
  426. const filter = ctx.query.filter || 'ALL';
  427. const stats = await statsModel.getStats(filter);
  428. ctx.body = statsView(stats, filter);
  429. })
  430. .get("/public/popular/:period", async (ctx) => {
  431. const { period } = ctx.params;
  432. const popularMod = ctx.cookies.get("popularMod") || 'on';
  433. if (popularMod !== 'on') {
  434. ctx.redirect('/modules');
  435. return;
  436. }
  437. const i18n = require("../client/assets/translations/i18n");
  438. const lang = ctx.cookies.get('lang') || 'en';
  439. const translations = i18n[lang] || i18n['en'];
  440. const publicPopular = async ({ period }) => {
  441. const messages = await post.popular({ period });
  442. const prefix = nav(
  443. div({ class: "filters" },
  444. ul(
  445. li(
  446. form({ method: "GET", action: "/public/popular/day" },
  447. button({ type: "submit", class: "filter-btn" }, translations.day)
  448. )
  449. ),
  450. li(
  451. form({ method: "GET", action: "/public/popular/week" },
  452. button({ type: "submit", class: "filter-btn" }, translations.week)
  453. )
  454. ),
  455. li(
  456. form({ method: "GET", action: "/public/popular/month" },
  457. button({ type: "submit", class: "filter-btn" }, translations.month)
  458. )
  459. ),
  460. li(
  461. form({ method: "GET", action: "/public/popular/year" },
  462. button({ type: "submit", class: "filter-btn" }, translations.year)
  463. )
  464. )
  465. )
  466. )
  467. );
  468. return popularView({
  469. messages,
  470. prefix,
  471. });
  472. };
  473. ctx.body = await publicPopular({ period });
  474. })
  475. // modules
  476. .get("/modules", async (ctx) => {
  477. const configMods = getConfig().modules;
  478. const modules = [
  479. 'popular', 'topics', 'summaries', 'latest', 'threads', 'multiverse', 'invites', 'wallet',
  480. 'legacy', 'cipher', 'bookmarks', 'videos', 'docs', 'audios', 'tags', 'images', 'trending',
  481. 'events', 'tasks', 'market', 'tribes', 'governance', 'reports', 'opinions', 'transfers',
  482. 'feed', 'pixelia', 'agenda', 'ai', 'forum'
  483. ];
  484. const moduleStates = modules.reduce((acc, mod) => {
  485. acc[`${mod}Mod`] = configMods[`${mod}Mod`];
  486. return acc;
  487. }, {});
  488. ctx.body = modulesView(moduleStates);
  489. })
  490. // AI
  491. .get('/ai', async (ctx) => {
  492. const aiMod = ctx.cookies.get("aiMod") || 'on';
  493. if (aiMod !== 'on') {
  494. ctx.redirect('/modules');
  495. return;
  496. }
  497. startAI();
  498. const historyPath = path.join(__dirname, '..', '..', 'src', 'configs', 'AI-history.json');
  499. let chatHistory = [];
  500. try {
  501. const fileData = fs.readFileSync(historyPath, 'utf-8');
  502. chatHistory = JSON.parse(fileData);
  503. } catch (e) {
  504. chatHistory = [];
  505. }
  506. const config = getConfig();
  507. const userPrompt = config.ai?.prompt?.trim() || '';
  508. ctx.body = aiView(chatHistory, userPrompt);
  509. })
  510. // pixelArt
  511. .get('/pixelia', async (ctx) => {
  512. const pixeliaMod = ctx.cookies.get("pixeliaMod") || 'on';
  513. if (pixeliaMod !== 'on') {
  514. ctx.redirect('/modules');
  515. return;
  516. }
  517. const pixelArt = await pixeliaModel.listPixels();
  518. ctx.body = pixeliaView(pixelArt);
  519. })
  520. .get("/public/latest", async (ctx) => {
  521. const latestMod = ctx.cookies.get("latestMod") || 'on';
  522. if (latestMod !== 'on') {
  523. ctx.redirect('/modules');
  524. return;
  525. }
  526. const messages = await post.latest();
  527. ctx.body = await latestView({ messages });
  528. })
  529. .get("/public/latest/extended", async (ctx) => {
  530. const extendedMod = ctx.cookies.get("extendedMod") || 'on';
  531. if (extendedMod !== 'on') {
  532. ctx.redirect('/modules');
  533. return;
  534. }
  535. const messages = await post.latestExtended();
  536. ctx.body = await extendedView({ messages });
  537. })
  538. .get("/public/latest/topics", async (ctx) => {
  539. const topicsMod = ctx.cookies.get("topicsMod") || 'on';
  540. if (topicsMod !== 'on') {
  541. ctx.redirect('/modules');
  542. return;
  543. }
  544. const messages = await post.latestTopics();
  545. const channels = await post.channels();
  546. const list = channels.map((c) => {
  547. return li(a({ href: `/hashtag/${c}` }, `#${c}`));
  548. });
  549. const prefix = nav(ul(list));
  550. ctx.body = await topicsView({ messages, prefix });
  551. })
  552. .get("/public/latest/summaries", async (ctx) => {
  553. const summariesMod = ctx.cookies.get("summariesMod") || 'on';
  554. if (summariesMod !== 'on') {
  555. ctx.redirect('/modules');
  556. return;
  557. }
  558. const messages = await post.latestSummaries();
  559. ctx.body = await summaryView({ messages });
  560. })
  561. .get("/public/latest/threads", async (ctx) => {
  562. const threadsMod = ctx.cookies.get("threadsMod") || 'on';
  563. if (threadsMod !== 'on') {
  564. ctx.redirect('/modules');
  565. return;
  566. }
  567. const messages = await post.latestThreads();
  568. ctx.body = await threadsView({ messages });
  569. })
  570. .get("/author/:feed", async (ctx) => {
  571. const { feed } = ctx.params;
  572. const gt = Number(ctx.request.query["gt"] || -1);
  573. const lt = Number(ctx.request.query["lt"] || -1);
  574. if (lt > 0 && gt > 0 && gt >= lt)
  575. throw new Error("Given search range is empty");
  576. const author = async (feedId) => {
  577. const description = await about.description(feedId);
  578. const name = await about.name(feedId);
  579. const image = await about.image(feedId);
  580. const messages = await post.fromPublicFeed(feedId, gt, lt);
  581. const firstPost = await post.firstBy(feedId);
  582. const lastPost = await post.latestBy(feedId);
  583. const relationship = await friend.getRelationship(feedId);
  584. const avatarUrl = getAvatarUrl(image);
  585. return authorView({
  586. feedId,
  587. messages,
  588. firstPost,
  589. lastPost,
  590. name,
  591. description,
  592. avatarUrl,
  593. relationship,
  594. });
  595. };
  596. ctx.body = await author(feed);
  597. })
  598. .get("/search", async (ctx) => {
  599. const query = ctx.query.query || '';
  600. if (!query) {
  601. return ctx.body = await searchView({ messages: [], query, types: [] });
  602. }
  603. const results = await searchModel.search({ query, types: [] });
  604. const groupedResults = Object.entries(results).reduce((acc, [type, msgs]) => {
  605. acc[type] = msgs.map(msg => {
  606. if (!msg.value || !msg.value.content) {
  607. return {};
  608. }
  609. return {
  610. ...msg,
  611. content: msg.value.content,
  612. author: msg.value.content.author || 'Unknown',
  613. };
  614. });
  615. return acc;
  616. }, {});
  617. ctx.body = await searchView({ results: groupedResults, query, types: [] });
  618. })
  619. .get('/images', async (ctx) => {
  620. const imagesMod = ctx.cookies.get("imagesMod") || 'on';
  621. if (imagesMod !== 'on') {
  622. ctx.redirect('/modules');
  623. return;
  624. }
  625. const filter = ctx.query.filter || 'all'
  626. const images = await imagesModel.listAll(filter);
  627. ctx.body = await imageView(images, filter, null);
  628. })
  629. .get('/images/edit/:id', async (ctx) => {
  630. const imagesMod = ctx.cookies.get("imagesMod") || 'on';
  631. if (imagesMod !== 'on') {
  632. ctx.redirect('/modules');
  633. return;
  634. }
  635. const filter = 'edit';
  636. const img = await imagesModel.getImageById(ctx.params.id, false);
  637. ctx.body = await imageView([img], filter, ctx.params.id);
  638. })
  639. .get('/images/:imageId', async ctx => {
  640. const imageId = ctx.params.imageId;
  641. const filter = ctx.query.filter || 'all';
  642. const image = await imagesModel.getImageById(imageId);
  643. ctx.body = await singleImageView(image, filter);
  644. })
  645. .get('/audios', async (ctx) => {
  646. const audiosMod = ctx.cookies.get("audiosMod") || 'on';
  647. if (audiosMod !== 'on') {
  648. ctx.redirect('/modules');
  649. return;
  650. }
  651. const filter = ctx.query.filter || 'all';
  652. const audios = await audiosModel.listAll(filter);
  653. ctx.body = await audioView(audios, filter, null);
  654. })
  655. .get('/audios/edit/:id', async (ctx) => {
  656. const audiosMod = ctx.cookies.get("audiosMod") || 'on';
  657. if (audiosMod !== 'on') {
  658. ctx.redirect('/modules');
  659. return;
  660. }
  661. const audio = await audiosModel.getAudioById(ctx.params.id);
  662. ctx.body = await audioView([audio], 'edit', ctx.params.id);
  663. })
  664. .get('/audios/:audioId', async ctx => {
  665. const audioId = ctx.params.audioId;
  666. const filter = ctx.query.filter || 'all';
  667. const audio = await audiosModel.getAudioById(audioId);
  668. ctx.body = await singleAudioView(audio, filter);
  669. })
  670. .get('/videos', async (ctx) => {
  671. const filter = ctx.query.filter || 'all';
  672. const videos = await videosModel.listAll(filter);
  673. ctx.body = await videoView(videos, filter, null);
  674. })
  675. .get('/videos/edit/:id', async (ctx) => {
  676. const video = await videosModel.getVideoById(ctx.params.id);
  677. ctx.body = await videoView([video], 'edit', ctx.params.id);
  678. })
  679. .get('/videos/:videoId', async ctx => {
  680. const videoId = ctx.params.videoId;
  681. const filter = ctx.query.filter || 'all';
  682. const video = await videosModel.getVideoById(videoId);
  683. ctx.body = await singleVideoView(video, filter);
  684. })
  685. .get('/documents', async (ctx) => {
  686. const filter = ctx.query.filter || 'all';
  687. const documents = await documentsModel.listAll(filter);
  688. ctx.body = await documentView(documents, filter, null);
  689. })
  690. .get('/documents/edit/:id', async (ctx) => {
  691. const document = await documentsModel.getDocumentById(ctx.params.id);
  692. ctx.body = await documentView([document], 'edit', ctx.params.id);
  693. })
  694. .get('/documents/:documentId', async ctx => {
  695. const documentId = ctx.params.documentId;
  696. const filter = ctx.query.filter || 'all';
  697. const document = await documentsModel.getDocumentById(documentId);
  698. ctx.body = await singleDocumentView(document, filter);
  699. })
  700. .get('/cv', async ctx => {
  701. const cv = await cvModel.getCVByUserId()
  702. ctx.body = await cvView(cv)
  703. })
  704. .get('/cv/create', async ctx => {
  705. ctx.body = await createCVView()
  706. })
  707. .get('/cv/edit/:id', async ctx => {
  708. const cv = await cvModel.getCVByUserId()
  709. ctx.body = await createCVView(cv, true)
  710. })
  711. .get('/pm', async ctx => {
  712. ctx.body = await pmView();
  713. })
  714. .get("/inbox", async (ctx) => {
  715. const inboxMod = ctx.cookies.get("inboxMod") || 'on';
  716. if (inboxMod !== 'on') {
  717. ctx.redirect('/modules');
  718. return;
  719. }
  720. const inboxMessages = async () => {
  721. const messages = await post.inbox();
  722. return privateView({ messages });
  723. };
  724. ctx.body = await inboxMessages();
  725. })
  726. .get('/tags', async ctx => {
  727. const filter = ctx.query.filter || 'all'
  728. const tags = await tagsModel.listTags(filter)
  729. ctx.body = await tagsView(tags, filter)
  730. })
  731. .get('/reports', async ctx => {
  732. const filter = ctx.query.filter || 'all';
  733. const reports = await reportsModel.listAll(filter);
  734. ctx.body = await reportView(reports, filter, null);
  735. })
  736. .get('/reports/edit/:id', async ctx => {
  737. const report = await reportsModel.getReportById(ctx.params.id);
  738. ctx.body = await reportView([report], 'edit', ctx.params.id);
  739. })
  740. .get('/reports/:reportId', async ctx => {
  741. const reportId = ctx.params.reportId;
  742. const filter = ctx.query.filter || 'all';
  743. const report = await reportsModel.getReportById(reportId, filter);
  744. ctx.body = await singleReportView(report, filter);
  745. })
  746. .get('/trending', async (ctx) => {
  747. const filter = ctx.query.filter || 'RECENT';
  748. const trendingItems = await trendingModel.listTrending(filter);
  749. const items = trendingItems.filtered || [];
  750. const categories = trendingModel.categories;
  751. ctx.body = await trendingView(items, filter, categories);
  752. })
  753. .get('/agenda', async (ctx) => {
  754. const filter = ctx.query.filter || 'all';
  755. const data = await agendaModel.listAgenda(filter);
  756. ctx.body = await agendaView(data, filter);
  757. })
  758. .get("/hashtag/:hashtag", async (ctx) => {
  759. const { hashtag } = ctx.params;
  760. const messages = await post.fromHashtag(hashtag);
  761. ctx.body = await hashtagView({ hashtag, messages });
  762. })
  763. .get('/inhabitants', async (ctx) => {
  764. const filter = ctx.query.filter || 'all';
  765. const query = {
  766. search: ctx.query.search || ''
  767. };
  768. if (['CVs', 'MATCHSKILLS'].includes(filter)) {
  769. query.location = ctx.query.location || '';
  770. query.language = ctx.query.language || '';
  771. query.skills = ctx.query.skills || '';
  772. }
  773. const inhabitants = await inhabitantsModel.listInhabitants({
  774. filter,
  775. ...query
  776. });
  777. ctx.body = await inhabitantsView(inhabitants, filter, query);
  778. })
  779. .get('/inhabitant/:id', async (ctx) => {
  780. const id = ctx.params.id;
  781. const about = await inhabitantsModel._getLatestAboutById(id);
  782. const cv = await inhabitantsModel.getCVByUserId(id);
  783. const feed = await inhabitantsModel.getFeedByUserId(id);
  784. ctx.body = await inhabitantsProfileView({ about, cv, feed });
  785. })
  786. .get('/tribes', async ctx => {
  787. const filter = ctx.query.filter || 'all';
  788. const search = ctx.query.search || '';
  789. const tribes = await tribesModel.listAll();
  790. let filteredTribes = tribes;
  791. if (search) {
  792. filteredTribes = tribes.filter(tribe =>
  793. tribe.title.toLowerCase().includes(search.toLowerCase())
  794. );
  795. }
  796. ctx.body = await tribesView(filteredTribes, filter, null, ctx.query);
  797. })
  798. .get('/tribes/create', async ctx => {
  799. ctx.body = await tribesView([], 'create', null)
  800. })
  801. .get('/tribes/edit/:id', async ctx => {
  802. const tribe = await tribesModel.getTribeById(ctx.params.id)
  803. ctx.body = await tribesView([tribe], 'edit', ctx.params.id)
  804. })
  805. .get('/tribe/:tribeId', koaBody(), async ctx => {
  806. const tribeId = ctx.params.tribeId;
  807. const tribe = await tribesModel.getTribeById(tribeId);
  808. const userId = SSBconfig.config.keys.id;
  809. const query = ctx.query;
  810. if (!query.feedFilter) {
  811. query.feedFilter = 'TOP';
  812. }
  813. if (tribe.isAnonymous === false && !tribe.members.includes(userId)) {
  814. ctx.status = 403;
  815. ctx.body = { message: 'You cannot access to this tribe!' };
  816. return;
  817. }
  818. if (!tribe.members.includes(userId)) {
  819. ctx.status = 403;
  820. ctx.body = { message: 'You cannot access to this tribe!' };
  821. return;
  822. }
  823. ctx.body = await tribeView(tribe, userId, query);
  824. })
  825. .get('/activity', async ctx => {
  826. const filter = ctx.query.filter || 'recent';
  827. const actions = await activityModel.listFeed(filter);
  828. ctx.body = activityView(actions, filter);
  829. })
  830. .get("/profile", async (ctx) => {
  831. const myFeedId = await meta.myFeedId();
  832. const gt = Number(ctx.request.query["gt"] || -1);
  833. const lt = Number(ctx.request.query["lt"] || -1);
  834. if (lt > 0 && gt > 0 && gt >= lt)
  835. throw new Error("Given search range is empty");
  836. const description = await about.description(myFeedId);
  837. const name = await about.name(myFeedId);
  838. const image = await about.image(myFeedId);
  839. const messages = await post.fromPublicFeed(myFeedId, gt, lt);
  840. const firstPost = await post.firstBy(myFeedId);
  841. const lastPost = await post.latestBy(myFeedId);
  842. const avatarUrl = getAvatarUrl(image);
  843. ctx.body = await authorView({
  844. feedId: myFeedId,
  845. messages,
  846. firstPost,
  847. lastPost,
  848. name,
  849. description,
  850. avatarUrl,
  851. relationship: { me: true },
  852. });
  853. })
  854. .get("/profile/edit", async (ctx) => {
  855. const myFeedId = await meta.myFeedId();
  856. const description = await about.description(myFeedId);
  857. const name = await about.name(myFeedId);
  858. ctx.body = await editProfileView({
  859. name,
  860. description,
  861. });
  862. })
  863. .post("/profile/edit", koaBody({ multipart: true }), async (ctx) => {
  864. const name = String(ctx.request.body.name);
  865. const description = String(ctx.request.body.description);
  866. const image = await promisesFs.readFile(ctx.request.files.image.filepath);
  867. ctx.body = await post.publishProfileEdit({
  868. name,
  869. description,
  870. image,
  871. });
  872. ctx.redirect("/profile");
  873. })
  874. .get("/publish/custom", async (ctx) => {
  875. ctx.body = await publishCustomView();
  876. })
  877. .get("/json/:message", async (ctx) => {
  878. if (config.public) {
  879. throw new Error(
  880. "Sorry, many actions are unavailable when Oasis is running in public mode. Please run Oasis in the default mode and try again."
  881. );
  882. }
  883. const { message } = ctx.params;
  884. ctx.type = "application/json";
  885. const json = async (message) => {
  886. const json = await meta.get(message);
  887. return JSON.stringify(json, null, 2);
  888. };
  889. ctx.body = await json(message);
  890. })
  891. .get("/blob/:blobId", async (ctx) => {
  892. const { blobId } = ctx.params;
  893. const id = blobId.startsWith('&') ? blobId : `&${blobId}`;
  894. const buffer = await blob.getResolved({ blobId });
  895. let fileType;
  896. try {
  897. fileType = await FileType.fromBuffer(buffer);
  898. } catch {
  899. fileType = null;
  900. }
  901. let mime = fileType?.mime || "application/octet-stream";
  902. if (mime === "application/octet-stream" && buffer.slice(0, 4).toString() === "%PDF") {
  903. mime = "application/pdf";
  904. }
  905. ctx.set("Content-Type", mime);
  906. ctx.set("Content-Disposition", `inline; filename="${blobId}"`);
  907. ctx.set("Cache-Control", "public, max-age=31536000, immutable");
  908. ctx.body = buffer;
  909. })
  910. .get("/image/:imageSize/:blobId", async (ctx) => {
  911. const { blobId, imageSize } = ctx.params;
  912. const size = Number(imageSize);
  913. const fallbackPixel = Buffer.from(
  914. "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
  915. "base64"
  916. );
  917. const fakeImage = () => {
  918. if (typeof sharp !== "function") {
  919. return Promise.resolve(fallbackPixel);
  920. }
  921. return sharp({
  922. create: {
  923. width: size,
  924. height: size,
  925. channels: 4,
  926. background: { r: 0, g: 0, b: 0, alpha: 0.5 },
  927. },
  928. }).png().toBuffer();
  929. };
  930. try {
  931. const buffer = await blob.getResolved({ blobId });
  932. if (!buffer) {
  933. ctx.set("Content-Type", "image/png");
  934. ctx.body = await fakeImage();
  935. return;
  936. }
  937. const fileType = await FileType.fromBuffer(buffer);
  938. const mimeType = fileType?.mime || "application/octet-stream";
  939. ctx.set("Content-Type", mimeType);
  940. if (typeof sharp === "function") {
  941. ctx.body = await sharp(buffer)
  942. .resize(size, size)
  943. .png()
  944. .toBuffer();
  945. } else {
  946. ctx.body = buffer;
  947. }
  948. } catch (err) {
  949. ctx.set("Content-Type", "image/png");
  950. ctx.body = await fakeImage();
  951. }
  952. })
  953. .get("/settings", async (ctx) => {
  954. const theme = ctx.cookies.get("theme") || "Dark-SNH";
  955. const config = getConfig();
  956. const aiPrompt = config.ai?.prompt || "";
  957. const getMeta = async ({ theme, aiPrompt }) => {
  958. return settingsView({
  959. theme,
  960. version: version.toString(),
  961. aiPrompt
  962. });
  963. };
  964. ctx.body = await getMeta({ theme, aiPrompt });
  965. })
  966. .get("/peers", async (ctx) => {
  967. const theme = ctx.cookies.get("theme") || config.theme;
  968. const getMeta = async () => {
  969. const allPeers = await meta.peers();
  970. const connected = allPeers.filter(([, data]) => data.state === "connected");
  971. const offline = allPeers.filter(([, data]) => data.state !== "connected");
  972. const enrich = async (peers) => {
  973. return await Promise.all(
  974. peers.map(async ([address, data]) => {
  975. const feedId = data.key || data.id;
  976. const name = await about.name(feedId);
  977. return [
  978. address,
  979. {
  980. ...data,
  981. key: feedId,
  982. name: name || feedId,
  983. },
  984. ];
  985. })
  986. );
  987. };
  988. const connectedPeers = await enrich(connected);
  989. const offlinePeers = await enrich(offline);
  990. return peersView({
  991. connectedPeers,
  992. peers: offlinePeers,
  993. });
  994. };
  995. ctx.body = await getMeta();
  996. })
  997. .get("/invites", async (ctx) => {
  998. const theme = ctx.cookies.get("theme") || config.theme;
  999. const invitesMod = ctx.cookies.get("invitesMod") || 'on';
  1000. if (invitesMod !== 'on') {
  1001. ctx.redirect('/modules');
  1002. return;
  1003. }
  1004. const getMeta = async ({ theme }) => {
  1005. return invitesView({});
  1006. };
  1007. ctx.body = await getMeta({ theme });
  1008. })
  1009. .get("/likes/:feed", async (ctx) => {
  1010. const { feed } = ctx.params;
  1011. const likes = async ({ feed }) => {
  1012. const pendingMessages = post.likes({ feed });
  1013. const pendingName = about.name(feed);
  1014. return likesView({
  1015. messages: await pendingMessages,
  1016. feed,
  1017. name: await pendingName,
  1018. });
  1019. };
  1020. ctx.body = await likes({ feed });
  1021. })
  1022. .get("/mentions", async (ctx) => {
  1023. const { messages, myFeedId } = await post.mentionsMe();
  1024. ctx.body = await mentionsView({ messages, myFeedId });
  1025. })
  1026. .get('/opinions', async (ctx) => {
  1027. const filter = ctx.query.filter || 'RECENT';
  1028. const opinions = await opinionsModel.listOpinions(filter);
  1029. ctx.body = await opinionsView(opinions, filter);
  1030. })
  1031. .get('/feed', async ctx => {
  1032. const filter = ctx.query.filter || 'ALL';
  1033. const feeds = await feedModel.listFeeds(filter);
  1034. ctx.body = feedView(feeds, filter);
  1035. })
  1036. .get('/feed/create', async ctx => {
  1037. ctx.body = feedCreateView();
  1038. })
  1039. .get('/forum', async ctx => {
  1040. const forumMod = ctx.cookies.get("forumMod") || 'on';
  1041. if (forumMod !== 'on') {
  1042. ctx.redirect('/modules');
  1043. return;
  1044. }
  1045. const filter = ctx.query.filter || 'hot';
  1046. const forums = await forumModel.listAll(filter);
  1047. ctx.body = await forumView(forums, filter);
  1048. })
  1049. .get('/forum/:forumId', async ctx => {
  1050. const rawId = ctx.params.forumId
  1051. const msg = await forumModel.getMessageById(rawId)
  1052. const isReply = Boolean(msg.root)
  1053. const forumId = isReply ? msg.root : rawId
  1054. const highlightCommentId = isReply ? rawId : null
  1055. const forum = await forumModel.getForumById(forumId)
  1056. const messagesData = await forumModel.getMessagesByForumId(forumId)
  1057. ctx.body = await singleForumView(
  1058. forum,
  1059. messagesData,
  1060. ctx.query.filter,
  1061. highlightCommentId
  1062. )
  1063. })
  1064. .get('/legacy', async (ctx) => {
  1065. const legacyMod = ctx.cookies.get("legacyMod") || 'on';
  1066. if (legacyMod !== 'on') {
  1067. ctx.redirect('/modules');
  1068. return;
  1069. }
  1070. try {
  1071. ctx.body = await legacyView();
  1072. } catch (error) {
  1073. ctx.body = { error: error.message };
  1074. }
  1075. })
  1076. .get('/bookmarks', async (ctx) => {
  1077. const bookmarksMod = ctx.cookies.get("bookmarksMod") || 'on';
  1078. if (bookmarksMod !== 'on') {
  1079. ctx.redirect('/modules');
  1080. return;
  1081. }
  1082. const filter = ctx.query.filter || 'all';
  1083. const bookmarks = await bookmarksModel.listAll(null, filter);
  1084. ctx.body = await bookmarkView(bookmarks, filter, null);
  1085. })
  1086. .get('/bookmarks/edit/:id', async (ctx) => {
  1087. const bookmarksMod = ctx.cookies.get("bookmarksMod") || 'on';
  1088. if (bookmarksMod !== 'on') {
  1089. ctx.redirect('/modules');
  1090. return;
  1091. }
  1092. const bookmarkId = ctx.params.id;
  1093. const bookmark = await bookmarksModel.getBookmarkById(bookmarkId);
  1094. if (bookmark.opinions_inhabitants && bookmark.opinions_inhabitants.length > 0) {
  1095. ctx.flash = { message: "This bookmark has received votes and cannot be updated." };
  1096. ctx.redirect(`/bookmarks?filter=mine`);
  1097. }
  1098. ctx.body = await bookmarkView([bookmark], 'edit', bookmarkId);
  1099. })
  1100. .get('/bookmarks/:bookmarkId', async ctx => {
  1101. const bookmarkId = ctx.params.bookmarkId;
  1102. const filter = ctx.query.filter || 'all';
  1103. const bookmark = await bookmarksModel.getBookmarkById(bookmarkId);
  1104. ctx.body = await singleBookmarkView(bookmark, filter);
  1105. })
  1106. .get('/tasks', async ctx=>{
  1107. const filter = ctx.query.filter||'all';
  1108. const tasks = await tasksModel.listAll(filter);
  1109. ctx.body = await taskView(tasks,filter,null);
  1110. })
  1111. .get('/tasks/edit/:id', async ctx=>{
  1112. const id = ctx.params.id;
  1113. const task = await tasksModel.getTaskById(id);
  1114. ctx.body = await taskView(task,'edit',id);
  1115. })
  1116. .get('/tasks/:taskId', async ctx => {
  1117. const taskId = ctx.params.taskId;
  1118. const filter = ctx.query.filter || 'all';
  1119. const task = await tasksModel.getTaskById(taskId, filter);
  1120. ctx.body = await taskView([task], filter, taskId);
  1121. })
  1122. .get('/events', async (ctx) => {
  1123. const eventsMod = ctx.cookies.get("eventsMod") || 'on';
  1124. if (eventsMod !== 'on') {
  1125. ctx.redirect('/modules');
  1126. return;
  1127. }
  1128. const filter = ctx.query.filter || 'all';
  1129. const events = await eventsModel.listAll(null, filter);
  1130. ctx.body = await eventView(events, filter, null);
  1131. })
  1132. .get('/events/edit/:id', async (ctx) => {
  1133. const eventsMod = ctx.cookies.get("eventsMod") || 'on';
  1134. if (eventsMod !== 'on') {
  1135. ctx.redirect('/modules');
  1136. return;
  1137. }
  1138. const eventId = ctx.params.id;
  1139. const event = await eventsModel.getEventById(eventId);
  1140. ctx.body = await eventView([event], 'edit', eventId);
  1141. })
  1142. .get('/events/:eventId', async ctx => {
  1143. const eventId = ctx.params.eventId;
  1144. const filter = ctx.query.filter || 'all';
  1145. const event = await eventsModel.getEventById(eventId);
  1146. ctx.body = await singleEventView(event, filter);
  1147. })
  1148. .get('/votes', async ctx => {
  1149. const filter = ctx.query.filter || 'all';
  1150. const voteList = await votesModel.listAll(filter);
  1151. ctx.body = await voteView(voteList, filter, null);
  1152. })
  1153. .get('/votes/:voteId', async ctx => {
  1154. const voteId = ctx.params.voteId;
  1155. const vote = await votesModel.getVoteById(voteId);
  1156. ctx.body = await voteView(vote);
  1157. })
  1158. .get('/votes/edit/:id', async ctx => {
  1159. const id = ctx.params.id;
  1160. const vote = await votesModel.getVoteById(id);
  1161. ctx.body = await voteView([vote], 'edit', id);
  1162. })
  1163. .get('/market', async ctx => {
  1164. const filter = ctx.query.filter || 'all';
  1165. const marketItems = await marketModel.listAllItems(filter);
  1166. ctx.body = await marketView(marketItems, filter, null);
  1167. })
  1168. .get('/market/edit/:id', async ctx => {
  1169. const id = ctx.params.id;
  1170. const marketItem = await marketModel.getItemById(id);
  1171. ctx.body = await marketView([marketItem], 'edit', marketItem);
  1172. })
  1173. .get('/market/:itemId', async ctx => {
  1174. const itemId = ctx.params.itemId;
  1175. const filter = ctx.query.filter || 'all';
  1176. const item = await marketModel.getItemById(itemId);
  1177. ctx.body = await singleMarketView(item, filter);
  1178. })
  1179. .get('/cipher', async (ctx) => {
  1180. const cipherMod = ctx.cookies.get("cipherMod") || 'on';
  1181. if (cipherMod !== 'on') {
  1182. ctx.redirect('/modules');
  1183. return;
  1184. }
  1185. try {
  1186. ctx.body = await cipherView();
  1187. } catch (error) {
  1188. ctx.body = { error: error.message };
  1189. }
  1190. })
  1191. .get("/thread/:message", async (ctx) => {
  1192. const { message } = ctx.params;
  1193. const thread = async (message) => {
  1194. const messages = await post.fromThread(message);
  1195. return threadView({ messages });
  1196. };
  1197. ctx.body = await thread(message);
  1198. })
  1199. .get("/subtopic/:message", async (ctx) => {
  1200. const { message } = ctx.params;
  1201. const rootMessage = await post.get(message);
  1202. const myFeedId = await meta.myFeedId();
  1203. debug("%O", rootMessage);
  1204. const messages = [rootMessage];
  1205. ctx.body = await subtopicView({ messages, myFeedId });
  1206. })
  1207. .get("/publish", async (ctx) => {
  1208. ctx.body = await publishView();
  1209. })
  1210. .get("/comment/:message", async (ctx) => {
  1211. const { messages, myFeedId, parentMessage } =
  1212. await resolveCommentComponents(ctx);
  1213. ctx.body = await commentView({ messages, myFeedId, parentMessage });
  1214. })
  1215. .get("/wallet", async (ctx) => {
  1216. const { url, user, pass } = getConfig().wallet;
  1217. const walletMod = ctx.cookies.get("walletMod") || 'on';
  1218. if (walletMod !== 'on') {
  1219. ctx.redirect('/modules');
  1220. return;
  1221. }
  1222. try {
  1223. const balance = await walletModel.getBalance(url, user, pass);
  1224. const address = await walletModel.getAddress(url, user, pass);
  1225. ctx.body = await walletView(balance, address);
  1226. } catch (error) {
  1227. ctx.body = await walletErrorView(error);
  1228. }
  1229. })
  1230. .get("/wallet/history", async (ctx) => {
  1231. const { url, user, pass } = getConfig().wallet;
  1232. try {
  1233. const balance = await walletModel.getBalance(url, user, pass);
  1234. const transactions = await walletModel.listTransactions(url, user, pass);
  1235. const address = await walletModel.getAddress(url, user, pass);
  1236. ctx.body = await walletHistoryView(balance, transactions, address);
  1237. } catch (error) {
  1238. ctx.body = await walletErrorView(error);
  1239. }
  1240. })
  1241. .get("/wallet/receive", async (ctx) => {
  1242. const { url, user, pass } = getConfig().wallet;
  1243. try {
  1244. const balance = await walletModel.getBalance(url, user, pass);
  1245. const address = await walletModel.getAddress(url, user, pass);
  1246. ctx.body = await walletReceiveView(balance, address);
  1247. } catch (error) {
  1248. ctx.body = await walletErrorView(error);
  1249. }
  1250. })
  1251. .get("/wallet/send", async (ctx) => {
  1252. const { url, user, pass, fee } = getConfig().wallet;
  1253. try {
  1254. const balance = await walletModel.getBalance(url, user, pass);
  1255. const address = await walletModel.getAddress(url, user, pass);
  1256. ctx.body = await walletSendFormView(balance, null, null, fee, null, address);
  1257. } catch (error) {
  1258. ctx.body = await walletErrorView(error);
  1259. }
  1260. })
  1261. .get('/transfers', async ctx => {
  1262. const filter = ctx.query.filter || 'all'
  1263. const list = await transfersModel.listAll(filter)
  1264. ctx.body = await transferView(list, filter, null)
  1265. })
  1266. .get('/transfers/edit/:id', async ctx => {
  1267. const tr = await transfersModel.getTransferById(ctx.params.id)
  1268. ctx.body = await transferView([tr], 'edit', ctx.params.id)
  1269. })
  1270. .get('/transfers/:transferId', async ctx => {
  1271. const transferId = ctx.params.transferId;
  1272. const filter = ctx.query.filter || 'all';
  1273. const transfer = await transfersModel.getTransferById(transferId);
  1274. ctx.body = await singleTransferView(transfer, filter);
  1275. })
  1276. //POST backend routes
  1277. .post('/ai', koaBody(), async (ctx) => {
  1278. const axios = require('../server/node_modules/axios').default;
  1279. const { input } = ctx.request.body;
  1280. if (!input) {
  1281. ctx.status = 400;
  1282. ctx.body = { error: 'No input provided' };
  1283. return;
  1284. }
  1285. const config = getConfig();
  1286. const userPrompt = config.ai?.prompt?.trim() || "Provide an informative and precise response.";
  1287. const response = await axios.post('http://localhost:4001/ai', { input });
  1288. const aiResponse = response.data.answer;
  1289. const historyPath = path.join(__dirname, '..', '..', 'src', 'configs', 'AI-history.json');
  1290. let chatHistory = [];
  1291. try {
  1292. const fileData = fs.readFileSync(historyPath, 'utf-8');
  1293. chatHistory = JSON.parse(fileData);
  1294. } catch (e) {
  1295. chatHistory = [];
  1296. }
  1297. chatHistory.unshift({
  1298. prompt: userPrompt,
  1299. question: input,
  1300. answer: aiResponse,
  1301. timestamp: Date.now()
  1302. });
  1303. chatHistory = chatHistory.slice(0, 20);
  1304. fs.writeFileSync(historyPath, JSON.stringify(chatHistory, null, 2), 'utf-8');
  1305. ctx.body = aiView(chatHistory, userPrompt);
  1306. })
  1307. .post('/ai/clear', async (ctx) => {
  1308. const fs = require('fs');
  1309. const path = require('path');
  1310. const { getConfig } = require('../configs/config-manager.js');
  1311. const historyPath = path.join(__dirname, '..', '..', 'src', 'configs', 'AI-history.json');
  1312. fs.writeFileSync(historyPath, '[]', 'utf-8');
  1313. const config = getConfig();
  1314. const userPrompt = config.ai?.prompt?.trim() || '';
  1315. ctx.body = aiView([], userPrompt);
  1316. })
  1317. .post('/pixelia/paint', koaBody(), async (ctx) => {
  1318. const { x, y, color } = ctx.request.body;
  1319. if (x < 1 || x > 50 || y < 1 || y > 200) {
  1320. const errorMessage = 'Coordinates are wrong!';
  1321. const pixelArt = await pixeliaModel.listPixels();
  1322. ctx.body = pixeliaView(pixelArt, errorMessage);
  1323. return;
  1324. }
  1325. await pixeliaModel.paintPixel(x, y, color);
  1326. ctx.redirect('/pixelia');
  1327. })
  1328. .post('/pm', koaBody(), async ctx => {
  1329. const { recipients, subject, text } = ctx.request.body;
  1330. const recipientsArr = recipients.split(',').map(s => s.trim()).filter(Boolean);
  1331. await pmModel.sendMessage(recipientsArr, subject, text);
  1332. ctx.redirect('/pm');
  1333. })
  1334. .post('/inbox/delete/:id', koaBody(), async ctx => {
  1335. const { id } = ctx.params;
  1336. await pmModel.deleteMessageById(id);
  1337. ctx.redirect('/inbox');
  1338. })
  1339. .post("/search", koaBody(), async (ctx) => {
  1340. const body = ctx.request.body;
  1341. const query = body.query || "";
  1342. let types = body.type || [];
  1343. if (typeof types === "string") types = [types];
  1344. if (!Array.isArray(types)) types = [];
  1345. if (!query) {
  1346. return ctx.body = await searchView({ messages: [], query, types });
  1347. }
  1348. const results = await searchModel.search({ query, types });
  1349. const groupedResults = Object.entries(results).reduce((acc, [type, msgs]) => {
  1350. acc[type] = msgs.map(msg => {
  1351. if (!msg.value || !msg.value.content) {
  1352. return {};
  1353. }
  1354. return {
  1355. ...msg,
  1356. content: msg.value.content,
  1357. author: msg.value.content.author || 'Unknown',
  1358. };
  1359. });
  1360. return acc;
  1361. }, {});
  1362. ctx.body = await searchView({ results: groupedResults, query, types });
  1363. })
  1364. .post("/subtopic/preview/:message",
  1365. koaBody({ multipart: true }),
  1366. async (ctx) => {
  1367. const { message } = ctx.params;
  1368. const rootMessage = await post.get(message);
  1369. const myFeedId = await meta.myFeedId();
  1370. const rawContentWarning = String(ctx.request.body.contentWarning).trim();
  1371. const contentWarning =
  1372. rawContentWarning.length > 0 ? rawContentWarning : undefined;
  1373. const messages = [rootMessage];
  1374. const previewData = await preparePreview(ctx);
  1375. ctx.body = await previewSubtopicView({
  1376. messages,
  1377. myFeedId,
  1378. previewData,
  1379. contentWarning,
  1380. });
  1381. }
  1382. )
  1383. .post("/subtopic/:message", koaBody(), async (ctx) => {
  1384. const { message } = ctx.params;
  1385. const text = String(ctx.request.body.text);
  1386. const rawContentWarning = String(ctx.request.body.contentWarning).trim();
  1387. const contentWarning =
  1388. rawContentWarning.length > 0 ? rawContentWarning : undefined;
  1389. const publishSubtopic = async ({ message, text }) => {
  1390. const mentions = extractMentions(text);
  1391. const parent = await post.get(message);
  1392. return post.subtopic({
  1393. parent,
  1394. message: { text, mentions, contentWarning },
  1395. });
  1396. };
  1397. ctx.body = await publishSubtopic({ message, text });
  1398. ctx.redirect(`/thread/${encodeURIComponent(message)}`);
  1399. })
  1400. .post("/comment/preview/:message", koaBody({ multipart: true }), async (ctx) => {
  1401. const { messages, contentWarning, myFeedId, parentMessage } = await resolveCommentComponents(ctx);
  1402. const previewData = await preparePreview(ctx);
  1403. ctx.body = await previewCommentView({
  1404. messages,
  1405. myFeedId,
  1406. contentWarning,
  1407. previewData,
  1408. parentMessage,
  1409. });
  1410. })
  1411. .post("/comment/:message", koaBody(), async (ctx) => {
  1412. let decodedMessage;
  1413. try {
  1414. decodedMessage = decodeURIComponent(ctx.params.message);
  1415. } catch {
  1416. decodedMessage = ctx.params.message;
  1417. }
  1418. const text = String(ctx.request.body.text);
  1419. const rawContentWarning = String(ctx.request.body.contentWarning);
  1420. const contentWarning =
  1421. rawContentWarning.length > 0 ? rawContentWarning : undefined;
  1422. let mentions = extractMentions(text);
  1423. if (!Array.isArray(mentions)) mentions = [];
  1424. const parent = await meta.get(decodedMessage);
  1425. ctx.body = await post.comment({
  1426. parent,
  1427. message: {
  1428. text,
  1429. mentions,
  1430. contentWarning
  1431. },
  1432. });
  1433. ctx.redirect(`/thread/${encodeURIComponent(parent.key)}`);
  1434. })
  1435. .post("/publish/preview", koaBody({multipart: true, formidable: { multiples: false }, urlencoded: true }), async (ctx) => {
  1436. const rawContentWarning = ctx.request.body.contentWarning?.toString().trim() || "";
  1437. const contentWarning = rawContentWarning.length > 0 ? rawContentWarning : undefined;
  1438. const previewData = await preparePreview(ctx);
  1439. ctx.body = await previewView({ previewData, contentWarning });
  1440. })
  1441. .post("/publish", koaBody({ multipart: true, urlencoded: true, formidable: { multiples: false } }), async (ctx) => {
  1442. const text = ctx.request.body.text?.toString().trim() || "";
  1443. const rawContentWarning = ctx.request.body.contentWarning?.toString().trim() || "";
  1444. const contentWarning = rawContentWarning.length > 0 ? rawContentWarning : undefined;
  1445. let mentions = [];
  1446. try {
  1447. mentions = JSON.parse(ctx.request.body.mentions || "[]");
  1448. } catch (e) {
  1449. mentions = await extractMentions(text);
  1450. }
  1451. await post.root({ text, mentions, contentWarning });
  1452. ctx.redirect("/public/latest");
  1453. })
  1454. .post("/publish/custom", koaBody(), async (ctx) => {
  1455. const text = String(ctx.request.body.text);
  1456. const obj = JSON.parse(text);
  1457. ctx.body = await post.publishCustom(obj);
  1458. ctx.redirect(`/public/latest`);
  1459. })
  1460. .post("/follow/:feed", koaBody(), async (ctx) => {
  1461. const { feed } = ctx.params;
  1462. const referer = new URL(ctx.request.header.referer);
  1463. ctx.body = await friend.follow(feed);
  1464. ctx.redirect(referer.href);
  1465. })
  1466. .post("/unfollow/:feed", koaBody(), async (ctx) => {
  1467. const { feed } = ctx.params;
  1468. const referer = new URL(ctx.request.header.referer);
  1469. ctx.body = await friend.unfollow(feed);
  1470. ctx.redirect(referer.href);
  1471. })
  1472. .post("/block/:feed", koaBody(), async (ctx) => {
  1473. const { feed } = ctx.params;
  1474. const referer = new URL(ctx.request.header.referer);
  1475. ctx.body = await friend.block(feed);
  1476. ctx.redirect(referer.href);
  1477. })
  1478. .post("/unblock/:feed", koaBody(), async (ctx) => {
  1479. const { feed } = ctx.params;
  1480. const referer = new URL(ctx.request.header.referer);
  1481. ctx.body = await friend.unblock(feed);
  1482. ctx.redirect(referer.href);
  1483. })
  1484. .post("/like/:message", koaBody(), async (ctx) => {
  1485. const { message } = ctx.params;
  1486. const messageKey = message;
  1487. const voteValue = Number(ctx.request.body.voteValue);
  1488. const encoded = {
  1489. message: encodeURIComponent(message),
  1490. };
  1491. const referer = new URL(ctx.request.header.referer);
  1492. referer.hash = `centered-footer-${encoded.message}`;
  1493. const like = async ({ messageKey, voteValue }) => {
  1494. const value = Number(voteValue);
  1495. const message = await post.get(messageKey);
  1496. const isPrivate = message.value.meta.private === true;
  1497. const messageRecipients = isPrivate ? message.value.content.recps : [];
  1498. const normalized = messageRecipients.map((recipient) => {
  1499. if (typeof recipient === "string") {
  1500. return recipient;
  1501. }
  1502. if (typeof recipient.link === "string") {
  1503. return recipient.link;
  1504. }
  1505. return null;
  1506. });
  1507. const recipients = normalized.length > 0 ? normalized : undefined;
  1508. return vote.publish({ messageKey, value, recps: recipients });
  1509. };
  1510. ctx.body = await like({ messageKey, voteValue });
  1511. ctx.redirect(referer.href);
  1512. })
  1513. .post('/forum/create', koaBody(), async ctx => {
  1514. const { category, title, text } = ctx.request.body;
  1515. await forumModel.createForum(category, title, text);
  1516. ctx.redirect('/forum');
  1517. })
  1518. .post('/forum/:id/message', koaBody(), async ctx => {
  1519. const forumId = ctx.params.id;
  1520. const { message, parentId } = ctx.request.body;
  1521. const userId = SSBconfig.config.keys.id;
  1522. const newMessage = { text: message, author: userId, timestamp: new Date().toISOString() };
  1523. await forumModel.addMessageToForum(forumId, newMessage, parentId);
  1524. ctx.redirect(`/forum/${encodeURIComponent(forumId)}`);
  1525. })
  1526. .post('/forum/:forumId/vote', koaBody(), async ctx => {
  1527. const { forumId } = ctx.params;
  1528. const { target, value } = ctx.request.body;
  1529. await forumModel.voteContent(target, parseInt(value, 10));
  1530. const back = ctx.get('referer') || `/forum/${encodeURIComponent(forumId)}`;
  1531. ctx.redirect(back);
  1532. })
  1533. .post('/forum/delete/:id', koaBody(), async ctx => {
  1534. await forumModel.deleteForumById(ctx.params.id);
  1535. ctx.redirect('/forum');
  1536. })
  1537. .post('/legacy/export', koaBody(), async (ctx) => {
  1538. const password = ctx.request.body.password;
  1539. if (!password || password.length < 32) {
  1540. ctx.redirect('/legacy');
  1541. return;
  1542. }
  1543. try {
  1544. const encryptedFilePath = await legacyModel.exportData({ password });
  1545. ctx.body = {
  1546. message: 'Data exported successfully!',
  1547. file: encryptedFilePath
  1548. };
  1549. ctx.redirect('/legacy');
  1550. } catch (error) {
  1551. ctx.status = 500;
  1552. ctx.body = { error: `Error: ${error.message}` };
  1553. ctx.redirect('/legacy');
  1554. }
  1555. })
  1556. .post('/legacy/import', koaBody({
  1557. multipart: true,
  1558. formidable: {
  1559. keepExtensions: true,
  1560. uploadDir: '/tmp',
  1561. }
  1562. }), async (ctx) => {
  1563. const uploadedFile = ctx.request.files?.uploadedFile;
  1564. const password = ctx.request.body.importPassword;
  1565. if (!uploadedFile) {
  1566. ctx.body = { error: 'No file uploaded' };
  1567. ctx.redirect('/legacy');
  1568. return;
  1569. }
  1570. if (!password || password.length < 32) {
  1571. ctx.body = { error: 'Password is too short or missing.' };
  1572. ctx.redirect('/legacy');
  1573. return;
  1574. }
  1575. try {
  1576. await legacyModel.importData({ filePath: uploadedFile.filepath, password });
  1577. ctx.body = { message: 'Data imported successfully!' };
  1578. ctx.redirect('/legacy');
  1579. } catch (error) {
  1580. ctx.body = { error: error.message };
  1581. ctx.redirect('/legacy');
  1582. }
  1583. })
  1584. .post('/trending/:contentId/:category', async (ctx) => {
  1585. const { contentId, category } = ctx.params;
  1586. const voterId = SSBconfig?.keys?.id;
  1587. const target = await trendingModel.getMessageById(contentId);
  1588. if (target?.content?.opinions_inhabitants?.includes(voterId)) {
  1589. ctx.flash = { message: 'You have already opined.' };
  1590. ctx.redirect('/trending');
  1591. return;
  1592. }
  1593. await trendingModel.createVote(contentId, category);
  1594. ctx.redirect('/trending');
  1595. })
  1596. .post('/opinions/:contentId/:category', async (ctx) => {
  1597. const { contentId, category } = ctx.params;
  1598. const voterId = SSBconfig?.keys?.id;
  1599. const target = await opinionsModel.getMessageById(contentId);
  1600. if (target?.content?.opinions_inhabitants?.includes(voterId)) {
  1601. ctx.flash = { message: 'You have already opined.' };
  1602. ctx.redirect('/opinions');
  1603. return;
  1604. }
  1605. await opinionsModel.createVote(contentId, category);
  1606. ctx.redirect('/opinions');
  1607. })
  1608. .post('/agenda/discard/:itemId', async (ctx) => {
  1609. const { itemId } = ctx.params;
  1610. await agendaModel.discardItem(itemId);
  1611. ctx.redirect('/agenda');
  1612. })
  1613. .post('/agenda/restore/:itemId', async (ctx) => {
  1614. const { itemId } = ctx.params;
  1615. await agendaModel.restoreItem(itemId);
  1616. ctx.redirect('/agenda?filter=discarded');
  1617. })
  1618. .post('/feed/create', koaBody(), async ctx => {
  1619. const { text } = ctx.request.body || {};
  1620. await feedModel.createFeed(text.trim());
  1621. ctx.redirect('/feed');
  1622. })
  1623. .post('/feed/opinions/:feedId/:category', async ctx => {
  1624. const { feedId, category } = ctx.params;
  1625. await opinionsModel.createVote(feedId, category);
  1626. ctx.redirect('/feed');
  1627. })
  1628. .post('/feed/refeed/:id', koaBody(), async ctx => {
  1629. await feedModel.createRefeed(ctx.params.id);
  1630. ctx.redirect('/feed');
  1631. })
  1632. .post('/bookmarks/create', koaBody(), async (ctx) => {
  1633. const { url, tags, description, category, lastVisit } = ctx.request.body;
  1634. const formattedLastVisit = lastVisit ? moment(lastVisit).isBefore(moment()) ? moment(lastVisit).toISOString() : moment().toISOString() : moment().toISOString();
  1635. await bookmarksModel.createBookmark(url, tags, description, category, formattedLastVisit);
  1636. ctx.redirect('/bookmarks');
  1637. })
  1638. .post('/bookmarks/update/:id', koaBody(), async (ctx) => {
  1639. const { url, tags, description, category, lastVisit } = ctx.request.body;
  1640. const bookmarkId = ctx.params.id;
  1641. const formattedLastVisit = lastVisit
  1642. ? moment(lastVisit).isBefore(moment())
  1643. ? moment(lastVisit).toISOString()
  1644. : moment().toISOString()
  1645. : moment().toISOString();
  1646. const bookmark = await bookmarksModel.getBookmarkById(bookmarkId);
  1647. if (bookmark.opinions_inhabitants && bookmark.opinions_inhabitants.length > 0) {
  1648. ctx.flash = { message: "This bookmark has received votes and cannot be updated." };
  1649. ctx.redirect(`/bookmarks?filter=mine`);
  1650. }
  1651. await bookmarksModel.updateBookmarkById(bookmarkId, {
  1652. url,
  1653. tags,
  1654. description,
  1655. category,
  1656. lastVisit: formattedLastVisit,
  1657. createdAt: bookmark.createdAt,
  1658. author: bookmark.author,
  1659. });
  1660. ctx.redirect('/bookmarks?filter=mine');
  1661. })
  1662. .post('/bookmarks/delete/:id', koaBody(), async (ctx) => {
  1663. const bookmarkId = ctx.params.id;
  1664. await bookmarksModel.deleteBookmarkById(bookmarkId);
  1665. ctx.redirect('/bookmarks?filter=mine');
  1666. })
  1667. .post('/bookmarks/opinions/:bookmarkId/:category', async (ctx) => {
  1668. const { bookmarkId, category } = ctx.params;
  1669. const voterId = SSBconfig?.keys?.id;
  1670. const bookmark = await bookmarksModel.getBookmarkById(bookmarkId);
  1671. if (bookmark.opinions_inhabitants && bookmark.opinions_inhabitants.includes(voterId)) {
  1672. ctx.flash = { message: "You have already opined." };
  1673. ctx.redirect('/bookmarks');
  1674. return;
  1675. }
  1676. await opinionsModel.createVote(bookmarkId, category, 'bookmark');
  1677. ctx.redirect('/bookmarks');
  1678. })
  1679. .post('/images/create', koaBody({ multipart: true }), async ctx => {
  1680. const blob = await handleBlobUpload(ctx, 'image');
  1681. const { tags, title, description, meme } = ctx.request.body;
  1682. await imagesModel.createImage(blob, tags, title, description, meme);
  1683. ctx.redirect('/images');
  1684. })
  1685. .post('/images/update/:id', koaBody({ multipart: true }), async ctx => {
  1686. const { tags, title, description, meme } = ctx.request.body;
  1687. const blob = ctx.request.files?.image ? await handleBlobUpload(ctx, 'image') : null;
  1688. const match = blob?.match(/\(([^)]+)\)/);
  1689. const blobId = match ? match[1] : blob;
  1690. await imagesModel.updateImageById(ctx.params.id, blobId, tags, title, description, meme);
  1691. ctx.redirect('/images?filter=mine');
  1692. })
  1693. .post('/images/delete/:id', koaBody(), async ctx => {
  1694. await imagesModel.deleteImageById(ctx.params.id);
  1695. ctx.redirect('/images?filter=mine');
  1696. })
  1697. .post('/images/opinions/:imageId/:category', async ctx => {
  1698. const { imageId, category } = ctx.params;
  1699. const voterId = SSBconfig?.keys?.id;
  1700. const image = await imagesModel.getImageById(imageId);
  1701. if (image.opinions_inhabitants && image.opinions_inhabitants.includes(voterId)) {
  1702. ctx.flash = { message: "You have already opined." };
  1703. ctx.redirect('/images');
  1704. return;
  1705. }
  1706. await imagesModel.createOpinion(imageId, category, 'image');
  1707. ctx.redirect('/images');
  1708. })
  1709. .post('/audios/create', koaBody({ multipart: true }), async (ctx) => {
  1710. const audioBlob = await handleBlobUpload(ctx, 'audio');
  1711. const { tags, title, description } = ctx.request.body;
  1712. await audiosModel.createAudio(audioBlob, tags, title, description);
  1713. ctx.redirect('/audios');
  1714. })
  1715. .post('/audios/update/:id', koaBody({ multipart: true }), async (ctx) => {
  1716. const { tags, title, description } = ctx.request.body;
  1717. const blob = ctx.request.files?.audio ? await handleBlobUpload(ctx, 'audio') : null;
  1718. await audiosModel.updateAudioById(ctx.params.id, blob, tags, title, description);
  1719. ctx.redirect('/audios?filter=mine');
  1720. })
  1721. .post('/audios/delete/:id', koaBody(), async (ctx) => {
  1722. await audiosModel.deleteAudioById(ctx.params.id);
  1723. ctx.redirect('/audios?filter=mine');
  1724. })
  1725. .post('/audios/opinions/:audioId/:category', async (ctx) => {
  1726. const { audioId, category } = ctx.params;
  1727. const voterId = SSBconfig?.keys?.id;
  1728. const audio = await audiosModel.getAudioById(audioId);
  1729. if (audio.opinions_inhabitants?.includes(voterId)) {
  1730. ctx.flash = { message: "You have already opined." };
  1731. ctx.redirect('/audios');
  1732. return;
  1733. }
  1734. await audiosModel.createOpinion(audioId, category);
  1735. ctx.redirect('/audios');
  1736. })
  1737. .post('/videos/create', koaBody({ multipart: true }), async (ctx) => {
  1738. const videoBlob = await handleBlobUpload(ctx, 'video');
  1739. const { tags, title, description } = ctx.request.body;
  1740. await videosModel.createVideo(videoBlob, tags, title, description);
  1741. ctx.redirect('/videos');
  1742. })
  1743. .post('/videos/update/:id', koaBody({ multipart: true }), async (ctx) => {
  1744. const { tags, title, description } = ctx.request.body;
  1745. const blob = ctx.request.files?.video ? await handleBlobUpload(ctx, 'video') : null;
  1746. await videosModel.updateVideoById(ctx.params.id, blob, tags, title, description);
  1747. ctx.redirect('/videos?filter=mine');
  1748. })
  1749. .post('/videos/delete/:id', koaBody(), async (ctx) => {
  1750. await videosModel.deleteVideoById(ctx.params.id);
  1751. ctx.redirect('/videos?filter=mine');
  1752. })
  1753. .post('/videos/opinions/:videoId/:category', async (ctx) => {
  1754. const { videoId, category } = ctx.params;
  1755. const voterId = SSBconfig?.keys?.id;
  1756. const video = await videosModel.getVideoById(videoId);
  1757. if (video.opinions_inhabitants?.includes(voterId)) {
  1758. ctx.flash = { message: "You have already opined." };
  1759. ctx.redirect('/videos');
  1760. return;
  1761. }
  1762. await videosModel.createOpinion(videoId, category);
  1763. ctx.redirect('/videos');
  1764. })
  1765. .post('/documents/create', koaBody({ multipart: true }), async (ctx) => {
  1766. const docBlob = await handleBlobUpload(ctx, 'document');
  1767. const { tags, title, description } = ctx.request.body;
  1768. await documentsModel.createDocument(docBlob, tags, title, description);
  1769. ctx.redirect('/documents');
  1770. })
  1771. .post('/documents/update/:id', koaBody({ multipart: true }), async (ctx) => {
  1772. const { tags, title, description } = ctx.request.body;
  1773. const blob = ctx.request.files?.document ? await handleBlobUpload(ctx, 'document') : null;
  1774. await documentsModel.updateDocumentById(ctx.params.id, blob, tags, title, description);
  1775. ctx.redirect('/documents?filter=mine');
  1776. })
  1777. .post('/documents/delete/:id', koaBody(), async (ctx) => {
  1778. await documentsModel.deleteDocumentById(ctx.params.id);
  1779. ctx.redirect('/documents?filter=mine');
  1780. })
  1781. .post('/documents/opinions/:documentId/:category', async (ctx) => {
  1782. const { documentId, category } = ctx.params;
  1783. const voterId = SSBconfig?.keys?.id;
  1784. const document = await documentsModel.getDocumentById(documentId);
  1785. if (document.opinions_inhabitants?.includes(voterId)) {
  1786. ctx.flash = { message: "You have already opined." };
  1787. ctx.redirect('/documents');
  1788. return;
  1789. }
  1790. await documentsModel.createOpinion(documentId, category);
  1791. ctx.redirect('/documents');
  1792. })
  1793. .post('/cv/upload', koaBody({ multipart: true }), async ctx => {
  1794. const photoUrl = await handleBlobUpload(ctx, 'image')
  1795. await cvModel.createCV(ctx.request.body, photoUrl)
  1796. ctx.redirect('/cv')
  1797. })
  1798. .post('/cv/update/:id', koaBody({ multipart: true }), async ctx => {
  1799. const photoUrl = await handleBlobUpload(ctx, 'image')
  1800. await cvModel.updateCV(ctx.params.id, ctx.request.body, photoUrl)
  1801. ctx.redirect('/cv')
  1802. })
  1803. .post('/cv/delete/:id', async ctx => {
  1804. await cvModel.deleteCVById(ctx.params.id)
  1805. ctx.redirect('/cv')
  1806. })
  1807. .post('/cipher/encrypt', koaBody(), async (ctx) => {
  1808. const { text, password } = ctx.request.body;
  1809. if (password.length < 32) {
  1810. ctx.body = { error: 'Password is too short or missing.' };
  1811. ctx.redirect('/cipher');
  1812. return;
  1813. }
  1814. const { encryptedText, iv, salt, authTag } = cipherModel.encryptData(text, password);
  1815. const view = await cipherView(encryptedText, "", iv, password);
  1816. ctx.body = view;
  1817. })
  1818. .post('/cipher/decrypt', koaBody(), async (ctx) => {
  1819. const { encryptedText, password } = ctx.request.body;
  1820. if (password.length < 32) {
  1821. ctx.body = { error: 'Password is too short or missing.' };
  1822. ctx.redirect('/cipher');
  1823. return;
  1824. }
  1825. const decryptedText = cipherModel.decryptData(encryptedText, password);
  1826. const view = await cipherView("", decryptedText, "", password);
  1827. ctx.body = view;
  1828. })
  1829. .post('/tribes/create', koaBody({ multipart: true }), async ctx => {
  1830. const { title, description, location, tags, isLARP, isAnonymous, inviteMode } = ctx.request.body;
  1831. // Block L.A.R.P. creation
  1832. if (isLARP === 'true' || isLARP === true) {
  1833. ctx.status = 400;
  1834. ctx.body = { error: "L.A.R.P. tribes cannot be created." };
  1835. return;
  1836. }
  1837. const image = await handleBlobUpload(ctx, 'image');
  1838. await tribesModel.createTribe(
  1839. title,
  1840. description,
  1841. image,
  1842. location,
  1843. tags,
  1844. isLARP === 'true',
  1845. isAnonymous === 'true',
  1846. inviteMode
  1847. );
  1848. ctx.redirect('/tribes');
  1849. })
  1850. .post('/tribes/update/:id', koaBody({ multipart: true }), async ctx => {
  1851. const { title, description, location, isLARP, isAnonymous, inviteMode, tags } = ctx.request.body;
  1852. // Block L.A.R.P. creation
  1853. if (isLARP === 'true' || isLARP === true) {
  1854. ctx.status = 400;
  1855. ctx.body = { error: "L.A.R.P. tribes cannot be updated." };
  1856. return;
  1857. }
  1858. const parsedTags = tags ? tags.split(',').map(t => t.trim()).filter(Boolean) : [];
  1859. const image = await handleBlobUpload(ctx, 'image');
  1860. await tribesModel.updateTribeById(ctx.params.id, {
  1861. title,
  1862. description,
  1863. image,
  1864. location,
  1865. tags: parsedTags,
  1866. isLARP: isLARP === 'true',
  1867. isAnonymous: isAnonymous === 'true',
  1868. inviteMode
  1869. });
  1870. ctx.redirect('/tribes?filter=mine');
  1871. })
  1872. .post('/tribes/delete/:id', async ctx => {
  1873. await tribesModel.deleteTribeById(ctx.params.id)
  1874. ctx.redirect('/tribes?filter=mine')
  1875. })
  1876. .post('/tribes/generate-invite', koaBody(), async ctx => {
  1877. const { tribeId } = ctx.request.body;
  1878. const inviteCode = await tribesModel.generateInvite(tribeId);
  1879. ctx.body = await renderInvitePage(inviteCode);
  1880. })
  1881. .post('/tribes/join-code', koaBody(), async ctx => {
  1882. const { inviteCode } = ctx.request.body
  1883. await tribesModel.joinByInvite(inviteCode)
  1884. ctx.redirect('/tribes?filter=membership')
  1885. })
  1886. .post('/tribes/leave/:id', koaBody(), async ctx => {
  1887. await tribesModel.leaveTribe(ctx.params.id)
  1888. ctx.redirect('/tribes?filter=membership')
  1889. })
  1890. .post('/tribes/:id/message', koaBody(), async ctx => {
  1891. const tribeId = ctx.params.id;
  1892. const message = ctx.request.body.message;
  1893. await tribesModel.postMessage(tribeId, message);
  1894. ctx.redirect(ctx.headers.referer);
  1895. })
  1896. .post('/tribes/:id/refeed/:msgId', koaBody(), async ctx => {
  1897. const tribeId = ctx.params.id;
  1898. const msgId = ctx.params.msgId;
  1899. await tribesModel.refeed(tribeId, msgId);
  1900. ctx.redirect(ctx.headers.referer);
  1901. })
  1902. .post('/tribe/:id/message', koaBody(), async ctx => {
  1903. const tribeId = ctx.params.id;
  1904. const message = ctx.request.body.message;
  1905. await tribesModel.postMessage(tribeId, message);
  1906. ctx.redirect('/tribes?filter=mine')
  1907. })
  1908. .post('/panic/remove', koaBody(), async (ctx) => {
  1909. const { exec } = require('child_process');
  1910. try {
  1911. await panicmodeModel.removeSSB();
  1912. ctx.body = {
  1913. message: 'Your blockchain has been succesfully deleted!'
  1914. };
  1915. exec('pkill -f "node SSB_server.js start"');
  1916. setTimeout(() => {
  1917. process.exit(0);
  1918. }, 1000);
  1919. } catch (error) {
  1920. ctx.body = {
  1921. error: 'Error deleting your blockchain: ' + error.message
  1922. };
  1923. }
  1924. })
  1925. .post('/export/create', async (ctx) => {
  1926. try {
  1927. const outputPath = path.join(os.homedir(), 'ssb_exported.zip');
  1928. await exportmodeModel.exportSSB(outputPath);
  1929. ctx.set('Content-Type', 'application/zip');
  1930. ctx.set('Content-Disposition', `attachment; filename=ssb_exported.zip`);
  1931. ctx.body = fs.createReadStream(outputPath);
  1932. ctx.res.on('finish', () => {
  1933. fs.unlinkSync(outputPath);
  1934. });
  1935. } catch (error) {
  1936. ctx.body = {
  1937. error: 'Error exporting your blockchain: ' + error.message
  1938. };
  1939. }
  1940. })
  1941. .post('/tasks/create', koaBody(), async (ctx) => {
  1942. const { title, description, startTime, endTime, priority, location, tags, isPublic } = ctx.request.body;
  1943. await tasksModel.createTask(title, description, startTime, endTime, priority, location, tags, isPublic);
  1944. ctx.redirect('/tasks?filter=mine');
  1945. })
  1946. .post('/tasks/update/:id', koaBody(), async (ctx) => {
  1947. const { title, description, startTime, endTime, priority, location, tags, isPublic } = ctx.request.body;
  1948. const taskId = ctx.params.id;
  1949. const task = await tasksModel.getTaskById(taskId);
  1950. await tasksModel.updateTaskById(taskId, {
  1951. title,
  1952. description,
  1953. startTime,
  1954. endTime,
  1955. priority,
  1956. location,
  1957. tags,
  1958. isPublic,
  1959. createdAt: task.createdAt,
  1960. author: task.author
  1961. });
  1962. ctx.redirect('/tasks?filter=mine');
  1963. })
  1964. .post('/tasks/assign/:id', koaBody(), async (ctx) => {
  1965. const taskId = ctx.params.id;
  1966. await tasksModel.toggleAssignee(taskId);
  1967. ctx.redirect('/tasks');
  1968. })
  1969. .post('/tasks/delete/:id', koaBody(), async (ctx) => {
  1970. const taskId = ctx.params.id;
  1971. await tasksModel.deleteTaskById(taskId);
  1972. ctx.redirect('/tasks?filter=mine');
  1973. })
  1974. .post('/tasks/status/:id', koaBody(), async (ctx) => {
  1975. const taskId = ctx.params.id;
  1976. const { status } = ctx.request.body;
  1977. await tasksModel.updateTaskStatus(taskId, status);
  1978. ctx.redirect('/tasks?filter=mine');
  1979. })
  1980. .post('/reports/create', koaBody({ multipart: true }), async ctx => {
  1981. const { title, description, category, tags, severity } = ctx.request.body;
  1982. const image = await handleBlobUpload(ctx, 'image');
  1983. await reportsModel.createReport(title, description, category, image, tags, severity);
  1984. ctx.redirect('/reports');
  1985. })
  1986. .post('/reports/update/:id', koaBody({ multipart: true }), async ctx => {
  1987. const { title, description, category, tags, severity } = ctx.request.body;
  1988. const image = await handleBlobUpload(ctx, 'image');
  1989. await reportsModel.updateReportById(ctx.params.id, {
  1990. title, description, category, image, tags, severity
  1991. });
  1992. ctx.redirect('/reports?filter=mine');
  1993. })
  1994. .post('/reports/delete/:id', async ctx => {
  1995. await reportsModel.deleteReportById(ctx.params.id);
  1996. ctx.redirect('/reports?filter=mine');
  1997. })
  1998. .post('/reports/confirm/:id', async ctx => {
  1999. await reportsModel.confirmReportById(ctx.params.id);
  2000. ctx.redirect('/reports');
  2001. })
  2002. .post('/reports/status/:id', koaBody(), async (ctx) => {
  2003. const reportId = ctx.params.id;
  2004. const { status } = ctx.request.body;
  2005. await reportsModel.updateReportById(reportId, { status });
  2006. ctx.redirect('/reports?filter=mine');
  2007. })
  2008. .post('/events/create', koaBody(), async (ctx) => {
  2009. const { title, description, date, location, price, url, attendees, tags, isPublic } = ctx.request.body;
  2010. await eventsModel.createEvent(title, description, date, location, price, url, attendees, tags, isPublic);
  2011. ctx.redirect('/events?filter=mine');
  2012. })
  2013. .post('/events/update/:id', koaBody(), async (ctx) => {
  2014. const { title, description, date, location, price, url, attendees, tags, isPublic } = ctx.request.body;
  2015. const eventId = ctx.params.id;
  2016. const event = await eventsModel.getEventById(eventId);
  2017. await eventsModel.updateEventById(eventId, {
  2018. title,
  2019. description,
  2020. date,
  2021. location,
  2022. price,
  2023. url,
  2024. attendees,
  2025. tags,
  2026. isPublic,
  2027. createdAt: event.createdAt,
  2028. organizer: event.organizer,
  2029. });
  2030. ctx.redirect('/events?filter=mine');
  2031. })
  2032. .post('/events/attend/:id', koaBody(), async (ctx) => {
  2033. const eventId = ctx.params.id;
  2034. await eventsModel.toggleAttendee(eventId);
  2035. ctx.redirect('/events');
  2036. })
  2037. .post('/events/delete/:id', koaBody(), async (ctx) => {
  2038. const eventId = ctx.params.id;
  2039. await eventsModel.deleteEventById(eventId);
  2040. ctx.redirect('/events?filter=mine');
  2041. })
  2042. .post('/votes/create', koaBody(), async ctx => {
  2043. const { question, deadline, options = 'YES,NO,ABSTENTION', tags = '' } = ctx.request.body;
  2044. const parsedOptions = options.split(',').map(o => o.trim()).filter(Boolean);
  2045. const parsedTags = tags.split(',').map(t => t.trim()).filter(Boolean);
  2046. await votesModel.createVote(question, deadline, parsedOptions, parsedTags);
  2047. ctx.redirect('/votes');
  2048. })
  2049. .post('/votes/update/:id', koaBody(), async ctx => {
  2050. const id = ctx.params.id;
  2051. const { question, deadline, options = 'YES,NO,ABSTENTION', tags = '' } = ctx.request.body;
  2052. const parsedOptions = options.split(',').map(o => o.trim()).filter(Boolean);
  2053. const parsedTags = tags ? tags.split(',').map(t => t.trim()).filter(Boolean) : [];
  2054. await votesModel.updateVoteById(id, { question, deadline, options: parsedOptions, tags: parsedTags });
  2055. ctx.redirect('/votes?filter=mine');
  2056. })
  2057. .post('/votes/delete/:id', koaBody(), async ctx => {
  2058. const id = ctx.params.id;
  2059. await votesModel.deleteVoteById(id);
  2060. ctx.redirect('/votes?filter=mine');
  2061. })
  2062. .post('/votes/vote/:id', koaBody(), async ctx => {
  2063. const id = ctx.params.id;
  2064. const { choice } = ctx.request.body;
  2065. await votesModel.voteOnVote(id, choice);
  2066. ctx.redirect('/votes?filter=open');
  2067. })
  2068. .post('/votes/opinions/:voteId/:category', async (ctx) => {
  2069. const { voteId, category } = ctx.params;
  2070. const voterId = SSBconfig?.keys?.id;
  2071. const vote = await votesModel.getVoteById(voteId);
  2072. if (vote.opinions_inhabitants && vote.opinions_inhabitants.includes(voterId)) {
  2073. ctx.flash = { message: "You have already opined." };
  2074. ctx.redirect('/votes');
  2075. return;
  2076. }
  2077. await opinionsModel.createVote(voteId, category, 'votes');
  2078. ctx.redirect('/votes');
  2079. })
  2080. .post('/market/create', koaBody({ multipart: true }), async ctx => {
  2081. const { item_type, title, description, price, tags, item_status, deadline, includesShipping, stock } = ctx.request.body;
  2082. const image = await handleBlobUpload(ctx, 'image');
  2083. if (!stock || stock <= 0) {
  2084. ctx.throw(400, 'Stock must be a positive number.');
  2085. }
  2086. await marketModel.createItem(item_type, title, description, image, price, tags, item_status, deadline, includesShipping, stock);
  2087. ctx.redirect('/market');
  2088. })
  2089. .post('/market/update/:id', koaBody({ multipart: true }), async ctx => {
  2090. const id = ctx.params.id;
  2091. const { item_type, title, description, price, tags = '', item_status, deadline, includesShipping, stock } = ctx.request.body;
  2092. const parsedTags = tags.split(',').map(t => t.trim()).filter(Boolean);
  2093. if (stock < 0) {
  2094. ctx.throw(400, 'Stock cannot be negative.');
  2095. }
  2096. const updatedData = {
  2097. item_type,
  2098. title,
  2099. description,
  2100. price,
  2101. item_status,
  2102. deadline,
  2103. includesShipping,
  2104. tags: parsedTags,
  2105. stock
  2106. };
  2107. const image = await handleBlobUpload(ctx, 'image');
  2108. updatedData.image = image;
  2109. await marketModel.updateItemById(id, updatedData);
  2110. ctx.redirect('/market?filter=mine');
  2111. })
  2112. .post('/market/delete/:id', koaBody(), async ctx => {
  2113. const id = ctx.params.id;
  2114. await marketModel.deleteItemById(id);
  2115. ctx.redirect('/market?filter=mine');
  2116. })
  2117. .post('/market/sold/:id', koaBody(), async ctx => {
  2118. const id = ctx.params.id;
  2119. const marketItem = await marketModel.getItemById(id);
  2120. if (marketItem.stock <= 0) {
  2121. ctx.throw(400, 'No stock left to mark as sold.');
  2122. }
  2123. if (marketItem.status !== 'SOLD') {
  2124. await marketModel.setItemAsSold(id);
  2125. await marketModel.decrementStock(id);
  2126. }
  2127. ctx.redirect('/market?filter=mine');
  2128. })
  2129. .post('/market/buy/:id', koaBody(), async ctx => {
  2130. const id = ctx.params.id;
  2131. const marketItem = await marketModel.getItemById(id);
  2132. if (marketItem.item_type === 'exchange') {
  2133. if (marketItem.status !== 'SOLD') {
  2134. const buyerId = ctx.request.body.buyerId;
  2135. const { price, title, seller } = marketItem;
  2136. const subject = `Your item "${title}" has been sold`;
  2137. const text = `The item with title: "${title}" has been sold. The buyer with OASIS ID: ${buyerId} purchased it for: $${price}.`;
  2138. await pmModel.sendMessage([seller], subject, text);
  2139. await marketModel.setItemAsSold(id);
  2140. }
  2141. }
  2142. await marketModel.decrementStock(id);
  2143. ctx.redirect('/inbox?filter=sent');
  2144. })
  2145. .post('/market/bid/:id', koaBody(), async ctx => {
  2146. const id = ctx.params.id;
  2147. const { bidAmount } = ctx.request.body;
  2148. const marketItem = await marketModel.getItemById(id);
  2149. await marketModel.addBidToAuction(id, userId, bidAmount);
  2150. if (marketItem.stock > 0 && marketItem.status === 'SOLD') {
  2151. await marketModel.decrementStock(id);
  2152. }
  2153. ctx.redirect('/market?filter=auctions');
  2154. })
  2155. // UPDATE OASIS
  2156. .post("/update", koaBody(), async (ctx) => {
  2157. const util = require("node:util");
  2158. const exec = util.promisify(require("node:child_process").exec);
  2159. async function updateTool() {
  2160. const { stdout, stderr } = await exec("git reset --hard && git pull");
  2161. console.log("oasis@version: updating Oasis...");
  2162. console.log(stdout);
  2163. console.log(stderr);
  2164. const { stdout: shOut, stderr: shErr } = await exec("sh install.sh");
  2165. console.log("oasis@version: running install.sh...");
  2166. console.log(shOut);
  2167. console.error(shErr);
  2168. }
  2169. await updateTool();
  2170. const referer = new URL(ctx.request.header.referer);
  2171. ctx.redirect(referer.href);
  2172. })
  2173. .post("/settings/theme", koaBody(), async (ctx) => {
  2174. const theme = String(ctx.request.body.theme);
  2175. const currentConfig = getConfig();
  2176. if (theme) {
  2177. currentConfig.themes.current = theme;
  2178. const configPath = path.join(__dirname, '../configs', 'oasis-config.json');
  2179. fs.writeFileSync(configPath, JSON.stringify(currentConfig, null, 2));
  2180. ctx.cookies.set("theme", theme);
  2181. ctx.redirect("/settings");
  2182. } else {
  2183. currentConfig.themes.current = "Dark-SNH";
  2184. fs.writeFileSync(path.join(__dirname, 'configs', 'oasis-config.json'), JSON.stringify(currentConfig, null, 2));
  2185. ctx.cookies.set("theme", "Dark-SNH");
  2186. ctx.redirect("/settings");
  2187. }
  2188. })
  2189. .post("/language", koaBody(), async (ctx) => {
  2190. const language = String(ctx.request.body.language);
  2191. ctx.cookies.set("language", language);
  2192. const referer = new URL(ctx.request.header.referer);
  2193. ctx.redirect(referer.href);
  2194. })
  2195. .post("/settings/conn/start", koaBody(), async (ctx) => {
  2196. await meta.connStart();
  2197. ctx.redirect("/peers");
  2198. })
  2199. .post("/settings/conn/stop", koaBody(), async (ctx) => {
  2200. await meta.connStop();
  2201. ctx.redirect("/peers");
  2202. })
  2203. .post("/settings/conn/sync", koaBody(), async (ctx) => {
  2204. await meta.sync();
  2205. ctx.redirect("/peers");
  2206. })
  2207. .post("/settings/conn/restart", koaBody(), async (ctx) => {
  2208. await meta.connRestart();
  2209. ctx.redirect("/peers");
  2210. })
  2211. .post("/settings/invite/accept", koaBody(), async (ctx) => {
  2212. try {
  2213. const invite = String(ctx.request.body.invite);
  2214. await meta.acceptInvite(invite);
  2215. } catch (e) {
  2216. }
  2217. ctx.redirect("/invites");
  2218. })
  2219. .post("/settings/rebuild", async (ctx) => {
  2220. meta.rebuild();
  2221. ctx.redirect("/settings");
  2222. })
  2223. .post("/save-modules", koaBody(), async (ctx) => {
  2224. const modules = [
  2225. 'popular', 'topics', 'summaries', 'latest', 'threads', 'multiverse', 'invites', 'wallet',
  2226. 'legacy', 'cipher', 'bookmarks', 'videos', 'docs', 'audios', 'tags', 'images', 'trending',
  2227. 'events', 'tasks', 'market', 'tribes', 'governance', 'reports', 'opinions', 'transfers',
  2228. 'feed', 'pixelia', 'agenda', 'ai', 'forum'
  2229. ];
  2230. const currentConfig = getConfig();
  2231. modules.forEach(mod => {
  2232. const modKey = `${mod}Mod`;
  2233. const formKey = `${mod}Form`;
  2234. const modValue = ctx.request.body[formKey] === 'on' ? 'on' : 'off';
  2235. currentConfig.modules[modKey] = modValue;
  2236. });
  2237. saveConfig(currentConfig);
  2238. ctx.redirect(`/modules`);
  2239. })
  2240. .post("/settings/ai", koaBody(), async (ctx) => {
  2241. const aiPrompt = String(ctx.request.body.ai_prompt || "").trim();
  2242. if (aiPrompt.length > 128) {
  2243. ctx.status = 400;
  2244. ctx.body = "Prompt too long. Must be 128 characters or fewer.";
  2245. return;
  2246. }
  2247. const currentConfig = getConfig();
  2248. currentConfig.ai = currentConfig.ai || {};
  2249. currentConfig.ai.prompt = aiPrompt;
  2250. saveConfig(currentConfig);
  2251. const referer = new URL(ctx.request.header.referer);
  2252. ctx.redirect("/settings");
  2253. })
  2254. .post('/transfers/create',
  2255. koaBody(),
  2256. async ctx => {
  2257. const { to, concept, amount, deadline, tags } = ctx.request.body
  2258. await transfersModel.createTransfer(to, concept, amount, deadline, tags)
  2259. ctx.redirect('/transfers')
  2260. })
  2261. .post('/transfers/update/:id',
  2262. koaBody(),
  2263. async ctx => {
  2264. const { to, concept, amount, deadline, tags } = ctx.request.body
  2265. await transfersModel.updateTransferById(
  2266. ctx.params.id, to, concept, amount, deadline, tags
  2267. )
  2268. ctx.redirect('/transfers?filter=mine')
  2269. })
  2270. .post('/transfers/confirm/:id', async ctx => {
  2271. await transfersModel.confirmTransferById(ctx.params.id)
  2272. ctx.redirect('/transfers')
  2273. })
  2274. .post('/transfers/delete/:id', async ctx => {
  2275. await transfersModel.deleteTransferById(ctx.params.id)
  2276. ctx.redirect('/transfers?filter=mine')
  2277. })
  2278. .post('/transfers/opinions/:transferId/:category', async ctx => {
  2279. const { transferId, category } = ctx.params
  2280. const voterId = SSBconfig?.keys?.id;
  2281. const t = await transfersModel.getTransferById(transferId)
  2282. if (t.opinions_inhabitants.includes(voterId)) {
  2283. ctx.flash = { message: 'You have already opined.' }
  2284. ctx.redirect('/transfers')
  2285. return
  2286. }
  2287. await opinionsModel.createVote(transferId, category, 'transfer')
  2288. ctx.redirect('/transfers')
  2289. })
  2290. .post("/settings/wallet", koaBody(), async (ctx) => {
  2291. const url = String(ctx.request.body.wallet_url);
  2292. const user = String(ctx.request.body.wallet_user);
  2293. const pass = String(ctx.request.body.wallet_pass);
  2294. const fee = String(ctx.request.body.wallet_fee);
  2295. const currentConfig = getConfig();
  2296. if (url) currentConfig.wallet.url = url;
  2297. if (user) currentConfig.wallet.user = user;
  2298. if (pass) currentConfig.wallet.pass = pass;
  2299. if (fee) currentConfig.wallet.fee = fee;
  2300. saveConfig(currentConfig);
  2301. const referer = new URL(ctx.request.header.referer);
  2302. ctx.redirect(referer.href);
  2303. })
  2304. .post("/wallet/send", koaBody(), async (ctx) => {
  2305. const action = String(ctx.request.body.action);
  2306. const destination = String(ctx.request.body.destination);
  2307. const amount = Number(ctx.request.body.amount);
  2308. const fee = Number(ctx.request.body.fee);
  2309. const { url, user, pass } = getConfig().wallet;
  2310. let balance = null
  2311. try {
  2312. balance = await walletModel.getBalance(url, user, pass);
  2313. } catch (error) {
  2314. ctx.body = await walletErrorView(error);
  2315. }
  2316. switch (action) {
  2317. case 'confirm':
  2318. const validation = await walletModel.validateSend(url, user, pass, destination, amount, fee);
  2319. if (validation.isValid) {
  2320. try {
  2321. ctx.body = await walletSendConfirmView(balance, destination, amount, fee);
  2322. } catch (error) {
  2323. ctx.body = await walletErrorView(error);
  2324. }
  2325. } else {
  2326. try {
  2327. const statusMessages = {
  2328. type: 'error',
  2329. title: 'validation_errors',
  2330. messages: validation.errors,
  2331. }
  2332. ctx.body = await walletSendFormView(balance, destination, amount, fee, statusMessages);
  2333. } catch (error) {
  2334. ctx.body = await walletErrorView(error);
  2335. }
  2336. }
  2337. break;
  2338. case 'send':
  2339. try {
  2340. const txId = await walletModel.sendToAddress(url, user, pass, destination, amount);
  2341. ctx.body = await walletSendResultView(balance, destination, amount, txId);
  2342. } catch (error) {
  2343. ctx.body = await walletErrorView(error);
  2344. }
  2345. break;
  2346. }
  2347. });
  2348. const routes = router.routes();
  2349. const middleware = [
  2350. async (ctx, next) => {
  2351. if (config.public && ctx.method !== "GET") {
  2352. throw new Error(
  2353. "Sorry, many actions are unavailable when Oasis is running in public mode. Please run Oasis in the default mode and try again."
  2354. );
  2355. }
  2356. await next();
  2357. },
  2358. async (ctx, next) => {
  2359. const selectedLanguage = ctx.cookies.get("language") || "en";
  2360. setLanguage(selectedLanguage);
  2361. await next();
  2362. },
  2363. async (ctx, next) => {
  2364. const ssb = await cooler.open();
  2365. const status = await ssb.status();
  2366. const values = Object.values(status.sync.plugins);
  2367. const totalCurrent = Object.values(status.sync.plugins).reduce(
  2368. (acc, cur) => acc + cur,
  2369. 0
  2370. );
  2371. const totalTarget = status.sync.since * values.length;
  2372. const left = totalTarget - totalCurrent;
  2373. const percent = Math.floor((totalCurrent / totalTarget) * 1000) / 10;
  2374. const megabyte = 1024 * 1024;
  2375. if (left > megabyte) {
  2376. ctx.response.body = indexingView({ percent });
  2377. } else {
  2378. try {
  2379. await next();
  2380. } catch (err) {
  2381. ctx.status = err.status || 500;
  2382. ctx.body = { message: err.message || 'Internal Server Error' };
  2383. }
  2384. }
  2385. },
  2386. routes,
  2387. ];
  2388. const { allowHost } = config;
  2389. const app = http({ host, port, middleware, allowHost });
  2390. app._close = () => {
  2391. nameWarmup.close();
  2392. cooler.close();
  2393. };
  2394. module.exports = app;
  2395. if (config.open === true) {
  2396. open(url);
  2397. }