backend.js 86 KB

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