diff --git a/packages/public-api/src/apis/reddit/models/ModNote.ts b/packages/public-api/src/apis/reddit/models/ModNote.ts index c0bd0916..4d2f1c32 100644 --- a/packages/public-api/src/apis/reddit/models/ModNote.ts +++ b/packages/public-api/src/apis/reddit/models/ModNote.ts @@ -14,7 +14,7 @@ import type { T1ID, T2ID, T3ID, T5ID } from '../../../types/tid.js'; import { asT2ID, asT5ID, asTID } from '../../../types/tid.js'; import type { ListingFetchOptions, ListingFetchResponse } from './Listing.js'; import { Listing } from './Listing.js'; -import type { ModAction } from './ModAction.js'; +import type { ModActionType } from './ModAction.js'; export type ModNoteType = | 'NOTE' @@ -28,6 +28,21 @@ export type ModNoteType = | 'MOD_ACTION' | 'ALL'; +/** + * Action metadata from a mod note's `modActionData`. Kept separate from + * {@link ModAction} to reduce field duplication — mod notes and moderation + * log entries are distinct use cases, and {@link ModNote} already provides + * moderator, subreddit, and timestamp context. + */ +export type ModNoteAction = { + action: ModActionType; + redditId?: T1ID | T2ID | T3ID | undefined; + /** For `banuser` actions, the number of days in a format like `"1 days"` */ + details?: string | undefined; + /** For `banuser` actions, the reasoning for the ban */ + description?: string | undefined; +}; + export type UserNoteLabel = | 'BOT_BAN' | 'PERMA_BAN' @@ -61,7 +76,7 @@ export interface ModNote { type: ModNoteType; createdAt: Date; userNote?: UserNote; - modAction?: ModAction; + modAction?: ModNoteAction; } export type GetModNotesOptions = Prettify< @@ -101,6 +116,8 @@ export class ModNote { assertNonNull(protoModNote.userNoteData, 'Mod note userNote is null or undefined'); assertNonNull(protoModNote.modActionData, 'Mod note modAction is null or undefined'); + const modActionData = protoModNote.modActionData; + return { id: protoModNote.id, user: { @@ -124,6 +141,16 @@ export class ModNote { label: protoModNote.userNoteData?.label as UserNoteLabel, }, type: protoModNote.type as ModNoteType, + modAction: modActionData?.action + ? { + action: modActionData.action as ModActionType, + redditId: modActionData.redditId + ? asTID(modActionData.redditId) + : undefined, + details: modActionData.details, + description: modActionData.description, + } + : undefined, }; } diff --git a/packages/public-api/src/apis/reddit/tests/modnote.api.test.ts b/packages/public-api/src/apis/reddit/tests/modnote.api.test.ts index d6666bd7..3e1673b5 100644 --- a/packages/public-api/src/apis/reddit/tests/modnote.api.test.ts +++ b/packages/public-api/src/apis/reddit/tests/modnote.api.test.ts @@ -13,6 +13,50 @@ describe('ModNote API', () => { }); describe('RedditAPIClient:ModNote', () => { + test('getModNotes() maps modActionData to modAction', async () => { + const spyPlugin = vi.spyOn(Devvit.redditAPIPlugins.ModNote, 'GetNotes'); + spyPlugin.mockImplementationOnce(async () => ({ + modNotes: [ + { + id: 'ModNote_test', + createdAt: 1_709_251_200, + type: 'MOD_ACTION', + subreddit: 'testsub', + subredditId: 't5_test', + user: 'test-user', + userId: 't2_user', + operator: 'test-mod', + operatorId: 't2_mod', + userNoteData: {}, + modActionData: { + action: 'banuser', + details: '14 day ban', + description: 'Second ban', + redditId: 't2_target', + }, + }, + ], + hasNextPage: false, + })); + + const notes = await api.reddit + .getModNotes({ + subreddit: 'testsub', + user: 'test-user', + filter: 'MOD_ACTION', + limit: 100, + }) + .all(); + + expect(notes).toHaveLength(1); + expect(notes[0]?.modAction).toEqual({ + action: 'banuser', + description: 'Second ban', + details: '14 day ban', + redditId: 't2_target', + }); + }); + test('addRemovalNote()', async () => { const spyPlugin = vi.spyOn(Devvit.redditAPIPlugins.ModNote, 'PostRemovalNote'); spyPlugin.mockImplementationOnce(async () => ({})); diff --git a/packages/reddit/src/models/ModNote.ts b/packages/reddit/src/models/ModNote.ts index 3a847213..9fd8e9ba 100644 --- a/packages/reddit/src/models/ModNote.ts +++ b/packages/reddit/src/models/ModNote.ts @@ -14,7 +14,7 @@ import { asTid, T1, T2, T3, T5 } from '@devvit/shared-types/tid.js'; import { getRedditApiPlugins } from '../plugin.js'; import type { ListingFetchOptions, ListingFetchResponse } from './Listing.js'; import { Listing } from './Listing.js'; -import type { ModAction } from './ModAction.js'; +import type { ModActionType } from './ModAction.js'; export type ModNoteType = | 'NOTE' @@ -28,6 +28,21 @@ export type ModNoteType = | 'MOD_ACTION' | 'ALL'; +/** + * Action metadata from a mod note's `modActionData`. Kept separate from + * {@link ModAction} to reduce field duplication — mod notes and moderation + * log entries are distinct use cases, and {@link ModNote} already provides + * moderator, subreddit, and timestamp context. + */ +export type ModNoteAction = { + action: ModActionType; + redditId?: T1 | T2 | T3 | undefined; + /** For `banuser` actions, the number of days in a format like `"1 days"` */ + details?: string | undefined; + /** For `banuser` actions, the reasoning for the ban */ + description?: string | undefined; +}; + export type UserNoteLabel = | 'BOT_BAN' | 'PERMA_BAN' @@ -60,8 +75,8 @@ export interface ModNote { }; type: ModNoteType; createdAt: Date; - userNote?: UserNote | undefined; - modAction?: ModAction; + userNote?: UserNote; + modAction?: ModNoteAction; } export type GetModNotesOptions = Prettify< @@ -101,6 +116,8 @@ export class ModNote { assertNonNull(protoModNote.userNoteData, 'Mod note userNote is null or undefined'); assertNonNull(protoModNote.modActionData, 'Mod note modAction is null or undefined'); + const modActionData = protoModNote.modActionData; + return { id: protoModNote.id, user: { @@ -124,6 +141,16 @@ export class ModNote { label: protoModNote.userNoteData?.label as UserNoteLabel, }, type: protoModNote.type as ModNoteType, + modAction: modActionData?.action + ? { + action: modActionData.action as ModActionType, + redditId: modActionData.redditId + ? asTid(modActionData.redditId) + : undefined, + details: modActionData.details, + description: modActionData.description, + } + : undefined, }; } diff --git a/packages/reddit/src/tests/modnote.api.test.ts b/packages/reddit/src/tests/modnote.api.test.ts index 095bb540..960e5f55 100644 --- a/packages/reddit/src/tests/modnote.api.test.ts +++ b/packages/reddit/src/tests/modnote.api.test.ts @@ -17,6 +17,52 @@ describe('ModNote API', () => { const redditAPI = new RedditClient(); describe('RedditClient:ModNote', () => { + test('getModNotes() maps modActionData to modAction', async () => { + const spyPlugin = redditApiPlugins.ModNote.GetNotes; + spyPlugin.mockImplementationOnce(async () => ({ + modNotes: [ + { + id: 'ModNote_test', + createdAt: 1_709_251_200, + type: 'MOD_ACTION', + subreddit: 'testsub', + subredditId: 't5_test', + user: 'test-user', + userId: 't2_user', + operator: 'test-mod', + operatorId: 't2_mod', + userNoteData: {}, + modActionData: { + action: 'banuser', + details: '14 day ban', + description: 'Second ban', + redditId: 't2_target', + }, + }, + ], + hasNextPage: false, + })); + + await runWithTestContext(async () => { + const notes = await redditAPI + .getModNotes({ + subreddit: 'testsub', + user: 'test-user', + filter: 'MOD_ACTION', + limit: 100, + }) + .all(); + + expect(notes).toHaveLength(1); + expect(notes[0]?.modAction).toEqual({ + action: 'banuser', + description: 'Second ban', + details: '14 day ban', + redditId: 't2_target', + }); + }); + }); + test('addRemovalNote()', async () => { const spyPlugin = redditApiPlugins.ModNote.PostRemovalNote; spyPlugin.mockImplementationOnce(async () => ({}));