enhance(frontend): 「今日誕生日のフォロー中ユーザー」ウィジェットをリファクタリング
This commit is contained in:
parent
9723d01277
commit
24652b9364
|
@ -2153,6 +2153,7 @@ _widgets:
|
||||||
chooseList: "Select a list"
|
chooseList: "Select a list"
|
||||||
clicker: "Clicker"
|
clicker: "Clicker"
|
||||||
birthdayFollowings: "Users who celebrate their birthday today"
|
birthdayFollowings: "Users who celebrate their birthday today"
|
||||||
|
birthdaySoon: "Users who will celebrate their birthday soon"
|
||||||
_cw:
|
_cw:
|
||||||
hide: "Hide"
|
hide: "Hide"
|
||||||
show: "Show content"
|
show: "Show content"
|
||||||
|
|
4
locales/index.d.ts
vendored
4
locales/index.d.ts
vendored
|
@ -8396,6 +8396,10 @@ export interface Locale extends ILocale {
|
||||||
* 今日誕生日のユーザー
|
* 今日誕生日のユーザー
|
||||||
*/
|
*/
|
||||||
"birthdayFollowings": string;
|
"birthdayFollowings": string;
|
||||||
|
/**
|
||||||
|
* もうすぐ誕生日のユーザー
|
||||||
|
*/
|
||||||
|
"birthdaySoon": string;
|
||||||
};
|
};
|
||||||
"_cw": {
|
"_cw": {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2207,6 +2207,7 @@ _widgets:
|
||||||
chooseList: "リストを選択"
|
chooseList: "リストを選択"
|
||||||
clicker: "クリッカー"
|
clicker: "クリッカー"
|
||||||
birthdayFollowings: "今日誕生日のユーザー"
|
birthdayFollowings: "今日誕生日のユーザー"
|
||||||
|
birthdaySoon: "もうすぐ誕生日のユーザー"
|
||||||
|
|
||||||
_cw:
|
_cw:
|
||||||
hide: "隠す"
|
hide: "隠す"
|
||||||
|
|
|
@ -2131,6 +2131,7 @@ _widgets:
|
||||||
chooseList: "리스트 선택"
|
chooseList: "리스트 선택"
|
||||||
clicker: "클리커"
|
clicker: "클리커"
|
||||||
birthdayFollowings: "오늘이 생일인 사용자"
|
birthdayFollowings: "오늘이 생일인 사용자"
|
||||||
|
birthdaySoon: "곧 생일인 사용자"
|
||||||
_cw:
|
_cw:
|
||||||
hide: "숨기기"
|
hide: "숨기기"
|
||||||
show: "더 보기"
|
show: "더 보기"
|
||||||
|
|
15
packages/backend/migration/1711478468155-birthday-index.js
Normal file
15
packages/backend/migration/1711478468155-birthday-index.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
export class BirthdayIndex1711478468155 {
|
||||||
|
name = 'BirthdayIndex1711478468155'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_de22cd2b445eee31ae51cdbe99"`);
|
||||||
|
await queryRunner.query(`CREATE OR REPLACE FUNCTION get_birthday_date(birthday TEXT) RETURNS SMALLINT AS $$ BEGIN RETURN CAST((SUBSTR(birthday, 6, 2) || SUBSTR(birthday, 9, 2)) AS SMALLINT); END; $$ LANGUAGE plpgsql IMMUTABLE;`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_USERPROFILE_BIRTHDAY_DATE" ON "user_profile" (get_birthday_date("birthday"))`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_de22cd2b445eee31ae51cdbe99" ON "user_profile" (substr("birthday", 6, 5))`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_USERPROFILE_BIRTHDAY_DATE"`);
|
||||||
|
await queryRunner.query(`DROP FUNCTION IF EXISTS get_birthday_date(birthday TEXT)`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -642,8 +642,8 @@ export class UserEntityService implements OnModuleInit {
|
||||||
|
|
||||||
// -- 特に前提条件のない値群を取得
|
// -- 特に前提条件のない値群を取得
|
||||||
|
|
||||||
const profilesMap = await this.userProfilesRepository.findBy({ userId: In(_userIds) })
|
const profilesMap = (options?.schema !== 'UserLite') ? await this.userProfilesRepository.findBy({ userId: In(_userIds) })
|
||||||
.then(profiles => new Map(profiles.map(p => [p.userId, p])));
|
.then(profiles => new Map(profiles.map(p => [p.userId, p]))) : undefined;
|
||||||
|
|
||||||
// -- 実行者の有無や指定スキーマの種別によって要否が異なる値群を取得
|
// -- 実行者の有無や指定スキーマの種別によって要否が異なる値群を取得
|
||||||
|
|
||||||
|
@ -680,7 +680,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (await Promise.allSettled(_users.map(u => this.pack(u, me, { ...options, userProfile: profilesMap.get(u.id), userRelations: userRelations, userMemos: userMemos, pinNotes: pinNotes }))))
|
return (await Promise.allSettled(_users.map(u => this.pack(u, me, { ...options, userProfile: profilesMap?.get(u.id), userRelations: userRelations, userMemos: userMemos, pinNotes: pinNotes }))))
|
||||||
.filter(result => result.status === 'fulfilled')
|
.filter(result => result.status === 'fulfilled')
|
||||||
.map(result => (result as PromiseFulfilledResult<Packed<S>>).value);
|
.map(result => (result as PromiseFulfilledResult<Packed<S>>).value);
|
||||||
}
|
}
|
||||||
|
|
|
@ -348,6 +348,7 @@ import * as ep___users_clips from './endpoints/users/clips.js';
|
||||||
import * as ep___users_followers from './endpoints/users/followers.js';
|
import * as ep___users_followers from './endpoints/users/followers.js';
|
||||||
import * as ep___users_following from './endpoints/users/following.js';
|
import * as ep___users_following from './endpoints/users/following.js';
|
||||||
import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js';
|
import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js';
|
||||||
|
import * as ep___users_getFollowingBirthdayUsers from './endpoints/users/get-following-birthday-users.js';
|
||||||
import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js';
|
import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js';
|
||||||
import * as ep___users_featuredNotes from './endpoints/users/featured-notes.js';
|
import * as ep___users_featuredNotes from './endpoints/users/featured-notes.js';
|
||||||
import * as ep___users_lists_create from './endpoints/users/lists/create.js';
|
import * as ep___users_lists_create from './endpoints/users/lists/create.js';
|
||||||
|
@ -733,6 +734,7 @@ const $users_clips: Provider = { provide: 'ep:users/clips', useClass: ep___users
|
||||||
const $users_followers: Provider = { provide: 'ep:users/followers', useClass: ep___users_followers.default };
|
const $users_followers: Provider = { provide: 'ep:users/followers', useClass: ep___users_followers.default };
|
||||||
const $users_following: Provider = { provide: 'ep:users/following', useClass: ep___users_following.default };
|
const $users_following: Provider = { provide: 'ep:users/following', useClass: ep___users_following.default };
|
||||||
const $users_gallery_posts: Provider = { provide: 'ep:users/gallery/posts', useClass: ep___users_gallery_posts.default };
|
const $users_gallery_posts: Provider = { provide: 'ep:users/gallery/posts', useClass: ep___users_gallery_posts.default };
|
||||||
|
const $users_getFollowingBirthdayUsers: Provider = { provide: 'ep:users/get-following-birthday-users', useClass: ep___users_getFollowingBirthdayUsers.default };
|
||||||
const $users_getFrequentlyRepliedUsers: Provider = { provide: 'ep:users/get-frequently-replied-users', useClass: ep___users_getFrequentlyRepliedUsers.default };
|
const $users_getFrequentlyRepliedUsers: Provider = { provide: 'ep:users/get-frequently-replied-users', useClass: ep___users_getFrequentlyRepliedUsers.default };
|
||||||
const $users_featuredNotes: Provider = { provide: 'ep:users/featured-notes', useClass: ep___users_featuredNotes.default };
|
const $users_featuredNotes: Provider = { provide: 'ep:users/featured-notes', useClass: ep___users_featuredNotes.default };
|
||||||
const $users_lists_create: Provider = { provide: 'ep:users/lists/create', useClass: ep___users_lists_create.default };
|
const $users_lists_create: Provider = { provide: 'ep:users/lists/create', useClass: ep___users_lists_create.default };
|
||||||
|
@ -1122,6 +1124,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||||
$users_followers,
|
$users_followers,
|
||||||
$users_following,
|
$users_following,
|
||||||
$users_gallery_posts,
|
$users_gallery_posts,
|
||||||
|
$users_getFollowingBirthdayUsers,
|
||||||
$users_getFrequentlyRepliedUsers,
|
$users_getFrequentlyRepliedUsers,
|
||||||
$users_featuredNotes,
|
$users_featuredNotes,
|
||||||
$users_lists_create,
|
$users_lists_create,
|
||||||
|
@ -1503,6 +1506,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||||
$users_followers,
|
$users_followers,
|
||||||
$users_following,
|
$users_following,
|
||||||
$users_gallery_posts,
|
$users_gallery_posts,
|
||||||
|
$users_getFollowingBirthdayUsers,
|
||||||
$users_getFrequentlyRepliedUsers,
|
$users_getFrequentlyRepliedUsers,
|
||||||
$users_featuredNotes,
|
$users_featuredNotes,
|
||||||
$users_lists_create,
|
$users_lists_create,
|
||||||
|
|
|
@ -348,6 +348,7 @@ import * as ep___users_clips from './endpoints/users/clips.js';
|
||||||
import * as ep___users_followers from './endpoints/users/followers.js';
|
import * as ep___users_followers from './endpoints/users/followers.js';
|
||||||
import * as ep___users_following from './endpoints/users/following.js';
|
import * as ep___users_following from './endpoints/users/following.js';
|
||||||
import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js';
|
import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js';
|
||||||
|
import * as ep___users_getFollowingBirthdayUsers from './endpoints/users/get-following-birthday-users.js';
|
||||||
import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js';
|
import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js';
|
||||||
import * as ep___users_featuredNotes from './endpoints/users/featured-notes.js';
|
import * as ep___users_featuredNotes from './endpoints/users/featured-notes.js';
|
||||||
import * as ep___users_lists_create from './endpoints/users/lists/create.js';
|
import * as ep___users_lists_create from './endpoints/users/lists/create.js';
|
||||||
|
@ -731,6 +732,7 @@ const eps = [
|
||||||
['users/followers', ep___users_followers],
|
['users/followers', ep___users_followers],
|
||||||
['users/following', ep___users_following],
|
['users/following', ep___users_following],
|
||||||
['users/gallery/posts', ep___users_gallery_posts],
|
['users/gallery/posts', ep___users_gallery_posts],
|
||||||
|
['users/get-following-birthday-users', ep___users_getFollowingBirthdayUsers],
|
||||||
['users/get-frequently-replied-users', ep___users_getFrequentlyRepliedUsers],
|
['users/get-frequently-replied-users', ep___users_getFrequentlyRepliedUsers],
|
||||||
['users/featured-notes', ep___users_featuredNotes],
|
['users/featured-notes', ep___users_featuredNotes],
|
||||||
['users/lists/create', ep___users_lists_create],
|
['users/lists/create', ep___users_lists_create],
|
||||||
|
|
|
@ -67,7 +67,10 @@ export const paramDef = {
|
||||||
description: 'The local host is represented with `null`.',
|
description: 'The local host is represented with `null`.',
|
||||||
},
|
},
|
||||||
|
|
||||||
birthday: { ...birthdaySchema, nullable: true },
|
birthday: {
|
||||||
|
...birthdaySchema, nullable: true,
|
||||||
|
description: '@deprecated use get-following-birthday-users instead.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
anyOf: [
|
anyOf: [
|
||||||
{ required: ['userId'] },
|
{ required: ['userId'] },
|
||||||
|
@ -126,14 +129,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
.andWhere('following.followerId = :userId', { userId: user.id })
|
.andWhere('following.followerId = :userId', { userId: user.id })
|
||||||
.innerJoinAndSelect('following.followee', 'followee');
|
.innerJoinAndSelect('following.followee', 'followee');
|
||||||
|
|
||||||
|
// @deprecated use get-following-birthday-users instead.
|
||||||
if (ps.birthday) {
|
if (ps.birthday) {
|
||||||
try {
|
query.innerJoin(this.userProfilesRepository.metadata.targetName, 'followeeProfile', 'followeeProfile.userId = following.followeeId');
|
||||||
const birthday = ps.birthday.substring(5, 10);
|
|
||||||
const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile');
|
|
||||||
birthdayUserQuery.select('user_profile.userId')
|
|
||||||
.where(`SUBSTR(user_profile.birthday, 6, 5) = '${birthday}'`);
|
|
||||||
|
|
||||||
query.andWhere(`following.followeeId IN (${ birthdayUserQuery.getQuery() })`);
|
try {
|
||||||
|
const birthday = ps.birthday.split('-');
|
||||||
|
birthday.shift(); // 年の部分を削除
|
||||||
|
// なぜか get_birthday_date() = :birthday だとインデックスが効かないので、BETWEEN で対応
|
||||||
|
query.andWhere('get_birthday_date(followeeProfile.birthday) BETWEEN :birthday AND :birthday', { birthday: parseInt(birthday.join('')) });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new ApiError(meta.errors.birthdayInvalid);
|
throw new ApiError(meta.errors.birthdayInvalid);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type {
|
||||||
|
FollowingsRepository,
|
||||||
|
UserProfilesRepository,
|
||||||
|
} from '@/models/_.js';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['users'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
kind: 'read:account',
|
||||||
|
|
||||||
|
description: 'Find users who have a birthday on the specified range.',
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
properties: {
|
||||||
|
birthday: {
|
||||||
|
type: 'string', format: 'date-time',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
ref: 'UserLite',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
|
offset: { type: 'integer', default: 0 },
|
||||||
|
birthday: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
month: { type: 'integer', minimum: 1, maximum: 12 },
|
||||||
|
day: { type: 'integer', minimum: 1, maximum: 31 },
|
||||||
|
begin: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
month: { type: 'integer', minimum: 1, maximum: 12 },
|
||||||
|
day: { type: 'integer', minimum: 1, maximum: 31 },
|
||||||
|
},
|
||||||
|
required: ['month', 'day'],
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
month: { type: 'integer', minimum: 1, maximum: 12 },
|
||||||
|
day: { type: 'integer', minimum: 1, maximum: 31 },
|
||||||
|
},
|
||||||
|
required: ['month', 'day'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
anyOf: [
|
||||||
|
{ required: ['month', 'day'] },
|
||||||
|
{ required: ['begin', 'end'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['birthday'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.userProfilesRepository)
|
||||||
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
@Inject(DI.followingsRepository)
|
||||||
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
|
private userEntityService: UserEntityService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const query = this.followingsRepository
|
||||||
|
.createQueryBuilder('following')
|
||||||
|
.andWhere('following.followerId = :userId', { userId: me.id })
|
||||||
|
.innerJoin(this.userProfilesRepository.metadata.targetName, 'followeeProfile', 'followeeProfile.userId = following.followeeId');
|
||||||
|
|
||||||
|
if (Object.hasOwn(ps.birthday, 'begin') && Object.hasOwn(ps.birthday, 'end')) {
|
||||||
|
const { begin, end } = ps.birthday as { begin: { month: number; day: number }; end: { month: number; day: number }; };
|
||||||
|
query.andWhere('get_birthday_date(followeeProfile.birthday) BETWEEN :begin AND :end', { begin: begin.month * 100 + begin.day, end: end.month * 100 + end.day });
|
||||||
|
} else {
|
||||||
|
const { month, day } = ps.birthday as { month: number; day: number };
|
||||||
|
// なぜか get_birthday_date() = :birthday だとインデックスが効かないので、BETWEEN で対応
|
||||||
|
query.andWhere('get_birthday_date(followeeProfile.birthday) BETWEEN :birthday AND :birthday', { birthday: month * 100 + day });
|
||||||
|
}
|
||||||
|
|
||||||
|
query.select('following.followeeId', 'user_id');
|
||||||
|
query.addSelect('get_birthday_date(followeeProfile.birthday)', 'birthday_date');
|
||||||
|
query.orderBy('birthday_date', 'ASC');
|
||||||
|
|
||||||
|
const birthdayUsers = await query
|
||||||
|
.offset(ps.offset).limit(ps.limit)
|
||||||
|
.getRawMany<{ birthday_date: number; user_id: string }>();
|
||||||
|
|
||||||
|
const users = new Map<string, Packed<'UserLite'>>((
|
||||||
|
await this.userEntityService.packMany(
|
||||||
|
birthdayUsers.map(u => u.user_id),
|
||||||
|
me,
|
||||||
|
{ schema: 'UserLite' },
|
||||||
|
)
|
||||||
|
).map(u => [u.id, u]));
|
||||||
|
|
||||||
|
return birthdayUsers
|
||||||
|
.map(item => {
|
||||||
|
const birthday = new Date();
|
||||||
|
birthday.setMonth(Math.floor(item.birthday_date / 100) - 1);
|
||||||
|
birthday.setDate(item.birthday_date % 100);
|
||||||
|
birthday.setHours(0, 0, 0, 0);
|
||||||
|
if (birthday.getTime() < Date.now()) birthday.setFullYear(new Date().getFullYear() + 1);
|
||||||
|
return { birthday: birthday.toISOString(), user: users.get(item.user_id) };
|
||||||
|
})
|
||||||
|
.filter(item => item.user !== undefined)
|
||||||
|
.map(item => item as { birthday: string; user: Packed<'UserLite'> });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,10 +27,21 @@ export const meta = {
|
||||||
res: {
|
res: {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
oneOf: [
|
oneOf: [
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
ref: 'UserLite',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'object',
|
type: 'object',
|
||||||
ref: 'UserDetailed',
|
ref: 'UserDetailed',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'UserLite',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'array',
|
type: 'array',
|
||||||
items: {
|
items: {
|
||||||
|
@ -71,6 +82,7 @@ export const paramDef = {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
description: 'The local host is represented with `null`.',
|
description: 'The local host is represented with `null`.',
|
||||||
},
|
},
|
||||||
|
detailed: { type: 'boolean', default: true },
|
||||||
},
|
},
|
||||||
anyOf: [
|
anyOf: [
|
||||||
{ required: ['userId'] },
|
{ required: ['userId'] },
|
||||||
|
@ -117,7 +129,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.userEntityService.packMany(_users, me, {
|
return await this.userEntityService.packMany(_users, me, {
|
||||||
schema: 'UserDetailed',
|
schema: ps.detailed ? 'UserDetailed' : 'UserLite',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Lookup user
|
// Lookup user
|
||||||
|
@ -147,7 +159,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.userEntityService.pack(user, me, {
|
return await this.userEntityService.pack(user, me, {
|
||||||
schema: 'UserDetailed',
|
schema: ps.detailed ? 'UserDetailed' : 'UserLite',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -29,6 +29,7 @@ const users = ref<Misskey.entities.UserLite[]>([]);
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
users.value = await misskeyApi('users/show', {
|
users.value = await misskeyApi('users/show', {
|
||||||
userIds: props.userIds,
|
userIds: props.userIds,
|
||||||
|
detailed: false,
|
||||||
}) as unknown as Misskey.entities.UserLite[];
|
}) as unknown as Misskey.entities.UserLite[];
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -4,43 +4,76 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkContainer :showHeader="widgetProps.showHeader" class="mkw-bdayfollowings">
|
<MkContainer :style="`height: ${widgetProps.height}px;`" :showHeader="widgetProps.showHeader" :scrollable="true" class="mkw-bdayfollowings">
|
||||||
<template #icon><i class="ti ti-cake"></i></template>
|
<template #icon><i class="ti ti-cake"></i></template>
|
||||||
<template #header>{{ i18n.ts._widgets.birthdayFollowings }}</template>
|
<template v-if="widgetProps.period === 'today'" #header>{{ i18n.ts._widgets.birthdayFollowings }}</template>
|
||||||
<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="actualFetch()"><i class="ti ti-refresh"></i></button></template>
|
<template v-else #header>{{ i18n.ts._widgets.birthdaySoon }}</template>
|
||||||
|
<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="fetch(true)"><i class="ti ti-refresh"></i></button></template>
|
||||||
|
|
||||||
<div :class="$style.bdayFRoot">
|
<MkPagination ref="paginationEl" :pagination="birthdayUsersPagination">
|
||||||
<MkLoading v-if="fetching"/>
|
<template #empty>
|
||||||
<div v-else-if="users.length > 0" :class="$style.bdayFGrid">
|
<div :class="$style.empty" :style="`height: ${widgetProps.showHeader ? widgetProps.height - 38 : widgetProps.height}px;`">
|
||||||
<MkAvatar v-for="user in users" :key="user.id" :user="user.followee" link preview></MkAvatar>
|
<img :src="infoImageUrl" class="_ghost"/>
|
||||||
</div>
|
<div>{{ i18n.ts.nothing }}</div>
|
||||||
<div v-else :class="$style.bdayFFallback">
|
</div>
|
||||||
<img :src="infoImageUrl" class="_ghost" :class="$style.bdayFFallbackImage"/>
|
</template>
|
||||||
<div>{{ i18n.ts.nothing }}</div>
|
|
||||||
</div>
|
<template #default="{ items: users }">
|
||||||
</div>
|
<MkDateSeparatedList v-slot="{ item }" :items="toMisskeyEntity(users)" :noGap="true">
|
||||||
|
<div v-if="item.user" :key="item.id" style="display: flex; gap: 8px; padding-right: 16px">
|
||||||
|
<MkA :to="userPage(item.user)" style="flex-grow: 1;">
|
||||||
|
<MkUserCardMini :user="item.user" :withChart="false" style="background: inherit; border-radius: unset;"/>
|
||||||
|
</MkA>
|
||||||
|
<button v-tooltip.noDelay="i18n.ts.note" class="_button" :class="$style.post" @click="os.post({initialText: `@${item.user.username}${item.user.host ? `@${item.user.host}` : ''} `})">
|
||||||
|
<i class="ti-fw ti ti-confetti" :class="$style.postIcon"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</MkDateSeparatedList>
|
||||||
|
</template>
|
||||||
|
</MkPagination>
|
||||||
</MkContainer>
|
</MkContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import type { MisskeyEntity } from '@/types/date-separated-list.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
|
||||||
import { useInterval } from '@/scripts/use-interval.js';
|
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { userPage } from '@/filters/user.js';
|
||||||
import { infoImageUrl } from '@/instance.js';
|
import { infoImageUrl } from '@/instance.js';
|
||||||
import { $i } from '@/account.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
|
import { useInterval } from '@/scripts/use-interval.js';
|
||||||
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
|
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
|
||||||
|
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||||
|
|
||||||
const name = i18n.ts._widgets.birthdayFollowings;
|
const name = i18n.ts._widgets.birthdaySoon;
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
showHeader: {
|
showHeader: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean' as const,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
height: {
|
||||||
|
type: 'number' as const,
|
||||||
|
default: 300,
|
||||||
|
},
|
||||||
|
period: {
|
||||||
|
type: 'radio' as const,
|
||||||
|
default: 'today',
|
||||||
|
options: [{
|
||||||
|
value: 'today', label: i18n.ts.today,
|
||||||
|
}, {
|
||||||
|
value: '3day', label: i18n.tsx.dayX({ day: 3 }),
|
||||||
|
}, {
|
||||||
|
value: 'week', label: i18n.ts.oneWeek,
|
||||||
|
}, {
|
||||||
|
value: 'month', label: i18n.ts.oneMonth,
|
||||||
|
}],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
@ -48,56 +81,73 @@ type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
const props = defineProps<WidgetComponentProps<WidgetProps>>();
|
const props = defineProps<WidgetComponentProps<WidgetProps>>();
|
||||||
const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
|
const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
|
||||||
|
|
||||||
const { widgetProps, configure } = useWidgetPropsManager(name,
|
const { widgetProps, configure } = useWidgetPropsManager(
|
||||||
|
name,
|
||||||
widgetPropsDef,
|
widgetPropsDef,
|
||||||
props,
|
props,
|
||||||
emit,
|
emit,
|
||||||
);
|
);
|
||||||
|
|
||||||
const users = ref<Misskey.Endpoints['users/following']['res']>([]);
|
const begin = ref<Date>(new Date());
|
||||||
const fetching = ref(true);
|
const end = computed(() => {
|
||||||
let lastFetchedAt = '1970-01-01';
|
switch (widgetProps.period) {
|
||||||
|
case '3day':
|
||||||
const fetch = () => {
|
return new Date(begin.value.getTime() + 1000 * 60 * 60 * 24 * 3);
|
||||||
if (!$i) {
|
case 'week':
|
||||||
users.value = [];
|
return new Date(begin.value.getTime() + 1000 * 60 * 60 * 24 * 7);
|
||||||
fetching.value = false;
|
case 'month':
|
||||||
return;
|
return new Date(begin.value.getTime() + 1000 * 60 * 60 * 24 * 30);
|
||||||
|
default:
|
||||||
|
return begin.value;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const lfAtD = new Date(lastFetchedAt);
|
const paginationEl = ref<InstanceType<typeof MkPagination>>();
|
||||||
lfAtD.setHours(0, 0, 0, 0);
|
const birthdayUsersPagination = {
|
||||||
const now = new Date();
|
endpoint: 'users/get-following-birthday-users' as const,
|
||||||
now.setHours(0, 0, 0, 0);
|
limit: 18,
|
||||||
|
offsetMode: true,
|
||||||
if (now > lfAtD) {
|
params: computed(() => {
|
||||||
actualFetch();
|
if (widgetProps.period === 'today') {
|
||||||
|
return {
|
||||||
lastFetchedAt = now.toISOString();
|
birthday: {
|
||||||
}
|
month: begin.value.getMonth() + 1,
|
||||||
|
day: begin.value.getDate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
birthday: {
|
||||||
|
begin: {
|
||||||
|
month: begin.value.getMonth() + 1,
|
||||||
|
day: begin.value.getDate(),
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
month: end.value.getMonth() + 1,
|
||||||
|
day: end.value.getDate(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
function actualFetch() {
|
function fetch(force = false) {
|
||||||
if ($i == null) {
|
|
||||||
users.value = [];
|
|
||||||
fetching.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
now.setHours(0, 0, 0, 0);
|
if (force || now.getDate() !== begin.value.getDate()) {
|
||||||
fetching.value = true;
|
// computed() で再評価されるので、paginationEl.value!.reload() は不要
|
||||||
misskeyApi('users/following', {
|
begin.value = now;
|
||||||
limit: 18,
|
}
|
||||||
birthday: `${now.getFullYear().toString().padStart(4, '0')}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}`,
|
}
|
||||||
userId: $i.id,
|
|
||||||
}).then(res => {
|
function toMisskeyEntity(items): MisskeyEntity[] {
|
||||||
users.value = res;
|
const r = items.map((item: { userId: string, birthday: string, user: Misskey.entities.UserLite }) => ({
|
||||||
window.setTimeout(() => {
|
id: item.user.id,
|
||||||
// 早すぎるとチカチカする
|
createdAt: item.birthday,
|
||||||
fetching.value = false;
|
user: item.user,
|
||||||
}, 100);
|
}));
|
||||||
});
|
|
||||||
|
return [{ id: '_', createdAt: begin.value.toISOString() }, ...r];
|
||||||
}
|
}
|
||||||
|
|
||||||
useInterval(fetch, 1000 * 60, {
|
useInterval(fetch, 1000 * 60, {
|
||||||
|
@ -113,32 +163,39 @@ defineExpose<WidgetComponentExpose>({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.bdayFRoot {
|
.empty {
|
||||||
overflow: hidden;
|
|
||||||
min-height: calc(calc(calc(50px * 3) - 8px) + calc(var(--margin) * 2));
|
|
||||||
}
|
|
||||||
.bdayFGrid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(6, 42px);
|
|
||||||
grid-template-rows: repeat(3, 42px);
|
|
||||||
place-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin: var(--margin) auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bdayFFallback {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
> img {
|
||||||
|
height: 96px;
|
||||||
|
width: auto;
|
||||||
|
max-width: 90%;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bdayFFallbackImage {
|
.post {
|
||||||
height: 96px;
|
display: flex;
|
||||||
width: auto;
|
justify-content: center;
|
||||||
max-width: 90%;
|
align-items: center;
|
||||||
margin-bottom: 8px;
|
height: 40px;
|
||||||
border-radius: var(--radius);
|
margin: auto;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
border-radius: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
&:before {
|
||||||
|
background: var(--accentLighten);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.postIcon {
|
||||||
|
color: var(--fgOnAccent);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1686,6 +1686,8 @@ declare namespace entities {
|
||||||
UsersFollowingResponse,
|
UsersFollowingResponse,
|
||||||
UsersGalleryPostsRequest,
|
UsersGalleryPostsRequest,
|
||||||
UsersGalleryPostsResponse,
|
UsersGalleryPostsResponse,
|
||||||
|
UsersGetFollowingBirthdayUsersRequest,
|
||||||
|
UsersGetFollowingBirthdayUsersResponse,
|
||||||
UsersGetFrequentlyRepliedUsersRequest,
|
UsersGetFrequentlyRepliedUsersRequest,
|
||||||
UsersGetFrequentlyRepliedUsersResponse,
|
UsersGetFrequentlyRepliedUsersResponse,
|
||||||
UsersFeaturedNotesRequest,
|
UsersFeaturedNotesRequest,
|
||||||
|
@ -3091,6 +3093,12 @@ type UsersGalleryPostsRequest = operations['users___gallery___posts']['requestBo
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type UsersGalleryPostsResponse = operations['users___gallery___posts']['responses']['200']['content']['application/json'];
|
type UsersGalleryPostsResponse = operations['users___gallery___posts']['responses']['200']['content']['application/json'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type UsersGetFollowingBirthdayUsersRequest = operations['users___get-following-birthday-users']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type UsersGetFollowingBirthdayUsersResponse = operations['users___get-following-birthday-users']['responses']['200']['content']['application/json'];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type UsersGetFrequentlyRepliedUsersRequest = operations['users___get-frequently-replied-users']['requestBody']['content']['application/json'];
|
type UsersGetFrequentlyRepliedUsersRequest = operations['users___get-frequently-replied-users']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
|
|
|
@ -3804,6 +3804,17 @@ declare module '../api.js' {
|
||||||
credential?: string | null,
|
credential?: string | null,
|
||||||
): Promise<SwitchCaseResponseType<E, P>>;
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find users who have a birthday on the specified range.
|
||||||
|
*
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *read:account*
|
||||||
|
*/
|
||||||
|
request<E extends 'users/get-following-birthday-users', P extends Endpoints[E]['req']>(
|
||||||
|
endpoint: E,
|
||||||
|
params: P,
|
||||||
|
credential?: string | null,
|
||||||
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a list of other users that the specified user frequently replies to.
|
* Get a list of other users that the specified user frequently replies to.
|
||||||
*
|
*
|
||||||
|
|
|
@ -506,6 +506,8 @@ import type {
|
||||||
UsersFollowingResponse,
|
UsersFollowingResponse,
|
||||||
UsersGalleryPostsRequest,
|
UsersGalleryPostsRequest,
|
||||||
UsersGalleryPostsResponse,
|
UsersGalleryPostsResponse,
|
||||||
|
UsersGetFollowingBirthdayUsersRequest,
|
||||||
|
UsersGetFollowingBirthdayUsersResponse,
|
||||||
UsersGetFrequentlyRepliedUsersRequest,
|
UsersGetFrequentlyRepliedUsersRequest,
|
||||||
UsersGetFrequentlyRepliedUsersResponse,
|
UsersGetFrequentlyRepliedUsersResponse,
|
||||||
UsersFeaturedNotesRequest,
|
UsersFeaturedNotesRequest,
|
||||||
|
@ -916,6 +918,7 @@ export type Endpoints = {
|
||||||
'users/followers': { req: UsersFollowersRequest; res: UsersFollowersResponse };
|
'users/followers': { req: UsersFollowersRequest; res: UsersFollowersResponse };
|
||||||
'users/following': { req: UsersFollowingRequest; res: UsersFollowingResponse };
|
'users/following': { req: UsersFollowingRequest; res: UsersFollowingResponse };
|
||||||
'users/gallery/posts': { req: UsersGalleryPostsRequest; res: UsersGalleryPostsResponse };
|
'users/gallery/posts': { req: UsersGalleryPostsRequest; res: UsersGalleryPostsResponse };
|
||||||
|
'users/get-following-birthday-users': { req: UsersGetFollowingBirthdayUsersRequest; res: UsersGetFollowingBirthdayUsersResponse };
|
||||||
'users/get-frequently-replied-users': { req: UsersGetFrequentlyRepliedUsersRequest; res: UsersGetFrequentlyRepliedUsersResponse };
|
'users/get-frequently-replied-users': { req: UsersGetFrequentlyRepliedUsersRequest; res: UsersGetFrequentlyRepliedUsersResponse };
|
||||||
'users/featured-notes': { req: UsersFeaturedNotesRequest; res: UsersFeaturedNotesResponse };
|
'users/featured-notes': { req: UsersFeaturedNotesRequest; res: UsersFeaturedNotesResponse };
|
||||||
'users/lists/create': { req: UsersListsCreateRequest; res: UsersListsCreateResponse };
|
'users/lists/create': { req: UsersListsCreateRequest; res: UsersListsCreateResponse };
|
||||||
|
|
|
@ -509,6 +509,8 @@ export type UsersFollowingRequest = operations['users___following']['requestBody
|
||||||
export type UsersFollowingResponse = operations['users___following']['responses']['200']['content']['application/json'];
|
export type UsersFollowingResponse = operations['users___following']['responses']['200']['content']['application/json'];
|
||||||
export type UsersGalleryPostsRequest = operations['users___gallery___posts']['requestBody']['content']['application/json'];
|
export type UsersGalleryPostsRequest = operations['users___gallery___posts']['requestBody']['content']['application/json'];
|
||||||
export type UsersGalleryPostsResponse = operations['users___gallery___posts']['responses']['200']['content']['application/json'];
|
export type UsersGalleryPostsResponse = operations['users___gallery___posts']['responses']['200']['content']['application/json'];
|
||||||
|
export type UsersGetFollowingBirthdayUsersRequest = operations['users___get-following-birthday-users']['requestBody']['content']['application/json'];
|
||||||
|
export type UsersGetFollowingBirthdayUsersResponse = operations['users___get-following-birthday-users']['responses']['200']['content']['application/json'];
|
||||||
export type UsersGetFrequentlyRepliedUsersRequest = operations['users___get-frequently-replied-users']['requestBody']['content']['application/json'];
|
export type UsersGetFrequentlyRepliedUsersRequest = operations['users___get-frequently-replied-users']['requestBody']['content']['application/json'];
|
||||||
export type UsersGetFrequentlyRepliedUsersResponse = operations['users___get-frequently-replied-users']['responses']['200']['content']['application/json'];
|
export type UsersGetFrequentlyRepliedUsersResponse = operations['users___get-frequently-replied-users']['responses']['200']['content']['application/json'];
|
||||||
export type UsersFeaturedNotesRequest = operations['users___featured-notes']['requestBody']['content']['application/json'];
|
export type UsersFeaturedNotesRequest = operations['users___featured-notes']['requestBody']['content']['application/json'];
|
||||||
|
|
|
@ -3276,6 +3276,15 @@ export type paths = {
|
||||||
*/
|
*/
|
||||||
post: operations['users___gallery___posts'];
|
post: operations['users___gallery___posts'];
|
||||||
};
|
};
|
||||||
|
'/users/get-following-birthday-users': {
|
||||||
|
/**
|
||||||
|
* users/get-following-birthday-users
|
||||||
|
* @description Find users who have a birthday on the specified range.
|
||||||
|
*
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *read:account*
|
||||||
|
*/
|
||||||
|
post: operations['users___get-following-birthday-users'];
|
||||||
|
};
|
||||||
'/users/get-frequently-replied-users': {
|
'/users/get-frequently-replied-users': {
|
||||||
/**
|
/**
|
||||||
* users/get-frequently-replied-users
|
* users/get-frequently-replied-users
|
||||||
|
@ -25425,6 +25434,7 @@ export type operations = {
|
||||||
username?: string;
|
username?: string;
|
||||||
/** @description The local host is represented with `null`. */
|
/** @description The local host is represented with `null`. */
|
||||||
host?: string | null;
|
host?: string | null;
|
||||||
|
/** @description @deprecated use get-following-birthday-users instead. */
|
||||||
birthday?: string | null;
|
birthday?: string | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -25528,6 +25538,78 @@ export type operations = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* users/get-following-birthday-users
|
||||||
|
* @description Find users who have a birthday on the specified range.
|
||||||
|
*
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *read:account*
|
||||||
|
*/
|
||||||
|
'users___get-following-birthday-users': {
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
/** @default 10 */
|
||||||
|
limit?: number;
|
||||||
|
/** @default 0 */
|
||||||
|
offset?: number;
|
||||||
|
birthday: {
|
||||||
|
month?: number;
|
||||||
|
day?: number;
|
||||||
|
begin?: {
|
||||||
|
month: number;
|
||||||
|
day: number;
|
||||||
|
};
|
||||||
|
end?: {
|
||||||
|
month: number;
|
||||||
|
day: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description OK (with results) */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
/** Format: date-time */
|
||||||
|
birthday: string;
|
||||||
|
user: components['schemas']['UserLite'];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Client error */
|
||||||
|
400: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Authentication error */
|
||||||
|
401: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Forbidden error */
|
||||||
|
403: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description I'm Ai */
|
||||||
|
418: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Internal server error */
|
||||||
|
500: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* users/get-frequently-replied-users
|
* users/get-frequently-replied-users
|
||||||
* @description Get a list of other users that the specified user frequently replies to.
|
* @description Get a list of other users that the specified user frequently replies to.
|
||||||
|
@ -26896,6 +26978,8 @@ export type operations = {
|
||||||
username?: string;
|
username?: string;
|
||||||
/** @description The local host is represented with `null`. */
|
/** @description The local host is represented with `null`. */
|
||||||
host?: string | null;
|
host?: string | null;
|
||||||
|
/** @default true */
|
||||||
|
detailed?: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -26903,7 +26987,7 @@ export type operations = {
|
||||||
/** @description OK (with results) */
|
/** @description OK (with results) */
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
'application/json': components['schemas']['UserDetailed'] | components['schemas']['UserDetailed'][];
|
'application/json': components['schemas']['UserLite'] | components['schemas']['UserDetailed'] | components['schemas']['UserLite'][] | components['schemas']['UserDetailed'][];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Client error */
|
/** @description Client error */
|
||||||
|
|
Loading…
Reference in a new issue