├── assets ├── templates │ └── .gitkeep ├── favicon.ico ├── images │ ├── balls.png │ ├── steps.png │ ├── fhq-500.png │ ├── GreyBall.png │ ├── SvExRules.png │ ├── TradeRules.png │ ├── ballflairs.png │ ├── eggflairs.png │ ├── spinda_500.png │ ├── HatchFlairs.png │ ├── TradeFlairs.png │ ├── old_man_403.png │ ├── ribbonFlairs.png │ └── judging-scatterbug.png ├── tools │ ├── darkmode.png │ ├── greasemonkey.png │ └── tools.ejs ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.ttf │ └── glyphicons-halflings-regular.woff ├── styles │ ├── bootstrap │ │ ├── mixins │ │ │ ├── center-block.less │ │ │ ├── text-emphasis.less │ │ │ ├── size.less │ │ │ ├── opacity.less │ │ │ ├── background-variant.less │ │ │ ├── text-overflow.less │ │ │ ├── tab-focus.less │ │ │ ├── resize.less │ │ │ ├── labels.less │ │ │ ├── progress-bar.less │ │ │ ├── reset-filter.less │ │ │ ├── nav-divider.less │ │ │ ├── alerts.less │ │ │ ├── nav-vertical-align.less │ │ │ ├── responsive-visibility.less │ │ │ ├── pagination.less │ │ │ ├── border-radius.less │ │ │ ├── panels.less │ │ │ ├── list-group.less │ │ │ ├── hide-text.less │ │ │ ├── clearfix.less │ │ │ ├── table-row.less │ │ │ ├── image.less │ │ │ ├── buttons.less │ │ │ ├── forms.less │ │ │ └── grid-framework.less │ │ ├── wells.less │ │ ├── breadcrumbs.less │ │ ├── responsive-embed.less │ │ ├── component-animations.less │ │ ├── close.less │ │ ├── thumbnails.less │ │ ├── utilities.less │ │ ├── media.less │ │ ├── pager.less │ │ ├── mixins.less │ │ ├── bootstrap.less │ │ ├── jumbotron.less │ │ ├── badges.less │ │ ├── labels.less │ │ ├── code.less │ │ ├── grid.less │ │ ├── alerts.less │ │ ├── print.less │ │ ├── pagination.less │ │ ├── progress-bars.less │ │ ├── tooltip.less │ │ └── scaffolding.less │ ├── fonts.less │ └── spinners.less ├── search │ ├── log │ │ ├── result.ejs │ │ ├── form.ejs │ │ └── controller.js │ ├── ref │ │ ├── result.ejs │ │ ├── controller.js │ │ └── form.ejs │ ├── user │ │ ├── result.ejs │ │ ├── form.ejs │ │ └── controller.js │ ├── modmail │ │ ├── result.ejs │ │ ├── form.ejs │ │ └── controller.js │ ├── search.module.js │ ├── types.js │ ├── README.md │ ├── main.ejs │ └── header.ejs ├── robots.txt ├── tooltip │ ├── tooltip.view.html │ ├── label.view.html │ └── tooltip.module.js ├── views │ ├── home │ │ ├── addDiscord.ejs │ │ ├── banlist.ejs │ │ ├── profileInfo.ejs │ │ ├── row.ejs │ │ ├── applist.ejs │ │ ├── viewreference.ejs │ │ ├── header.ejs │ │ └── banuser.ejs │ ├── 403.ejs │ ├── auth │ │ └── index.ejs │ ├── 500.ejs │ ├── 404.ejs │ ├── layout.ejs │ └── privacyPolicy.ejs ├── markdown │ ├── remapURLs.js │ └── markdown.module.js ├── numberPadding.js ├── common │ └── regexCommon.js ├── adminCtrl.js ├── app.js └── ngReallyClick.js ├── test ├── .eslintrc └── unit │ ├── data │ ├── friendCodes.json │ ├── flairTexts.json │ ├── markdownStrings.json │ ├── flairCssClasses.json │ ├── referenceFactory.js │ └── users.json │ └── markdown │ └── remapURLs.test.js ├── config ├── locales │ ├── de.json │ ├── en.json │ ├── fr.json │ ├── es.json │ └── _README.md ├── debug_vars.js ├── schedule.js ├── env │ ├── development.js │ └── production.js ├── connections.js ├── local.example.js ├── log.js ├── bootstrap.js ├── models.js ├── session.js ├── policies.js ├── i18n.js ├── express.js ├── csrf.js └── globals.js ├── .sailsrc ├── api ├── models │ ├── Sessions.js │ ├── ModNote.js │ ├── Application.js │ ├── Comment.js │ ├── PointLog.js │ ├── Game.js │ ├── ContestStats.js │ ├── Event.js │ ├── Flair.js │ ├── Team.js │ ├── Reference.js │ ├── Modmail.js │ └── User.js ├── policies │ ├── isFlairMod.js │ ├── isAdmin.js │ ├── isPostMod.js │ ├── passport.js │ └── sessionAuth.js ├── controllers │ ├── EventController.js │ ├── SearchController.js │ └── HomeController.js ├── .eslintrc ├── services │ ├── Modmails.js │ ├── Users.js │ └── Usernotes.js └── responses │ ├── ok.js │ ├── badRequest.js │ ├── forbidden.js │ ├── serverError.js │ └── notFound.js ├── tasks ├── register │ ├── test.js │ ├── default.js │ ├── prod.js │ └── compileAssets.js ├── config │ ├── browserify.js │ ├── clean.js │ ├── uglify.js │ ├── cssmin.js │ ├── eslint.js │ ├── test.js │ ├── concat.js │ ├── less.js │ ├── copy.js │ └── watch.js ├── pipeline.js └── README.md ├── .eslintrc ├── .gitignore ├── .travis.yml ├── .github └── workflows │ └── build.yml ├── app.js ├── Gruntfile.js └── package.json /assets/templates/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokemontrades/flairhq/HEAD/assets/favicon.ico -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "it": true, 4 | "describe": true 5 | } 6 | } -------------------------------------------------------------------------------- /assets/images/balls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokemontrades/flairhq/HEAD/assets/images/balls.png -------------------------------------------------------------------------------- /assets/images/steps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokemontrades/flairhq/HEAD/assets/images/steps.png -------------------------------------------------------------------------------- /assets/images/fhq-500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokemontrades/flairhq/HEAD/assets/images/fhq-500.png -------------------------------------------------------------------------------- /assets/tools/darkmode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokemontrades/flairhq/HEAD/assets/tools/darkmode.png -------------------------------------------------------------------------------- /config/locales/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome": "Willkommen", 3 | "A brand new app.": "Eine neue App." 4 | } 5 | -------------------------------------------------------------------------------- /config/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome": "Welcome", 3 | "A brand new app.": "A brand new app." 4 | } 5 | -------------------------------------------------------------------------------- /assets/images/GreyBall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokemontrades/flairhq/HEAD/assets/images/GreyBall.png -------------------------------------------------------------------------------- /assets/images/SvExRules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokemontrades/flairhq/HEAD/assets/images/SvExRules.png -------------------------------------------------------------------------------- /assets/images/TradeRules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokemontrades/flairhq/HEAD/assets/images/TradeRules.png -------------------------------------------------------------------------------- /assets/images/ballflairs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokemontrades/flairhq/HEAD/assets/images/ballflairs.png -------------------------------------------------------------------------------- /assets/images/eggflairs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokemontrades/flairhq/HEAD/assets/images/eggflairs.png -------------------------------------------------------------------------------- /assets/images/spinda_500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokemontrades/flairhq/HEAD/assets/images/spinda_500.png -------------------------------------------------------------------------------- /assets/images/HatchFlairs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokemontrades/flairhq/HEAD/assets/images/HatchFlairs.png -------------------------------------------------------------------------------- /assets/images/TradeFlairs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokemontrades/flairhq/HEAD/assets/images/TradeFlairs.png -------------------------------------------------------------------------------- /assets/images/old_man_403.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokemontrades/flairhq/HEAD/assets/images/old_man_403.png -------------------------------------------------------------------------------- /assets/images/ribbonFlairs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokemontrades/flairhq/HEAD/assets/images/ribbonFlairs.png -------------------------------------------------------------------------------- /assets/tools/greasemonkey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokemontrades/flairhq/HEAD/assets/tools/greasemonkey.png -------------------------------------------------------------------------------- /config/locales/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome": "Bienvenue", 3 | "A brand new app.": "Une toute nouvelle application." 4 | } 5 | -------------------------------------------------------------------------------- /.sailsrc: -------------------------------------------------------------------------------- 1 | { 2 | "generators": { 3 | "modules": {} 4 | }, 5 | "paths": { 6 | "views": "./assets/views" 7 | } 8 | } -------------------------------------------------------------------------------- /config/locales/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome": "Bienvenido", 3 | "A brand new app.": "Una aplicación de la nueva marca." 4 | } 5 | -------------------------------------------------------------------------------- /api/models/Sessions.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | attributes: { 3 | session: "json", 4 | expires: "string" 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /assets/images/judging-scatterbug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokemontrades/flairhq/HEAD/assets/images/judging-scatterbug.png -------------------------------------------------------------------------------- /api/policies/isFlairMod.js: -------------------------------------------------------------------------------- 1 | module.exports = (req, res, next) => Users.hasModPermission(req.user, 'flair') ? next() : res.forbidden(); 2 | -------------------------------------------------------------------------------- /api/policies/isAdmin.js: -------------------------------------------------------------------------------- 1 | module.exports = (req, res, next) => Users.hasModPermission(req.user, 'all') ? next() : res.forbidden('Not a mod'); 2 | -------------------------------------------------------------------------------- /assets/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokemontrades/flairhq/HEAD/assets/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /assets/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokemontrades/flairhq/HEAD/assets/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /assets/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokemontrades/flairhq/HEAD/assets/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /tasks/register/test.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.registerTask('test', [ 3 | 'eslint', 4 | 'mochaTest' 5 | ]); 6 | }; 7 | -------------------------------------------------------------------------------- /tasks/register/default.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.registerTask('default', [ 3 | 'compileDev', 4 | 'focus:dev' 5 | ]); 6 | }; 7 | -------------------------------------------------------------------------------- /tasks/register/prod.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.registerTask('prod', [ 3 | 'compileProd', 4 | 'concat', 5 | 'cssmin' 6 | ]); 7 | }; 8 | -------------------------------------------------------------------------------- /api/policies/isPostMod.js: -------------------------------------------------------------------------------- 1 | module.exports = (req, res, next) => (Users.hasModPermission(req.user, 'posts') && Users.hasModPermission(req.user, 'wiki')) ? next() : res.forbidden("Not post mod"); 2 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/center-block.less: -------------------------------------------------------------------------------- 1 | // Center-align a block level element 2 | 3 | .center-block() { 4 | display: block; 5 | margin-left: auto; 6 | margin-right: auto; 7 | } 8 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/text-emphasis.less: -------------------------------------------------------------------------------- 1 | // Typography 2 | 3 | .text-emphasis-variant(@color) { 4 | color: @color; 5 | a&:hover { 6 | color: darken(@color, 10%); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/size.less: -------------------------------------------------------------------------------- 1 | // Sizing shortcuts 2 | 3 | .size(@width; @height) { 4 | width: @width; 5 | height: @height; 6 | } 7 | 8 | .square(@size) { 9 | .size(@size; @size); 10 | } 11 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/opacity.less: -------------------------------------------------------------------------------- 1 | // Opacity 2 | 3 | .opacity(@opacity) { 4 | opacity: @opacity; 5 | // IE8 filter 6 | @opacity-ie: (@opacity * 100); 7 | filter: ~"alpha(opacity=@{opacity-ie})"; 8 | } 9 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/background-variant.less: -------------------------------------------------------------------------------- 1 | // Contextual backgrounds 2 | 3 | .bg-variant(@color) { 4 | background-color: @color; 5 | a&:hover { 6 | background-color: darken(@color, 10%); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /assets/search/log/result.ejs: -------------------------------------------------------------------------------- 1 |

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

4 | 5 |

6 | {{result.content}} 7 |

-------------------------------------------------------------------------------- /assets/search/ref/result.ejs: -------------------------------------------------------------------------------- 1 |

2 | /u/{{result.user}} and /u/{{result.user2}} 3 |

4 |

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

-------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/text-overflow.less: -------------------------------------------------------------------------------- 1 | // Text overflow 2 | // Requires inline-block or block for proper styling 3 | 4 | .text-overflow() { 5 | overflow: hidden; 6 | text-overflow: ellipsis; 7 | white-space: nowrap; 8 | } 9 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/tab-focus.less: -------------------------------------------------------------------------------- 1 | // WebKit-style focus 2 | 3 | .tab-focus() { 4 | // Default 5 | outline: thin dotted; 6 | // WebKit 7 | outline: 5px auto -webkit-focus-ring-color; 8 | outline-offset: -2px; 9 | } 10 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/resize.less: -------------------------------------------------------------------------------- 1 | // Resize anything 2 | 3 | .resizable(@direction) { 4 | resize: @direction; // Options: horizontal, vertical, both 5 | overflow: auto; // Per CSS3 UI, `resize` only applies when `overflow` isn't `visible` 6 | } 7 | -------------------------------------------------------------------------------- /test/unit/data/friendCodes.json: -------------------------------------------------------------------------------- 1 | { 2 | "valid1":"0000-0000-0135", 3 | "valid2":"0000-0000-0165", 4 | "invalid1":"0000-0000-0000", 5 | "invalid2":"3333-3333-3333", 6 | "exceedsMaximum":"7777-7777-7777", 7 | "badFormat":"This is not a friend code." 8 | } -------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/labels.less: -------------------------------------------------------------------------------- 1 | // Labels 2 | 3 | .label-variant(@color) { 4 | background-color: @color; 5 | 6 | &[href] { 7 | &:hover, 8 | &:focus { 9 | background-color: darken(@color, 10%); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /assets/search/user/result.ejs: -------------------------------------------------------------------------------- 1 |

2 | /u/{{result._id}} 3 |

4 | 5 |

6 | /r/PokemonTrades: {{result.flair.ptrades.flair_text}}
7 | /r/SVExchange: {{result.flair.svex.flair_text}} 8 |

-------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/progress-bar.less: -------------------------------------------------------------------------------- 1 | // Progress bars 2 | 3 | .progress-bar-variant(@color) { 4 | background-color: @color; 5 | 6 | // Deprecated parent class requirement as of v3.2.0 7 | .progress-striped & { 8 | #gradient > .striped(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /config/debug_vars.js: -------------------------------------------------------------------------------- 1 | module.exports.debug = { 2 | reddit: false, // If true, redirects reddit-modifying actions to a debug subreddit 3 | subreddit: 'crownofnails' // The debug subreddit to redirect to 4 | }; 5 | 6 | module.exports.version = require("../package.json").version; 7 | -------------------------------------------------------------------------------- /assets/search/modmail/result.ejs: -------------------------------------------------------------------------------- 1 |

2 | {{result.subject}} by /u/{{result.author}} 3 |

4 |

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

7 | 8 | -------------------------------------------------------------------------------- /assets/robots.txt: -------------------------------------------------------------------------------- 1 | # The robots.txt file is used to control how search engines index your live URLs. 2 | # See http://www.robotstxt.org/wc/norobots.html for more information. 3 | 4 | 5 | 6 | # To prevent search engines from seeing the site altogether, uncomment the next two lines: 7 | # User-Agent: * 8 | # Disallow: / 9 | -------------------------------------------------------------------------------- /assets/search/log/form.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 6 |
-------------------------------------------------------------------------------- /assets/search/user/form.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 6 |
-------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/reset-filter.less: -------------------------------------------------------------------------------- 1 | // Reset filters for IE 2 | // 3 | // When you need to remove a gradient background, do not forget to use this to reset 4 | // the IE filter for IE9 and below. 5 | 6 | .reset-filter() { 7 | filter: e(%("progid:DXImageTransform.Microsoft.gradient(enabled = false)")); 8 | } 9 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/nav-divider.less: -------------------------------------------------------------------------------- 1 | // Horizontal dividers 2 | // 3 | // Dividers (basically an hr) within dropdowns and nav lists 4 | 5 | .nav-divider(@color: #e5e5e5) { 6 | height: 1px; 7 | margin: ((@line-height-computed / 2) - 1) 0; 8 | overflow: hidden; 9 | background-color: @color; 10 | } 11 | -------------------------------------------------------------------------------- /assets/tooltip/tooltip.view.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /assets/search/search.module.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | 5 | var angular = require("angular"); 6 | var controller = require("./search.controller.js"); 7 | 8 | var searchModule = angular.module("fapp.search", []); 9 | searchModule.controller("SearchController", ['$scope', '$timeout', controller]); 10 | 11 | module.exports = searchModule; 12 | -------------------------------------------------------------------------------- /assets/views/home/addDiscord.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 |

7 | You have joined Discord as <%= discordInfo.user.username %>#<%= discordInfo.user.discriminator %> 8 |

