backend.js 85 KB

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