Browse Source

feat(Activities): hide some activities that are repeated unnecessarily

Signed-off-by: Jonathan <theflametrooper@gmail.com>
Jonathan 4 years ago
parent
commit
6ad6ed7cf4

+ 127 - 6
backend/logic/activities.js

@@ -2,8 +2,9 @@ import async from "async";
 
 import CoreClass from "../core";
 
-// let ActivitiesModule;
+let ActivitiesModule;
 let DBModule;
+let CacheModule;
 let UtilsModule;
 let IOModule;
 
@@ -12,7 +13,7 @@ class _ActivitiesModule extends CoreClass {
 	constructor() {
 		super("activities");
 
-		// ActivitiesModule = this;
+		ActivitiesModule = this;
 	}
 
 	/**
@@ -23,6 +24,7 @@ class _ActivitiesModule extends CoreClass {
 	initialize() {
 		return new Promise(resolve => {
 			DBModule = this.moduleManager.modules.db;
+			CacheModule = this.moduleManager.modules.cache;
 			UtilsModule = this.moduleManager.modules.utils;
 			IOModule = this.moduleManager.modules.io;
 
@@ -54,6 +56,7 @@ class _ActivitiesModule extends CoreClass {
 							.then(res => next(null, res))
 							.catch(next);
 					},
+
 					(ActivityModel, next) => {
 						const { userId, type } = payload;
 
@@ -82,15 +85,133 @@ class _ActivitiesModule extends CoreClass {
 						});
 
 						return next(null, activity);
-					}
+					},
+
+					(activity, next) => {
+						const activitiesToCheckFor = [
+							"user__toggle_nightmode",
+							"user__toggle_autoskip_disliked_songs",
+							"song__like",
+							"song__unlike",
+							"song__dislike",
+							"song__undislike"
+						];
+
+						CacheModule.runJob("HGET", { table: "recentActivities", key: activity.userId })
+							.then(recentActivity => {
+								if (recentActivity) {
+									const FifteenMinsTimeDifference =
+										new Date() - new Date(recentActivity.createdAt) < 15 * 60 * 1000;
+
+									// check if most recent and the new activity have the same type, if it was in the last 15 mins,
+									// and if it is within the activitiesToCheckFor array
+									if (
+										recentActivity.type === activity.type &&
+										!!FifteenMinsTimeDifference &&
+										activitiesToCheckFor.includes(activity.type)
+									)
+										return ActivitiesModule.runJob(
+											"CHECK_FOR_ACTIVITY_SPAM",
+											{ userId: activity.userId, type: activity.type },
+											this
+										)
+											.then(() => next(null, activity))
+											.catch(next);
+
+									return next(null, activity);
+								}
+
+								return next(null, activity);
+							})
+							.catch(next);
+					},
+
+					// store most recent activity in cache to be quickly accessible
+					(activity, next) =>
+						CacheModule.runJob(
+							"HSET",
+							{
+								table: "recentActivities",
+								key: activity.userId,
+								value: { createdAt: activity.createdAt, type: activity.type }
+							},
+							this
+						)
+							.then(() => next(null))
+							.catch(next)
 				],
 				async (err, activity) => {
 					if (err) {
 						err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-						reject(new Error(err));
-					} else {
-						resolve(activity);
+						return reject(new Error(err));
 					}
+
+					return resolve(activity);
+				}
+			);
+		});
+	}
+
+	/**
+	 * Removes any activities of the same type within a 15-minute period to prevent spam
+	 *
+	 * @param {object} payload - object that contains the payload
+	 * @param {string} payload.userId - the id of the user to check for duplicates
+	 * @param {string} payload.type - the type of activity to check for duplicates
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	async CHECK_FOR_ACTIVITY_SPAM(payload) {
+		const activityModel = await DBModule.runJob("GET_MODEL", { modelName: "activity" }, this);
+
+		return new Promise((resolve, reject) => {
+			async.waterfall(
+				[
+					// find all activities of this type from the last 15 minutes
+					next => {
+						activityModel
+							.find(
+								{
+									userId: payload.userId,
+									type: payload.type,
+									hidden: false,
+									createdAt: {
+										$gte: new Date(new Date() - 15 * 60 * 1000)
+									}
+								},
+								"_id"
+							)
+							.sort({ createdAt: -1 })
+							.skip(1)
+							.exec(next);
+					},
+
+					// hide these activities and emit to socket listeners
+					(activities, next) => {
+						activities.forEach(activity => {
+							activityModel.updateOne({ _id: activity._id }, { $set: { hidden: true } }).catch(next);
+
+							IOModule.runJob("SOCKETS_FROM_USER", { userId: payload.userId }, this)
+								.then(res =>
+									res.sockets.forEach(socket => socket.emit("event:activity.hide", activity._id))
+								)
+								.catch(next);
+
+							IOModule.runJob("EMIT_TO_ROOM", {
+								room: `profile-${payload.userId}-activities`,
+								args: ["event:activity.hide", activity._id]
+							});
+						});
+
+						return next();
+					}
+				],
+				async err => {
+					if (err) {
+						err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+						return reject(new Error(err));
+					}
+
+					return resolve();
 				}
 			);
 		});

+ 2 - 7
backend/logic/cache/index.js

@@ -36,7 +36,8 @@ class _CacheModule extends CoreClass {
 			playlist: await importSchema("playlist"),
 			officialPlaylist: await importSchema("officialPlaylist"),
 			song: await importSchema("song"),
-			punishment: await importSchema("punishment")
+			punishment: await importSchema("punishment"),
+			recentActivity: await importSchema("recentActivity")
 		};
 
 		return new Promise((resolve, reject) => {
@@ -108,7 +109,6 @@ class _CacheModule extends CoreClass {
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	HSET(payload) {
-		// table, key, value, cb, stringifyJson = true
 		return new Promise((resolve, reject) => {
 			let { key } = payload;
 			let { value } = payload;
@@ -134,7 +134,6 @@ class _CacheModule extends CoreClass {
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	HGET(payload) {
-		// table, key, parseJson = true
 		return new Promise((resolve, reject) => {
 			let { key } = payload;
 
@@ -164,7 +163,6 @@ class _CacheModule extends CoreClass {
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	HDEL(payload) {
-		// table, key, cb
 		return new Promise((resolve, reject) => {
 			// if (!payload.key || !table || typeof key !== "string")
 			// return cb(null, null);
@@ -192,7 +190,6 @@ class _CacheModule extends CoreClass {
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	HGETALL(payload) {
-		// table, cb, parseJson = true
 		return new Promise((resolve, reject) => {
 			if (!payload.table) return reject(new Error("Invalid table!"));
 
@@ -219,7 +216,6 @@ class _CacheModule extends CoreClass {
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	PUB(payload) {
-		// channel, value, stringifyJson = true
 		return new Promise((resolve, reject) => {
 			/* if (pubs[channel] === undefined) {
             pubs[channel] = redis.createClient({ url: CacheModule.url });
@@ -250,7 +246,6 @@ class _CacheModule extends CoreClass {
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	SUB(payload) {
-		// channel, cb, parseJson = true
 		return new Promise((resolve, reject) => {
 			if (!payload.channel) return reject(new Error("Invalid channel!"));
 

+ 4 - 0
backend/logic/cache/schemas/recentActivity.js

@@ -0,0 +1,4 @@
+export default activity => ({
+	type: activity.type,
+	createdAt: activity.createdAt
+});

+ 0 - 2
backend/logic/playlists.js

@@ -471,7 +471,6 @@ class _PlaylistsModule extends CoreClass {
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	UPDATE_PLAYLIST(payload) {
-		// playlistId, cb
 		return new Promise((resolve, reject) =>
 			async.waterfall(
 				[
@@ -520,7 +519,6 @@ class _PlaylistsModule extends CoreClass {
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	DELETE_PLAYLIST(payload) {
-		// playlistId, cb
 		return new Promise((resolve, reject) =>
 			async.waterfall(
 				[

+ 0 - 1
backend/logic/songs.js

@@ -178,7 +178,6 @@ class _SongsModule extends CoreClass {
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	UPDATE_SONG(payload) {
-		// songId, cb
 		return new Promise((resolve, reject) =>
 			async.waterfall(
 				[

+ 4 - 1
frontend/src/pages/Profile/tabs/RecentActivity.vue

@@ -103,6 +103,8 @@ export default {
 			});
 
 			this.socket.on("event:activity.removeAllForUser", () => {
+				console.log("jasdasdsad");
+
 				this.activities = [];
 				this.position = 1;
 				this.maxPosition = 1;
@@ -156,6 +158,7 @@ export default {
 <style lang="scss" scoped>
 #activity-items {
 	overflow: auto;
-	height: 600px;
+	min-height: auto;
+	max-height: 600px;
 }
 </style>