9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/alerts.less: -------------------------------------------------------------------------------- 1 | // Alerts 2 | 3 | .alert-variant(@background; @border; @text-color) { 4 | background-color: @background; 5 | border-color: @border; 6 | color: @text-color; 7 | 8 | hr { 9 | border-top-color: darken(@border, 5%); 10 | } 11 | .alert-link { 12 | color: darken(@text-color, 10%); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tasks/register/compileAssets.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.registerTask('compileDev', [ 3 | 'clean:dev', 4 | 'less:dev', 5 | 'copy:dev', 6 | 'browserify:dev' 7 | ]); 8 | 9 | grunt.registerTask('compileProd', [ 10 | 'clean:dev', 11 | 'less:dev', 12 | 'copy:dev', 13 | 'browserify:dev', 14 | 'uglify' 15 | ]); 16 | }; 17 | -------------------------------------------------------------------------------- /tasks/config/browserify.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | grunt.config.set('browserify', { 4 | dev: { 5 | files: { 6 | ".tmp/public/js/app.js": "assets/app.js" 7 | }, 8 | options: { 9 | transform: [["babelify"]], 10 | watch: true 11 | } 12 | } 13 | }); 14 | 15 | grunt.loadNpmTasks('grunt-browserify'); 16 | }; -------------------------------------------------------------------------------- /api/models/ModNote.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Reference.js 3 | * 4 | * @description :: TODO: You might write a short summary of how this model works and what it represents here. 5 | * @docs :: http://sailsjs.org/#!documentation/models 6 | */ 7 | 8 | module.exports = { 9 | 10 | attributes: { 11 | user: "string", 12 | refUser: "string", 13 | note: "string" 14 | } 15 | }; 16 | 17 | -------------------------------------------------------------------------------- /api/models/Application.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Application.js 3 | * 4 | * @description :: TODO: You might write a short summary of how this model works and what it represents here. 5 | * @docs :: http://sailsjs.org/#!documentation/models 6 | */ 7 | 8 | module.exports = { 9 | 10 | attributes: { 11 | user: "string", 12 | flair: "string", 13 | sub: "string" 14 | } 15 | }; 16 | 17 | -------------------------------------------------------------------------------- /api/models/Comment.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Reference.js 3 | * 4 | * @description :: TODO: You might write a short summary of how this model works and what it represents here. 5 | * @docs :: http://sailsjs.org/#!documentation/models 6 | */ 7 | 8 | module.exports = { 9 | 10 | attributes: { 11 | user: "string", 12 | user2: "string", 13 | message: "text" 14 | } 15 | 16 | }; 17 | 18 | -------------------------------------------------------------------------------- /assets/markdown/remapURLs.js: -------------------------------------------------------------------------------- 1 | module.exports = function (value) { 2 | var userRegex = /()(\/?\2)(<\/a>)/g; 3 | var userReplaced = value.replace(userRegex, "$1https://www.reddit.com/$2$3$4$5 ($1/$2$3FlairHQ$5)"); 4 | var subRegex = /()(\/?\2)(<\/a>)/g; 5 | return userReplaced.replace(subRegex, "$1https://www.reddit.com/$2$3$4$5"); 6 | }; -------------------------------------------------------------------------------- /assets/search/modmail/form.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /assets/numberPadding.js: -------------------------------------------------------------------------------- 1 | var ng = require("angular"); 2 | 3 | ng.module('numberPaddingModule', []).filter('numberPadding', function () { 4 | return function (n, len) { 5 | var num = parseInt(n, 10); 6 | len = parseInt(len, 10); 7 | if (isNaN(num) || isNaN(len)) { 8 | return n; 9 | } 10 | num = '' + num; 11 | while (num.length < len) { 12 | num = '0' + num; 13 | } 14 | return num; 15 | }; 16 | }); -------------------------------------------------------------------------------- /assets/tooltip/label.view.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 10 |
11 |
12 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/nav-vertical-align.less: -------------------------------------------------------------------------------- 1 | // Navbar vertical align 2 | // 3 | // Vertically center elements in the navbar. 4 | // Example: an element has a height of 30px, so write out `.navbar-vertical-align(30px);` to calculate the appropriate top margin. 5 | 6 | .navbar-vertical-align(@element-height) { 7 | margin-top: ((@navbar-height - @element-height) / 2); 8 | margin-bottom: ((@navbar-height - @element-height) / 2); 9 | } 10 | -------------------------------------------------------------------------------- /api/models/PointLog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PointLog.js 3 | * 4 | * @description :: TODO: You might write a short summary of how this model works and what it represents here. 5 | * @docs :: http://sailsjs.org/#!documentation/models 6 | */ 7 | 8 | module.exports = { 9 | attributes: { 10 | time: "string", 11 | team: "string", 12 | from: "string", 13 | pointType: "string", 14 | reason: "string", 15 | points: "integer" 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/responsive-visibility.less: -------------------------------------------------------------------------------- 1 | // Responsive utilities 2 | 3 | // 4 | // More easily include all the states for responsive-utilities.less. 5 | .responsive-visibility() { 6 | display: block !important; 7 | table& { display: table; } 8 | tr& { display: table-row !important; } 9 | th&, 10 | td& { display: table-cell !important; } 11 | } 12 | 13 | .responsive-invisibility() { 14 | display: none !important; 15 | } 16 | -------------------------------------------------------------------------------- /api/controllers/EventController.js: -------------------------------------------------------------------------------- 1 | /* global module, User, Event */ 2 | /** 3 | * UserController 4 | * 5 | * @description :: Server-side logic for managing Users 6 | */ 7 | 8 | module.exports = { 9 | 10 | get: function (req, res) { 11 | var appData = { 12 | limit: 20, 13 | sort: "createdAt DESC" 14 | }; 15 | 16 | Event.find(appData).exec(function (err, events) { 17 | return res.ok(events); 18 | }); 19 | } 20 | }; 21 | 22 | -------------------------------------------------------------------------------- /assets/search/log/controller.js: -------------------------------------------------------------------------------- 1 | /* global Search */ 2 | module.exports = function (req, res) { 3 | var params = req.allParams(); 4 | if (!params.keyword) { 5 | return res.view("../search/main", {searchType: 'log', searchTerm: ''}); 6 | } 7 | var searchData = { 8 | keyword: params.keyword 9 | }; 10 | 11 | searchData.skip = params.skip || 0; 12 | 13 | Search.logs(searchData, function (results) { 14 | return res.ok(results); 15 | }); 16 | }; -------------------------------------------------------------------------------- /assets/search/user/controller.js: -------------------------------------------------------------------------------- 1 | /* global Search */ 2 | module.exports = function (req, res) { 3 | var params = req.allParams(); 4 | if (!params.keyword) { 5 | return res.view("../search/main", {searchType: 'user', searchTerm: ''}); 6 | } 7 | var searchData = { 8 | keyword: params.keyword 9 | }; 10 | 11 | searchData.skip = params.skip || 0; 12 | 13 | Search.users(searchData, function (results) { 14 | return res.ok(results); 15 | }); 16 | }; -------------------------------------------------------------------------------- /api/models/Game.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Reference.js 3 | * 4 | * @description :: TODO: You might write a short summary of how this model works and what it represents here. 5 | * @docs :: http://sailsjs.org/#!documentation/models 6 | */ 7 | 8 | module.exports = { 9 | 10 | attributes: { 11 | user: "string", 12 | ign: "string", 13 | tsv: { 14 | type: "int", 15 | max: 4095, 16 | min: -1, 17 | numeric: true 18 | } 19 | } 20 | }; 21 | 22 | -------------------------------------------------------------------------------- /assets/search/modmail/controller.js: -------------------------------------------------------------------------------- 1 | /* global Search */ 2 | module.exports = function (req, res) { 3 | var params = req.allParams(); 4 | if (!params.keyword) { 5 | return res.view("../search/main", {searchType: 'modmail', searchTerm: ''}); 6 | } 7 | var searchData = { 8 | keyword: params.keyword 9 | }; 10 | 11 | searchData.skip = params.skip || 0; 12 | 13 | Search.modmails(searchData, function (results) { 14 | return res.ok(results); 15 | }); 16 | }; -------------------------------------------------------------------------------- /api/models/ContestStats.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ContestStats.js 3 | * 4 | * @description :: TODO: You might write a short summary of how this model works and what it represents here. 5 | * @docs :: http://sailsjs.org/#!documentation/models 6 | */ 7 | 8 | module.exports = { 9 | attributes: { 10 | user: { 11 | type: "string", 12 | columnName: 'id', 13 | primaryKey: true 14 | }, 15 | expPoints: "integer", 16 | battleWins: "integer" 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /assets/search/types.js: -------------------------------------------------------------------------------- 1 | var types = [ 2 | {"short": "ref", "name": "References", controller: require("./ref/controller.js"), "modOnly": false}, 3 | {"short": "user", "name": "Users", controller: require("./user/controller.js"), "modOnly": false}, 4 | {"short": "log", "name": "Logs", controller: require("./log/controller.js"), "modOnly": true}, 5 | {"short": "modmail", "name": "Modmails", controller: require("./modmail/controller.js"), "modOnly": true} 6 | ]; 7 | 8 | module.exports = types; -------------------------------------------------------------------------------- /assets/views/403.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Forbidden 5 | 6 | 7 | 8 |
9 |
10 | 11 | <% if (typeof error !== 'undefined') { %> 12 |

13 |             <%- error %>
14 |           
15 | <% } %> 16 |
17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "globals": { 4 | "module": true, 5 | "require": true, 6 | "exports": true 7 | }, 8 | "rules": { 9 | "strict": 0, 10 | "no-console": 0, 11 | "linebreak-style": [ 12 | 2, 13 | "unix" 14 | ], 15 | "indent": [ 16 | 2, 17 | 2 18 | ], 19 | "semi": [ 20 | 2, 21 | "always" 22 | ] 23 | }, 24 | "env": { 25 | "es6": true, 26 | "browser": true 27 | }, 28 | "extends": "eslint:recommended" 29 | } 30 | -------------------------------------------------------------------------------- /api/models/Event.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Event.js 3 | * 4 | * @description :: TODO: You might write a short summary of how this model works and what it represents here. 5 | * @docs :: http://sailsjs.org/#!documentation/models 6 | */ 7 | 8 | module.exports = { 9 | 10 | attributes: { 11 | user: "string", 12 | type: { 13 | type: "string", 14 | enum: [ 15 | "flairTextChange", 16 | "flairCssChange", 17 | "banUser", 18 | "discordJoin" 19 | ] 20 | }, 21 | content: "string" 22 | } 23 | }; 24 | 25 | -------------------------------------------------------------------------------- /tasks/config/clean.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Clean files and folders. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * This grunt task is configured to clean out the contents in the .tmp/public of your 7 | * sails project. 8 | * 9 | * For usage docs see: 10 | * https://github.com/gruntjs/grunt-contrib-clean 11 | */ 12 | module.exports = function (grunt) { 13 | 14 | grunt.config.set('clean', { 15 | dev: ['.tmp/public/**'], 16 | build: ['www'] 17 | }); 18 | 19 | grunt.loadNpmTasks('grunt-contrib-clean'); 20 | }; 21 | -------------------------------------------------------------------------------- /assets/views/auth/index.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 12 |
13 |
14 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/pagination.less: -------------------------------------------------------------------------------- 1 | // Pagination 2 | 3 | .pagination-size(@padding-vertical; @padding-horizontal; @font-size; @border-radius) { 4 | > li { 5 | > a, 6 | > span { 7 | padding: @padding-vertical @padding-horizontal; 8 | font-size: @font-size; 9 | } 10 | &:first-child { 11 | > a, 12 | > span { 13 | .border-left-radius(@border-radius); 14 | } 15 | } 16 | &:last-child { 17 | > a, 18 | > span { 19 | .border-right-radius(@border-radius); 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tasks/config/uglify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Minify files with UglifyJS. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Minifies client-side javascript `assets`. 7 | * 8 | * For usage docs see: 9 | * https://github.com/gruntjs/grunt-contrib-uglify 10 | * 11 | */ 12 | module.exports = function (grunt) { 13 | 14 | grunt.config.set('uglify', { 15 | dist: { 16 | src: ['.tmp/public/js/app.js'], 17 | dest: '.tmp/public/min/production.min.js' 18 | } 19 | }); 20 | 21 | grunt.loadNpmTasks('grunt-contrib-uglify'); 22 | }; 23 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/border-radius.less: -------------------------------------------------------------------------------- 1 | // Single side border-radius 2 | 3 | .border-top-radius(@radius) { 4 | border-top-right-radius: @radius; 5 | border-top-left-radius: @radius; 6 | } 7 | .border-right-radius(@radius) { 8 | border-bottom-right-radius: @radius; 9 | border-top-right-radius: @radius; 10 | } 11 | .border-bottom-radius(@radius) { 12 | border-bottom-right-radius: @radius; 13 | border-bottom-left-radius: @radius; 14 | } 15 | .border-left-radius(@radius) { 16 | border-bottom-left-radius: @radius; 17 | border-top-left-radius: @radius; 18 | } 19 | -------------------------------------------------------------------------------- /tasks/config/cssmin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Compress CSS files. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Minifies css files and places them into .tmp/public/min directory. 7 | * 8 | * For usage docs see: 9 | * https://github.com/gruntjs/grunt-contrib-cssmin 10 | */ 11 | module.exports = function (grunt) { 12 | 13 | grunt.config.set('cssmin', { 14 | dist: { 15 | src: ['.tmp/public/concat/production.css'], 16 | dest: '.tmp/public/min/production.min.css' 17 | } 18 | }); 19 | 20 | grunt.loadNpmTasks('grunt-contrib-cssmin'); 21 | }; 22 | -------------------------------------------------------------------------------- /tasks/config/eslint.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Clean files and folders. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * This grunt task is configured to clean out the contents in the .tmp/public of your 7 | * sails project. 8 | * 9 | * For usage docs see: 10 | * https://github.com/gruntjs/grunt-contrib-clean 11 | */ 12 | module.exports = function (grunt) { 13 | 14 | grunt.config.set('eslint', { 15 | target: ['Gruntfile.js', 'app.js', 'api/**/*.js', 'tasks/**/*.js', 'assets/**/*.js'] 16 | }); 17 | 18 | grunt.loadNpmTasks('grunt-eslint'); 19 | 20 | }; 21 | -------------------------------------------------------------------------------- /assets/views/500.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Server Error 5 | 6 | 7 |
8 |
9 |

500: Internal Server Error

10 | <% if (typeof error !== 'undefined') { %> 11 |

12 |         	<%- error %>
13 |         
14 | <% } else { %> 15 |

16 |

17 | <% } %> 18 | 19 |
20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /tasks/config/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Clean files and folders. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * This grunt task is configured to clean out the contents in the .tmp/public of your 7 | * sails project. 8 | * 9 | * For usage docs see: 10 | * https://github.com/gruntjs/grunt-contrib-clean 11 | */ 12 | module.exports = function (grunt) { 13 | 14 | grunt.config.set('mochaTest', { 15 | unit: { 16 | options: { 17 | reporter: 'spec' 18 | }, 19 | src: ['test/**/*.js'] 20 | } 21 | }); 22 | 23 | grunt.loadNpmTasks('grunt-mocha-test'); 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /api/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "globals": { 6 | "Reference": true, 7 | "User": true, 8 | "Users": true, 9 | "Ban": true, 10 | "Reddit": true, 11 | "Event": true, 12 | "sails": true, 13 | "Flair": true, 14 | "Flairs": true, 15 | "Usernotes": true, 16 | "PointLog": true, 17 | "References": true, 18 | "Sessions": true, 19 | "Game": true, 20 | "Application": true, 21 | "ModNote": true, 22 | "Modmail": true, 23 | "Modmails": true, 24 | "Team": true, 25 | "ContestStats": true, 26 | "Discord": true, 27 | "_": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /assets/search/ref/controller.js: -------------------------------------------------------------------------------- 1 | /* global Search */ 2 | module.exports = function (req, res) { 3 | var params = req.allParams(); 4 | if (!params.keyword) { 5 | return res.view("../search/main", {searchType: 'ref', searchTerm: ''}); 6 | } 7 | var searchData = { 8 | description: params.keyword 9 | }; 10 | 11 | if (params.user) { 12 | searchData.user = params.user; 13 | } 14 | 15 | if (params.categories) { 16 | searchData.categories = params.categories.split(","); 17 | } 18 | 19 | searchData.skip = params.skip || 0; 20 | 21 | Search.refs(searchData, function (results) { 22 | return res.ok(results); 23 | }); 24 | }; -------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/panels.less: -------------------------------------------------------------------------------- 1 | // Panels 2 | 3 | .panel-variant(@border; @heading-text-color; @heading-bg-color; @heading-border) { 4 | border-color: @border; 5 | 6 | & > .panel-heading { 7 | color: @heading-text-color; 8 | background-color: @heading-bg-color; 9 | border-color: @heading-border; 10 | 11 | + .panel-collapse > .panel-body { 12 | border-top-color: @border; 13 | } 14 | .badge { 15 | color: @heading-bg-color; 16 | background-color: @heading-text-color; 17 | } 18 | } 19 | & > .panel-footer { 20 | + .panel-collapse > .panel-body { 21 | border-bottom-color: @border; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /assets/views/home/banlist.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 |

Ban list

7 | 8 | 19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/wells.less: -------------------------------------------------------------------------------- 1 | // 2 | // Wells 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base class 7 | .well { 8 | min-height: 20px; 9 | padding: 19px; 10 | margin-bottom: 20px; 11 | background-color: @well-bg; 12 | border: 1px solid @well-border; 13 | border-radius: @border-radius-base; 14 | .box-shadow(inset 0 1px 1px rgba(0,0,0,.05)); 15 | blockquote { 16 | border-color: #ddd; 17 | border-color: rgba(0,0,0,.15); 18 | } 19 | } 20 | 21 | // Sizes 22 | .well-lg { 23 | padding: 24px; 24 | border-radius: @border-radius-large; 25 | } 26 | .well-sm { 27 | padding: 9px; 28 | border-radius: @border-radius-small; 29 | } 30 | -------------------------------------------------------------------------------- /api/models/Flair.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Flair.js 3 | * 4 | * @description :: TODO: You might write a short summary of how this model works and what it represents here. 5 | * @docs :: http://sailsjs.org/#!documentation/models 6 | */ 7 | 8 | module.exports = { 9 | 10 | attributes: { 11 | name: "string", 12 | sub: "string", 13 | 14 | trades: { 15 | type: "integer", 16 | defaultsTo: 0 17 | }, 18 | involvement: { 19 | type: "integer", 20 | defaultsTo: 0 21 | }, 22 | eggs: { 23 | type: "integer", 24 | defaultsTo: 0 25 | }, 26 | giveaways: { 27 | type: "integer", 28 | defaultsTo: 0 29 | } 30 | } 31 | }; 32 | 33 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/list-group.less: -------------------------------------------------------------------------------- 1 | // List Groups 2 | 3 | .list-group-item-variant(@state; @background; @color) { 4 | .list-group-item-@{state} { 5 | color: @color; 6 | background-color: @background; 7 | 8 | a& { 9 | color: @color; 10 | 11 | .list-group-item-heading { 12 | color: inherit; 13 | } 14 | 15 | &:hover, 16 | &:focus { 17 | color: @color; 18 | background-color: darken(@background, 5%); 19 | } 20 | &.active, 21 | &.active:hover, 22 | &.active:focus { 23 | color: #fff; 24 | background-color: @color; 25 | border-color: @color; 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/hide-text.less: -------------------------------------------------------------------------------- 1 | // CSS image replacement 2 | // 3 | // Heads up! v3 launched with with only `.hide-text()`, but per our pattern for 4 | // mixins being reused as classes with the same name, this doesn't hold up. As 5 | // of v3.0.1 we have added `.text-hide()` and deprecated `.hide-text()`. 6 | // 7 | // Source: https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757 8 | 9 | // Deprecated as of v3.0.1 (will be removed in v4) 10 | .hide-text() { 11 | font: ~"0/0" a; 12 | color: transparent; 13 | text-shadow: none; 14 | background-color: transparent; 15 | border: 0; 16 | } 17 | 18 | // New mixin to use as of v3.0.1 19 | .text-hide() { 20 | .hide-text(); 21 | } 22 | -------------------------------------------------------------------------------- /config/schedule.js: -------------------------------------------------------------------------------- 1 | module.exports.schedule = { 2 | sailsInContext: true, 3 | tasks: { 4 | updateModmail: { 5 | cron : "0 8 * * *", 6 | task : function (context, sails) { 7 | sails.log.info('[Daily task]: Updating modmail archives...'); 8 | Promise.all([Modmails.updateArchive('pokemontrades'), Modmails.updateArchive('SVExchange')]).then(function (results) { 9 | sails.log.info('[Daily task]: Finished updating modmail archives.'); 10 | }, function (error) { 11 | sails.log.error('There was an issue updating the modmail archives.'); 12 | sails.log.error(error); 13 | }); 14 | }, 15 | context : {} 16 | } 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /tasks/config/concat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Concatenate files. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Concatenates files javascript and css from a defined array. Creates concatenated files in 7 | * .tmp/public/contact directory 8 | * [concat](https://github.com/gruntjs/grunt-contrib-concat) 9 | * 10 | * For usage docs see: 11 | * https://github.com/gruntjs/grunt-contrib-concat 12 | */ 13 | module.exports = function (grunt) { 14 | 15 | grunt.config.set('concat', { 16 | css: { 17 | src: require('../pipeline').cssFilesToInject, 18 | dest: '.tmp/public/concat/production.css' 19 | } 20 | }); 21 | 22 | grunt.loadNpmTasks('grunt-contrib-concat'); 23 | }; 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | 27 | .tmp/ 28 | 29 | .idea/ 30 | 31 | config/local.js 32 | 33 | .DS_Store 34 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/clearfix.less: -------------------------------------------------------------------------------- 1 | // Clearfix 2 | // 3 | // For modern browsers 4 | // 1. The space content is one way to avoid an Opera bug when the 5 | // contenteditable attribute is included anywhere else in the document. 6 | // Otherwise it causes space to appear at the top and bottom of elements 7 | // that are clearfixed. 8 | // 2. The use of `table` rather than `block` is only necessary if using 9 | // `:before` to contain the top-margins of child elements. 10 | // 11 | // Source: http://nicolasgallagher.com/micro-clearfix-hack/ 12 | 13 | .clearfix() { 14 | &:before, 15 | &:after { 16 | content: " "; // 1 17 | display: table; // 2 18 | } 19 | &:after { 20 | clear: both; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/breadcrumbs.less: -------------------------------------------------------------------------------- 1 | // 2 | // Breadcrumbs 3 | // -------------------------------------------------- 4 | 5 | 6 | .breadcrumb { 7 | padding: @breadcrumb-padding-vertical @breadcrumb-padding-horizontal; 8 | margin-bottom: @line-height-computed; 9 | list-style: none; 10 | background-color: @breadcrumb-bg; 11 | border-radius: @border-radius-base; 12 | 13 | > li { 14 | display: inline-block; 15 | 16 | + li:before { 17 | content: "@{breadcrumb-separator}\00a0"; // Unicode space added since inline-block means non-collapsing white-space 18 | padding: 0 5px; 19 | color: @breadcrumb-color; 20 | } 21 | } 22 | 23 | > .active { 24 | color: @breadcrumb-active-color; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/responsive-embed.less: -------------------------------------------------------------------------------- 1 | // Embeds responsive 2 | // 3 | // Credit: Nicolas Gallagher and SUIT CSS. 4 | 5 | .embed-responsive { 6 | position: relative; 7 | display: block; 8 | height: 0; 9 | padding: 0; 10 | overflow: hidden; 11 | 12 | .embed-responsive-item, 13 | iframe, 14 | embed, 15 | object { 16 | position: absolute; 17 | top: 0; 18 | left: 0; 19 | bottom: 0; 20 | height: 100%; 21 | width: 100%; 22 | border: 0; 23 | } 24 | 25 | // Modifier class for 16:9 aspect ratio 26 | &.embed-responsive-16by9 { 27 | padding-bottom: 56.25%; 28 | } 29 | 30 | // Modifier class for 4:3 aspect ratio 31 | &.embed-responsive-4by3 { 32 | padding-bottom: 75%; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /api/controllers/SearchController.js: -------------------------------------------------------------------------------- 1 | var searchTypes = require("../../assets/search/types.js"); 2 | var exportObject = {}; 3 | 4 | for (let i = 0; i < searchTypes.length; i++) { 5 | // Let's programmatically add the views, because we can. 6 | let type = searchTypes[i]; 7 | exportObject[type.short + "View"] = function (req, res) { 8 | return res.view("../search/main", { 9 | searchType: type.short, 10 | searchTerm: decodeURIComponent(req.params.searchterm) 11 | }); 12 | }; 13 | } 14 | 15 | for (let i = 0; i < searchTypes.length; i++) { 16 | // And here we will programmatically add the search functions 17 | let type = searchTypes[i]; 18 | exportObject[type.short] = type.controller; 19 | } 20 | 21 | 22 | module.exports = exportObject; -------------------------------------------------------------------------------- /assets/styles/bootstrap/component-animations.less: -------------------------------------------------------------------------------- 1 | // 2 | // Component animations 3 | // -------------------------------------------------- 4 | 5 | // Heads up! 6 | // 7 | // We don't use the `.opacity()` mixin here since it causes a bug with text 8 | // fields in IE7-8. Source: https://github.com/twbs/bootstrap/pull/3552. 9 | 10 | .fade { 11 | opacity: 0; 12 | .transition(opacity .15s linear); 13 | &.in { 14 | opacity: 1; 15 | } 16 | } 17 | 18 | .collapse { 19 | display: none; 20 | 21 | &.in { display: block; } 22 | tr&.in { display: table-row; } 23 | tbody&.in { display: table-row-group; } 24 | } 25 | 26 | .collapsing { 27 | position: relative; 28 | height: 0; 29 | overflow: hidden; 30 | .transition(height .35s ease); 31 | } 32 | -------------------------------------------------------------------------------- /api/models/Team.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Team.js 3 | * 4 | * @description :: TODO: You might write a short summary of how this model works and what it represents here. 5 | * @docs :: http://sailsjs.org/#!documentation/models 6 | */ 7 | 8 | module.exports = { 9 | autoPK: false, 10 | attributes: { 11 | team: { 12 | type: "string", 13 | columnName: 'id', 14 | enum: ['kanto', 'alola'], 15 | primaryKey: true 16 | }, 17 | members: { 18 | type: "array" 19 | }, 20 | membershipPoints: { 21 | type: "integer" 22 | }, 23 | battlePoints: { 24 | type: "integer" 25 | }, 26 | contestPoints: { 27 | type: "integer" 28 | }, 29 | triviaPoints: { 30 | type: "integer" 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /api/policies/passport.js: -------------------------------------------------------------------------------- 1 | var passport = require('passport'); 2 | 3 | module.exports = function (req, res, next) { 4 | // Initialize Passport 5 | passport.initialize()(req, res, function () { 6 | // Use the built-in sessions 7 | passport.session()(req, res, async function () { 8 | try { 9 | res.locals.user = req.user; 10 | if (req.user) { 11 | res.locals.user.games = await Game.find({user: req.user.name}); 12 | } 13 | res.locals.query = req.query; 14 | res.locals.flairs = await Flairs.getFlairs(); 15 | if (Users.hasModPermission(req.user, 'flair')) { 16 | res.locals.flairApps = await Flairs.getApps(); 17 | } 18 | next(); 19 | } catch (err) { 20 | return res.serverError(err); 21 | } 22 | }); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /tasks/config/less.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Compiles LESS files into CSS. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Only the `assets/styles/importer.less` is compiled. 7 | * This allows you to control the ordering yourself, i.e. import your 8 | * dependencies, mixins, variables, resets, etc. before other stylesheets) 9 | * 10 | * For usage docs see: 11 | * https://github.com/gruntjs/grunt-contrib-less 12 | */ 13 | module.exports = function (grunt) { 14 | 15 | grunt.config.set('less', { 16 | dev: { 17 | files: [{ 18 | expand: true, 19 | cwd: 'assets/styles/', 20 | src: ['importer.less'], 21 | dest: '.tmp/public/styles/', 22 | ext: '.css' 23 | }] 24 | } 25 | }); 26 | 27 | grunt.loadNpmTasks('grunt-contrib-less'); 28 | }; 29 | -------------------------------------------------------------------------------- /api/policies/sessionAuth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * sessionAuth 3 | * 4 | * @module :: Policy 5 | * @description :: Simple policy to allow any authenticated user 6 | * Assumes that your login action in one of your controllers sets `req.session.authenticated = true;` 7 | * @docs :: http://sailsjs.org/#!documentation/policies 8 | * 9 | */ 10 | module.exports = function(req, res, next) { 11 | if (req.user) { 12 | if (req.user.banned) { 13 | req.session.destroy(); 14 | return res.view(403, {error: "You have been banned from FlairHQ"}); 15 | } 16 | return next(); 17 | } 18 | if (req.isSocket) { 19 | return res.status(403).json({status: 403, redirectTo: "/login"}); 20 | } 21 | return res.redirect('/login' + (req.url !== '/' ? '?redirect=' + encodeURIComponent(req.url) : '')); 22 | }; 23 | -------------------------------------------------------------------------------- /assets/views/404.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page Not Found 5 | 6 | 7 |
8 |
9 |
10 | <%if (data && data.user) {%> 11 |

User <%=data.user%> hasn't created a reference page on FlairHQ yet!

12 |

Be sure you have the right username.

13 | <% } else {%> 14 |

404 Page Not Found

15 |

Judging Scatterbug judges you

16 | <% }%> 17 | 18 |
19 |
20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /assets/markdown/markdown.module.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A module to give us a nice, easy way to use markdown in the app 3 | */ 4 | 5 | var angular = require("angular"); 6 | var Snudown = require("snudown-js"); 7 | var remapURLs = require("./remapURLs"); 8 | 9 | module.exports = angular.module("fapp.md", []) 10 | .directive("md", function () { 11 | return { 12 | restrict: "E", 13 | require: "?ngModel", 14 | link: function ($scope, $elem, $attrs, ngModel) { 15 | if (!ngModel) { 16 | var html = remapURLs(Snudown.markdown($elem.text())); 17 | $elem.html(html); 18 | return; 19 | } 20 | ngModel.$render = function () { 21 | var html = remapURLs(Snudown.markdown(ngModel.$viewValue || "")); 22 | $elem.html(html); 23 | }; 24 | } 25 | }; 26 | }); -------------------------------------------------------------------------------- /assets/views/home/profileInfo.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{refUser.name}}'s Information

