merge: Add more "detail" flags to API endpoints (!1186)

View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1186

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Marie <github@yuugi.dev>
This commit is contained in:
Hazelnoot 2025-07-27 14:22:11 +00:00
commit b6c8f4f876
16 changed files with 128 additions and 50 deletions

View file

@ -443,6 +443,8 @@ export class WebhookTestService {
return {
...user,
createdAt: this.idService.parse(user.id).date.toISOString(),
updatedAt: null,
lastFetchedAt: null,
id: user.id,
name: user.name,
username: user.username,

View file

@ -546,6 +546,8 @@ export class UserEntityService implements OnModuleInit {
avatarBlurhash: (user.avatarId == null ? null : user.avatarBlurhash),
description: mastoapi ? mastoapi.description : profile ? profile.description : '',
createdAt: this.idService.parse(user.id).date.toISOString(),
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
avatarDecorations: user.avatarDecorations.length > 0 ? this.avatarDecorationService.getAll().then(decorations => user.avatarDecorations.filter(ud => decorations.some(d => d.id === ud.id)).map(ud => ({
id: ud.id,
angle: ud.angle || undefined,
@ -601,8 +603,6 @@ export class UserEntityService implements OnModuleInit {
? Promise.all(user.alsoKnownAs.map(uri => Promise.resolve(opts.userIdsByUri?.get(uri) ?? this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null))))
.then(xs => xs.length === 0 ? null : xs.filter(x => x != null))
: null,
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
bannerUrl: user.bannerId == null ? null : user.bannerUrl,
bannerBlurhash: user.bannerId == null ? null : user.bannerBlurhash,
backgroundUrl: user.backgroundId == null ? null : user.backgroundUrl,

View file

@ -69,6 +69,16 @@ export const packedUserLiteSchema = {
nullable: false, optional: false,
format: 'date-time',
},
updatedAt: {
type: 'string',
nullable: true, optional: false,
format: 'date-time',
},
lastFetchedAt: {
type: 'string',
nullable: true, optional: false,
format: 'date-time',
},
approved: {
type: 'boolean',
nullable: false, optional: false,
@ -304,16 +314,6 @@ export const packedUserDetailedNotMeOnlySchema = {
nullable: false, optional: false,
},
},
updatedAt: {
type: 'string',
nullable: true, optional: false,
format: 'date-time',
},
lastFetchedAt: {
type: 'string',
nullable: true, optional: false,
format: 'date-time',
},
bannerUrl: {
type: 'string',
format: 'url',

View file

@ -35,7 +35,7 @@ export const meta = {
properties: {
id: { type: 'string', format: 'misskey:id' },
createdAt: { type: 'string', format: 'date-time' },
user: { ref: 'UserDetailed' },
user: { ref: 'User' },
expiresAt: { type: 'string', format: 'date-time', nullable: true },
},
required: ['id', 'createdAt', 'user'],
@ -50,6 +50,11 @@ export const paramDef = {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
detail: {
type: 'boolean',
nullable: false,
default: true,
},
},
required: ['roleId'],
} as const;
@ -90,12 +95,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.getMany();
const _users = assigns.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' })
const _userMap = await this.userEntityService.packMany(_users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' })
.then(users => new Map(users.map(u => [u.id, u])));
return await Promise.all(assigns.map(async assign => ({
id: assign.id,
createdAt: this.idService.parse(assign.id).date.toISOString(),
user: _userMap.get(assign.userId) ?? await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
user: _userMap.get(assign.userId) ?? await this.userEntityService.pack(assign.user!, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' }),
expiresAt: assign.expiresAt?.toISOString() ?? null,
})));
});

View file

@ -24,7 +24,7 @@ export const meta = {
items: {
type: 'object',
nullable: false, optional: false,
ref: 'UserDetailed',
ref: 'User',
},
},
} as const;
@ -44,6 +44,11 @@ export const paramDef = {
default: null,
description: 'The local host is represented with `null`.',
},
detail: {
type: 'boolean',
nullable: false,
default: true,
},
},
required: [],
} as const;
@ -115,7 +120,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const users = await query.getMany();
return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' });
return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });
});
}
}

