diff --git a/api/controllers/FlairController.js b/api/controllers/FlairController.js index 53fbfaef..6afbb6a4 100644 --- a/api/controllers/FlairController.js +++ b/api/controllers/FlairController.js @@ -50,17 +50,30 @@ module.exports = { } 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 css_flair = Flairs.makeNewCSSClass(_.get(user, 'flair.' + shortened + '.flair_css_class') || '', app.flair, app.sub); + user.flair[shortened].flair_css_class = css_flair; + let current_text = user.flair[shortened].flair_text.replace(/:[a-zA-Z0-9_-]*:/g,''); + let flair_text = Flairs.makeNewFlairText(css_flair, current_text, shortened); + + // Check length of flair_text and give a warning message + var warning = ''; + if (flair_text.length > 64) { + warning = ' However, the length of your flair was too long, so your flair text was trimmed automatically. Please go to [FHQ](https://hq.porygon.co) to set your flair again.'; + flair_text = Flairs.makeNewFlairText(css_flair, current_text.slice(0,55), shortened); + } + + // Set the user's flair + await Reddit.setUserFlair(req.user.redToken, user.name, css_flair, 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(Event.create({type: "flairTextChange", user: req.user.name,content: "Changed " + user.name + "'s flair to " + css_flair})); + + // Send a PM to let them know application was accepted. + var pmContent = 'Your application for ' + Flairs.formattedName(app.flair) + ' flair on /r/' + app.sub + ' has been approved.' + warning; 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); + sails.log.info("/u/" + req.user.name + ": Changed " + user.name + "'s flair to " + css_flair); return res.ok(await Flairs.getApps()); } catch (err) { return res.serverError(err); @@ -118,6 +131,14 @@ module.exports = { var pFlair = _.get(req, "user.flair.ptrades.flair_css_class") || "default"; var svFlair = _.get(req, "user.flair.svex.flair_css_class") || ""; svFlair = svFlair.replace(/2/, ""); + + // Build flair text for ptrades and svex from css class + var ptrades_current_text = flairs.ptrades; + var ptrades_flair_text = Flairs.makeNewFlairText(pFlair, ptrades_current_text, 'ptrades'); + + var svex_current_text = flairs.svex; + var svex_flair_text = Flairs.makeNewFlairText(svFlair, svex_current_text, 'svex'); + var promises = []; var eventFlair = null; // Change to req.allParams().eventFlair during events @@ -130,7 +151,7 @@ module.exports = { req.user.team = _.includes(Flairs.kantoFlair, eventFlair) ? "kanto" : "alola"; pFlair = Flairs.makeNewCSSClass(pFlair, `kva-${eventFlair}-1`, "PokemonTrades"); module.exports.addMembershipPoints(req, res, "add").then(() => { - promises.push(Reddit.setUserFlair(refreshToken, req.user.name, pFlair, flairs.ptrades, "PokemonTrades").catch((err) => { + promises.push(Reddit.setUserFlair(refreshToken, req.user.name, pFlair, ptrades_flair_text, "PokemonTrades").catch((err) => { sails.log.warn(`Reverting team ${req.user.team} join for ${req.user.name} due to the following error:`); sails.log.warn(err); module.exports.addMembershipPoints(req, res, "remove"); @@ -141,13 +162,13 @@ module.exports = { return res.status(400).json({error: "Unexpected extra flair."}); } } else { - promises.push(Reddit.setUserFlair(refreshToken, req.user.name, pFlair, flairs.ptrades, "PokemonTrades")); + promises.push(Reddit.setUserFlair(refreshToken, req.user.name, pFlair, ptrades_flair_text, "PokemonTrades")); } - promises.push(Reddit.setUserFlair(refreshToken, req.user.name, svFlair, flairs.svex, "SVExchange")); + promises.push(Reddit.setUserFlair(refreshToken, req.user.name, svFlair, svex_flair_text, "SVExchange")); promises.push(User.update({name: req.user.name}, {loggedFriendCodes: friend_codes})); if (!blockReport && (users_with_matching_fcs.length !== 0 || matching_ip_usernames.length !== 0 || flagged.length)) { - var message = 'The user /u/' + req.user.name + ' set the following flairs:\n\n' + flairs.ptrades + '\n\n' + flairs.svex + '\n\n'; + var message = 'The user /u/' + req.user.name + ' set the following flairs:\n\n' + ptrades_flair_text + '\n\n' + svex_flair_text + '\n\n'; if (users_with_matching_fcs.length !== 0) { message += 'This flair contains a friend code that matches ' + '/u/' + matching_fc_usernames.join(', /u/') + '\'s friend code: ' + matching_friend_codes + '\n\n'; var altNote = "Alt of " + matching_fc_usernames; @@ -189,15 +210,16 @@ module.exports = { User.native(function(err, collection) { collection.update({"_id": req.user.name}, { $set:{ - "flair.ptrades.flair_text": flairs.ptrades, + "flair.ptrades.flair_text": ptrades_flair_text, "flair.ptrades.flair_css_class": pFlair, - "flair.svex.flair_text": flairs.svex, + "flair.svex.flair_text": svex_flair_text, "flair.svex.flair_css_class": svFlair } }); }); return res.ok(); }); + } catch (err) { return res.serverError(err); } diff --git a/api/services/Ban.js b/api/services/Ban.js index 7fe2aa40..1e812a2b 100644 --- a/api/services/Ban.js +++ b/api/services/Ban.js @@ -13,10 +13,24 @@ exports.banFromSub = async function (redToken, username, banMessage, banNote, su exports.giveBannedUserFlair = async function (redToken, username, current_css_class, current_flair_text, subreddit) { try { 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); - sails.log('Changed ' + username + '\'s flair to ' + css_class + ' on /r/' + subreddit); - return 'Changed ' + username + '\'s flair to ' + css_class + ' on /r/' + subreddit; + + // Remove emoji if it exists + var flair_arr = flair_text.split(' '); + if (flair_arr[0] !== null) { + if (flair_arr[0].indexOf(':') !== -1) { + flair_arr.shift(); + } + } + + // Add BANNED USER to flair text + flair_text = 'BANNED USER ' + flair_arr.join(' '); + if (flair_text.length >= 64) { + flair_text = flair_text.slice(0, 64); + } + + await Reddit.setUserFlair(redToken, username, 'banned', flair_text, subreddit); + sails.log('Changed ' + username + '\'s flair to banned on /r/' + subreddit); + return 'Changed ' + username + '\'s flair to banned on /r/' + subreddit; } catch (err) { throw {error: 'Failed to give banned user flair'}; } diff --git a/api/services/Flairs.js b/api/services/Flairs.js index 109e9c60..20755a81 100644 --- a/api/services/Flairs.js +++ b/api/services/Flairs.js @@ -8,6 +8,53 @@ var alolaFlair = ['rowlet', 'litten', 'popplio']; var eventFlair = kantoFlair.concat(alolaFlair); var eventFlairRegExp = new RegExp('\\bkva-(' + eventFlair.join("|") + ')-[1-3]\\b'); +// Mappings from css_flair to emoji names +const emojiMap = { + "ptrades": { + "default" : ":0:", + "gen2" : ":2:", + "pokeball" : ":10:", + "premierball" : ":20:", + "greatball" : ":30:", + "ultraball" : ":40:", + "luxuryball" : ":50:", + "masterball" : ":60:", + "dreamball" : ":70:", + "cherishball" : ":80:", + "ovalcharm" : ":90:", + "shinycharm" : ":100:", + "pokeball1" : ":10i:", + "premierball1" : ":20i:", + "greatball1" : ":30i:", + "ultraball1" : ":40i:", + "luxuryball1" : ":50i:", + "masterball1" : ":60i:", + "dreamball1" : ":70i:", + "cherishball1" : ":80i:", + "ovalcharm1" : ":90i:", + "shinycharm1" : ":100i:", + "gsball1" : ":GSi:", + "upgrade" : ":u:", + "eventribbon" : ":helper:" + }, + "svex": { + "lucky" : ":1:", + "egg" : ":5:", + "eevee" : ":10:", + "togepi" : ":20:", + "torchic" : ":30:", + "pichu" : ":50:", + "manaphy" : ":75:", + "eggcup" : ":100:", + "cuteribbon" : ":1r:", + "coolribbon" : ":2r:", + "beautyribbon" : ":3r:", + "smartribbon" : ":4r:", + "toughribbon" : ":5r:", + "upgrade" : ":u:" + } +}; + exports.eventFlair = eventFlair; exports.kantoFlair = kantoFlair; exports.eventFlairRegExp = eventFlairRegExp; @@ -115,17 +162,6 @@ exports.getUserFlairs = function (user, allflairs) { return exports.userHasFlair(user, flair); }); }; -exports.getFlairTextForSVEx = function (user) { - if (!user || !user.flair || !user.flair.svex || !user.flair.svex.flair_css_class) { - return; - } - var flairs = user.flair.svex.flair_css_class.split(' '), - flairText = ""; - for (var i = 0; i < flairs.length; i++) { - flairText += "flair-" + flairs[i] + " "; - } - return flairText; -}; exports.canUserApply = function (refs, applicationFlair, currentFlairs) { if (!applicationFlair) { return false; @@ -232,13 +268,17 @@ exports.flairCheck = function (ptrades, svex) { if (!ptrades || !svex) { throw "Need both flairs."; } - if (ptrades.length > 64 || svex.length > 64) { - throw "Flairs too long"; + + const regex_emoji = /:[a-zA-Z0-9_-]*:/; + if (ptrades.match(regex_emoji) || svex.match(regex_emoji)) { + throw "Flair has emoji."; + } + + if (ptrades.length > 55 || svex.length > 56) { + throw "Flairs too long."; } const friendCodeGroup = /((?:SW-)?(?:\d{4}-){2}\d{4}(?:, (?:SW-)?(?:\d{4}-){2}\d{4})*)/; - const gameGroup = '^(' + exports.legalIgn + '(?: \\((?:' + exports.gameOptions + ')(?:, (?:' + exports.gameOptions + '))*\\))(?:,(?: ' + - exports.legalIgn + ')?(?: \\((?:' + exports.gameOptions + ')(?:, (?:' + exports.gameOptions + '))*\\))?)*)$'; var tradesParts = ptrades.split(' || '); var svexParts = svex.split(' || '); if (tradesParts.length !== 2 || svexParts.length !== 3) { @@ -247,8 +287,8 @@ exports.flairCheck = function (ptrades, svex) { if (!tradesParts[0].match(friendCodeGroup) || !svexParts[0].match(friendCodeGroup)) { throw "Error with FCs"; } - if (!tradesParts[1].match(RegExp(gameGroup)) || !svexParts[1].match(RegExp(gameGroup))) { - throw "We need at least 1 game."; + if (!tradesParts[1].match(RegExp(exports.legalIgn)) || !svexParts[1].match(RegExp(exports.legalIgn))) { + throw "We need at least one IGN."; } if (!/\d{4}(, \d{4})*|XXXX/.test(svexParts[2])) { throw "Error with TSVs"; @@ -258,7 +298,7 @@ exports.flairCheck = function (ptrades, svex) { svex: svex, games: exports.combineGames(exports.parseGames(tradesParts[1]), exports.parseGames(svexParts[1])), tsvs: svexParts[2].split(', '), - fcs: _.union(tradesParts[0].split(', '), svexParts[0].split(', ')) + fcs: _.union(tradesParts[0].replace(/:[a-zA-Z0-9_-]*:/, "").split(', '), svexParts[0].split(', ')) }; return response; }; @@ -293,6 +333,21 @@ exports.makeNewCSSClass = function (previous_flair, new_addition, subreddit) { return previous_flair.replace(/([^ ]*)(.*)/, '$1 ' + new_addition + '$2'); }; +// Create new flair text with emojis +exports.makeNewFlairText = function (css_class, current_text, subreddit) { + + // Loop through CSS class and grab the appropriate emoji + const cssClasses = css_class.split(' '); + let emoji = ''; + for (let cssClass of cssClasses) { + // If the word is a key in the flair map, then grab the appropriate emoji + if (cssClass in emojiMap[subreddit]) { + emoji += emojiMap[subreddit][cssClass]; + } + } + return emoji + current_text; +}; + // Get the Damerau–Levenshtein distance (edit distance) between two strings. exports.edit_distance = function (string1, string2) { var distance_matrix = {}; diff --git a/assets/common/regexCommon.js b/assets/common/regexCommon.js index dc78a97f..1b957d58 100644 --- a/assets/common/regexCommon.js +++ b/assets/common/regexCommon.js @@ -1,5 +1,6 @@ -var ptradesFlair = "((?:SW-)?([0-9]{4}-){2}[0-9]{4})(, ((?:SW-)?([0-9]{4}-){2}[0-9]{4}))* \\|\\| ([^ ,|(]*( \\((X|Y|ΩR|αS|S|M|US|UM|LGP|LGE|SW|SH)(, (X|Y|ΩR|αS|S|M|US|UM|LGP|LGE|SW|SH))*\\))?)(, ([^ ,|(]*( \\((X|Y|ΩR|αS|S|M|US|UM|LGP|LGE|SW|SH)(, (X|Y|ΩR|αS|S|M|US|UM|LGP|LGE|SW|SH))*\\))?))*"; +var ptradesFlair = "(:[a-zA-Z0-9_-]*:)*((?:SW-)?([0-9]{4}-){2}[0-9]{4})(, ((?:SW-)?([0-9]{4}-){2}[0-9]{4}))* \\|\\| ([^ ,|(]*( \\((X|Y|ΩR|αS|S|M|US|UM|LGP|LGE|SW|SH)(, (X|Y|ΩR|αS|S|M|US|UM|LGP|LGE|SW|SH))*\\))?)(, ([^ ,|(]*( \\((X|Y|ΩR|αS|S|M|US|UM|LGP|LGE|SW|SH)(, (X|Y|ΩR|αS|S|M|US|UM|LGP|LGE|SW|SH))*\\))?))*"; var regex = { + emoji: "(:[a-zA-Z0-9_-]*:)", tsv: "[0-3]\\d{3}|40(?:[0-8]\\d|9[0-5])", tsvBars: "(\\|\\| [0-9]{4})|(, [0-9]{4})", fc: "((?:SW-)?([0-9]{4}-){2}[0-9]{4})", @@ -19,6 +20,7 @@ var global = function (reg) { }; module.exports = { + emoji: global(regex.emoji), tsv: global(regex.tsvBars), fc: global(regex.fc), game: global(regex.game), diff --git a/assets/sharedClientFunctions.js b/assets/sharedClientFunctions.js index ab3718a7..eb2607a6 100644 --- a/assets/sharedClientFunctions.js +++ b/assets/sharedClientFunctions.js @@ -134,8 +134,17 @@ module.exports = { $scope.numberOfApprovedEggChecks = function () { return referenceService.numberOfApprovedEggChecks(_.get($scope, pathToRefs)); }; - $scope.getFlairTextForSVEx = function () { - return flairService.getFlairTextForSVEx($scope.refUser); + $scope.renderCSSClass = function(classes) { + if (typeof classes !== 'string') { + return ''; + } + return classes.replace(/(\S+)/g, "flair-$1"); + }; + $scope.renderFlair = function(flair) { + if (typeof flair !== 'string') { + return ''; + } + return flair.replace(/:([^:]+):/g,''); }; $scope.applied = function (flair) { return flairService.applied(_.get($scope, pathToApps), flair); diff --git a/assets/styles/importer.less b/assets/styles/importer.less index 47d3ea0c..ad66a0a8 100644 --- a/assets/styles/importer.less +++ b/assets/styles/importer.less @@ -140,17 +140,13 @@ tr[data-toggle='collapse']{ margin-top: 10px; } -.flairTop{ - margin-top: 10px; - margin-bottom: 0px; -} - -.flairBottom{ - margin-top:2px; +.flairListing{ + margin: 2px 0 0 0; } .flairs{ display: inline-block; + padding: 8px 0 10px 0; } td h3 { diff --git a/assets/userCtrl.js b/assets/userCtrl.js index 9e6fd59f..ae3a86c4 100644 --- a/assets/userCtrl.js +++ b/assets/userCtrl.js @@ -191,61 +191,60 @@ module.exports = function ($scope, $location, io) { $scope.$apply(); }); }; - $scope.ptradesCreatedFlair = function () { - if (!$scope.user || !$scope.user.flairFriendCodes) { - return ""; - } - var fcs = $scope.user.flairFriendCodes.slice(0), - text = ""; - for (var i = 0; i < fcs.length; i++) { - text += fcs[i] && fcs[i].match(regex.fc) ? fcs[i] : ""; - if (i + 1 !== fcs.length) { - text += ", "; - } - } - return text + " || " + (flairService.formatGames($scope.user.flairGames) || ""); - }; - $scope.svexCreatedFlair = function () { - if (!$scope.user || !$scope.user.flairFriendCodes) { + $scope.renderFlairTextString = function (format) { + if (!$scope.user) { return ""; } - var fcs = $scope.user.flairFriendCodes.slice(0), - games = $scope.user.flairGames, - text = ""; - var fcText = ""; - for (var i = 0; i < fcs.length; i++) { - fcText += fcs[i] && fcs[i].match(regex.fc) ? fcs[i] : ""; - if (i + 1 !== fcs.length) { - fcText += ", "; + return format.replace(/\{([^\}]+?)\}/g, function (match, replacement, offset, string) { + if (replacement === "fcs") { + if (!$scope.user.flairFriendCodes) { + return ""; + } + var validFCs = []; + $scope.user.flairFriendCodes.forEach(function (fc) { + if (fc.match(regex.fc)) { + validFCs.push(fc); + } + }); + if (validFCs.length > 0) { + return validFCs.join(", "); + } else { + return ""; + } } - } - text += fcText + " || " + flairService.formatGames($scope.user.flairGames) + " || "; - var tsvText = ""; - for (var k = 0; k < games.length; k++) { - var tsv = ""; - if (games[k].tsv && games[k].tsv < 4096) { - // The server will reject any TSV that isn't 4 characters long, so pad it with zeros. - // something something npm install left-pad - tsv = ('0000' + games[k].tsv).slice(-4); + if (replacement === "games") { + if (!$scope.user.flairGames) { + return ""; + } + return flairService.formatGames($scope.user.flairGames); } - if (tsv && tsvText) { - tsvText += ", "; + if (replacement === "tsvs") { + var validTSVs = []; + $scope.user.flairGames.forEach(function (game) { + if (game.tsv && game.tsv < 4096) { + validTSVs.push(game.tsv.toString().padStart(4, "0")); + } + }); + if (validTSVs.length > 0) { + return validTSVs.join(", "); + } else { + return "XXXX"; + } } - tsvText += tsv; - } - return text + (tsvText || "XXXX"); + return string; + }); }; $scope.isCorrectFlairText = function () { - var svex = $scope.svexCreatedFlair(); - var ptrades = $scope.ptradesCreatedFlair(); + var svex = $scope.renderFlairTextString("{fcs} || {games} || {tsvs}"); + var ptrades = $scope.renderFlairTextString("{fcs} || {games}"); if (!$scope.user || !$scope.user.flairFriendCodes || !$scope.user.flairGames) { return; } - if (svex.length > 64 || ptrades.length > 64) { - return {correct: false, error: "Your flair is too long; Reddit's maximum is 64 characters. Please delete something."}; + if (svex.length > 55 || ptrades.length > 55) { + return {correct: false, error: "Your flair text is too long; it may be at most 55 characters. If you have multiple games or friend codes listed, please consider removing the least frequently used one from your flair."}; } for (var i = 0; i < $scope.user.flairFriendCodes.length; i++) { @@ -274,6 +273,12 @@ module.exports = function ($scope, $location, io) { if (illegal_match) { return {correct: false, error: 'Your in-game name contains an illegal character: ' + illegal_match}; } + + // Since Reddit's flair_text system is emoji based, we want to prevent users from submitting an IGN with colons to prevent conflicts. + var has_colon = game.ign.match(/:/); + if (has_colon) { + return {correct: false, error: 'Your in-game name contains a colon. Please omit the colons to prevent conflicts with emoji-based flair text.'}; + } } if (game.tsv >= 4096) { return {correct: false, error: "Invalid TSV, they should be between 0 and 4095."}; @@ -292,13 +297,12 @@ module.exports = function ($scope, $location, io) { $("#setTextError").html("").hide(); $scope.userok.setFlairText = false; $scope.userspin.setFlairText = true; - var ptrades = $scope.ptradesCreatedFlair(), - svex = $scope.svexCreatedFlair(), - url = "/flair/setText"; + + var url = "/flair/setText"; io.socket.post(url, { - "ptrades": ptrades, - "svex": svex, + "ptrades": $scope.renderFlairTextString("{fcs} || {games}"), + "svex": $scope.renderFlairTextString("{fcs} || {games} || {tsvs}"), "eventFlair": $scope.user.eventFlair }, function (data, res) { if (res.statusCode === 200) { @@ -358,6 +362,11 @@ module.exports = function ($scope, $location, io) { $scope = _.assign($scope, params); $scope.user.console = new Array(); console.log("Created console array"); + for (var subreddit in $scope.user.flair) { + if ($scope.user.flair[subreddit].flair_text) { + $scope.user.flair[subreddit].flair_text = $scope.user.flair[subreddit].flair_text.replace(/:[^:]+:/,''); + } + } if ($scope.user) { $scope.user.isFlairMod = $scope.user.isMod && ($scope.user.modPermissions.includes('all') || $scope.user.modPermissions.includes('flair')); try { diff --git a/assets/views/home/flairText.ejs b/assets/views/home/flairText.ejs index b61c4db6..f20c73ab 100644 --- a/assets/views/home/flairText.ejs +++ b/assets/views/home/flairText.ejs @@ -99,12 +99,13 @@