Skip to content

Commit

Permalink
feat: Enable reporting users (#2577)
Browse files Browse the repository at this point in the history
  • Loading branch information
AmarTrebinjac authored Jan 9, 2025
1 parent 23a7a5d commit 9564be0
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 0 deletions.
24 changes: 24 additions & 0 deletions __tests__/workers/cdc/primary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ import {
SourcePostModerationStatus,
} from '../../../src/entity/SourcePostModeration';
import { NotificationType } from '../../../src/notifications/common';
import type { UserReport } from '../../../src/entity/UserReport';

jest.mock('../../../src/common', () => ({
...(jest.requireActual('../../../src/common') as Record<string, unknown>),
Expand All @@ -163,6 +164,7 @@ jest.mock('../../../src/common', () => ({
notifyPostReport: jest.fn(),
notifySourceReport: jest.fn(),
notifyCommentReport: jest.fn(),
notifyReportUser: jest.fn(),
notifySourceFeedAdded: jest.fn(),
notifySourceFeedRemoved: jest.fn(),
notifySettingsUpdated: jest.fn(),
Expand Down Expand Up @@ -1573,6 +1575,28 @@ describe('comment report', () => {
});
});

describe('User report', () => {
type ObjectType = UserReport;
const base: ChangeObject<ObjectType> = {
reportedUserId: '2',
reason: ReportReason.Harassment,
note: 'This guy is very mean',
};

it('should notify on new user report', async () => {
const after: ChangeObject<ObjectType> = base;
await expectSuccessfulBackground(
worker,
mockChangeMessage<ObjectType>({
after,
before: null,
op: 'c',
table: 'user_report',
}),
);
});
});

describe('post report', () => {
type ObjectType = PostReport;
const base: ChangeObject<ObjectType> = {
Expand Down
21 changes: 21 additions & 0 deletions src/common/reporting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import {
ReportEntity,
ReportReason,
sourceReportReasonsMap,
userReportReasonsMap,
} from '../entity/common';
import { ValidationError } from 'apollo-server-errors';
import { AuthContext } from '../Context';
import { SourceReport } from '../entity/sources/SourceReport';
import { ensureSourcePermissions } from '../schema/sources';
import { CommentReport } from '../entity/CommentReport';
import { UserReport } from '../entity/UserReport';

interface SaveHiddenPostArgs {
postId: string;
Expand Down Expand Up @@ -156,8 +158,27 @@ export const reportSource = async ({
});
};

export const reportUser = async ({
ctx,
id,
reason,
comment,
}: BaseReportArgs) => {
if (!userReportReasonsMap.has(reason)) {
throw new ValidationError('Reason is invalid');
}

await ctx.getRepository(UserReport).insert({
reportedUserId: id,
userId: ctx.userId,
reason,
note: comment,
});
};

export const reportFunctionMap: Record<ReportEntity, ReportFunction> = {
[ReportEntity.Post]: reportPost,
[ReportEntity.Comment]: reportComment,
[ReportEntity.Source]: reportSource,
[ReportEntity.User]: reportUser,
};
31 changes: 31 additions & 0 deletions src/common/slack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,37 @@ export const notifyCommentReport = async (
});
};

export const notifyReportUser = async (
reportedUserId: string,
reason: string,
note?: string,
): Promise<void> => {
await webhooks.content.send({
text: 'A user was just reported!',
attachments: [
{
title: `User profile`,
title_link: `https://app.daily.dev/${reportedUserId}`,
fields: [
{
title: 'Reported User',
value: reportedUserId,
},
{
title: 'Reason',
value: reason,
},
{
title: 'Note',
value: note || '',
},
],
color: '#FF1E1F',
},
],
});
};

export const getSlackIntegration = async ({
id,
userId,
Expand Down
37 changes: 37 additions & 0 deletions src/entity/UserReport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {
Column,
Entity,
Index,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import type { User } from './user';
import { ReportReason } from './common';

@Entity()
export class UserReport {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ type: 'text' })
@Index('IDX_user_report_reported_user_id')
reportedUserId: string;

@Column({ type: 'text' })
userId: string;

@Column({ default: () => 'now()' })
createdAt: Date;

@Column({ length: 36, type: 'varchar' })
reason: ReportReason;

@Column({ type: 'text', nullable: true })
note: string;

@ManyToOne('User', {
lazy: true,
onDelete: 'CASCADE',
})
user: Promise<User>;
}
18 changes: 18 additions & 0 deletions src/entity/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export enum ReportEntity {
Post = 'post',
Source = 'source',
Comment = 'comment',
User = 'user',
}

export enum ReportReason {
Expand All @@ -19,6 +20,10 @@ export enum ReportReason {
Miscategorized = 'MISCATEGORIZED',
Misinformation = 'MISINFORMATION',
Illegal = 'ILLEGAL',
Inappropriate = 'INAPPROPRIATE',
Trolling = 'TROLLING',
Impersonation = 'IMPERSONATION',
Plagiarism = 'PLAGIARISM',
}

export const postReportReasonsMap: Map<ReportReason, string> = new Map([
Expand Down Expand Up @@ -50,3 +55,16 @@ export const reportCommentReasonsMap: Map<ReportReason, string> = new Map([
[ReportReason.Misinformation, 'False Information or Misinformation'],
[ReportReason.Other, 'Other'],
]);

export const userReportReasonsMap: Map<ReportReason, string> = new Map([
[ReportReason.Inappropriate, 'Inappropriate or NSFW Content'],
[ReportReason.Trolling, 'Trolling or Disruptive Behavior'],
[ReportReason.Harassment, 'Harassment or Bullying'],
[ReportReason.Impersonation, 'Impersonation or False Identity'],
[ReportReason.Spam, 'Spam or Unsolicited Advertising'],
[ReportReason.Misinformation, 'Misinformation or False Claims'],
[ReportReason.Hateful, 'Hate Speech or Discrimination'],
[ReportReason.Privacy, 'Privacy or Copyright Violation'],
[ReportReason.Plagiarism, 'Plagiarism or Content Theft'],
[ReportReason.Other, 'Other'],
]);
17 changes: 17 additions & 0 deletions src/migration/1736363898760-UserReport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class UserReport1736363898760 implements MigrationInterface {
name = 'UserReport1736363898760'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE IF NOT EXISTS "user_report" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "reportedUserId" text NOT NULL, "userId" character varying NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "reason" character varying(36) NOT NULL, "note" text, CONSTRAINT "PK_58c08f0e20fa66561b119421eb2" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_user_report_reported_user_id" ON "user_report" ("reportedUserId") `);
await queryRunner.query(`ALTER TABLE "user_report" ADD CONSTRAINT "FK_cfc9cf9a552e98d6a634377496f" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_report" DROP CONSTRAINT "FK_cfc9cf9a552e98d6a634377496f"`);
await queryRunner.query(`DROP INDEX IF EXISTS "public"."IDX_user_report_reported_user_id"`);
await queryRunner.query(`DROP TABLE IF EXISTS "user_report"`);
}
}
16 changes: 16 additions & 0 deletions src/workers/cdc/primary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ import {
notifySourceReport,
DayOfWeek,
processApprovedModeratedPost,
notifyReportUser,
} from '../../common';
import { ChangeMessage, ChangeObject, UserVote } from '../../types';
import { DataSource, IsNull } from 'typeorm';
Expand Down Expand Up @@ -117,6 +118,7 @@ import {
postReportReasonsMap,
reportCommentReasonsMap,
sourceReportReasonsMap,
userReportReasonsMap,
} from '../../entity/common';
import { utcToZonedTime } from 'date-fns-tz';
import { SourceReport } from '../../entity/sources/SourceReport';
Expand All @@ -125,6 +127,7 @@ import {
SourcePostModerationStatus,
} from '../../entity/SourcePostModeration';
import { cleanupSourcePostModerationNotifications } from '../../notifications/common';
import { UserReport } from '../../entity/UserReport';

const isFreeformPostLongEnough = (
freeform: ChangeMessage<FreeformPost>,
Expand Down Expand Up @@ -629,6 +632,16 @@ const onPostReportChange = async (
}
};

const onUserReportChange = async (data: ChangeMessage<UserReport>) => {
if (data.payload.op === 'c') {
await notifyReportUser(
data.payload.after!.reportedUserId,
userReportReasonsMap.get(data.payload.after!.reason)!,
data.payload.after!.note,
);
}
};

const onCommentReportChange = async (
con: DataSource,
logger: FastifyBaseLogger,
Expand Down Expand Up @@ -1141,6 +1154,9 @@ const worker: Worker = {
case getTableName(con, PostReport):
await onPostReportChange(con, logger, data);
break;
case getTableName(con, UserReport):
await onUserReportChange(data);
break;
case getTableName(con, SourceReport):
await onSourceReportChange(con, logger, data);
break;
Expand Down

0 comments on commit 9564be0

Please sign in to comment.