added option to report users

This commit is contained in:
Miodec 2023-01-09 18:20:48 +01:00
parent ef6da63b73
commit adf47214db
11 changed files with 332 additions and 1 deletions

View file

@ -16,6 +16,9 @@ import * as LeaderboardsDAL from "../../dal/leaderboards";
import { purgeUserFromDailyLeaderboards } from "../../utils/daily-leaderboards";
import { randomBytes } from "crypto";
import * as RedisClient from "../../init/redis";
import { v4 as uuidv4 } from "uuid";
import { ObjectId } from "mongodb";
import * as ReportDAL from "../../dal/report";
async function verifyCaptcha(captcha: string): Promise<void> {
if (!(await verify(captcha))) {
@ -665,3 +668,31 @@ export async function updateInbox(
return new MonkeyResponse("Inbox updated");
}
export async function reportUser(
req: MonkeyTypes.Request
): Promise<MonkeyResponse> {
const { uid } = req.ctx.decodedToken;
const {
reporting: { maxReports, contentReportLimit },
} = req.ctx.configuration.quotes;
const { uid: uidToReport, reason, comment, captcha } = req.body;
await verifyCaptcha(captcha);
const newReport: MonkeyTypes.Report = {
_id: new ObjectId(),
id: uuidv4(),
type: "user",
timestamp: new Date().getTime(),
uid,
contentId: `${uidToReport}`,
reason,
comment,
};
await ReportDAL.createReport(newReport, maxReports, contentReportLimit);
return new MonkeyResponse("User reported");
}

View file

@ -6,6 +6,7 @@ import {
asyncHandler,
validateRequest,
validateConfiguration,
checkUserPermissions,
} from "../../middlewares/api-utils";
import * as RateLimit from "../../middlewares/rate-limit";
import { withApeRateLimiter } from "../../middlewares/ape-rate-limit";
@ -530,4 +531,46 @@ router.patch(
asyncHandler(UserController.updateInbox)
);
const withCustomMessages = joi.string().messages({
"string.pattern.base": "Invalid parameter format",
});
router.post(
"/report",
validateConfiguration({
criteria: (configuration) => {
return configuration.quotes.reporting.enabled;
},
invalidMessage: "User reporting is unavailable.",
}),
authenticateRequest(),
RateLimit.quoteReportSubmit,
validateRequest({
body: {
uid: withCustomMessages.regex(/^\w+$/).required(),
reason: joi
.string()
.valid(
"Inappropriate name",
"Inappropriate bio",
"Inappropriate social links",
"Suspected cheating"
)
.required(),
comment: withCustomMessages
.allow("")
.regex(/^([.]|[^/<>])+$/)
.max(250)
.required(),
captcha: withCustomMessages.regex(/[\w-_]+/).required(),
},
}),
checkUserPermissions({
criteria: (user) => {
return !user.cannotReport;
},
}),
asyncHandler(UserController.reportUser)
);
export default router;

View file

@ -432,7 +432,7 @@ declare namespace MonkeyTypes {
level?: number;
}
type ReportTypes = "quote";
type ReportTypes = "quote" | "user";
interface Report {
_id: ObjectId;

View file

@ -1131,6 +1131,60 @@
}
}
#userReportPopupWrapper {
#userReportPopup {
background: var(--bg-color);
border-radius: var(--roundness);
padding: 2rem;
display: grid;
gap: 1rem;
grid-template-rows: auto auto auto auto auto auto auto auto auto;
height: auto;
max-height: 40rem;
overflow-y: scroll;
width: calc(100% - 4rem);
margin-left: 2rem;
max-width: 800px;
label {
color: var(--sub-color);
margin-bottom: -1rem;
}
.text {
// color: var(--sub-color);
}
.user {
font-size: 1.5rem;
}
.title {
font-size: 1.5rem;
color: var(--sub-color);
}
textarea {
resize: vertical;
width: 100%;
padding: 10px;
line-height: 1.2rem;
min-height: 5rem;
}
.characterCount {
position: absolute;
top: -1.25rem;
right: 0.25rem;
color: var(--sub-color);
user-select: none;
&.red {
color: var(--error-color);
}
}
}
}
#resultEditTagsPanelWrapper {
#resultEditTagsPanel {
background: var(--bg-color);

View file

@ -232,6 +232,7 @@
padding: 1rem;
border-radius: var(--roundness);
align-content: center;
padding-right: 3rem;
// grid-template-columns: 15rem auto 15rem auto 2fr auto auto;

View file

@ -225,4 +225,20 @@ export default class Users {
};
return await this.httpClient.patch(`${BASE_PATH}/inbox`, { payload });
}
async report(
uid: string,
reason: string,
comment: string,
captcha: string
): Ape.EndpointData {
const payload = {
uid,
reason,
comment,
captcha,
};
return await this.httpClient.post(`${BASE_PATH}/report`, { payload });
}
}

View file

