(Backend) Ability to make Quotes Favorite (#2826) rizwanmustafa

* Created API for making quotes fav

* Moved routes over to quotes

* Renamed favQuotes to favoriteQuotes

* Renamed routes

* Removed error to prevent confusion and panic

* Added regex check for quote id and added fav quote number check

* Some fixy

* Added configuration for max favorite quotes

* Added grouping

* Created variable for storing collection

* Changed variable name

* Changed variable name

* Fixed some problems

* Fixed bug

* Added length chekc for quoteId

* MOved routes over to user

* Renamed routes
This commit is contained in:
Rizwan Mustafa 2022-04-15 01:53:44 +05:00 committed by GitHub
parent 6c6ebc2a24
commit a0d5392884
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 194 additions and 9 deletions

View file

@ -302,3 +302,41 @@ export async function getPersonalBests(
)) ?? null;
return new MonkeyResponse("Personal bests retrieved", data);
}
export async function getFavoriteQuotes(
req: MonkeyTypes.Request
): Promise<MonkeyResponse> {
const { uid } = req.ctx.decodedToken;
const quotes = await UserDAL.getFavoriteQuotes(uid);
return new MonkeyResponse("Favorite quotes retrieved", quotes);
}
export async function addFavoriteQuote(
req: MonkeyTypes.Request
): Promise<MonkeyResponse> {
const { uid } = req.ctx.decodedToken;
const { language, quoteId } = req.body;
await UserDAL.addFavoriteQuote(
uid,
language,
quoteId,
req.ctx.configuration.favoriteQuotes.maxFavorites
);
return new MonkeyResponse("Quote added to favorites");
}
export async function removeFavoriteQuote(
req: MonkeyTypes.Request
): Promise<MonkeyResponse> {
const { uid } = req.ctx.decodedToken;
const { quoteId, language } = req.body;
await UserDAL.removeFavoriteQuote(uid, language, quoteId);
return new MonkeyResponse("Quote removed from favorites");
}

View file