View file

@ -23,7 +23,7 @@ export const meta = {
items: {
type: 'object',
optional: false, nullable: false,
ref: 'UserDetailed',
ref: 'User',
},
},
@ -43,6 +43,11 @@ export const paramDef = {
state: { type: 'string', enum: ['all', 'alive'], default: 'all' },
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' },
trending: { type: 'boolean', default: false },
detail: {
type: 'boolean',
nullable: false,
default: true,
},
},
required: ['tag', 'sort'],
} as const;
@ -96,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.map(([u]) => u);
}
return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' });
return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });
});
}
}

View file

@ -23,7 +23,7 @@ export const meta = {
items: {
type: 'object',
optional: false, nullable: false,
ref: 'UserDetailed',
ref: 'User',
},
},
@ -36,7 +36,13 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {},
properties: {
detail: {
type: 'boolean',
nullable: false,
default: true,
},
},
required: [],
} as const;
@ -57,7 +63,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
host: acct.host ?? IsNull(),
})));
return await this.userEntityService.packMany(users.filter(x => x != null), me, { schema: 'UserDetailed' });
return await this.userEntityService.packMany(users.filter(x => x != null), me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });
});
}
}

View file

@ -37,7 +37,7 @@ export const meta = {
},
user: {
type: 'object',
ref: 'UserDetailed',
ref: 'User',
},
},
required: ['id', 'user'],
@ -58,6 +58,11 @@ export const paramDef = {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
detail: {
type: 'boolean',
nullable: false,
default: true,
},
},
required: ['roleId'],
} as const;
@ -99,11 +104,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.getMany();
const _users = assigns.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' })
const _userMap = await this.userEntityService.packMany(_users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' })
.then(users => new Map(users.map(u => [u.id, u])));
return await Promise.all(assigns.map(async assign => ({
id: assign.id,
user: _userMap.get(assign.userId) ?? await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
user: _userMap.get(assign.userId) ?? await this.userEntityService.pack(assign.user!, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' }),
})));
});
}

View file

@ -24,7 +24,7 @@ export const meta = {
items: {
type: 'object',
optional: false, nullable: false,
ref: 'UserDetailed',
ref: 'User',
},
},
@ -50,6 +50,11 @@ export const paramDef = {
default: null,
description: 'The local host is represented with `null`.',
},
detail: {
type: 'boolean',
nullable: false,
default: true,
},
},
required: [],
} as const;
@ -111,7 +116,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.filter(([,p]) => p.canTrend)
.map(([u]) => u);
return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' });
return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });
});
}

View file

@ -30,7 +30,7 @@ export const meta = {
user: {
type: 'object',
optional: false, nullable: false,
ref: 'UserDetailed',
ref: 'User',
},
weight: {
type: 'number',
@ -60,6 +60,11 @@ export const paramDef = {
properties: {
userId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
detail: {
type: 'boolean',
nullable: false,
default: true,
},
},
required: ['userId'],
} as const;
@ -127,10 +132,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const topRepliedUserIds = repliedUsersSorted.slice(0, ps.limit);
// Make replies object (includes weights)
const _userMap = await this.userEntityService.packMany(topRepliedUserIds, me, { schema: 'UserDetailed' })
const _userMap = await this.userEntityService.packMany(topRepliedUserIds, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' })
.then(users => new Map(users.map(u => [u.id, u])));
const repliesObj = await Promise.all(topRepliedUserIds.map(async (userId) => ({
user: _userMap.get(userId) ?? await this.userEntityService.pack(userId, me, { schema: 'UserDetailed' }),
user: _userMap.get(userId) ?? await this.userEntityService.pack(userId, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' }),
weight: repliedUsers[userId] / peak,
})));

View file

@ -26,7 +26,7 @@ export const meta = {
items: {
type: 'object',
optional: false, nullable: false,
ref: 'UserDetailed',
ref: 'User',
},
},
@ -42,6 +42,11 @@ export const paramDef = {
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
offset: { type: 'integer', default: 0 },
detail: {
type: 'boolean',
nullable: false,
default: true,
},
},
required: [],
} as const;
@ -83,7 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const users = await query.limit(ps.limit).offset(ps.offset).getMany();
return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' });
return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });
});
}
}

