mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-03-10 05:35:05 +08:00
(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:
parent
6c6ebc2a24
commit
a0d5392884
7 changed files with 194 additions and 9 deletions
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -29,6 +29,9 @@ const BASE_CONFIGURATION: MonkeyTypes.Configuration = {
|
|||
useRedisForBotTasks: {
|
||||
enabled: false,
|
||||
},
|
||||
favoriteQuotes: {
|
||||
maxFavorites: 100,
|
||||
},
|
||||
};
|
||||
|
||||
export default BASE_CONFIGURATION;
|
||||
|
|
|
@ -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 } }
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
4
backend/types/types.d.ts
vendored
4
backend/types/types.d.ts
vendored
|
@ -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>>;
|
||||
|
|
Loading…
Reference in a new issue