Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions packages/public-api/src/apis/reddit/models/ModNote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my concern with adding a new type is that it will be a breaking change for existing usages. in light of this, what do you think about extending ModAction like:

export type ModNoteAction = ModAction & {
  action: ModActionType;
  /** The ID of the thing being moderated. */
  redditId?: T1ID | T2ID | T3ID | undefined;
}

or even just adding the fields as optional to ModAction?

* 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'
Expand Down Expand Up @@ -61,7 +76,7 @@ export interface ModNote {
type: ModNoteType;
createdAt: Date;
userNote?: UserNote;
modAction?: ModAction;
modAction?: ModNoteAction;
}

export type GetModNotesOptions = Prettify<
Expand Down Expand Up @@ -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: {
Expand All @@ -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<T1ID | T2ID | T3ID>(modActionData.redditId)
: undefined,
details: modActionData.details,
description: modActionData.description,
}
: undefined,
};
}

Expand Down
44 changes: 44 additions & 0 deletions packages/public-api/src/apis/reddit/tests/modnote.api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => ({}));
Expand Down
33 changes: 30 additions & 3 deletions packages/reddit/src/models/ModNote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -60,8 +75,8 @@ export interface ModNote {
};
type: ModNoteType;
createdAt: Date;
userNote?: UserNote | undefined;
modAction?: ModAction;
userNote?: UserNote;
modAction?: ModNoteAction;
}

export type GetModNotesOptions = Prettify<
Expand Down Expand Up @@ -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: {
Expand All @@ -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<T1 | T2 | T3>(modActionData.redditId)
: undefined,
details: modActionData.details,
description: modActionData.description,
}
: undefined,
};
}

Expand Down
46 changes: 46 additions & 0 deletions packages/reddit/src/tests/modnote.api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => ({}));
Expand Down