4 |
5 |
6 | 7 |
8 |
9 |

Introduction:

10 | 11 |
12 |
13 |

Friend codes:

14 | 17 | 18 |

Games:

19 | 26 |
27 |
28 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/close.less: -------------------------------------------------------------------------------- 1 | // 2 | // Close icons 3 | // -------------------------------------------------- 4 | 5 | 6 | .close { 7 | float: right; 8 | font-size: (@font-size-base * 1.5); 9 | font-weight: @close-font-weight; 10 | line-height: 1; 11 | color: @close-color; 12 | text-shadow: @close-text-shadow; 13 | .opacity(.2); 14 | 15 | &:hover, 16 | &:focus { 17 | color: @close-color; 18 | text-decoration: none; 19 | cursor: pointer; 20 | .opacity(.5); 21 | } 22 | 23 | // Additional properties for button version 24 | // iOS requires the button element instead of an anchor tag. 25 | // If you want the anchor version, it requires `href="#"`. 26 | button& { 27 | padding: 0; 28 | cursor: pointer; 29 | background: transparent; 30 | border: 0; 31 | -webkit-appearance: none; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/table-row.less: -------------------------------------------------------------------------------- 1 | // Tables 2 | 3 | .table-row-variant(@state; @background) { 4 | // Exact selectors below required to override `.table-striped` and prevent 5 | // inheritance to nested tables. 6 | .table > thead > tr, 7 | .table > tbody > tr, 8 | .table > tfoot > tr { 9 | > td.@{state}, 10 | > th.@{state}, 11 | &.@{state} > td, 12 | &.@{state} > th { 13 | background-color: @background; 14 | } 15 | } 16 | 17 | // Hover states for `.table-hover` 18 | // Note: this is not available for cells or rows within `thead` or `tfoot`. 19 | .table-hover > tbody > tr { 20 | > td.@{state}:hover, 21 | > th.@{state}:hover, 22 | &.@{state}:hover > td, 23 | &:hover > .@{state}, 24 | &.@{state}:hover > th { 25 | background-color: darken(@background, 5%); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /assets/search/README.md: -------------------------------------------------------------------------------- 1 | # Adding a new search function (client side) 2 | 3 | Assuming you have done everything client, side, because that is simple, there are a few small things that are not 4 | obvious with the client side code. 5 | 6 | Firstly, you need to add a new directory (obvious) and have a form.ejs and result.ejs files. These are for the 7 | advanced form (for the advanced page) and the results, for both the advanced page and the dropdown from the header. 8 | 9 | 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 10 | automated, is that we can't loop over the directories in ejs, unless we have an array somewhere of what ones we have, and 11 | 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. 12 | 13 | I'm sure there is a nice way somewhere... -------------------------------------------------------------------------------- /assets/styles/bootstrap/thumbnails.less: -------------------------------------------------------------------------------- 1 | // 2 | // Thumbnails 3 | // -------------------------------------------------- 4 | 5 | 6 | // Mixin and adjust the regular image class 7 | .thumbnail { 8 | display: block; 9 | padding: @thumbnail-padding; 10 | margin-bottom: @line-height-computed; 11 | line-height: @line-height-base; 12 | background-color: @thumbnail-bg; 13 | border: 1px solid @thumbnail-border; 14 | border-radius: @thumbnail-border-radius; 15 | .transition(all .2s ease-in-out); 16 | 17 | > img, 18 | a > img { 19 | &:extend(.img-responsive); 20 | margin-left: auto; 21 | margin-right: auto; 22 | } 23 | 24 | // Add a hover state for linked versions only 25 | a&:hover, 26 | a&:focus, 27 | a&.active { 28 | border-color: @link-color; 29 | } 30 | 31 | // Image captions 32 | .caption { 33 | padding: @thumbnail-caption-padding; 34 | color: @thumbnail-caption-color; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/unit/data/flairTexts.json: -------------------------------------------------------------------------------- 1 | { 2 | "tradesFlairStd": "1111-1111-1111 || YMK (X)", 3 | "tradesFlairMultipleFCs": "1111-1111-1111, 2222-2222-2222 || YMK (X)", 4 | "svexFlairStd": "1111-1111-1111 || YMK (X) || 1234", 5 | "svexFlairDifferentFC": "2222-2222-2222 || YMK (X) || 1234", 6 | "svexFlairBadTSV": "1111-2222-3333 || NAA (Y) || COOKIES", 7 | "incorrectFlair": "not a correct flair", 8 | "SMFlair": "1111-1111-1111 || YMK (S) || Nothing", 9 | "SMFlair2": "1111-1111-1111 || YMK (S, M), YaManicKill (X, Y) || Nothing", 10 | "lotsOfGames": { 11 | "ptrades": "1111-1111-1111 || AAA (Y, ΩR), Joe, Bob (X)", 12 | "svex": "1111-9999-1111 || AAA (Y, ΩR), Joe, Bob (X) || 0000", 13 | "fcs": ["1111-1111-1111", "1111-9999-1111"], 14 | "games": [ 15 | {"ign": "AAA", "game": "Y"}, 16 | {"ign": "AAA", "game": "ΩR"}, 17 | {"ign": "Joe", "game": ""}, 18 | {"ign": "Bob", "game": "X"} 19 | ], 20 | "tsvs": ["0000"] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /config/env/development.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Development environment settings 3 | * 4 | * This file can include shared settings for a development team, 5 | * such as API keys or remote database passwords. If you're using 6 | * a version control solution for your Sails app, this file will 7 | * be committed to your repository unless you add it to your .gitignore 8 | * file. If your repository will be publicly viewable, don't add 9 | * any private information to this file! 10 | * 11 | */ 12 | 13 | module.exports = { 14 | 15 | log: { 16 | level: "verbose" 17 | } 18 | 19 | /*************************************************************************** 20 | * Set the default database connection for models in the development * 21 | * environment (see config/connections.js and config/models.js ) * 22 | ***************************************************************************/ 23 | 24 | // models: { 25 | // connection: 'someMongodbServer' 26 | // } 27 | 28 | }; 29 | -------------------------------------------------------------------------------- /assets/search/ref/form.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 6 | 7 | 9 |
10 |
11 |
12 | 13 |
14 |
15 | 22 |
23 |
-------------------------------------------------------------------------------- /tasks/config/copy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copy files and folders. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * # dev task config 7 | * Copies all directories and files, exept coffescript and less fiels, from the sails 8 | * assets folder into the .tmp/public directory. 9 | * 10 | * # build task config 11 | * Copies all directories nd files from the .tmp/public directory into a www directory. 12 | * 13 | * For usage docs see: 14 | * https://github.com/gruntjs/grunt-contrib-copy 15 | */ 16 | module.exports = function (grunt) { 17 | 18 | grunt.config.set('copy', { 19 | dev: { 20 | files: [{ 21 | expand: true, 22 | cwd: './assets', 23 | src: ['**/*.!(coffee|less)'], 24 | dest: '.tmp/public' 25 | }] 26 | }, 27 | build: { 28 | files: [{ 29 | expand: true, 30 | cwd: '.tmp/public', 31 | src: ['**/*'], 32 | dest: 'www' 33 | }] 34 | } 35 | }); 36 | 37 | grunt.loadNpmTasks('grunt-contrib-copy'); 38 | }; 39 | -------------------------------------------------------------------------------- /test/unit/data/markdownStrings.json: -------------------------------------------------------------------------------- 1 | { 2 | "userUrl": "/u/test", 3 | "userUrlAfter": "/u/test (FlairHQ)", 4 | "userUrlWithExtra": "/u/testsomething else", 5 | "userUrlWithExtraAfter": "/u/test (FlairHQ)something else", 6 | "userUrlWrong": "/u/not_test", 7 | "userUrlWrongAfter": "/u/not_test", 8 | "subUrl": "/r/test", 9 | "subUrlAfter": "/r/test", 10 | "subUrlWithExtra": "/r/testsomething else", 11 | "subUrlWithExtraAfter": "/r/testsomething else", 12 | "subUrlWrong": "/r/not-test", 13 | "subUrlWrongAfter": "/r/not-test" 14 | } -------------------------------------------------------------------------------- /assets/styles/bootstrap/utilities.less: -------------------------------------------------------------------------------- 1 | // 2 | // Utility classes 3 | // -------------------------------------------------- 4 | 5 | 6 | // Floats 7 | // ------------------------- 8 | 9 | .clearfix { 10 | .clearfix(); 11 | } 12 | .center-block { 13 | .center-block(); 14 | } 15 | .pull-right { 16 | float: right !important; 17 | } 18 | .pull-left { 19 | float: left !important; 20 | } 21 | 22 | 23 | // Toggling content 24 | // ------------------------- 25 | 26 | // Note: Deprecated .hide in favor of .hidden or .sr-only (as appropriate) in v3.0.1 27 | .hide { 28 | display: none !important; 29 | } 30 | .show { 31 | display: block !important; 32 | } 33 | .invisible { 34 | visibility: hidden; 35 | } 36 | .text-hide { 37 | .text-hide(); 38 | } 39 | 40 | 41 | // Hide from screenreaders and browsers 42 | // 43 | // Credit: HTML5 Boilerplate 44 | 45 | .hidden { 46 | display: none !important; 47 | visibility: hidden !important; 48 | } 49 | 50 | 51 | // For Affix plugin 52 | // ------------------------- 53 | 54 | .affix { 55 | position: fixed; 56 | .translate3d(0, 0, 0); 57 | } 58 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/media.less: -------------------------------------------------------------------------------- 1 | // Media objects 2 | // Source: http://stubbornella.org/content/?p=497 3 | // -------------------------------------------------- 4 | 5 | 6 | // Common styles 7 | // ------------------------- 8 | 9 | // Clear the floats 10 | .media, 11 | .media-body { 12 | overflow: hidden; 13 | zoom: 1; 14 | } 15 | 16 | // Proper spacing between instances of .media 17 | .media, 18 | .media .media { 19 | margin-top: 15px; 20 | } 21 | .media:first-child { 22 | margin-top: 0; 23 | } 24 | 25 | // For images and videos, set to block 26 | .media-object { 27 | display: block; 28 | } 29 | 30 | // Reset margins on headings for tighter default spacing 31 | .media-heading { 32 | margin: 0 0 5px; 33 | } 34 | 35 | 36 | // Media image alignment 37 | // ------------------------- 38 | 39 | .media { 40 | > .pull-left { 41 | margin-right: 10px; 42 | } 43 | > .pull-right { 44 | margin-left: 10px; 45 | } 46 | } 47 | 48 | 49 | // Media list variation 50 | // ------------------------- 51 | 52 | // Undo default ul/ol styles 53 | .media-list { 54 | padding-left: 0; 55 | list-style: none; 56 | } 57 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/pager.less: -------------------------------------------------------------------------------- 1 | // 2 | // Pager pagination 3 | // -------------------------------------------------- 4 | 5 | 6 | .pager { 7 | padding-left: 0; 8 | margin: @line-height-computed 0; 9 | list-style: none; 10 | text-align: center; 11 | &:extend(.clearfix all); 12 | li { 13 | display: inline; 14 | > a, 15 | > span { 16 | display: inline-block; 17 | padding: 5px 14px; 18 | background-color: @pager-bg; 19 | border: 1px solid @pager-border; 20 | border-radius: @pager-border-radius; 21 | } 22 | 23 | > a:hover, 24 | > a:focus { 25 | text-decoration: none; 26 | background-color: @pager-hover-bg; 27 | } 28 | } 29 | 30 | .next { 31 | > a, 32 | > span { 33 | float: right; 34 | } 35 | } 36 | 37 | .previous { 38 | > a, 39 | > span { 40 | float: left; 41 | } 42 | } 43 | 44 | .disabled { 45 | > a, 46 | > a:hover, 47 | > a:focus, 48 | > span { 49 | color: @pager-disabled-color; 50 | background-color: @pager-bg; 51 | cursor: not-allowed; 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins.less: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------------------------------- 3 | 4 | // Utilities 5 | @import "mixins/hide-text"; 6 | @import "mixins/opacity"; 7 | @import "mixins/image"; 8 | @import "mixins/labels"; 9 | @import "mixins/reset-filter"; 10 | @import "mixins/resize"; 11 | @import "mixins/responsive-visibility"; 12 | @import "mixins/size"; 13 | @import "mixins/tab-focus"; 14 | @import "mixins/text-emphasis"; 15 | @import "mixins/text-overflow"; 16 | @import "mixins/vendor-prefixes"; 17 | 18 | // Components 19 | @import "mixins/alerts"; 20 | @import "mixins/buttons"; 21 | @import "mixins/panels"; 22 | @import "mixins/pagination"; 23 | @import "mixins/list-group"; 24 | @import "mixins/nav-divider"; 25 | @import "mixins/forms"; 26 | @import "mixins/progress-bar"; 27 | @import "mixins/table-row"; 28 | 29 | // Skins 30 | @import "mixins/background-variant"; 31 | @import "mixins/border-radius"; 32 | @import "mixins/gradients"; 33 | 34 | // Layout 35 | @import "mixins/clearfix"; 36 | @import "mixins/center-block"; 37 | @import "mixins/nav-vertical-align"; 38 | @import "mixins/grid-framework"; 39 | @import "mixins/grid"; 40 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/bootstrap.less: -------------------------------------------------------------------------------- 1 | // Core variables and mixins 2 | @import "variables"; 3 | @import "mixins"; 4 | 5 | // Reset and dependencies 6 | @import "normalize"; 7 | @import "print"; 8 | @import "glyphicons"; 9 | 10 | // Core CSS 11 | @import "scaffolding"; 12 | @import "type"; 13 | @import "code"; 14 | @import "grid"; 15 | @import "tables"; 16 | @import "forms"; 17 | @import "buttons"; 18 | 19 | // Components 20 | @import "component-animations"; 21 | @import "dropdowns"; 22 | @import "button-groups"; 23 | @import "input-groups"; 24 | @import "navs"; 25 | @import "navbar"; 26 | @import "breadcrumbs"; 27 | @import "pagination"; 28 | @import "pager"; 29 | @import "labels"; 30 | @import "badges"; 31 | @import "jumbotron"; 32 | @import "thumbnails"; 33 | @import "alerts"; 34 | @import "progress-bars"; 35 | @import "media"; 36 | @import "list-group"; 37 | @import "panels"; 38 | @import "responsive-embed"; 39 | @import "wells"; 40 | @import "close"; 41 | 42 | // Components w/ JavaScript 43 | @import "modals"; 44 | @import "tooltip"; 45 | @import "popovers"; 46 | @import "carousel"; 47 | 48 | // Utility classes 49 | @import "utilities"; 50 | @import "responsive-utilities"; 51 | -------------------------------------------------------------------------------- /api/models/Reference.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Reference.js 3 | * 4 | * @description :: TODO: You might write a short summary of how this model works and what it represents here. 5 | * @docs :: http://sailsjs.org/#!documentation/models 6 | */ 7 | 8 | module.exports = { 9 | 10 | attributes: { 11 | url: { 12 | type: "string" 13 | }, 14 | user: "string", 15 | user2: "string", 16 | gave: "string", 17 | got: "string", 18 | description: "string", 19 | number: { 20 | type: "integer", 21 | min: 0, 22 | required: false 23 | }, 24 | type: { 25 | type: "string", 26 | enum: [ 27 | "event", 28 | "shiny", 29 | "casual", 30 | "bank", 31 | "egg", 32 | "giveaway", 33 | "involvement", 34 | "eggcheck", 35 | "misc" 36 | ] 37 | }, 38 | // This is true if the other user has added the same url, and the trade has been approved. 39 | verified: "boolean", 40 | // This defines if the mods have approved it as a trade that can count 41 | approved: "boolean", 42 | edited: "boolean", 43 | notes: "string", 44 | privatenotes: "string" 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /config/connections.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Connections 3 | * (sails.config.connections) 4 | * 5 | * `Connections` are like "saved settings" for your adapters. What's the difference between 6 | * a connection and an adapter, you might ask? An adapter (e.g. `sails-mysql`) is generic-- 7 | * it needs some additional information to work (e.g. your database host, password, user, etc.) 8 | * A `connection` is that additional information. 9 | * 10 | * Each model must have a `connection` property (a string) which is references the name of one 11 | * of these connections. If it doesn't, the default `connection` configured in `config/models.js` 12 | * will be applied. Of course, a connection can (and usually is) shared by multiple models. 13 | * . 14 | * Note: If you're using version control, you should put your passwords/api keys 15 | * in `config/local.js`, environment variables, or use another strategy. 16 | * (this is to prevent you inadvertently sensitive credentials up to your repository.) 17 | * 18 | * For more information on configuration, check out: 19 | * http://sailsjs.org/#/documentation/reference/sails.config/sails.config.connections.html 20 | */ 21 | 22 | module.exports.connections = { 23 | 24 | }; -------------------------------------------------------------------------------- /assets/styles/bootstrap/jumbotron.less: -------------------------------------------------------------------------------- 1 | // 2 | // Jumbotron 3 | // -------------------------------------------------- 4 | 5 | 6 | .jumbotron { 7 | padding: @jumbotron-padding; 8 | margin-bottom: @jumbotron-padding; 9 | color: @jumbotron-color; 10 | background-color: @jumbotron-bg; 11 | 12 | h1, 13 | .h1 { 14 | color: @jumbotron-heading-color; 15 | } 16 | p { 17 | margin-bottom: (@jumbotron-padding / 2); 18 | font-size: @jumbotron-font-size; 19 | font-weight: 200; 20 | } 21 | 22 | > hr { 23 | border-top-color: darken(@jumbotron-bg, 10%); 24 | } 25 | 26 | .container & { 27 | border-radius: @border-radius-large; // Only round corners at higher resolutions if contained in a container 28 | } 29 | 30 | .container { 31 | max-width: 100%; 32 | } 33 | 34 | @media screen and (min-width: @screen-sm-min) { 35 | padding-top: (@jumbotron-padding * 1.6); 36 | padding-bottom: (@jumbotron-padding * 1.6); 37 | 38 | .container & { 39 | padding-left: (@jumbotron-padding * 2); 40 | padding-right: (@jumbotron-padding * 2); 41 | } 42 | 43 | h1, 44 | .h1 { 45 | font-size: (@font-size-base * 4.5); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /assets/tooltip/tooltip.module.js: -------------------------------------------------------------------------------- 1 | var ng = require("angular"); 2 | var $ = require('jquery'); 3 | 4 | ng.module("tooltipModule", []).directive("ngTooltip", function () { 5 | return { 6 | restrict: 'E', 7 | replace: true, 8 | scope: { 9 | title: '@title', 10 | label: '@label' 11 | }, 12 | templateUrl: function (tElement, tAttrs) { 13 | if (tAttrs.unlabeled) { 14 | return '/tooltip/tooltip.view.html'; 15 | } else { 16 | return '/tooltip/label.view.html'; 17 | } 18 | }, 19 | transclude: true, 20 | link: function (scope, element) { 21 | var thisElement = $(element[0]).find('[data-toggle=tooltip]'); 22 | thisElement.tooltip({ 23 | html: true, 24 | trigger: 'manual', 25 | title: scope.title 26 | }).on("mouseenter", function () { 27 | thisElement.tooltip("show"); 28 | $(".tooltip").on("mouseleave", function () { 29 | thisElement.tooltip('hide'); 30 | }); 31 | }).on("mouseleave", function () { 32 | setTimeout(function () { 33 | if (!$(".tooltip:hover").length) { 34 | thisElement.tooltip("hide"); 35 | } 36 | }, 100); 37 | }); 38 | } 39 | }; 40 | }); -------------------------------------------------------------------------------- /config/local.example.js: -------------------------------------------------------------------------------- 1 | // This is an example file. Be sure to rename it to local.js and add valid credentials. 2 | module.exports = { 3 | port: 1337, 4 | environment: "development", 5 | hookTimeout: "50000", 6 | reddit: { 7 | clientID: "CLIENT ID GOES HERE", 8 | clientIDSecret: "SECRET ID GOES HERE", 9 | redirectURL: "http://localhost:1337/auth/reddit/callback", 10 | adminRefreshToken: "ADMIN REFRESH TOKEN GOES HERE", 11 | userAgent: 'FlairHQ development version by /u/DEVELOPERS_USERNAME || hq.porygon.co/info || v' + require('../package.json').version 12 | }, 13 | connections: { 14 | "default": "mongo", 15 | mongo: { 16 | adapter: 'sails-mongo', 17 | host: 'localhost', 18 | port: 27017, 19 | user: '', 20 | password: '', 21 | database: 'fapp' 22 | } 23 | }, 24 | session: { 25 | adapter: 'mongo', 26 | host: 'localhost', 27 | port: 27017, 28 | db: 'fapp', 29 | collection: 'sessions' 30 | }, 31 | discord: { 32 | client_id: 'ID GOES HERE', 33 | client_secret: 'SECRET HERE', 34 | redirect_host: 'http://localhost:1337', 35 | server_id: '111111', 36 | authenticatedRole_id: ['2222222'], 37 | bot_token: 'aaaaaaaaaaa' 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /api/models/Modmail.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | types: { 4 | stringOrNull: function (val) { 5 | return typeof val === 'string' || val === null; 6 | } 7 | }, 8 | 9 | autoPK: false, 10 | 11 | attributes: { 12 | name: { //The fullname ('t4_' + base36id) of the message 13 | columnName: 'id', 14 | type: 'string', 15 | unique: true, 16 | primaryKey: true 17 | }, 18 | subject: 'string', //Subject of the message 19 | body: 'string', //Body of the message 20 | author: 'string', //Username of the message author 21 | subreddit: { //The subreddit that the modmail was sent to 22 | enum: ['pokemontrades', 'SVExchange'] 23 | }, 24 | first_message_name: { //The fullname of the first message in this chain, or null if this is the first message 25 | stringOrNull: true 26 | }, 27 | created_utc: { //The UTC timestamp of when the message was created 28 | type: 'integer' 29 | }, 30 | parent_id: { //The fullname of the parent message, or null if this is the first message 31 | stringOrNull: true 32 | }, 33 | distinguished: { //This will be 'moderator' if the author was a mod, 'admin' if the author was a reddit admin, or null otherwise 34 | enum: ['moderator', 'admin', null] 35 | } 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/image.less: -------------------------------------------------------------------------------- 1 | // Image Mixins 2 | // - Responsive image 3 | // - Retina image 4 | 5 | 6 | // Responsive image 7 | // 8 | // Keep images from scaling beyond the width of their parents. 9 | .img-responsive(@display: block) { 10 | display: @display; 11 | width: 100% \9; // Force IE10 and below to size SVG images correctly 12 | max-width: 100%; // Part 1: Set a maximum relative to the parent 13 | height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching 14 | } 15 | 16 | 17 | // Retina image 18 | // 19 | // Short retina mixin for setting background-image and -size. Note that the 20 | // spelling of `min--moz-device-pixel-ratio` is intentional. 21 | .img-retina(@file-1x; @file-2x; @width-1x; @height-1x) { 22 | background-image: url("@{file-1x}"); 23 | 24 | @media 25 | only screen and (-webkit-min-device-pixel-ratio: 2), 26 | only screen and ( min--moz-device-pixel-ratio: 2), 27 | only screen and ( -o-min-device-pixel-ratio: 2/1), 28 | only screen and ( min-device-pixel-ratio: 2), 29 | only screen and ( min-resolution: 192dpi), 30 | only screen and ( min-resolution: 2dppx) { 31 | background-image: url("@{file-2x}"); 32 | background-size: @width-1x @height-1x; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /config/locales/_README.md: -------------------------------------------------------------------------------- 1 | # Internationalization / Localization Settings 2 | 3 | > Also see the official docs on internationalization/localization: 4 | > http://links.sailsjs.org/docs/config/locales 5 | 6 | ## Locales 7 | All locale files live under `config/locales`. Here is where you can add translations 8 | as JSON key-value pairs. The name of the file should match the language that you are supporting, which allows for automatic language detection based on request headers. 9 | 10 | Here is an example locale stringfile for the Spanish language (`config/locales/es.json`): 11 | ```json 12 | { 13 | "Hello!": "Hola!", 14 | "Hello %s, how are you today?": "¿Hola %s, como estas?", 15 | } 16 | ``` 17 | ## Usage 18 | Locales can be accessed in controllers/policies through `res.i18n()`, or in views through the `__(key)` or `i18n(key)` functions. 19 | Remember that the keys are case sensitive and require exact key matches, e.g. 20 | 21 | ```ejs 22 |