View file

@ -30,13 +30,13 @@ export const meta = {
oneOf: [
{
type: 'object',
ref: 'UserDetailed',
ref: 'User',
},
{
type: 'array',
items: {
type: 'object',
ref: 'UserDetailed',
ref: 'User',
},
},
],
@ -79,6 +79,11 @@ export const paramDef = {
nullable: true,
description: 'The local host is represented with `null`.',
},
detail: {
type: 'boolean',
nullable: false,
default: true,
},
},
anyOf: [
{ required: ['userId'] },
@ -125,7 +130,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (user != null) _users.push(user);
}
const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' })
const _userMap = await this.userEntityService.packMany(_users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' })
.then(users => new Map(users.map(u => [u.id, u])));
return _users.map(u => _userMap.get(u.id)!);
} else {
@ -156,7 +161,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
return await this.userEntityService.pack(user, me, {
schema: 'UserDetailed',
schema: ps.detail ? 'UserDetailed' : 'UserLite',
});
}
});

View file

@ -2067,6 +2067,7 @@ declare namespace entities {
PagesUnlikeRequest,
PagesUpdateRequest,
PingResponse,
PinnedUsersRequest,
PinnedUsersResponse,
PromoReadRequest,
RenoteMuteCreateRequest,
@ -3411,6 +3412,9 @@ export const permissions: readonly ["read:account", "write:account", "read:block
// @public (undocumented)
type PingResponse = operations['ping']['responses']['200']['content']['application/json'];
// @public (undocumented)
type PinnedUsersRequest = operations['pinned-users']['requestBody']['content']['application/json'];
// @public (undocumented)
type PinnedUsersResponse = operations['pinned-users']['responses']['200']['content']['application/json'];

View file

@ -563,6 +563,7 @@ import type {
PagesUnlikeRequest,
PagesUpdateRequest,
PingResponse,
PinnedUsersRequest,
PinnedUsersResponse,
PromoReadRequest,
RenoteMuteCreateRequest,
@ -1044,7 +1045,7 @@ export type Endpoints = {
'pages/unlike': { req: PagesUnlikeRequest; res: EmptyResponse };
'pages/update': { req: PagesUpdateRequest; res: EmptyResponse };
'ping': { req: EmptyRequest; res: PingResponse };
'pinned-users': { req: EmptyRequest; res: PinnedUsersResponse };
'pinned-users': { req: PinnedUsersRequest; res: PinnedUsersResponse };
'promo/read': { req: PromoReadRequest; res: EmptyResponse };
'renote-mute/create': { req: RenoteMuteCreateRequest; res: EmptyResponse };
'renote-mute/delete': { req: RenoteMuteDeleteRequest; res: EmptyResponse };

View file

@ -566,6 +566,7 @@ export type PagesShowResponse = operations['pages___show']['responses']['200']['
export type PagesUnlikeRequest = operations['pages___unlike']['requestBody']['content']['application/json'];
export type PagesUpdateRequest = operations['pages___update']['requestBody']['content']['application/json'];
export type PingResponse = operations['ping']['responses']['200']['content']['application/json'];
export type PinnedUsersRequest = operations['pinned-users']['requestBody']['content']['application/json'];
export type PinnedUsersResponse = operations['pinned-users']['responses']['200']['content']['application/json'];
export type PromoReadRequest = operations['promo___read']['requestBody']['content']['application/json'];
export type RenoteMuteCreateRequest = operations['renote-mute___create']['requestBody']['content']['application/json'];

View file

@ -4254,6 +4254,10 @@ export type components = {
host: string | null;
/** Format: date-time */
createdAt: string;
/** Format: date-time */
updatedAt: string | null;
/** Format: date-time */
lastFetchedAt: string | null;
approved: boolean;
/** @example Hi masters, I am Ai! */
description: string | null;
@ -4320,10 +4324,6 @@ export type components = {
/** Format: uri */
movedTo: string | null;
alsoKnownAs: string[] | null;
/** Format: date-time */
updatedAt: string | null;
/** Format: date-time */
lastFetchedAt: string | null;
/** Format: url */
bannerUrl: string | null;
bannerBlurhash: string | null;
@ -10902,6 +10902,8 @@ export type operations = {
untilId?: string;
/** @default 10 */
limit?: number;
/** @default true */
detail?: boolean;
};
};
};
@ -10914,7 +10916,7 @@ export type operations = {
id: string;
/** Format: date-time */
createdAt: string;
user: components['schemas']['UserDetailed'];
user: components['schemas']['User'];
/** Format: date-time */
expiresAt: string | null;
})[];
@ -11410,6 +11412,8 @@ export type operations = {
* @default null
*/
hostname?: string | null;
/** @default true */
detail?: boolean;
};
};
};
@ -11417,7 +11421,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
'application/json': components['schemas']['UserDetailed'][];
'application/json': components['schemas']['User'][];
};
};
/** @description Client error */
@ -21857,6 +21861,8 @@ export type operations = {
origin?: 'combined' | 'local' | 'remote';
/** @default false */
trending?: boolean;
/** @default true */
detail?: boolean;
};
};
};
@ -21864,7 +21870,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
'application/json': components['schemas']['UserDetailed'][];
'application/json': components['schemas']['User'][];
};
};
/** @description Client error */
@ -29834,11 +29840,19 @@ export type operations = {
* **Credential required**: *No*
*/
'pinned-users': {
requestBody: {
content: {
'application/json': {
/** @default true */
detail?: boolean;
};
};
};
responses: {
/** @description OK (with results) */
200: {
content: {
'application/json': components['schemas']['UserDetailed'][];
'application/json': components['schemas']['User'][];
};
};
/** @description Client error */
@ -30966,6 +30980,8 @@ export type operations = {
untilId?: string;
/** @default 10 */
limit?: number;
/** @default true */
detail?: boolean;
};
};
};
@ -30976,7 +30992,7 @@ export type operations = {
'application/json': {
/** Format: misskey:id */
id: string;
user: components['schemas']['UserDetailed'];
user: components['schemas']['User'];
}[];
};
};
@ -31639,6 +31655,8 @@ export type operations = {
* @default null
*/
hostname?: string | null;
/** @default true */
detail?: boolean;
};
};
};
@ -31646,7 +31664,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
'application/json': components['schemas']['UserDetailed'][];
'application/json': components['schemas']['User'][];
};
};
/** @description Client error */
@ -32162,6 +32180,8 @@ export type operations = {
userId: string;
/** @default 10 */
limit?: number;
/** @default true */
detail?: boolean;
};
};
};
@ -32170,7 +32190,7 @@ export type operations = {
200: {
content: {
'application/json': {
user: components['schemas']['UserDetailed'];
user: components['schemas']['User'];
weight: number;
}[];
};
@ -33183,6 +33203,8 @@ export type operations = {
limit?: number;
/** @default 0 */
offset?: number;
/** @default true */
detail?: boolean;
};
};
};
@ -33190,7 +33212,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
'application/json': components['schemas']['UserDetailed'][];
'application/json': components['schemas']['User'][];
};
};
/** @description Client error */
@ -33525,6 +33547,8 @@ export type operations = {
username?: string;
/** @description The local host is represented with `null`. */
host?: string | null;
/** @default true */
detail?: boolean;
};
};
};
@ -33532,7 +33556,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
'application/json': components['schemas']['UserDetailed'] | components['schemas']['UserDetailed'][];
'application/json': components['schemas']['User'] | components['schemas']['User'][];
};
};
/** @description Client error */