diff options
| -rw-r--r-- | bucky/app/site.js | 4 | ||||
| -rw-r--r-- | bucky/bin/build-search.js | 8 | ||||
| -rw-r--r-- | bucky/search/bdb.js | 106 | ||||
| -rw-r--r-- | bucky/search/lexicon.js | 268 | ||||
| -rw-r--r-- | bucky/search/middleware.js | 160 | ||||
| -rw-r--r-- | bucky/search/parse_term.js | 3 | ||||
| -rw-r--r-- | bucky/search/search.js | 9 | ||||
| -rw-r--r-- | bucky/search/snippet.js | 57 | ||||
| -rw-r--r-- | bucky/search/stopwords.js | 32 | ||||
| -rw-r--r-- | fortune/mail-verbs | 2 | ||||
| -rw-r--r-- | package.json | 4 | ||||
| -rw-r--r-- | public/assets/js/lib/views/details/settings.js | 501 | ||||
| -rw-r--r-- | public/assets/js/lib/views/index/hootbox.js | 1 | ||||
| -rw-r--r-- | public/assets/js/lib/views/mail/mailbox.js | 126 | ||||
| -rw-r--r-- | public/assets/js/lib/views/mail/message.js | 85 | ||||
| -rw-r--r-- | public/assets/js/util/format.js | 3 | ||||
| -rw-r--r-- | views/pages/mailbox.ejs | 2 | ||||
| -rw-r--r-- | views/pages/message.ejs | 21 | ||||
| -rw-r--r-- | views/partials/message.ejs | 20 | ||||
| -rw-r--r-- | yarn.lock | 136 |
20 files changed, 822 insertions, 726 deletions
diff --git a/bucky/app/site.js b/bucky/app/site.js index 3627bac..de42155 100644 --- a/bucky/app/site.js +++ b/bucky/app/site.js @@ -19,6 +19,7 @@ var RedisStore = require("connect-redis")(session); var redisClient = redis.createClient(); var upload = require("../util/upload"); +var lexicon = require("../search/lexicon"); var app, server; @@ -83,7 +84,7 @@ site.init = function () { server = http.createServer(app).listen(process.env.PORT || 5000, function () { console.log( "Bucky listening at http://" + process.env.HOST_NAME + ":%s", - server.address().port + server.address().port, ); }); @@ -101,6 +102,7 @@ site.init = function () { if (process.env.NODE_ENV === "production") { require("../bin/build-scripts"); } + lexicon.watch(); }; site.api = require("./api"); site.pages = require("./pages"); diff --git a/bucky/bin/build-search.js b/bucky/bin/build-search.js index 23657b3..cb12c23 100644 --- a/bucky/bin/build-search.js +++ b/bucky/bin/build-search.js @@ -1,4 +1,6 @@ -var lexicon = require('../search/lexicon') - -lexicon.build().then(() => process.exit()) +var lexicon = require("../search/lexicon"); +lexicon.build().then(() => { + lexicon.save(); + process.exit(); +}); diff --git a/bucky/search/bdb.js b/bucky/search/bdb.js index 0495666..e62f59e 100644 --- a/bucky/search/bdb.js +++ b/bucky/search/bdb.js @@ -1,68 +1,48 @@ var fs = require("fs"); -function berkeleydb(fn) { - var db; - var bdb_lib = require("berkeleydb"); - var dbenv = new bdb_lib.DbEnv(); - var bdb_status = dbenv.open("./search/db/env"); - if (bdb_status) { - console.log("open dbenv failed:", bdb_status); - process.exit(); - } - - fn = "./" + fn + ".db"; - - function exitHandler(options, err) { - if (db) db.close(); - // if (options.cleanup) console.log('clean'); - if (err) console.log(err.stack); - if (options.exit) process.exit(); - } - - // do something when app is closing - process.on("exit", exitHandler.bind(null, { cleanup: true })); - - // catches ctrl+c event - process.on("SIGINT", exitHandler.bind(null, { exit: true })); - - // catches "kill pid" (for example: nodemon restart) - process.on("SIGUSR1", exitHandler.bind(null, { exit: true })); - process.on("SIGUSR2", exitHandler.bind(null, { exit: true })); +var databases = {}; - //catches uncaught exceptions - process.on("uncaughtException", exitHandler.bind(null, { exit: true })); - - function open(fn) { - if (db) db.close(); - var _db = new bdb_lib.Db(dbenv); - var bdb_status = _db.open(fn); - if (bdb_status) { - console.log("openĀ " + fn + " failed:", bdb_status); - process.exit(); - } - db = _db; +function jsondb(dbName) { + if (databases[dbName]) { + return databases[dbName]; } - open(fn); + let db = {}; + let filename = "./" + dbName + ".db"; - return { + // Store context for this database + var controller = { + load: function () { + if (fs.existsSync(filename)) { + try { + db = JSON.parse(fs.readFileSync(filename)); + } catch (err) { + console.error("couldn't read " + filename); + process.exit(); + } + } else { + db = {}; + } + }, + save: function () { + fs.writeFileSync(filename, JSON.stringify(db, false, 0)); + }, + reset: function () { + db = {}; + }, put: function (term, serialized) { - db.put(term, serialized); + db[term] = serialized; }, get: function (term) { - return db.get(term); + return db[term]; }, }; -} -function jsondb(fn) { - let db; - - fn = "./" + fn + ".db"; + databases[dbName] = controller; function exitHandler(options, err) { - if (db) { - fs.writeFileSync(fn, JSON.stringify(db, false, 0)); - } + // if (db) { + // fs.writeFileSync(fn, JSON.stringify(db, false, 0)); + // } // if (options.cleanup) console.log('clean'); if (err) console.log(err.stack); if (options.exit) process.exit(); @@ -81,24 +61,8 @@ function jsondb(fn) { //catches uncaught exceptions process.on("uncaughtException", exitHandler.bind(null, { exit: true })); - if (fs.existsSync(fn)) { - try { - db = JSON.parse(fs.readFileSync(fn)); - } catch (err) { - console.error("couldn't read " + fn); - process.exit(); - } - } else { - db = {}; - } + controller.load(); - return { - put: function (term, serialized) { - db[term] = serialized; - }, - get: function (term) { - return db[term]; - }, - }; + return controller; } -module.exports = process.env.USE_BDB === "true" ? berkeleydb : jsondb; +module.exports = jsondb; diff --git a/bucky/search/lexicon.js b/bucky/search/lexicon.js index dc1d7ab..d725777 100644 --- a/bucky/search/lexicon.js +++ b/bucky/search/lexicon.js @@ -1,129 +1,185 @@ -require('dotenv').load(); +require("dotenv").load(); -var STOPWORDS = require('./stopwords') -var bdb = require('./bdb') -var db = require('../db') +var STOPWORDS = require("./stopwords"); +var bdb = require("./bdb"); +var db = require("../db"); +var parse_term = require("./parse_term"); -var search_db = bdb('search') +var search_db = bdb("search"); -var lexicon = {} -var lex_counts = {} -var total = 0 +var lexicon = new Map(); +var lex_counts = new Map(); +var total = 0; -module.exports = { build: build_index } +module.exports = { + build: build_index, + watch: watch_index, + save: () => search_db.save(), +}; + +var BUILD_DELAY = 1000 * 60 * 60 * 24; +function watch_index() { + build_index(); + console.log( + "rebuilding search index every", + BUILD_DELAY / (60 * 60 * 1000), + "hours", + ); + var interval = setInterval(build_index, BUILD_DELAY); +} function build_index(cb) { - console.log("building index") + console.log("building search index"); + lexicon = new Map(); + lex_counts = new Map(); + total = 0; return parse_threads() .then(parse_comments) .then(parse_files) - .then( () => { - var unique = Object.keys(lexicon).length - console.log( "--- WORD COUNT: ", total ); - console.log( "--- UNIQUE WORDS: ", unique ); + .then(() => { + var unique = lexicon.size; + console.log("--- WORD COUNT: ", total); + console.log("--- UNIQUE WORDS: ", unique); lexicon_store(); - console.log( "Done!") - return { total, unique } - }) + console.log("Done!"); + return { total, unique }; + }); } function parse_threads() { - return db.Thread.where('id', '>', 1).fetchAll().then( (threads) => { - console.log('got threads', threads.length) - threads.forEach( (thread) => { - total += parse_terms({ - string: thread.get('title'), - thread: thread.get('id'), - }) - }) - }) + return db.Thread.where("id", ">", 1) + .fetchAll() + .then((threads) => { + console.log("got threads", threads.length); + for (const thread of threads) { + total += parse_terms({ + string: thread.get("title"), + thread: thread.get("id"), + }); + } + }); } function parse_comments() { - return db.Comment.where('thread', '>', 1).fetchAll().then( (comments) => { - console.log('got comments', comments.length) - comments.forEach( (comment) => { - total += parse_terms({ - string: comment.get('comment').toString(), - thread: comment.get('thread'), - comment: comment.get('id'), - }) - }) - }) + return db.Comment.where("thread", ">", 1) + .fetchAll() + .then((comments) => { + console.log("got comments", comments.length); + for (const comment of comments) { + total += parse_terms({ + string: comment.get("comment").toString(), + thread: comment.get("thread"), + comment: comment.get("id"), + }); + } + }); } function parse_files() { - return db.File.fetchAll().then( (files) => { - console.log('got files', files.length) - files.forEach( (file) => { + return db.File.fetchAll().then((files) => { + console.log("got files", files.length); + for (const file of files) { total += parse_terms({ - string: file.get('filename'), - thread: file.get('thread'), - file: file.get('id'), - }) - }) - }) + string: file.get("filename"), + thread: file.get("thread"), + file: file.get("id"), + }); + } + }); } -var underscoreRegexp = new RegExp('_', 'g') -var spaceRegexp = new RegExp('[^a-zA-Z0-9]+', 'g') +var underscoreRegexp = new RegExp("_", "g"); +var spaceRegexp = new RegExp("[^a-zA-Z0-9]+", "g"); -function parse_terms (opt) { - var thread = opt.thread - var comment = opt.comment || 0 - var file = opt.file || 0 - var string = opt.string - if (!string || !thread) return 0 - var count = 0 - var terms = string - .replace(underscoreRegexp, ' ') - .split(spaceRegexp) - .forEach((term) => { - var t = term.toLowerCase() - var lookup = lexicon[t] = lexicon[t] || {} - var res = lookup[thread] = lookup[thread] || { strength: 0 } - res.thread = res.thread || thread - res.comment = res.comment || comment - res.file = res.file || file - // prioritize threads - if (!comment && !file) { - res.strength += 2 - } - else { - res.strength += 1 - } - count += 1 - lex_counts[term] = lex_counts[term] || 0 - lex_counts[term] += 1 - }) - return count || 0 +/** + * For each term, create mappings: + * - lexicon[term][thread] => {thread, comment, file, strength} + * - lex_counts[term] => document frequency + * - total terms ++ + */ +function parse_terms(opt) { + var thread = opt.thread; + var comment = opt.comment || 0; + var file = opt.file || 0; + var string = opt.string; + if (!string || !thread) { + return 0; + } + var term_count = 0; + var terms = string.replace(underscoreRegexp, " ").split(spaceRegexp); + for (const term of terms) { + var parsedTerm = parse_term(term); + if (STOPWORDS.has(parsedTerm)) { + continue; + } + if (!term || !parsedTerm) { + continue; + } + if (!lexicon.has(parsedTerm)) { + lexicon.set(parsedTerm, {}); + } + var lookup = lexicon.get(parsedTerm); + lookup[thread] = lookup[thread] || { strength: 1 }; + + var res = lookup[thread]; + res.thread = res.thread || thread; + res.comment = res.comment || comment; + res.file = res.file || file; + + // prioritize threads + if (!comment && !file) { + res.strength += 100; + } else { + res.strength += 1; + } + term_count += 1; + + if (!lex_counts.has(parsedTerm)) { + lex_counts.set(parsedTerm, new Set()); + } + const lex_count = lex_counts.get(parsedTerm); + + try { + lex_count.add(res.thread); + } catch (error) { + console.error(error); + console.log(term, terms, lex_count); + } + } + return term_count || 0; } -var put_total = 0 -function lexicon_store () { - console.log('writing db...') - Object.keys(lexicon).forEach( (term) => { - if (STOPWORDS.has(term)) return - var serialized = serialize_matches(term); - if (! serialized) return; - if ((put_total % 5000) === 0) console.log(put_total + '...') - put_total += 1 - // if (put_total > 10) return - // console.log(term) - search_db.put(term, serialized) - }) +var put_total = 0; +function lexicon_store() { + console.log("writing db..."); + // console.log(Object.keys(lexicon)); + search_db.reset(); + for (const term of lexicon.keys()) { + var serialized = serialize_matches(term); + if (!serialized) return; + if (put_total % 5000 === 0) console.log(put_total + "..."); + put_total += 1; + // if (put_total > 10) return + // console.log(term) + search_db.put(term, serialized); + } + // search_db.save(); +} +function serialize_matches(term) { + var matches = lexicon.get(term); + var lex_count = lex_counts.get(term)?.size || 0; + if (!lex_count) { + return null; + } + var idf = Math.log(total / lex_count); + var serialized_matches = []; + Object.values(matches).forEach((match) => { + if (!match) return; + var s = [ + match.thread, + match.comment, + match.file, + Number((match.strength * idf).toFixed(2)), + ]; + if (s) serialized_matches.push(s); + }); + if (!serialized_matches.length) return; + return serialized_matches; } -function serialize_matches (term) { - var matches = lexicon[term] - var idf = Math.log(total / lex_counts[term]) - var serialized_matches = []; - Object.values(matches).forEach( (match) => { - if (!match) return - var s = [ - match.thread, - match.comment, - match.file, - match.strength * idf - ].join(' ') - if (s) serialized_matches.push(s) - }) - if (!serialized_matches.length) return - return serialized_matches.join(',') -}
\ No newline at end of file diff --git a/bucky/search/middleware.js b/bucky/search/middleware.js index 0cca05c..a93ee7f 100644 --- a/bucky/search/middleware.js +++ b/bucky/search/middleware.js @@ -1,111 +1,121 @@ -var db = require('../db') +var db = require("../db"); -var search = require('./search') -var snippet = require('./snippet') -var lexicon = require('./lexicon') +var search = require("./search"); +var snippet = require("./snippet"); +var lexicon = require("./lexicon"); module.exports = { - search: function (req, res, next) { - res.search = search.search(req.query.query, req.query.start, req.query.limit) - if (! res.search) { - res.sendStatus(400) - return + res.search = search.search( + req.query.query, + req.query.start, + req.query.limit, + ); + if (!res.search) { + res.sendStatus(400); + return; } - next() + next(); }, - - getThreads: function (req, res, next){ + + getThreads: function (req, res, next) { var thread_ids = res.search.thread_ids; - if (! thread_ids || ! thread_ids.length) { - res.search.threads = [] - return next() + if (!thread_ids || !thread_ids.length) { + res.search.threads = []; + return next(); } - db.getThreadsById(thread_ids).then(function(threads){ + db.getThreadsById(thread_ids).then(function (threads) { threads.forEach((thread) => { - var flag_id = thread.get('flagged') + var flag_id = thread.get("flagged"); if (flag_id) { - res.search.file_ids.push(flag_id) + res.search.file_ids.push(flag_id); } - }) - res.search.threads = threads - next() - }) + }); + res.search.threads = threads; + next(); + }); }, - - getComments: function (req, res, next){ + + getComments: function (req, res, next) { var comment_ids = res.search.comment_ids; - if (! comment_ids || ! comment_ids.length) { - res.search.comments = [] - return next() + if (!comment_ids || !comment_ids.length) { + res.search.comments = []; + return next(); } - db.getCommentsById(comment_ids).then(function(comments){ - var terms = res.search.meta.terms - comments.forEach(function(comment){ - const snip = snippet(comment.get('comment').toString(), terms) - comment.set('comment', snip) - }) - res.search.comments = comments - next() - }) + db.getCommentsById(comment_ids).then(function (comments) { + var terms = res.search.meta.terms; + comments.forEach(function (comment) { + const snip = snippet(comment.get("comment").toString(), terms); + comment.set("comment", snip); + }); + res.search.comments = comments; + next(); + }); }, - - getFiles: function (req, res, next){ - var file_ids = res.search.file_ids - if (! file_ids || ! file_ids.length) { - res.search.files = [] - return next() + + getFiles: function (req, res, next) { + var file_ids = res.search.file_ids; + if (!file_ids || !file_ids.length) { + res.search.files = []; + return next(); } - db.getFilesById(file_ids).then(function(files){ - res.search.files = files - next() - }) + db.getFilesById(file_ids).then(function (files) { + res.search.files = files; + next(); + }); }, - logQuery: function(req, res, next) { + logQuery: function (req, res, next) { // req.search.query, req.search.count - next() + next(); }, - success: function(req, res, next){ - var terms = res.search.meta.terms - var threads = {}, comments = {}, files = {} - res.search.threads.forEach((t) => { threads[t.id] = t }) - res.search.comments.forEach((t) => { comments[t.id] = t }) - res.search.files.forEach((t) => { files[t.id] = t }) + success: function (req, res, next) { + var terms = res.search.meta.terms; + var threads = {}, + comments = {}, + files = {}; + res.search.threads.forEach((t) => { + threads[t.id] = t; + }); + res.search.comments.forEach((t) => { + comments[t.id] = t; + }); + res.search.files.forEach((t) => { + files[t.id] = t; + }); var results = res.search.results.map((r) => { - var m = {} - m.thread = threads[r.thread] - m.comment = comments[r.comment] - m.file = files[r.file] - m.count = r.count - m.strength = r.strength + var m = {}; + m.thread = threads[r.thread]; + m.comment = comments[r.comment]; + m.file = files[r.file]; + m.count = r.count; + m.strength = r.strength; if (m.thread) { - var flagged = m.thread.get('flagged') + var flagged = m.thread.get("flagged"); if (flagged) { - m.thread.set('flagged', files[flagged]) + m.thread.set("flagged", files[flagged]); } - var allowed = m.thread.get('allowed') + var allowed = m.thread.get("allowed"); if (allowed) { - m.thread.set('allowed', allowed.toString().split(" ")) + m.thread.set("allowed", allowed.toString().split(" ")); } - var display = m.thread.get('display') + var display = m.thread.get("display"); if (display) { - m.thread.set('display', display.toString().split(" ")) + m.thread.set("display", display.toString().split(" ")); } } - return m - }) + return m; + }); res.json({ meta: res.search.meta, results: results, - }) + }); }, - rebuild: function(req, res, next){ - lexicon.build().then( (data) => { - res.json(data) - }) + rebuild: function (req, res, next) { + lexicon.build().then((data) => { + res.json(data); + }); }, - -} +}; diff --git a/bucky/search/parse_term.js b/bucky/search/parse_term.js new file mode 100644 index 0000000..9cac238 --- /dev/null +++ b/bucky/search/parse_term.js @@ -0,0 +1,3 @@ +module.exports = function parse_term(term) { + return term ? String(term).toLowerCase().replace(/s?$/, "") : ""; +}; diff --git a/bucky/search/search.js b/bucky/search/search.js index fb3bb2d..8924b1f 100644 --- a/bucky/search/search.js +++ b/bucky/search/search.js @@ -1,12 +1,14 @@ var db = require("../db"); var bdb = require("./bdb")("search"); var STOPWORDS = require("./stopwords"); +var parse_term = require("./parse_term"); var wordRegexp = new RegExp("[^a-z0-9]+", "g"); function parse_terms(s) { return s .toLowerCase() .split(wordRegexp) + .map(parse_term) .filter((term) => !!term); } function cmp(a, b) { @@ -16,12 +18,11 @@ function cmp(a, b) { function find_term(term) { var row = bdb.get(term); if (!row) return []; - var res = row.toString(); + var res = row; // console.log(res) if (!res.length) return []; - var matches = res.split(",").map((s) => { - if (!s.length) return; - var partz = s.split(" "); + var matches = res.map((partz) => { + if (!partz.length) return; return { thread: parseInt(partz[0]), comment: parseInt(partz[1]), diff --git a/bucky/search/snippet.js b/bucky/search/snippet.js index 17988d2..787a53f 100644 --- a/bucky/search/snippet.js +++ b/bucky/search/snippet.js @@ -1,35 +1,36 @@ -var util = require('../util/util') -var STOPWORDS = require('./stopwords') +var util = require("../util/util"); +var STOPWORDS = require("./stopwords"); +var parse_term = require("./parse_term"); function snippet(s, terms) { - s = util.sanitize(s) - var term_set = new Set(terms) - - var words = s.split(/[^a-zA-Z0-9]+/) - var snippet = ""; - + s = util.sanitize(s); + var term_set = new Set(terms); + + var words = s.split(/[^a-zA-Z0-9]+/); + var snippet = ""; + // deduper for matching @words indexes, so we don't add a word twice - var index_matches = {} + var index_matches = {}; // words in the eventual snippet - var words_matched = [] + var words_matched = []; // counter for aggregating context after a match - var aggr = 0; + var aggr = 0; // amount of context to show, in number of words surrounding a match var pad = 10; // loop over each of the words in the string - var word for (var i = 0, len = words.length; i < len; i++) { - word = words[i] + var word = words[i]; + var term = parse_term(word); - // if the word matches... - if (term_set.has(word.toLowerCase()) && ! STOPWORDS.has(word.toLowerCase())) { + // if the word matches... + if (term && term_set.has(term) && !STOPWORDS.has(term.toLowerCase())) { // if we aren't already aggregating, add an ellipsis - if (! aggr) { - words_matched.push("...") + if (!aggr) { + words_matched.push("..."); } // look backward $pad words @@ -44,38 +45,38 @@ function snippet(s, terms) { if (index_matches[idx]) continue INNER; // checks out, save this word - words_matched.push(words[idx]) + words_matched.push(words[idx]); // note the matching index in our deduper index_matches[idx] = 1; - } + } // enter aggregate mode -- add the next (pad) words aggr = pad; - } + } // have we been told to aggregate? else if (aggr) { // save this word - words_matched.push(word) + words_matched.push(word); // add index to the deduper index_matches[i] = 1; // one less word to aggregate aggr--; - } + } // keep snippets to a modest length - if (words_matched.length > 30) break - } + if (words_matched.length > 30) break; + } // add a trailing ellipsis - words_matched.push("...") + words_matched.push("..."); // create the snippet from the saved context words - snippet = words_matched.join(" ") + snippet = words_matched.join(" "); - return snippet + return snippet; } -module.exports = snippet
\ No newline at end of file +module.exports = snippet; diff --git a/bucky/search/stopwords.js b/bucky/search/stopwords.js index ceffe14..735e94d 100644 --- a/bucky/search/stopwords.js +++ b/bucky/search/stopwords.js @@ -1,18 +1,18 @@ module.exports = new Set( - "a about above across adj after again against all almost alone along also " + - "although always am among an and another any anybody anyone anything anywhere " + - "apart are around as aside at away be because been before behind being below " + - "besides between beyond both but by can cannot could did do does doing done " + - "down downwards during each either else enough etc even ever every everybody " + - "everyone except far few for forth from get gets got had hardly has have having " + - "her here herself him himself his how however i if in indeed instead into inward " + - "is it its itself just kept many maybe might mine more most mostly much must " + - "myself near neither next no nobody none nor not nothing nowhere of off often on " + - "only onto or other others ought our ours out outside over own p per please plus " + - "pp quite rather really said seem self selves several shall she should since so " + - "some somebody somewhat still such than that the their theirs them themselves " + - "then there therefore these they this thorough thoroughly those through thus to " + - "together too toward towards under until up upon v very was well were what " + - "whatever when whenever where whether which while who whom whose will with" + - "within without would yet young your yourself s".split(" ") + "a adj " + + "am an and " + + "are as at be been " + + "but by can could did do does doing done " + + "down " + + "far few for forth from get gets got had hardly has have having " + + "her here herself him himself his how i if in into " + + "is it its itself just kept many maybe might mine more much must " + + "myself near neither next no none nor not of off often on " + + "only onto or other others ought our ours out over own p per please plus " + + "pp quite rather really said seem self selves several shall she should since so " + + "some somebody somewhat still such than that the their theirs them themselves " + + "then there therefore these they this thorough thoroughly those through thus to " + + "together too toward towards under until up upon v very was well were what " + + "whatever when whenever where whether which while who whom whose will with" + + "within without would yet young your yourself s".split(" "), ); diff --git a/fortune/mail-verbs b/fortune/mail-verbs index 2ac2d6d..ab37cb5 100644 --- a/fortune/mail-verbs +++ b/fortune/mail-verbs @@ -96,7 +96,7 @@ flexed that cosmic wit said it... in a song lassoed a cold one passed out on your lawn -crossed picket lines to say +stood on picket lines to say minged up the div forwarded this spam accidentally rapped diff --git a/package.json b/package.json index 351c15c..018f7ec 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "build:scripts": "node bucky/bin/build-scripts", "export:keyword": "node bucky/bin/export-keyword", "test": "echo \"Error: no test specified\" && exit 1", - "restart": "git pull; pm2 restart bucky; yarn build:scripts" + "restart": "git pull && node bucky/bin/build-scripts && node index" }, "repository": { "type": "git", @@ -37,7 +37,7 @@ "mongodb": "^2.2.36", "multer": "^1.4.2", "multiparty": "^4.2.1", - "mysql2": "^0.15.8", + "mysql2": "^3.14.3", "node-fetch": "^1.7.3", "node-uuid": "^1.4.8", "passport": "^0.3.0", diff --git a/public/assets/js/lib/views/details/settings.js b/public/assets/js/lib/views/details/settings.js index c2ff078..fd23c46 100644 --- a/public/assets/js/lib/views/details/settings.js +++ b/public/assets/js/lib/views/details/settings.js @@ -1,9 +1,8 @@ var ThreadSettingsForm = FormView.extend({ - el: "#thread_settings", events: { - "click": "hide", + // click: "hide", "click .inner": "stopPropagation", "click .thread_delete": "deleteThread", "click .file_delete": "deleteFile", @@ -20,152 +19,174 @@ var ThreadSettingsForm = FormView.extend({ }, action: "", - method: 'put', + method: "put", - initialize: function(){ - this.__super__.initialize.call(this) - this.template = this.$(".template").html() - this.allowedTemplate = this.$(".allowedTemplate").html() - this.filesTemplate = this.$(".settingsFilesTemplate").html() + initialize: function () { + this.__super__.initialize.call(this); + this.template = this.$(".template").html(); + this.allowedTemplate = this.$(".allowedTemplate").html(); + this.filesTemplate = this.$(".settingsFilesTemplate").html(); }, - populate: function(){ - var data = this.options.parent.data - var keywords = data.keywords - var keyword = data.keyword - var thread = data.thread - var comments = data.comments - var files = data.files - var settings = thread.settings - var display = thread.display + populate: function () { + var data = this.options.parent.data; + var keywords = data.keywords; + var keyword = data.keyword; + var thread = data.thread; + var comments = data.comments; + var files = data.files; + var settings = thread.settings; + var display = thread.display; - this.thread = thread - this.files = data.files - this.action = "/api/thread/" + thread.id - this.allowed = (this.thread.allowed || "").split(" ").map(s => s.trim()).filter(s => !! s) + this.thread = thread; + this.files = data.files; + this.action = "/api/thread/" + thread.id; + this.allowed = (this.thread.allowed || "") + .split(" ") + .map((s) => s.trim()) + .filter((s) => !!s); - this.$(".close_link").attr("href", "/details/" + thread.id) - this.$(".metadata").html(metadata(thread)) - this.$("[name=title]").val(thread.title) + this.$(".close_link").attr("href", "/details/" + thread.id); + this.$(".metadata").html(metadata(thread)); + this.$("[name=title]").val(thread.title); - this.$("[name=hootbox]").prop("checked", !!thread.settings.hootbox) - this.$("[name=shorturls]").prop("checked", !!thread.settings.shorturls) - this.$("[name=noupload]").prop("checked", !!thread.settings.noupload) - this.$("[name=privacy]").prop("checked", !!thread.privacy) + this.$("[name=hootbox]").prop("checked", !!thread.settings.hootbox); + this.$("[name=shorturls]").prop("checked", !!thread.settings.shorturls); + this.$("[name=noupload]").prop("checked", !!thread.settings.noupload); + this.$("[name=privacy]").prop("checked", !!thread.privacy); - var $color = this.$('[name=color]') + var $color = this.$("[name=color]"); Object.keys(COLORS).forEach((color) => { - var option = document.createElement('option') - option.value = color - option.innerHTML = color - $color.append(option) - }) - $color.val(thread && thread.color? thread.color: keyword? keyword.color: "") + var option = document.createElement("option"); + option.value = color; + option.innerHTML = color; + $color.append(option); + }); + $color.val( + thread && thread.color ? thread.color : keyword ? keyword.color : "", + ); - var $sort = this.$('[name=sort]') + var $sort = this.$("[name=sort]"); FILE_SORTS.forEach((sort) => { - var option = document.createElement('option') - option.value = sort.key - option.innerHTML = sort.label - $sort.append(option) - }) - $sort.val(thread.settings.sort || "name_asc") + var option = document.createElement("option"); + option.value = sort.key; + option.innerHTML = sort.label; + $sort.append(option); + }); + $sort.val(thread.settings.sort || "name_asc"); - this.toggleAllowed() - this.fetchKeywords() + this.toggleAllowed(); + this.fetchKeywords(); - var $files = this.$(".files") - $files.empty() - files.sort((a,b) => cmp(a.filename, b.filename)) - .forEach(file => { - var size = hush_size(file.size) - var datetime = verbose_date(file.date, true) - var date_class = carbon_date(file.date) - var link = make_link(file) - var t = this.filesTemplate.replace(/{{username}}/g, file.username) - .replace(/{{link}}/g, link) - .replace(/{{filename}}/g, file.filename) - .replace(/{{date_class}}/g, date_class) - .replace(/{{date}}/g, datetime[0]) - .replace(/{{time}}/g, datetime[1]) - .replace(/{{size_class}}/g, size[0]) - .replace(/{{size}}/g, size[1]) - .replace(/{{id}}/g, file.id) - var $t = $(t) - $files.append($t) - }) + var $files = this.$(".files"); + $files.empty(); + files + .sort((a, b) => cmp(a.filename, b.filename)) + .forEach((file) => { + var size = hush_size(file.size); + var datetime = verbose_date(file.date, true); + var date_class = carbon_date(file.date); + var link = make_link(file); + var t = this.filesTemplate + .replace(/{{username}}/g, file.username) + .replace(/{{link}}/g, link) + .replace(/{{filename}}/g, file.filename) + .replace(/{{date_class}}/g, date_class) + .replace(/{{date}}/g, datetime[0]) + .replace(/{{time}}/g, datetime[1]) + .replace(/{{size_class}}/g, size[0]) + .replace(/{{size}}/g, size[1]) + .replace(/{{id}}/g, file.id); + var $t = $(t); + $files.append($t); + }); - $("body").removeClass("loading") + $("body").removeClass("loading"); }, - fetchKeywords: function(thread){ - $.get('/api/keywords', function(data){ - var $keyword = this.$('[name=keyword]') - data.keywords - .map( (a) => a.keyword) - .sort( (a,b) => a < b ? -1 : a === b ? 0 : 1 ) - .forEach((keyword) => { - var option = document.createElement('option') - option.value = keyword - option.innerHTML = keyword - $keyword.append(option) - }) - $keyword.val(this.thread.keyword) - }.bind(this)) + fetchKeywords: function (thread) { + $.get( + "/api/keywords", + function (data) { + var $keyword = this.$("[name=keyword]"); + data.keywords + .map((a) => a.keyword) + .sort((a, b) => (a < b ? -1 : a === b ? 0 : 1)) + .forEach((keyword) => { + var option = document.createElement("option"); + option.value = keyword; + option.innerHTML = keyword; + $keyword.append(option); + }); + $keyword.val(this.thread.keyword); + }.bind(this), + ); }, - loadThreads: function(threads){ + loadThreads: function (threads) { // update the dropdown list of threads - var $thread_select = this.$('[name=thread]') + var $thread_select = this.$("[name=thread]"); if (!threads || !threads.length) { - $thread_select.parent().hide() - return + $thread_select.parent().hide(); + return; } - $thread_select.parent().show() + $thread_select.parent().show(); threads - .map( (a) => [a.title.toLowerCase(), a]) - .sort( (a,b) => a[0].localeCompare(b[0]) ) - .forEach((pair) => { - const thread = pair[1] - var option = document.createElement('option') - option.value = thread.id - // console.log(thread, get_revision(thread)) - option.innerHTML = '[' + thread.id + get_revision(thread) + '] ' + sanitize(thread.title) - $thread_select.append(option) - }) + .map((a) => [a.title.toLowerCase(), a]) + .sort((a, b) => a[0].localeCompare(b[0])) + .forEach((pair) => { + const thread = pair[1]; + var option = document.createElement("option"); + option.value = thread.id; + // console.log(thread, get_revision(thread)) + option.innerHTML = + "[" + + thread.id + + get_revision(thread) + + "] " + + sanitize(thread.title); + $thread_select.append(option); + }); // console.log(threads) }, - toggleAllowed: function(e){ - var checked = this.$('[name=privacy]').prop('checked') - this.$(".allowed_field_container").toggle(checked) - this.$(".allowed_names").toggle(checked) - if (! checked) return - this.$("[name=allowed_field]").focus() - this.displayAllowed() + toggleAllowed: function (e) { + var checked = this.$("[name=privacy]").prop("checked"); + this.$(".allowed_field_container").toggle(checked); + this.$(".allowed_names").toggle(checked); + if (!checked) return; + this.$("[name=allowed_field]").focus(); + this.displayAllowed(); }, - displayAllowed: function(e){ - var $allowedNames = this.$(".allowed_names").empty() - this.allowed.forEach(username => { - var t = this.allowedTemplate.replace(/{{username}}/g, username) - $allowedNames.append(t) - }) + displayAllowed: function (e) { + var $allowedNames = this.$(".allowed_names").empty(); + this.allowed.forEach((username) => { + var t = this.allowedTemplate.replace(/{{username}}/g, username); + $allowedNames.append(t); + }); }, - keydownAllowed: function(e){ - if (e.keyCode === 13) { // enter - e.preventDefault() - e.stopPropagation() - this.updateAllowed() + keydownAllowed: function (e) { + if (e.keyCode === 13) { + // enter + e.preventDefault(); + e.stopPropagation(); + this.updateAllowed(); } }, - updateAllowed: function(){ - var usernames = this.$('[name=allowed_field]').val().replace(/,/g, ' ').split(' ').map(s => s.trim()).filter(s => !! s) - this.$('[name=allowed_field]').val('') - usernames = usernames.filter( (name) => this.allowed.indexOf(name) === -1 ) - .map( (name) => sanitizeHTML(name) ) + updateAllowed: function () { + var usernames = this.$("[name=allowed_field]") + .val() + .replace(/,/g, " ") + .split(" ") + .map((s) => s.trim()) + .filter((s) => !!s); + this.$("[name=allowed_field]").val(""); + usernames = usernames + .filter((name) => this.allowed.indexOf(name) === -1) + .map((name) => sanitizeHTML(name)); $.ajax({ method: "PUT", url: "/api/checkUsernames", @@ -173,31 +194,33 @@ var ThreadSettingsForm = FormView.extend({ data: JSON.stringify({ csrf: csrf(), usernames: usernames }), contentType: "application/json", dataType: "json", - success: function(data){ - if (! data.usernames || ! data.usernames.length) return - this.allowed = this.allowed.concat(data.usernames) - this.displayAllowed() + success: function (data) { + if (!data.usernames || !data.usernames.length) return; + this.allowed = this.allowed.concat(data.usernames); + this.displayAllowed(); }.bind(this), - }) + }); }, - removeAllowed: function(){ - this.allowed = this.$("#allowed_names input[type=checkbox]:checked").map(function(){ - return $(this).val() - }) - this.displayAllowed() + removeAllowed: function () { + this.allowed = this.$("#allowed_names input[type=checkbox]:checked").map( + function () { + return $(this).val(); + }, + ); + this.displayAllowed(); }, - validate: function(){ - var errors = [] - var title = $("[name=title]").val() - if (! title || ! title.length) { - errors.push("Please enter a title.") + validate: function () { + var errors = []; + var title = $("[name=title]").val(); + if (!title || !title.length) { + errors.push("Please enter a title."); } - return errors.length ? errors : null + return errors.length ? errors : null; }, - serialize: function(){ + serialize: function () { var data = { title: $("[name=title]").val(), keyword: $("[name=keyword]").val(), @@ -208,115 +231,127 @@ var ThreadSettingsForm = FormView.extend({ hootbox: $("[name=hootbox]:checked").val() ? true : false, shorturls: $("[name=shorturls]:checked").val() ? true : false, noupload: $("[name=noupload]:checked").val() ? true : false, - sort: $("[name=sort]").val() + sort: $("[name=sort]").val(), }, - } - return JSON.stringify(data) + }; + return JSON.stringify(data); }, - success: function(data){ - console.log(data) - window.location.href = "/details/" + this.options.parent.data.thread.id + success: function (data) { + console.log(data); + window.location.href = "/details/" + this.options.parent.data.thread.id; }, visible: false, - show: function(){ - this.visible = true - app.typing = true - this.populate() - this.$el.addClass('visible') - app.router.pushState("/details/" + this.options.parent.data.thread.id + "/settings") + show: function () { + this.visible = true; + app.typing = true; + this.populate(); + this.$el.addClass("visible"); + app.router.pushState( + "/details/" + this.options.parent.data.thread.id + "/settings", + ); }, - hide: function(e){ - e && e.preventDefault() - this.visible = false - app.typing = false - this.$el.removeClass('visible') - app.router.pushState("/details/" + this.options.parent.data.thread.id) + hide: function (e) { + e && e.preventDefault(); + this.visible = false; + app.typing = false; + this.$el.removeClass("visible"); + app.router.pushState("/details/" + this.options.parent.data.thread.id); }, - toggle: function(){ - if (this.visible) this.hide() - else this.show() + toggle: function () { + if (this.visible) this.hide(); + else this.show(); }, - changeColor: function(){ - var color_name = this.$("[name=color]").val() - set_background_color(color_name) + changeColor: function () { + var color_name = this.$("[name=color]").val(); + set_background_color(color_name); }, - changeSort: function(){ - var sort_name = this.$("[name=sort]").val() - console.log(">", sort_name) - app.view.files.resort(sort_name) + changeSort: function () { + var sort_name = this.$("[name=sort]").val(); + console.log(">", sort_name); + app.view.files.resort(sort_name); }, - toggleFile: function(e){ + toggleFile: function (e) { // e.preventDefault() - e.stopPropagation() - const $input = $(e.currentTarget) - const $tr = $input.closest('tr.file') - const value = e.currentTarget.checked + e.stopPropagation(); + const $input = $(e.currentTarget); + const $tr = $input.closest("tr.file"); + const value = e.currentTarget.checked; // $(e.currentTarget).prop('checked', value) // console.log('check', $input, value) - this.toggleFileChecked($tr, null, value) + this.toggleFileChecked($tr, null, value); }, - toggleFileRow: function(e){ + toggleFileRow: function (e) { // e.preventDefault() - e.stopPropagation() - const $tr = $(e.currentTarget) - const $input = $tr.find('input[type="checkbox"]') - const value = ! $input.prop('checked') - this.toggleFileChecked($tr, $input, value) + e.stopPropagation(); + const $tr = $(e.currentTarget); + const $input = $tr.find('input[type="checkbox"]'); + const value = !$input.prop("checked"); + this.toggleFileChecked($tr, $input, value); }, - toggleFileChecked: function($tr, $input, value){ - $tr.toggleClass('checked', value) - if ($input) $input.prop('checked', value) + toggleFileChecked: function ($tr, $input, value) { + $tr.toggleClass("checked", value); + if ($input) $input.prop("checked", value); }, - moveFiles: function(){ - var thread_id = this.$("[name=thread]").val() + moveFiles: function () { + var thread_id = this.$("[name=thread]").val(); // if (!thread_id) return alert("Please choose a thread") - var file_ids = toArray(this.el.querySelectorAll("[name=file_id]:checked")).map(input => input.value) - console.log("thread:", thread_id) - console.log("files:", file_ids) - var promises = file_ids.map(file_id => { + var file_ids = toArray( + this.el.querySelectorAll("[name=file_id]:checked"), + ).map((input) => input.value); + console.log("thread:", thread_id); + console.log("files:", file_ids); + var promises = file_ids.map((file_id) => { return new Promise((resolve, reject) => { $.ajax({ method: "GET", - url: '/api/file/' + file_id + '/move/' + thread_id, + url: "/api/file/" + file_id + "/move/" + thread_id, headers: { "csrf-token": $("[name=_csrf]").attr("value") }, dataType: "json", - success: function(data){ - console.log('moved', file_id) - resolve(data) + success: function (data) { + console.log("moved", file_id); + resolve(data); }, - error: function(){ - console.log('error moving', file_id) - reject() - } - }) + error: function () { + console.log("error moving", file_id); + reject(); + }, + }); + }); + }); + Promise.all(promises) + .then(() => { + window.location.href = "/details/" + thread_id; }) - }) - Promise.all(promises).then( () => { - window.location.href = '/details/' + thread_id - }).catch(() =>{ - console.error('whaaaaa') - alert('there was a problem moving the files...') - }) + .catch(() => { + console.error("whaaaaa"); + alert("there was a problem moving the files..."); + }); }, - deleteThread: function(e){ - var data = this.options.parent.data - var id = data.thread.id - var comment_count = (data.comments || []).length - var file_count = (data.files || []).length - var msg = "Are you sure you want to delete this thread?\n\n#" + id + ' "' + sanitizeHTML(data.thread.title) + '"' - msg += " + " + comment_count + " comment" + courtesy_s(comment_count) - if ( file_count) msg += " + " + file_count + " file" + courtesy_s(file_count) - var should_remove = confirm(msg) + deleteThread: function (e) { + var data = this.options.parent.data; + var id = data.thread.id; + var comment_count = (data.comments || []).length; + var file_count = (data.files || []).length; + var msg = + "Are you sure you want to delete this thread?\n\n#" + + id + + ' "' + + sanitizeHTML(data.thread.title) + + '"'; + msg += " + " + comment_count + " comment" + courtesy_s(comment_count); + if (file_count) + msg += " + " + file_count + " file" + courtesy_s(file_count); + var should_remove = confirm(msg); if (should_remove) { $.ajax({ method: "DELETE", @@ -324,25 +359,30 @@ var ThreadSettingsForm = FormView.extend({ headers: { "csrf-token": $("[name=_csrf]").attr("value") }, data: JSON.stringify({ csrf: csrf() }), dataType: "json", - success: function(){ - window.location.href = "/" - } - }) + success: function () { + window.location.href = "/"; + }, + }); } }, - deleteFile: function(e){ - e.preventDefault() - e.stopPropagation() - var $el = $(e.currentTarget) - var $parent = $el.closest('.file') - var file_id = $el.data('id') - if (! file_id) return - var data = this.options.parent.data - var file = data.files.find(f => f.id === file_id) - if (! file) return - var msg = "Are you sure you want to delete this file?\n\n#" + file_id + ' "' + sanitizeHTML(file.filename) + '"' - var should_remove = confirm(msg) + deleteFile: function (e) { + e.preventDefault(); + e.stopPropagation(); + var $el = $(e.currentTarget); + var $parent = $el.closest(".file"); + var file_id = $el.data("id"); + if (!file_id) return; + var data = this.options.parent.data; + var file = data.files.find((f) => f.id === file_id); + if (!file) return; + var msg = + "Are you sure you want to delete this file?\n\n#" + + file_id + + ' "' + + sanitizeHTML(file.filename) + + '"'; + var should_remove = confirm(msg); if (should_remove) { $.ajax({ method: "DELETE", @@ -350,12 +390,11 @@ var ThreadSettingsForm = FormView.extend({ headers: { "csrf-token": $("[name=_csrf]").attr("value") }, data: JSON.stringify({ csrf: csrf() }), dataType: "json", - success: function(data){ - console.log(data) - $parent.remove() - } - }) + success: function (data) { + console.log(data); + $parent.remove(); + }, + }); } }, - -}) +}); diff --git a/public/assets/js/lib/views/index/hootbox.js b/public/assets/js/lib/views/index/hootbox.js index c874e74..a5d2270 100644 --- a/public/assets/js/lib/views/index/hootbox.js +++ b/public/assets/js/lib/views/index/hootbox.js @@ -29,7 +29,6 @@ var HootBox = FormView.extend({ }, parse: function (comment) { - console.log(comment); var t = this.template .replace(/{{image}}/g, profile_image(comment.username)) .replace(/{{username}}/g, comment.username) diff --git a/public/assets/js/lib/views/mail/mailbox.js b/public/assets/js/lib/views/mail/mailbox.js index c48d948..cae87f0 100644 --- a/public/assets/js/lib/views/mail/mailbox.js +++ b/public/assets/js/lib/views/mail/mailbox.js @@ -2,91 +2,95 @@ var MailboxView = View.extend({ el: "#messages", events: { - 'click .discard_link': 'discard', + "click .discard_link": "discard", }, action: "/api/mailbox/", - initialize: function(){ - this.__super__.initialize.call(this) - this.template = this.$(".template").html() - this.boxlist = new BoxList () + initialize: function () { + this.__super__.initialize.call(this); + this.template = this.$(".template").html(); + this.boxlist = new BoxList(); + this.message = new MessageView(); }, - load: function(name){ - name = sanitizeHTML(name) || "inbox" - $("h1").html(name) - var query = window.location.search.substr(1) - $.get(this.action + name, query, this.populate.bind(this)) + load: function (name) { + name = sanitizeHTML(name) || "inbox"; + $("h1").html(name); + var query = window.location.search.substr(1); + $.get(this.action + name, query, this.populate.bind(this)); }, - populate: function(data){ + populate: function (data) { if (data.boxes) { - this.boxlist.load(data.boxes) + this.boxlist.load(data.boxes); - var user = data.user - var max = data.messages.length-1 + var user = data.user; + var max = data.messages.length - 1; if (data.messages.length) { - var limit = data.query.limit || 50 - var offset = data.query.offset + data.messages.length + var limit = data.query.limit || 50; + var offset = data.query.offset + data.messages.length; if (limit > data.messages.length) { - $(".next_page").hide() + $(".next_page").hide(); + } else { + var query = { limit, offset }; + $(".next_page a").attr("href", "?" + querystring(query)); } - else { - var query = { limit, offset } - $(".next_page a").attr("href", "?" + querystring(query)) - } - } - else { - $("#no_messages").show() - $(".next_page").hide() + } else { + $("#no_messages").show(); + $(".next_page").hide(); } - data.messages.forEach(function(message, i){ - var $row = this.parse(message, user) - if (i === 0) $row.addClass("first") - if (i === max) $row.addClass("last") - this.$el.append($row) - }.bind(this)) + data.messages.forEach( + function (message, i) { + var $row = this.parse(message, user); + if (i === 0) $row.addClass("first"); + if (i === max) $row.addClass("last"); + this.$el.append($row); + }.bind(this), + ); } - $("body").removeClass('loading') + $("body").removeClass("loading"); }, - parse: function(message, user){ - var datetime = verbose_date(message.date) - var size = hush_size(message.size) - var id = message.id + parse: function (message, user) { + var datetime = verbose_date(message.date); + var size = hush_size(message.size); + var id = message.id; - var is_sender = message.sender === user.username + var is_sender = message.sender === user.username; var t = this.template - .replace(/{{id}}/g, message.id) - .replace(/{{to}}/g, is_sender ? "to " : "") - .replace(/{{unread}}/g, message.unread ? "unread" : "") - .replace(/{{username}}/g, is_sender ? message.recipient : message.sender) - .replace(/{{subject}}/g, message.subject) - .replace(/{{date}}/g, datetime[0]) - .replace(/{{time}}/g, datetime[1]) - .replace(/{{date_class}}/g, carbon_date(message.lastmodified) ) - .replace(/{{size}}/g, size[1] ) - .replace(/{{size_class}}/g, size[0] ) - var $t = $(t) + .replace(/{{id}}/g, message.id) + .replace(/{{to}}/g, is_sender ? "to " : "") + .replace(/{{unread}}/g, message.unread ? "unread" : "") + .replace(/{{username}}/g, is_sender ? message.recipient : message.sender) + .replace(/{{subject}}/g, message.subject) + .replace(/{{date}}/g, datetime[0]) + .replace(/{{time}}/g, datetime[1]) + .replace(/{{date_class}}/g, carbon_date(message.lastmodified)) + .replace(/{{size}}/g, size[1]) + .replace(/{{size_class}}/g, size[0]); + var $t = $(t); if (is_sender) { - $t.find('.reply_link').remove() + $t.find(".reply_link").remove(); } - return $t + return $t; }, - discard: function(e){ - var id = $(e.target).data('id') - var ok = confirm("Really delete this message?") - if (! ok) return + discard: function (e) { + var id = $(e.target).data("id"); + var ok = confirm("Really delete this message?"); + if (!ok) return; $.ajax({ - method: 'delete', - url: '/api/message/' + id, + method: "delete", + url: "/api/message/" + id, headers: { "csrf-token": csrf() }, data: { _csrf: csrf() }, - success: function(){ window.location.reload() }, - error: function(){ window.location.reload() }, - }) + success: function () { + window.location.reload(); + }, + error: function () { + window.location.reload(); + }, + }); }, - -}) +}); diff --git a/public/assets/js/lib/views/mail/message.js b/public/assets/js/lib/views/mail/message.js index 6fa3d78..6ea2274 100644 --- a/public/assets/js/lib/views/mail/message.js +++ b/public/assets/js/lib/views/mail/message.js @@ -1,67 +1,70 @@ var MessageView = View.extend({ - el: "#message", events: { - 'click .discard_link': 'discard', + "click .discard_link": "discard", }, - action: '/api/message/', + action: "/api/message/", - initialize: function(){ - this.template = this.$(".template").html() + initialize: function () { + this.template = this.$(".template").html(); + this.$el.empty().hide(); }, - load: function(name){ - name = sanitizeHTML(name) || "inbox" + load: function (name) { + name = sanitizeHTML(name) || "inbox"; $.ajax({ url: this.action + name, - method: 'get', + method: "get", success: this.populate.bind(this), - error: app.router.error404 - }) + error: app.router.error404, + }); }, - populate: function(data){ - this.parse(data) - $("body").removeClass('loading') + populate: function (data) { + this.parse(data); + $("body").removeClass("loading"); }, - parse: function(data){ - var message = data.message -// var user = data.user + parse: function (data) { + var message = data.message; + // var user = data.user - $("h1").html(message.subject) - var datetime = verbose_date(message.date) - var id = message.id - var is_sender = message.sender === auth.user.username + $("h1").html(message.subject); + var datetime = verbose_date(message.date); + var id = message.id; + var is_sender = message.sender === auth.user.username; var t = this.template - .replace(/{{id}}/g, message.id) - .replace(/{{sender}}/g, message.sender) - .replace(/{{avatar}}/g, profile_image(message.sender)) - .replace(/{{subject}}/g, message.subject) - .replace(/{{date}}/g, datetime[0]) - .replace(/{{time}}/g, datetime[1]) - .replace(/{{body}}/g, tidy_urls(message.body) ) - var $t = $(t) + .replace(/{{id}}/g, message.id) + .replace(/{{sender}}/g, message.sender) + .replace(/{{avatar}}/g, profile_image(message.sender)) + .replace(/{{subject}}/g, message.subject) + .replace(/{{date}}/g, datetime[0]) + .replace(/{{time}}/g, datetime[1]) + .replace(/{{body}}/g, tidy_urls(message.body)); + var $t = $(t); if (is_sender) { - $t.find('.reply_link').remove() + $t.find(".reply_link").remove(); } - this.$el.empty().append($t) + this.$el.empty().append($t).show(); }, - discard: function(e){ - var id = $(e.target).data('id') - var ok = confirm("Really delete this message?") - if (! ok) return + discard: function (e) { + var id = $(e.target).data("id"); + var ok = confirm("Really delete this message?"); + if (!ok) return; $.ajax({ - method: 'delete', - url: '/api/message/' + id, + method: "delete", + url: "/api/message/" + id, headers: { "csrf-token": csrf() }, data: { _csrf: csrf() }, - success: function(){ window.location.href = "/mail" }, - error: function(){ window.location.href = "/mail" }, - }) + success: function () { + window.location.href = "/mail"; + }, + error: function () { + window.location.href = "/mail"; + }, + }); }, - -}) +}); diff --git a/public/assets/js/util/format.js b/public/assets/js/util/format.js index 8920a38..a594bdd 100644 --- a/public/assets/js/util/format.js +++ b/public/assets/js/util/format.js @@ -20,7 +20,8 @@ function csrf() { function bold_terms(s, terms) { s = sanitizeHTML(s); terms.forEach((term) => { - s = s.replace(new RegExp(term, "ig"), "<b>" + term + "</b>"); + const sanitized_term = term.replace(/[^a-zA-Z0-9]/g, ""); + s = s.replace(new RegExp(`(${sanitized_term}\\w*)`, "ig"), "<b>$1</b>"); }); return s; } diff --git a/views/pages/mailbox.ejs b/views/pages/mailbox.ejs index 756cb21..b7f55c8 100644 --- a/views/pages/mailbox.ejs +++ b/views/pages/mailbox.ejs @@ -64,4 +64,6 @@ </div> </div> +<% include ../partials/message %> + <% include ../partials/footer %> diff --git a/views/pages/message.ejs b/views/pages/message.ejs index 93987b4..8a8868f 100644 --- a/views/pages/message.ejs +++ b/views/pages/message.ejs @@ -5,25 +5,6 @@ <a href='/mail/'>Inbox</a> </div> -<div class="bluebox" id="message"> - <script class="template" type="text/html"> - <a href="/profile/{{sender}}" class="av" style="background-image:url({{avatar}});"></a> - <span class="subject">{{subject}}</span> - <span class="sender"> - sent by - <a href="/profile/{{sender}}">{{sender}}</a> - on {{date}} {{time}} - <span class='reply_link'> - · - <a href="/mail/reply/{{id}}">reply</a> - </span> - · - <a href='#' class='discard_link'>discard</a> - </span> - <div class="body"> - <span class="contents">{{body}}</span> - </div> - </script> -</div> +<% include ../partials/message %> <% include ../partials/footer %> diff --git a/views/partials/message.ejs b/views/partials/message.ejs new file mode 100644 index 0000000..b48b7b4 --- /dev/null +++ b/views/partials/message.ejs @@ -0,0 +1,20 @@ +<div class="bluebox" id="message"> + <script class="template" type="text/html"> + <a href="/profile/{{sender}}" class="av" style="background-image:url({{avatar}});"></a> + <span class="subject">{{subject}}</span> + <span class="sender"> + sent by + <a href="/profile/{{sender}}">{{sender}}</a> + on {{date}} {{time}} + <span class='reply_link'> + · + <a href="/mail/reply/{{id}}">reply</a> + </span> + · + <a href='#' class='discard_link'>discard</a> + </span> + <div class="body"> + <span class="contents">{{body}}</span> + </div> + </script> +</div> @@ -144,11 +144,6 @@ ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansicolors@~0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.2.1.tgz#be089599097b74a5c9c4a84a0cdbcdb62bd87aef" - integrity sha512-tOIuy1/SK/dr94ZA0ckDohKXNeBNqZ4us6PjMVLs5h1w2GBB6uPtOknp2+VF4F/zcy9LI70W+Z+pE2Soajky1w== - anymatch@~3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" @@ -215,6 +210,11 @@ async@^3.2.0, async@~3.2.0: resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== +aws-ssl-profiles@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz#157dd77e9f19b1d123678e93f120e6f193022641" + integrity sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g== + axios@^0.21.0: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" @@ -242,11 +242,6 @@ bluebird@^3.7.2: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -bn.js@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-2.0.0.tgz#825c4107f7fa789378ee1b0d86be0503d7ac743b" - integrity sha512-NmOLApC80+n+P28y06yHgwGlOCkq/X4jRh5s590959FZXSrM+I/61h0xxuIaYsg0mD44mEAZYG/rnclWuRoz+A== - bodec@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/bodec/-/bodec-0.1.0.tgz#bc851555430f23c9f7650a75ef64c6a94c3418cc" @@ -352,14 +347,6 @@ call-bind@^1.0.0: function-bind "^1.1.1" get-intrinsic "^1.0.2" -cardinal@0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/cardinal/-/cardinal-0.4.4.tgz#ca5bb68a5b511b90fe93b9acea49bdee5c32bfe2" - integrity sha512-3MxV0o9wOpQcobrcSrRpaSxlYkohCcZu0ytOjJUww/Yo/223q4Ecloo7odT+M0SI5kPgb1JhvSaF4EEuVXOLAQ== - dependencies: - ansicolors "~0.2.1" - redeyed "~0.4.0" - chalk@3.0.0, chalk@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" @@ -604,6 +591,11 @@ denque@^1.4.1: resolved "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz" integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ== +denque@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== + depd@1.1.1, depd@~1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz" @@ -642,11 +634,6 @@ dotenv@^1.2.0: resolved "https://registry.npmjs.org/dotenv/-/dotenv-1.2.0.tgz" integrity sha1-fNc+FuB/BXyAchR6W8OoZ38KtcY= -double-ended-queue@2.0.0-0: - version "2.0.0-0" - resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.0.0-0.tgz#7847fda1c00fb722245aff83643a4887670efd2c" - integrity sha512-t5ouWOpItmHrm0J0+bX/cFrIjBFWnJkk5LbIJq6bbU/M4aLX2c3LrM4QYsBptwvlPe3WzdpQefQ0v1pe/A5wjg== - ee-first@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" @@ -725,11 +712,6 @@ esprima@^4.0.0, esprima@^4.0.1: resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esprima@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.0.4.tgz#9f557e08fc3b4d26ece9dd34f8fbf476b62585ad" - integrity sha512-rp5dMKN8zEs9dfi9g0X1ClLmV//WRyk/R15mppFNICIFRG5P92VP7Z04p8pk++gABo9W2tY+kHyu6P1mEHgmTA== - estraverse@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" @@ -945,6 +927,13 @@ function-bind@^1.1.1: resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +generate-function@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" + integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ== + dependencies: + is-property "^1.0.2" + get-intrinsic@^1.0.2: version "1.1.3" resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz" @@ -1114,6 +1103,13 @@ iconv-lite@0.4.24, iconv-lite@^0.4.4: dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + inflection@^1.12.0: version "1.13.4" resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.13.4.tgz#65aa696c4e2da6225b148d7a154c449366633a32" @@ -1193,6 +1189,11 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-property@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + integrity sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g== + is-stream@^1.0.1: version "1.1.0" resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz" @@ -1318,10 +1319,10 @@ log-driver@^1.2.7: resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.7.tgz#63b95021f0702fedfa2c9bb0a24e7797d71871d8" integrity sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg== -lru-cache@2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.5.0.tgz#d82388ae9c960becbea0c73bb9eb79b6c6ce9aeb" - integrity sha512-dVmQmXPBlTgFw77hm60ud//l2bCuDKkqC2on1EBoM7s9Urm9IQDrnujwZ93NFnAq0dVZ0HBXTS7PwEG+YE7+EQ== +long@^5.2.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" + integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== lru-cache@^5.1.1: version "5.1.1" @@ -1337,6 +1338,16 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-cache@^7.14.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + +lru.min@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/lru.min/-/lru.min-1.1.2.tgz#01ce1d72cc50c7faf8bd1f809ebf05d4331021eb" + integrity sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg== + media-typer@0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" @@ -1479,23 +1490,27 @@ mute-stream@~0.0.4: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -mysql2@^0.15.8: - version "0.15.8" - resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-0.15.8.tgz#f16650b6f7c5bb568b34511e21bafe36d5a89ef1" - integrity sha512-3x5o6C20bfwJYPSoT74MOoad7/chJoq4qXHDL5VAuRBBrIyErovLoj04Dz/5EY9X2kTxWSGNiTegtxpROTd2YQ== +mysql2@^3.14.3: + version "3.14.3" + resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.14.3.tgz#52aa6266b416d8b629bf398ba2be9840492b9b08" + integrity sha512-fD6MLV8XJ1KiNFIF0bS7Msl8eZyhlTDCDl75ajU5SJtpdx9ZPEACulJcqJWr1Y8OYyxsFc4j3+nflpmhxCU5aQ== dependencies: - bn.js "2.0.0" - cardinal "0.4.4" - double-ended-queue "2.0.0-0" - named-placeholders "0.1.3" - readable-stream "1.0.33" + aws-ssl-profiles "^1.1.1" + denque "^2.1.0" + generate-function "^2.3.1" + iconv-lite "^0.6.3" + long "^5.2.1" + lru.min "^1.0.0" + named-placeholders "^1.1.3" + seq-queue "^0.0.5" + sqlstring "^2.3.2" -named-placeholders@0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-0.1.3.tgz#353776ee259ad105227e13852eef4215ac631e84" - integrity sha512-Mt79RtxZ6MYTIEemPGv/YDKpbuavcAyGHb0r37xB2mnE5jej3uBzc4+nzOeoZ4nZiii1M32URKt9IjkSTZAmTA== +named-placeholders@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-1.1.3.tgz#df595799a36654da55dda6152ba7a137ad1d9351" + integrity sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w== dependencies: - lru-cache "2.5.0" + lru-cache "^7.14.1" needle@2.4.0: version "2.4.0" @@ -1871,16 +1886,6 @@ read@^1.0.4: dependencies: mute-stream "~0.0.4" -readable-stream@1.0.33: - version "1.0.33" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.33.tgz#3a360dd66c1b1d7fd4705389860eda1d0f61126c" - integrity sha512-72KxhcKi8bAvHP/cyyWSP+ODS5ef0DIRs0OzrhGXw31q41f19aoELCbvd42FjhpyEDxQMRiiC5rq9rfE5PzTqg== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - readable-stream@1.1.x: version "1.1.14" resolved "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz" @@ -1918,13 +1923,6 @@ rechoir@^0.8.0: dependencies: resolve "^1.20.0" -redeyed@~0.4.0: - version "0.4.4" - resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-0.4.4.tgz#37e990a6f2b21b2a11c2e6a48fd4135698cba97f" - integrity sha512-pnk1vsaNLu1UAAClKsImKz9HjBvg9i8cbRqTRzJbiCjGF0fZSMqpdcA5W3juO3c4etFvTrabECkq9wjC45ZyxA== - dependencies: - esprima "~1.0.4" - redis-commands@^1.5.0: version "1.5.0" resolved "https://registry.npmjs.org/redis-commands/-/redis-commands-1.5.0.tgz" @@ -2025,7 +2023,7 @@ safe-buffer@5.2.1, safe-buffer@^5.2.1: resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -"safer-buffer@>= 2.1.2 < 3": +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -2081,6 +2079,11 @@ send@0.18.0: range-parser "~1.2.1" statuses "2.0.1" +seq-queue@^0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e" + integrity sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q== + serve-favicon@^2.5.0: version "2.5.0" resolved "https://registry.npmjs.org/serve-favicon/-/serve-favicon-2.5.0.tgz" @@ -2223,6 +2226,11 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== +sqlstring@^2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.3.tgz#2ddc21f03bce2c387ed60680e739922c65751d0c" + integrity sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg== + statuses@2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" |