@ -11,6 +11,7 @@ type ProfileViewPaths = "profile" | "account";
interface ProfileData extends MonkeyTypes.Snapshot {
allTimeLbs: MonkeyTypes.LeaderboardMemory;
uid: string;
}
export async function update(
@ -21,6 +22,9 @@ export async function update(
const profileElement = $(`.page${elementClass} .profile`);
const details = $(`.page${elementClass} .profile .details`);
profileElement.attr("uid", profile.uid ?? "");
profileElement.attr("name", profile.name ?? "");
// ============================================================================
// DO FREAKING NOT USE .HTML OR .APPEND HERE - USER INPUT!!!!!!
// ============================================================================

View file

@ -4,6 +4,7 @@ import * as Profile from "../elements/profile";
import * as PbTables from "../account/pb-tables";
import * as Notifications from "../elements/notifications";
import { checkIfGetParameterExists } from "../utils/misc";
import * as UserReportPopup from "../popups/user-report-popup";
function reset(): void {
$(".page.pageProfile .preloader").removeClass("hidden");
@ -64,6 +65,15 @@ function reset(): void {
<div class="title">socials</div>
<div class="value">-</div>
</div>
<div class="buttonGroup">
<div
class="userReportButton button"
data-balloon-pos="left"
aria-label="Report user"
>
<i class="fas fa-flag"></i>
</div>
</div>
</div>
<div class="leaderboardsPositions">
<div class="title">All-Time English Leaderboards</div>
@ -181,6 +191,13 @@ async function update(options: UpdateOptions): Promise<void> {
}
}
$(".page.pageProfile").on("click", ".profile .userReportButton", () => {
const uid = $(".page.pageProfile .profile").attr("uid") ?? "";
const name = $(".page.pageProfile .profile").attr("name") ?? "";
UserReportPopup.show({ uid, name });
});
export const page = new Page(
"profile",
$(".page.pageProfile"),

View file

@ -0,0 +1,126 @@
import Ape from "../ape";
import * as Loader from "../elements/loader";
import * as Notifications from "../elements/notifications";
import * as CaptchaController from "../controllers/captcha-controller";
interface State {
userUid?: string;
}
const state: State = {
userUid: undefined,
};
interface ShowOptions {
uid: string;
name: string;
}
export async function show(options: ShowOptions): Promise<void> {
if ($("#userReportPopupWrapper").hasClass("hidden")) {
CaptchaController.render(
document.querySelector("#userReportPopup .g-recaptcha") as HTMLElement,
"userReportPopup"
);
const { name } = options;
state.userUid = options.uid;
$("#userReportPopup .user").text(name);
$("#userReportPopup .reason").val("Inappropriate name");
$("#userReportPopup .comment").val("");
$("#userReportPopup .characterCount").text("-");
$("#userReportPopup .reason").select2({
minimumResultsForSearch: Infinity,
});
$("#userReportPopupWrapper")
.stop(true, true)
.css("opacity", 0)
.removeClass("hidden")
.animate({ opacity: 1 }, 100, () => {
$("#userReportPopup textarea").trigger("focus").trigger("select");
});
}
}
export async function hide(): Promise<void> {
if (!$("#userReportPopupWrapper").hasClass("hidden")) {
$("#userReportPopupWrapper")
.stop(true, true)
.css("opacity", 1)
.animate(
{
opacity: 0,
},
100,
() => {
CaptchaController.reset("userReportPopup");
$("#userReportPopupWrapper").addClass("hidden");
}
);
}
}
async function submitReport(): Promise<void> {
const captchaResponse = CaptchaController.getResponse("userReportPopup");
if (!captchaResponse) {
return Notifications.add("Please complete the captcha");
}
const reason = $("#userReportPopup .reason").val() as string;
const comment = $("#userReportPopup .comment").val() as string;
const captcha = captchaResponse as string;
if (!reason) {
return Notifications.add("Please select a valid report reason");
}
if (!comment) {
return Notifications.add("Please provide a comment");
}
const characterDifference = comment.length - 250;
if (characterDifference > 0) {
return Notifications.add(
`Report comment is ${characterDifference} character(s) too long`
);
}
Loader.show();
const response = await Ape.users.report(
state.userUid as string,
reason,
comment,
captcha
);
Loader.hide();
if (response.status !== 200) {
return Notifications.add("Failed to report user: " + response.message, -1);
}
Notifications.add("Report submitted. Thank you!", 1);
hide();
}
$("#userReportPopupWrapper").on("mousedown", (e) => {
if ($(e.target).attr("id") === "userReportPopupWrapper") {
hide();
}
});
$("#userReportPopup .comment").on("input", () => {
setTimeout(() => {
const len = ($("#userReportPopup .comment").val() as string).length;
$("#userReportPopup .characterCount").text(len);
if (len > 250) {
$("#userReportPopup .characterCount").addClass("red");
} else {
$("#userReportPopup .characterCount").removeClass("red");
}
}, 1);
});
$("#userReportPopup .submit").on("click", async () => {
await submitReport();
});

View file

@ -74,6 +74,15 @@
<div class="title">socials</div>
<div class="value">-</div>
</div>
<div class="buttonGroup">
<div
class="userReportButton button"
data-balloon-pos="left"
aria-label="Report user"
>
<i class="fas fa-flag"></i>
</div>
</div>
</div>
<div class="leaderboardsPositions">
<div class="title">All-Time English Leaderboards</div>

View file

@ -736,6 +736,36 @@
<div class="button submit">Report</div>
</div>
</div>
<div id="userReportPopupWrapper" class="popupWrapper hidden">
<div id="userReportPopup" mode="">
<div class="title">Report a User</div>
<div class="text">
Please report users responsibly. Please add comments in English only.
Misuse may result in you losing access to this feature.
</div>
<label>user</label>
<div class="user"></div>
<label>reason</label>
<select name="report-reason" class="reason">
<option value="Inappropriate name">Inappropriate name</option>
<option value="Inappropriate bio">Inappropriate bio</option>
<option value="Inappropriate social links">
Inappropriate social links
</option>
<option value="Suspected cheating">Suspected cheating</option>
</select>
<label>comment</label>
<div style="position: relative">
<textarea class="comment" type="text" autocomplete="off"></textarea>
<div class="characterCount">-</div>
</div>
<div
class="g-recaptcha"
data-sitekey="6Lc-V8McAAAAAJ7s6LGNe7MBZnRiwbsbiWts87aj"
></div>
<div class="button submit">Report</div>
</div>
</div>
<div id="quoteApprovePopupWrapper" class="popupWrapper hidden">
<div id="quoteApprovePopup" mode="">
<div class="top">