@ -10,7 +10,7 @@ import {
validateRequest,
} from "../../middlewares/api-utils";
const quotesRouter = Router();
const router = Router();
const checkIfUserIsQuoteMod = checkUserPermissions({
criteria: (user) => {
@ -18,7 +18,7 @@ const checkIfUserIsQuoteMod = checkUserPermissions({
},
});
quotesRouter.get(
router.get(
"/",
RateLimit.newQuotesGet,
authenticateRequest(),
@ -26,7 +26,7 @@ quotesRouter.get(
asyncHandler(QuoteController.getQuotes)
);
quotesRouter.post(
router.post(
"/",
validateConfiguration({
criteria: (configuration) => {
@ -49,7 +49,7 @@ quotesRouter.post(
asyncHandler(QuoteController.addQuote)
);
quotesRouter.post(
router.post(
"/approve",
RateLimit.newQuotesAction,
authenticateRequest(),
@ -65,7 +65,7 @@ quotesRouter.post(
asyncHandler(QuoteController.approveQuote)
);
quotesRouter.post(
router.post(
"/reject",
RateLimit.newQuotesAction,
authenticateRequest(),
@ -78,7 +78,7 @@ quotesRouter.post(
asyncHandler(QuoteController.refuseQuote)
);
quotesRouter.get(
router.get(
"/rating",
RateLimit.quoteRatingsGet,
authenticateRequest(),
@ -91,7 +91,7 @@ quotesRouter.get(
asyncHandler(QuoteController.getRating)
);
quotesRouter.post(
router.post(
"/rating",
RateLimit.quoteRatingsSubmit,
authenticateRequest(),
@ -105,7 +105,7 @@ quotesRouter.post(
asyncHandler(QuoteController.submitRating)
);
quotesRouter.post(
router.post(
"/report",
validateConfiguration({
criteria: (configuration) => {
@ -140,4 +140,4 @@ quotesRouter.post(
asyncHandler(QuoteController.reportQuote)
);
export default quotesRouter;
export default router;

View file

@ -72,6 +72,9 @@ const usernameValidation = joi
"Username invalid. Name cannot contain special characters or contain more than 14 characters. Can include _ . and -",
});
const languageSchema = joi.string().min(1).required();
const quoteIdSchema = joi.string().min(1).max(5).regex(/\d+/).required();
router.get(
"/",
RateLimit.userGet,
@ -304,4 +307,37 @@ router.get(
asyncHandler(UserController.getPersonalBests)
);
router.get(
"/favoriteQuotes",
RateLimit.quoteFavoriteGet,
authenticateRequest(),
asyncHandler(UserController.getFavoriteQuotes)
);
router.post(
"/favoriteQuotes",
RateLimit.quoteFavoritePost,
authenticateRequest(),
validateRequest({
body: {
language: languageSchema,
quoteId: quoteIdSchema,
},
}),
asyncHandler(UserController.addFavoriteQuote)
);
router.delete(
"/favoriteQuotes",
RateLimit.quoteFavoriteDelete,
authenticateRequest(),
validateRequest({
body: {
language: languageSchema,
quoteId: quoteIdSchema,
},
}),
asyncHandler(UserController.removeFavoriteQuote)
);
export default router;

View file

@ -29,6 +29,9 @@ const BASE_CONFIGURATION: MonkeyTypes.Configuration = {
useRedisForBotTasks: {
enabled: false,
},
favoriteQuotes: {
maxFavorites: 100,
},
};
export default BASE_CONFIGURATION;

View file

@ -534,3 +534,85 @@ export async function getPersonalBests(
return user?.personalBests?.[mode];
}
export async function getFavoriteQuotes(
uid
): Promise<MonkeyTypes.User["favoriteQuotes"]> {
const user = await db.collection<MonkeyTypes.User>("users").findOne({ uid });
if (!user) {
throw new MonkeyError(404, "User not found", "getFavoriteQuotes");
}
return user.favoriteQuotes ?? {};
}
export async function addFavoriteQuote(
uid: string,
language: string,
quoteId: string,
maxQuotes: number
): Promise<void> {
const usersCollection = db.collection<MonkeyTypes.User>("users");
const user = await usersCollection.findOne({ uid });
if (!user) {
throw new MonkeyError(404, "User does not exist", "addFavoriteQuote");
}
if (user.favoriteQuotes) {
if (
user.favoriteQuotes[language] &&
user.favoriteQuotes[language].includes(quoteId)
) {
return;
}
const quotesLength = _.sumBy(
Object.values(user.favoriteQuotes),
(favQuotes) => favQuotes.length
);
if (quotesLength >= maxQuotes) {
throw new MonkeyError(
409,
"Too many favorite quotes",
"addFavoriteQuote"
);
}
}
await usersCollection.updateOne(
{ uid },
{
$push: {
[`favoriteQuotes.${language}`]: quoteId,
},
}
);
}
export async function removeFavoriteQuote(
uid: string,
language: string,
quoteId: string
): Promise<void> {
const usersCollection = await db.collection<MonkeyTypes.User>("users");
const user = await usersCollection.findOne({ uid });
if (!user) {
throw new MonkeyError(404, "User does not exist", "deleteFavoriteQuote");
}
if (
!user.favoriteQuotes ||
!user.favoriteQuotes[language] ||
!user.favoriteQuotes[language].includes(quoteId)
) {
return;
}
await usersCollection.updateOne(
{ uid },
{ $pull: { [`favoriteQuotes.${language}`]: quoteId } }
);
}

View file

@ -90,6 +90,28 @@ export const quoteReportSubmit = rateLimit({
handler: customHandler,
});
// Quote favorites
export const quoteFavoriteGet = rateLimit({
windowMs: 30 * 60 * 1000, // 30 min
max: 50 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const quoteFavoritePost = rateLimit({
windowMs: 30 * 60 * 1000, // 30 min
max: 50 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const quoteFavoriteDelete = rateLimit({
windowMs: 30 * 60 * 1000, // 30 min
max: 50 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
// Presets Routing
export const presetsGet = rateLimit({
windowMs: ONE_HOUR,

View file

@ -29,6 +29,9 @@ declare namespace MonkeyTypes {
useRedisForBotTasks: {
enabled: boolean;
};
favoriteQuotes: {
maxFavorites: number;
};
}
interface DecodedToken {
@ -71,6 +74,7 @@ declare namespace MonkeyTypes {
cannotReport?: boolean;
banned?: boolean;
canManageApeKeys?: boolean;
favoriteQuotes?: Record<string, string[]>;
}
type UserQuoteRatings = Record<string, Record<string, number>>;