From 5e6f42159e6e18a8019b9a70b9e0255d19f80d95 Mon Sep 17 00:00:00 2001 From: Varsh Date: Thu, 4 Dec 2025 16:29:01 +0100 Subject: [PATCH] appeal sdk --- src/moderation.ts | 114 ++++++++++++++++++++++++++- src/types.ts | 194 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 303 insertions(+), 5 deletions(-) diff --git a/src/moderation.ts b/src/moderation.ts index 230f68482..9fc132bc6 100644 --- a/src/moderation.ts +++ b/src/moderation.ts @@ -1,6 +1,12 @@ import type { APIResponse, + AppealOptions, + AppealRequest, + AppealResponse, + AppealsSort, CustomCheckFlag, + DecideAppealRequest, + GetAppealResponse, GetConfigResponse, GetUserModerationReportOptions, GetUserModerationReportResponse, @@ -11,6 +17,9 @@ import type { ModerationRuleRequest, MuteUserResponse, Pager, + QueryAppealsFilters, + QueryAppealsPaginationOptions, + QueryAppealsResponse, QueryConfigsResponse, QueryModerationConfigsFilters, QueryModerationConfigsSort, @@ -24,6 +33,7 @@ import type { ReviewQueueResponse, ReviewQueueSort, SubmitActionOptions, + SubmitActionResponse, UpsertConfigResponse, UpsertModerationRuleResponse, } from './types'; @@ -186,6 +196,108 @@ export class Moderation { ); } + /** + * Appeal against the moderation decision + * @param {AppealRequest} appealRequest Appeal request to be appealed against + */ + async appeal(appealRequest: AppealRequest, options: AppealOptions = {}) { + return await this.client.post( + this.client.baseURL + '/api/v2/moderation/appeal', + { + appeal_reason: appealRequest.appeal_reason || appealRequest.text, + entity_id: appealRequest.entityID, + entity_type: appealRequest.entityType, + attachments: appealRequest.attachments, + ...options, + }, + ); + } + + /** + * Decide on an appeal + * @param {DecideAppealRequest} decideAppealRequest Request to decide on an appeal + */ + async decideAppeal( + decideAppealRequest: DecideAppealRequest, + options: AppealOptions = {}, + ) { + return await this.client.post( + this.client.baseURL + '/api/v2/moderation/decide_appeal', + { + appeal_id: decideAppealRequest.appealID, + status: decideAppealRequest.status, + decision_reason: decideAppealRequest.decisionReason, + ...(decideAppealRequest.channelCIDs + ? { channel_cids: decideAppealRequest.channelCIDs } + : {}), + ...options, + }, + ); + } + + /** + * Accept an appeal + * @param {appealID} appealID ID of appeal + * @param {decisionReason} decisionReason Reason for accepting an appeal + */ + async acceptAppeal( + appealID: string, + decisionReason: string, + options: AppealOptions = {}, + ) { + return await this.decideAppeal( + { appealID, decisionReason, status: 'accepted' }, + options, + ); + } + + /** + * Reject an appeal + * @param {appealID} appealID ID of appeal + * @param {decisionReason} decisionReason Reason for rejecting an appeal + */ + async rejectAppeal( + appealID: string, + decisionReason: string, + options: AppealOptions = {}, + ) { + return await this.decideAppeal( + { appealID, decisionReason, status: 'rejected' }, + options, + ); + } + + /** + * Get Appeal Item + * @param {string} appealID ID of the appeal to be fetched + */ + async getAppeal(appealID: string) { + return await this.client.get( + this.client.baseURL + '/api/v2/moderation/appeal/' + appealID, + ); + } + + /** + * Query appeals + * @param {Object} filterConditions Filter conditions for querying appeals + * @param {Object} sort Sort conditions for querying appeals + * @param {Object} options Pagination options for querying appeals + */ + async queryAppeals( + filterConditions: QueryAppealsFilters = {}, + sort: AppealsSort = [], + options: QueryAppealsPaginationOptions = {}, + ) { + return await this.client.post( + this.client.baseURL + '/api/v2/moderation/appeals', + { + filter: filterConditions, + sort: normalizeQuerySort(sort), + ...options, + }, + ); + } + /** * Upsert moderation config * @param {Object} config Moderation config to be upserted @@ -241,7 +353,7 @@ export class Moderation { itemID: string, options: SubmitActionOptions = {}, ) { - return await this.client.post<{ item_id: string } & APIResponse>( + return await this.client.post( this.client.baseURL + '/api/v2/moderation/submit_action', { action_type: actionType, diff --git a/src/types.ts b/src/types.ts index 6e2495aaa..fd9bd0d82 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3620,6 +3620,23 @@ export type ModerationFlag = { moderation_payload_hash?: string; }; +export type AppealItem = { + attachments: string[]; + created_at: string; + updated_at: string; + decision_reason: string; + entity_id: string; + entity_type: string; + status: string; + /** + * @deprecated use appeal_reason instead + */ + text?: string; + appeal_reason: string; + user: UserResponse; + id: string; +}; + export type ReviewQueueItem = { // eslint-disable-next-line @typescript-eslint/no-explicit-any actions_taken: any[]; @@ -3648,6 +3665,7 @@ export type ReviewQueueItem = { reviewed_at: string; status: string; updated_at: string; + appeal?: AppealItem; }; export type CustomCheckFlag = { @@ -3667,28 +3685,100 @@ export type DeleteMessageOptions = { hardDelete?: boolean; }; +export type UnbanActionRequest = { + channel_cid?: string; + decision_reason?: string; +}; + +export type RejectAppealRequest = { + decision_reason: string; +}; + +export type RestoreActionRequest = { + decision_reason?: string; +}; + +export type UnblockActionRequest = { + decision_reason?: string; +}; + +export type BlockActionRequest = { + reason: string; +}; + +export type ShadowBlockActionRequest = { + reason?: string; +}; + +export type VideoKickUserRequest = Record; + +export type VideoEndCallRequest = Record; + +export type MarkReviewedRequest = { + disable_marking_content_as_reviewed?: boolean; + content_to_mark_as_reviewed_limit?: number; + decision_reason?: string; +}; + +export type DeleteActivityRequest = { + hard_delete: boolean; + reason?: string; +}; + +export type DeleteCommentRequest = { + hard_delete: boolean; + reason?: string; +}; + +export type DeleteReactionRequest = { + hard_delete: boolean; + reason?: string; +}; + export type SubmitActionOptions = { + appeal_id?: string; ban?: { channel_ban_only?: boolean; reason?: string; timeout?: number; + decision_reason?: string; delete_messages?: MessageDeletionStrategy; + shadow?: boolean; + ip_ban?: boolean; }; delete_message?: { hard_delete?: boolean; + decision_reason?: string; + reason?: string; }; delete_user?: { delete_conversation_channels?: boolean; hard_delete?: boolean; mark_messages_deleted?: boolean; + decision_reason?: string; + reason?: string; + delete_feeds_content?: boolean; }; - restore?: {}; - unban?: { - channel_cid?: string; - }; + restore?: RestoreActionRequest; + unban?: UnbanActionRequest; + block?: BlockActionRequest; + shadow_block?: ShadowBlockActionRequest; + kick_user?: VideoKickUserRequest; + end_call?: VideoEndCallRequest; + reject_appeal?: RejectAppealRequest; + mark_reviewed?: MarkReviewedRequest; + delete_activity?: DeleteActivityRequest; + delete_comment?: DeleteCommentRequest; + delete_reaction?: DeleteReactionRequest; + unblock?: UnblockActionRequest; user_id?: string; }; +export type SubmitActionResponse = APIResponse & { + item?: ReviewQueueItem; + appeal_item?: AppealItem; +}; + export type GetUserModerationReportResponse = { user: UserResponse; user_blocks?: Array<{ @@ -3829,6 +3919,12 @@ export type ReviewQueueFilters = QueryFilters< date_range?: RequireOnlyOne<{ $eq?: string; // Format: "date1_date2" }>; + } & { + appeal?: boolean; + } & { + appeal_status?: RequireOnlyOne<{ + $eq?: 'submitted' | 'accepted' | 'rejected'; + }>; } >; @@ -3836,6 +3932,60 @@ export type ReviewQueueSort = | Sort> | Array>>; +export type AppealsSort = + | Sort> + | Array>>; + +export type QueryAppealsFilters = QueryFilters< + { + entity_type?: + | RequireOnlyOne, '$eq' | '$in'>> + | PrimitiveFilter; + } & { + created_at?: + | RequireOnlyOne< + Pick< + QueryFilter, + '$eq' | '$gt' | '$lt' | '$gte' | '$lte' + > + > + | PrimitiveFilter; + } & { + id?: + | RequireOnlyOne, '$eq' | '$in'>> + | PrimitiveFilter; + } & { + entity_id?: + | RequireOnlyOne, '$eq' | '$in'>> + | PrimitiveFilter; + } & { + status?: + | RequireOnlyOne, '$eq' | '$in'>> + | PrimitiveFilter; + } & { + updated_at?: + | RequireOnlyOne< + Pick< + QueryFilter, + '$eq' | '$gt' | '$lt' | '$gte' | '$lte' + > + > + | PrimitiveFilter; + } & { + text?: RequireOnlyOne<{ + $eq?: string; + }>; + } & { + decision_reason?: RequireOnlyOne<{ + $eq?: string; + }>; + } & { + review_queue_item_id?: RequireOnlyOne<{ + $eq?: string; + }>; + } +>; + export type QueryModerationConfigsSort = Array>; export type ReviewQueuePaginationOptions = Pager; @@ -3846,6 +3996,14 @@ export type ReviewQueueResponse = { prev?: string; }; +export type QueryAppealsResponse = APIResponse & { + items: AppealItem[]; + next?: string; + prev?: string; +}; + +export type QueryAppealsPaginationOptions = Pager; + export type ModerationConfig = { key: string; ai_image_config?: AIImageConfig; @@ -3878,6 +4036,14 @@ export type UpsertConfigResponse = { config: ModerationConfigResponse; }; +export type AppealResponse = APIResponse & { + appeal_id: string; +}; + +export type GetAppealResponse = APIResponse & { + item: AppealItem; +}; + // Moderation Rule Builder Types export type ModerationRule = { id: string; @@ -3900,6 +4066,21 @@ export type ModerationRuleRequest = { enabled: boolean; }; +export type AppealRequest = { + appeal_reason?: string; + text?: string; + entityID: string; + entityType: string; + attachments: string[]; +}; + +export type DecideAppealRequest = { + appealID: string; + status: 'accepted' | 'rejected'; + decisionReason: string; + channelCIDs?: string[]; +}; + export type RuleBuilderRule = { id: string; rule_type: 'user' | 'content'; @@ -4034,6 +4215,11 @@ export type ModerationFlagOptions = { user_id?: string; }; +export type AppealOptions = { + custom?: Record; + user_id?: string; +}; + export type ModerationMuteOptions = { timeout?: number; user_id?: string;