diff --git a/.gitignore b/.gitignore index 0e732e0e..85d6d63f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ views/legit.* config/community_guidelines.md config/privacy_policy.md config/tos.md +config/achievements.js modules/* ### Linux ### diff --git a/app.js b/app.js index 9055d248..7612a35a 100644 --- a/app.js +++ b/app.js @@ -95,7 +95,8 @@ app.recreateServer = () => { } app.recreateServer(); -mongoose.connect(app.config.database); +mongoose.connect(app.config.database, {useMongoClient: true}); +mongoose.Promise = global.Promise; const handlePendingDeletions = () => { setInterval(() => { diff --git a/config/achievements.js b/config/achievements.js new file mode 100644 index 00000000..ba3f539a --- /dev/null +++ b/config/achievements.js @@ -0,0 +1,126 @@ +const olderThan = (date = Date.now(), time = {milliseconds: 0, seconds: 0, minutes: 0, hours: 0, days: 0, weeks: 0, months: 0, years: 0}) => { + let computedTime = Date.now(); + if (time.milliseconds) { + computedTime -= time.milliseconds; + } + if (time.seconds) { + computedTime -= time.seconds * 1000; + } + if (time.minutes) { + computedTime -= time.minutes * 1000 * 60; + } + if (time.hours) { + computedTime -= time.hours * 1000 * 60 * 60; + } + if (time.days) { + computedTime -= time.days * 1000 * 60 * 60 * 24; + } + if (time.weeks) { + computedTime -= time.weeks * 1000 * 60 * 60 * 24 * 7; + } + if (time.months) { + let cTime = new Date(); + cTime.setTime(Date.now() - computedTime); + const newMonths = cTime.getMonth() + time.months; + cTime.setMonth(newMonths); + computedTime = cTime.getTime(); + } + if (time.years) { + computedTime -= time.years * 1000 * 60 * 60 * 24 * 365; + } + console.log("Minimum: " + computedTime); + console.log("Date: " + date); + return computedTime > date; +}; + +module.exports = [ + // User has placed at least five pixels + { + name: "First Pixel!", + description: "You've placed one pixel!", + imageURL: null, + meetsCriteria(user) { + return user.placeCount >= 1; + } + }, + { + name: "Ten Pixels!", + description: "Congrats on hitting ten pixels! Keep it going!", + imageURL: null, + meetsCriteria(user) { + return user.placeCount >= 10; + } + }, + { + name: "100 Pixels!", + description: "W00T! Let's go let's go let's go!!! Get to 1000 pixels!", + imageURL: null, + meetsCriteria(user) { + return user.placeCount >= 100; + } + }, + { + name: "1000 Pixels!", + description: "I see you, placing those pixels all sexy 'n shit.", + imageURL: null, + meetsCriteria(user) { + return user.placeCount >= 1000; + } + }, + { + name: "Addict", + description: "People can safely default to assuming you're on canvas.place", + imageURL: null, + meetsCriteria(user) { + return user.placeCount >= 10000; + } + }, + { + name: "Ultra-Addict", + description: "You play canvas.place so much, you should just run the website.", + imageURL: null, + meetsCriteria(user) { + return user.placeCount >= 50000; + } + }, + { + name: "Beginner", + description: "You're just starting out, but don't fret. You're going great places.", + imageURL: null, + meetsCriteria(user) { + return olderThan(user.creationDate, {days: 1}); + } + }, + { + name: "Novice", + description: "You're wiser than the average fellow but have much left to learn.", + imageURL: null, + meetsCriteria(user) { + return olderThan(user.creationDate, {weeks: 1}); + } + }, + { + name: "Intermediate", + description: "You're getting the hang of it! Keep at it.", + imageURL: null, + meetsCriteria(user) { + return olderThan(user.creationDate, {months: 1}); + } + }, + { + name: "Advanced", + description: "You've got it! I consider you to be proficient at the art of placing.", + imageURL: null, + meetsCriteria(user) { + return olderThan(user.creationDate, {months: 6}); + } + }, + { + name: "Expert", + description: "You're better than me, so..", + imageURL: null, + meetsCriteria(user) { + return olderThan(user.creationDate, {years: 1}); + } + } +]; \ No newline at end of file diff --git a/controllers/AchievementsController.js b/controllers/AchievementsController.js new file mode 100644 index 00000000..69686123 --- /dev/null +++ b/controllers/AchievementsController.js @@ -0,0 +1,19 @@ +const User = require("../models/user"); + +const handleBadRequest = (res, e) => { + console.error(e); + res.status(500).json({success: false, error: {message: "An error occurred while processing your request"}}); +} + +exports.getUserAchievements = (req, res, next) => { + if (!req.params.username) return res.status(400).json({success: false, error: {code: "bad_request", message: "The username parameter is rqeuired."}}); + const name = req.params.username; + User.findByUsername(name).then(user => { + if (!user) { + return res.status(404).json({success: false, error: {code: "not_found", message: "The username provided does not match any registered users."}}); + } + user.getAchievements().then(achievements => { + res.json({achievements, success: true}); + }).catch((e) => handleBadRequest(res, e)); + }).catch((e) => handleBadRequest(res, e)); +} \ No newline at end of file diff --git a/models/access.js b/models/access.js index ad25756a..98e2efbb 100644 --- a/models/access.js +++ b/models/access.js @@ -85,7 +85,7 @@ AccessSchema.statics.findSimilarIPUserIDs = function(user) { this.findIPsForUser(user).then((ipAddresses) => { this.find({ hashedIPAddress: { $in: ipAddresses }, userID: { $ne: user._id } }).then((accesses) => { var userIDs = accesses.map((access) => String(access.userID)); - resolve([...new Set(userIDs)]); + resolve(stripDuplicates((userIDs))); }).catch(reject); }).catch(reject); }); diff --git a/models/user.js b/models/user.js index cb80a0dc..a9da65d5 100644 --- a/models/user.js +++ b/models/user.js @@ -6,6 +6,7 @@ const Pixel = require("./pixel"); const Access = require("./access"); const dataTables = require("mongoose-datatables"); const TOSManager = require("../util/TOSManager"); +const achievements = require("../config/achievements"); var UserSchema = new Schema({ name: { @@ -124,6 +125,13 @@ UserSchema.pre("save", function(next) { } }); +UserSchema.methods.getAchievements = function() { + return new Promise((resolve, reject) => { + const userAchievements = achievements.filter(achievement => achievement.meetsCriteria(this)); + resolve(userAchievements); + }); +} + UserSchema.methods.comparePassword = function(passwd, cb) { bcrypt.compare(passwd, this.password, function(err, isMatch) { if (err) return cb(err); diff --git a/public/js/jquery.confetti.js b/public/js/jquery.confetti.js old mode 100644 new mode 100755 diff --git a/routes/api.js b/routes/api.js index 825f98d9..6c78c292 100644 --- a/routes/api.js +++ b/routes/api.js @@ -15,6 +15,7 @@ const AccountPageController = require("../controllers/AccountPageController"); const TOTPSetupController = require("../controllers/TOTPSetupController"); const ChangelogController = require("../controllers/ChangelogController"); const WarpController = require("../controllers/WarpController"); +const AchievementsController = require("../controllers/AchievementsController"); const UserDownloadController = require("../controllers/UserDownloadController"); function APIRouter(app) { @@ -187,6 +188,7 @@ function APIRouter(app) { router.route("/chat").get(ChatController.getAPIChat).post([requireUser, chatRatelimit.prevent], ChatController.postAPIChatMessage); router.get("/user/:username", AccountPageController.getAPIAccount); + router.get("/user/:username/achievements", AchievementsController.getUserAchievements); router.get("/changelog/latest", ChangelogController.getLatestChangelog); router.route("/changelog/missed").get([requireUser, ChangelogController.getMissedChangelogs]).post([requireUser, ChangelogController.postMissedChangelogs]).delete([requireUser, ChangelogController.deleteMissedChangelogs]); diff --git a/views/layout.pug b/views/layout.pug index a83b3ff2..51dea5ff 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -18,6 +18,7 @@ block dependencies - var hasCommunityGuidelines = fs.existsSync("./config/community_guidelines.md"); - var hasTOS = TOSManager.hasTOSSync(); - var hasPP = TOSManager.hasPrivacyPolicySync(); +- const stripDuplicates = (arr = []) => { for (let i = 0; i < arr.length; i++) { if (arr.indexOf(arr[i]) !== i) arr.splice(i, 1); } return arr; } mixin renderBadge(badge, prefersShortText = false) span.label.badge-label(class=`label-${badge.style || "default"}`, title=badge.title) #{prefersShortText && badge.shortText ? badge.shortText : badge.text} mixin renderBadges(badges, prefersShortText = false) @@ -73,7 +74,7 @@ html(lang="en") link(href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css", rel="stylesheet") // Provided CSS - css.unshift("/css/global.css") - each item in [...new Set(css)] + each item in stripDuplicates(css) link(href=item + "?v=" + resourceVersion, rel="stylesheet") +getViewExtensions("head") // Shivs @@ -180,7 +181,7 @@ html(lang="en") - js.unshift("https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js", "/js/cookies-eu-banner.min.js", "/js/build/site.js"); each item in [...new Set(js)] script(src=item + "?v=" + resourceVersion) - each item in [...new Set(jsSnippets.concat(jsModSnippets))] + each item in stripDuplicates(jsSnippets.concat(jsModSnippets)) script !{item} if fs.existsSync("./views/public/legit.js") && !isAdmin && needsLegit script !{fs.readFileSync("./views/public/legit.js")} @@ -194,4 +195,4 @@ html(lang="en") ga("create", "#{config.googleAnalyticsTrackingID}", "auto"); ga("send", "pageview"); - }); \ No newline at end of file + }); diff --git a/views/public/index.pug b/views/public/index.pug index f1cb7094..f1f23476 100644 --- a/views/public/index.pug +++ b/views/public/index.pug @@ -25,4 +25,4 @@ block content unless user include views/auth-dialog include views/help-dialog - - var js = ["https://cdn.jsdelivr.net/interact.js/1.2.6/interact.min.js", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap-slider/10.0.0/bootstrap-slider.min.js", "https://cdn.jsdelivr.net/jquery.minicolors/2.1.2/jquery.minicolors.min.js", "https://cdn.jsdelivr.net/clipboard.js/1.6.0/clipboard.min.js", "https://unpkg.com/eventemitter3@latest/umd/eventemitter3.min.js", "/js/build/socket.js", "/js/build/popout.js", "/js/build/place.js", "https://cdn.rawgit.com/rmm5t/jquery-timeago/180864a9c544a49e43719b457250af216d5e4c3a/jquery.timeago.js"]; \ No newline at end of file + - var js = ["https://cdn.jsdelivr.net/interact.js/1.2.6/interact.min.js", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap-slider/10.0.0/bootstrap-slider.min.js", "https://cdn.jsdelivr.net/jquery.minicolors/2.1.2/jquery.minicolors.min.js", "https://cdn.jsdelivr.net/clipboard.js/1.6.0/clipboard.min.js", "https://unpkg.com/eventemitter3@latest/umd/eventemitter3.min.js", "/js/build/socket.js", "/js/build/popout.js", "/js/build/place.js", "https://cdn.rawgit.com/rmm5t/jquery-timeago/180864a9c544a49e43719b457250af216d5e4c3a/jquery.timeago.js"];