<%= __('Welcome to PencilPals!') %>

23 |

<%= i18n('Hello %s, how are you today?', 'Pencil Maven') %>

24 |

<%= i18n('That\'s right-- you can use either i18n() or __()') %>

25 | ``` 26 | 27 | ## Configuration 28 | Localization/internationalization config can be found in `config/i18n.js`, from where you can set your supported locales. 29 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/buttons.less: -------------------------------------------------------------------------------- 1 | // Button variants 2 | // 3 | // Easily pump out default styles, as well as :hover, :focus, :active, 4 | // and disabled options for all buttons 5 | 6 | .button-variant(@color; @background; @border) { 7 | color: @color; 8 | background-color: @background; 9 | border-color: @border; 10 | 11 | &:hover, 12 | &:focus, 13 | &:active, 14 | &.active, 15 | .open > .dropdown-toggle& { 16 | color: @color; 17 | background-color: darken(@background, 10%); 18 | border-color: darken(@border, 12%); 19 | } 20 | &:active, 21 | &.active, 22 | .open > .dropdown-toggle& { 23 | background-image: none; 24 | } 25 | &.disabled, 26 | &[disabled], 27 | fieldset[disabled] & { 28 | &, 29 | &:hover, 30 | &:focus, 31 | &:active, 32 | &.active { 33 | background-color: @background; 34 | border-color: @border; 35 | } 36 | } 37 | 38 | .badge { 39 | color: @background; 40 | background-color: @color; 41 | } 42 | } 43 | 44 | // Button sizes 45 | .button-size(@padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) { 46 | padding: @padding-vertical @padding-horizontal; 47 | font-size: @font-size; 48 | line-height: @line-height; 49 | border-radius: @border-radius; 50 | } 51 | -------------------------------------------------------------------------------- /api/services/Modmails.js: -------------------------------------------------------------------------------- 1 | var relevantKeys = ['name', 'subject', 'body', 'author', 'subreddit', 'first_message_name', 'created_utc', 'parent_id', 'distinguished']; 2 | var makeModmailObjects = function (modmails) { 3 | var all_modmails = []; 4 | for (let i = 0; i < modmails.length; i++) { 5 | let compressed = {}; 6 | for (let j = 0; j < relevantKeys.length; j++) { 7 | compressed[relevantKeys[j]] = modmails[i].data[relevantKeys[j]]; 8 | } 9 | all_modmails.push(compressed); 10 | if (modmails[i].data.replies) { 11 | all_modmails = all_modmails.concat(makeModmailObjects(modmails[i].data.replies.data.children)); 12 | } 13 | } 14 | return all_modmails; 15 | }; 16 | exports.updateArchive = async function (subreddit) { 17 | let most_recent = await Modmail.find({subreddit: subreddit, limit: 1, sort: 'created_utc DESC'}); 18 | if (!most_recent.length) { 19 | sails.log.warn('Modmail archives for /r/' + subreddit + ' could not be found for some reason. Recreating from scratch...'); 20 | return Modmail.findOrCreate(makeModmailObjects(await Reddit.getModmail(sails.config.reddit.adminRefreshToken, subreddit))); 21 | } 22 | return Modmail.findOrCreate(makeModmailObjects(await Reddit.getModmail(sails.config.reddit.adminRefreshToken, subreddit, undefined, most_recent[0].name))); 23 | }; 24 | -------------------------------------------------------------------------------- /tasks/config/watch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Run predefined tasks whenever watched file patterns are added, changed or deleted. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Watch for changes on 7 | * - files in the `assets` folder 8 | * - the `tasks/pipeline.js` file 9 | * and re-run the appropriate tasks. 10 | * 11 | * For usage docs see: 12 | * https://github.com/gruntjs/grunt-contrib-watch 13 | * 14 | */ 15 | module.exports = function (grunt) { 16 | 17 | grunt.config.set('watch', { 18 | api: { 19 | files: ['api/**/*'] 20 | }, 21 | less: { 22 | files: [ 23 | 'assets/styles/**/*' 24 | ], 25 | tasks: [ 26 | 'less:dev' 27 | ], 28 | options: { 29 | livereload: true, 30 | livereloadOnError: false 31 | } 32 | }, 33 | js: { 34 | files: [ 35 | 'assets/**/*.js' 36 | ], 37 | tasks: [ 38 | 'copy:dev', 39 | 'eslint' 40 | ], 41 | options: { 42 | livereload: true, 43 | livereloadOnError: false 44 | } 45 | } 46 | }); 47 | 48 | grunt.config.set('focus', { 49 | dev: { 50 | include: ['less', 'js'] 51 | } 52 | }); 53 | 54 | grunt.loadNpmTasks('grunt-focus'); 55 | grunt.loadNpmTasks('grunt-contrib-watch'); 56 | }; 57 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/badges.less: -------------------------------------------------------------------------------- 1 | // 2 | // Badges 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base class 7 | .badge { 8 | display: inline-block; 9 | min-width: 10px; 10 | padding: 3px 7px; 11 | font-size: @font-size-small; 12 | font-weight: @badge-font-weight; 13 | color: @badge-color; 14 | line-height: @badge-line-height; 15 | vertical-align: baseline; 16 | white-space: nowrap; 17 | text-align: center; 18 | background-color: @badge-bg; 19 | border-radius: @badge-border-radius; 20 | 21 | // Empty badges collapse automatically (not available in IE8) 22 | &:empty { 23 | display: none; 24 | } 25 | 26 | // Quick fix for badges in buttons 27 | .btn & { 28 | position: relative; 29 | top: -1px; 30 | } 31 | .btn-xs & { 32 | top: 0; 33 | padding: 1px 5px; 34 | } 35 | 36 | // Hover state, but only for links 37 | a& { 38 | &:hover, 39 | &:focus { 40 | color: @badge-link-hover-color; 41 | text-decoration: none; 42 | cursor: pointer; 43 | } 44 | } 45 | 46 | // Account for badges in navs 47 | a.list-group-item.active > &, 48 | .nav-pills > .active > a > & { 49 | color: @badge-active-color; 50 | background-color: @badge-active-bg; 51 | } 52 | .nav-pills > li > a > & { 53 | margin-left: 3px; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: cpp 2 | compiler: gcc 3 | sudo: false 4 | 5 | os: 6 | - linux 7 | - osx 8 | 9 | env: 10 | global: 11 | - SKIP_SASS_BINARY_DOWNLOAD_FOR_CI=true 12 | matrix: 13 | - export NODE_VERSION="6" 14 | 15 | matrix: 16 | fast_finish: true 17 | 18 | addons: 19 | apt: 20 | sources: 21 | - ubuntu-toolchain-r-test 22 | packages: 23 | - gcc-4.7 24 | - g++-4.7 25 | 26 | 27 | before_install: 28 | - git submodule update --init --recursive 29 | - git clone https://github.com/creationix/nvm.git ./.nvm 30 | - source ./.nvm/nvm.sh 31 | - nvm install $NODE_VERSION 32 | - nvm use $NODE_VERSION 33 | - npm config set python `which python` 34 | - if [ $TRAVIS_OS_NAME == "linux" ]; then 35 | export CC="gcc-4.7"; 36 | export CXX="g++-4.7"; 37 | export LINK="gcc-4.7"; 38 | export LINKXX="g++-4.7"; 39 | sudo apt install build-essential checkinstall libssl-dev; 40 | fi 41 | - gcc --version 42 | - g++ --version 43 | - rm package-lock.json 44 | - npm cache clean --f 45 | - rm -rf node_modules 46 | - npm install -g npm@latest-6 47 | 48 | before_script: 49 | - npm install -g grunt-cli 50 | - npm install -g eslint 51 | 52 | script: 53 | - npm install 54 | - npm test 55 | 56 | cache: 57 | directories: 58 | - $HOME/.node-gyp 59 | - $HOME/.npm 60 | -------------------------------------------------------------------------------- /api/models/User.js: -------------------------------------------------------------------------------- 1 | /** 2 | * User.js 3 | * 4 | * @description :: TODO: You might write a short summary of how this model works and what it represents here. 5 | * @docs :: http://sailsjs.org/#!documentation/models 6 | */ 7 | 8 | module.exports = { 9 | 10 | types: { 11 | friendCodeFormat: function (codes) { 12 | for (var code in codes) { 13 | var patt = /(?:SW-)?([0-9]{4})(-?)(?:([0-9]{4})\2)([0-9]{4})/; 14 | if (!patt.test(codes[code])) { 15 | return false; 16 | } 17 | } 18 | return true; 19 | } 20 | }, 21 | 22 | autoPK: false, 23 | 24 | attributes: { 25 | provider: 'STRING', 26 | name: { 27 | type: "string", 28 | columnName: 'id', 29 | unique: true, 30 | primaryKey: true 31 | }, 32 | email: "string", 33 | firstname: "string", 34 | lastname: "string", 35 | intro: { 36 | type: "text", 37 | maxLength: 10000 38 | }, 39 | friendCodes: { 40 | type: "array", 41 | friendCodeFormat: true 42 | }, 43 | loggedFriendCodes: { 44 | type: "array", 45 | friendCodeFormat: true 46 | }, 47 | isMod: "boolean", 48 | modPermissions: { 49 | type: "array", 50 | defaultsTo: null 51 | }, 52 | banned: "boolean", 53 | redToken: "string", 54 | flair: "json" 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/labels.less: -------------------------------------------------------------------------------- 1 | // 2 | // Labels 3 | // -------------------------------------------------- 4 | 5 | .label { 6 | display: inline; 7 | padding: .2em .6em .3em; 8 | font-size: 75%; 9 | font-weight: bold; 10 | line-height: 1; 11 | color: @label-color; 12 | text-align: center; 13 | white-space: nowrap; 14 | vertical-align: baseline; 15 | border-radius: .25em; 16 | 17 | // Add hover effects, but only for links 18 | a& { 19 | &:hover, 20 | &:focus { 21 | color: @label-link-hover-color; 22 | text-decoration: none; 23 | cursor: pointer; 24 | } 25 | } 26 | 27 | // Empty labels collapse automatically (not available in IE8) 28 | &:empty { 29 | display: none; 30 | } 31 | 32 | // Quick fix for labels in buttons 33 | .btn & { 34 | position: relative; 35 | top: -1px; 36 | } 37 | } 38 | 39 | // Colors 40 | // Contextual variations (linked labels get darker on :hover) 41 | 42 | .label-default { 43 | .label-variant(@label-default-bg); 44 | } 45 | 46 | .label-primary { 47 | .label-variant(@label-primary-bg); 48 | } 49 | 50 | .label-success { 51 | .label-variant(@label-success-bg); 52 | } 53 | 54 | .label-info { 55 | .label-variant(@label-info-bg); 56 | } 57 | 58 | .label-warning { 59 | .label-variant(@label-warning-bg); 60 | } 61 | 62 | .label-danger { 63 | .label-variant(@label-danger-bg); 64 | } 65 | -------------------------------------------------------------------------------- /config/log.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Built-in Log Configuration 3 | * (sails.config.log) 4 | * 5 | * Configure the log level for your app, as well as the transport 6 | * (Underneath the covers, Sails uses Winston for logging, which 7 | * allows for some pretty neat custom transports/adapters for log messages) 8 | * 9 | * For more information on the Sails logger, check out: 10 | * http://sailsjs.org/#/documentation/concepts/Logging 11 | */ 12 | 13 | module.exports.log = { 14 | 15 | /*************************************************************************** 16 | * * 17 | * Valid `level` configs: i.e. the minimum log level to capture with * 18 | * sails.log.*() * 19 | * * 20 | * The order of precedence for log levels from lowest to highest is: * 21 | * silly, verbose, info, debug, warn, error * 22 | * * 23 | * You may also set the level to "silent" to suppress all logs. * 24 | * * 25 | ***************************************************************************/ 26 | 27 | // level: 'info' 28 | 29 | }; 30 | -------------------------------------------------------------------------------- /assets/common/regexCommon.js: -------------------------------------------------------------------------------- 1 | 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|BD|SP|PLA)(, (X|Y|ΩR|αS|S|M|US|UM|LGP|LGE|SW|SH|BD|SP|PLA))*\\))?)(, ([^ ,|(]*( \\((X|Y|ΩR|αS|S|M|US|UM|LGP|LGE|SW|SH|BD|SP|PLA)(, (X|Y|ΩR|αS|S|M|US|UM|LGP|LGE|SW|SH|BD|SP|PLA))*\\))?))*"; 2 | var regex = { 3 | emoji: "(:[a-zA-Z0-9_-]*:)", 4 | tsv: "[0-3]\\d{3}|40(?:[0-8]\\d|9[0-5])", 5 | tsvBars: "(\\|\\| [0-9]{4})|(, [0-9]{4})", 6 | fc: "((?:SW-)?([0-9]{4}-){2}[0-9]{4})", 7 | console: "Switch|3DS", 8 | game: "((\\()|(,))(X|Y|ΩR|αS|S|M|US|UM|LGP|LGE|SW|SH|BD|SP|PLA)((,)|(\\)))", 9 | ign: "((\\d \\|\\|)|(\\),)) [^(|,]*( (\\()|(\\|)|(,)|$)", 10 | 11 | ptradesFlair: ptradesFlair, 12 | svexFlair: ptradesFlair + " \\|\\| ([0-9]{4}|XXXX)(, (([0-9]{4})|XXXX))*" 13 | }; 14 | 15 | var single = function (reg) { 16 | return new RegExp(reg); 17 | }; 18 | var global = function (reg) { 19 | return new RegExp(reg, "g"); 20 | }; 21 | 22 | module.exports = { 23 | emoji: global(regex.emoji), 24 | tsv: global(regex.tsvBars), 25 | fc: global(regex.fc), 26 | game: global(regex.game), 27 | ign: global(regex.ign), 28 | 29 | fcSingle: single(regex.fc), 30 | consoleSingle: single(regex.console), 31 | tsvSingle: single(regex.tsv), 32 | 33 | ptradesFlair: single(regex.ptradesFlair), 34 | svexFlair: single(regex.svexFlair) 35 | }; 36 | -------------------------------------------------------------------------------- /assets/tools/tools.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 |

Tools

7 | 8 |
9 |

Subreddit Greasemonkey Script

10 | 11 | 17 | 18 | 19 |
20 | 21 |
22 |

FlairHQ Dark Mode

23 | 24 | 28 | 29 |

Note, this will probably be pretty buggy, as it was mostly added just as a joke.

30 | 31 | 32 |
33 | 34 |
35 |
36 |
37 | -------------------------------------------------------------------------------- /config/bootstrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Bootstrap 3 | * (sails.config.bootstrap) 4 | * 5 | * An asynchronous bootstrap function that runs before your Sails app gets lifted. 6 | * This gives you an opportunity to set up your data model, run jobs, or perform some special logic. 7 | * 8 | * For more information on bootstrapping your app, check out: 9 | * http://sailsjs.org/#/documentation/reference/sails.config/sails.config.bootstrap.html 10 | */ 11 | 12 | module.exports.bootstrap = function(cb) { 13 | 14 | var passport = require('passport'), 15 | initialize = passport.initialize(), 16 | session = passport.session(), 17 | http = require('http'), 18 | methods = ['login', 'logIn', 'logout', 'logOut', 'isAuthenticated', 'isUnauthenticated']; 19 | 20 | sails.removeAllListeners('router:request'); 21 | sails.on('router:request', function(req, res) { 22 | initialize(req, res, function () { 23 | session(req, res, function (err) { 24 | if (err) { 25 | return sails.config[500](500, req, res); 26 | } 27 | for (var i = 0; i < methods.length; i++) { 28 | req[methods[i]] = http.IncomingMessage.prototype[methods[i]].bind(req); 29 | } 30 | sails.router.route(req, res); 31 | }); 32 | }); 33 | }); 34 | 35 | // It's very important to trigger this callback method when you are finished 36 | // with the bootstrap! (otherwise your server will never lift, since it's waiting on the bootstrap) 37 | cb(); 38 | }; 39 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI Action 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | schedule: 11 | - cron: '0 0 * * 0' # weekly 12 | 13 | jobs: 14 | build: 15 | runs-on: '${{ matrix.os }}' 16 | strategy: 17 | matrix: 18 | os: [ubuntu-20.04] 19 | node: [12] 20 | steps: 21 | - name: Cache NPM 22 | uses: actions/cache@v3 23 | with: 24 | path: ~/.npm 25 | key: ${{ runner.os }}-build-${{hashFiles('**/package-lock.json')}} 26 | restore-keys: | 27 | ${{ runner.os }}-build- 28 | ${{ runner.os }}- 29 | - uses: actions/checkout@v4 30 | - uses: actions/setup-node@v3 31 | with: 32 | node-version: ${{matrix.node}} 33 | - run: npm config set python `which python` 34 | - name: Before Install 35 | if: runner.os == 'Linux' 36 | run: |- 37 | export CC="gcc-9.3.0" 38 | export CXX="g++-9.3.0" 39 | export LINK="gcc-9.3.0" 40 | export LINKXX="g++-9.3.0" 41 | sudo apt install build-essential checkinstall libssl-dev 42 | - run: gcc --version 43 | - run: g++ --version 44 | - run: rm package-lock.json 45 | - run: npm cache clean --f 46 | - run: rm -rf node_modules 47 | - run: npm install -g grunt-cli 48 | - run: npm install -g eslint 49 | - run: npm install 50 | - run: npm test -------------------------------------------------------------------------------- /api/responses/ok.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 200 (OK) Response 3 | * 4 | * Usage: 5 | * return res.ok(); 6 | * return res.ok(data); 7 | * return res.ok(data, 'auth/login'); 8 | * 9 | * @param {Object} data 10 | * @param {String|Object} options 11 | * - pass string to render specified view 12 | */ 13 | 14 | module.exports = function sendOK (data, options) { 15 | 16 | // Get access to `req`, `res`, & `sails` 17 | var req = this.req; 18 | var res = this.res; 19 | var sails = req._sails; 20 | 21 | sails.log.silly('res.ok() :: Sending 200 ("OK") response'); 22 | 23 | // Set status code 24 | res.status(200); 25 | 26 | // If appropriate, serve data as JSON(P) 27 | if (req.wantsJSON) { 28 | return res.jsonx(data); 29 | } 30 | 31 | // If second argument is a string, we take that to mean it refers to a view. 32 | // If it was omitted, use an empty object (`{}`) 33 | options = (typeof options === 'string') ? { view: options } : options || {}; 34 | 35 | // If a view was provided in options, serve it. 36 | // Otherwise try to guess an appropriate view, or if that doesn't 37 | // work, just send JSON. 38 | if (options.view) { 39 | return res.view(options.view, { data: data }); 40 | } 41 | 42 | // If no second argument provided, try to serve the implied view, 43 | // but fall back to sending JSON(P) if no view can be inferred. 44 | else return res.guessView({ data: data }, function couldNotGuessView () { 45 | return res.jsonx(data); 46 | }); 47 | 48 | }; 49 | -------------------------------------------------------------------------------- /assets/search/main.ejs: -------------------------------------------------------------------------------- 1 |
4 |
5 |
6 |
7 |
8 |

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

9 |
10 |
11 |
12 |
13 | <%- partial(searchType + '/form.ejs') %> 14 |
15 |
16 |
17 |
18 | 39 |
40 |
41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /config/models.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default model configuration 3 | * (sails.config.models) 4 | * 5 | * Unless you override them, the following properties will be included 6 | * in each of your models. 7 | * 8 | * For more info on Sails models, see: 9 | * http://sailsjs.org/#/documentation/concepts/ORM 10 | */ 11 | 12 | module.exports.models = { 13 | 14 | /*************************************************************************** 15 | * * 16 | * Your app's default connection. i.e. the name of one of your app's * 17 | * connections (see `config/connections.js`) * 18 | * * 19 | ***************************************************************************/ 20 | connection: 'mongo', 21 | adapter: 'mongo', 22 | 23 | /*************************************************************************** 24 | * * 25 | * How and whether Sails will attempt to automatically rebuild the * 26 | * tables/collections/etc. in your schema. * 27 | * * 28 | * See http://sailsjs.org/#/documentation/concepts/ORM/model-settings.html * 29 | * * 30 | ***************************************************************************/ 31 | migrate: 'safe' 32 | 33 | }; 34 | -------------------------------------------------------------------------------- /config/env/production.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Production environment settings 3 | * 4 | * This file can include shared settings for a production environment, 5 | * such as API keys or remote database passwords. If you're using 6 | * a version control solution for your Sails app, this file will 7 | * be committed to your repository unless you add it to your .gitignore 8 | * file. If your repository will be publicly viewable, don't add 9 | * any private information to this file! 10 | * 11 | */ 12 | 13 | module.exports = { 14 | 15 | /*************************************************************************** 16 | * Set the default database connection for models in the production * 17 | * environment (see config/connections.js and config/models.js ) * 18 | ***************************************************************************/ 19 | 20 | // models: { 21 | // connection: 'someMysqlServer' 22 | // }, 23 | 24 | /*************************************************************************** 25 | * Set the port in the production environment to 80 * 26 | ***************************************************************************/ 27 | 28 | // port: 80, 29 | 30 | /*************************************************************************** 31 | * Set the log level in production environment to "silent" * 32 | ***************************************************************************/ 33 | 34 | log: { 35 | level: 'info', 36 | timestamp: true, 37 | timestampFormat: 'YYYY-MM-DD HH:mm:ss.SSS' 38 | } 39 | 40 | }; 41 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/code.less: -------------------------------------------------------------------------------- 1 | // 2 | // Code (inline and block) 3 | // -------------------------------------------------- 4 | 5 | 6 | // Inline and block code styles 7 | code, 8 | kbd, 9 | pre, 10 | samp { 11 | font-family: @font-family-monospace; 12 | } 13 | 14 | // Inline code 15 | code { 16 | padding: 2px 4px; 17 | font-size: 90%; 18 | color: @code-color; 19 | background-color: @code-bg; 20 | border-radius: @border-radius-base; 21 | } 22 | 23 | // User input typically entered via keyboard 24 | kbd { 25 | padding: 2px 4px; 26 | font-size: 90%; 27 | color: @kbd-color; 28 | background-color: @kbd-bg; 29 | border-radius: @border-radius-small; 30 | box-shadow: inset 0 -1px 0 rgba(0,0,0,.25); 31 | 32 | kbd { 33 | padding: 0; 34 | font-size: 100%; 35 | box-shadow: none; 36 | } 37 | } 38 | 39 | // Blocks of code 40 | pre { 41 | display: block; 42 | padding: ((@line-height-computed - 1) / 2); 43 | margin: 0 0 (@line-height-computed / 2); 44 | font-size: (@font-size-base - 1); // 14px to 13px 45 | line-height: @line-height-base; 46 | word-break: break-all; 47 | word-wrap: break-word; 48 | color: @pre-color; 49 | background-color: @pre-bg; 50 | border: 1px solid @pre-border-color; 51 | border-radius: @border-radius-base; 52 | 53 | // Account for some code outputs that place code tags in pre tags 54 | code { 55 | padding: 0; 56 | font-size: inherit; 57 | color: inherit; 58 | white-space: pre-wrap; 59 | background-color: transparent; 60 | border-radius: 0; 61 | } 62 | } 63 | 64 | // Enable scrollable blocks of code 65 | .pre-scrollable { 66 | max-height: @pre-scrollable-max-height; 67 | overflow-y: scroll; 68 | } 69 | -------------------------------------------------------------------------------- /assets/adminCtrl.js: -------------------------------------------------------------------------------- 1 | var shared = require('./sharedClientFunctions.js'); 2 | module.exports = function ($scope, io) { 3 | shared.addRepeats($scope, io); 4 | $scope.users = []; 5 | $scope.flairAppError = ""; 6 | $scope.adminok = { 7 | appFlair: {} 8 | }; 9 | $scope.adminspin = { 10 | appFlair: {} 11 | }; 12 | 13 | $scope.denyApp = function (id) { 14 | var url = "/flair/app/deny"; 15 | $scope.flairAppError = ""; 16 | 17 | io.socket.post(url, {id: id}, function (data, res) { 18 | if (res.statusCode === 200) { 19 | $scope.flairApps = data; 20 | } else if (res.statusCode === 404) { 21 | $scope.flairApps = data; 22 | $scope.flairAppError = "That app no longer exists."; 23 | } else { 24 | $scope.flairAppError = "Couldn't deny, for some reason."; 25 | console.log(data); 26 | } 27 | $scope.$apply(); 28 | }); 29 | }; 30 | 31 | $scope.approveApp = function (id) { 32 | $scope.adminok.appFlair[id] = false; 33 | $scope.adminspin.appFlair[id] = true; 34 | $scope.flairAppError = ""; 35 | var url = "/flair/app/approve"; 36 | 37 | io.socket.post(url, {id: id}, function (data, res) { 38 | if (res.statusCode === 200) { 39 | $scope.adminok.appFlair[id] = true; 40 | $scope.flairApps = data; 41 | } else if (res.statusCode === 404) { 42 | $scope.flairApps = data; 43 | $scope.flairAppError = "That app no longer exists."; 44 | } else { 45 | $scope.flairAppError = "Couldn't approve, for some reason."; 46 | console.log(data); 47 | } 48 | $scope.adminspin.appFlair[id] = false; 49 | $scope.$apply(); 50 | }); 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /test/unit/data/flairCssClasses.json: -------------------------------------------------------------------------------- 1 | { 2 | "pokemontrades": { 3 | "pokeball,premierball": "premierball", 4 | "greatball hok,ultraball": "ultraball hok", 5 | "ovalcharm1,shinycharm": "shinycharm1", 6 | "greatball,involvement": "greatball1", 7 | "ovalcharm,involvement": "ovalcharm1", 8 | "premierball hok,involvement": "premierball1 hok", 9 | ",banned": "banned", 10 | "pokeball,banned": "pokeball banned", 11 | "pokeball1,banned": "pokeball1 banned", 12 | "ovalcharm hok,banned": "ovalcharm banned", 13 | "ovalcharm1 hok,banned": "ovalcharm1 banned", 14 | "masterball1 hok something-else,banned": "masterball1 banned", 15 | "dreamball banned,banned": "dreamball banned", 16 | "banned,banned": "banned" 17 | }, 18 | 19 | "SVExchange": { 20 | ",lucky": "lucky", 21 | "lucky,egg": "egg", 22 | "eevee,togepi": "togepi", 23 | "togepi smartribbon,torchic": "torchic smartribbon", 24 | ",cuteribbon": "cuteribbon", 25 | "cuteribbon,coolribbon": "coolribbon", 26 | "manaphy smartribbon,toughribbon": "manaphy toughribbon", 27 | "eggcup,beautyribbon": "eggcup beautyribbon", 28 | "beautyribbon,eggcup": "eggcup beautyribbon", 29 | ",banned": "banned", 30 | "lucky,banned": "lucky banned", 31 | "lucky cuteribbon,banned": "lucky cuteribbon banned", 32 | "toughribbon,banned": "toughribbon banned", 33 | "banned,banned": "banned", 34 | "lucky banned,banned": "lucky banned", 35 | "lucky cuteribbon banned,banned":"lucky cuteribbon banned", 36 | "toughribbon banned,banned": "toughribbon banned", 37 | "pichu banned,toughribbon": "pichu toughribbon banned", 38 | "coolribbon banned,togepi": "togepi coolribbon banned" 39 | } 40 | } -------------------------------------------------------------------------------- /assets/styles/bootstrap/grid.less: -------------------------------------------------------------------------------- 1 | // 2 | // Grid system 3 | // -------------------------------------------------- 4 | 5 | 6 | // Container widths 7 | // 8 | // Set the container width, and override it for fixed navbars in media queries. 9 | 10 | .container { 11 | .container-fixed(); 12 | 13 | @media (min-width: @screen-sm-min) { 14 | width: @container-sm; 15 | } 16 | @media (min-width: @screen-md-min) { 17 | width: @container-md; 18 | } 19 | @media (min-width: @screen-lg-min) { 20 | width: @container-lg; 21 | } 22 | } 23 | 24 | 25 | // Fluid container 26 | // 27 | // Utilizes the mixin meant for fixed width containers, but without any defined 28 | // width for fluid, full width layouts. 29 | 30 | .container-fluid { 31 | .container-fixed(); 32 | } 33 | 34 | 35 | // Row 36 | // 37 | // Rows contain and clear the floats of your columns. 38 | 39 | .row { 40 | .make-row(); 41 | } 42 | 43 | 44 | // Columns 45 | // 46 | // Common styles for small and large grid columns 47 | 48 | .make-grid-columns(); 49 | 50 | 51 | // Extra small grid 52 | // 53 | // Columns, offsets, pushes, and pulls for extra small devices like 54 | // smartphones. 55 | 56 | .make-grid(xs); 57 | 58 | 59 | // Small grid 60 | // 61 | // Columns, offsets, pushes, and pulls for the small device range, from phones 62 | // to tablets. 63 | 64 | @media (min-width: @screen-sm-min) { 65 | .make-grid(sm); 66 | } 67 | 68 | 69 | // Medium grid 70 | // 71 | // Columns, offsets, pushes, and pulls for the desktop device range. 72 | 73 | @media (min-width: @screen-md-min) { 74 | .make-grid(md); 75 | } 76 | 77 | 78 | // Large grid 79 | // 80 | // Columns, offsets, pushes, and pulls for the large desktop device range. 81 | 82 | @media (min-width: @screen-lg-min) { 83 | .make-grid(lg); 84 | } 85 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/alerts.less: -------------------------------------------------------------------------------- 1 | // 2 | // Alerts 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base styles 7 | // ------------------------- 8 | 9 | .alert { 10 | padding: @alert-padding; 11 | margin-bottom: @line-height-computed; 12 | border: 1px solid transparent; 13 | border-radius: @alert-border-radius; 14 | 15 | // Headings for larger alerts 16 | h4 { 17 | margin-top: 0; 18 | // Specified for the h4 to prevent conflicts of changing @headings-color 19 | color: inherit; 20 | } 21 | // Provide class for links that match alerts 22 | .alert-link { 23 | font-weight: @alert-link-font-weight; 24 | } 25 | 26 | // Improve alignment and spacing of inner content 27 | > p, 28 | > ul { 29 | margin-bottom: 0; 30 | } 31 | > p + p { 32 | margin-top: 5px; 33 | } 34 | } 35 | 36 | // Dismissible alerts 37 | // 38 | // Expand the right padding and account for the close button's positioning. 39 | 40 | .alert-dismissable, // The misspelled .alert-dismissable was deprecated in 3.2.0. 41 | .alert-dismissible { 42 | padding-right: (@alert-padding + 20); 43 | 44 | // Adjust close link position 45 | .close { 46 | position: relative; 47 | top: -2px; 48 | right: -21px; 49 | color: inherit; 50 | } 51 | } 52 | 53 | // Alternate styles 54 | // 55 | // Generate contextual modifier classes for colorizing the alert. 56 | 57 | .alert-success { 58 | .alert-variant(@alert-success-bg; @alert-success-border; @alert-success-text); 59 | } 60 | .alert-info { 61 | .alert-variant(@alert-info-bg; @alert-info-border; @alert-info-text); 62 | } 63 | .alert-warning { 64 | .alert-variant(@alert-warning-bg; @alert-warning-border; @alert-warning-text); 65 | } 66 | .alert-danger { 67 | .alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text); 68 | } 69 | -------------------------------------------------------------------------------- /test/unit/markdown/remapURLs.test.js: -------------------------------------------------------------------------------- 1 | var assert = require("chai").assert; 2 | var remapURLs = require("../../../assets/markdown/remapURLs"); 3 | 4 | var markdownData = require("../data/markdownStrings.json"); 5 | 6 | describe("Markdown User URLs", function () { 7 | it("Replaces " + markdownData.userUrl + " with " + markdownData.userUrlAfter, function () { 8 | var test = remapURLs(markdownData.userUrl); 9 | assert.equal(test, markdownData.userUrlAfter, "Not mapping user urls correctly."); 10 | }); 11 | 12 | it("Replaces " + markdownData.userUrlWithExtra + " with " + markdownData.userUrlWithExtraAfter, function () { 13 | var test = remapURLs(markdownData.userUrlWithExtra); 14 | assert.equal(test, markdownData.userUrlWithExtraAfter, "Not mapping user urls correctly."); 15 | }); 16 | 17 | it("Replaces " + markdownData.userUrlWrong + " with " + markdownData.userUrlWrongAfter, function () { 18 | var test = remapURLs(markdownData.userUrlWrong); 19 | assert.equal(test, markdownData.userUrlWrongAfter, "Not mapping user urls correctly."); 20 | }); 21 | }); 22 | 23 | describe("Markdown Subreddit URLs", function () { 24 | it("Replaces " + markdownData.subUrl + " with " + markdownData.subUrlAfter, function () { 25 | var test = remapURLs(markdownData.subUrl); 26 | assert.equal(test, markdownData.subUrlAfter, "Not mapping sub urls correctly."); 27 | }); 28 | 29 | it("Replaces " + markdownData.subUrlWithExtra + " with " + markdownData.subUrlWithExtraAfter, function () { 30 | var test = remapURLs(markdownData.subUrlWithExtra); 31 | assert.equal(test, markdownData.subUrlWithExtraAfter, "Not mapping sub urls correctly."); 32 | }); 33 | 34 | it("Replaces " + markdownData.subUrlWrong + " with " + markdownData.subUrlWrongAfter, function () { 35 | var test = remapURLs(markdownData.subUrlWrong); 36 | assert.equal(test, markdownData.subUrlWrongAfter, "Not mapping sub urls correctly."); 37 | }); 38 | }); -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * app.js 3 | * 4 | * Use `app.js` to run your app without `sails lift`. 5 | * To start the server, run: `node app.js`. 6 | * 7 | * This is handy in situations where the sails CLI is not relevant or useful. 8 | * 9 | * For example: 10 | * => `node app.js` 11 | * => `forever start app.js` 12 | * => `node debug app.js` 13 | * => `modulus deploy` 14 | * => `heroku scale` 15 | * 16 | * 17 | * The same command-line arguments are supported, e.g.: 18 | * `node app.js --silent --port=80 --prod` 19 | */ 20 | 21 | // Ensure a "sails" can be located: 22 | (function() { 23 | var sails; 24 | try { 25 | sails = require('sails'); 26 | } catch (e) { 27 | console.error('To run an app using `node app.js`, you usually need to have a version of `sails` installed in the same directory as your app.'); 28 | console.error('To do that, run `npm install sails`'); 29 | console.error(''); 30 | console.error('Alternatively, if you have sails installed globally (i.e. you did `npm install -g sails`), you can use `sails lift`.'); 31 | console.error('When you run `sails lift`, your app will still use a local `./node_modules/sails` dependency if it exists,'); 32 | console.error('but if it doesn\'t, the app will run with the global sails instead!'); 33 | return; 34 | } 35 | 36 | // Try to get `rc` dependency 37 | var rc; 38 | try { 39 | rc = require('rc'); 40 | } catch (e0) { 41 | try { 42 | rc = require('sails/node_modules/rc'); 43 | } catch (e1) { 44 | console.error('Could not find dependency: `rc`.'); 45 | console.error('Your `.sailsrc` file(s) will be ignored.'); 46 | console.error('To resolve this, run:'); 47 | console.error('npm install rc --save'); 48 | rc = function () { return {}; }; 49 | } 50 | } 51 | 52 | require("babel-core/register")({/* babel options */}); 53 | 54 | // Start server 55 | sails.lift(rc('sails')); 56 | })(); 57 | -------------------------------------------------------------------------------- /config/session.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Session Configuration 3 | * (sails.config.session) 4 | * 5 | * Sails session integration leans heavily on the great work already done by 6 | * Express, but also unifies Socket.io with the Connect session store. It uses 7 | * Connect's cookie parser to normalize configuration differences between Express 8 | * and Socket.io and hooks into Sails' middleware interpreter to allow you to access 9 | * and auto-save to `req.session` with Socket.io the same way you would with Express. 10 | * 11 | * For more information on configuring the session, check out: 12 | * http://sailsjs.org/#/documentation/reference/sails.config/sails.config.session.html 13 | */ 14 | 15 | module.exports.session = { 16 | 17 | /*************************************************************************** 18 | * * 19 | * Session secret is automatically generated when your new app is created * 20 | * Replace at your own risk in production-- you will invalidate the cookies * 21 | * of your users, forcing them to log in again. * 22 | * * 23 | ***************************************************************************/ 24 | secret: '5750234438bfe009', 25 | 26 | /*************************************************************************** 27 | * * 28 | * Set the session cookie expire time The maxAge is set by milliseconds, * 29 | * the example below is for 24 hours * 30 | * * 31 | ***************************************************************************/ 32 | 33 | cookie: { 34 | maxAge: 30 * 24 * 60 * 60 * 1000 35 | }, 36 | 37 | autoReconnect: true 38 | 39 | }; 40 | -------------------------------------------------------------------------------- /api/responses/badRequest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 400 (Bad Request) Handler 3 | * 4 | * Usage: 5 | * return res.badRequest(); 6 | * return res.badRequest(data); 7 | * return res.badRequest(data, 'some/specific/badRequest/view'); 8 | * 9 | * e.g.: 10 | * ``` 11 | * return res.badRequest( 12 | * 'Please choose a valid `password` (6-12 characters)', 13 | * 'trial/signup' 14 | * ); 15 | * ``` 16 | */ 17 | 18 | module.exports = function badRequest(data, options) { 19 | 20 | // Get access to `req`, `res`, & `sails` 21 | var req = this.req; 22 | var res = this.res; 23 | var sails = req._sails; 24 | 25 | // Set status code 26 | res.status(400); 27 | 28 | // Log error to console 29 | if (data !== undefined) { 30 | sails.log.verbose('Sending 400 ("Bad Request") response: \n',data); 31 | } 32 | else sails.log.verbose('Sending 400 ("Bad Request") response'); 33 | 34 | // Only include errors in response if application environment 35 | // is not set to 'production'. In production, we shouldn't 36 | // send back any identifying information about errors. 37 | if (sails.config.environment === 'production') { 38 | data = undefined; 39 | } 40 | 41 | // If the user-agent wants JSON, always respond with JSON 42 | if (req.wantsJSON) { 43 | return res.jsonx(data); 44 | } 45 | 46 | // If second argument is a string, we take that to mean it refers to a view. 47 | // If it was omitted, use an empty object (`{}`) 48 | options = (typeof options === 'string') ? { view: options } : options || {}; 49 | 50 | // If a view was provided in options, serve it. 51 | // Otherwise try to guess an appropriate view, or if that doesn't 52 | // work, just send JSON. 53 | if (options.view) { 54 | return res.view(options.view, { data: data }); 55 | } 56 | 57 | // If no second argument provided, try to serve the implied view, 58 | // but fall back to sending JSON(P) if no view can be inferred. 59 | else return res.guessView({ data: data }, function couldNotGuessView () { 60 | return res.jsonx(data); 61 | }); 62 | 63 | }; 64 | 65 | -------------------------------------------------------------------------------- /assets/app.js: -------------------------------------------------------------------------------- 1 | var ng = require('angular'); 2 | var $ = require('jquery'); 3 | var _csrf = $('#app').attr('_csrf'); 4 | 5 | var refCtrl = require('./refCtrl'); 6 | var indexCtrl = require('./indexCtrl'); 7 | var adminCtrl = require('./adminCtrl'); 8 | var banCtrl = require('./banCtrl'); 9 | var userCtrl = require('./userCtrl'); 10 | require('./search/search.module'); 11 | require('./markdown/markdown.module'); 12 | require('angular-spinner'); 13 | require('angular-bootstrap-npm'); 14 | require('angular-mask'); 15 | require('bootstrap'); 16 | require('./ngReallyClick'); 17 | require('./tooltip/tooltip.module'); 18 | require('./numberPadding'); 19 | //require('spin'); 20 | 21 | var fapp = ng.module("fapp", [ 22 | 'angularSpinner', 23 | 'ngReallyClickModule', 24 | 'numberPaddingModule', 25 | 'tooltipModule', 26 | 'ngMask', 27 | 'fapp.search', 28 | 'fapp.md' 29 | ]); 30 | 31 | fapp.config(['$locationProvider', function($locationProvider) { 32 | $locationProvider.hashPrefix(''); 33 | }]); 34 | 35 | fapp.service('io', function () { 36 | var socket = require('socket.io-client'); 37 | var io = require('sails.io.js')(socket); 38 | io.socket.post = function (url, data, callback) { 39 | data._csrf = _csrf; 40 | io.socket.request({method: 'post', url: url, params: data}, callback); 41 | }; 42 | return io; 43 | }); 44 | 45 | // Define controllers, and their angular dependencies 46 | fapp.controller("referenceCtrl", ['$scope', 'io', refCtrl]); 47 | fapp.controller("indexCtrl", ['$scope', 'io', indexCtrl]); 48 | fapp.controller("userCtrl", ['$scope', '$location', 'io', userCtrl]); 49 | fapp.controller("adminCtrl", ['$scope', 'io', adminCtrl]); 50 | fapp.controller("banCtrl", ['$scope', 'io', banCtrl]); 51 | 52 | // Bug fix for iOS safari 53 | $(function () { 54 | $("[data-toggle='collapse']").click(function () { 55 | // For some reason, iOS safari doesn't let collapse work on a div if it 56 | // doesn't have a click handler. The click handler doesn't need to do anything. 57 | }); 58 | }); 59 | 60 | ng.bootstrap(document, ['fapp'], {strictDi: true}); 61 | -------------------------------------------------------------------------------- /assets/search/header.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/print.less: -------------------------------------------------------------------------------- 1 | // 2 | // Basic print styles 3 | // -------------------------------------------------- 4 | // Source: https://github.com/h5bp/html5-boilerplate/blob/master/css/main.css 5 | 6 | @media print { 7 | 8 | * { 9 | text-shadow: none !important; 10 | color: #000 !important; // Black prints faster: h5bp.com/s 11 | background: transparent !important; 12 | box-shadow: none !important; 13 | } 14 | 15 | a, 16 | a:visited { 17 | text-decoration: underline; 18 | } 19 | 20 | a[href]:after { 21 | content: " (" attr(href) ")"; 22 | } 23 | 24 | abbr[title]:after { 25 | content: " (" attr(title) ")"; 26 | } 27 | 28 | // Don't show links for images, or javascript/internal links 29 | a[href^="javascript:"]:after, 30 | a[href^="#"]:after { 31 | content: ""; 32 | } 33 | 34 | pre, 35 | blockquote { 36 | border: 1px solid #999; 37 | page-break-inside: avoid; 38 | } 39 | 40 | thead { 41 | display: table-header-group; // h5bp.com/t 42 | } 43 | 44 | tr, 45 | img { 46 | page-break-inside: avoid; 47 | } 48 | 49 | img { 50 | max-width: 100% !important; 51 | } 52 | 53 | p, 54 | h2, 55 | h3 { 56 | orphans: 3; 57 | widows: 3; 58 | } 59 | 60 | h2, 61 | h3 { 62 | page-break-after: avoid; 63 | } 64 | 65 | // Chrome (OSX) fix for https://github.com/twbs/bootstrap/issues/11245 66 | // Once fixed, we can just straight up remove this. 67 | select { 68 | background: #fff !important; 69 | } 70 | 71 | // Bootstrap components 72 | .navbar { 73 | display: none; 74 | } 75 | .table { 76 | td, 77 | th { 78 | background-color: #fff !important; 79 | } 80 | } 81 | .btn, 82 | .dropup > .btn { 83 | > .caret { 84 | border-top-color: #000 !important; 85 | } 86 | } 87 | .label { 88 | border: 1px solid #000; 89 | } 90 | 91 | .table { 92 | border-collapse: collapse !important; 93 | } 94 | .table-bordered { 95 | th, 96 | td { 97 | border: 1px solid #ddd !important; 98 | } 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /tasks/pipeline.js: -------------------------------------------------------------------------------- 1 | /** 2 | * grunt/pipeline.js 3 | * 4 | * The order in which your css, javascript, and template files should be 5 | * compiled and linked from your views and static HTML files. 6 | * 7 | * (Note that you can take advantage of Grunt-style wildcard/glob/splat expressions 8 | * for matching multiple files.) 9 | */ 10 | 11 | 12 | 13 | // CSS files to inject in order 14 | // 15 | // (if you're using LESS with the built-in default config, you'll want 16 | // to change `assets/styles/importer.less` instead.) 17 | var cssFilesToInject = [ 18 | 'styles/**/*.css' 19 | ]; 20 | 21 | 22 | // Client-side javascript files to inject in order 23 | // (uses Grunt-style wildcard/glob/splat expressions) 24 | var jsFilesToInject = [ 25 | 'js/app.js' 26 | ]; 27 | var prodJSFilesToInject = [ 28 | 'min/production.min.js' 29 | ]; 30 | 31 | 32 | // Client-side HTML templates are injected using the sources below 33 | // The ordering of these templates shouldn't matter. 34 | // (uses Grunt-style wildcard/glob/splat expressions) 35 | // 36 | // By default, Sails uses JST templates and precompiles them into 37 | // functions for you. If you want to use jade, handlebars, dust, etc., 38 | // with the linker, no problem-- you'll just want to make sure the precompiled 39 | // templates get spit out to the same file. Be sure and check out `tasks/README.md` 40 | // for information on customizing and installing new tasks. 41 | var templateFilesToInject = [ 42 | 'templates/**/*.html' 43 | ]; 44 | 45 | 46 | // Prefix relative paths to source files so they point to the proper locations 47 | // (i.e. where the other Grunt tasks spit them out, or in some cases, where 48 | // they reside in the first place) 49 | module.exports.cssFilesToInject = cssFilesToInject.map(function (path) { 50 | return '.tmp/public/' + path; 51 | }); 52 | module.exports.prodJSFilesToInject = prodJSFilesToInject.map(function (path) { 53 | return '.tmp/public/' + path; 54 | }); 55 | module.exports.jsFilesToInject = jsFilesToInject.map(function (path) { 56 | return '.tmp/public/' + path; 57 | }); 58 | module.exports.templateFilesToInject = templateFilesToInject.map(function (path) { 59 | return 'assets/' + path; 60 | }); 61 | -------------------------------------------------------------------------------- /config/policies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Policy Mappings 3 | * (sails.config.policies) 4 | * 5 | * Policies are simple functions which run **before** your controllers. 6 | * You can apply one or more policies to a given controller, or protect 7 | * its actions individually. 8 | * 9 | * Any policy file (e.g. `api/policies/authenticated.js`) can be accessed 10 | * below by its filename, minus the extension, (e.g. "authenticated") 11 | * 12 | * For more information on how policies work, see: 13 | * http://sailsjs.org/#/documentation/concepts/Policies 14 | * 15 | * For more information on configuring policies, check out: 16 | * http://sailsjs.org/#/documentation/reference/sails.config/sails.config.policies.html 17 | */ 18 | 19 | const anyone = ['passport']; 20 | const user = ['passport', 'sessionAuth']; 21 | const flairMod = ['passport', 'sessionAuth', 'isFlairMod']; 22 | const postMod = ['passport', 'sessionAuth', 'isPostMod']; 23 | const admin = ['passport', 'sessionAuth', 'isAdmin']; 24 | 25 | module.exports.policies = { 26 | 27 | '*': admin, 28 | 29 | AuthController: { 30 | '*': anyone 31 | }, 32 | 33 | FlairController: { 34 | '*': admin, 35 | applist: flairMod, 36 | apply: user, 37 | setText: user, 38 | denyApp: flairMod, 39 | approveApp: flairMod 40 | }, 41 | 42 | HomeController: { 43 | '*': admin, 44 | index: user, 45 | reference: anyone, 46 | search: user, 47 | info: anyone, 48 | tools: anyone, 49 | applist: flairMod, 50 | discord: user 51 | }, 52 | 53 | ReferenceController: { 54 | '*': admin, 55 | get: user, 56 | add: user, 57 | edit: user, 58 | deleteRef: user, 59 | comment: user, 60 | delComment: user, 61 | approve: flairMod, 62 | approveAll: flairMod, 63 | saveFlairs: flairMod, 64 | getFlairs: user 65 | }, 66 | 67 | SearchController: { 68 | '*': admin, 69 | ref: user, 70 | refView: user, 71 | user: user, 72 | userView: user 73 | }, 74 | 75 | UserController: { 76 | '*': admin, 77 | edit: user, 78 | mine: user, 79 | get: anyone 80 | }, 81 | 82 | ModNoteController: { 83 | '*': admin, 84 | find: postMod 85 | } 86 | }; 87 | -------------------------------------------------------------------------------- /api/services/Users.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | var removeSecretInformation = function (user) { 6 | user.redToken = undefined; 7 | user.loggedFriendCodes = undefined; 8 | if (user.apps) { 9 | user.apps.forEach(function (app) { 10 | app.claimedBy = undefined; 11 | }); 12 | } 13 | return user; 14 | }; 15 | 16 | exports.get = async function (requester, username) { 17 | var user = await User.findOne(username); 18 | if (!user) { 19 | throw {statusCode: 404}; 20 | } 21 | var promises = []; 22 | 23 | promises.push(Game.find({user: user.name}).sort({createdAt: 'desc'}).then(function (result) { 24 | user.games = result; 25 | })); 26 | 27 | promises.push(Comment.find({user: user.name}).sort({createdAt: 'desc'}).then(function (result) { 28 | user.comments = result; 29 | })); 30 | 31 | if (Users.hasModPermission(requester, 'posts') && Users.hasModPermission(requester, 'wiki')) { 32 | promises.push(ModNote.find({refUser: user.name}).sort({createdAt: 'desc'}).then(function (result) { 33 | user.modNotes = result; 34 | })); 35 | } 36 | 37 | if (requester && requester.name === user.name) { 38 | promises.push(Flairs.getApps(user.name).then(function (result) { 39 | user.apps = result; 40 | })); 41 | } 42 | 43 | promises.push(Reference.find({user: user.name}).sort({type: 'asc', createdAt: 'desc'}).then(function (result) { 44 | result.forEach(function (ref) { 45 | if (!requester || requester.name !== user.name) { 46 | ref.privatenotes = undefined; 47 | } 48 | if (!Users.hasModPermission(requester, 'flair')) { 49 | ref.approved = undefined; 50 | ref.verified = undefined; 51 | } 52 | }); 53 | user.references = result; 54 | })); 55 | await* promises; 56 | return removeSecretInformation(user); 57 | }; 58 | 59 | // Returns a promise for all banned users 60 | exports.getBannedUsers = function () { 61 | return User.find({banned: true}).then(function (results) { 62 | return results.map(removeSecretInformation); 63 | }); 64 | }; 65 | 66 | exports.hasModPermission = (user, modPermission) => { 67 | return user && user.isMod && user.modPermissions && (_.includes(user.modPermissions, 'all') || _.includes(user.modPermissions, modPermission)); 68 | }; 69 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/pagination.less: -------------------------------------------------------------------------------- 1 | // 2 | // Pagination (multiple pages) 3 | // -------------------------------------------------- 4 | .pagination { 5 | display: inline-block; 6 | padding-left: 0; 7 | margin: @line-height-computed 0; 8 | border-radius: @border-radius-base; 9 | 10 | > li { 11 | display: inline; // Remove list-style and block-level defaults 12 | > a, 13 | > span { 14 | position: relative; 15 | float: left; // Collapse white-space 16 | padding: @padding-base-vertical @padding-base-horizontal; 17 | line-height: @line-height-base; 18 | text-decoration: none; 19 | color: @pagination-color; 20 | background-color: @pagination-bg; 21 | border: 1px solid @pagination-border; 22 | margin-left: -1px; 23 | } 24 | &:first-child { 25 | > a, 26 | > span { 27 | margin-left: 0; 28 | .border-left-radius(@border-radius-base); 29 | } 30 | } 31 | &:last-child { 32 | > a, 33 | > span { 34 | .border-right-radius(@border-radius-base); 35 | } 36 | } 37 | } 38 | 39 | > li > a, 40 | > li > span { 41 | &:hover, 42 | &:focus { 43 | color: @pagination-hover-color; 44 | background-color: @pagination-hover-bg; 45 | border-color: @pagination-hover-border; 46 | } 47 | } 48 | 49 | > .active > a, 50 | > .active > span { 51 | &, 52 | &:hover, 53 | &:focus { 54 | z-index: 2; 55 | color: @pagination-active-color; 56 | background-color: @pagination-active-bg; 57 | border-color: @pagination-active-border; 58 | cursor: default; 59 | } 60 | } 61 | 62 | > .disabled { 63 | > span, 64 | > span:hover, 65 | > span:focus, 66 | > a, 67 | > a:hover, 68 | > a:focus { 69 | color: @pagination-disabled-color; 70 | background-color: @pagination-disabled-bg; 71 | border-color: @pagination-disabled-border; 72 | cursor: not-allowed; 73 | } 74 | } 75 | } 76 | 77 | // Sizing 78 | // -------------------------------------------------- 79 | 80 | // Large 81 | .pagination-lg { 82 | .pagination-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @border-radius-large); 83 | } 84 | 85 | // Small 86 | .pagination-sm { 87 | .pagination-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @border-radius-small); 88 | } 89 | -------------------------------------------------------------------------------- /assets/styles/fonts.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Open Sans'; 3 | font-style: normal; 4 | font-weight: 300; 5 | src: local('Open Sans Light'), local('OpenSans-Light'), url(/fonts/OpenSans-Light.woff) format('woff'); 6 | } 7 | @font-face { 8 | font-family: 'Open Sans'; 9 | font-style: normal; 10 | font-weight: 400; 11 | src: local('Open Sans'), local('OpenSans'), url(/fonts/OpenSans.woff) format('woff'); 12 | } 13 | @font-face { 14 | font-family: 'Open Sans'; 15 | font-style: normal; 16 | font-weight: 700; 17 | src: local('Open Sans Bold'), local('OpenSans-Bold'), url(/fonts/OpenSans-Bold.woff) format('woff'); 18 | } 19 | @font-face { 20 | font-family: 'Open Sans'; 21 | font-style: italic; 22 | font-weight: 300; 23 | src: local('Open Sans Light Italic'), local('OpenSansLight-Italic'), url(/fonts/OpenSansLight-Italic.woff) format('woff'); 24 | } 25 | @font-face { 26 | font-family: 'Open Sans'; 27 | font-style: italic; 28 | font-weight: 700; 29 | src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(/fonts/OpenSans-BoldItalic.woff) format('woff'); 30 | } 31 | 32 | 33 | /* 34 | * Web Fonts from fontspring.com 35 | * 36 | * All OpenType features and all extended glyphs have been removed. 37 | * Fully installable fonts can be purchased at http://www.fontspring.com 38 | * 39 | * The fonts included in this stylesheet are subject to the End User License you purchased 40 | * from Fontspring. The fonts are protected under domestic and international trademark and 41 | * copyright law. You are prohibited from modifying, reverse engineering, duplicating, or 42 | * distributing this font software. 43 | * 44 | * (c) 2010-2014 Fontspring 45 | * 46 | * 47 | * 48 | * 49 | * The fonts included are copyrighted by the vendor listed below. 50 | * 51 | * Vendor: Fontfabric 52 | * License URL: http://www.fontspring.com/fflicense/fontfabric 53 | * 54 | * 55 | */ 56 | 57 | @font-face { 58 | font-family: 'nexa_lightregular'; 59 | src: url(/fonts/Nexa_Free_Light-webfont.eot); 60 | src: url(/fonts/Nexa_Free_Light-webfont.eot?#iefix) format('embedded-opentype'), 61 | url(/fonts/Nexa_Free_Light-webfont.woff) format('woff'), 62 | url(/fonts/Nexa_Free_Light-webfont.ttf) format('truetype'), 63 | url(/fonts/Nexa_Free_Light-webfont.svg#nexa_lightregular) format('svg'); 64 | font-weight: normal; 65 | font-style: normal; 66 | 67 | } 68 | 69 | -------------------------------------------------------------------------------- /assets/ngReallyClick.js: -------------------------------------------------------------------------------- 1 | var ng = require("angular"), 2 | _ = require('lodash'); 3 | 4 | ng.module('ngReallyClickModule', ['ui.bootstrap']) 5 | .controller('ngReallyClickCtrl', ['$scope', '$modalInstance', function ($scope, $modalInstance) { 6 | $scope.ok = function () { 7 | $modalInstance.close(); 8 | }; 9 | 10 | $scope.cancel = function () { 11 | $modalInstance.dismiss('cancel'); 12 | }; 13 | }]) 14 | .directive('ngReallyClick', ['$uibModal', 15 | function ($uibModal) { 16 | 17 | return { 18 | restrict: 'A', 19 | scope: { 20 | ngReallyClick: "&" 21 | }, 22 | link: function (scope, element, attrs) { 23 | element.bind('click', function () { 24 | var user = attrs.ngReallyUser; 25 | var flair = attrs.ngReallyFlair; 26 | var switchInfo = attrs.ngReallySwitch; 27 | var modalHtml = ""; 28 | var deleteHtml = ''; 31 | var denyHtml = ''; 34 | var defaultHtml = ''; 37 | 38 | switch (switchInfo) { 39 | case "deleteRef": 40 | modalHtml = deleteHtml; 41 | break; 42 | case "denyApp": 43 | modalHtml = denyHtml; 44 | break; 45 | default: 46 | modalHtml = defaultHtml; 47 | break; 48 | } 49 | 50 | modalHtml += ''; 54 | 55 | var modalInstance = $uibModal.open({ 56 | template: modalHtml, 57 | controller: 'ngReallyClickCtrl' 58 | }); 59 | 60 | modalInstance.result.then(function () { 61 | scope.ngReallyClick(); 62 | }, function () { 63 | //Modal dismissed 64 | }); 65 | 66 | }); 67 | 68 | } 69 | }; 70 | } 71 | ]); -------------------------------------------------------------------------------- /api/controllers/HomeController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * HomeController.js 3 | * 4 | * @description :: 5 | * @docs :: http://sailsjs.org/#!documentation/controllers 6 | */ 7 | 8 | var _ = require("lodash"); 9 | 10 | module.exports = { 11 | 12 | index: async function (req, res) { 13 | res.view({refUser: await Users.get(req.user, req.user.name)}); 14 | Reddit.getBothFlairs(sails.config.reddit.adminRefreshToken, req.user.name).then(function (flairs) { 15 | if (flairs[0] || flairs[1]) { 16 | req.user.flair = {ptrades: flairs[0], svex: flairs[1]}; 17 | var ptrades_fcs, svex_fcs; 18 | if (flairs[0] && flairs[0].flair_text) { 19 | ptrades_fcs = flairs[0].flair_text.match(/(\d{4}-){2}\d{4}/g); 20 | } 21 | if (flairs[1] && flairs[1].flair_text) { 22 | svex_fcs = flairs[1].flair_text.match(/(\d{4}-){2}\d{4}/g); 23 | } 24 | req.user.loggedFriendCodes = _.union(ptrades_fcs, svex_fcs, req.user.loggedFriendCodes); 25 | req.user.save(function (err) { 26 | if (err) { 27 | sails.log.error(err); 28 | } 29 | }); 30 | } 31 | }, sails.log.error); 32 | }, 33 | 34 | reference: async function(req, res) { 35 | try { 36 | return res.view({refUser: await Users.get(req.user, req.params.user)}); 37 | } catch (err) { 38 | if (err.statusCode === 404) { 39 | return res.view('404', {data: {user: req.params.user, error: "User not found"}}); 40 | } 41 | return res.serverError(err); 42 | } 43 | }, 44 | 45 | banlist: async function (req, res) { 46 | try { 47 | return res.view({bannedUsers: await Users.getBannedUsers()}); 48 | } catch (err) { 49 | return res.serverError(err); 50 | } 51 | }, 52 | 53 | banuser: function (req, res) { 54 | res.view(); 55 | }, 56 | 57 | applist: function (req, res) { 58 | res.view(); 59 | }, 60 | 61 | info: function (req, res) { 62 | res.view(); 63 | }, 64 | 65 | tools: function (req, res) { 66 | res.view("../tools/tools.ejs"); 67 | }, 68 | 69 | version: function(req, res) { 70 | res.ok(sails.config.version); 71 | }, 72 | 73 | discord: function (req, res) { 74 | let redirect_uri = encodeURIComponent(sails.config.discord.redirect_host + '/discord/callback'); 75 | let authorize_uri = 'https://discordapp.com/api/oauth2/authorize?client_id='+ sails.config.discord.client_id + '&redirect_uri='+ redirect_uri + '&response_type=code&scope=identify%20guilds.join'; 76 | res.redirect(authorize_uri); 77 | } 78 | 79 | }; 80 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* global __dirname */ 2 | /** 3 | * Gruntfile 4 | * 5 | * This Node script is executed when you run `grunt` or `sails lift`. 6 | * It's purpose is to load the Grunt tasks in your project's `tasks` 7 | * folder, and allow you to add and remove tasks as you see fit. 8 | * For more information on how this works, check out the `README.md` 9 | * file that was generated in your `tasks` folder. 10 | * 11 | * WARNING: 12 | * Unless you know what you're doing, you shouldn't change this file. 13 | * Check out the `tasks` directory instead. 14 | */ 15 | 16 | module.exports = function (grunt) { 17 | 18 | 19 | // Load the include-all library in order to require all of our grunt 20 | // configurations and task registrations dynamically. 21 | var includeAll; 22 | try { 23 | includeAll = require('include-all'); 24 | } catch (e0) { 25 | try { 26 | includeAll = require('sails/node_modules/include-all'); 27 | } 28 | catch (e1) { 29 | console.error('Could not find `include-all` module.'); 30 | console.error('Skipping grunt tasks...'); 31 | console.error('To fix this, please run:'); 32 | console.error('npm install include-all --save`'); 33 | console.error(); 34 | 35 | grunt.registerTask('default', []); 36 | return; 37 | } 38 | } 39 | 40 | 41 | /** 42 | * Loads Grunt configuration modules from the specified 43 | * relative path. These modules should export a function 44 | * that, when run, should either load/configure or register 45 | * a Grunt task. 46 | */ 47 | function loadTasks(relPath) { 48 | return includeAll({ 49 | dirname: require('path').resolve(__dirname, relPath), 50 | filter: /(.+)\.js$/ 51 | }) || {}; 52 | } 53 | 54 | /** 55 | * Invokes the function from a Grunt configuration module with 56 | * a single argument - the `grunt` object. 57 | */ 58 | function invokeConfigFn(tasks) { 59 | for (var taskName in tasks) { 60 | if (tasks.hasOwnProperty(taskName)) { 61 | tasks[taskName](grunt); 62 | } 63 | } 64 | } 65 | 66 | 67 | // Load task functions 68 | var taskConfigurations = loadTasks('./tasks/config'), 69 | registerDefinitions = loadTasks('./tasks/register'); 70 | 71 | // (ensure that a default task exists) 72 | if (!registerDefinitions.default) { 73 | registerDefinitions.default = function (grunt) { 74 | grunt.registerTask('default', []); 75 | }; 76 | } 77 | 78 | // Run task functions to configure Grunt. 79 | invokeConfigFn(taskConfigurations); 80 | invokeConfigFn(registerDefinitions); 81 | 82 | }; 83 | -------------------------------------------------------------------------------- /assets/views/home/row.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{$index + 1}}. 4 | 5 | 6 | 7 | 8 | 9 | {{reference.gave}} for {{reference.got}} 10 | 11 | 12 | 13 | 14 | 15 | {{reference.description || reference.descrip}} 16 | 17 | 18 | 19 | 20 | {{reference.description || reference.descrip}} 21 | {{reference.number ? "(" + reference.number + " checked)" : ""}} 22 | 23 | 24 | 25 | 26 | 27 | {{reference.description || reference.descrip}} (Sub: {{reference.url.split("/")[4]}}{{reference.number ? ", " + reference.number + " given" : ""}}) 28 | 29 | 30 | 31 | 32 | 33 | {{getRedditUser(reference.user2)}} 34 | 35 | 36 | 37 | 38 | 39 | 40 | * 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 53 | 54 | 55 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /api/responses/forbidden.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 403 (Forbidden) Handler 3 | * 4 | * Usage: 5 | * return res.forbidden(); 6 | * return res.forbidden(err); 7 | * return res.forbidden(err, 'some/specific/forbidden/view'); 8 | * 9 | * e.g.: 10 | * ``` 11 | * return res.forbidden('Access denied.'); 12 | * ``` 13 | */ 14 | 15 | module.exports = function forbidden (data, options) { 16 | 17 | // Get access to `req`, `res`, & `sails` 18 | var req = this.req; 19 | var res = this.res; 20 | var sails = req._sails; 21 | 22 | // Set status code 23 | res.status(403); 24 | 25 | // Log error to console 26 | if (data !== undefined) { 27 | sails.log.verbose('Sending 403 ("Forbidden") response: \n',data); 28 | } 29 | else sails.log.verbose('Sending 403 ("Forbidden") response'); 30 | 31 | // Only include errors in response if application environment 32 | // is not set to 'production'. In production, we shouldn't 33 | // send back any identifying information about errors. 34 | if (sails.config.environment === 'production') { 35 | data = undefined; 36 | } 37 | 38 | // If the user-agent wants JSON, always respond with JSON 39 | if (req.wantsJSON) { 40 | return res.jsonx(data); 41 | } 42 | 43 | // If second argument is a string, we take that to mean it refers to a view. 44 | // If it was omitted, use an empty object (`{}`) 45 | options = (typeof options === 'string') ? { view: options } : options || {}; 46 | 47 | // If a view was provided in options, serve it. 48 | // Otherwise try to guess an appropriate view, or if that doesn't 49 | // work, just send JSON. 50 | if (options.view) { 51 | return res.view(options.view, { data: data }); 52 | } 53 | 54 | // If no second argument provided, try to serve the default view, 55 | // but fall back to sending JSON(P) if any errors occur. 56 | else return res.view('403', { data: data }, function (err, html) { 57 | 58 | // If a view error occured, fall back to JSON(P). 59 | if (err) { 60 | // 61 | // Additionally: 62 | // • If the view was missing, ignore the error but provide a verbose log. 63 | if (err.code === 'E_VIEW_FAILED') { 64 | sails.log.verbose('res.forbidden() :: Could not locate view for error page (sending JSON instead). Details: ',err); 65 | } 66 | // Otherwise, if this was a more serious error, log to the console with the details. 67 | else { 68 | sails.log.warn('res.forbidden() :: When attempting to render error page view, an error occured (sending JSON instead). Details: ', err); 69 | } 70 | return res.jsonx(data); 71 | } 72 | 73 | return res.send(html); 74 | }); 75 | 76 | }; 77 | 78 | -------------------------------------------------------------------------------- /test/unit/data/referenceFactory.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | module.exports = { 3 | // Generate random references 4 | // Intended use is for unit testing, not flair grinding 5 | getRefs: function (numberOfRefs, params) { 6 | var refs = []; 7 | for (var i = 0; i < numberOfRefs; i++) { 8 | var subreddit, url, type, approved; 9 | if (params.url) { 10 | url = params.url; 11 | subreddit = params.url.indexOf('/r/pokemontrades') !== -1 ? 'pokemontrades' : 'SVExchange'; 12 | } else { 13 | if (params.subreddit) { 14 | subreddit = params.subreddit; 15 | } else if (params.type === 'egg' || params.type === 'eggcheck') { 16 | subreddit = 'SVExchange'; 17 | } else if (params.type && params.type !== 'giveaway') { 18 | subreddit = 'pokemontrades'; 19 | } else { 20 | subreddit = _.sample(['pokemontrades', 'SVExchange']); 21 | } 22 | url = 'https://reddit.com/r/' + subreddit + '/comments/a/a/' + Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 20); 23 | } 24 | if (params.type) { 25 | type = params.type; 26 | } 27 | else if (subreddit === 'pokemontrades') { 28 | type = _.sample(['event', 'shiny', 'casual', 'bank', 'involvement', 'giveaway']); 29 | } else { 30 | type = _.sample(['egg', 'eggcheck', 'giveaway']); 31 | } 32 | approved = _.sample([true, false]); 33 | refs.push({ 34 | url: url, 35 | user: params.user || Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 20), 36 | user2: params.user2 || Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 20), 37 | description: params.description || Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 20), 38 | gave: params.gave || Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 20), 39 | got: params.got || Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 20), 40 | type: type, 41 | notes: params.notes || Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 20), 42 | privatenotes: params.privatenotes || Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 20), 43 | edited: params.edited || _.sample([true, false]), 44 | number: _.random(0, Number.MAX_SAFE_INTEGER), 45 | createdAt: params.createdAt || new Date(_.random(0, 4294967295000)).toISOString(), 46 | updatedAt: params.updatedAt || new Date(_.random(0, 4294967295000)).toISOString(), 47 | approved: approved, 48 | verified: approved && _.sample([true, false]) 49 | }); 50 | } 51 | return refs; 52 | } 53 | }; -------------------------------------------------------------------------------- /assets/views/layout.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FlairHQ 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 |
22 | <%- partial ('home/header.ejs') %> 23 |
24 |
25 |
26 |
27 |
28 |
29 | 37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | <%- body %> 46 |
47 |
48 | 49 | 54 | 55 | 56 | 57 | 58 | <% if(sails.config.environment == 'development' ){ %> <% } %> 59 | <%- partial('privacyPolicy.ejs') %> 60 | 61 | 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FlairHQ", 3 | "version": "2.8.0", 4 | "description": "A project to allow easy adding of flair applications for subreddits (focusing initially on /r/pokemontrades) and easy moderation for moderators.", 5 | "scripts": { 6 | "start": "node ./node_modules/sails/bin/sails.js lift", 7 | "test": "grunt test" 8 | }, 9 | "main": "app.js", 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/yamanickill/flairhq.git" 13 | }, 14 | "author": "YaManicKill", 15 | "license": "Apache-2.0", 16 | "keywords": [], 17 | "dependencies": { 18 | "angular": "^1.5.3", 19 | "angular-bootstrap-npm": "^0.14.2", 20 | "angular-spinner": "^0.7.0", 21 | "angular-ui-mask": "~1.4.3", 22 | "async": "~1.4.2", 23 | "babel-core": "^6.3.17", 24 | "babel-eslint": "^4.1.4", 25 | "babelify": "6.3.0", 26 | "bootstrap": "^3.3.5", 27 | "chai": "^3.4.1", 28 | "connect-mongo": "^0.8.2", 29 | "crypto": "^0.0.3", 30 | "ejs": "~2.5.5", 31 | "grunt": "~0.4.5", 32 | "grunt-browserify": "^5.0.0", 33 | "grunt-contrib-clean": "~0.6.0", 34 | "grunt-contrib-concat": "~0.5.1", 35 | "grunt-contrib-copy": "~0.8.1", 36 | "grunt-contrib-cssmin": "~0.14.0", 37 | "grunt-contrib-less": "~1.0.1", 38 | "grunt-contrib-uglify": "~0.9.2", 39 | "grunt-contrib-watch": "~0.6.1", 40 | "grunt-eslint": "^17.3.1", 41 | "grunt-focus": "^0.1.1", 42 | "grunt-mocha-test": "^0.12.7", 43 | "grunt-sync": "~0.4.1", 44 | "include-all": "~0.1.3", 45 | "jquery-browserify": "^1.8.1", 46 | "lodash": "^3.10.1", 47 | "mocha": "^2.3.4", 48 | "moment": "~2.19.3", 49 | "ng-mask": "^3.0.12", 50 | "node-cache": "^3.0.0", 51 | "node-sha1": "^1.0.1", 52 | "pako": "^0.2.8", 53 | "passport": "^0.3.0", 54 | "passport-reddit": "^0.2.4", 55 | "q": "1.4.1", 56 | "rc": "~1.1.2", 57 | "regex": "^0.1.1", 58 | "request": "~2.64.0", 59 | "request-promise": "^1.0.2", 60 | "sails": "~0.11.2", 61 | "sails-disk": "~0.10.8", 62 | "sails-hook-babel": "^5.0.1", 63 | "sails-hook-schedule": "^0.2.2", 64 | "sails-hook-winston": "^1.1.0", 65 | "sails-mongo": "^0.11.4", 66 | "sails.io.js": "^0.11.7", 67 | "sha256": "^0.2.0", 68 | "snudown-js": "^1.4.0", 69 | "socket.io-browserify": "^0.9.6", 70 | "socket.io-client": "^1.3.7" 71 | }, 72 | "browser": { 73 | "jquery": "jquery-browserify", 74 | "node-jquery-deparam": "./node_modules/node-jquery-deparam/node-jquery-deparam.js", 75 | "angular-mask": "./node_modules/ng-mask/dist/ngMask.js", 76 | "angular-md": "./node_modules/angular-md/dist/angular-md.js", 77 | "regex": "./assets/common/regexCommon.js" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /api/responses/serverError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 500 (Server Error) Response 3 | * 4 | * Usage: 5 | * return res.serverError(); 6 | * return res.serverError(err); 7 | * return res.serverError(err, 'some/specific/error/view'); 8 | * 9 | * NOTE: 10 | * If something throws in a policy or controller, or an internal 11 | * error is encountered, Sails will call `res.serverError()` 12 | * automatically. 13 | */ 14 | 15 | module.exports = function serverError (data, options) { 16 | 17 | // Get access to `req`, `res`, & `sails` 18 | var req = this.req; 19 | var res = this.res; 20 | var sails = req._sails; 21 | 22 | // Set status code 23 | res.status(500); 24 | 25 | // Log error to console 26 | if (data !== undefined) { 27 | sails.log.error('Sending 500 ("Server Error") response: \n',data); 28 | } 29 | else sails.log.error('Sending empty 500 ("Server Error") response'); 30 | 31 | // Only include errors in response if application environment 32 | // is not set to 'production'. In production, we shouldn't 33 | // send back any identifying information about errors. 34 | if (sails.config.environment === 'production') { 35 | data = undefined; 36 | } 37 | 38 | // If the user-agent wants JSON, always respond with JSON 39 | if (req.wantsJSON) { 40 | return res.jsonx(data); 41 | } 42 | 43 | // If second argument is a string, we take that to mean it refers to a view. 44 | // If it was omitted, use an empty object (`{}`) 45 | options = (typeof options === 'string') ? { view: options } : options || {}; 46 | 47 | // If a view was provided in options, serve it. 48 | // Otherwise try to guess an appropriate view, or if that doesn't 49 | // work, just send JSON. 50 | if (options.view) { 51 | return res.view(options.view, { data: data }); 52 | } 53 | 54 | // If no second argument provided, try to serve the default view, 55 | // but fall back to sending JSON(P) if any errors occur. 56 | else return res.view('500', { data: data }, function (err, html) { 57 | 58 | // If a view error occured, fall back to JSON(P). 59 | if (err) { 60 | // 61 | // Additionally: 62 | // • If the view was missing, ignore the error but provide a verbose log. 63 | if (err.code === 'E_VIEW_FAILED') { 64 | sails.log.verbose('res.serverError() :: Could not locate view for error page (sending JSON instead). Details: ',err); 65 | } 66 | // Otherwise, if this was a more serious error, log to the console with the details. 67 | else { 68 | sails.log.warn('res.serverError() :: When attempting to render error page view, an error occured (sending JSON instead). Details: ', err); 69 | } 70 | return res.jsonx(data); 71 | } 72 | 73 | return res.send(html); 74 | }); 75 | 76 | }; 77 | 78 | -------------------------------------------------------------------------------- /assets/views/home/applist.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/progress-bars.less: -------------------------------------------------------------------------------- 1 | // 2 | // Progress bars 3 | // -------------------------------------------------- 4 | 5 | 6 | // Bar animations 7 | // ------------------------- 8 | 9 | // WebKit 10 | @-webkit-keyframes progress-bar-stripes { 11 | from { background-position: 40px 0; } 12 | to { background-position: 0 0; } 13 | } 14 | 15 | // Spec and IE10+ 16 | @keyframes progress-bar-stripes { 17 | from { background-position: 40px 0; } 18 | to { background-position: 0 0; } 19 | } 20 | 21 | 22 | 23 | // Bar itself 24 | // ------------------------- 25 | 26 | // Outer container 27 | .progress { 28 | overflow: hidden; 29 | height: @line-height-computed; 30 | margin-bottom: @line-height-computed; 31 | background-color: @progress-bg; 32 | border-radius: @border-radius-base; 33 | .box-shadow(inset 0 1px 2px rgba(0,0,0,.1)); 34 | } 35 | 36 | // Bar of progress 37 | .progress-bar { 38 | float: left; 39 | width: 0%; 40 | height: 100%; 41 | font-size: @font-size-small; 42 | line-height: @line-height-computed; 43 | color: @progress-bar-color; 44 | text-align: center; 45 | background-color: @progress-bar-bg; 46 | .box-shadow(inset 0 -1px 0 rgba(0,0,0,.15)); 47 | .transition(width .6s ease); 48 | } 49 | 50 | // Striped bars 51 | // 52 | // `.progress-striped .progress-bar` is deprecated as of v3.2.0 in favor of the 53 | // `.progress-bar-striped` class, which you just add to an existing 54 | // `.progress-bar`. 55 | .progress-striped .progress-bar, 56 | .progress-bar-striped { 57 | #gradient > .striped(); 58 | background-size: 40px 40px; 59 | } 60 | 61 | // Call animation for the active one 62 | // 63 | // `.progress.active .progress-bar` is deprecated as of v3.2.0 in favor of the 64 | // `.progress-bar.active` approach. 65 | .progress.active .progress-bar, 66 | .progress-bar.active { 67 | .animation(progress-bar-stripes 2s linear infinite); 68 | } 69 | 70 | // Account for lower percentages 71 | .progress-bar { 72 | &[aria-valuenow="1"], 73 | &[aria-valuenow="2"] { 74 | min-width: 30px; 75 | } 76 | 77 | &[aria-valuenow="0"] { 78 | color: @gray-light; 79 | min-width: 30px; 80 | background-color: transparent; 81 | background-image: none; 82 | box-shadow: none; 83 | } 84 | } 85 | 86 | 87 | 88 | // Variations 89 | // ------------------------- 90 | 91 | .progress-bar-success { 92 | .progress-bar-variant(@progress-bar-success-bg); 93 | } 94 | 95 | .progress-bar-info { 96 | .progress-bar-variant(@progress-bar-info-bg); 97 | } 98 | 99 | .progress-bar-warning { 100 | .progress-bar-variant(@progress-bar-warning-bg); 101 | } 102 | 103 | .progress-bar-danger { 104 | .progress-bar-variant(@progress-bar-danger-bg); 105 | } 106 | -------------------------------------------------------------------------------- /config/i18n.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internationalization / Localization Settings 3 | * (sails.config.i18n) 4 | * 5 | * If your app will touch people from all over the world, i18n (or internationalization) 6 | * may be an important part of your international strategy. 7 | * 8 | * 9 | * For more informationom i18n in Sails, check out: 10 | * http://sailsjs.org/#/documentation/concepts/Internationalization 11 | * 12 | * For a complete list of i18n options, see: 13 | * https://github.com/mashpie/i18n-node#list-of-configuration-options 14 | * 15 | * 16 | */ 17 | 18 | module.exports.i18n = { 19 | 20 | /*************************************************************************** 21 | * * 22 | * Which locales are supported? * 23 | * * 24 | ***************************************************************************/ 25 | 26 | // locales: ['en', 'es', 'fr', 'de'] 27 | 28 | /**************************************************************************** 29 | * * 30 | * What is the default locale for the site? Note that this setting will be * 31 | * overridden for any request that sends an "Accept-Language" header (i.e. * 32 | * most browsers), but it's still useful if you need to localize the * 33 | * response for requests made by non-browser clients (e.g. cURL). * 34 | * * 35 | ****************************************************************************/ 36 | 37 | // defaultLocale: 'en', 38 | 39 | /**************************************************************************** 40 | * * 41 | * Automatically add new keys to locale (translation) files when they are * 42 | * encountered during a request? * 43 | * * 44 | ****************************************************************************/ 45 | 46 | // updateFiles: false, 47 | 48 | /**************************************************************************** 49 | * * 50 | * Path (relative to app root) of directory to store locale (translation) * 51 | * files in. * 52 | * * 53 | ****************************************************************************/ 54 | 55 | // localesDirectory: '/config/locales' 56 | 57 | }; 58 | -------------------------------------------------------------------------------- /api/responses/notFound.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 404 (Not Found) Handler 3 | * 4 | * Usage: 5 | * return res.notFound(); 6 | * return res.notFound(err); 7 | * return res.notFound(err, 'some/specific/notfound/view'); 8 | * 9 | * e.g.: 10 | * ``` 11 | * return res.notFound(); 12 | * ``` 13 | * 14 | * NOTE: 15 | * If a request doesn't match any explicit routes (i.e. `config/routes.js`) 16 | * or route blueprints (i.e. "shadow routes", Sails will call `res.notFound()` 17 | * automatically. 18 | */ 19 | 20 | module.exports = function notFound (data, options) { 21 | 22 | // Get access to `req`, `res`, & `sails` 23 | var req = this.req; 24 | var res = this.res; 25 | var sails = req._sails; 26 | 27 | res.locals.user = req.user; 28 | 29 | // Set status code 30 | res.status(404); 31 | 32 | // Log error to console 33 | if (data !== undefined) { 34 | sails.log.verbose('Sending 404 ("Not Found") response: \n',data); 35 | } 36 | else sails.log.verbose('Sending 404 ("Not Found") response'); 37 | 38 | // Only include errors in response if application environment 39 | // is not set to 'production'. In production, we shouldn't 40 | // send back any identifying information about errors. 41 | if (sails.config.environment === 'production') { 42 | data = undefined; 43 | } 44 | 45 | // If the user-agent wants JSON, always respond with JSON 46 | if (req.wantsJSON) { 47 | return res.jsonx(data); 48 | } 49 | 50 | // If second argument is a string, we take that to mean it refers to a view. 51 | // If it was omitted, use an empty object (`{}`) 52 | options = (typeof options === 'string') ? { view: options } : options || {}; 53 | 54 | // If a view was provided in options, serve it. 55 | // Otherwise try to guess an appropriate view, or if that doesn't 56 | // work, just send JSON. 57 | if (options.view) { 58 | return res.view(options.view, { data: data }); 59 | } 60 | 61 | // If no second argument provided, try to serve the default view, 62 | // but fall back to sending JSON(P) if any errors occur. 63 | else return res.view('404', { data: data }, function (err, html) { 64 | 65 | // If a view error occured, fall back to JSON(P). 66 | if (err) { 67 | // 68 | // Additionally: 69 | // • If the view was missing, ignore the error but provide a verbose log. 70 | if (err.code === 'E_VIEW_FAILED') { 71 | sails.log.verbose('res.notFound() :: Could not locate view for error page (sending JSON instead). Details: ',err); 72 | } 73 | // Otherwise, if this was a more serious error, log to the console with the details. 74 | else { 75 | sails.log.warn('res.notFound() :: When attempting to render error page view, an error occured (sending JSON instead). Details: ', err); 76 | } 77 | return res.jsonx(data); 78 | } 79 | 80 | return res.send(html); 81 | }); 82 | 83 | }; 84 | 85 | -------------------------------------------------------------------------------- /api/services/Usernotes.js: -------------------------------------------------------------------------------- 1 | var pako = require('pako'), 2 | sha256 = require('sha256'), 3 | moment = require('moment'); 4 | var decompress = function(blob) { 5 | var inflate = new pako.Inflate({to: 'string'}); 6 | inflate.push(new Buffer(blob, 'base64').toString('binary')); 7 | return JSON.parse(inflate.result); 8 | }; 9 | var compress = function(notesObject) { 10 | var deflate = new pako.Deflate({to: 'string'}); 11 | deflate.push(JSON.stringify(notesObject), true); 12 | return (new Buffer(deflate.result.toString(), 'binary')).toString('base64'); 13 | }; 14 | exports.addUsernote = async function (redToken, modname, subreddit, user, noteText, type, link_index) { 15 | let compressed_notes = await Reddit.getWikiPage(redToken, subreddit, 'usernotes'); 16 | var parsed = JSON.parse(compressed_notes); 17 | var mods = parsed.constants.users; 18 | var warnings = parsed.constants.warnings; 19 | var notes = decompress(parsed.blob); 20 | if (!notes[user]) { 21 | notes[user] = {ns: []}; 22 | } 23 | if (mods.indexOf(modname) == -1) { 24 | mods.push(modname); 25 | } 26 | if (warnings.indexOf(type) == -1) { 27 | warnings.push(type); 28 | } 29 | var newNote = { 30 | n: noteText, 31 | t: moment().unix(), 32 | m: mods.indexOf(modname), 33 | l: link_index, 34 | w: warnings.indexOf(type) 35 | }; 36 | notes[user].ns.unshift(newNote); 37 | parsed.blob = compress(notes); 38 | await Reddit.editWikiPage(redToken, subreddit, 'usernotes', JSON.stringify(parsed), 'FlairHQ: Created note on /u/' + user); 39 | var hash = sha256(user + newNote.n + newNote.t + newNote.m + newNote.l + newNote.w); 40 | /* By default, notes on a particular user are not indexed. This makes it difficult if one wants to delete a specific note that it created, 41 | * because new notes might have been added or removed since the note in question was created. 42 | * To resolve this issue, the addUsernote function returns a hash of the note when it's added. Then a specific note can be deleted by 43 | * searching for a note that matches a particular hash. */ 44 | return hash; 45 | }; 46 | exports.removeUsernote = async function (redToken, username, subreddit, note_hash) { 47 | let compressed_notes = await Reddit.getWikiPage(redToken, subreddit, 'usernotes'); 48 | var pageObject = JSON.parse(compressed_notes); 49 | var notes = decompress(pageObject.blob); 50 | for (var i = 0; i < notes[username].ns.length; i++) { 51 | var note = notes[username].ns[i]; 52 | if (note_hash === sha256(username + note.n + note.t + note.m + note.l + note.w)) { 53 | notes[username].ns.splice(i,1); 54 | i--; 55 | } 56 | } 57 | pageObject.blob = compress(notes); 58 | var reason = 'FlairHQ: Deleted note ' + note_hash.substring(0,7) + ' on ' + username; 59 | await Reddit.editWikiPage(redToken, subreddit, 'usernotes', JSON.stringify(pageObject), reason); 60 | return; 61 | }; 62 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/forms.less: -------------------------------------------------------------------------------- 1 | // Form validation states 2 | // 3 | // Used in forms.less to generate the form validation CSS for warnings, errors, 4 | // and successes. 5 | 6 | .form-control-validation(@text-color: #555; @border-color: #ccc; @background-color: #f5f5f5) { 7 | // Color the label and help text 8 | .help-block, 9 | .control-label, 10 | .radio, 11 | .checkbox, 12 | .radio-inline, 13 | .checkbox-inline { 14 | color: @text-color; 15 | } 16 | // Set the border and box shadow on specific inputs to match 17 | .form-control { 18 | border-color: @border-color; 19 | .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work 20 | &:focus { 21 | border-color: darken(@border-color, 10%); 22 | @shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten(@border-color, 20%); 23 | .box-shadow(@shadow); 24 | } 25 | } 26 | // Set validation states also for addons 27 | .input-group-addon { 28 | color: @text-color; 29 | border-color: @border-color; 30 | background-color: @background-color; 31 | } 32 | // Optional feedback icon 33 | .form-control-feedback { 34 | color: @text-color; 35 | } 36 | } 37 | 38 | 39 | // Form control focus state 40 | // 41 | // Generate a customized focus state and for any input with the specified color, 42 | // which defaults to the `@input-border-focus` variable. 43 | // 44 | // We highly encourage you to not customize the default value, but instead use 45 | // this to tweak colors on an as-needed basis. This aesthetic change is based on 46 | // WebKit's default styles, but applicable to a wider range of browsers. Its 47 | // usability and accessibility should be taken into account with any change. 48 | // 49 | // Example usage: change the default blue border and shadow to white for better 50 | // contrast against a dark gray background. 51 | .form-control-focus(@color: @input-border-focus) { 52 | @color-rgba: rgba(red(@color), green(@color), blue(@color), .6); 53 | &:focus { 54 | border-color: @color; 55 | outline: 0; 56 | .box-shadow(~"inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px @{color-rgba}"); 57 | } 58 | } 59 | 60 | // Form control sizing 61 | // 62 | // Relative text size, padding, and border-radii changes for form controls. For 63 | // horizontal sizing, wrap controls in the predefined grid classes. ` 25 | * 26 | * 27 | * or (b) For AJAX/Socket-heavy and/or single-page apps: 28 | * Sending a GET request to the `/csrfToken` route, where it will be returned 29 | * as JSON, e.g.: 30 | * { _csrf: 'ajg4JD(JGdajhLJALHDa' } 31 | * 32 | * 33 | * Enabling this option requires managing the token in your front-end app. 34 | * For traditional web apps, it's as easy as passing the data from a view into a form action. 35 | * In AJAX/Socket-heavy apps, just send a GET request to the /csrfToken route to get a valid token. 36 | * 37 | * For more information on CSRF, check out: 38 | * http://en.wikipedia.org/wiki/Cross-site_request_forgery 39 | * 40 | * For more information on this configuration file, including info on CSRF + CORS, see: 41 | * http://beta.sailsjs.org/#/documentation/reference/sails.config/sails.config.csrf.html 42 | * 43 | */ 44 | 45 | /**************************************************************************** 46 | * * 47 | * Enabled CSRF protection for your site? * 48 | * * 49 | ****************************************************************************/ 50 | 51 | module.exports.csrf = true; 52 | 53 | /**************************************************************************** 54 | * * 55 | * You may also specify more fine-grained settings for CSRF, including the * 56 | * domains which are allowed to request the CSRF token via AJAX. These * 57 | * settings override the general CORS settings in your config/cors.js file. * 58 | * * 59 | ****************************************************************************/ 60 | 61 | // module.exports.csrf = { 62 | // grantTokenViaAjax: true, 63 | // origin: '' 64 | // } 65 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/mixins/grid-framework.less: -------------------------------------------------------------------------------- 1 | // Framework grid generation 2 | // 3 | // Used only by Bootstrap to generate the correct number of grid classes given 4 | // any value of `@grid-columns`. 5 | 6 | .make-grid-columns() { 7 | // Common styles for all sizes of grid columns, widths 1-12 8 | .col(@index) when (@index = 1) { // initial 9 | @item: ~".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}"; 10 | .col((@index + 1), @item); 11 | } 12 | .col(@index, @list) when (@index =< @grid-columns) { // general; "=<" isn't a typo 13 | @item: ~".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}"; 14 | .col((@index + 1), ~"@{list}, @{item}"); 15 | } 16 | .col(@index, @list) when (@index > @grid-columns) { // terminal 17 | @{list} { 18 | position: relative; 19 | // Prevent columns from collapsing when empty 20 | min-height: 1px; 21 | // Inner gutter via padding 22 | padding-left: (@grid-gutter-width / 2); 23 | padding-right: (@grid-gutter-width / 2); 24 | } 25 | } 26 | .col(1); // kickstart it 27 | } 28 | 29 | .float-grid-columns(@class) { 30 | .col(@index) when (@index = 1) { // initial 31 | @item: ~".col-@{class}-@{index}"; 32 | .col((@index + 1), @item); 33 | } 34 | .col(@index, @list) when (@index =< @grid-columns) { // general 35 | @item: ~".col-@{class}-@{index}"; 36 | .col((@index + 1), ~"@{list}, @{item}"); 37 | } 38 | .col(@index, @list) when (@index > @grid-columns) { // terminal 39 | @{list} { 40 | float: left; 41 | } 42 | } 43 | .col(1); // kickstart it 44 | } 45 | 46 | .calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) { 47 | .col-@{class}-@{index} { 48 | width: percentage((@index / @grid-columns)); 49 | } 50 | } 51 | .calc-grid-column(@index, @class, @type) when (@type = push) and (@index > 0) { 52 | .col-@{class}-push-@{index} { 53 | left: percentage((@index / @grid-columns)); 54 | } 55 | } 56 | .calc-grid-column(@index, @class, @type) when (@type = push) and (@index = 0) { 57 | .col-@{class}-push-0 { 58 | left: auto; 59 | } 60 | } 61 | .calc-grid-column(@index, @class, @type) when (@type = pull) and (@index > 0) { 62 | .col-@{class}-pull-@{index} { 63 | right: percentage((@index / @grid-columns)); 64 | } 65 | } 66 | .calc-grid-column(@index, @class, @type) when (@type = pull) and (@index = 0) { 67 | .col-@{class}-pull-0 { 68 | right: auto; 69 | } 70 | } 71 | .calc-grid-column(@index, @class, @type) when (@type = offset) { 72 | .col-@{class}-offset-@{index} { 73 | margin-left: percentage((@index / @grid-columns)); 74 | } 75 | } 76 | 77 | // Basic looping in LESS 78 | .loop-grid-columns(@index, @class, @type) when (@index >= 0) { 79 | .calc-grid-column(@index, @class, @type); 80 | // next iteration 81 | .loop-grid-columns((@index - 1), @class, @type); 82 | } 83 | 84 | // Create grid for specific class 85 | .make-grid(@class) { 86 | .float-grid-columns(@class); 87 | .loop-grid-columns(@grid-columns, @class, width); 88 | .loop-grid-columns(@grid-columns, @class, pull); 89 | .loop-grid-columns(@grid-columns, @class, push); 90 | .loop-grid-columns(@grid-columns, @class, offset); 91 | } 92 | -------------------------------------------------------------------------------- /assets/views/home/viewreference.ejs: -------------------------------------------------------------------------------- 1 | 71 | -------------------------------------------------------------------------------- /tasks/README.md: -------------------------------------------------------------------------------- 1 | # About the `tasks` folder 2 | 3 | The `tasks` directory is a suite of Grunt tasks and their configurations, bundled for your convenience. The Grunt integration is mainly useful for bundling front-end assets, (like stylesheets, scripts, & markup templates) but it can also be used to run all kinds of development tasks, from browserify compilation to database migrations. 4 | 5 | If you haven't used [Grunt](http://gruntjs.com/) before, be sure to check out the [Getting Started](http://gruntjs.com/getting-started) guide, as it explains how to create a [Gruntfile](http://gruntjs.com/sample-gruntfile) as well as install and use Grunt plugins. Once you're familiar with that process, read on! 6 | 7 | 8 | ### How does this work? 9 | 10 | The asset pipeline bundled in Sails is a set of Grunt tasks configured with conventional defaults designed to make your project more consistent and productive. 11 | 12 | The entire front-end asset workflow in Sails is completely customizable-- while it provides some suggestions out of the box, Sails makes no pretense that it can anticipate all of the needs you'll encounter building the browser-based/front-end portion of your application. Who's to say you're even building an app for a browser? 13 | 14 | 15 | 16 | ### What tasks does Sails run automatically? 17 | 18 | Sails runs some of these tasks (the ones in the `tasks/register` folder) automatically when you run certain commands. 19 | 20 | ###### `sails lift` 21 | 22 | Runs the `default` task (`tasks/register/default.js`). 23 | 24 | ###### `sails lift --prod` 25 | 26 | Runs the `prod` task (`tasks/register/prod.js`). 27 | 28 | ###### `sails www` 29 | 30 | Runs the `build` task (`tasks/register/build.js`). 31 | 32 | ###### `sails www --prod` (production) 33 | 34 | Runs the `buildProd` task (`tasks/register/buildProd.js`). 35 | 36 | 37 | ### Can I customize this for SASS, Angular, client-side Jade templates, etc? 38 | 39 | You can modify, omit, or replace any of these Grunt tasks to fit your requirements. You can also add your own Grunt tasks- just add a `someTask.js` file in the `grunt/config` directory to configure the new task, then register it with the appropriate parent task(s) (see files in `grunt/register/*.js`). 40 | 41 | 42 | ### Do I have to use Grunt? 43 | 44 | Nope! To disable Grunt integration in Sails, just delete your Gruntfile or disable the Grunt hook. 45 | 46 | 47 | ### What if I'm not building a web frontend? 48 | 49 | That's ok! A core tenant of Sails is client-agnosticism-- it's especially designed for building APIs used by all sorts of clients; native Android/iOS/Cordova, serverside SDKs, etc. 50 | 51 | You can completely disable Grunt by following the instructions above. 52 | 53 | If you still want to use Grunt for other purposes, but don't want any of the default web front-end stuff, just delete your project's `assets` folder and remove the front-end oriented tasks from the `grunt/register` and `grunt/config` folders. You can also run `sails new myCoolApi --no-frontend` to omit the `assets` folder and front-end-oriented Grunt tasks for future projects. You can also replace your `sails-generate-frontend` module with alternative community generators, or create your own. This allows `sails new` to create the boilerplate for native iOS apps, Android apps, Cordova apps, SteroidsJS apps, etc. 54 | 55 | -------------------------------------------------------------------------------- /assets/views/privacyPolicy.ejs: -------------------------------------------------------------------------------- 1 | 64 | -------------------------------------------------------------------------------- /assets/styles/bootstrap/scaffolding.less: -------------------------------------------------------------------------------- 1 | // 2 | // Scaffolding 3 | // -------------------------------------------------- 4 | 5 | 6 | // Reset the box-sizing 7 | // 8 | // Heads up! This reset may cause conflicts with some third-party widgets. 9 | // For recommendations on resolving such conflicts, see 10 | // http://getbootstrap.com/getting-started/#third-box-sizing 11 | * { 12 | .box-sizing(border-box); 13 | } 14 | *:before, 15 | *:after { 16 | .box-sizing(border-box); 17 | } 18 | 19 | 20 | // Body reset 21 | 22 | html { 23 | font-size: 10px; 24 | -webkit-tap-highlight-color: rgba(0,0,0,0); 25 | } 26 | 27 | body { 28 | font-family: @font-family-base; 29 | font-size: @font-size-base; 30 | line-height: @line-height-base; 31 | color: @text-color; 32 | background-color: @body-bg; 33 | } 34 | 35 | // Reset fonts for relevant elements 36 | input, 37 | button, 38 | select, 39 | textarea { 40 | font-family: inherit; 41 | font-size: inherit; 42 | line-height: inherit; 43 | } 44 | 45 | 46 | // Links 47 | 48 | a { 49 | color: @link-color; 50 | text-decoration: none; 51 | 52 | &:hover, 53 | &:focus { 54 | color: @link-hover-color; 55 | text-decoration: underline; 56 | } 57 | 58 | &:focus { 59 | .tab-focus(); 60 | } 61 | } 62 | 63 | 64 | // Figures 65 | // 66 | // We reset this here because previously Normalize had no `figure` margins. This 67 | // ensures we don't break anyone's use of the element. 68 | 69 | figure { 70 | margin: 0; 71 | } 72 | 73 | 74 | // Images 75 | 76 | img { 77 | vertical-align: middle; 78 | } 79 | 80 | // Responsive images (ensure images don't scale beyond their parents) 81 | .img-responsive { 82 | .img-responsive(); 83 | } 84 | 85 | // Rounded corners 86 | .img-rounded { 87 | border-radius: @border-radius-large; 88 | } 89 | 90 | // Image thumbnails 91 | // 92 | // Heads up! This is mixin-ed into thumbnails.less for `.thumbnail`. 93 | .img-thumbnail { 94 | padding: @thumbnail-padding; 95 | line-height: @line-height-base; 96 | background-color: @thumbnail-bg; 97 | border: 1px solid @thumbnail-border; 98 | border-radius: @thumbnail-border-radius; 99 | .transition(all .2s ease-in-out); 100 | 101 | // Keep them at most 100% wide 102 | .img-responsive(inline-block); 103 | } 104 | 105 | // Perfect circle 106 | .img-circle { 107 | border-radius: 50%; // set radius in percents 108 | } 109 | 110 | 111 | // Horizontal rules 112 | 113 | hr { 114 | margin-top: @line-height-computed; 115 | margin-bottom: @line-height-computed; 116 | border: 0; 117 | border-top: 1px solid @hr-border; 118 | } 119 | 120 | 121 | // Only display content to screen readers 122 | // 123 | // See: http://a11yproject.com/posts/how-to-hide-content/ 124 | 125 | .sr-only { 126 | position: absolute; 127 | width: 1px; 128 | height: 1px; 129 | margin: -1px; 130 | padding: 0; 131 | overflow: hidden; 132 | clip: rect(0,0,0,0); 133 | border: 0; 134 | } 135 | 136 | // Use in conjunction with .sr-only to only display content when it's focused. 137 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 138 | // Credit: HTML5 Boilerplate 139 | 140 | .sr-only-focusable { 141 | &:active, 142 | &:focus { 143 | position: static; 144 | width: auto; 145 | height: auto; 146 | margin: 0; 147 | overflow: visible; 148 | clip: auto; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /assets/views/home/header.ejs: -------------------------------------------------------------------------------- 1 | <%- partial('editProfile.ejs') %> 2 | <%- partial('flairMod.ejs') %> 3 | <%- partial('flairApply.ejs') %> 4 | <%- partial('flairText.ejs') %> 5 | <%- partial('applist.ejs') %> 6 | 7 | 79 | -------------------------------------------------------------------------------- /config/globals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Global Variable Configuration 3 | * (sails.config.globals) 4 | * 5 | * Configure which global variables which will be exposed 6 | * automatically by Sails. 7 | * 8 | * For more information on configuration, check out: 9 | * http://sailsjs.org/#/documentation/reference/sails.config/sails.config.globals.html 10 | */ 11 | module.exports.globals = { 12 | 13 | /**************************************************************************** 14 | * * 15 | * Expose the lodash installed in Sails core as a global variable. If this * 16 | * is disabled, like any other node module you can always run npm install * 17 | * lodash --save, then var _ = require('lodash') at the top of any file. * 18 | * * 19 | ****************************************************************************/ 20 | 21 | // _: true, 22 | 23 | /**************************************************************************** 24 | * * 25 | * Expose the async installed in Sails core as a global variable. If this is * 26 | * disabled, like any other node module you can always run npm install async * 27 | * --save, then var async = require('async') at the top of any file. * 28 | * * 29 | ****************************************************************************/ 30 | 31 | // async: true, 32 | 33 | /**************************************************************************** 34 | * * 35 | * Expose the sails instance representing your app. If this is disabled, you * 36 | * can still get access via req._sails. * 37 | * * 38 | ****************************************************************************/ 39 | 40 | // sails: true, 41 | 42 | /**************************************************************************** 43 | * * 44 | * Expose each of your app's services as global variables (using their * 45 | * "globalId"). E.g. a service defined in api/models/NaturalLanguage.js * 46 | * would have a globalId of NaturalLanguage by default. If this is disabled, * 47 | * you can still access your services via sails.services.* * 48 | * * 49 | ****************************************************************************/ 50 | 51 | // services: true, 52 | 53 | /**************************************************************************** 54 | * * 55 | * Expose each of your app's models as global variables (using their * 56 | * "globalId"). E.g. a model defined in api/models/User.js would have a * 57 | * globalId of User by default. If this is disabled, you can still access * 58 | * your models via sails.models.*. * 59 | * * 60 | ****************************************************************************/ 61 | 62 | // models: true 63 | }; 64 | -------------------------------------------------------------------------------- /assets/views/home/banuser.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 30 | 31 | 32 | 33 | 35 | 36 | 37 | 38 | 40 | 41 | 42 | 43 | 45 | 46 | 47 | 48 |
49 |
50 | 57 |
58 |
59 | 60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | -------------------------------------------------------------------------------- /assets/styles/spinners.less: -------------------------------------------------------------------------------- 1 | 2 | ////////////////////////////////// 3 | // ### Small spinner button 4 | ///////////////////////////////// 5 | 6 | .spinner { 7 | display: inline-block; 8 | opacity: 0; 9 | max-width: 0; 10 | 11 | -webkit-transition: opacity 0.25s, max-width 0.45s; 12 | -moz-transition: opacity 0.25s, max-width 0.45s; 13 | -o-transition: opacity 0.25s, max-width 0.45s; 14 | transition: opacity 0.25s, max-width 0.45s; 15 | } 16 | 17 | .has-spinner.active { 18 | cursor:progress; 19 | } 20 | 21 | .has-spinner.active .spinner { 22 | opacity: 1; 23 | max-width: 50px; 24 | } 25 | 26 | ////////////////////////////////// 27 | // ### Pokeball spinner 28 | ///////////////////////////////// 29 | 30 | .bigspinner { 31 | margin: 50px auto; 32 | width: 200px; 33 | height: 200px; 34 | position: relative; 35 | animation: catchBall ease 1.5s infinite !important; 36 | -webkit-animation: catchBall ease 2.7s infinite !important; 37 | background: url("/images/GreyBall.png"); 38 | background-size: 100%; 39 | } 40 | 41 | @keyframes catchBall{ 42 | 0% {transform: rotate(0deg); transform-origin: center bottom 0;} 43 | 4% {transform: rotate(25deg); transform-origin: center bottom 0;} 44 | 8% {transform: rotate(0deg); transform-origin: center bottom 0;} 45 | 12% {transform: rotate(-25deg); transform-origin: center bottom 0;} 46 | 16% {transform: rotate(0deg); transform-origin: center bottom 0;} 47 | 40% {transform: rotate(0deg); transform-origin: center bottom 0;} 48 | 44% {transform: rotate(25deg); transform-origin: center bottom 0;} 49 | 48% {transform: rotate(0deg); transform-origin: center bottom 0;} 50 | 52% {transform: rotate(-25deg); transform-origin: center bottom 0;} 51 | 56% {transform: rotate(0deg); transform-origin: center bottom 0;} 52 | 70% {transform: rotate(0deg); transform-origin: center bottom 0;} 53 | 74% {transform: rotate(25deg); transform-origin: center bottom 0;} 54 | 78% {transform: rotate(0deg); transform-origin: center bottom 0;} 55 | 82% {transform: rotate(-25deg); transform-origin: center bottom 0;} 56 | 86% {transform: rotate(0deg); transform-origin: center bottom 0;} 57 | 100% {transform: rotate(0deg); transform-origin: center bottom 0;} 58 | } 59 | 60 | @-webkit-keyframes catchBall{ 61 | 0% {-webkit-transform: rotate(0deg); -webkit-transform-origin: center bottom 0; } 62 | 4% {-webkit-transform: rotate(25deg); -webkit-transform-origin: center bottom 0;} 63 | 8% {-webkit-transform: rotate(0deg); -webkit-transform-origin: center bottom 0;} 64 | 12% {-webkit-transform: rotate(-25deg); -webkit-transform-origin: center bottom 0;} 65 | 16% {-webkit-transform: rotate(0deg); -webkit-transform-origin: center bottom 0;} 66 | 40% {-webkit-transform: rotate(0deg); -webkit-transform-origin: center bottom 0;} 67 | 44% {-webkit-transform: rotate(25deg); -webkit-transform-origin: center bottom 0;} 68 | 48% {-webkit-transform: rotate(0deg); -webkit-transform-origin: center bottom 0;} 69 | 52% {-webkit-transform: rotate(-25deg); -webkit-transform-origin: center bottom 0;} 70 | 56% {-webkit-transform: rotate(0deg); -webkit-transform-origin: center bottom 0;} 71 | 70% {-webkit-transform: rotate(0deg); -webkit-transform-origin: center bottom 0;} 72 | 74% {-webkit-transform: rotate(25deg); -webkit-transform-origin: center bottom 0;} 73 | 78% {-webkit-transform: rotate(0deg); -webkit-transform-origin: center bottom 0;} 74 | 82% {-webkit-transform: rotate(-25deg); -webkit-transform-origin: center bottom 0;} 75 | 86% {-webkit-transform: rotate(0deg); -webkit-transform-origin: center bottom 0;} 76 | 100% {-webkit-transform: rotate(0deg); -webkit-transform-origin: center bottom 0;} 77 | 78 | } 79 | --------------------------------------------------------------------------------