backend.js 84 KB

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