diff --git a/.eslintrc b/.eslintrc index 08d18509..0b9dca5a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -10,14 +10,14 @@ }, "rules": { "no-console": 0, - "indent": [ - 2, - 2 - ], "linebreak-style": [ 2, "unix" ], + "indent": [ + 2, + 2 + ], "semi": [ 2, "always" diff --git a/.sailsrc b/.sailsrc index fa89f5e1..22bdb33c 100644 --- a/.sailsrc +++ b/.sailsrc @@ -1,5 +1,8 @@ { "generators": { "modules": {} + }, + "paths": { + "views": "./assets/views" } } \ No newline at end of file diff --git a/README.md b/README.md index 7e27ff52..6ffa326e 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,14 @@ ## Setup -1. Make sure you have a recent version of Node and NPM installed (NPM is usually bundled with Node these days). -1. Clone the repository `git clone https://github.com/yamanickill/reddit-flair-apps.git` -1. Navigate into the directory `reddit-flair-apps` +1. Make sure you have Node (>=4.0.0), and MongoDB installed +1. Clone the repository `git clone https://github.com/YaManicKill/flairhq.git` +1. Navigate into the directory `flairhq` 1. Run `npm install` to install the dependencies -1. Set up your MongoDB settings `config/local.js` -1. Start your MongoDB from the command line with `sudo mongod` +1. Copy config/local.example.js to config/local.js +1. Create a reddit app on [https://www.reddit.com/prefs/apps](https://www.reddit.com/prefs/apps) +1. Copy the id and secret to config/local.js +1. Use something like https://github.com/xoru/easy-oauth to get a refresh token for a moderator on the subs +1. Start your MongoDB 1. Start sails with `npm start` 1. Open `http://localhost:1337` in your browser diff --git a/api/.eslintrc b/api/.eslintrc index e60f82fd..ed5f0fe3 100644 --- a/api/.eslintrc +++ b/api/.eslintrc @@ -5,16 +5,20 @@ "globals": { "Reference": true, "User": true, + "Users": true, "Ban": true, "Reddit": true, "Event": true, "sails": true, + "Flair": true, "Flairs": true, "Usernotes": true, "References": true, "Sessions": true, "Game": true, "Application": true, - "ModNote": true + "ModNote": true, + "Modmail": true, + "Modmails": true } } \ No newline at end of file diff --git a/api/controllers/AuthController.js b/api/controllers/AuthController.js index ac824369..d0578780 100644 --- a/api/controllers/AuthController.js +++ b/api/controllers/AuthController.js @@ -21,90 +21,72 @@ module.exports = { }, reddit: function (req, res) { - req.session.state = crypto.randomBytes(32).toString('hex'); - if (req.query.url) { - req.session.redirectUrl = req.query.url; + /* Pass the redirect info as JSON with the OAuth state. This behavior is more intuitive than storing it in the session, because otherwise the user + * might fail to complete the login and then be confused when they get redirected somewhere unexpected the next time they visit the site. */ + var login_info = {type: req.query.loginType, redirect: req.query.redirect || '/', validation: crypto.randomBytes(32).toString('hex')}; + req.session.validation = login_info.validation; + var auth_data = {state: JSON.stringify(login_info)}; + if (req.query.loginType === 'mod') { + auth_data.duration = 'permanent'; //Mods have a permanent access token. Scope is not specified here, so the scope from config/express.js is used. + } else { + auth_data.scope = 'identity'; //Regular users only need a temporary access token with reduced scope. } - passport.authenticate('reddit', { - state: req.session.state, - duration: 'permanent', - failureRedirect: '/login', - scope: 'identity' - })(req, res); - }, - - modAuth: function (req, res) { - req.session.state = crypto.randomBytes(32).toString('hex') + '_modlogin'; //The bit at the end prevents an infinite loop, see below - if (req.query.url) { - req.session.redirectUrl = req.query.url; - } - passport.authenticate('reddit', { - state: req.session.state, - duration: 'permanent', - failureRedirect: '/login' - })(req, res); + passport.authenticate('reddit', auth_data)(req, res); }, callback: function (req, res) { - passport.authenticate('reddit', - { - state: req.session.state, - duration: 'permanent', - failureRedirect: '/login' - }, - function (err, user) { - if (req.query.state !== req.session.state) { - console.log("Warning: A user was redirected to the reddit callback, but does not have a valid session state."); - console.log("The code in the request belongs to /u/" + user.name + '.'); - return res.forbidden(); - } - var url = req.session.redirectUrl ? req.session.redirectUrl : '/'; - req.session.redirectUrl = ""; - req.logIn(user, function (err) { - if (err) { - console.log("Failed login: " + err); - return res.forbidden(); + passport.authenticate('reddit', async function (err, user) { + try { + if (err) { + if (err === 'banned') { + return res.view(403, {error: 'You have been banned from FlairHQ'}); } - - Reddit.checkModeratorStatus(sails.config.reddit.adminRefreshToken, user.name, 'pokemontrades').then(function (modStatus) { - if (modStatus) { //User is a mod, set isMod to true - user.isMod = true; - user.save(function (err) { - if (err) { - console.log('Failed to give /u/' + user.name + ' moderator status'); - return res.view(500, {error: "You appear to be a mod, but you weren't given moderator status for some reason.\nTry logging in again."}); - } - /* Redirect to the mod authentication page. If the state ends in '_modlogin', the user was just there, so get rid of the _modlogin flag - * instead of redirecting there again. If a mod ends up on a different page while they still have the _modlogin flag, they have not - * successfully authenticated, so they will get redirected to /auth/modauth. */ - if (req.session.state.substr(-9) === '_modlogin') { - req.session.state = req.session.state.slice(0, -9); - return res.redirect(url); - } - return res.redirect('/auth/modauth'); - }); + return res.view(403, {error: 'Sorry, something went wrong. Try logging in again.'}); + } + var login_info; + try { + login_info = JSON.parse(req.query.state); + } catch (err) { + console.log('Error with parsing /u/' + user.name + '\'s session state'); + return res.serverError(err); + } + if (login_info.validation !== req.session.validation) { + console.log("Failed login for /u/" + user.name + ": invalid session state"); + return res.view(403, {error: 'You have an invalid session state. (Try logging in again.)'}); + } + let finishLogin = function () { + req.logIn(user, function (err) { + if (err) { + sails.log.error('Failed login: ' + err); + return res.forbidden(err); } - - else if (user.isMod) { // User is not a mod, but had isMod set for some reason (e.g. maybe the user used to be a mod). Set isMod to false. - user.isMod = false; - user.save(function (err) { - if (err) { - console.log('Failed to demote user /u/' + user.name + 'from moderator status'); - return res.view(500, {error: err}); - } - return res.redirect(url); - }); - } else { // Regular user - return res.redirect(url); + var url = decodeURIComponent(login_info.redirect); + // Don't redirect to other callback urls (this may cause infinite loops) or to absolute url paths (which might lead to other sites). + if (url.indexOf('/auth/reddit/callback') === 0 || /^(?:[a-z]+:)?\/\//i.test(url)) { + url = '/'; } - }, function (err) { - console.log('Failed to check whether /u/' + user.name + ' is a moderator.'); - console.log(err); + req.session.validation = ''; return res.redirect(url); }); - - }); - })(req, res); + }; + let modStatus = await Reddit.checkModeratorStatus(sails.config.reddit.adminRefreshToken, user.name, 'pokemontrades'); + if (modStatus) { //User is a mod, set isMod to true + User.update(user.name, {isMod: true}).exec(function () { + /* Redirect to the mod authentication page, or to the desired url if this was mod authentication.*/ + if (login_info.type !== 'mod') { + return res.redirect('/auth/reddit?loginType=mod' + (login_info.redirect ? '&redirect=' + encodeURIComponent(login_info.redirect) : '')); + } + return finishLogin(); + }); + } + else if (user.isMod) { // User is not a mod, but had isMod set for some reason (e.g. maybe the user used to be a mod). Set isMod to false. + User.update(user.name, {isMod: false}).exec(finishLogin); + } else { // Regular user + return finishLogin(); + } + } catch (err) { + return res.serverError(err); + } + })(req, res); } - }; diff --git a/api/controllers/FlairController.js b/api/controllers/FlairController.js index 1cfdaa8f..c96dce45 100644 --- a/api/controllers/FlairController.js +++ b/api/controllers/FlairController.js @@ -5,143 +5,83 @@ var _ = require("lodash"); module.exports = { - apply: function (req, res) { - var appData = { - user: req.user.name, - flair: req.allParams().flair, - sub: req.allParams().sub - }; - - Application.find(appData).exec(function (err, app) { - if (err) { - return res.serverError(err); + apply: async function (req, res) { + try { + var allFlairs = await Flair.find(); + var userRefs = await Reference.find({user: req.user.name}); + var appData = {user: req.user.name, flair: req.allParams().flair, sub: req.allParams().sub}; + if (await Application.findOne(appData)) { + return res.status(400).json({error: 'You have already applied for that flair'}); } - if (app.length > 0) { - return res.badRequest("Application already exists"); + var flairIndex = allFlairs.map(function (flairObj) { + return flairObj.name; + }).indexOf(req.allParams().flair); + if (flairIndex === -1) { + return res.status(400).json({error: 'That flair does not exist'}); } - Application.create(appData).exec(function (err, apps) { - if (err) { - return res.serverError(err); - } - if (apps) { - return res.ok(appData); - } - }); - }); + var applicationFlair = allFlairs[flairIndex]; + if (!Flairs.canUserApply(userRefs, applicationFlair, Flairs.getUserFlairs(req.user, allFlairs))) { + return res.status(400).json({error: 'You do not qualify for that flair'}); + } + return res.ok(await Application.create(appData)); + } catch (err) { + return res.serverError(err); + } }, - denyApp: function (req, res) { - Application.destroy(req.allParams().id).exec(function (err, results) { - if (err) { - return res.serverError(err); + denyApp: async function (req, res) { + try { + var matching_apps = await Application.destroy(req.allParams().id); + var apps = await Flairs.getApps(); + if (!matching_apps.length) { + return res.status(404).json(apps); } - return res.ok(results); - }); + return res.ok(apps); + } catch (err) { + return res.serverError(err); + } }, - approveApp: function (req, res) { - Application.findOne(req.allParams().id).exec(function (err, app) { + approveApp: async function (req, res) { + try { + var app = await Application.findOne(req.allParams().id); if (!app) { - return res.notFound("Application not found."); + return res.status(404).json(await Flairs.getApps()); } - User.findOne({name: app.user}).exec(function (err, user) { - if (err) { - return res.serverError(err); - } - var formatted = Flairs.formattedName(app.flair); - var flair, - css_class; - if (app.sub === "pokemontrades" && user.flair.ptrades) { - flair = user.flair.ptrades.flair_text; - css_class = user.flair.ptrades.flair_css_class; - } else if (app.sub === "svexchange" && user.flair.svex) { - flair = user.flair.svex.flair_text; - css_class = user.flair.svex.flair_css_class; - } - if (app.flair === "involvement" && css_class && css_class.indexOf("1") === -1) { - app.flair = user.flair.ptrades.flair_css_class + "1"; - } else if (app.flair === "involvement") { - app.flair = user.flair.ptrades.flair_css_class; - } else if (css_class && css_class.indexOf("1") > -1) { - app.flair += "1"; - } - if (css_class && css_class.slice(-1) === "2") { - app.flair += "2"; - } - if (css_class && css_class.indexOf(' ') > -1) { - if (app.flair.indexOf('ribbon') > -1) { - css_class = css_class.substr(0, css_class.indexOf(' ')) + " " + app.flair; - } else { - css_class = app.flair + " " + css_class.substr(css_class.indexOf(' ') + 1); - } - } else { - if (app.flair.indexOf('ribbon') > -1 && css_class && css_class.indexOf('ribbon') === -1) { - css_class = css_class + " " + app.flair; - } else if (app.flair.indexOf('ribbon') === -1 && css_class && css_class.indexOf('ribbon') > -1) { - css_class = app.flair + " " + css_class; - } else { - css_class = app.flair; - } - } - Reddit.setFlair(req.user.redToken, user.name, css_class, flair, app.sub).then(function () { - Event.create({ - type: "flairTextChange", - user: req.user.name, - content: "Changed " + user.name + "'s flair to " + css_class - }).exec(function () { - }); - - console.log("/u/" + req.user.name + ": Changed " + user.name + "'s flair to " + css_class); - Reddit.sendPrivateMessage( - refreshToken, - 'FlairHQ Notification', - 'Your application for ' + formatted + ' flair on /r/' + app.sub + ' has been approved.', - user.name).then(undefined, function () { - console.log('Failed to send a confirmation PM to ' + user.name); - } - ); - if (app.sub === 'pokemontrades') { - user.flair.ptrades.flair_css_class = css_class; - } else { - user.flair.svex.flair_css_class = css_class; - } - user.save(function (err) { - if (err) { - console.log(err); - } - }); - Application.destroy({id: req.allParams().id}).exec(function (err, app) { - if (err) { - return res.serverError(err); - } - return res.ok(app); - }); - }, function (err) { - return res.serverError(err); - }); - }); - }); + var user = await User.findOne(app.user); + var shortened = app.sub === 'pokemontrades' ? 'ptrades' : 'svex'; + var relevant_flair = Flairs.makeNewCSSClass(_.get(user, 'flair.' + shortened + '.flair_css_class') || '', app.flair, app.sub); + user.flair[shortened].flair_css_class = relevant_flair; + await Reddit.setUserFlair(req.user.redToken, user.name, relevant_flair, user.flair[shortened].flair_text, app.sub); + var promises = []; + promises.push(user.save()); + promises.push(Event.create({type: "flairTextChange", user: req.user.name,content: "Changed " + user.name + "'s flair to " + relevant_flair})); + var pmContent = 'Your application for ' + Flairs.formattedName(app.flair) + ' flair on /r/' + app.sub + ' has been approved.'; + promises.push(Reddit.sendPrivateMessage(refreshToken, 'FlairHQ Notification', pmContent, user.name)); + promises.push(Application.destroy({id: req.allParams().id})); + await* promises; + sails.log.info("/u/" + req.user.name + ": Changed " + user.name + "'s flair to " + relevant_flair); + return res.ok(await Flairs.getApps()); + } catch (err) { + return res.serverError(err); + } }, - setText: function (req, res) { + setText: async function (req, res) { var flairs; try { flairs = Flairs.flairCheck(req.allParams().ptrades, req.allParams().svex); } catch (e) { return res.status(400).json({error: e}); } - - var appData = { - limit: 1, - sort: "createdAt DESC", - user: req.user.name, - type: "flairTextChange" - }; - - Event.find(appData).exec(function (err, events) { - if (err) { - res.status(500).json({error: "Unknown error"}); - } + try { + var appData = { + limit: 1, + sort: "createdAt DESC", + user: req.user.name, + type: "flairTextChange" + }; + var events = await Event.find(appData); var now = moment(); if (events.length) { var then = moment(events[0].createdAt); @@ -151,32 +91,51 @@ module.exports = { } } - var flagged = []; - - for (var i = 0; i < flairs.fcs.length; i++) { - let fc = flairs.fcs[i]; - if (!Flairs.validFC(fc) && flairs.fcs[i] !== req.user.loggedFriendCodes[i]) { - flagged.push(fc); - } - } - + var blockReport = _.isEqual(flairs.fcs, req.user.loggedFriendCodes.slice(0, flairs.fcs.length)); + + var flagged = _.reject(flairs.fcs, Flairs.validFC); + var ipAddress = req.headers['x-forwarded-for'] || req.ip; + // Get IP matches with banned users + var events_with_ip = await Event.find({content: {contains: ipAddress}, user: {not: req.user.name}}); + var matching_alt_usernames = _.uniq(_.map(events_with_ip, 'user')); + var matching_banned_users = await User.find({name: matching_alt_usernames, banned: true}); + var banned_alts = _.map(matching_banned_users, 'name'); + + // Get friend codes that are similar (have a low edit distance) to banned friend codes + var similar_banned_fcs = _.flatten(await* flairs.fcs.map(Flairs.getSimilarBannedFCs)); + // Get friend codes that are identical to banned users' friend codes + var identical_banned_fcs = _.intersection(flairs.fcs, similar_banned_fcs); + var friend_codes = _.union(flairs.fcs, req.user.loggedFriendCodes); - User.update({name: req.user.name}, {loggedFriendCodes: friend_codes}, function (err) { - if (err) { - console.log("Failed to update /u/" + req.user.name + "'s logged friend codes, for some reason"); - return; - } - }); - var newPFlair = _.get(req, "user.flair.ptrades.flair_css_class") || "default"; var newsvFlair = _.get(req, "user.flair.svex.flair_css_class") || ""; newsvFlair = newsvFlair.replace(/2/, ""); var promises = []; - promises.push(Reddit.setFlair(refreshToken, req.user.name, newPFlair, flairs.ptrades, "PokemonTrades")); - promises.push(Reddit.setFlair(refreshToken, req.user.name, newsvFlair, flairs.svex, "SVExchange")); + promises.push(Reddit.setUserFlair(refreshToken, req.user.name, newPFlair, flairs.ptrades, "PokemonTrades")); + promises.push(Reddit.setUserFlair(refreshToken, req.user.name, newsvFlair, flairs.svex, "SVExchange")); + promises.push(User.update({name: req.user.name}, {loggedFriendCodes: friend_codes})); + + if (!blockReport && (identical_banned_fcs.length || banned_alts.length || flagged.length)) { + var message = 'The user /u/' + req.user.name + ' set the following flairs:\n\n' + flairs.ptrades + '\n\n' + flairs.svex + '\n\n'; + if (identical_banned_fcs.length) { + message += 'This flair contains a banned friend code: ' + identical_banned_fcs + '\n\n'; + } else if (flagged.length && similar_banned_fcs.length) { + message += 'This flair contains a friend code similar to the following banned friend code' + (similar_banned_fcs.length > 1 ? 's: ' : ': ') + + similar_banned_fcs.join(', ') + '\n\n'; + } + if (banned_alts.length) { + message += 'This user may be an alt of the banned user' + (banned_alts.length === 1 ? '' : 's') + ' /u/' + banned_alts.join(', /u/') + '.\n\n'; + } + if (flagged.length) { + message += 'The friend code' + (flagged.length === 1 ? ' ' + flagged + ' is' : 's ' + flagged.join(', ') + ' are') + ' invalid.\n\n'; + var formattedNote = "Invalid friend code" + (flagged.length == 1 ? "" : "s") + ": " + flagged.join(', '); + promises.push(Usernotes.addUsernote(refreshToken, 'FlairHQ', 'pokemontrades', req.user.name, formattedNote, 'spamwarn', '')); + } + message = message.slice(0,-2); + promises.push(Reddit.sendPrivateMessage(refreshToken, "FlairHQ notification", message, "/r/pokemontrades")); + } Promise.all(promises).then(function () { - var ipAddress = req.headers['x-forwarded-for'] || req.ip; Event.create([{ type: "flairTextChange", user: req.user.name, @@ -185,42 +144,29 @@ module.exports = { type: "flairTextChange", user: req.user.name, content: "Changed SVExchange flair text to: " + req.allParams().svex + ". IP: " + ipAddress - }]).exec(function () { - }); - return res.ok(req.user); + }]).exec(function () {}); + return res.ok(); }); - if (flagged.length) { - var message = "The user /u/" + req.user.name + " set a flair containing " + - (flagged.length == 1 ? "an invalid friend code" : flagged.length + " invalid friend codes") + ".\n\n"; - if (req.allParams().ptrades) { - message += "/u/" + req.user.name + " " + req.allParams().ptrades + " (/r/pokemontrades)\n\n"; - } - if (req.allParams().svex) { - message += "/u/" + req.user.name + " " + req.allParams().svex + " (/r/SVExchange)\n\n"; - } - if (flairs.fcs.length > flagged.length) { - message += "The following friend code" + (flagged.length == 1 ? " is" : "s are") + " invalid:\n\n"; - for (i = 0; i < flagged.length; i++) { - message += flagged[i] + "\n\n"; - } - } - Reddit.sendPrivateMessage(refreshToken, "FlairHQ notification", message, "/r/pokemontrades").then(function () { - console.log("Sent a modmail reporting /u/" + req.user.name + "'s invalid friend code(s)."); - }, function () { - console.log("Failed to send a modmail reporting /u/" + req.user.name + "'s invalid friend code(s)."); - }); - var formattedNote = "Invalid friend code" + (flagged.length == 1 ? "" : "s") + ": " + flagged.toString(); - Usernotes.addUsernote(refreshToken, 'FlairHQ', 'pokemontrades', req.user.name, formattedNote, 'spamwarn', '').catch(function () { - console.log('Failed to create a usernote on /u/' + req.user.name); - }); - } - }); + } catch (err) { + return res.serverError(err); + } }, - getApps: function (req, res) { - Application.find().exec(function (err, apps) { - return res.ok(apps); - }); + getApps: async function (req, res) { + try { + return res.ok(await Flairs.getApps()); + } catch (err) { + return res.serverError(err); + } + }, + + refreshClaim: async function (req, res) { + try { + await Flairs.refreshAppClaim(req.allParams(), req.user.name); + return res.ok(); + } catch (err) { + return res.serverError(err); + } } }; diff --git a/api/controllers/HomeController.js b/api/controllers/HomeController.js index b3c1c898..d63d235d 100644 --- a/api/controllers/HomeController.js +++ b/api/controllers/HomeController.js @@ -10,7 +10,7 @@ var _ = require("lodash"); module.exports = { index: async function (req, res) { - res.view(); + res.view({refUser: await Users.get(req.user, req.user.name)}); Reddit.getBothFlairs(sails.config.reddit.adminRefreshToken, req.user.name).then(function (flairs) { if (flairs[0] || flairs[1]) { req.user.flair = {ptrades: flairs[0], svex: flairs[1]}; @@ -31,18 +31,23 @@ module.exports = { }); }, - reference: function(req, res) { - User.findOne({name: req.params.user}).exec(function (err, user){ - if (user) { - res.view(); - } else { - res.view('404', {data: {user: req.params.user, error: "User not found"}}); + reference: async function(req, res) { + try { + return res.view({refUser: await Users.get(req.user, req.params.user)}); + } catch (err) { + if (err.statusCode === 404) { + return res.view('404', {data: {user: req.params.user, error: "User not found"}}); } - }); + return res.serverError(err); + } }, - banlist: function (req, res) { - res.view(); + banlist: async function (req, res) { + try { + return res.view({bannedUsers: await Users.getBannedUsers()}); + } catch (err) { + return res.serverError(err); + } }, banuser: function (req, res) { @@ -57,6 +62,10 @@ module.exports = { res.view(); }, + tools: function (req, res) { + res.view("../tools/tools.ejs"); + }, + version: function(req, res) { res.ok(sails.config.version); } diff --git a/api/controllers/ReferenceController.js b/api/controllers/ReferenceController.js index 03bbbb0f..3efdf79c 100644 --- a/api/controllers/ReferenceController.js +++ b/api/controllers/ReferenceController.js @@ -44,7 +44,6 @@ module.exports = { return res.ok(results); }); }); - }, add: function (req, res) { @@ -60,7 +59,7 @@ module.exports = { return res.serverError(err); } if (ref && (ref.type !== "egg" || req.params.type !== "egg")) { - return res.badRequest(); + return res.status(400).json({err: 'Already added that URL.'}); } Reference.create( { @@ -145,7 +144,7 @@ module.exports = { var query = { user: ref.user2, url: new RegExp(ref.url.substring(ref.url.indexOf("/r/"))), - user2: '/u/' + ref.user + user2: ref.user }; //If a verified reference is deleted, its compelmentary reference is un-verified. Reference.update(query, {verified: false}, function (err) { @@ -195,18 +194,16 @@ module.exports = { }); }, - approve: function (req, res) { - Reference.findOne(req.allParams().id, function (err, ref) { - if (!ref || err) { - return res.notFound(err); + approve: async function (req, res) { + try { + var ref = await Reference.findOne(req.allParams().id); + if (!References.isApprovable(ref)) { + return res.badRequest(); } - References.approve(ref, req.allParams().approve).then(function (result) { - return res.ok(result); - }, function (error) { - console.log(error); - return res.serverError(error); - }); - }); + return res.ok(await References.approve(ref, req.allParams().approve)); + } catch (err) { + return res.serverError(err); + } }, approveAll: function (req, res) { @@ -249,13 +246,12 @@ module.exports = { }); }, - getFlairs: function (req, res) { - Flair.find().exec(function (err, flairs) { - if (err) { - return res.serverError(err); - } - return res.ok(flairs); - }); + getFlairs: async function (req, res) { + try { + return res.ok(await Flairs.getFlairs()); + } catch (err) { + return res.serverError(err); + } } }; diff --git a/api/controllers/SearchController.js b/api/controllers/SearchController.js index f09688e6..689be14d 100644 --- a/api/controllers/SearchController.js +++ b/api/controllers/SearchController.js @@ -1,47 +1,22 @@ -/* global module, Reference, Search, User */ - -module.exports = { - - refView: function(req, res) { - return res.view("search/refs", {searchTerm: decodeURIComponent(req.params.searchterm)}); - }, - - logView: function(req, res) { - return res.view("search/logs", {searchTerm: decodeURIComponent(req.params.searchterm)}); - }, - - refs: function (req, res) { - var params = req.allParams(); - var searchData = { - description: params.keyword - }; - - if (params.user) { - searchData.user = params.user; - } - - if (params.categories) { - searchData.categories = params.categories.split(","); - } - - searchData.skip = params.skip || 0; - - Search.refs(searchData, function (results) { - return res.ok(results); +var searchTypes = require("../../assets/search/types.js"); +var exportObject = {}; + +for (let i = 0; i < searchTypes.length; i++) { + // Let's programmatically add the views, because we can. + let type = searchTypes[i]; + exportObject[type.short + "View"] = function (req, res) { + return res.view("../search/main", { + searchType: type.short, + searchTerm: decodeURIComponent(req.params.searchterm) }); - }, + }; +} - log: function (req, res) { - var params = req.allParams(); - var searchData = { - keyword: params.keyword - }; +for (let i = 0; i < searchTypes.length; i++) { + // And here we will programmatically add the search functions + let type = searchTypes[i]; + exportObject[type.short] = type.controller; +} - searchData.skip = params.skip || 0; - - Search.logs(searchData, function (results) { - return res.ok(results); - }); - } -}; +module.exports = exportObject; \ No newline at end of file diff --git a/api/controllers/UserController.js b/api/controllers/UserController.js index b3da899f..04a73f60 100644 --- a/api/controllers/UserController.js +++ b/api/controllers/UserController.js @@ -81,80 +81,15 @@ module.exports = { }); }, - mine: function (req, res) { - Game.find() - .where({user: req.user.name}) - .exec(function (err, games) { - req.user.games = games; - - var appData = { - user: req.user.name - }; - - Application.find(appData).exec(function (err, app) { - if (err) { - return res.serverError(err); - } - req.user.apps = app; - res.ok(req.user); - }); - }); - }, - - get: function (req, res) { - User.findOne(req.params.name, function (err, user) { - if (!user) { + get: async function (req, res) { + try { + return res.ok(await Users.get(req.user, req.params.name)); + } catch (err) { + if (err.statusCode === 404) { return res.notFound(); } - Game.find() - .where({user: user.name}) - .sort({createdAt: "desc"}) - .exec(function (err, games) { - - Reference.find() - .where({user: user.name}) - .sort({type: "asc", createdAt: "desc"}) - .exec(function (err, references) { - - Comment.find() - .where({user: user.name}) - .sort({createdAt: "desc"}) - .exec(function (err, comments) { - - ModNote.find() - .where({refUser: user.name}) - .sort({createdAt: "desc"}) - .exec(function (err, notes) { - - if (req.user && user.name === req.user.name) { - user.isMod = req.user.isMod; - } - var publicReferences = references; - //Censor confidential/classified info - publicReferences.forEach(function (entry) { - if (!req.user || req.user.name !== req.params.name) { - entry.privatenotes = undefined; - } - if (!req.user || !req.user.isMod) { - entry.approved = undefined; - entry.verified = undefined; - } - }); - if (req.user && req.user.isMod) { - user.modNotes = notes; - } else { - user.loggedFriendCodes = undefined; - } - user.references = publicReferences; - user.games = games; - user.comments = comments; - user.redToken = undefined; - return res.ok(user); - }); - }); - }); - }); - }); + return res.serverError(err); + } }, addNote: function (req, res) { @@ -179,150 +114,155 @@ module.exports = { }); }, - ban: function (req, res) { + ban: async function (req, res) { /* Form parameters: - req.params.username: The user who is being banned (String) - req.params.banNote: The ban reason to go on the mod log (not visible to banned user, 300 characters max) (String) - req.params.banMessage: The note that gets sent with the "you are banned" PM (String) - req.params.banlistEntry: The ban reason to appear on the public banlist (String) - req.params.duration: The number of days that the user will be banned for. (Integer) - req.params.additionalFCs: A list of additional friend codes that should be banned. (Array of Strings) - Ban process: - 1. Ban user from /r/pokemontrades - 2. Ban user from /r/SVExchange - 3. Add "BANNED USER" to user's flair on /r/pokemontrades - 4. Add "BANNED USER" to user's flair on /r/SVExchange - 5. Add user's friend code to /r/pokemontrades AutoModerator config (2 separate lists) - 6. Add user's friend code to /r/SVExchange AutoModerator config (2 separate lists) - 7. Add a usernote for the user on /r/pokemontrades - 8. Add a usernote for the user on /r/SVExchange - 9. Remove all of the user's TSV threads on /r/SVExchange - 10. Add user's info to banlist wiki on /r/pokemontrades - 11. Locally ban user from FlairHQ - */ + req.params.username: The user who is being banned (String) + req.params.banNote: The ban reason to go on the mod log (not visible to banned user, 300 characters max) (String) + req.params.banMessage: The note that gets sent with the "you are banned" PM (String) + req.params.banlistEntry: The ban reason to appear on the public banlist (String) + req.params.duration: The number of days that the user will be banned for. (Integer) + req.params.knownAlt: Known alt of the user for the public banlist (String) + req.params.additionalFCs: A list of additional friend codes that should be banned. (Array of Strings) + Ban process: + 1. Ban user from /r/pokemontrades + 2. Ban user from /r/SVExchange + 3. Add "BANNED USER" to user's flair on /r/pokemontrades + 4. Add "BANNED USER" to user's flair on /r/SVExchange + 5. Add user's friend code to /r/pokemontrades AutoModerator config (2 separate lists) + 6. Add user's friend code to /r/SVExchange AutoModerator config (2 separate lists) + 7. Add a usernote for the user on /r/pokemontrades + 8. Add a usernote for the user on /r/SVExchange + 9. Remove all of the user's TSV threads on /r/SVExchange + 10. Add user's info to banlist wiki on /r/pokemontrades + 11. Locally ban user from FlairHQ + */ - req.params = req.allParams(); + try { + req.params = req.allParams(); - if (typeof req.params.username !== 'string' || !req.params.username.match(/^[A-Za-z0-9_-]{1,20}$/)) { - return res.status(400).json({error: "Invalid username"}); - } + if (typeof req.params.username !== 'string' || !req.params.username.match(/^[A-Za-z0-9_-]{1,20}$/)) { + return res.status(400).json({error: "Invalid username"}); + } - if (typeof req.params.banNote !== 'string') { - return res.status(400).json({error: "Invalid ban note"}); - } - if (req.params.banNote.length > 300) { - return res.status(400).json({error: "Ban note too long"}); - } + if (typeof req.params.banNote !== 'string') { + return res.status(400).json({error: "Invalid ban note"}); + } + if (req.params.banNote.length > 300) { + return res.status(400).json({error: "Ban note too long"}); + } - if (typeof req.params.banMessage !== 'string') { - return res.status(400).json({error: "Invalid ban message"}); - } + if (typeof req.params.banMessage !== 'string') { + return res.status(400).json({error: "Invalid ban message"}); + } - if (typeof req.params.banlistEntry !== 'string') { - return res.status(400).json({error: "Invalid banlist entry"}); - } + if (typeof req.params.banlistEntry !== 'string') { + return res.status(400).json({error: "Invalid banlist entry"}); + } - if (req.params.duration && (typeof req.params.duration !== 'number' || req.params.duration < 0 || req.params.duration > 999 || req.params.duration % 1 !== 0)) { - return res.status(400).json({error: "Invalid duration"}); - } + if (req.params.duration && (typeof req.params.duration !== 'number' || req.params.duration < 0 || req.params.duration > 999 || req.params.duration % 1 !== 0)) { + return res.status(400).json({error: "Invalid duration"}); + } - if (!(req.params.additionalFCs instanceof Array)) { - return res.status(400).json({error: "Invalid friendcode list"}); - } - for (var FC = 0; FC < req.params.additionalFCs.length; FC++) { - if (typeof req.params.additionalFCs[FC] !== 'string' || !req.params.additionalFCs[FC].match(/^(\d{4}-){2}\d{4}$/g)) { + if (req.params.knownAlt && (typeof req.params.knownAlt !== 'string' || !req.params.knownAlt.match(/^[A-Za-z0-9_-]{1,20}$/))) { + return res.status(400).json({error: "Invalid username of alt"}); + } + + if (!(req.params.additionalFCs instanceof Array)) { return res.status(400).json({error: "Invalid friendcode list"}); } - } - console.log("/u/" + req.user.name + ": Started process to ban /u/" + req.params.username); - User.findOne(req.params.username, function (finding_user_error, user) { - Reddit.getBothFlairs(req.user.redToken, req.params.username).then(function (flairs) { - var flair1 = flairs[0]; - var flair2 = flairs[1]; - if (flair1 && flair1.flair_css_class && flair1.flair_text) { - if (flair1.flair_css_class.indexOf(' ') === -1) { - flair1.flair_css_class += ' banned'; - } else { - flair1.flair_css_class = flair1.flair_css_class.substring(0, flair1.flair_css_class.indexOf(' ')) + ' banned'; - } - } else { - flair1 = {flair_css_class: 'default banned'}; - flair1.flair_text = ''; + for (var FC = 0; FC < req.params.additionalFCs.length; FC++) { + if (typeof req.params.additionalFCs[FC] !== 'string' || !req.params.additionalFCs[FC].match(/^(\d{4}-){2}\d{4}$/g)) { + return res.status(400).json({error: "Invalid friendcode list"}); } - if (flair2 && flair2.flair_text) { - if (flair2.flair_css_class) { - flair2.flair_css_class += ' banned'; - } - else { - flair2.flair_css_class = 'banned'; + } + console.log("/u/" + req.user.name + ": Started process to ban /u/" + req.params.username); + var user; + try { + user = await User.findOne(req.params.username); + if (!user) { + if (await Reddit.checkUsernameAvailable(req.params.username)) { + console.log("Ban aborted (user does not exist)"); + return res.status(404).json({error: "That user does not exist."}); } - } else { - flair2 = {flair_css_class: 'banned'}; - flair2.flair_text = ''; } - var logged_fcs; - if (user) { - logged_fcs = user.loggedFriendCodes; + } catch (err) { + console.log(err); + return res.status(500).json(err); + } + var flairs; + try { + flairs = await Reddit.getBothFlairs(req.user.redToken, req.params.username); + } + catch (err) { + // Reddit will return 403 when looking up the flair of a user with a deleted account. + if (err.statusCode !== 403) { + return res.status(err.statusCode).json(err); } - var unique_fcs = _.union( - flair1.flair_text.match(/(\d{4}-){2}\d{4}/g), - flair2.flair_text.match(/(\d{4}-){2}\d{4}/g), - logged_fcs, - req.params.additionalFCs - ); - var igns = flair1.flair_text.substring(flair1.flair_text.indexOf("||") + 3); - var promises = []; + } + var logged_fcs = user ? user.loggedFriendCodes : []; + var unique_fcs = _.union(logged_fcs, req.params.additionalFCs); + var igns; + if (flairs) { + var fc_match = /(\d{4}-){2}\d{4}/g; + unique_fcs = _.union(flairs[0].flair_text.match(fc_match), flairs[1].flair_text.match(fc_match), unique_fcs); + igns = flairs[0].flair_text.substring(flairs[0].flair_text.indexOf('||') + 3); + } else if (user) { + igns = user.flair.ptrades.flair_text.substring(user.flair.ptrades.flair_text.indexOf('||') + 3); + } + var promises = []; + if (flairs) { promises.push(Ban.banFromSub(req.user.redToken, req.params.username, req.params.banMessage, req.params.banNote, 'pokemontrades', req.params.duration)); promises.push(Ban.banFromSub(req.user.redToken, req.params.username, req.params.banMessage, req.params.banNote, 'SVExchange', req.params.duration)); - promises.push(Ban.addUsernote(req.user.redToken, req.user.name, 'pokemontrades', req.params.username, req.params.banNote, req.params.duration)); - promises.push(Ban.addUsernote(req.user.redToken, req.user.name, 'SVExchange', req.params.username, req.params.banNote, req.params.duration)); - if (!req.params.duration) { - promises.push(Ban.giveBannedUserFlair(req.user.redToken, req.params.username, flair1.flair_css_class, flair1.flair_text, 'pokemontrades')); - promises.push(Ban.giveBannedUserFlair(req.user.redToken, req.params.username, flair2.flair_css_class, flair2.flair_text, 'SVExchange')); - promises.push(Ban.updateAutomod(req.user.redToken, req.params.username, 'pokemontrades', unique_fcs)); - promises.push(Ban.updateAutomod(req.user.redToken, req.params.username, 'SVExchange', unique_fcs)); - promises.push(Ban.removeTSVThreads(req.user.redToken, req.params.username)); - promises.push(Ban.updateBanlist(req.user.redToken, req.params.username, req.params.banlistEntry, unique_fcs, igns)); - promises.push(Ban.localBanUser(req.params.username)); + } + if (!req.params.duration) { + if (flairs) { + promises.push(Ban.giveBannedUserFlair(req.user.redToken, req.params.username, flairs[0] && flairs[0].flair_css_class, flairs[0] && flairs[0].flair_text, 'pokemontrades')); + promises.push(Ban.giveBannedUserFlair(req.user.redToken, req.params.username, flairs[0] && flairs[1].flair_css_class, flairs[1] && flairs[1].flair_text, 'SVExchange')); + promises.push(Ban.markTSVThreads(req.user.redToken, req.params.username)); } - Promise.all(promises).then(function () { - res.ok(); - }, function (error) { - console.log(error); - res.status(500).json(error); - }); - Event.create({ - user: req.user.name, - type: "banUser", - content: "Banned /u/" + req.params.username - }).exec(function () { - }); - }, function (err) { - return res.serverError(err); + promises.push(Ban.updateAutomod(req.user.redToken, req.params.username, 'pokemontrades', unique_fcs)); + promises.push(Ban.updateAutomod(req.user.redToken, req.params.username, 'SVExchange', unique_fcs)); + promises.push(Ban.addUsernote(req.user.redToken, req.user.name, 'pokemontrades', req.params.username, req.params.banNote)); + promises.push(Ban.addUsernote(req.user.redToken, req.user.name, 'SVExchange', req.params.username, req.params.banNote)); + promises.push(Ban.updateBanlist(req.user.redToken, req.params.username, req.params.banlistEntry, unique_fcs, igns, req.params.knownAlt)); + promises.push(Ban.localBanUser(req.params.username)); + } + Promise.all(promises).then(function () { + console.log('Process to ban /u/' + req.params.username + ' was completed successfully.'); + res.ok(); + }, function(error) { + console.log(error); + res.status(error.statusCode || 500).json(error); }); - }); + Event.create({ + user: req.user.name, + type: "banUser", + content: "Banned /u/" + req.params.username + }).exec(function () {}); + } catch (err) { + return res.serverError(err); + } }, setLocalBan: function (req, res) { - User.update(req.allParams().username, {banned: req.allParams().ban}).exec(function (err, user) { + User.update(req.allParams().username, {banned: req.allParams().ban}).exec(function (err, users) { if (err) { console.log(err); return res.serverError(err); } - if (!user) { + if (!users.length) { return res.notFound(); } - return res.ok(user[0]); + return res.ok(users[0]); }); }, - bannedUsers: function (req, res) { - User.find({banned: true}).exec(function (err, users) { - if (err) { - return res.serverError(err); - } - return res.ok(users); - }); + bannedUsers: async function (req, res) { + try { + return res.ok(await Users.getBannedUsers()); + } catch (err) { + return res.serverError(err); + } }, clearSession: function (req, res) { diff --git a/api/models/Modmail.js b/api/models/Modmail.js new file mode 100644 index 00000000..53d835bd --- /dev/null +++ b/api/models/Modmail.js @@ -0,0 +1,37 @@ +module.exports = { + + types: { + stringOrNull: function (val) { + return typeof val === 'string' || val === null; + } + }, + + autoPK: false, + + attributes: { + name: { //The fullname ('t4_' + base36id) of the message + columnName: 'id', + type: 'string', + unique: true, + primaryKey: true + }, + subject: 'string', //Subject of the message + body: 'string', //Body of the message + author: 'string', //Username of the message author + subreddit: { //The subreddit that the modmail was sent to + enum: ['pokemontrades', 'SVExchange'] + }, + first_message_name: { //The fullname of the first message in this chain, or null if this is the first message + stringOrNull: true + }, + created_utc: { //The UTC timestamp of when the message was created + type: 'integer' + }, + parent_id: { //The fullname of the parent message, or null if this is the first message + stringOrNull: true + }, + distinguished: { //This will be 'moderator' if the author was a mod, 'admin' if the author was a reddit admin, or null otherwise + enum: ['moderator', 'admin', null] + } + } +}; diff --git a/api/policies/bearerAuth.js b/api/policies/bearerAuth.js deleted file mode 100644 index 44660dd9..00000000 --- a/api/policies/bearerAuth.js +++ /dev/null @@ -1,7 +0,0 @@ -var passport = require('passport'); - -module.exports = function(req, res, next) { - - return passport.authenticate('reddit', { session: false })(req, res, next); - -}; \ No newline at end of file diff --git a/api/policies/passport.js b/api/policies/passport.js index 266f542c..b0329d10 100644 --- a/api/policies/passport.js +++ b/api/policies/passport.js @@ -4,11 +4,21 @@ module.exports = function (req, res, next) { // Initialize Passport passport.initialize()(req, res, function () { // Use the built-in sessions - passport.session()(req, res, function () { - // Make the user available throughout the frontend - res.locals.user = req.user; - - next(); + passport.session()(req, res, async function () { + try { + res.locals.user = req.user; + if (req.user) { + res.locals.user.games = await Game.find({user: req.user.name}); + } + res.locals.query = req.query; + res.locals.flairs = await Flairs.getFlairs(); + if (req.user && req.user.isMod) { + res.locals.flairApps = await Flairs.getApps(); + } + next(); + } catch (err) { + return res.serverError(err); + } }); }); }; \ No newline at end of file diff --git a/api/policies/sessionAuth.js b/api/policies/sessionAuth.js index 127b0279..b4fe2493 100644 --- a/api/policies/sessionAuth.js +++ b/api/policies/sessionAuth.js @@ -8,30 +8,15 @@ * */ module.exports = function(req, res, next) { - - // User is banned, log them out. - if (req.user && req.user.banned) { - req.logout(); - if (req.isSocket) { - return res.forbidden("You have been banned from FAPP"); - } - return res.forbidden("You have been banned from FAPP"); - } - - // User is allowed, proceed to the next policy, - // or if this is the last policy, the controller - if (req.user || (req.isAuthenticated && req.isAuthenticated())) { - //Redirect mods to the modauth page if they only have normal user scope. - if (req.user.isMod && req.session.state.substr(-9) === '_modlogin') { - return res.redirect('/auth/modauth'); + if (req.user) { + if (req.user.banned) { + req.logout(); + return res.view(403, {error: "You have been banned from FlairHQ"}); } return next(); } - - // User is not allowed - // (default res.forbidden() behavior can be overridden in `config/403.js`) if (req.isSocket) { return res.status(403).json({status: 403, redirectTo: "/login"}); } - return res.redirect('/login'); + return res.redirect('/login' + (req.url !== '/' ? '?redirect=' + encodeURIComponent(req.url) : '')); }; diff --git a/api/services/Ban.js b/api/services/Ban.js index cd7de2ae..885041af 100644 --- a/api/services/Ban.js +++ b/api/services/Ban.js @@ -1,3 +1,4 @@ +var _ = require('lodash'); exports.banFromSub = async function (redToken, username, banMessage, banNote, subreddit, duration) { try { await Reddit.banUser(redToken, username, banMessage, banNote, subreddit, duration); @@ -9,9 +10,11 @@ exports.banFromSub = async function (redToken, username, banMessage, banNote, su }; //Give the 'BANNED USER' flair on a subreddit -exports.giveBannedUserFlair = async function (redToken, username, css_class, flair_text, subreddit) { +exports.giveBannedUserFlair = async function (redToken, username, current_css_class, current_flair_text, subreddit) { try { - await Reddit.setFlair(redToken, username, css_class, flair_text, subreddit); + var flair_text = current_flair_text || ''; + var css_class = Flairs.makeNewCSSClass(current_css_class, 'banned', subreddit); + await Reddit.setUserFlair(redToken, username, css_class, flair_text, subreddit); console.log('Changed ' + username + '\'s flair to ' + css_class + ' on /r/' + subreddit); return 'Changed ' + username + '\'s flair to ' + css_class + ' on /r/' + subreddit; } catch (err) { @@ -38,7 +41,10 @@ exports.updateAutomod = async function (redToken, username, subreddit, friend_co var end_delimiter_index = lines[fclist_indices[listno]].lastIndexOf(punctuation[0]); var before_end = lines[fclist_indices[listno]].substring(0, end_delimiter_index); for (var i = 0; i < friend_codes.length; i++) { - before_end += punctuation[1] + friend_codes[i].replace(/-/g, punctuation[2]) + punctuation[3]; + let formatted = friend_codes[i].replace(/-/g, punctuation[2]); + if (lines[fclist_indices[listno]].indexOf(formatted) === -1) { + before_end += punctuation[1] + formatted + punctuation[3]; + } } lines[fclist_indices[listno]] = before_end + lines[fclist_indices[listno]].substring(end_delimiter_index); } @@ -53,31 +59,56 @@ exports.updateAutomod = async function (redToken, username, subreddit, friend_co console.log(output); return output; }; -//Remove the user's TSV threads on /r/SVExchange. -exports.removeTSVThreads = async function (redToken, username) { - var response = await Reddit.searchTSVThreads(redToken, username); - var removeTSVPromises = []; - response.data.children.forEach(function (entry) { - removeTSVPromises.push(Reddit.removePost(redToken, entry.data.id, 'false')); +//Lock and give flair to the user's TSV threads. +exports.markTSVThreads = async function (redToken, username) { + var threads = await Reddit.searchTSVThreads(redToken, username); + var tsv_promises = []; + threads.forEach(function (entry) { + tsv_promises.push(Reddit.lockPost(redToken, entry.data.id)); + tsv_promises.push(Reddit.markNsfw(redToken, entry.data.id)); + tsv_promises.push(Reddit.setLinkFlair(redToken, entry.data.subreddit, entry.data.id, 'banned', '[Banned User] Trainer Shiny Value')); }); - await Promise.all(removeTSVPromises); - var output = 'Removed /u/' + username + '\'s TSV threads (' + response.data.children.length.toString() + ' total)'; + await Promise.all(tsv_promises); + var output = 'Marked and locked /u/' + username + '\'s TSV threads (' + threads.length.toString() + ' total)'; console.log(output); return output; }; //Update the public banlist with the user's information -exports.updateBanlist = async function (redToken, username, banlistEntry, friend_codes, igns) { +exports.updateBanlist = async function (redToken, username, banlistEntry, friend_codes, igns, knownAlt) { + var valid_FCs = friend_codes.filter(Flairs.validFC); + if (valid_FCs.length) { + friend_codes = valid_FCs; + } var current_list = await Reddit.getWikiPage(redToken, 'pokemontrades', 'banlist'); var lines = current_list.replace(/\r/g, '').split("\n"); var start_index = lines.indexOf('[//]:# (BEGIN BANLIST)') + 3; - if (start_index == 2) { - console.log('Error: Could not find start marker in public banlist'); - throw {error: 'Error while parsing public banlist'}; + var end_index = lines.indexOf('[//]:# (END BANLIST)'); + if (start_index === 2 || end_index === -1) { + console.log('Error: Could not find parsing marker in public banlist'); + throw {error: 'Error: Could not find parsing marker in public banlist'}; + } + var updated_content = ''; + for (let i = start_index; i < end_index; i++) { + if (knownAlt && lines[i].match(new RegExp('/u/' + knownAlt)) || _.intersection(lines[i].match(/(\d{4}-){2}\d{4}/g), friend_codes).length) { + // User was an alt account, modify the existing line instead of creating a new one + let blocks = lines[i].split(' | '); + if (blocks.length !== 4) { + break; + } + blocks[0] += ', /u/' + username; + blocks[1] = _.union(blocks[1].match(/(\d{4}-){2}\d{4}/g), friend_codes).join(', '); + blocks[3] = _.compact(_.union(blocks[3].split(', '), [igns])).join(', '); + let new_line = blocks.join(' | '); + updated_content = lines.slice(0, start_index).concat(new_line).concat(lines.slice(start_index, i)).concat(lines.slice(i + 1)).join('\n'); + } + } + if (!updated_content) { + // User was probably not an alt, create a new line + let new_line = ['/u/' + username, friend_codes.join(', '), banlistEntry, igns].join(' | '); + updated_content = lines.slice(0, start_index).concat(new_line).concat(lines.slice(start_index)).join('\n'); } - var line_to_add = '/u/' + username + ' | ' + friend_codes.join(', ') + ' | ' + banlistEntry + ' | ' + igns; - var content = lines.slice(0, start_index).join("\n") + "\n" + line_to_add + "\n" + lines.slice(start_index).join("\n"); try { - await Reddit.editWikiPage(redToken, 'pokemontrades', 'banlist', content, ''); + await Reddit.editWikiPage(redToken, 'pokemontrades', 'banlist', updated_content, ''); } catch (e) { console.log(e); throw {error: 'Failed to update public banlist'}; @@ -85,23 +116,15 @@ exports.updateBanlist = async function (redToken, username, banlistEntry, friend console.log('Added /u/' + username + ' to public banlist'); return 'Added /u/' + username + ' to public banlist'; }; -exports.localBanUser = async function (username) { - User.findOne({name: username}).exec(function (err, user) { - if (!user) { - console.log('/u/' + username + ' was not locally banned because that user does not exist in the FlairHQ database.'); - return '/u/' + username + ' was not locally banned because that user does not exist in the FlairHQ database.'; - } - else { - user.banned = true; - user.save(function (err) { - if (err) { - throw {error: 'Error banning user from local FlairHQ database'}; - } - console.log('Banned /u/' + username + ' from local FlairHQ database'); - return 'Banned /u/' + username + ' from local FlairHQ database'; - }); - } - }); +exports.localBanUser = async function(username) { + try { + let update = await User.update(username, {banned: true}); + console.log('Updated local banlist'); + return update; + } catch (err) { + console.log(err); + throw {error: 'Failed to locally ban /u/' + username}; + } }; exports.addUsernote = function (redToken, modname, subreddit, username, banNote, duration) { var type = duration ? 'ban' : 'permban'; diff --git a/api/services/Flairs.js b/api/services/Flairs.js index dc96e515..58cc7d96 100644 --- a/api/services/Flairs.js +++ b/api/services/Flairs.js @@ -1,6 +1,8 @@ var sha1 = require('node-sha1'); var _ = require('lodash'); var referenceService = require('./References.js'); +var NodeCache = require('node-cache'); +var app_claim_cache = new NodeCache({stdTTL: 300}); exports.formattedName = function(name) { if (!name) { @@ -56,11 +58,11 @@ exports.getFlair = function (name, flairs) { return flair.name === name; }); }; -exports.applied = function (user, flair) { - if (!user || !user.apps) { +exports.applied = function (apps, flair) { + if (!apps || !flair) { return false; } - return _.find(user.apps, function (app) { + return _.find(apps, function (app) { return app.flair === flair.name && app.sub === flair.sub; }); }; @@ -116,26 +118,24 @@ exports.getFlairTextForSVEx = function (user) { } return flairText; }; -exports.canUserApply = function (user, applicationFlair, allflairs) { - if (typeof applicationFlair === 'string') { - applicationFlair = exports.getFlair(applicationFlair, allflairs); - } - if (!user || !user.references || !applicationFlair || exports.userHasFlair(user, applicationFlair) || exports.applied(user, applicationFlair)) { +exports.canUserApply = function (refs, applicationFlair, currentFlairs) { + if (!applicationFlair) { return false; } - if (user.flair.ptrades.flair_css_class === "default" && applicationFlair.name === "involvement") { + var userHasDefaultFlair = currentFlairs.filter(function (flair) { + return flair.sub === 'pokemontrades'; + }).length === 0; + if (userHasDefaultFlair && applicationFlair.name === 'involvement') { return false; } - var refs = user.references, - trades = applicationFlair.trades || 0, + var trades = applicationFlair.trades || 0, involvement = applicationFlair.involvement || 0, eggs = applicationFlair.eggs || 0, giveaways = applicationFlair.giveaways || 0, - userTrades = _.filter(refs, referenceService.isTrade).length, + userTrades = referenceService.numberOfTrades(refs), userInvolvement = _.filter(refs, referenceService.isInvolvement).length, userEgg = _.filter(refs, referenceService.isEgg).length, - userGiveaway = referenceService.numberOfEggChecks(user) + referenceService.numberOfEggsGivenAway(user), - currentFlairs = exports.getUserFlairs(user, allflairs); + userGiveaway = referenceService.numberOfEggChecks(refs) + referenceService.numberOfEggsGivenAway(refs); if (applicationFlair.sub === "pokemontrades") { userGiveaway = _.filter(refs, function (e) { return referenceService.isGiveaway(e) && e.url.indexOf("pokemontrades") > -1; @@ -187,36 +187,147 @@ exports.flairCheck = function (ptrades, svex) { if (!ptrades || !svex) { throw "Need both flairs."; } - var ptradesFlair = "(([0-9]{4}-){2}[0-9]{4})(, (([0-9]{4}-){2}[0-9]{4}))* \\|\\| ([^,|(]*( \\((X|Y|ΩR|αS)(, (X|Y|ΩR|αS))*\\))?)(, ([^,|(]*( \\((X|Y|ΩR|αS)(, (X|Y|ΩR|αS))*\\))?))*"; - var svExFlair = ptradesFlair + " \\|\\| ([0-9]{4}|XXXX)(, (([0-9]{4})|XXXX))*"; - var tradesParts = ptrades.split("||"); - var svexParts = svex.split("||"); + if (ptrades.length > 64 || svex.length > 64) { + throw "Flairs too long"; + } + const gameOptions = ['X', 'Y', 'ΩR', 'αS'].join('|'); + const legalIgn = '[^()|,]{1,12}'; + const friendCodeGroup = /((?:\d{4}-){2}\d{4}(?:, (?:\d{4}-){2}\d{4})*)/; + const gameGroup = '^(' + legalIgn + '(?: \\((?:' + gameOptions + ')(?:, (?:' + gameOptions + '))*\\))(?:, (?:' + legalIgn + ')?(?: \\((?:' + + gameOptions + ')(?:, (?:' + gameOptions + '))*\\))?)*)$'; + var tradesParts = ptrades.split(' || '); + var svexParts = svex.split(' || '); if (tradesParts.length !== 2 || svexParts.length !== 3) { throw "Error with format."; } - var tradesFCs = tradesParts[0]; - var tradesGames = tradesParts[1]; - var svexFCs = svexParts[0]; - var svexGames = svexParts[1]; - - if (!tradesFCs.trim().match(new RegExp("(([0-9]{4}-){2}[0-9]{4})(, (([0-9]{4}-){2}[0-9]{4}))*")) || - !svexFCs.trim().match(new RegExp("(([0-9]{4}-){2}[0-9]{4})(, (([0-9]{4}-){2}[0-9]{4}))*"))) { + if (!tradesParts[0].match(friendCodeGroup) || !svexParts[0].match(friendCodeGroup)) { throw "Error with FCs"; } - if (tradesGames.trim() === "" || svexGames.trim() === "") { + if (!tradesParts[1].match(RegExp(gameGroup)) || !svexParts[1].match(RegExp(gameGroup))) { throw "We need at least 1 game."; } - if (!ptrades.match(new RegExp(ptradesFlair)) || !svex.match(new RegExp(svExFlair))) { - throw "Error with format."; - } - + var games = []; + var ignBlocks = _.union(tradesParts[1].split(/(?!\([^)]*), (?![^(]*\))/), svexParts[1].split(/(?!\([^)]*), (?![^(]*\))/)); + // Parse the games. e.g. 'ExampleName (X, Y)' --> [{ign: 'ExampleName', game: 'X'}, {ign: 'ExampleName', game: 'Y'}] + // The regex is more complicated than necessary at the moment, but this should make it easier if we decide to allow special characters in the future. + ignBlocks.forEach(function (block) { + var parts = RegExp('^(' + legalIgn + ')(?: \\(((?:' + gameOptions + ')(?:, (?:' + gameOptions + '))*)\\))?$').exec(block); + if (parts[2]) { + parts[2].split(', ').forEach(function (game) { + if (_.findIndex(games, {ign: parts[1], game: game}) === -1) { + games.push({ign: parts[1], game: game}); + } + }); + } + else if (!_.includes(_.map(games, 'ign'), parts[1])) { + games.push({ign: parts[1], game: ''}); + } + }); var response = { ptrades: ptrades, svex: svex, - fcs: [] + games: games, + tsvs: svexParts[2].split(', '), + fcs: _.union(tradesParts[0].split(', '), svexParts[0].split(', ')) }; - response.fcs = _.union(ptrades.match(/(\d{4}-){2}\d{4}/g), svex.match(/(\d{4}-){2}\d{4}/g)); - return response; -}; \ No newline at end of file +}; + +exports.makeNewCSSClass = function (previous_flair, new_addition, subreddit) { + if (!previous_flair) { + return new_addition; + } + if (new_addition === 'banned') { + if (subreddit === 'pokemontrades') { + return previous_flair.replace(/^banned$/, '').replace(/([^ ]+)( .*)?$/, '$1 ') + 'banned'; + } + return previous_flair.replace(/ ?banned/, '').replace(/(.)$/, '$1 ') + 'banned'; + } + if (new_addition === 'involvement') { + return previous_flair.replace(/( |$)/, '1$1'); + } + if (subreddit === 'pokemontrades' || !/ribbon/.test(previous_flair + new_addition)) { + return previous_flair.replace(/[^ 1]*/, new_addition); + } + if (/ribbon/.test(previous_flair)) { + if (/ribbon/.test(new_addition)) { + return previous_flair.replace(/(([^ ]* )*)[^ ]*ribbon(.*)/, '$1' + new_addition + '$3'); + } + return previous_flair.replace(/^.*?([^ ]*ribbon.*)/, new_addition + ' $1'); + } + return previous_flair.replace(/([^ ]*)(.*)/, '$1 ' + new_addition + '$2'); +}; + +// Get the Damerau–Levenshtein distance (edit distance) between two strings. +exports.edit_distance = function (string1, string2) { + var distance_matrix = {}; + var dist = function (str1, str2) { + if (!distance_matrix[[str1.length, str2.length]]) { + if (!str1 || !str2) { + distance_matrix[[str1.length, str2.length]] = str1.length || str2.length; + } else { + var vals = [ + dist(str1.slice(0, -1), str2) + 1, + dist(str1, str2.slice(0, -1)) + 1, + dist(str1.slice(0, -1), str2.slice(0, -1)) + (str1.slice(-1) === str2.slice(-1) ? 0 : 1) + ]; + if (str1.slice(-2, -1) === str2.slice(-1) && str1.slice(-1) === str2.slice(-2, -1) && str1.slice(-2) !== str2.slice(-2)) { + vals.push(dist(str1.slice(0, -2), str1.slice(0, -2)) + 1); + } + distance_matrix[[str1.length, str2.length]] = _.min(vals); + } + } + return distance_matrix[[str1.length, str2.length]]; + }; + return dist(string1, string2); +}; + +// Given a friend code, return all banned friend codes that have an edit distance of less than 3 to the given friend code. +exports.getSimilarBannedFCs = function (fc) { + return User.find({banned: true}).then(function (bannedUsers) { + return _(bannedUsers).map('loggedFriendCodes').flatten().compact().filter(function (banned_fc) { + return exports.edit_distance(fc, banned_fc) < 3; + }).value(); + }); +}; + +// Returns a promise of all flair apps for a particular username. If username is undefined, returns flair apps for all users. +// Note: This will include the claimedBy property, which we probably don't want the user to see. +exports.getApps = function (username) { + var query = username ? {user: username} : {}; + return Application.find(query).then(function (apps) { + apps.forEach(function (app) { + app.claimedBy = app_claim_cache.get(app.id) || app_claim_cache.get(app.user); + }); + return apps; + }); +}; + +// Returns a promise for all flairs +exports.getFlairs = function () { + return Flair.find({}); +}; + +exports.refreshAppClaim = function (ref, mod_username) { + // Guess what app a mod is working on based on the links they click + var query = {}; + if (References.isTrade(ref)) { + query = {sub: 'pokemontrades', flair: {not: 'involvement'}}; + } else if (References.isInvolvement(ref) || References.isGiveaway(ref) && /reddit\.com\/r\/pokemontrades/.test(ref.url)) { + query = {sub: 'pokemontrades', flair: 'involvement'}; + } else if (References.isEgg(ref)) { + query = {sub: 'svexchange', flair: {$not: /ribbon$/}}; + } else if (References.isEggCheck(ref) || References.isGiveaway(ref) && /reddit\.com\/r\/SVExchange/.test(ref.url)) { + query = {sub: 'svexchange', flair: {endsWith: 'ribbon'}}; + } else { + return []; + } + query.user = ref.user; + return Application.find(query).then(function (apps) { + apps.forEach(function (app) { + app_claim_cache.set(app.id, mod_username); + }); + return apps; + }); +}; diff --git a/api/services/Modmails.js b/api/services/Modmails.js new file mode 100644 index 00000000..cba5c030 --- /dev/null +++ b/api/services/Modmails.js @@ -0,0 +1,24 @@ +var relevantKeys = ['name', 'subject', 'body', 'author', 'subreddit', 'first_message_name', 'created_utc', 'parent_id', 'distinguished']; +var makeModmailObjects = function (modmails) { + var all_modmails = []; + for (let i = 0; i < modmails.length; i++) { + let compressed = {}; + for (let j = 0; j < relevantKeys.length; j++) { + compressed[relevantKeys[j]] = modmails[i].data[relevantKeys[j]]; + } + all_modmails.push(compressed); + if (modmails[i].data.replies) { + all_modmails = all_modmails.concat(makeModmailObjects(modmails[i].data.replies.data.children)); + } + } + return all_modmails; +}; +exports.updateArchive = async function (subreddit) { + let most_recent = await Modmail.find({subreddit: subreddit, limit: 1, sort: 'created_utc DESC'}); + if (!most_recent.length) { + console.log('Modmail archives for /r/' + subreddit + ' could not be found for some reason. Recreating from scratch...'); + return Modmail.findOrCreate(makeModmailObjects(await Reddit.getModmail(sails.config.reddit.adminRefreshToken, subreddit))); + } + let before = most_recent[0].first_message_name || most_recent[0].name; + return Modmail.findOrCreate(makeModmailObjects(await Reddit.getModmail(sails.config.reddit.adminRefreshToken, subreddit, undefined, before))); +}; diff --git a/api/services/Reddit.js b/api/services/Reddit.js index 29feaf60..241cff47 100644 --- a/api/services/Reddit.js +++ b/api/services/Reddit.js @@ -1,9 +1,9 @@ var request = require("request-promise"), moment = require('moment'), NodeCache = require('node-cache'), + _ = require('lodash'), left = 600, - resetTime = moment().add(600, "seconds"), - userAgent = "Webpage:hq.porygon.co:v" + sails.config.version; + resetTime = moment().add(600, "seconds"); var cache = new NodeCache({stdTTL: 3480}); // Cached tokens expire after 58 minutes, leave a bit of breathing room in case stuff is slow exports.refreshToken = async function(refreshToken) { @@ -19,10 +19,12 @@ exports.refreshToken = async function(refreshToken) { json: true, headers: { "Authorization": auth, - "User-Agent": userAgent, + "User-Agent": sails.config.reddit.userAgent, "Content-Type": "application/x-www-form-urlencoded", "Content-Length": data.length } + }).catch(function (error) { + throw {statusCode: 502, error: 'Error retrieving token; Reddit responded with status code ' + error.statusCode}; }); if (body && body.access_token) { cache.set(refreshToken, body.access_token); @@ -32,14 +34,16 @@ exports.refreshToken = async function(refreshToken) { } }; -var makeRequest = async function (refreshToken, requestType, url, data, rateLimitRemainingThreshold) { - let token = await exports.refreshToken(refreshToken); +var makeRequest = async function (refreshToken, requestType, url, data, rateLimitRemainingThreshold, silenceErrors) { if (left < rateLimitRemainingThreshold && moment().before(resetTime)) { - throw "Rate limited"; + throw {statusCode: 504, error: "Rate limited"}; + } + // Prevent Reddit from sanitizing '> < &' to '> < &' in the response + url += (url.indexOf('?') === -1 ? '?' : '&') + 'raw_json=1'; + var headers = {"User-Agent": sails.config.reddit.userAgent}; + if (url.indexOf("oauth.reddit.com") !== -1) { + headers.Authorization = "bearer " + await exports.refreshToken(refreshToken); } - var headers = { - Authorization: "bearer " + token, "User-Agent": userAgent - }; var options = { url: url, headers: headers, @@ -47,22 +51,34 @@ var makeRequest = async function (refreshToken, requestType, url, data, rateLimi method: requestType, formData: data }; - let response = await request(options); + let response = await request(options).catch(function (error) { + if (!silenceErrors) { + console.log('Reddit error: ' + requestType + ' request sent to ' + url + ' returned ' + error.statusCode); + console.log('Form data sent: ' + JSON.stringify(data)); + } + throw {statusCode: error.statusCode, error: '(Reddit response)'}; + }); updateRateLimits(response); var bodyJson; try { bodyJson = JSON.parse(response.body); } catch (error) { console.log("Error with parsing: " + response.body); - throw "Error with parsing: " + response.body; + throw {error: "Error with parsing: " + response.body}; } + return bodyJson; +}; - if (response.statusCode !== 200) { - console.log('Reddit error: ' + requestType + ' request sent to ' + url + ' returned ' + response.statusCode + - ' - ' + response.statusMessage + '\nForm data sent: ' + JSON.stringify(data)); - throw response.statusMessage; +var getEntireListing = async function (refreshToken, endpoint, query, rateThreshold, after, before) { + var url = endpoint + query + (query ? '&' : '?') + 'count=102&limit=100' + (after ? '&after=' + after : '') + (before ? '&before=' + before : ''); + var batch = await makeRequest(refreshToken, 'GET', url, undefined, rateThreshold, after, before); + var results = batch.data.children; + after = before ? undefined : batch.data.after; + before = before ? batch.data.before : undefined; + if (!after && !before) { + return results; } - return bodyJson; + return _.union(results, await getEntireListing(refreshToken, endpoint, query, rateThreshold, after, before)); }; exports.getFlair = async function (refreshToken, user, subreddit) { @@ -79,13 +95,23 @@ exports.getBothFlairs = async function (refreshToken, user) { return Promise.all([ptradesFlairPromise, svexFlairPromise]); }; -exports.setFlair = function (refreshToken, name, cssClass, text, subreddit) { +exports.setUserFlair = function (refreshToken, name, cssClass, text, subreddit) { var actual_sub = sails.config.debug.reddit ? sails.config.debug.subreddit : subreddit; var url = 'https://oauth.reddit.com/r/' + actual_sub + '/api/flair'; var data = {api_type: 'json', css_class: cssClass, name: name, text: text}; return makeRequest(refreshToken, 'POST', url, data, 5); }; +exports.setLinkFlair = function (refreshToken, subreddit, link_id, cssClass, text) { + var url = 'https://oauth.reddit.com/r/' + subreddit + '/api/flair'; + var data = {api_type: 'json', css_class: cssClass, link: 't3_' + link_id, text: text}; + return makeRequest(refreshToken, 'POST', url, data, 5); +}; + +exports.checkUsernameAvailable = async function (name) { + return makeRequest(undefined, 'GET', 'https://www.reddit.com/api/username_available.json?user=' + name, undefined, 10); +}; + exports.banUser = function (refreshToken, username, ban_message, note, subreddit, duration) { var actual_sub = sails.config.debug.reddit ? sails.config.debug.subreddit : subreddit; var url = 'https://oauth.reddit.com/r/' + actual_sub + '/api/friend'; @@ -94,7 +120,7 @@ exports.banUser = function (refreshToken, username, ban_message, note, subreddit }; exports.getWikiPage = async function (refreshToken, subreddit, page) { - var url = 'https://oauth.reddit.com/r/' + subreddit + '/wiki/' + page + '?raw_json=1'; + var url = 'https://oauth.reddit.com/r/' + subreddit + '/wiki/' + page; //Return a Promise for content of the page instead of all the other data let res = await makeRequest(refreshToken, 'GET', url, undefined, 5); return res.data.content_md; @@ -109,8 +135,15 @@ exports.editWikiPage = function (refreshToken, subreddit, page, content, reason) exports.searchTSVThreads = function (refreshToken, username) { var actual_sub = sails.config.debug.reddit ? sails.config.debug.subreddit : 'SVExchange'; - var url = 'https://oauth.reddit.com/r/' + actual_sub + '/search?q=flair%3Ashiny+AND+author%3A' + username + '&restrict_sr=on&sort=new&t=all'; - return makeRequest(refreshToken, 'GET', url, undefined, 15); + var query = "(and (or (field flair 'banned') (field flair 'sv')) (field author '" + username + "'))"; + return exports.search(refreshToken, actual_sub, query, true, 'new', 'all', 'cloudsearch'); +}; + +exports.search = function (refreshToken, subreddit, query, restrict_sr, sort, time, syntax) { + var querystring = '?q=' + encodeURIComponent(query) + (restrict_sr ? '&restrict_sr=on' : '') + + (sort ? '&sort=' + sort : '') + (time ? '&t=' + time : '') + '&syntax=' + (syntax ? syntax : 'cloudsearch'); + var endpoint = 'https://oauth.reddit.com/r/' + subreddit + '/search'; + return getEntireListing(refreshToken, endpoint, querystring, 10); }; exports.removePost = function (refreshToken, id, isSpam) { @@ -119,6 +152,25 @@ exports.removePost = function (refreshToken, id, isSpam) { return makeRequest(refreshToken, 'POST', url, data, 5); }; +exports.lockPost = function (refreshToken, post_id) { + var url = 'https://oauth.reddit.com/api/lock'; + var data = {id: 't3_' + post_id}; + /* Attempting to lock an archived post results in a 400 response. The request is still considered successful if this happens, so the error is + * returned instead of being thrown. */ + return makeRequest(refreshToken, 'POST', url, data, 5, true).catch(function (error) { + if (error.statusCode === 400) { + return error; + } + throw error; + }); +}; + +exports.markNsfw = function (refreshToken, post_id) { + var url = 'https://oauth.reddit.com/api/marknsfw'; + var data = {id: 't3_' + post_id}; + return makeRequest(refreshToken, 'POST', url, data, 5); +}; + exports.sendPrivateMessage = function (refreshToken, subject, text, recipient) { var url = 'https://oauth.reddit.com/api/compose'; var data = {api_type: 'json', subject: subject, text: text, to: recipient}; @@ -137,6 +189,11 @@ exports.checkModeratorStatus = async function (refreshToken, username, subreddit return res.data.children.length !== 0; }; +exports.getModmail = async function (refreshToken, subreddit, after, before) { + var endpoint = 'https://oauth.reddit.com/r/' + subreddit + '/message/moderator'; + return getEntireListing(refreshToken, endpoint, '', 20, after, before); +}; + var updateRateLimits = function (res) { if (res && res.headers && res.headers['x-ratelimit-remaining'] && res.headers['x-ratelimit-reset']) { left = res.headers['x-ratelimit-remaining']; diff --git a/api/services/References.js b/api/services/References.js index 15920841..b08e1e24 100644 --- a/api/services/References.js +++ b/api/services/References.js @@ -72,6 +72,9 @@ exports.isEggCheck = function (el) { exports.isMisc = function (el) { return el.type === "misc"; }; +exports.isApprovable = function (el) { + return ['event', 'shiny', 'casual', 'egg', 'giveaway', 'involvement', 'eggcheck'].indexOf(el.type) !== -1; +}; exports.isNotNormalTrade = function (type) { return type === 'egg' || type === 'giveaway' || type === 'misc' || type === 'eggcheck' || type === 'involvement'; }; @@ -85,59 +88,57 @@ exports.getRedditUser = function (username) { return username; } }; -exports.numberOfPokemonGivenAway = function (user) { +exports.numberOfPokemonGivenAway = function (refs) { var givenAway = 0; - if (!user || !user.references) { - return; + if (!refs) { + return 0; } - user.references.filter(function (item) { + refs.filter(function (item) { return exports.isGiveaway(item) && item.url.indexOf("pokemontrades") !== -1; }).forEach(function (ref) { givenAway += (ref.number || 0); }); return givenAway; }; -exports.numberOfEggsGivenAway = function (user) { +exports.numberOfEggsGivenAway = function (refs) { var givenAway = 0; - if (!user || !user.references) { - return; + if (!refs) { + return 0; } - user.references.filter(function (item) { + refs.filter(function (item) { return exports.isGiveaway(item) && item.url.indexOf("SVExchange") > -1; }).forEach(function (ref) { givenAway += (ref.number || 0); }); return givenAway; }; -exports.numberOfEggChecks = function (user) { +exports.numberOfEggChecks = function (refs) { var givenAway = 0; - if (!user || !user.references) { - return; + if (!refs) { + return 0; } - user.references.filter(function (item) { + refs.filter(function (item) { return exports.isEggCheck(item); }).forEach(function (ref) { - if (ref.url.indexOf("SVExchange") > -1) { - givenAway += (ref.number || 0); - } + givenAway += (ref.number || 0); }); return givenAway; }; -exports.numberOfApprovedEggChecks = function (user) { +exports.numberOfApprovedEggChecks = function (refs) { var num = 0; - if (!user || !user.references) { - return; + if (!refs) { + return 0; } - user.references.filter(function (item) { + refs.filter(function (item) { return exports.isEggCheck(item) && exports.isApproved(item); }).forEach(function (ref) { num += ref.number || 0; }); return num; }; -exports.numberOfTrades = function (user) { - if (!user || !user.references) { +exports.numberOfTrades = function (refs) { + if (!refs) { return 0; } - return user.references.filter(exports.isTrade).length; + return refs.filter(exports.isTrade).length; }; diff --git a/api/services/Search.js b/api/services/Search.js index 4baa1156..df099308 100644 --- a/api/services/Search.js +++ b/api/services/Search.js @@ -77,4 +77,63 @@ module.exports.logs = function (searchData, cb) { Event.find(appData).exec(function (err, apps) { cb(apps); }); -}; \ No newline at end of file +}; + +module.exports.users = function (searchData, cb) { + var data = { + "$or": [ + { + "_id": { + "$regex": "(?i)" + searchData.keyword + } + }, + { + "flair.ptrades.flair_text": { + "$regex": "(?i)" + searchData.keyword + } + }, + { + "flair.svex.flair_text": { + "$regex": "(?i)" + searchData.keyword + } + } + ] + }; + + // We can't do deep searching on the flair using waterline, so let's use mongo natively + // I guess this means we can't use any other databases in the future anymore. Ach well. + User.native(function (err, collection) { + collection.find(data) + .limit(20) + .skip(0) + .toArray(function (err, results) { + cb(results); + }); + }); +}; + +module.exports.modmails = function (searchData, cb) { + var words = searchData.keyword.split(' '); + var fields = ['body', 'author', 'subject']; + var requirements = []; + for (let i = 0; i < words.length; i++) { + var current_req = {'$or': []}; + for (let j = 0; j < fields.length; j++) { + let obj = {}; + obj[fields[j]] = {'$regex': words[i], '$options': 'i'}; + current_req['$or'].push(obj); + } + requirements.push(current_req); + } + //Finds modmails where all of the words in the search query appear somewhere in either the body, subject, or author. + var mailData = {'$and': requirements}; + Modmail.native(function (err, collection) { + collection.find(mailData).sort({created_utc: -1}).skip(searchData.skip ? parseInt(searchData.skip) : 0).limit(20).toArray().then(function (mail) { + mail.forEach(function (message) { + message.name = message._id; + delete message._id; + }); + cb(mail); + }); + }); +}; diff --git a/api/services/Users.js b/api/services/Users.js new file mode 100644 index 00000000..57687214 --- /dev/null +++ b/api/services/Users.js @@ -0,0 +1,60 @@ +var removeSecretInformation = function (user) { + user.redToken = undefined; + user.loggedFriendCodes = undefined; + if (user.apps) { + user.apps.forEach(function (app) { + app.claimedBy = undefined; + }); + } + return user; +}; + +exports.get = async function (requester, username) { + var user = await User.findOne(username); + if (!user) { + throw {statusCode: 404}; + } + var promises = []; + + promises.push(Game.find({user: user.name}).sort({createdAt: 'desc'}).then(function (result) { + user.games = result; + })); + + promises.push(Comment.find({user: user.name}).sort({createdAt: 'desc'}).then(function (result) { + user.comments = result; + })); + + if (requester && requester.isMod) { + promises.push(ModNote.find({refUser: user.name}).sort({createdAt: 'desc'}).then(function (result) { + user.modNotes = result; + })); + } + + if (requester && requester.name === user.name) { + promises.push(Flairs.getApps(user.name).then(function (result) { + user.apps = result; + })); + } + + promises.push(Reference.find({user: user.name}).sort({type: 'asc', createdAt: 'desc'}).then(function (result) { + result.forEach(function (ref) { + if (!requester || requester.name !== user.name) { + ref.privatenotes = undefined; + } + if (!requester || !requester.isMod) { + ref.approved = undefined; + ref.verified = undefined; + } + }); + user.references = result; + })); + await* promises; + return removeSecretInformation(user); +}; + +// Returns a promise for all banned users +exports.getBannedUsers = function () { + return User.find({banned: true}).then(function (results) { + return results.map(removeSecretInformation); + }); +}; diff --git a/app.js b/app.js index d65a8984..d9834edb 100644 --- a/app.js +++ b/app.js @@ -49,6 +49,7 @@ } } + require("babel/register")({/* babel options */}); // Start server sails.lift(rc('sails')); diff --git a/assets/js/adminCtrl.js b/assets/adminCtrl.js similarity index 50% rename from assets/js/adminCtrl.js rename to assets/adminCtrl.js index 30a03134..07286e2b 100644 --- a/assets/js/adminCtrl.js +++ b/assets/adminCtrl.js @@ -1,10 +1,7 @@ -var socket = require("socket.io-client"); -var io = require("sails.io.js")(socket); -var sharedService = require("./sharedClientFunctions.js"); - -module.exports = function ($scope) { +var shared = require('./sharedClientFunctions.js'); +module.exports = function ($scope, io) { + shared.addRepeats($scope, io); $scope.users = []; - $scope.flairApps = []; $scope.flairAppError = ""; $scope.adminok = { appFlair: {} @@ -12,43 +9,26 @@ module.exports = function ($scope) { $scope.adminspin = { appFlair: {} }; - sharedService.addRepeats($scope); - - $scope.getFlairApps = function () { - io.socket.get("/flair/apps/all", function (data, res) { - if (res.statusCode === 200) { - $scope.flairApps = data; - $scope.$apply(); - } - }); - }; - - $scope.getBannedUsers = function () { - io.socket.get("/user/banned", function (data, res) { - if (res.statusCode === 200) { - $scope.users = data; - $scope.$apply(); - } - }); - }; - $scope.denyApp = function (id, $index) { + $scope.denyApp = function (id) { var url = "/flair/app/deny"; $scope.flairAppError = ""; io.socket.post(url, {id: id}, function (data, res) { if (res.statusCode === 200) { - $scope.flairApps.splice($index, 1); - $scope.$apply(); + $scope.flairApps = data; + } else if (res.statusCode === 404) { + $scope.flairApps = data; + $scope.flairAppError = "That app no longer exists."; } else { $scope.flairAppError = "Couldn't deny, for some reason."; - $scope.$apply(); console.log(data); } + $scope.$apply(); }); }; - $scope.approveApp = function (id, $index) { + $scope.approveApp = function (id) { $scope.adminok.appFlair[id] = false; $scope.adminspin.appFlair[id] = true; $scope.flairAppError = ""; @@ -57,7 +37,10 @@ module.exports = function ($scope) { io.socket.post(url, {id: id}, function (data, res) { if (res.statusCode === 200) { $scope.adminok.appFlair[id] = true; - $scope.flairApps.splice($index, 1); + $scope.flairApps = data; + } else if (res.statusCode === 404) { + $scope.flairApps = data; + $scope.flairAppError = "That app no longer exists."; } else { $scope.flairAppError = "Couldn't approve, for some reason."; console.log(data); @@ -66,7 +49,4 @@ module.exports = function ($scope) { $scope.$apply(); }); }; - - $scope.getBannedUsers(); - $scope.getFlairApps(); -}; \ No newline at end of file +}; diff --git a/assets/app.js b/assets/app.js new file mode 100644 index 00000000..a7ba1f71 --- /dev/null +++ b/assets/app.js @@ -0,0 +1,56 @@ +var ng = require('angular'); +var $ = require('jquery'); +var _csrf = $('#app').attr('_csrf'); + +var refCtrl = require('./refCtrl'); +var indexCtrl = require('./indexCtrl'); +var adminCtrl = require('./adminCtrl'); +var banCtrl = require('./banCtrl'); +var userCtrl = require('./userCtrl'); +require('./search/search.module'); +require('./markdown/markdown.module'); +require('angular-spinner'); +require('angular-bootstrap-npm'); +require('angular-mask'); +require('bootstrap'); +require('./ngReallyClick'); +require('./tooltip/tooltip.module'); +require('./numberPadding'); +//require('spin'); + +var fapp = ng.module("fapp", [ + 'angularSpinner', + 'ngReallyClickModule', + 'numberPaddingModule', + 'tooltipModule', + 'ngMask', + 'fapp.search', + 'fapp.md' +]); + +fapp.service('io', function () { + var socket = require('socket.io-client'); + var io = require('sails.io.js')(socket); + io.socket.post = function (url, data, callback) { + data._csrf = _csrf; + io.socket.request({method: 'post', url: url, params: data}, callback); + }; + return io; +}); + +// Define controllers, and their angular dependencies +fapp.controller("referenceCtrl", ['$scope', 'io', refCtrl]); +fapp.controller("indexCtrl", ['$scope', 'io', indexCtrl]); +fapp.controller("userCtrl", ['$scope', '$location', 'io', userCtrl]); +fapp.controller("adminCtrl", ['$scope', 'io', adminCtrl]); +fapp.controller("banCtrl", ['$scope', 'io', banCtrl]); + +// Bug fix for iOS safari +$(function () { + $("[data-toggle='collapse']").click(function () { + // For some reason, iOS safari doesn't let collapse work on a div if it + // doesn't have a click handler. The click handler doesn't need to do anything. + }); +}); + +ng.bootstrap(document, ['fapp']); diff --git a/assets/js/banCtrl.js b/assets/banCtrl.js similarity index 68% rename from assets/js/banCtrl.js rename to assets/banCtrl.js index fa1a84b4..12bf95ff 100644 --- a/assets/js/banCtrl.js +++ b/assets/banCtrl.js @@ -1,15 +1,14 @@ -var socket = require("socket.io-client"); -var io = require("sails.io.js")(socket); - -module.exports = function ($scope) { - +var shared = require('./sharedClientFunctions.js'); +module.exports = function ($scope, io) { + shared.addRepeats($scope, io); $scope.banInfo = { - username: "", - banNote: "", - banMessage: "", - banlistEntry: "", - duration: "", - additionalFCs: "" + username: $scope.query.username || '', + banNote: $scope.query.banNote || '', + banMessage: $scope.query.banMessage || '', + banlistEntry: $scope.query.banlistEntry || '', + duration: $scope.query.duration || '', + knownAlt: $scope.query.knownAlt || '', + additionalFCs: $scope.query.additionalFCs || '' }; $scope.banError = ""; @@ -31,18 +30,17 @@ module.exports = function ($scope) { return; } - if ($scope.banInfo.username.substring(0, 3) === '/u/') { - $scope.banInfo.username = $scope.banInfo.username.substring(3); - } - - else if ($scope.banInfo.username.substring(0, 2) === 'u/') { - $scope.banInfo.username = $scope.banInfo.username.substring(2); - } + var names = ['username', 'knownAlt']; + for (let i = 0; i < names.length; i++) { - if (!$scope.banInfo.username.match(/^[A-Za-z0-9_-]{1,20}$/)) { - $scope.banError = "Invalid username"; - $scope.indexSpin.ban = false; - return; + if ($scope.banInfo[names[i]].match(/^\/?u\//)) { + $scope.banInfo[names[i]] = $scope.banInfo[names[i]].substring($scope.banInfo[names[i]].indexOf('u/') + 2); + } + if ($scope.banInfo[names[i]] && !$scope.banInfo[names[i]].match(/^[A-Za-z0-9_-]{1,20}$/)) { + $scope.banError = "Invalid " + names[i]; + $scope.indexSpin.ban = false; + return; + } } if ($scope.banInfo.banNote.length > 300) { @@ -79,6 +77,7 @@ module.exports = function ($scope) { "banMessage": $scope.banInfo.banMessage, "banlistEntry": $scope.banInfo.banlistEntry, "duration": parseInt($scope.banInfo.duration), + "knownAlt": $scope.banInfo.knownAlt, "additionalFCs": FCs }; @@ -91,6 +90,7 @@ module.exports = function ($scope) { $scope.banInfo.banMessage = ""; $scope.banInfo.banlistEntry = ""; $scope.banInfo.duration = ""; + $scope.banInfo.knownAlt = ""; $scope.banInfo.additionalFCs = ""; $scope.indexOk.ban = true; window.setTimeout(function () { @@ -99,11 +99,11 @@ module.exports = function ($scope) { }, 1500); $scope.$apply(); } else { - $scope.indexOk = false; + $scope.indexOk.ban = false; if (res.body.error) { - $scope.banError = "Something went wrong; you might have to do stuff manually. Error " + res.statusCode + ": " + res.body.error; + $scope.banError = "Error " + res.statusCode + ": " + res.body.error; } else { - $scope.banError = "Something went wrong; you might have to do stuff manually."; + $scope.banError = "Error " + res.statusCode; } $scope.$apply(); } diff --git a/assets/common/genericTooltipModule.js b/assets/common/genericTooltipModule.js deleted file mode 100644 index 175175ff..00000000 --- a/assets/common/genericTooltipModule.js +++ /dev/null @@ -1,33 +0,0 @@ -var ng = require("angular"); -var $ = require('jquery'); - -ng.module("genericTooltipModule", []).directive("ngGenericTooltip", function () { - return { - restrict: 'E', - replace: true, - scope: { - title: '@title' - }, - templateUrl: '/common/genericTooltipView.html', - transclude: true, - link: function (scope, element) { - var thisElement = $(element[0]).find('[data-toggle=tooltip]'); - thisElement.tooltip({ - html: true, - trigger: 'manual', - title: scope.title - }).on("mouseenter", function () { - thisElement.tooltip("show"); - $(".tooltip").on("mouseleave", function () { - thisElement.tooltip('hide'); - }); - }).on("mouseleave", function () { - setTimeout(function () { - if (!$(".tooltip:hover").length) { - thisElement.tooltip("hide"); - } - }, 100); - }); - } - }; -}); \ No newline at end of file diff --git a/assets/js/indexCtrl.js b/assets/indexCtrl.js similarity index 82% rename from assets/js/indexCtrl.js rename to assets/indexCtrl.js index 3e1f10bb..4247ed5e 100644 --- a/assets/js/indexCtrl.js +++ b/assets/indexCtrl.js @@ -1,19 +1,18 @@ -var socket = require("socket.io-client"); -var io = require("sails.io.js")(socket); var $ = require('jquery'); -var sharedService = require('./sharedClientFunctions.js'); +var shared = require('./sharedClientFunctions.js'); -module.exports = function ($scope) { +module.exports = function ($scope, io) { + shared.addRepeats($scope, io); $scope.addInfo = { - refUrl: "", - type: "", - user2: "", - gave: "", - got: "", - number: "", - descrip: "", - notes: "", - privatenotes: "" + refUrl: $scope.query.refUrl || '', + type: $scope.query.type || '', + user2: $scope.query.user2 || '', + gave: $scope.query.gave || '', + got: $scope.query.got || '', + number: $scope.query.number || '', + descrip: $scope.query.descrip || '', + notes: $scope.query.notes || '', + privatenotes: $scope.query.privatenotes || '' }; $scope.selectedRef = {}; @@ -22,7 +21,6 @@ module.exports = function ($scope) { $scope.editRefError = ""; $scope.indexOk = {}; $scope.indexSpin = {}; - sharedService.addRepeats($scope); $scope.focus = { gavegot: false @@ -38,11 +36,6 @@ module.exports = function ($scope) { $scope.referenceToRevert = $.extend(true, {}, ref); }; - $scope.revertRef = function () { - var index = $scope.user.references.indexOf($scope.selectedRef); - $scope.user.references[index] = $.extend(true, {}, $scope.referenceToRevert); - }; - $scope.addReference = function () { $scope.addRefError = ""; $scope.indexOk.addRef = false; @@ -78,7 +71,7 @@ module.exports = function ($scope) { $scope.addInfo.notes = ""; $scope.addInfo.privatenotes = ""; $scope.addInfo.number = ""; - $scope.user.references.push(data); + $scope.refUser.references.push(data); if (data.type === "redemption") { $('#collapseevents').prev().children().animate({ @@ -116,7 +109,7 @@ module.exports = function ($scope) { if (data && data.err) { $scope.addRefError = data.err; } else { - $scope.addRefError = "Already added that URL."; + $scope.addRefError = "There was an error."; } $scope.$apply(); } diff --git a/assets/js/app.js b/assets/js/app.js deleted file mode 100644 index cc5a15d3..00000000 --- a/assets/js/app.js +++ /dev/null @@ -1,62 +0,0 @@ -var ng = require('angular'); -var $ = require('jquery'); -var marked = require('marked'); - -var refCtrl = require('./refCtrl'); -var indexCtrl = require('./indexCtrl'); -var adminCtrl = require('./adminCtrl'); -var banCtrl = require('./banCtrl'); -var userCtrl = require('./userCtrl'); -var searchCtrl = require('./searchCtrl'); -require('angular-spinner'); -require('angular-md'); -require('angular-bootstrap-npm'); -require('angular-mask'); -require('bootstrap'); -require('./ngReallyClick'); -require('../common/tooltipModule'); -require('../common/genericTooltipModule'); -require('./numberPadding'); -//require('spin'); - -var fapp = ng.module("fapp", [ - 'angularSpinner', - 'ngReallyClickModule', - 'numberPaddingModule', - 'yaru22.md', - 'tooltipModule', - 'genericTooltipModule', - 'ngMask' -]); - -fapp.factory('UserFactory', function () { - var user; - - return { - getUser: function () { - return user; - }, - setUser: function (newUser) { - user = newUser; - } - }; -}); - -// Define controllers, and their angular dependencies -fapp.controller("referenceCtrl", ['$scope', '$filter', refCtrl]); -fapp.controller("indexCtrl", ['$scope', indexCtrl]); -fapp.controller("userCtrl", ['$scope', '$filter', '$location', 'UserFactory', userCtrl]); -fapp.controller("searchCtrl", ['$scope', '$timeout', 'UserFactory', searchCtrl]); -fapp.controller("adminCtrl", ['$scope', adminCtrl]); -fapp.controller("banCtrl", ['$scope', banCtrl]); - -// Bug fix for iOS safari -$(function () { - $("[data-toggle='collapse']").click(function () { - // For some reason, iOS safari doesn't let collapse work on a div if it - // doesn't have a click handler. The click handler doesn't need to do anything. - }); -}); - -window.marked = marked; -ng.bootstrap(document, ['fapp']); \ No newline at end of file diff --git a/assets/js/dependencies/mask.min.js b/assets/js/dependencies/mask.min.js deleted file mode 100644 index 06f7df13..00000000 --- a/assets/js/dependencies/mask.min.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(){"use strict";angular.module("ngMask",[])}(),function(){"use strict";angular.module("ngMask").directive("mask",["$log","$timeout","MaskService",function(a,b,c){return{restrict:"A",require:"ngModel",compile:function(d,e){function f(a){"number"==typeof a&&(b.cancel(g),g=b(function(){var b=a+1,c=d[0];if(c.setSelectionRange)c.focus(),c.setSelectionRange(a,b);else if(c.createTextRange){var e=c.createTextRange();e.collapse(!0),e.moveEnd("character",b),e.moveStart("character",a),e.select()}}))}if(!e.mask||!e.ngModel)return void a.info("Mask and ng-model attributes are required!");var g,h,i=c.create();return{pre:function(a,b,c){h=i.generateRegex({mask:c.mask,repeat:c.repeat||c.maskRepeat,clean:"true"===(c.clean||c.maskClean),limit:"true"===(c.limit||c.maskLimit||"true"),restrict:c.restrict||c.maskRestrict||"select",validate:"true"===(c.validate||c.maskValidate||"true"),model:c.ngModel,value:c.ngValue})},post:function(c,d,e,g){h.then(function(){function h(b){b=b||"";var c=i.getViewValue(b),d=k.maskWithoutOptionals||"",e=c.withDivisors(!0),h=c.withoutDivisors(!0);try{var j=i.getRegex(e.length-1),l=i.getRegex(d.length-1),m=j.test(e)||l.test(e),n=b.length-e.length===1,o=d.length-e.length>0;if("accept"!==k.restrict)if("select"!==k.restrict||m&&!n)"reject"!==k.restrict||m||(c=i.removeWrongPositions(e),e=c.withDivisors(!0),h=c.withoutDivisors(!0));else{var p=b[b.length-1],q=e[e.length-1];p!==q&&o&&(e+=p);var r=i.getFirstWrongPosition(e);angular.isDefined(r)&&f(r)}k.limit||(e=c.withDivisors(!1),h=c.withoutDivisors(!1)),k.validate&&g.$dirty&&(l.test(e)||g.$isEmpty(g.$modelValue)?g.$setValidity("mask",!0):g.$setValidity("mask",!1)),b!==e&&(g.$setViewValue(angular.copy(e),"input"),g.$render())}catch(s){throw a.error("[mask - parseViewValue]"),s}return k.clean?h:e}var j,k=i.getOptions();g.$parsers.push(h),d.on("click input paste keyup",function(){j=b(function(){b.cancel(j),h(d.val()),c.$apply()},100)});var l=c.$watch(e.ngModel,function(a){angular.isDefined(a)&&(h(a),l())});k.value&&c.$evalAsync(function(){g.$setViewValue(angular.copy(k.value),"input"),g.$render()})})}}}}}])}(),function(){"use strict";angular.module("ngMask").factory("MaskService",["$q","OptionalService","UtilService",function(a,b,c){function d(){function d(a,b){var c;try{var d=t[a],e=C[d],f=h(a);e?c="("+e.source+")":(i(a)||(z.push(a),A[a]=d),c="(\\"+d+")")}catch(g){throw g}return(f||b)&&(c+="?"),new RegExp(c)}function e(a,b){var c,f;try{var g=d(a,b);c=g;var i=h(a),j=g.source;if(i&&u>a+1){var k=e(a+1,!0).elementOptionalRegex();j+=k.source}f=new RegExp(j)}catch(l){throw l}return{elementRegex:function(){return c},elementOptionalRegex:function(){return f}}}function f(c){var d=a.defer();s=c;try{var f=c.mask,g=c.repeat;g&&(f=Array(parseInt(g)+1).join(f)),w=b.getOptionals(f).fromMaskWithoutOptionals(),s.maskWithoutOptionals=t=b.removeOptionals(f),u=t.length;for(var h,i=0;u>i;i++){var l=e(i),m=l.elementRegex(),n=l.elementOptionalRegex(),o=h?h.source+n.source:n.source;o=new RegExp(o),h=h?h.source+m.source:m.source,h=new RegExp(h),B.push(o)}j(),v=k(t).length,d.resolve({options:s,divisors:z,divisorElements:A,optionalIndexes:w,optionalDivisors:x,optionalDivisorsCombinations:y})}catch(p){throw d.reject(p),p}return d.promise}function g(a){var b;try{b=B[a]?B[a].source:""}catch(c){throw c}return new RegExp("^"+b+"$")}function h(a){return c.inArray(a,w)}function i(a){return c.inArray(a,z)}function j(){function a(a,b){return a-b}for(var b=z.sort(a),c=w.sort(a),d=0;d=e)break;x[e]=x[e]?x[e].concat(e-f):[e-f],A[e-f]=A[e]}}function k(a){try{if(z.length>0&&a){for(var b=Object.keys(A),d=[],e=b.length-1;e>=0;e--){var f=A[b[e]];f&&d.push(f)}d=c.uniqueArray(d);var g=new RegExp("[\\"+d.join("\\")+"]","g");return a.replace(g,"")}return a}catch(h){throw h}}function l(a,b){function d(a,b){for(var c=b,d=0;d0){for(var e=[],f=Object.keys(x),h=0;h=0;h--){var j=angular.copy(b);j=l(j,y[h]);var k=j.join(""),m=g(t.length-1);if(m.test(k)){d=!1,b=j;break}}}return d&&(b=l(b,z)),b.join("")}function n(){return s}function o(a){try{var b=k(a),c=m(b);return{withDivisors:function(a){return a?c.substr(0,u):c},withoutDivisors:function(a){return a?b.substr(0,v):b}}}catch(d){throw d}}function p(a,b){var c=[];if(!a)return 0;for(var d=0;dk;++k)e[h]=i[k],b.apply(c,e);else for(var k=0;j>k;++k)e[h]=i[k],d(h+1);e.pop()}c||(c=this);for(var e=[],f=a.length-1,g=[],h=a.length;h--;)g[h]=a[h].length;d(0)}function b(a,b){var c;try{c=b.indexOf(a)>-1}catch(d){throw d}return c}function c(a){for(var b={},c=[],d=0,e=a.length;e>d;++d)b.hasOwnProperty(a[d])||(c.push(a[d]),b[a[d]]=1);return c}return{lazyProduct:a,inArray:b,uniqueArray:c}}])}(); -//# sourceMappingURL=ngMask.min.map \ No newline at end of file diff --git a/assets/js/searchCtrl.js b/assets/js/searchCtrl.js deleted file mode 100644 index 40640ccc..00000000 --- a/assets/js/searchCtrl.js +++ /dev/null @@ -1,140 +0,0 @@ -var socket = require("socket.io-client"); -var io = require("sails.io.js")(socket); -var _ = require("lodash"); -var $ = require("jquery"); - -module.exports = function ($scope, $timeout, UserFactory) { - - $scope.user = {}; - $scope.$watch( - UserFactory.getUser, - function (newU, oldU) { - if (newU !== oldU) { - $scope.user = UserFactory.getUser(); - for (var i = 0; i < $scope.potentialSearches.length; i++) { - if ($scope.user.isMod || !$scope.potentialSearches[i].modOnly) { - $scope.searchInfo.searches.push($scope.potentialSearches[i]); - } - } - if (!$scope.searchInfo.search) { - $scope.searchInfo.search = $scope.searchInfo.searches[0]; - } - } - } - ); - - $scope.potentialSearches = [{long: "ref", short: "s", name: "References"}, {long: "log", short: "l", name: "Logs", modOnly: true}]; - - $scope.searchInfo = { - keyword: "", - category: [], - user: "", - searches: [] - }; - - $scope.setSearch = function (long) { - $scope.searchInfo.search = _.find($scope.potentialSearches, function (item) { - return item.long === long; - }); - }; - $scope.searchInfo.uriKeyword = function () { - return encodeURIComponent($scope.searchInfo.keyword.replace(/\//g, "%2F")); - }; - $scope.searching = false; - $scope.searchResults = []; - $scope.searchedFor = ""; - $scope.lastSearch = ""; - $scope.numberSearched = 0; - - $scope.toggleCategory = function (name) { - var index = $scope.searchInfo.category.indexOf(name); - - if (index > -1) { - $scope.searchInfo.category.splice(index, 1); - } else { - $scope.searchInfo.category.push(name); - } - - if ($scope.searchInfo.keyword) { - $scope.search(); - } - }; - - $scope.getMore = function () { - if ($scope.searching) { - return; - } - $scope.numberSearched += 20; - $scope.search($scope.numberSearched); - }; - - $scope.search = function (skip) { - $('.search-results').show(); - $scope.searching = true; - if (!$scope.searchInfo.keyword) { - $scope.searching = false; - $scope.searchResults = []; - $scope.numberSearched = 0; - return; - } - - if (!skip) { - $scope.numberSearched = 0; - skip = 0; - } - - var url = "/search/" + $scope.searchInfo.search.long; - url += "?keyword=" + $scope.searchInfo.keyword; - if ($scope.searchInfo.category.length > 0) { - url += "&categories=" + $scope.searchInfo.category; - } - if ($scope.searchInfo.user) { - url += "&user=" + $scope.searchInfo.user; - } - url += "&skip=" + skip; - - $scope.searchedFor = url; - io.socket.get(url, function (data, res) { - if (res.statusCode === 200 && $scope.searchedFor === url) { - if (skip) { - $scope.searchResults = $scope.searchResults.concat(data); - } else { - $scope.searchResults = data; - } - $scope.searching = false; - $scope.$apply(); - } else if (res.statusCode !== 200) { - // Some error - console.log(res); - } - }); - }; - - var searchTimeout; - $scope.searchMaybe = function () { - if (searchTimeout) { - $timeout.cancel(searchTimeout); - } - - searchTimeout = $timeout(function () { - if (!_.isEqual($scope.lastSearch, $scope.searchInfo)) { - $scope.lastSearch = _.cloneDeep($scope.searchInfo); - $scope.search(); - console.log("searching " + $scope.searchInfo); - } - }, 500); - }; - - $scope.submit = function () { - if ($scope.searchInfo.keyword) { - window.location.href = "/search/" + $scope.searchInfo.search.short + "/" + $scope.searchInfo.uriKeyword(); - } - }; - - $timeout(function () { - if ($scope.searchInfo.keyword) { - $scope.search(); - } - }, 300); - $scope.getFlairs(); -}; diff --git a/assets/markdown/markdown.module.js b/assets/markdown/markdown.module.js new file mode 100644 index 00000000..6fc7a892 --- /dev/null +++ b/assets/markdown/markdown.module.js @@ -0,0 +1,26 @@ +/** + * A module to give us a nice, easy way to use markdown in the app + */ + +var angular = require("angular"); +var Snudown = require("snudown-js"); +var remapURLs = require("./remapURLs"); + +module.exports = angular.module("fapp.md", []) + .directive("md", function () { + return { + restrict: "E", + require: "?ngModel", + link: function ($scope, $elem, $attrs, ngModel) { + if (!ngModel) { + var html = remapURLs(Snudown.markdown($elem.text())); + $elem.html(html); + return; + } + ngModel.$render = function () { + var html = remapURLs(Snudown.markdown(ngModel.$viewValue || "")); + $elem.html(html); + }; + } + }; + }); \ No newline at end of file diff --git a/assets/markdown/remapURLs.js b/assets/markdown/remapURLs.js new file mode 100644 index 00000000..6b84c566 --- /dev/null +++ b/assets/markdown/remapURLs.js @@ -0,0 +1,6 @@ +module.exports = function (value) { + var userRegex = /()(\/?\2)(<\/a>)/g; + var userReplaced = value.replace(userRegex, "$1https://www.reddit.com/$2$3$4$5 ($1/$2$3FlairHQ$5)"); + var subRegex = /()(\/?\2)(<\/a>)/g; + return userReplaced.replace(subRegex, "$1https://www.reddit.com/$2$3$4$5"); +}; \ No newline at end of file diff --git a/assets/js/ngReallyClick.js b/assets/ngReallyClick.js similarity index 100% rename from assets/js/ngReallyClick.js rename to assets/ngReallyClick.js diff --git a/assets/js/numberPadding.js b/assets/numberPadding.js similarity index 100% rename from assets/js/numberPadding.js rename to assets/numberPadding.js diff --git a/assets/js/refCtrl.js b/assets/refCtrl.js similarity index 80% rename from assets/js/refCtrl.js rename to assets/refCtrl.js index a302fdf0..a11fefe2 100644 --- a/assets/js/refCtrl.js +++ b/assets/refCtrl.js @@ -1,9 +1,8 @@ -var socket = require("socket.io-client"); -var io = require("sails.io.js")(socket); var $ = require('jquery'); -var sharedService = require('./sharedClientFunctions.js'); +var shared = require('./sharedClientFunctions.js'); -module.exports = function ($scope, $filter) { +module.exports = function ($scope, io) { + shared.addRepeats($scope, io); $scope.newStuff = { newComment: "" }; @@ -15,41 +14,21 @@ module.exports = function ($scope, $filter) { approveAll: {} }; $scope.saving = {}; - $scope.refUser = { - name: window.location.pathname.substring(3) - }; $scope.selectedRef = {}; $scope.referenceToRevert = {}; $scope.indexOk = {}; $scope.indexSpin = {}; - $scope.editReference = function (ref) { $scope.selectedRef = ref; $scope.referenceToRevert = $.extend(true, {}, ref); }; $scope.revertRef = function () { - var index = $scope.user.references.indexOf($scope.selectedRef); - $scope.user.references[index] = $.extend(true, {}, $scope.referenceToRevert); + var index = $scope.refUser.references.indexOf($scope.selectedRef); + $scope.refUser.references[index] = $.extend(true, {}, $scope.referenceToRevert); }; - sharedService.addRepeats($scope); - - io.socket.get("/user/get/" + $scope.refUser.name, function (data, res) { - if (res.statusCode === 200) { - $scope.refUser = data; - if (!$scope.refUser.friendCodes || $scope.refUser.friendCodes.length === 0) { - $scope.refUser.friendCodes = [""]; - } - if ($scope.refUser.games.length === 0) { - $scope.refUser.games = [{tsv: "", ign: ""}]; - } - window.document.title = data.name + "'s reference"; - $scope.$apply(); - } - }); - $scope.addComment = function () { var comment = $scope.newStuff.newComment, url = "/reference/comment/add"; @@ -89,9 +68,6 @@ module.exports = function ($scope, $filter) { $scope.modSaveProfile = function () { $scope.ok.modSaveProfile = false; $scope.spin.modSaveProfile = true; - if (!$scope.user.isMod) { - return; - } var intro = $scope.refUser.intro, fcs = $scope.refUser.friendCodes.slice(0), games = $scope.refUser.games, @@ -204,9 +180,13 @@ module.exports = function ($scope, $filter) { } else { $scope.ok.approveAll[type] = true; if (type === "event") { - $scope.refUser.references = $filter("filter")($scope.refUser.references, {type: "!redemption"}); + $scope.refUser.references = $scope.refUser.references.filter(function (ref) { + return ref.type !== 'redemption'; + }); } - $scope.refUser.references = $filter("filter")($scope.refUser.references, {type: "!" + type}); + $scope.refUser.references = $scope.refUser.references.filter(function (ref) { + return ref.type !== type; + }); $scope.refUser.references = $scope.refUser.references.concat(data); } $scope.spin.approveAll[type] = false; diff --git a/assets/search/README.md b/assets/search/README.md new file mode 100644 index 00000000..f819ba4b --- /dev/null +++ b/assets/search/README.md @@ -0,0 +1,13 @@ +# Adding a new search function (client side) + +Assuming you have done everything client, side, because that is simple, there are a few small things that are not +obvious with the client side code. + +Firstly, you need to add a new directory (obvious) and have a form.ejs and result.ejs files. These are for the +advanced form (for the advanced page) and the results, for both the advanced page and the dropdown from the header. + +Then you need to add an option to the ./header.ejs file pointing to the right result.ejs file. The reason this can't be +automated, is that we can't loop over the directories in ejs, unless we have an array somewhere of what ones we have, and +that feels messier than this. Maybe. I don't know, we can probably create one sometime or figure out a nicer way to do it. + +I'm sure there is a nice way somewhere... \ No newline at end of file diff --git a/assets/search/header.ejs b/assets/search/header.ejs new file mode 100644 index 00000000..74766736 --- /dev/null +++ b/assets/search/header.ejs @@ -0,0 +1,48 @@ + \ No newline at end of file diff --git a/assets/search/log/controller.js b/assets/search/log/controller.js new file mode 100644 index 00000000..0caf3746 --- /dev/null +++ b/assets/search/log/controller.js @@ -0,0 +1,16 @@ +/* global Search */ +module.exports = function (req, res) { + var params = req.allParams(); + if (!params.keyword) { + return res.view("../search/main", {searchType: 'log', searchTerm: ''}); + } + var searchData = { + keyword: params.keyword + }; + + searchData.skip = params.skip || 0; + + Search.logs(searchData, function (results) { + return res.ok(results); + }); +}; \ No newline at end of file diff --git a/assets/search/log/form.ejs b/assets/search/log/form.ejs new file mode 100644 index 00000000..f4c2ae56 --- /dev/null +++ b/assets/search/log/form.ejs @@ -0,0 +1,6 @@ +
+ + +
\ No newline at end of file diff --git a/assets/search/log/result.ejs b/assets/search/log/result.ejs new file mode 100644 index 00000000..5489a280 --- /dev/null +++ b/assets/search/log/result.ejs @@ -0,0 +1,7 @@ +

+ /u/{{result.user}} - {{result.createdAt | date:"yyyy-MM-dd HH:mm:ss' GMT'Z"}} +

+ +

+ {{result.content}} +

\ No newline at end of file diff --git a/assets/search/main.ejs b/assets/search/main.ejs new file mode 100644 index 00000000..58c07116 --- /dev/null +++ b/assets/search/main.ejs @@ -0,0 +1,43 @@ +
+
+
+
+
+

Advanced Search - {{search.getSearch().name}}

+
+
+
+
+ <%- partial(searchType + '/form.ejs') %> +
+
+
+
+ +
+
+
+
+
diff --git a/assets/search/modmail/controller.js b/assets/search/modmail/controller.js new file mode 100644 index 00000000..fd2661a7 --- /dev/null +++ b/assets/search/modmail/controller.js @@ -0,0 +1,16 @@ +/* global Search */ +module.exports = function (req, res) { + var params = req.allParams(); + if (!params.keyword) { + return res.view("../search/main", {searchType: 'modmail', searchTerm: ''}); + } + var searchData = { + keyword: params.keyword + }; + + searchData.skip = params.skip || 0; + + Search.modmails(searchData, function (results) { + return res.ok(results); + }); +}; \ No newline at end of file diff --git a/assets/search/modmail/form.ejs b/assets/search/modmail/form.ejs new file mode 100644 index 00000000..5e106951 --- /dev/null +++ b/assets/search/modmail/form.ejs @@ -0,0 +1,8 @@ +
+ + + +
diff --git a/assets/search/modmail/result.ejs b/assets/search/modmail/result.ejs new file mode 100644 index 00000000..4571ab71 --- /dev/null +++ b/assets/search/modmail/result.ejs @@ -0,0 +1,7 @@ +

+ {{result.subject}} by /u/{{result.author}} +

+

+ {{result.created_utc * 1000 | date:"yyyy-MM-dd HH:mm:ss' GMT'Z"}}
+

+ diff --git a/assets/search/ref/controller.js b/assets/search/ref/controller.js new file mode 100644 index 00000000..b5d5b462 --- /dev/null +++ b/assets/search/ref/controller.js @@ -0,0 +1,24 @@ +/* global Search */ +module.exports = function (req, res) { + var params = req.allParams(); + if (!params.keyword) { + return res.view("../search/main", {searchType: 'ref', searchTerm: ''}); + } + var searchData = { + description: params.keyword + }; + + if (params.user) { + searchData.user = params.user; + } + + if (params.categories) { + searchData.categories = params.categories.split(","); + } + + searchData.skip = params.skip || 0; + + Search.refs(searchData, function (results) { + return res.ok(results); + }); +}; \ No newline at end of file diff --git a/assets/search/ref/form.ejs b/assets/search/ref/form.ejs new file mode 100644 index 00000000..7839c6b1 --- /dev/null +++ b/assets/search/ref/form.ejs @@ -0,0 +1,23 @@ +
+ + + + +
+
+
+ +
+
+ +
+
\ No newline at end of file diff --git a/assets/search/ref/result.ejs b/assets/search/ref/result.ejs new file mode 100644 index 00000000..3553a973 --- /dev/null +++ b/assets/search/ref/result.ejs @@ -0,0 +1,6 @@ +

+ /u/{{result.user}} and /u/{{result.user2}} +

+

+ {{result.description || result.gave + " for " + result.got}} +

\ No newline at end of file diff --git a/assets/search/search.controller.js b/assets/search/search.controller.js new file mode 100644 index 00000000..c244a0b2 --- /dev/null +++ b/assets/search/search.controller.js @@ -0,0 +1,168 @@ +var socket = require("socket.io-client"); +var io = require("sails.io.js")(socket); +var _ = require("lodash"); +var $ = require("jquery"); + +module.exports = function ($scope, $timeout) { + var vm = this; + + vm.user = $scope.user; + vm.potentialSearches = require("./types.js"); + vm.input = { + keyword: "", + category: [], + user: "", + search: "", + uriKeyword: uriKeyword + }; + vm.inProgress = false; + vm.done = false; + vm.results = []; + + vm.getSearch = getSearch; + vm.toggleCategory = toggleCategory; + vm.changeSearchType = changeSearchType; + vm.searchMaybe = searchMaybe; + vm.getMore = getMore; + vm.searchesForUser = searchesForUser; + vm.submit = submit; + vm.linkAddress = linkAddress; + vm.searchedFor = ''; + + //////////////////////////////// + + vm.numberSearched = 0; + var lastSearch; + + $timeout(function () { + if (vm.input.keyword && vm.input.search) { + search(); + } + }, 300); + + if (!vm.input.search) { + vm.input.search = 'ref'; + } + + function linkAddress (result) { + if (vm.input.search === 'ref') { + return '/u/' + result.user; + } else if (vm.input.search === 'user') { + return '/u/' + result._id; + } else if (vm.input.search === 'modmail') { + return 'https://reddit.com/message/messages/' + result.name.substring(3); + } + } + + function searchesForUser() { + return _.filter(vm.potentialSearches, userAllowedSearch); + } + + function userAllowedSearch(search) { + // Either all users can access, or only mods + return vm.user.isMod || !search.modOnly; + } + + function getSearch() { + return _.find(vm.potentialSearches, function (item) { + return item.short === vm.input.search; + }); + } + + function uriKeyword() { + return encodeURIComponent(vm.input.keyword.replace(/\//g, "%2F")); + } + + function changeSearchType () { + //Clear the existing results since they're no longer relevant + vm.results = []; + if (vm.input.keyword) { + search(); + } + } + + function toggleCategory(name) { + var index = vm.input.category.indexOf(name); + + if (index > -1) { + vm.input.category.splice(index, 1); + } else { + vm.input.category.push(name); + } + + if (vm.input.keyword) { + search(); + } + } + + function getMore() { + if (vm.inProgress) { + return; + } + vm.numberSearched += 20; + search(vm.numberSearched); + } + + var searchTimeout; + function searchMaybe() { + if (searchTimeout) { + $timeout.cancel(searchTimeout); + } + + searchTimeout = $timeout(function () { + if (!_.isEqual(lastSearch, vm.input)) { + lastSearch = _.cloneDeep(vm.input); + search(); + } + }, 500); + } + + function submit() { + if (vm.input.keyword) { + window.location.href = "/search/" + vm.getSearch().short + "/" + vm.input.uriKeyword(); + } + } + + function search(skip) { + $('.search-results').show(); + vm.inProgress = true; + if (!vm.input.keyword) { + vm.inProgress = false; + vm.results = []; + vm.numberSearched = 0; + return; + } + + if (!skip) { + vm.numberSearched = 0; + skip = 0; + } + + var url = "/search/" + vm.getSearch().short; + url += "?keyword=" + vm.input.keyword; + if (vm.input.category.length > 0) { + url += "&categories=" + vm.input.category; + } + if (vm.input.user) { + url += "&user=" + vm.input.user; + } + url += "&skip=" + skip; + vm.done = false; + vm.searchedFor = url; + io.socket.get(url, function (data, res) { + if (res.statusCode === 200 && vm.searchedFor === url) { + if (skip) { + vm.results = vm.results.concat(data); + } else { + vm.results = data; + } + vm.done = !data.length; + vm.inProgress = false; + $scope.$apply(); + } else if (res.statusCode !== 200) { + // Some error + console.log(res); + } + }); + } +}; diff --git a/assets/search/search.module.js b/assets/search/search.module.js new file mode 100644 index 00000000..3af7ad95 --- /dev/null +++ b/assets/search/search.module.js @@ -0,0 +1,11 @@ +/** + * + */ + +var angular = require("angular"); +var controller = require("./search.controller.js"); + +var searchModule = angular.module("fapp.search", []); +searchModule.controller("SearchController", controller); + +module.exports = searchModule; \ No newline at end of file diff --git a/assets/search/types.js b/assets/search/types.js new file mode 100644 index 00000000..84b310f0 --- /dev/null +++ b/assets/search/types.js @@ -0,0 +1,8 @@ +var types = [ + {"short": "ref", "name": "References", controller: require("./ref/controller.js"), "modOnly": false}, + {"short": "user", "name": "Users", controller: require("./user/controller.js"), "modOnly": false}, + {"short": "log", "name": "Logs", controller: require("./log/controller.js"), "modOnly": true}, + {"short": "modmail", "name": "Modmails", controller: require("./modmail/controller.js"), "modOnly": true} +]; + +module.exports = types; \ No newline at end of file diff --git a/assets/search/user/controller.js b/assets/search/user/controller.js new file mode 100644 index 00000000..47ae08a5 --- /dev/null +++ b/assets/search/user/controller.js @@ -0,0 +1,16 @@ +/* global Search */ +module.exports = function (req, res) { + var params = req.allParams(); + if (!params.keyword) { + return res.view("../search/main", {searchType: 'user', searchTerm: ''}); + } + var searchData = { + keyword: params.keyword + }; + + searchData.skip = params.skip || 0; + + Search.users(searchData, function (results) { + return res.ok(results); + }); +}; \ No newline at end of file diff --git a/assets/search/user/form.ejs b/assets/search/user/form.ejs new file mode 100644 index 00000000..f4c2ae56 --- /dev/null +++ b/assets/search/user/form.ejs @@ -0,0 +1,6 @@ +
+ + +
\ No newline at end of file diff --git a/assets/search/user/result.ejs b/assets/search/user/result.ejs new file mode 100644 index 00000000..22ead2b5 --- /dev/null +++ b/assets/search/user/result.ejs @@ -0,0 +1,8 @@ +

+ /u/{{result._id}} +

+ +

+ /r/PokemonTrades: {{result.flair.ptrades.flair_text}}
+ /r/SVExchange: {{result.flair.svex.flair_text}} +

\ No newline at end of file diff --git a/assets/js/sharedClientFunctions.js b/assets/sharedClientFunctions.js similarity index 74% rename from assets/js/sharedClientFunctions.js rename to assets/sharedClientFunctions.js index 08d155da..34558763 100644 --- a/assets/js/sharedClientFunctions.js +++ b/assets/sharedClientFunctions.js @@ -1,11 +1,10 @@ -var socket = require("socket.io-client"); -var io = require("sails.io.js")(socket); -var referenceService = require('../../api/services/References.js'); -var flairService = require('../../api/services/Flairs.js'); - +var referenceService = require('../api/services/References.js'); +var flairService = require('../api/services/Flairs.js'); +var _ = require('lodash'); module.exports = { - addRepeats: function ($scope) { - var current_user = window.location.pathname.substring(0, 3) === '/u/' ? 'refUser' : 'user'; + addRepeats: function ($scope, io) { + var pathToRefs = 'refUser.references', + pathToApps = 'refUser.apps'; $scope.editRef = function () { var url = "/reference/edit"; $scope.editRefError = $scope.validateRef($scope.selectedRef); @@ -17,10 +16,10 @@ module.exports = { $scope.indexSpin.editRef = false; if (res.statusCode === 200) { $scope.indexOk.editRef = true; - var index = $scope.user.references.findIndex(function (searchRef) { + var index = $scope.refUser.references.findIndex(function (searchRef) { return searchRef.id === $scope.selectedRef.id; }); - $scope.user.references[index] = $scope.selectedRef; + $scope.refUser.references[index] = $scope.selectedRef; } else { $scope.editRefError = "There was an issue."; console.log(res); @@ -85,7 +84,7 @@ module.exports = { var url = "/reference/delete"; io.socket.post(url, {refId: id}, function (data, res) { if (res.statusCode === 200) { - $scope[current_user].references = $scope[current_user].references.filter(function (ref) { + $scope.refUser.references = $scope.refUser.references.filter(function (ref) { return ref.id !== id; }); $scope.$apply(); @@ -107,6 +106,7 @@ module.exports = { $scope.isGiveaway = referenceService.isGiveaway; $scope.isEggCheck = referenceService.isEggCheck; $scope.isMisc = referenceService.isMisc; + $scope.isApprovable = referenceService.isApprovable; $scope.isApproved = referenceService.isApproved; $scope.getRedditUser = referenceService.getRedditUser; $scope.formattedName = flairService.formattedName; @@ -115,35 +115,52 @@ module.exports = { $scope.inSVExchangeHatcher = flairService.inSVExchangeHatcher; $scope.inSVExchangeGiver = flairService.inSVExchangeGiver; $scope.getFlair = flairService.getFlair; + $scope.flairCheck = flairService.flairCheck; $scope.userHasFlair = function (flair) { return flairService.userHasFlair($scope.user, flair); }; $scope.numberOfTrades = function () { - return referenceService.numberOfTrades($scope[current_user]); + return referenceService.numberOfTrades(_.get($scope, pathToRefs)); }; $scope.numberOfPokemonGivenAway = function () { - return referenceService.numberOfPokemonGivenAway($scope[current_user]); + return referenceService.numberOfPokemonGivenAway(_.get($scope, pathToRefs)); }; $scope.numberOfEggsGivenAway = function () { - return referenceService.numberOfEggsGivenAway($scope[current_user]); + return referenceService.numberOfEggsGivenAway(_.get($scope, pathToRefs)); }; $scope.numberOfEggChecks = function () { - return referenceService.numberOfEggChecks($scope[current_user]); + return referenceService.numberOfEggChecks(_.get($scope, pathToRefs)); }; $scope.numberOfApprovedEggChecks = function () { - return referenceService.numberOfApprovedEggChecks($scope[current_user]); + return referenceService.numberOfApprovedEggChecks(_.get($scope, pathToRefs)); }; $scope.getFlairTextForSVEx = function () { - return flairService.getFlairTextForSVEx($scope[current_user]); + return flairService.getFlairTextForSVEx(_.get($scope, pathToRefs)); }; $scope.applied = function (flair) { - return flairService.applied($scope.user, flair); + return flairService.applied(_.get($scope, pathToApps), flair); }; $scope.canUserApply = function (applicationFlair) { - return flairService.canUserApply($scope.user, applicationFlair || $scope.selectedFlair, $scope.flairs); + if (!$scope.refUser || !$scope.user || $scope.user.name !== $scope.refUser.name) { + return false; + } + return flairService.canUserApply( + $scope.refUser.references, + applicationFlair, + flairService.getUserFlairs($scope.user, $scope.flairs) + ) && !$scope.applied(applicationFlair, $scope.flairs); }; $scope.formattedRequirements = function (flair) { return flairService.formattedRequirements(flair, $scope.flairs); }; + $scope.clickRefLink = function (ref) { + if ($scope.user.isMod) { + io.socket.post('/flair/app/refreshClaim', ref, function (data, res) { + if (res.statusCode !== 200) { + console.log('Error ' + res.statusCode + ': Could not send link data to server.'); + } + }); + } + }; } -}; +}; \ No newline at end of file diff --git a/assets/styles/importer.less b/assets/styles/importer.less index 4a3b80bc..70ee8428 100644 --- a/assets/styles/importer.less +++ b/assets/styles/importer.less @@ -79,6 +79,10 @@ table h1, table h2, table h3 { max-width:100px; } +img { + max-width: 100%; +} + .input-group-addon { padding: 0px 12px !important; } @@ -184,6 +188,18 @@ td h3 { color: green; } +[ng\:cloak], [ng-cloak], .ng-cloak { + display: none; +} + +.center-block{ + float: none; +} + .navbar .input-group .form-control{ width: auto !important; -} \ No newline at end of file +} + +.plus-minus{ + font-family: Courier; +} diff --git a/assets/tools/darkmode.png b/assets/tools/darkmode.png new file mode 100644 index 00000000..e8ac9c97 Binary files /dev/null and b/assets/tools/darkmode.png differ diff --git a/assets/tools/greasemonkey.png b/assets/tools/greasemonkey.png new file mode 100755 index 00000000..c76aebc9 Binary files /dev/null and b/assets/tools/greasemonkey.png differ diff --git a/assets/tools/tools.ejs b/assets/tools/tools.ejs new file mode 100644 index 00000000..f4936b79 --- /dev/null +++ b/assets/tools/tools.ejs @@ -0,0 +1,34 @@ +
+
+ +
+ +

Tools

+ +
+

Subreddit Greasemonkey Script

+ + + + +
+ +
+

FlairHQ Dark Mode

+ + + + +
+ +
+
+
diff --git a/assets/common/tooltipView.html b/assets/tooltip/label.view.html similarity index 100% rename from assets/common/tooltipView.html rename to assets/tooltip/label.view.html diff --git a/assets/common/tooltipModule.js b/assets/tooltip/tooltip.module.js similarity index 81% rename from assets/common/tooltipModule.js rename to assets/tooltip/tooltip.module.js index 6d22459c..56f780b0 100644 --- a/assets/common/tooltipModule.js +++ b/assets/tooltip/tooltip.module.js @@ -9,7 +9,13 @@ ng.module("tooltipModule", []).directive("ngTooltip", function () { title: '@title', label: '@label' }, - templateUrl: '/common/tooltipView.html', + templateUrl: function (tElement, tAttrs) { + if (tAttrs.unlabeled) { + return '/tooltip/tooltip.view.html'; + } else { + return '/tooltip/label.view.html'; + } + }, transclude: true, link: function (scope, element) { var thisElement = $(element[0]).find('[data-toggle=tooltip]'); diff --git a/assets/common/genericTooltipView.html b/assets/tooltip/tooltip.view.html similarity index 100% rename from assets/common/genericTooltipView.html rename to assets/tooltip/tooltip.view.html diff --git a/assets/js/userCtrl.js b/assets/userCtrl.js similarity index 65% rename from assets/js/userCtrl.js rename to assets/userCtrl.js index 8fa36915..be015e9d 100644 --- a/assets/js/userCtrl.js +++ b/assets/userCtrl.js @@ -1,20 +1,11 @@ -var socket = require("socket.io-client"); -var io = require("sails.io.js")(socket); var regex = require("regex"); var _ = require("lodash"); var $ = require("jquery"); -var sharedService = require("./sharedClientFunctions.js"); +var shared = require('./sharedClientFunctions.js'); -module.exports = function ($scope, $filter, $location, UserFactory) { +module.exports = function ($scope, $location, io) { + shared.addRepeats($scope, io); $scope.regex = regex; - $scope.scope = $scope; - $scope.user = undefined; - $scope.$watch('user', function (newU, oldU) { - if (newU !== oldU) { - UserFactory.setUser(newU); - } - }); - $scope.flairs = {}; $scope.selectedFlair = undefined; $scope.loaded = false; $scope.userok = {}; @@ -65,8 +56,21 @@ module.exports = function ($scope, $filter, $location, UserFactory) { {name: "misc", display: "Miscellaneous"} ]; - $scope.onSearchPage = $location.absUrl().indexOf('search') === -1; - sharedService.addRepeats($scope); + $scope.onSearchPage = $location.absUrl().indexOf('search') !== -1; + $scope.onIndexPage = location.pathname === '/'; + + if (window.location.hash === "#/comments") { + $('#tabList li:eq(1) a').tab('show'); + } else if (window.location.hash === "#/info") { + $('#tabList li:eq(2) a').tab('show'); + } else if (window.location.hash === "#/modEdit") { + $('#tabList li:eq(3) a').tab('show'); + } else if (window.location.hash === "#/privacypolicy") { + $('#privacypolicy').modal('show'); + } else if (window.location.hash === "#/flairtext") { + $('#flairText').modal('show'); + } + $scope.applyFlair = function () { $scope.errors.flairApp = ""; $scope.userok.applyFlair = false; @@ -75,13 +79,13 @@ module.exports = function ($scope, $filter, $location, UserFactory) { if ($scope.selectedFlair && $scope.canUserApply(flair)) { io.socket.post("/flair/apply", {flair: $scope.selectedFlair, sub: flair.sub}, function (data, res) { if (res.statusCode === 200) { - $scope.user.apps.push(data); + $scope.refUser.apps.push(data); $scope.selectedFlair = undefined; $scope.userok.applyFlair = true; $scope.userspin.applyFlair = false; $scope.$apply(); } else { - $scope.errors.flairApp = "Something unexpected happened."; + $scope.errors.flairApp = res.body.error || "Something unexpected happened."; $scope.userspin.applyFlair = false; $scope.$apply(); } @@ -93,115 +97,12 @@ module.exports = function ($scope, $filter, $location, UserFactory) { } }; - $scope.setselectedFlair = function (id, bool) { + $scope.setSelectedFlair = function (id, bool) { if (bool) { $scope.selectedFlair = id; } }; - io.socket.get("/user/mine", function (data, res) { - if (res.statusCode === 200) { - $scope.user = data; - if (!$scope.user.friendCodes) { - $scope.user.friendCodes = [""]; - } - if (!$scope.user.games.length) { - $scope.user.games = [{tsv: "", ign: ""}]; - } - - $scope.getReferences(); - $scope.$apply(); - } else { - $scope.loaded = true; - $scope.$apply(); - if (window.location.hash === "#/comments") { - $('#tabList').find('li:eq(1) a').tab('show'); - } else if (window.location.hash === "#/info") { - $('#tabList').find('li:eq(2) a').tab('show'); - } else if (window.location.hash === "#/modEdit") { - $('#tabList').find('li:eq(3) a').tab('show'); - } else if (window.location.hash === "#/privacypolicy") { - $('#privacypolicy').modal('show'); - } else if (window.location.hash === "#/flairtext") { - $('#flairText').modal('show'); - } - } - }); - - $scope.getReferences = function () { - if ($scope.user) { - io.socket.get("/user/get/" + $scope.user.name, function (data, res) { - if (res.statusCode === 200) { - _.merge($scope.user, data); - if ($scope.user.flair && $scope.user.flair.ptrades) { - $scope.user.flairFriendCodes = []; - $scope.user.flairGames = [{tsv: "", ign: ""}]; - var trades = $scope.user.flair.ptrades.flair_text || ""; - var sv = $scope.user.flair.svex.flair_text || ""; - - if (trades && sv) { - - - var fcs = _.merge(trades.match(regex.fc) || [], sv.match(regex.fc) || []); - var games = _.merge(trades.match(regex.game) || [], sv.match(regex.game) || []); - var igns = _.merge(trades.match(regex.ign) || [], sv.match(regex.ign) || []); - var tsvs = sv.match(regex.tsv) || []; - - - for (var k = 0; k < fcs.length; k++) { - $scope.user.flairFriendCodes.push(fcs[k]); - } - - $scope.user.flairGames = []; - for (var j = 0; j < games.length || j < igns.length || j < tsvs.length; j++) { - $scope.user.flairGames.push({ - game: j < games.length ? games[j].replace(/\(/g, "") - .replace(/\)/g, "") - .replace(/,/g, "") : "", - ign: j < igns.length ? igns[j].replace(/\d \|\| /g, "") - .replace(/ \(/g, "") - .replace(/ \|/g, "") - .replace(/\), /g, "") - .replace(/,/g, "") : "", - tsv: j < tsvs.length ? tsvs[j].replace(/\|\| /g, "") - .replace(/, /g, "") : "" - }); - } - } - } - if (!$scope.user.flairFriendCodes || !$scope.user.flairFriendCodes.length) { - $scope.user.flairFriendCodes = [""]; - } - if (!$scope.user.flairGames || !$scope.user.flairGames.length) { - $scope.user.flairGames = [{tsv: "", ign: "", game: ""}]; - } - if (!$scope.user.friendCodes || !$scope.user.friendCodes.length) { - $scope.user.friendCodes = [""]; - } - if (!$scope.user.games || !$scope.user.games.length) { - $scope.user.games = [{tsv: "", ign: ""}]; - } - } - $scope.$apply(); - $scope.loaded = true; - $scope.$apply(); - if (window.location.hash === "#/comments") { - $('#tabList li:eq(1) a').tab('show'); - } else if (window.location.hash === "#/info") { - $('#tabList li:eq(2) a').tab('show'); - } else if (window.location.hash === "#/modEdit") { - $('#tabList li:eq(3) a').tab('show'); - } else if (window.location.hash === "#/privacypolicy") { - $('#privacypolicy').modal('show'); - } else if (window.location.hash === "#/flairtext") { - $('#flairText').modal('show'); - } - }); - } else { - window.setTimeout($scope.getReferences, 1000); - } - }; - $scope.addFc = function () { $scope.user.friendCodes.push(""); }; @@ -291,6 +192,10 @@ module.exports = function ($scope, $filter, $location, UserFactory) { var mergedGames = {}, text = ""; for (var j = 0; j < games.length; j++) { + // Allow IGNs without games, and games without IGNs (e.g. if IGN was deleted for the character limit), but ignore empty-string IGNs without games + if (!games[j].ign && !games[j].game) { + continue; + } if (games[j] && mergedGames[games[j].ign]) { mergedGames[games[j].ign].push(games[j].game); } else if (games[j]) { @@ -369,13 +274,13 @@ module.exports = function ($scope, $filter, $location, UserFactory) { } if (svex.length > 64 || ptrades.length > 64) { - return {correct: false, error: "Your flair is too long, maximum is 64 characters, please delete something."}; + return {correct: false, error: "Your flair is too long; Reddit's maximum is 64 characters. Please delete something."}; } for (var i = 0; i < $scope.user.flairFriendCodes.length; i++) { var fc = $scope.user.flairFriendCodes[i]; - if (!fc || fc === "" || !fc.match(regex.fcSingle)) { - return {correct: false, error: "Please fill in all friend codes and IGNs."}; + if (!fc || !fc.match(regex.fcSingle)) { + return {correct: false, error: "Please fill in all friend codes and in-game names."}; } } @@ -384,13 +289,20 @@ module.exports = function ($scope, $filter, $location, UserFactory) { var game = $scope.user.flairGames[j]; if (game.ign) { hasIGN = true; + if (game.ign.length > 12) { + return {correct: false, error: 'In-game names have a maximum length of 12 characters.'}; + } + var illegal_match = game.ign.match(/\(|\)|\||,/); + if (illegal_match) { + return {correct: false, error: 'Your in-game name contains an illegal character: ' + illegal_match}; + } } if (game.tsv >= 4096) { return {correct: false, error: "Invalid TSV, they should be between 0 and 4095."}; } } if (!hasIGN) { - return {correct: false, error: "Please fill in all friend codes and IGNs."}; + return {correct: false, error: "Please fill in all friend codes and in-game names."}; } return {correct: true}; }; @@ -423,25 +335,6 @@ module.exports = function ($scope, $filter, $location, UserFactory) { }; - $scope.getFlairs = function () { - var url = "/flair/all"; - - io.socket.get(url, function (data, res) { - if (res.statusCode === 200) { - $scope.flairs = data; - if (data.length === 0) { - $scope.flairs[0] = { - name: "", - trades: "", - shinyevents: "", - eggs: "", - sub: "" - }; - } - } - }); - }; - $scope.addFlair = function () { $scope.flairs.push({}); }; @@ -473,4 +366,34 @@ module.exports = function ($scope, $filter, $location, UserFactory) { $scope.deleteFlair = function (index) { $scope.flairs.splice(index, 1); }; + + $scope.init = function (params) { + $scope = _.assign($scope, params); + if ($scope.user) { + try { + var parsed = $scope.flairCheck($scope.user.flair.ptrades.flair_text, $scope.user.flair.svex.flair_text); + $scope.user.flairFriendCodes = parsed.fcs; + $scope.user.flairGames = parsed.games; + for (var i = 0; i < parsed.games.length; i++) { + $scope.user.flairGames[i].tsv = parsed.tsvs[i]; + } + $scope.user.friendCodes = $scope.user.flairFriendCodes; + if (!$scope.user.games.length) { + $scope.user.games = $scope.user.flairGames; + } + } catch (err) { + $scope.user.flairFriendCodes = [""]; + $scope.user.flairGames = [{tsv: "", ign: "", game: ""}]; + $scope.user.games = $scope.user.flairGames; + $scope.user.friendCodes = [""]; + } + } + }; + $(document).ready(function () { + $scope.loaded = true; + $('.plus-minus').parent().parent().on('click', function () { + var plusminus = $(this).find('.plus-minus'); + plusminus.text(plusminus.text() === '+' ? '-' : '+'); + }); + }); }; diff --git a/views/403.ejs b/assets/views/403.ejs similarity index 58% rename from views/403.ejs rename to assets/views/403.ejs index 788e5cd3..48dad28a 100644 --- a/views/403.ejs +++ b/assets/views/403.ejs @@ -8,6 +8,11 @@
+ <% if (typeof error !== 'undefined') { %> +

+            <%- error %>
+          
+ <% } %>
diff --git a/views/404.ejs b/assets/views/404.ejs similarity index 100% rename from views/404.ejs rename to assets/views/404.ejs diff --git a/views/500.ejs b/assets/views/500.ejs similarity index 100% rename from views/500.ejs rename to assets/views/500.ejs diff --git a/views/auth/index.ejs b/assets/views/auth/index.ejs similarity index 56% rename from views/auth/index.ejs rename to assets/views/auth/index.ejs index 6030a735..1a5bd82f 100644 --- a/views/auth/index.ejs +++ b/assets/views/auth/index.ejs @@ -4,10 +4,9 @@

Welcome to FlairHQ

-

This is a site that helps with the application of flairs on Reddit

- -

To get started, login with Reddit and then start adding your trades

- +

This is a site that helps with the application of flairs on Reddit.

+

To get started, + login with Reddit and then start adding your trades.

diff --git a/views/home/applist.ejs b/assets/views/home/applist.ejs similarity index 79% rename from views/home/applist.ejs rename to assets/views/home/applist.ejs index 16cca2d1..1468e5b6 100644 --- a/views/home/applist.ejs +++ b/assets/views/home/applist.ejs @@ -11,23 +11,26 @@ @@ -47,7 +47,7 @@ + href="#" ng-click="setSelectedFlair(flair.name, canUserApply(flair))" title={{formattedRequirements(flair.name)}}> {{formattedName(flair.name)}} (Pending) @@ -62,9 +62,12 @@ @@ -50,7 +50,7 @@
- +
@@ -58,11 +58,11 @@
- + - +
diff --git a/assets/views/home/header.ejs b/assets/views/home/header.ejs new file mode 100644 index 00000000..56321d0e --- /dev/null +++ b/assets/views/home/header.ejs @@ -0,0 +1,72 @@ +<%- partial('editProfile.ejs') %> +<%- partial('../privacyPolicy.ejs') %> +<%- partial('flairMod.ejs') %> +<%- partial('flairApply.ejs') %> +<%- partial('flairText.ejs') %> +<%- partial('applist.ejs') %> + + diff --git a/views/home/index.ejs b/assets/views/home/index.ejs similarity index 89% rename from views/home/index.ejs rename to assets/views/home/index.ejs index 0579165b..8278f213 100644 --- a/views/home/index.ejs +++ b/assets/views/home/index.ejs @@ -15,8 +15,8 @@ or /r/svexchange how to.

- Public Profile - Set flair text + Public Profile + Set Flair Text Apply for Flair

@@ -25,7 +25,7 @@
- + {{thingamy.refUrl}}{{thingamy.type}} @@ -53,37 +53,37 @@ - + - + - + - + - + - + - +
@@ -104,7 +104,7 @@
- <%- partial('references.ejs', {theUser: "user", refPage: false}) %> + <%- partial('references.ejs', {refPage: false}) %> <%- partial('editreference.ejs') %> diff --git a/views/home/info.ejs b/assets/views/home/info.ejs similarity index 77% rename from views/home/info.ejs rename to assets/views/home/info.ejs index 5ec01669..39c75c50 100644 --- a/views/home/info.ejs +++ b/assets/views/home/info.ejs @@ -56,13 +56,24 @@

The site was made by /u/YaManicKill and any bugs you find (or features you would like added) can be filed - on GitHub. + on GitHub. If you have any issues about the rules, or if you've been banned, you just don't like the sub or you would like to just moan about life, feel free to contact the mods. They aren't that scary. Honestly.

+ +

Donations

+

+ The site costs money to run, and because of this, we have opened up donations to allow people to help us fund it. If you wish to help, please do so. +

+ + + + +
+

diff --git a/views/home/modEdit.ejs b/assets/views/home/modEdit.ejs similarity index 100% rename from views/home/modEdit.ejs rename to assets/views/home/modEdit.ejs diff --git a/views/home/profileInfo.ejs b/assets/views/home/profileInfo.ejs similarity index 100% rename from views/home/profileInfo.ejs rename to assets/views/home/profileInfo.ejs diff --git a/views/home/reference.ejs b/assets/views/home/reference.ejs similarity index 84% rename from views/home/reference.ejs rename to assets/views/home/reference.ejs index 3727180f..dec11cfe 100644 --- a/views/home/reference.ejs +++ b/assets/views/home/reference.ejs @@ -5,17 +5,17 @@

- {{refUser.name}} + <%= refUser.name %>

- {{refUser.flair.ptrades.flair_text}} + <%= refUser.flair.ptrades.flair_text || '' %>
- {{refUser.flair.svex.flair_text}} + <%= refUser.flair.svex.flair_text || '' %>
@@ -35,17 +35,14 @@
- <%- partial('references.ejs', {theUser: "refUser", refPage: true}) %> + <%- partial('references.ejs', {refPage: true}) %>
-

Leave a comment

-

Sometimes you might want to leave a comment for a user.

-

If you traded with them, let the world know how good they - were at that. Maybe they hatched your egg for you and you want - to tell the world how nice they are. Whatever you want.

+

Leave a comment below

+

Leave feedback on this user by typing your comment below. reddit markdown is supported.

diff --git a/assets/views/home/references.ejs b/assets/views/home/references.ejs new file mode 100644 index 00000000..0a5eb277 --- /dev/null +++ b/assets/views/home/references.ejs @@ -0,0 +1,207 @@ +
+ + + + + + + + + + + + + + + + <%- partial('row.ejs') %> + + + + + + + + + + + + <%- partial('row.ejs') %> + + + + + + + + + + + + <%- partial('row.ejs') %> + + + + + + + + + + + <%- partial('row.ejs') %> + + + + + + + + + + + + <%- partial('row.ejs') %> + + + + + + + + + + + + <%- partial('row.ejs') %> + + + + + + + + + + + + <%- partial('row.ejs') %> + + + + + + + + + + + + <%- partial('row.ejs') %> + + + + + + + + + + + + <%- partial('row.ejs') %> + + + +

Trades ({{numberOfTrades()}})

Approved

[+] Events ({{refUser.references.filter(isEvent).length}}) + + + {{refUser.references.filter(isEvent).filter(isApproved).length}} +
[+] Shinies ({{refUser.references.filter(isShiny).length}}) + + + {{refUser.references.filter(isShiny).filter(isApproved).length}} +
[+] Competitive / Casual ({{refUser.references.filter(isCasual).length}}) + + + {{refUser.references.filter(isCasual).filter(isApproved).length}} +
[+]

Bank trades ({{refUser.references.filter(isBank).length}})

[+]

Egg Hatches ({{refUser.references.filter(isEgg).length}})

+ + + {{refUser.references.filter(isEgg).filter(isApproved).length}} +
[+]

Giveaways/Contests ({{numberOfPokemonGivenAway(refUser)}} Pokémon given, {{numberOfEggsGivenAway(refUser)}} eggs given)

+ + +
[+]

Egg/TSV Checks ({{refUser.references.filter(isEggCheck).length}} threads, {{numberOfEggChecks(refUser)}} given)

+ + + {{numberOfApprovedEggChecks(refUser)}} +
[+]

Free Tradeback/Free Redemption ({{refUser.references.filter(isInvolvement).length}})

+ + + {{refUser.references.filter(isInvolvement).filter(isApproved).length}} +
[+]

Misc ({{refUser.references.filter(isMisc).length}})

+ + + +
+ + <%- partial('editreference.ejs') %> + <%- partial('viewreference.ejs') %> +
\ No newline at end of file diff --git a/assets/views/home/row.ejs b/assets/views/home/row.ejs new file mode 100644 index 00000000..29a6d06d --- /dev/null +++ b/assets/views/home/row.ejs @@ -0,0 +1,62 @@ + + + {{$index + 1}}. + + + + + + {{reference.gave}} for {{reference.got}} + + + + + + {{reference.description || reference.descrip}} + + + + + {{reference.description || reference.descrip}} + {{reference.number ? "(" + reference.number + " checked)" : ""}} + + + + + + {{reference.description || reference.descrip}} (Sub: {{reference.url.split("/")[4]}}{{reference.number ? ", " + reference.number + " given" : ""}}) + + + + + + {{getRedditUser(reference.user2)}} + + + + + + + + + * + + + + + + + + + + + + + + + + + diff --git a/views/home/viewreference.ejs b/assets/views/home/viewreference.ejs similarity index 100% rename from views/home/viewreference.ejs rename to assets/views/home/viewreference.ejs diff --git a/views/layout.ejs b/assets/views/layout.ejs similarity index 61% rename from views/layout.ejs rename to assets/views/layout.ejs index f72a4353..19f229b7 100644 --- a/views/layout.ejs +++ b/assets/views/layout.ejs @@ -7,7 +7,7 @@ - + @@ -25,7 +25,13 @@ - +
<%- partial ('home/header.ejs') %>
@@ -37,18 +43,19 @@ -
-
+
- @@ -60,7 +67,8 @@
- + + <% if(sails.config.environment == 'development' ){ %> <% } %> diff --git a/views/privacyPolicy.ejs b/assets/views/privacyPolicy.ejs similarity index 91% rename from views/privacyPolicy.ejs rename to assets/views/privacyPolicy.ejs index 92e9ec17..676d8ea4 100644 --- a/views/privacyPolicy.ejs +++ b/assets/views/privacyPolicy.ejs @@ -27,10 +27,6 @@

We require this to tie you to a reddit account, this is necessary for any Reddit oauth usage.

-
  • - Maintain this access indefinitely (or until manually revoked) -

    We require this so you don't have to log in every time you access the site.

    -
  • Google Analytics

    diff --git a/config/blueprints.js b/config/blueprints.js index 83dba3d7..fdac1e4e 100644 --- a/config/blueprints.js +++ b/config/blueprints.js @@ -44,7 +44,7 @@ module.exports.blueprints = { * * ***************************************************************************/ - // actions: true, + actions: false, /*************************************************************************** * * @@ -68,7 +68,7 @@ module.exports.blueprints = { * * ***************************************************************************/ - // rest: true, + rest: false, /*************************************************************************** * * @@ -82,7 +82,7 @@ module.exports.blueprints = { * * ***************************************************************************/ - // shortcuts: true, + shortcuts: false, /*************************************************************************** * * diff --git a/config/csrf.js b/config/csrf.js index 22e419a7..c728e60b 100644 --- a/config/csrf.js +++ b/config/csrf.js @@ -48,7 +48,7 @@ * * ****************************************************************************/ -// module.exports.csrf = false; +module.exports.csrf = true; /**************************************************************************** * * diff --git a/config/express.js b/config/express.js index 4fb63b81..32d98319 100644 --- a/config/express.js +++ b/config/express.js @@ -2,46 +2,33 @@ var passport = require('passport'), RedditStrategy = require('passport-reddit').Strategy; var verifyHandler = function (adminToken, token, tokenSecret, profile, done) { - process.nextTick(function() { - User.findOne({id: profile.name}, function(err, user) { - Reddit.getBothFlairs(adminToken, profile.name).then(function (flairs) { - if (user) { - if (user.banned) { - return done("You are banned from FAPP", user); - } - user.redToken = tokenSecret; - user.flair = {ptrades: flairs[0], svex: flairs[1]}; - user.save(function (err) { - if (err) { - console.log(err); - } - return done(null, user); - }); - } else { - var data = { - redToken: tokenSecret, - provider: profile.provider, - name: profile.name, - flair: {ptrades: flairs[0], svex: flairs[1]} - }; - - if (profile.emails && profile.emails[0] && profile.emails[0].value) { - data.email = profile.emails[0].value; - } - if (profile.name && profile.name.givenName) { - data.firstname = profile.name.givenName; - } - if (profile.name && profile.name.familyName) { - data.lastname = profile.name.familyName; + User.findOne({id: profile.name}, function(err, user) { + Reddit.getBothFlairs(adminToken, profile.name).then(function (flairs) { + if (user) { + if (user.banned) { + return done("banned", user); + } + user.redToken = tokenSecret; + user.flair = {ptrades: flairs[0], svex: flairs[1]}; + user.save(function (err) { + if (err) { + console.log(err); } + return done(null, user); + }); + } else { + var data = { + redToken: tokenSecret, + name: profile.name, + flair: {ptrades: flairs[0], svex: flairs[1]} + }; - User.create(data, function(err, user) { - return done(err, user); - }); - } - }, function (error) { - return res.serverError(error); - }); + User.create(data, function(err, user) { + return done(err, user); + }); + } + }, function (error) { + return res.serverError(error); }); }); }; diff --git a/config/local.example.js b/config/local.example.js index f91afcb3..1eb8a31d 100644 --- a/config/local.example.js +++ b/config/local.example.js @@ -7,7 +7,8 @@ module.exports = { clientID: "CLIENT ID GOES HERE", clientIDSecret: "SECRET ID GOES HERE", redirectURL: "http://localhost:1337/auth/reddit/callback", - adminRefreshToken: "ADMIN REFRESH TOKEN GOES HERE" + adminRefreshToken: "ADMIN REFRESH TOKEN GOES HERE", + userAgent: 'FlairHQ development version by /u/DEVELOPERS_USERNAME || hq.porygon.co/info || v' + require('../package.json').version }, connections: { "default": "mongo", @@ -26,5 +27,10 @@ module.exports = { port: 27017, db: 'fapp', collection: 'sessions' + }, + + // This should only be used for development + log: { + level: "verbose" } }; diff --git a/config/policies.js b/config/policies.js index 2ed19fe8..b66fbf1b 100644 --- a/config/policies.js +++ b/config/policies.js @@ -39,7 +39,8 @@ module.exports.policies = { index: user, reference: anyone, search: user, - info: anyone + info: anyone, + tools: anyone }, ReferenceController: { @@ -55,8 +56,10 @@ module.exports.policies = { SearchController: { '*': mod, - refs: user, - refView: user + ref: user, + refView: user, + user: user, + userView: user }, UserController: { diff --git a/config/routes.js b/config/routes.js index 1b6f60a4..e00cde11 100644 --- a/config/routes.js +++ b/config/routes.js @@ -1,3 +1,4 @@ +"use strict"; /** * Route Mappings * (sails.config.routes) @@ -33,7 +34,8 @@ module.exports.routes = { ***************************************************************************/ '/' : { - controller : 'home' + controller : 'home', + action: 'index' }, '/u/:user' : { @@ -41,11 +43,6 @@ module.exports.routes = { action: 'reference' }, - '/user/mine' : { - controller : 'user', - action : 'mine' - }, - '/user/get/:name' : { controller : 'user', action : 'get' @@ -71,6 +68,11 @@ module.exports.routes = { action : 'logout' }, + '/auth/reddit': { + controller: 'auth', + action: 'reddit' + }, + '/auth/reddit/callback' : { controller : 'auth', action : 'callback' @@ -91,11 +93,6 @@ module.exports.routes = { action : 'approve' }, - '/reference/all' : { - controller : 'reference', - action : 'all' - }, - '/reference/approve/all' : { controller : 'reference', action : 'approveAll' @@ -146,6 +143,16 @@ module.exports.routes = { action : 'denyApp' }, + '/flair/app/refreshClaim': { + controller: 'flair', + action: 'refreshClaim' + }, + + '/flair/setText': { + controller: 'flair', + action: 'setText' + }, + '/user/edit' : { controller : 'user', action : 'edit' @@ -185,34 +192,38 @@ module.exports.routes = { action : 'banlist' }, + '/event/get': { + controller: 'event', + action: 'get' + }, + '/info' : { controller : 'home', action : 'info' }, - '/version' : { + '/tools' : { controller : 'home', - action : 'version' - }, - - '/search/s/:searchterm' : { - controller : 'search', - action : 'refView' - }, - - '/search/l/:searchterm' : { - controller : 'search', - action : 'logView' + action : 'tools' }, - '/search/ref' : { - controller : 'search', - action : 'refs' - }, - - '/search/log' : { - controller : 'search', - action : 'log' + '/version' : { + controller : 'home', + action : 'version' } - }; + +var searchTypes = require("../assets/search/types.js"); + +for (let i = 0; i < searchTypes.length; i++) { + // Programatically add the routes for searches + let type = searchTypes[i]; + module.exports.routes['/search/' + type.short] = { + controller: 'search', + action: type.short + }; + module.exports.routes['/search/' + type.short + "/:searchterm"] = { + controller: 'search', + action: type.short + "View" + }; +} \ No newline at end of file diff --git a/config/schedule.js b/config/schedule.js new file mode 100644 index 00000000..c6f45c46 --- /dev/null +++ b/config/schedule.js @@ -0,0 +1,18 @@ +module.exports.schedule = { + sailsInContext: true, + tasks: { + updateModmail: { + cron : "0 8 * * *", + task : function (context, sails) { + console.log('[Daily task]: Updating modmail archives...'); + Promise.all([Modmails.updateArchive('pokemontrades'), Modmails.updateArchive('SVExchange')]).then(function (results) { + console.log('[Daily task]: Finished updating modmail archives.'); + }, function (error) { + console.log('There was an issue updating the modmail archives.'); + console.log(error); + }); + }, + context : {} + } + } +}; diff --git a/config/views.js b/config/views.js index b1cc11e0..aade7f07 100644 --- a/config/views.js +++ b/config/views.js @@ -57,7 +57,12 @@ module.exports.views = { * * ****************************************************************************/ - layout: 'layout' + layout: 'layout', + + locals: { + allTypes: require('../assets/search/types.js'), + flairService: require('../api/services/Flairs.js') + } /**************************************************************************** * * diff --git a/package.json b/package.json index 9966f17c..494c345d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "FlairHQ", - "version": "2.2.3", + "version": "2.3.0", "description": "A project to allow easy adding of flair applications for subreddits (focusing initially on /r/pokemontrades) and easy moderation for moderators.", "scripts": { "start": "node ./node_modules/sails/bin/sails.js lift" @@ -16,7 +16,6 @@ "dependencies": { "angular": "1.3.x", "angular-bootstrap-npm": "^0.14.2", - "angular-md": "^1.0.0", "angular-spinner": "^0.7.0", "angular-ui-mask": "~1.4.3", "async": "~1.4.2", @@ -39,6 +38,7 @@ "grunt-contrib-uglify": "~0.9.2", "grunt-contrib-watch": "~0.6.1", "grunt-eslint": "^17.3.1", + "grunt-focus": "^0.1.1", "grunt-mocha-test": "^0.12.7", "grunt-requirejs": "~0.4.2", "grunt-sails-linker": "~0.10.1", @@ -46,8 +46,9 @@ "include-all": "~0.1.3", "jquery-browserify": "^1.8.1", "lodash": "^3.10.1", - "marked": "^0.3.5", + "mocha": "^2.3.4", "moment": "~2.10.6", + "ng-mask": "^3.0.12", "node-cache": "^3.0.0", "node-sha1": "^1.0.1", "pako": "^0.2.8", @@ -60,16 +61,18 @@ "request-promise": "^1.0.2", "sails": "~0.11.2", "sails-disk": "~0.10.8", - "sails-hook-babel": "^5.0.1", + "sails-hook-schedule": "^0.2.1", "sails-mongo": "^0.11.4", "sails.io.js": "^0.11.7", "sha256": "^0.2.0", + "snudown-js": "^1.4.0", "socket.io-browserify": "^0.9.6", "socket.io-client": "^1.3.7" }, "browser": { "jquery": "jquery-browserify", - "angular-mask": "./assets/js/dependencies/mask.min.js", + "node-jquery-deparam": "./node_modules/node-jquery-deparam/node-jquery-deparam.js", + "angular-mask": "./node_modules/ng-mask/dist/ngMask.js", "angular-md": "./node_modules/angular-md/dist/angular-md.js", "regex": "./assets/common/regexCommon.js" } diff --git a/tasks/config/browserify.js b/tasks/config/browserify.js index f9c25008..6f4e7859 100644 --- a/tasks/config/browserify.js +++ b/tasks/config/browserify.js @@ -3,10 +3,11 @@ module.exports = function (grunt) { grunt.config.set('browserify', { dev: { files: { - ".tmp/public/js/app.js": "assets/js/app.js" + ".tmp/public/js/app.js": "assets/app.js" }, options: { - transform: [["babelify"]] + transform: [["babelify"]], + watch: true } } }); diff --git a/tasks/config/copy.js b/tasks/config/copy.js index cd2f8d84..3968335d 100644 --- a/tasks/config/copy.js +++ b/tasks/config/copy.js @@ -20,7 +20,7 @@ module.exports = function (grunt) { files: [{ expand: true, cwd: './assets', - src: ['**/*.!(coffee|less|js)'], + src: ['**/*.!(coffee|less)'], dest: '.tmp/public' }] }, diff --git a/tasks/config/eslint.js b/tasks/config/eslint.js index b5b8b258..7117cbcb 100644 --- a/tasks/config/eslint.js +++ b/tasks/config/eslint.js @@ -12,7 +12,7 @@ module.exports = function (grunt) { grunt.config.set('eslint', { - target: ['Gruntfile.js', 'app.js', 'api/**/*.js', 'tasks/**/*.js', 'assets/js/*.js', 'assets/common/*.js'] + target: ['Gruntfile.js', 'app.js', 'api/**/*.js', 'tasks/**/*.js', 'assets/**/*.js'] }); grunt.loadNpmTasks('grunt-eslint'); diff --git a/tasks/config/watch.js b/tasks/config/watch.js index fbdd36d4..32d8d5ce 100644 --- a/tasks/config/watch.js +++ b/tasks/config/watch.js @@ -16,29 +16,41 @@ module.exports = function (grunt) { grunt.config.set('watch', { api: { - - // API files to watch: files: ['api/**/*'] }, - assets: { - - // Assets to watch: + less: { files: [ - 'assets/**/*', - 'tasks/pipeline.js' + 'assets/styles/**/*' + ], + tasks: [ + 'less:dev' + ], + options: { + livereload: true, + livereloadOnError: false + } + }, + js: { + files: [ + 'assets/**/*.js' ], - - // When assets are changed: tasks: [ - 'less:dev', 'copy:dev', - 'sails-linker:devJs', - 'sails-linker:devStyles', - 'eslint', - 'browserify:dev' - ] + 'eslint' + ], + options: { + livereload: true, + livereloadOnError: false + } + } + }); + + grunt.config.set('focus', { + dev: { + include: ['less', 'js'] } }); + grunt.loadNpmTasks('grunt-focus'); grunt.loadNpmTasks('grunt-contrib-watch'); }; diff --git a/tasks/register/default.js b/tasks/register/default.js index 401377fb..b9b43888 100644 --- a/tasks/register/default.js +++ b/tasks/register/default.js @@ -3,8 +3,6 @@ module.exports = function (grunt) { 'compileDev', 'sails-linker:devJs', 'sails-linker:devStyles', - 'browserify:dev', - 'eslint', - 'watch:assets' + 'focus:dev' ]); }; diff --git a/test/unit/data/flairCssClasses.json b/test/unit/data/flairCssClasses.json new file mode 100644 index 00000000..45b449e2 --- /dev/null +++ b/test/unit/data/flairCssClasses.json @@ -0,0 +1,40 @@ +{ + "pokemontrades": { + "pokeball,premierball": "premierball", + "greatball hok,ultraball": "ultraball hok", + "ovalcharm1,shinycharm": "shinycharm1", + "greatball,involvement": "greatball1", + "ovalcharm,involvement": "ovalcharm1", + "premierball hok,involvement": "premierball1 hok", + ",banned": "banned", + "pokeball,banned": "pokeball banned", + "pokeball1,banned": "pokeball1 banned", + "ovalcharm hok,banned": "ovalcharm banned", + "ovalcharm1 hok,banned": "ovalcharm1 banned", + "masterball1 hok something-else,banned": "masterball1 banned", + "dreamball banned,banned": "dreamball banned", + "banned,banned": "banned" + }, + + "SVExchange": { + ",lucky": "lucky", + "lucky,egg": "egg", + "eevee,togepi": "togepi", + "togepi smartribbon,torchic": "torchic smartribbon", + ",cuteribbon": "cuteribbon", + "cuteribbon,coolribbon": "coolribbon", + "manaphy smartribbon,toughribbon": "manaphy toughribbon", + "eggcup,beautyribbon": "eggcup beautyribbon", + "beautyribbon,eggcup": "eggcup beautyribbon", + ",banned": "banned", + "lucky,banned": "lucky banned", + "lucky cuteribbon,banned": "lucky cuteribbon banned", + "toughribbon,banned": "toughribbon banned", + "banned,banned": "banned", + "lucky banned,banned": "lucky banned", + "lucky cuteribbon banned,banned":"lucky cuteribbon banned", + "toughribbon banned,banned": "toughribbon banned", + "pichu banned,toughribbon": "pichu toughribbon banned", + "coolribbon banned,togepi": "togepi coolribbon banned" + } +} \ No newline at end of file diff --git a/test/unit/data/flairTexts.json b/test/unit/data/flairTexts.json new file mode 100644 index 00000000..a4b5f18e --- /dev/null +++ b/test/unit/data/flairTexts.json @@ -0,0 +1,24 @@ +{ + "tradesFlairStd": "1111-1111-1111 || YMK (X)", + "tradesFlairMultipleFCs": "1111-1111-1111, 2222-2222-2222 || YMK (X)", + "svexFlairStd": "1111-1111-1111 || YMK (X) || 1234", + "svexFlairDifferentFC": "2222-2222-2222 || YMK (X) || 1234", + "incorrectFlair": "not a correct flair", + "lotsOfGames": { + "ptrades": "1111-1111-1111 || NAA (X, Y, ΩR), AAA (Y, ΩR), Othername", + "svex": "1111-9999-1111 || AAA (Y, X), Another Name (αS, Y) || 0000, 1111", + "fcs": ["1111-1111-1111", "1111-9999-1111"], + "games": [ + {"ign": "NAA", "game": "X"}, + {"ign": "NAA", "game": "Y"}, + {"ign": "NAA", "game": "ΩR"}, + {"ign": "AAA", "game": "Y"}, + {"ign": "AAA", "game": "ΩR"}, + {"ign": "Othername", "game": ""}, + {"ign": "AAA", "game": "X"}, + {"ign": "Another Name", "game": "αS"}, + {"ign": "Another Name", "game": "Y"} + ], + "tsvs": ["0000", "1111"] + } +} \ No newline at end of file diff --git a/test/unit/data/friendCodes.json b/test/unit/data/friendCodes.json new file mode 100644 index 00000000..32f49f02 --- /dev/null +++ b/test/unit/data/friendCodes.json @@ -0,0 +1,8 @@ +{ + "valid1":"0000-0000-0135", + "valid2":"0000-0000-0165", + "invalid1":"0000-0000-0000", + "invalid2":"3333-3333-3333", + "exceedsMaximum":"7777-7777-7777", + "badFormat":"This is not a friend code." +} \ No newline at end of file diff --git a/test/unit/data/markdownStrings.json b/test/unit/data/markdownStrings.json new file mode 100644 index 00000000..cc0802a6 --- /dev/null +++ b/test/unit/data/markdownStrings.json @@ -0,0 +1,14 @@ +{ + "userUrl": "/u/test", + "userUrlAfter": "/u/test (FlairHQ)", + "userUrlWithExtra": "/u/testsomething else", + "userUrlWithExtraAfter": "/u/test (FlairHQ)something else", + "userUrlWrong": "/u/not_test", + "userUrlWrongAfter": "/u/not_test", + "subUrl": "/r/test", + "subUrlAfter": "/r/test", + "subUrlWithExtra": "/r/testsomething else", + "subUrlWithExtraAfter": "/r/testsomething else", + "subUrlWrong": "/r/not-test", + "subUrlWrongAfter": "/r/not-test" +} \ No newline at end of file diff --git a/test/unit/data/referenceFactory.js b/test/unit/data/referenceFactory.js new file mode 100644 index 00000000..99339a5a --- /dev/null +++ b/test/unit/data/referenceFactory.js @@ -0,0 +1,53 @@ +var _ = require('lodash'); +module.exports = { + // Generate random references + // Intended use is for unit testing, not flair grinding + getRefs: function (numberOfRefs, params) { + var refs = []; + for (var i = 0; i < numberOfRefs; i++) { + var subreddit, url, type, approved; + if (params.url) { + url = params.url; + subreddit = params.url.indexOf('/r/pokemontrades') !== -1 ? 'pokemontrades' : 'SVExchange'; + } else { + if (params.subreddit) { + subreddit = params.subreddit; + } else if (params.type === 'egg' || params.type === 'eggcheck') { + subreddit = 'SVExchange'; + } else if (params.type && params.type !== 'giveaway') { + subreddit = 'pokemontrades'; + } else { + subreddit = _.sample(['pokemontrades', 'SVExchange']); + } + url = 'https://reddit.com/r/' + subreddit + '/comments/a/a/' + Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 20); + } + if (params.type) { + type = params.type; + } + else if (subreddit === 'pokemontrades') { + type = _.sample(['event', 'shiny', 'casual', 'bank', 'involvement', 'giveaway']); + } else { + type = _.sample(['egg', 'eggcheck', 'giveaway']); + } + approved = _.sample([true, false]); + refs.push({ + url: url, + user: params.user || Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 20), + user2: params.user2 || Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 20), + description: params.description || Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 20), + gave: params.gave || Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 20), + got: params.got || Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 20), + type: type, + notes: params.notes || Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 20), + privatenotes: params.privatenotes || Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 20), + edited: params.edited || _.sample([true, false]), + number: _.random(0, Number.MAX_SAFE_INTEGER), + createdAt: params.createdAt || new Date(_.random(0, 4294967295000)).toISOString(), + updatedAt: params.updatedAt || new Date(_.random(0, 4294967295000)).toISOString(), + approved: approved, + verified: approved && _.sample([true, false]) + }); + } + return refs; + } +}; \ No newline at end of file diff --git a/test/unit/data/standardFlairInfo.json b/test/unit/data/standardFlairInfo.json new file mode 100644 index 00000000..6cb5549d --- /dev/null +++ b/test/unit/data/standardFlairInfo.json @@ -0,0 +1,54 @@ +{ + "flairs": { + "pokeball":{"name":"pokeball","sub":"pokemontrades","trades":10,"shinyevents":0,"events":0,"eggs":0,"giveaways":0,"involvement":0}, + "premierball":{"name":"premierball","sub":"pokemontrades","trades":20,"shinyevents":0,"events":0,"eggs":0,"giveaways":0,"involvement":0}, + "greatball":{"name":"greatball","sub":"pokemontrades","trades":30,"shinyevents":10,"events":0,"eggs":0,"giveaways":0,"involvement":0}, + "ultraball":{"name":"ultraball","sub":"pokemontrades","trades":40,"shinyevents":20,"events":0,"eggs":0,"giveaways":0,"involvement":0}, + "luxuryball":{"name":"luxuryball","sub":"pokemontrades","trades":50,"shinyevents":0,"events":0,"eggs":0,"giveaways":0,"involvement":0}, + "masterball":{"name":"masterball","sub":"pokemontrades","trades":60,"shinyevents":40,"events":0,"eggs":0,"giveaways":0,"involvement":0}, + "dreamball":{"name":"dreamball","sub":"pokemontrades","trades":70,"shinyevents":0,"events":0,"eggs":0,"giveaways":0,"involvement":0}, + "cherishball":{"name":"cherishball","sub":"pokemontrades","trades":80,"shinyevents":65,"events":25,"eggs":0,"giveaways":0,"involvement":0}, + "ovalcharm":{"name":"ovalcharm","sub":"pokemontrades","trades":90,"shinyevents":0,"events":0,"eggs":0,"giveaways":0,"involvement":0}, + "shinycharm":{"name":"shinycharm","sub":"pokemontrades","trades":100,"shinyevents":140,"events":70,"eggs":0,"giveaways":0,"involvement":0}, + "involvement":{"name":"involvement","sub":"pokemontrades","giveaways":5,"involvement":10,"trades":0,"eggs":0,"shinyevents":0,"events":0}, + "lucky":{"name":"lucky","sub":"svexchange","eggs":1,"trades":0,"shinyevents":0,"events":0,"giveaways":0,"involvement":0}, + "egg":{"name":"egg","sub":"svexchange","eggs":5,"trades":0,"shinyevents":0,"events":0,"giveaways":0,"involvement":0}, + "eevee":{"name":"eevee","eggs":10,"sub":"svexchange","trades":0,"shinyevents":0,"events":0,"giveaways":0,"involvement":0}, + "togepi":{"name":"togepi","eggs":20,"sub":"svexchange","trades":0,"shinyevents":0,"events":0,"giveaways":0,"involvement":0}, + "torchic":{"name":"torchic","eggs":30,"sub":"svexchange","trades":0,"involvement":0,"giveaways":0}, + "pichu":{"name":"pichu","sub":"svexchange","eggs":50,"trades":0,"involvement":0,"giveaways":0}, + "manaphy":{"name":"manaphy","sub":"svexchange","eggs":75,"trades":0,"shinyevents":0,"events":0,"giveaways":0,"involvement":0}, + "eggcup":{"name":"eggcup","sub":"svexchange","eggs":100,"trades":0,"shinyevents":0,"events":0,"giveaways":0,"involvement":0}, + "cuteribbon":{"sub":"svexchange","name":"cuteribbon","eggs":0,"giveaways":30,"trades":0,"shinyevents":0,"events":0,"involvement":0}, + "coolribbon":{"sub":"svexchange","name":"coolribbon","eggs":0,"giveaways":100,"trades":0,"shinyevents":0,"events":0,"involvement":0}, + "beautyribbon":{"sub":"svexchange","name":"beautyribbon","eggs":0,"giveaways":200,"trades":0,"shinyevents":0,"events":0,"involvement":0}, + "smartribbon":{"name":"smartribbon","sub":"svexchange","giveaways":400,"eggs":0,"trades":0,"involvement":0}, + "toughribbon":{"name":"toughribbon","sub":"svexchange","giveaways":800,"eggs":0,"trades":0,"involvement":0} + }, + "expectedFormat":{ + "pokeball":"Poke Ball", + "premierball":"Premier Ball", + "greatball":"Great Ball", + "ultraball":"Ultra Ball", + "luxuryball":"Luxury Ball", + "masterball":"Master Ball", + "dreamball":"Dream Ball", + "cherishball":"Cherish Ball", + "ovalcharm":"Oval Charm", + "shinycharm":"Shiny Charm", + "involvement":"Involvement", + "lucky":"Lucky Egg", + "egg":"Egg", + "eevee":"Eevee Egg", + "togepi":"Togepi Egg", + "torchic":"Torchic Egg", + "pichu":"Pichu Egg", + "manaphy":"Manaphy Egg", + "eggcup":"Egg Cup", + "cuteribbon":"Cute Ribbon", + "coolribbon":"Cool Ribbon", + "beautyribbon":"Beauty Ribbon", + "smartribbon":"Smart Ribbon", + "toughribbon":"Tough Ribbon" + } +} \ No newline at end of file diff --git a/test/unit/data/users.json b/test/unit/data/users.json new file mode 100644 index 00000000..f5ca116c --- /dev/null +++ b/test/unit/data/users.json @@ -0,0 +1,94 @@ +{ + "regular_moderator": { + "name": "not_an_aardvark", + "friendCodes": [ + "0000-0000-0135", + "0000-0000-0165" + ], + "updatedAt": "2015-11-22T20:32:22.767Z", + "redToken": "Secret-Base64-String", + "isMod": true, + "intro": "Not_an_aardvark's interesting intro with lots of special characters (;\"'<<[", + "loggedFriendCodes": [ + "0000-0000-0135", + "0000-0000-0165" + ], + "provider": "reddit", + "createdAt": "2015-10-31T03:51:43.117Z", + "flair": { + "ptrades": { + "flair_css_class": "naa", + "flair_template_id": null, + "flair_text": "0000-0000-0135, 0000-0000-0165 || Naa (ΩR, X)", + "flair_position": "right" + }, + "svex": { + "flair_css_class": "wobbuffet", + "flair_template_id": null, + "flair_text": "0000-0000-0135, 0000-0000-0165 || Naa (ΩR, X) || 2325, 4015", + "flair_position": "right" + } + } + }, + + "greatball_user": { + "name": "actually_an_aardvark", + "friendCodes": [ + "1111-1111-1111" + ], + "updatedAt": "2015-11-22T20:39:49.965Z", + "redToken": "Secret-Base64-String", + "intro": "Actually_an_aardvark's intro!", + "loggedFriendCodes": [ + "0000-0000-0135", + "0000-0000-0000" + ], + "provider": "reddit", + "createdAt": "2015-10-31T03:52:34.214Z", + "flair": { + "ptrades": { + "flair_css_class": "greatball", + "flair_template_id": null, + "flair_text": "0000-0000-0135 || naa test (X)", + "flair_position": "right" + }, + "svex": { + "flair_css_class": "lucky cuteribbon", + "flair_template_id": null, + "flair_text": "0000-0000-0135 || naa test (X) || XXXX", + "flair_position": "right" + } + }, + "banned": false + }, + + "default_flair_user": { + "name": "mr_default_flair", + "friendCodes": [ + "1111-1111-1111" + ], + "updatedAt": "2015-11-22T20:39:49.965Z", + "redToken": "Secret-Base64-String", + "intro": "My intro", + "loggedFriendCodes": [ + "1111-1111-1111" + ], + "provider": "reddit", + "createdAt": "2015-10-31T03:52:34.214Z", + "flair": { + "ptrades": { + "flair_css_class": "default", + "flair_template_id": null, + "flair_text": "1111-1111-1111 || mr-default (ΩR)", + "flair_position": "right" + }, + "svex": { + "flair_css_class": "", + "flair_template_id": null, + "flair_text": "1111-1111-1111 || mr-default (ΩR) || 1234", + "flair_position": "right" + } + }, + "banned": false + } +} \ No newline at end of file diff --git a/test/unit/markdown/remapURLs.test.js b/test/unit/markdown/remapURLs.test.js new file mode 100644 index 00000000..500cf0cb --- /dev/null +++ b/test/unit/markdown/remapURLs.test.js @@ -0,0 +1,38 @@ +var assert = require("chai").assert; +var remapURLs = require("../../../assets/markdown/remapURLs"); + +var markdownData = require("../data/markdownStrings.json"); + +describe("Markdown User URLs", function () { + it("Replaces " + markdownData.userUrl + " with " + markdownData.userUrlAfter, function () { + var test = remapURLs(markdownData.userUrl); + assert.equal(test, markdownData.userUrlAfter, "Not mapping user urls correctly."); + }); + + it("Replaces " + markdownData.userUrlWithExtra + " with " + markdownData.userUrlWithExtraAfter, function () { + var test = remapURLs(markdownData.userUrlWithExtra); + assert.equal(test, markdownData.userUrlWithExtraAfter, "Not mapping user urls correctly."); + }); + + it("Replaces " + markdownData.userUrlWrong + " with " + markdownData.userUrlWrongAfter, function () { + var test = remapURLs(markdownData.userUrlWrong); + assert.equal(test, markdownData.userUrlWrongAfter, "Not mapping user urls correctly."); + }); +}); + +describe("Markdown Subreddit URLs", function () { + it("Replaces " + markdownData.subUrl + " with " + markdownData.subUrlAfter, function () { + var test = remapURLs(markdownData.subUrl); + assert.equal(test, markdownData.subUrlAfter, "Not mapping sub urls correctly."); + }); + + it("Replaces " + markdownData.subUrlWithExtra + " with " + markdownData.subUrlWithExtraAfter, function () { + var test = remapURLs(markdownData.subUrlWithExtra); + assert.equal(test, markdownData.subUrlWithExtraAfter, "Not mapping sub urls correctly."); + }); + + it("Replaces " + markdownData.subUrlWrong + " with " + markdownData.subUrlWrongAfter, function () { + var test = remapURLs(markdownData.subUrlWrong); + assert.equal(test, markdownData.subUrlWrongAfter, "Not mapping sub urls correctly."); + }); +}); \ No newline at end of file diff --git a/test/unit/services/Flairs.test.js b/test/unit/services/Flairs.test.js index feae19c4..e4f3d950 100644 --- a/test/unit/services/Flairs.test.js +++ b/test/unit/services/Flairs.test.js @@ -1,88 +1,175 @@ +var _ = require("lodash"); var assert = require("chai").assert; var Flairs = require("../../../api/services/Flairs"); -var tradesFlairStd = "1111-1111-1111 || YMK (X)"; -var tradesFlairMultipleFCs = "1111-1111-1111, 2222-2222-2222 || YMK (X)"; -var svexFlairStd = "1111-1111-1111 || YMK (X) || 1234"; -var svexFlairDifferentFC = "2222-2222-2222 || YMK (X) || 1234"; -var incorrectFlair = "not a correct flair"; +var flairTexts = require("../data/flairTexts.json"); +var flairCssClasses = require("../data/flairCssClasses.json"); +var stdFlairInfo = require("../data/standardFlairInfo.json"); +var fcs = require("../data/friendCodes.json"); +var users = require("../data/users.json"); +var refFactory = require("../data/referenceFactory.js"); -describe("Flair checks", function () { - it("Throws error on incorrect pokemntrades flair", function () { +describe("Flair text", function () { + it("Throws error on incorrect pokemontrades flair", function () { try{ - Flairs.flairCheck(incorrectFlair, svexFlairStd); + Flairs.flairCheck(flairTexts.incorrectFlair, flairTexts.svexFlairStd); } catch (e) { - return assert.equal(e, "Error with format."); + return assert.strictEqual(e, "Error with format."); } assert.fail(null, null, "Shouldn't reach this point."); }); it("Throws error on incorrect svex flair", function () { try{ - Flairs.flairCheck(tradesFlairStd, incorrectFlair); + Flairs.flairCheck(flairTexts.tradesFlairStd, flairTexts.incorrectFlair); } catch (e) { - return assert.equal(e, "Error with format."); + return assert.strictEqual(e, "Error with format."); } assert.fail(null, null, "Shouldn't reach this point."); }); it("Throws error on undefined trades flair", function () { try{ - Flairs.flairCheck(tradesFlairStd, undefined); + Flairs.flairCheck(flairTexts.tradesFlairStd, undefined); } catch (e) { - return assert.equal(e, "Need both flairs."); + return assert.strictEqual(e, "Need both flairs."); } assert.fail(null, null, "Shouldn't reach this point."); }); it("Throws error on undefined trades flair", function () { try{ - Flairs.flairCheck(undefined, svexFlairStd); + Flairs.flairCheck(undefined, flairTexts.svexFlairStd); } catch (e) { - return assert.equal(e, "Need both flairs."); + return assert.strictEqual(e, "Need both flairs."); } assert.fail(null, null, "Shouldn't reach this point."); }); describe("On success", function () { it("Returns object containing friend codes", function () { - var fcs = Flairs.flairCheck(tradesFlairStd, svexFlairStd).fcs; - assert.equal(fcs.length, 1, "Has 1 fc."); - assert.equal(fcs[0], "1111-1111-1111", "Has 1 fc."); + var fcs = Flairs.flairCheck(flairTexts.tradesFlairStd, flairTexts.svexFlairStd).fcs; + assert.strictEqual(fcs.length, 1, "Has 1 fc."); + assert.strictEqual(fcs[0], "1111-1111-1111", "Has 1 fc."); }); it("Returns object containing all friend codes", function () { - var fcs = Flairs.flairCheck(tradesFlairMultipleFCs, svexFlairStd).fcs; - assert.equal(fcs.length, 2, "Has 2 fcs."); - assert.equal(fcs[0], "1111-1111-1111", "Has 1111-1111-1111"); - assert.equal(fcs[1], "2222-2222-2222", "Has 2222-2222-2222"); + var fcs = Flairs.flairCheck(flairTexts.tradesFlairMultipleFCs, flairTexts.svexFlairStd).fcs; + assert.strictEqual(fcs.length, 2, "Has 2 fcs."); + assert.strictEqual(fcs[0], "1111-1111-1111", "Has 1111-1111-1111"); + assert.strictEqual(fcs[1], "2222-2222-2222", "Has 2222-2222-2222"); }); it("Returns object containing friend codes from both ", function () { - var fcs = Flairs.flairCheck(tradesFlairStd, svexFlairDifferentFC).fcs; - assert.equal(fcs.length, 2, "Has 2 fcs."); - assert.equal(fcs[0], "1111-1111-1111", "Has 1111-1111-1111"); - assert.equal(fcs[1], "2222-2222-2222", "Has 2222-2222-2222"); + var fcs = Flairs.flairCheck(flairTexts.tradesFlairStd, flairTexts.svexFlairDifferentFC).fcs; + assert.strictEqual(fcs.length, 2, "Has 2 fcs."); + assert.strictEqual(fcs[0], "1111-1111-1111", "Has 1111-1111-1111"); + assert.strictEqual(fcs[1], "2222-2222-2222", "Has 2222-2222-2222"); }); - }); + it("Correctly splits flairs into FCs, IGNs, games, and TSVs", function () { + var obj = Flairs.flairCheck(flairTexts.lotsOfGames.ptrades, flairTexts.lotsOfGames.svex); + assert.deepEqual(obj, flairTexts.lotsOfGames); + }); + }); describe("Incorrect flairs", function () { it ("Doesn't allow no IGNs in trades flair", function () { try{ - Flairs.flairCheck("1234-1234-1234 || ", svexFlairStd); + Flairs.flairCheck("1234-1234-1234 || ", flairTexts.svexFlairStd); } catch (e) { - return assert.equal(e, "We need at least 1 game."); + return assert.strictEqual(e, "We need at least 1 game."); } assert.fail(null, null, "Shouldn't reach this point."); }); it ("Doesn't allow no IGNs in svex flair", function () { try{ - Flairs.flairCheck(tradesFlairStd, "1234-1234-1234 || || 1234"); + Flairs.flairCheck(flairTexts.tradesFlairStd, "1234-1234-1234 || || 1234"); } catch (e) { - return assert.equal(e, "We need at least 1 game."); + return assert.strictEqual(e, "We need at least 1 game."); } assert.fail(null, null, "Shouldn't reach this point."); }); }); -}); \ No newline at end of file +}); + +describe("Flair template formatting", function () { + it("Formats flair names correctly", function () { + var names = _.keys(stdFlairInfo.flairs); + for (var i = 0; i < names.length; i++) { + assert.strictEqual(Flairs.formattedName(names[i]), stdFlairInfo.expectedFormat[names[i]], 'Formats ' + names[i] + ' flair incorrectly'); + } + }); +}); + +describe("Friend Code Validity", function () { + it("Correctly identifies valid friend codes", function () { + assert(Flairs.validFC(fcs.valid1), 'Incorrectly claims that "' + fcs.valid1 + '" is invalid'); + assert(Flairs.validFC(fcs.valid2), 'Incorrectly claims that "' + fcs.valid2 + '" is invalid'); + }); + + it("Correctly identifies invalid friend codes", function () { + assert(!Flairs.validFC(fcs.invalid1), 'Incorrectly claims that "' + fcs.invalid1 + '" is valid'); + assert(!Flairs.validFC(fcs.invalid2), 'Incorrectly claims that "' + fcs.invalid2 + '" is valid'); + assert(!Flairs.validFC(fcs.exceedsMaximum), 'Incorrectly claims that "' + fcs.exceedsMaximum + '" is valid'); + assert(!Flairs.validFC(fcs.badFormat), 'Incorrectly claims that "' + fcs.badFormat + '" is valid'); + }); +}); + +describe("Applying for Flair", function () { + it("Can apply for ball flair with enough references", function () { + var userFlairs = Flairs.getUserFlairs(users.greatball_user, stdFlairInfo.flairs); + var refs = refFactory.getRefs(40, {type: 'event'}); + assert(Flairs.canUserApply(refs, stdFlairInfo.flairs.ultraball, userFlairs), "Error: Can't apply for ball flair under normal circumstances"); + }); + + it("Cannot apply for ball flair if prerequisites are not met", function () { + var userFlairs = Flairs.getUserFlairs(users.greatball_user, stdFlairInfo.flairs); + var refs = refFactory.getRefs(39, {type: 'event'}); + assert(!Flairs.canUserApply(refs, stdFlairInfo.flairs.ultraball, userFlairs), 'Error: Can apply for flair without the required number of trades'); + }); + + it("Can apply for involvement flair with all the prerequisites", function () { + var userFlairs = Flairs.getUserFlairs(users.greatball_user, stdFlairInfo.flairs); + var refs = refFactory.getRefs(10, {type: 'involvement'}).concat(refFactory.getRefs(5, {type: 'giveaway', subreddit: 'pokemontrades'})); + assert(Flairs.canUserApply(refs, stdFlairInfo.flairs.involvement, userFlairs), "Error: Can't apply for involvement flair"); + }); + + it("Cannot apply for involvement flair with default flair", function () { + var userFlairs = Flairs.getUserFlairs(users.default_flair_user, stdFlairInfo.flairs); + var refs = refFactory.getRefs(10, {type: 'involvement'}).concat(refFactory.getRefs(5, {type: 'giveaway', subreddit: 'pokemontrades'})); + assert(!Flairs.canUserApply(refs, stdFlairInfo.flairs.involvement, userFlairs), 'Error: Can apply for involvement flair with default flair'); + }); + + it("Cannot apply for currently-possessed flair", function () { + var userFlairs = Flairs.getUserFlairs(users.greatball_user, stdFlairInfo.flairs); + var refs = refFactory.getRefs(10, {type: 'involvement'}).concat(refFactory.getRefs(5, {type: 'giveaway', subreddit: 'pokemontrades'})); + assert(!Flairs.canUserApply(refs, stdFlairInfo.flairs.greatball, userFlairs), 'Error: Can apply for currently-possessed flair'); + }); + + it("Cannot downgrade flair", function () { + var userFlairs = Flairs.getUserFlairs(users.greatball_user, stdFlairInfo.flairs); + var refs = refFactory.getRefs(60, {type: 'event'}); + assert(!Flairs.canUserApply(refs, stdFlairInfo.flairs.pokeball, userFlairs), 'Error: Can apply for lower flair'); + }); + + it("Bank trades do not count for flair", function () { + var userFlairs = Flairs.getUserFlairs(users.default_flair_user, stdFlairInfo.flairs); + var refs = refFactory.getRefs(10, {type: 'bank'}); + assert(!Flairs.canUserApply(refs, stdFlairInfo.flairs.pokeball, userFlairs), 'Error: Bank trades are counted for flair'); + }); +}); + +describe("Upgrading/combining flairs", function () { + ['pokemontrades', 'SVExchange'].forEach(function (sub) { + describe(sub + ' flairs', function () { + _.keysIn(flairCssClasses[sub]).forEach(function (test_case) { + var previous = test_case.split(',')[0]; + var added = test_case.split(',')[1]; + it((previous || '(no flair)') + ' + ' + added + ' → ' + flairCssClasses[sub][test_case], function () { + assert.strictEqual(Flairs.makeNewCSSClass(previous, added, sub), flairCssClasses[sub][test_case]); + }); + }); + }); + }); +}); diff --git a/views/home/header.ejs b/views/home/header.ejs deleted file mode 100644 index 9681e873..00000000 --- a/views/home/header.ejs +++ /dev/null @@ -1,100 +0,0 @@ -<%- partial('editProfile.ejs') %> -<%- partial('../privacyPolicy.ejs') %> -<%- partial('flairMod.ejs') %> -<%- partial('flairApply.ejs') %> -<%- partial('flairText.ejs') %> -<%- partial('applist.ejs') %> - - diff --git a/views/home/references.ejs b/views/home/references.ejs deleted file mode 100644 index 3040ea7a..00000000 --- a/views/home/references.ejs +++ /dev/null @@ -1,432 +0,0 @@ -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    Trades ({{numberOfTrades()}})

    Approved

    [+] Events ({{<%= theUser %>.references.filter(isEvent).length}}) - - - {{<%= theUser %>.references.filter(isEvent).filter(isApproved).length}} -
    {{$index + 1}}.{{reference.gave}} for {{reference.got}} - - * - - - - - - - - - - - -
    [+] Shinies ({{<%= theUser %>.references.filter(isShiny).length}}) - - - {{<%= theUser %>.references.filter(isShiny).filter(isApproved).length}} -
    {{$index + 1}}.{{reference.gave}} for {{reference.got}} - - * - - - - - - - - - - - -
    [+] Competitive / Casual ({{<%= theUser %>.references.filter(isCasual).length}}) - - - {{<%= theUser %>.references.filter(isCasual).filter(isApproved).length}} -
    {{$index + 1}}.{{reference.gave}} for {{reference.got}} - - * - - - - - - - - - - - -
    [+]

    Bank trades ({{<%= theUser %>.references.filter(isBank).length}})

    {{$index + 1}}.{{reference.gave}} for {{reference.got}} - - - - - - - - - -
    [+]

    Egg Hatches ({{<%= theUser %>.references.filter(isEgg).length}})

    - - - {{<%= theUser %>.references.filter(isEgg).filter(isApproved).length}} -
    {{$index + 1}}.{{reference.description || reference.descrip}} - - * - - - - - - - - - - -
    [+]

    Giveaways/Contests ({{numberOfPokemonGivenAway(<%= theUser %>)}} Pokémon given, {{numberOfEggsGivenAway(<%= theUser %>)}} eggs given)

    - - -
    {{$index + 1}}. - - * - - - - - - - - - - -
    [+]

    Egg/TSV Checks ({{<%= theUser %>.references.filter(isEggCheck).length}} threads, {{numberOfEggChecks(<%= theUser %>)}} given)

    - - - {{numberOfApprovedEggChecks(<%= theUser %>)}} -
    {{$index + 1}}. - - * - - - - - - - - - - -
    [+]

    Free Tradeback/Free Redemption ({{<%= theUser %>.references.filter(isInvolvement).length}})

    - - - {{<%= theUser %>.references.filter(isInvolvement).filter(isApproved).length}} -
    {{$index + 1}}.{{reference.description || reference.descrip}} - - * - - - - - - - - - - -
    [+]

    Misc ({{<%= theUser %>.references.filter(isMisc).length}})

    - - - -
    {{$index + 1}}.{{reference.description || reference.descrip}} - - - - - - - - - -
    - - <%- partial('editreference.ejs') %> - <%- partial('viewreference.ejs') %> -
    \ No newline at end of file diff --git a/views/search/logs.ejs b/views/search/logs.ejs deleted file mode 100644 index 2e5a1320..00000000 --- a/views/search/logs.ejs +++ /dev/null @@ -1,46 +0,0 @@ -
    -
    - -
    -
    -
    -

    Log Viewer

    -
    - -
    -
    -
    -
    - - -
    -
    - -
    - -
    -
    - -
    diff --git a/views/search/refs.ejs b/views/search/refs.ejs deleted file mode 100644 index 9d3d284f..00000000 --- a/views/search/refs.ejs +++ /dev/null @@ -1,64 +0,0 @@ -
    -
    - -
    -
    -
    -

    Advanced Search

    -
    - -
    -
    -
    -
    - - - - -
    -
    -
    - -
    -
    - -
    -
    -
    - -
    - -
    -
    - -