Skip to content

Commit

Permalink
feat: moderation v2 api endpoints (#1312)
Browse files Browse the repository at this point in the history
Co-authored-by: Jimmy Pettersson <jimmy.pettersson@getstream.io>
Co-authored-by: Guyon Morée <guyon.moree@gmail.com>
  • Loading branch information
3 people authored Jul 1, 2024
1 parent dd7b1fe commit b1f47f0
Show file tree
Hide file tree
Showing 2 changed files with 244 additions and 2 deletions.
97 changes: 95 additions & 2 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,14 @@ import {
QueryMessageHistorySort,
QueryMessageHistoryOptions,
QueryMessageHistoryResponse,
GetUserModerationReportResponse,
ReviewQueueFilters,
ReviewQueueSort,
ReviewQueuePaginationOptions,
ReviewQueueResponse,
GetConfigResponse,
UpsertConfigResponse,
Config,
} from './types';
import { InsightMetrics, postInsights } from './insights';
import { Thread } from './thread';
Expand Down Expand Up @@ -1549,6 +1557,91 @@ export class StreamChat<StreamChatGenerics extends ExtendableGenerics = DefaultG
});
}

async getUserModerationReport(
userID: string,
options: {
create_user_if_not_exists?: boolean;
include_user_blocks?: boolean;
include_user_mutes?: boolean;
} = {},
) {
return await this.get<GetUserModerationReportResponse<StreamChatGenerics>>(
this.baseURL + `/api/v2/moderation/user_report`,
{
user_id: userID,
...options,
},
);
}

async queryReviewQueue(
filterConditions: ReviewQueueFilters = {},
sort: ReviewQueueSort = [],
options: ReviewQueuePaginationOptions = {},
) {
return await this.post<ReviewQueueResponse>(this.baseURL + '/api/v2/moderation/review_queue', {
filter: filterConditions,
sort: normalizeQuerySort(sort),
...options,
});
}

async upsertConfig(config: Config = {}) {
return await this.post<UpsertConfigResponse>(this.baseURL + '/api/v2/moderation/config', config);
}

async getConfig(key: string) {
return await this.get<GetConfigResponse>(this.baseURL + '/api/v2/moderation/config/' + key);
}

async flagUserV2(flaggedUserID: string, reason: string, options: Record<string, unknown> = {}) {
return this.flagV2('stream:user', flaggedUserID, '', reason, options);
}

async flagMessageV2(messageID: string, reason: string, options: Record<string, unknown> = {}) {
return this.flagV2('stream:chat:v1:message', messageID, '', reason, options);
}

async flagV2(
entityType: string,
entityId: string,
entityCreatorID: string,
reason: string,
options: Record<string, unknown> = {},
) {
return await this.post<{ item_id: string } & APIResponse>(this.baseURL + '/api/v2/moderation/flag', {
entity_type: entityType,
entity_id: entityId,
entity_creator_id: entityCreatorID,
reason,
...options,
});
}

async muteUserV2(
targetID: string,
options: {
timeout?: number;
user_id?: string;
} = {},
) {
return await this.post<MuteUserResponse & APIResponse>(this.baseURL + '/api/v2/moderation/mute', {
target_ids: [targetID],
...options,
});
}

async unmuteUserV2(
targetID: string,
options: {
user_id?: string;
},
) {
return await this.post<{ item_id: string } & APIResponse>(this.baseURL + '/api/v2/moderation/unmute', {
target_ids: [targetID],
...options,
});
}
/**
* queryChannels - Query channels
*
Expand Down Expand Up @@ -2258,7 +2351,7 @@ export class StreamChat<StreamChatGenerics extends ExtendableGenerics = DefaultG
* @param {string} [options.user_id] currentUserID, only used with serverside auth
* @returns {Promise<APIResponse>}
*/
async flagMessage(targetMessageID: string, options: { user_id?: string } = {}) {
async flagMessage(targetMessageID: string, options: { reason?: string; user_id?: string } = {}) {
return await this.post<FlagMessageResponse<StreamChatGenerics>>(this.baseURL + '/moderation/flag', {
target_message_id: targetMessageID,
...options,
Expand All @@ -2271,7 +2364,7 @@ export class StreamChat<StreamChatGenerics extends ExtendableGenerics = DefaultG
* @param {string} [options.user_id] currentUserID, only used with serverside auth
* @returns {Promise<APIResponse>}
*/
async flagUser(targetID: string, options: { user_id?: string } = {}) {
async flagUser(targetID: string, options: { reason?: string; user_id?: string } = {}) {
return await this.post<FlagUserResponse<StreamChatGenerics>>(this.baseURL + '/moderation/flag', {
target_user_id: targetID,
...options,
Expand Down
149 changes: 149 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ export type FlagMessageResponse<StreamChatGenerics extends ExtendableGenerics =
reviewed_at?: string;
reviewed_by?: string;
};
review_queue_item_id?: string;
};

export type FlagUserResponse<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics> = APIResponse & {
Expand All @@ -457,6 +458,7 @@ export type FlagUserResponse<StreamChatGenerics extends ExtendableGenerics = Def
reviewed_at?: string;
reviewed_by?: string;
};
review_queue_item_id?: string;
};

export type FormatMessageResponse<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics> = Omit<
Expand Down Expand Up @@ -1899,6 +1901,7 @@ export type AsyncModerationOptions = {

export type AppSettings = {
agora_options?: AgoraOptions | null;
allowed_flag_reasons?: string[];
apn_config?: {
auth_key?: string;
auth_type?: string;
Expand Down Expand Up @@ -2006,6 +2009,8 @@ export type OGAttachment = {
export type BlockList = {
name: string;
words: string[];
type?: string;
validate?: boolean;
};

export type ChannelConfig<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics> = ChannelConfigFields &
Expand Down Expand Up @@ -3149,3 +3154,147 @@ export type QueryMessageHistoryResponse<StreamChatGenerics extends ExtendableGen
next?: string;
prev?: string;
};

// Moderation v2
export type ModerationPayload = {
created_at: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
custom?: Record<string, any>;
images?: string[];
texts?: string[];
videos?: string[];
};

export type ModV2ReviewStatus = 'complete' | 'flagged' | 'partial';

export type ModerationFlag = {
created_at: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
custom: Record<string, any>;
entity_creator_id: string;
entity_id: string;
entity_type: string;
id: string;
reason: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result: Record<string, any>[];
review_queue_item_id: string;
updated_at: string;
user: UserResponse;
moderation_payload?: ModerationPayload;
moderation_payload_hash?: string;
};

export type ReviewQueueItem = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
actions_taken: any[];
appealed_by: string;
assigned_to: string;
completed_at: string;
config_key: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: any[];
created_at: string;
created_by: string;
entity_id: string;
entity_type: string;
flags: ModerationFlag[];
has_image: boolean;
has_text: boolean;
has_video: boolean;
id: string;
moderation_payload: ModerationPayload;
moderation_payload_hash: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
options: any;
recommended_action: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
results: any;
reviewed_at: string;
status: string;
updated_at: string;
};

export type GetUserModerationReportResponse<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics> = {
user: UserResponse<StreamChatGenerics>;
user_blocks?: Array<{
blocked_at: string;
blocked_by_user_id: string;
blocked_user_id: string;
}>;
user_mutes?: Mute<StreamChatGenerics>[];
};

export type ReviewQueueFilters = QueryFilters<
{
assigned_to?:
| RequireOnlyOne<Pick<QueryFilter<ReviewQueueItem['assigned_to']>, '$eq' | '$in'>>
| PrimitiveFilter<ReviewQueueItem['assigned_to']>;
} & {
completed_at?:
| RequireOnlyOne<Pick<QueryFilter<ReviewQueueItem['completed_at']>, '$eq' | '$gt' | '$lt' | '$gte' | '$lte'>>
| PrimitiveFilter<ReviewQueueItem['completed_at']>;
} & {
config_key?:
| RequireOnlyOne<Pick<QueryFilter<ReviewQueueItem['config_key']>, '$eq' | '$in'>>
| PrimitiveFilter<ReviewQueueItem['config_key']>;
} & {
entity_type?:
| RequireOnlyOne<Pick<QueryFilter<ReviewQueueItem['entity_type']>, '$eq' | '$in'>>
| PrimitiveFilter<ReviewQueueItem['entity_type']>;
} & {
created_at?:
| RequireOnlyOne<Pick<QueryFilter<ReviewQueueItem['created_at']>, '$eq' | '$gt' | '$lt' | '$gte' | '$lte'>>
| PrimitiveFilter<ReviewQueueItem['created_at']>;
} & {
id?:
| RequireOnlyOne<Pick<QueryFilter<ReviewQueueItem['id']>, '$eq' | '$in'>>
| PrimitiveFilter<ReviewQueueItem['id']>;
} & {
entity_id?:
| RequireOnlyOne<Pick<QueryFilter<ReviewQueueItem['entity_id']>, '$eq' | '$in'>>
| PrimitiveFilter<ReviewQueueItem['entity_id']>;
} & {
reviewed?: boolean;
} & {
reviewed_at?:
| RequireOnlyOne<Pick<QueryFilter<ReviewQueueItem['reviewed_at']>, '$eq' | '$gt' | '$lt' | '$gte' | '$lte'>>
| PrimitiveFilter<ReviewQueueItem['reviewed_at']>;
} & {
status?:
| RequireOnlyOne<Pick<QueryFilter<ReviewQueueItem['status']>, '$eq' | '$in'>>
| PrimitiveFilter<ReviewQueueItem['status']>;
} & {
updated_at?:
| RequireOnlyOne<Pick<QueryFilter<ReviewQueueItem['updated_at']>, '$eq' | '$gt' | '$lt' | '$gte' | '$lte'>>
| PrimitiveFilter<ReviewQueueItem['updated_at']>;
} & {
has_image?: boolean;
} & {
has_text?: boolean;
} & {
has_video?: boolean;
}
>;

export type ReviewQueueSort =
| Sort<Pick<ReviewQueueItem, 'id' | 'created_at' | 'updated_at'>>
| Array<Sort<Pick<ReviewQueueItem, 'id' | 'created_at' | 'updated_at'>>>;

export type ReviewQueuePaginationOptions = Pager;

export type ReviewQueueResponse = {
items: ReviewQueueItem[];
next?: string;
prev?: string;
};

export type Config = {};

export type GetConfigResponse = {
config: Config;
};

export type UpsertConfigResponse = {
config: Config;
};

0 comments on commit b1f47f0

Please sign in to comment.