Merge tag '2024.3.1-io.4' into host
This commit is contained in:
commit
1d7f763ed9
34 changed files with 2806 additions and 1929 deletions
|
@ -18,6 +18,7 @@
|
|||
- 実装の都合により、プラグインは1つエラーを起こした時に即時停止するようになりました
|
||||
- Enhance: ページのデザインを変更
|
||||
- Enhance: 2要素認証(ワンタイムパスワード)の入力欄を改善
|
||||
- Enhance: 「今日誕生日のフォロー中ユーザー」ウィジェットを手動でリロードできるように
|
||||
- Fix: 一部のページ内リンクが正しく動作しない問題を修正
|
||||
- Fix: 周年の実績が閏年を考慮しない問題を修正
|
||||
- Fix: ローカルURLのプレビューポップアップが左上に表示される
|
||||
|
@ -27,6 +28,7 @@
|
|||
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/528)
|
||||
- Fix: コードブロックのシンタックスハイライトで使用される定義ファイルをCDNから取得するように #13177
|
||||
- CDNから取得せずMisskey本体にバンドルする場合は`pacakges/frontend/vite.config.ts`を修正してください。
|
||||
- Fix: タイムゾーンによっては、「今日誕生日のフォロー中ユーザー」ウィジェットが正しく動作しない問題を修正
|
||||
|
||||
### Server
|
||||
- Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに
|
||||
|
|
|
@ -2153,6 +2153,7 @@ _widgets:
|
|||
chooseList: "Select a list"
|
||||
clicker: "Clicker"
|
||||
birthdayFollowings: "Users who celebrate their birthday today"
|
||||
birthdaySoon: "Users who will celebrate their birthday soon"
|
||||
_cw:
|
||||
hide: "Hide"
|
||||
show: "Show content"
|
||||
|
@ -2574,4 +2575,18 @@ _reversi:
|
|||
_offlineScreen:
|
||||
title: "Offline - cannot connect to the server"
|
||||
header: "Unable to connect to the server"
|
||||
|
||||
_skebStatus:
|
||||
_genres:
|
||||
art: "Artwork"
|
||||
comic: "Comic"
|
||||
voice: "Voice"
|
||||
novel: "Text"
|
||||
video: "Movie"
|
||||
music: "Music"
|
||||
correction: "Advice"
|
||||
seeking: "Seeking"
|
||||
stopped: "Stopped"
|
||||
client: "Client"
|
||||
yenX: "JPY {x}"
|
||||
nWorks: "Delivered {n} works"
|
||||
nRequests: "Requested {n} times"
|
||||
|
|
62
locales/index.d.ts
vendored
62
locales/index.d.ts
vendored
|
@ -4865,7 +4865,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"wellKnownWebsites": string;
|
||||
/**
|
||||
* スペースで区切るとAND指定になり、改行で区切るとOR指定になります。スラッシュで囲むと正規表現になります。一致した場合、外部サイトへのリダイレクトの警告を省略させることができます。
|
||||
* スペースで区切るとAND指定になり、改行で区切るとOR指定になります。スラッシュで囲むと正規表現になります。ドメイン名だけ書くと後方一致になります。一致した場合、外部サイトへのリダイレクトの警告を省略させることができます。
|
||||
*/
|
||||
"wellKnownWebsitesDescription": string;
|
||||
/**
|
||||
|
@ -8396,6 +8396,10 @@ export interface Locale extends ILocale {
|
|||
* 今日誕生日のユーザー
|
||||
*/
|
||||
"birthdayFollowings": string;
|
||||
/**
|
||||
* もうすぐ誕生日のユーザー
|
||||
*/
|
||||
"birthdaySoon": string;
|
||||
};
|
||||
"_cw": {
|
||||
/**
|
||||
|
@ -10109,6 +10113,62 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"summaryProxyDescription2": string;
|
||||
};
|
||||
"_skebStatus": {
|
||||
"_genres": {
|
||||
/**
|
||||
* イラスト
|
||||
*/
|
||||
"art": string;
|
||||
/**
|
||||
* コミック
|
||||
*/
|
||||
"comic": string;
|
||||
/**
|
||||
* ボイス
|
||||
*/
|
||||
"voice": string;
|
||||
/**
|
||||
* テキスト
|
||||
*/
|
||||
"novel": string;
|
||||
/**
|
||||
* ムービー
|
||||
*/
|
||||
"video": string;
|
||||
/**
|
||||
* ミュージック
|
||||
*/
|
||||
"music": string;
|
||||
/**
|
||||
* アドバイス
|
||||
*/
|
||||
"correction": string;
|
||||
};
|
||||
/**
|
||||
* 募集中
|
||||
*/
|
||||
"seeking": string;
|
||||
/**
|
||||
* 停止中
|
||||
*/
|
||||
"stopped": string;
|
||||
/**
|
||||
* クライアント
|
||||
*/
|
||||
"client": string;
|
||||
/**
|
||||
* {x}円
|
||||
*/
|
||||
"yenX": ParameterizedString<"x">;
|
||||
/**
|
||||
* 納品実績 {n}件
|
||||
*/
|
||||
"nWorks": ParameterizedString<"n">;
|
||||
/**
|
||||
* 取引実績 {n}件
|
||||
*/
|
||||
"nRequests": ParameterizedString<"n">;
|
||||
};
|
||||
}
|
||||
declare const locales: {
|
||||
[lang: string]: Locale;
|
||||
|
|
|
@ -2207,6 +2207,7 @@ _widgets:
|
|||
chooseList: "リストを選択"
|
||||
clicker: "クリッカー"
|
||||
birthdayFollowings: "今日誕生日のユーザー"
|
||||
birthdaySoon: "もうすぐ誕生日のユーザー"
|
||||
|
||||
_cw:
|
||||
hide: "隠す"
|
||||
|
@ -2692,3 +2693,19 @@ _urlPreviewSetting:
|
|||
summaryProxy: "プレビューを生成するプロキシのエンドポイント"
|
||||
summaryProxyDescription: "Misskey本体ではなく、サマリープロキシを使用してプレビューを生成します。"
|
||||
summaryProxyDescription2: "プロキシには下記パラメータがクエリ文字列として連携されます。プロキシ側がこれらをサポートしない場合、設定値は無視されます。"
|
||||
|
||||
_skebStatus:
|
||||
_genres:
|
||||
art: "イラスト"
|
||||
comic: "コミック"
|
||||
voice: "ボイス"
|
||||
novel: "テキスト"
|
||||
video: "ムービー"
|
||||
music: "ミュージック"
|
||||
correction: "アドバイス"
|
||||
seeking: "募集中"
|
||||
stopped: "停止中"
|
||||
client: "クライアント"
|
||||
yenX: "{x}円"
|
||||
nWorks: "納品実績 {n}件"
|
||||
nRequests: "取引実績 {n}件"
|
||||
|
|
|
@ -2131,6 +2131,7 @@ _widgets:
|
|||
chooseList: "리스트 선택"
|
||||
clicker: "클리커"
|
||||
birthdayFollowings: "오늘이 생일인 사용자"
|
||||
birthdaySoon: "곧 생일인 사용자"
|
||||
_cw:
|
||||
hide: "숨기기"
|
||||
show: "더 보기"
|
||||
|
@ -2548,4 +2549,18 @@ _reversi:
|
|||
_offlineScreen:
|
||||
title: "오프라인 - 서버에 접속할 수 없습니다"
|
||||
header: "서버에 접속할 수 없습니다"
|
||||
|
||||
_skebStatus:
|
||||
_genres:
|
||||
art: "작품"
|
||||
comic: "만화"
|
||||
voice: "음성"
|
||||
novel: "텍스트"
|
||||
video: "동영상"
|
||||
music: "음악"
|
||||
correction: "조언"
|
||||
seeking: "모집 중"
|
||||
stopped: "정지 중"
|
||||
client: "클라이언트"
|
||||
yenX: "JPY {x}"
|
||||
nWorks: "납품 실적 {n}건"
|
||||
nRequests: "거래 실적 {n}건"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "2024.3.1-host.3",
|
||||
"version": "2024.3.1-host.4",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
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)`);
|
||||
}
|
||||
}
|
|
@ -81,6 +81,14 @@ type Source = {
|
|||
}
|
||||
};
|
||||
|
||||
skebStatus?: {
|
||||
method: string;
|
||||
endpoint: string;
|
||||
headers: { [x: string]: string };
|
||||
parameters: { [x: string]: string };
|
||||
userIdParameterName: string;
|
||||
}
|
||||
|
||||
proxy?: string;
|
||||
proxySmtp?: string;
|
||||
proxyBypassHosts?: string[];
|
||||
|
@ -115,6 +123,7 @@ type Source = {
|
|||
|
||||
bypassRateLimit?: { header: string; value: string }[];
|
||||
|
||||
remapDriveFileUrlForActivityPub?: { target: string; replacement: string }[];
|
||||
signToActivityPubGet?: boolean;
|
||||
|
||||
perChannelMaxNoteCacheCount?: number;
|
||||
|
@ -169,6 +178,13 @@ export type Config = {
|
|||
useProxy?: boolean;
|
||||
}
|
||||
} | undefined;
|
||||
skebStatus: {
|
||||
method: string;
|
||||
endpoint: string;
|
||||
headers: { [x: string]: string };
|
||||
parameters: { [x: string]: string };
|
||||
userIdParameterName: string;
|
||||
} | undefined;
|
||||
proxy: string | undefined;
|
||||
proxySmtp: string | undefined;
|
||||
proxyBypassHosts: string[] | undefined;
|
||||
|
@ -190,6 +206,7 @@ export type Config = {
|
|||
deliverJobMaxAttempts: number | undefined;
|
||||
inboxJobMaxAttempts: number | undefined;
|
||||
proxyRemoteFiles: boolean | undefined;
|
||||
remapDriveFileUrlForActivityPub: { target: string; replacement: string }[] | undefined;
|
||||
signToActivityPubGet: boolean | undefined;
|
||||
|
||||
version: string;
|
||||
|
@ -295,6 +312,7 @@ export function loadConfig(): Config {
|
|||
redisForObjectStorageQueue: config.redisForObjectStorageQueue ? convertRedisOptions(config.redisForObjectStorageQueue, host) : redisForJobQueue,
|
||||
redisForWebhookDeliverQueue: config.redisForWebhookDeliverQueue ? convertRedisOptions(config.redisForWebhookDeliverQueue, host) : redisForJobQueue,
|
||||
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
|
||||
skebStatus: config.skebStatus,
|
||||
id: config.id,
|
||||
proxy: config.proxy,
|
||||
proxySmtp: config.proxySmtp,
|
||||
|
@ -316,6 +334,7 @@ export function loadConfig(): Config {
|
|||
deliverJobMaxAttempts: config.deliverJobMaxAttempts,
|
||||
inboxJobMaxAttempts: config.inboxJobMaxAttempts,
|
||||
proxyRemoteFiles: config.proxyRemoteFiles,
|
||||
remapDriveFileUrlForActivityPub: config.remapDriveFileUrlForActivityPub,
|
||||
signToActivityPubGet: config.signToActivityPubGet,
|
||||
mediaProxy: externalMediaProxy ?? internalMediaProxy,
|
||||
externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy,
|
||||
|
|
|
@ -56,8 +56,10 @@ export class HashtagService {
|
|||
public async updateHashtag(user: { id: MiUser['id']; host: MiUser['host']; }, tag: string, isUserAttached = false, inc = true) {
|
||||
tag = normalizeForSearch(tag);
|
||||
|
||||
// TODO: サンプリング
|
||||
this.updateHashtagsRanking(tag, user.id);
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
// TODO: サンプリング
|
||||
this.updateHashtagsRanking(tag, user.id);
|
||||
}
|
||||
|
||||
const index = await this.hashtagsRepository.findOneBy({ name: tag });
|
||||
|
||||
|
|
|
@ -164,13 +164,20 @@ export class ApRendererService {
|
|||
return {
|
||||
type: 'Document',
|
||||
mediaType: file.webpublicType ?? file.type,
|
||||
url: this.driveFileEntityService.getPublicUrl(file),
|
||||
url: this.driveFileEntityService.getPublicUrl(file, { remapActivityPub: true }),
|
||||
name: file.comment,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderEmoji(emoji: MiEmoji): IApEmoji {
|
||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
let url = emoji.publicUrl || emoji.originalUrl;
|
||||
|
||||
this.config.remapDriveFileUrlForActivityPub?.forEach(({ target, replacement }) => {
|
||||
url = url.replace(target, replacement);
|
||||
});
|
||||
|
||||
return {
|
||||
id: `${this.config.url}/emojis/${emoji.name}`,
|
||||
type: 'Emoji',
|
||||
|
@ -179,8 +186,7 @@ export class ApRendererService {
|
|||
icon: {
|
||||
type: 'Image',
|
||||
mediaType: emoji.type ?? 'image/png',
|
||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
url: emoji.publicUrl || emoji.originalUrl,
|
||||
url: url,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -243,7 +249,7 @@ export class ApRendererService {
|
|||
public renderImage(file: MiDriveFile): IApImage {
|
||||
return {
|
||||
type: 'Image',
|
||||
url: this.driveFileEntityService.getPublicUrl(file),
|
||||
url: this.driveFileEntityService.getPublicUrl(file, { remapActivityPub: true }),
|
||||
sensitive: file.isSensitive,
|
||||
name: file.comment,
|
||||
};
|
||||
|
|
|
@ -267,12 +267,12 @@ export class ApPersonService implements OnModuleInit {
|
|||
return {
|
||||
...( avatar ? {
|
||||
avatarId: avatar.id,
|
||||
avatarUrl: avatar.url ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null,
|
||||
avatarUrl: avatar.url ? this.driveFileEntityService.getPublicUrl(avatar, { mode: 'avatar', remapActivityPub: true }) : null,
|
||||
avatarBlurhash: avatar.blurhash,
|
||||
} : {}),
|
||||
...( banner ? {
|
||||
bannerId: banner.id,
|
||||
bannerUrl: banner.url ? this.driveFileEntityService.getPublicUrl(banner) : null,
|
||||
bannerUrl: banner.url ? this.driveFileEntityService.getPublicUrl(banner, { remapActivityPub: true }) : null,
|
||||
bannerBlurhash: banner.blurhash,
|
||||
} : {}),
|
||||
};
|
||||
|
|
|
@ -109,10 +109,10 @@ export class DriveFileEntityService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public getPublicUrl(file: MiDriveFile, mode?: 'avatar'): string { // static = thumbnail
|
||||
public getPublicUrl(file: MiDriveFile, option?: { mode?: 'avatar', remapActivityPub?: boolean }): string { // static = thumbnail
|
||||
// リモートかつメディアプロキシ
|
||||
if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
|
||||
return this.getProxiedUrl(file.uri, mode);
|
||||
return this.getProxiedUrl(file.uri, option?.mode);
|
||||
}
|
||||
|
||||
// リモートかつ期限切れはローカルプロキシを試みる
|
||||
|
@ -121,17 +121,23 @@ export class DriveFileEntityService {
|
|||
|
||||
if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外
|
||||
const url = `${this.config.url}/files/${key}`;
|
||||
if (mode === 'avatar') return this.getProxiedUrl(file.uri, 'avatar');
|
||||
if (option?.mode === 'avatar') return this.getProxiedUrl(file.uri, 'avatar');
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
const url = file.webpublicUrl ?? file.url;
|
||||
|
||||
if (mode === 'avatar') {
|
||||
return this.getProxiedUrl(url, 'avatar');
|
||||
let publicUrl = file.webpublicUrl ?? file.url;
|
||||
if (option?.remapActivityPub) {
|
||||
this.config.remapDriveFileUrlForActivityPub?.forEach(({ target, replacement }) => {
|
||||
publicUrl = publicUrl.replace(target, replacement);
|
||||
});
|
||||
}
|
||||
return url;
|
||||
|
||||
const url = new URL(publicUrl);
|
||||
if (file.isSensitive) url.searchParams.set('sensitive', 'true');
|
||||
|
||||
if (option?.mode === 'avatar') return this.getProxiedUrl(url.href, 'avatar');
|
||||
else return url.href;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
@ -112,6 +112,7 @@ export class MetaEntityService {
|
|||
|
||||
mediaProxy: this.config.mediaProxy,
|
||||
enableUrlPreview: instance.urlPreviewEnabled,
|
||||
enableSkebStatus: !!this.config.skebStatus,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -642,8 +642,8 @@ export class UserEntityService implements OnModuleInit {
|
|||
|
||||
// -- 特に前提条件のない値群を取得
|
||||
|
||||
const profilesMap = await this.userProfilesRepository.findBy({ userId: In(_userIds) })
|
||||
.then(profiles => new Map(profiles.map(p => [p.userId, p])));
|
||||
const profilesMap = (options?.schema !== 'UserLite') ? await this.userProfilesRepository.findBy({ userId: In(_userIds) })
|
||||
.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')
|
||||
.map(result => (result as PromiseFulfilledResult<Packed<S>>).value);
|
||||
}
|
||||
|
|
|
@ -216,6 +216,10 @@ export const packedMetaLiteSchema = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableSkebStatus: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
backgroundImageUrl: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
|
|
|
@ -348,7 +348,9 @@ import * as ep___users_clips from './endpoints/users/clips.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_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_getSkebStatus from './endpoints/users/get-skeb-status.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_delete from './endpoints/users/lists/delete.js';
|
||||
|
@ -733,7 +735,9 @@ 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_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_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_getSkebStatus: Provider = { provide: 'ep:users/get-skeb-status', useClass: ep___users_getSkebStatus.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_delete: Provider = { provide: 'ep:users/lists/delete', useClass: ep___users_lists_delete.default };
|
||||
|
@ -1122,7 +1126,9 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
|||
$users_followers,
|
||||
$users_following,
|
||||
$users_gallery_posts,
|
||||
$users_getFollowingBirthdayUsers,
|
||||
$users_getFrequentlyRepliedUsers,
|
||||
$users_getSkebStatus,
|
||||
$users_featuredNotes,
|
||||
$users_lists_create,
|
||||
$users_lists_delete,
|
||||
|
@ -1503,7 +1509,9 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
|||
$users_followers,
|
||||
$users_following,
|
||||
$users_gallery_posts,
|
||||
$users_getFollowingBirthdayUsers,
|
||||
$users_getFrequentlyRepliedUsers,
|
||||
$users_getSkebStatus,
|
||||
$users_featuredNotes,
|
||||
$users_lists_create,
|
||||
$users_lists_delete,
|
||||
|
|
|
@ -348,7 +348,9 @@ import * as ep___users_clips from './endpoints/users/clips.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_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_getSkebStatus from './endpoints/users/get-skeb-status.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_delete from './endpoints/users/lists/delete.js';
|
||||
|
@ -731,7 +733,9 @@ const eps = [
|
|||
['users/followers', ep___users_followers],
|
||||
['users/following', ep___users_following],
|
||||
['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-skeb-status', ep___users_getSkebStatus],
|
||||
['users/featured-notes', ep___users_featuredNotes],
|
||||
['users/lists/create', ep___users_lists_create],
|
||||
['users/lists/delete', ep___users_lists_delete],
|
||||
|
|
|
@ -314,7 +314,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
if (!avatar.type.startsWith('image/')) throw new ApiError(meta.errors.avatarNotAnImage);
|
||||
|
||||
updates.avatarId = avatar.id;
|
||||
updates.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, 'avatar');
|
||||
updates.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, { mode: 'avatar' });
|
||||
updates.avatarBlurhash = avatar.blurhash;
|
||||
} else if (ps.avatarId === null) {
|
||||
updates.avatarId = null;
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import { IsNull } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/_.js';
|
||||
import { birthdaySchema } from '@/models/User.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js';
|
||||
|
@ -66,7 +67,10 @@ export const paramDef = {
|
|||
description: 'The local host is represented with `null`.',
|
||||
},
|
||||
|
||||
birthday: { type: 'string', nullable: true },
|
||||
birthday: {
|
||||
...birthdaySchema, nullable: true,
|
||||
description: '@deprecated use get-following-birthday-users instead.',
|
||||
},
|
||||
},
|
||||
anyOf: [
|
||||
{ required: ['userId'] },
|
||||
|
@ -125,16 +129,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.andWhere('following.followerId = :userId', { userId: user.id })
|
||||
.innerJoinAndSelect('following.followee', 'followee');
|
||||
|
||||
// @deprecated use get-following-birthday-users instead.
|
||||
if (ps.birthday) {
|
||||
try {
|
||||
const d = new Date(ps.birthday);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
const birthday = `${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`;
|
||||
const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile');
|
||||
birthdayUserQuery.select('user_profile.userId')
|
||||
.where(`SUBSTR(user_profile.birthday, 6, 5) = '${birthday}'`);
|
||||
query.innerJoin(this.userProfilesRepository.metadata.targetName, 'followeeProfile', 'followeeProfile.userId = following.followeeId');
|
||||
|
||||
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) {
|
||||
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'> });
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
|
||||
requireCredential: false,
|
||||
allowGet: true,
|
||||
cacheSec: 60 * 5,
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
screenName: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isCreator: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isAcceptable: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
creatorRequestCount: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
clientRequestCount: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
skills: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
amount: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
genre: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['art', 'comic', 'voice', 'novel', 'video', 'music', 'correction'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
skebStatusNotAvailable: {
|
||||
message: 'Skeb status is not available.',
|
||||
code: 'SKEB_STATUS_NOT_AVAILABLE',
|
||||
id: '1dd37c9c-7e97-4c24-be98-227a78b21d80',
|
||||
httpStatusCode: 403,
|
||||
},
|
||||
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '88d582ae-69d9-45e0-a8b3-13f9945e48bf',
|
||||
httpStatusCode: 404,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
private loggerService: LoggerService,
|
||||
private httpRequestService: HttpRequestService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps) => {
|
||||
if (!this.config.skebStatus) throw new ApiError(meta.errors.skebStatusNotAvailable);
|
||||
const logger = this.loggerService.getLogger('api:users:get-skeb-status');
|
||||
|
||||
const url = new URL(this.config.skebStatus.endpoint);
|
||||
for (const [key, value] of Object.entries(this.config.skebStatus.parameters)) {
|
||||
url.searchParams.set(key, value);
|
||||
}
|
||||
url.searchParams.set(this.config.skebStatus.userIdParameterName, ps.userId);
|
||||
|
||||
logger.info('Requesting Skeb status', { url: url.href, userId: ps.userId });
|
||||
const res = await this.httpRequestService.send(
|
||||
url.href,
|
||||
{
|
||||
method: this.config.skebStatus.method,
|
||||
headers: {
|
||||
...this.config.skebStatus.headers,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
timeout: 5000,
|
||||
},
|
||||
{
|
||||
throwErrorWhenResponseNotOk: false,
|
||||
},
|
||||
);
|
||||
|
||||
const json = (await res.json()) as {
|
||||
screen_name: string,
|
||||
is_creator: boolean,
|
||||
is_acceptable: boolean,
|
||||
creator_request_count: number,
|
||||
client_request_count: number,
|
||||
skills: { amount: number, genre: 'art' | 'comic' | 'voice' | 'novel' | 'video' | 'music' | 'correction' }[],
|
||||
ban_reason?: string | null
|
||||
error?: unknown,
|
||||
};
|
||||
|
||||
if (res.status > 399 || (json.error ?? json.ban_reason)) {
|
||||
logger.error('Skeb status response error', { url: url.href, userId: ps.userId, status: res.status, statusText: res.statusText, error: json.error ?? json.ban_reason });
|
||||
throw new ApiError(meta.errors.noSuchUser);
|
||||
}
|
||||
|
||||
logger.info('Skeb status response', { url: url.href, userId: ps.userId, status: res.status, statusText: res.statusText, skebStatus: json });
|
||||
|
||||
return {
|
||||
screenName: json.screen_name,
|
||||
isCreator: json.is_creator,
|
||||
isAcceptable: json.is_acceptable,
|
||||
creatorRequestCount: json.creator_request_count,
|
||||
clientRequestCount: json.client_request_count,
|
||||
skills: json.skills,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
|
@ -27,10 +27,21 @@ export const meta = {
|
|||
res: {
|
||||
optional: false, nullable: false,
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object',
|
||||
ref: 'UserLite',
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
ref: 'UserDetailed',
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
ref: 'UserLite',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
|
@ -71,6 +82,7 @@ export const paramDef = {
|
|||
nullable: true,
|
||||
description: 'The local host is represented with `null`.',
|
||||
},
|
||||
detailed: { type: 'boolean', default: true },
|
||||
},
|
||||
anyOf: [
|
||||
{ required: ['userId'] },
|
||||
|
@ -117,7 +129,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
|
||||
return await this.userEntityService.packMany(_users, me, {
|
||||
schema: 'UserDetailed',
|
||||
schema: ps.detailed ? 'UserDetailed' : 'UserLite',
|
||||
});
|
||||
} else {
|
||||
// Lookup user
|
||||
|
@ -147,7 +159,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
|
||||
return await this.userEntityService.pack(user, me, {
|
||||
schema: 'UserDetailed',
|
||||
schema: ps.detailed ? 'UserDetailed' : 'UserLite',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -93,7 +93,7 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) {
|
|||
const hasBody = (schema.type === 'object' && schema.properties && Object.keys(schema.properties).length >= 1);
|
||||
|
||||
const info = {
|
||||
operationId: endpoint.name,
|
||||
operationId: endpoint.name.replaceAll('/', '___'), // NOTE: スラッシュは使えない
|
||||
summary: endpoint.name,
|
||||
description: desc,
|
||||
externalDocs: {
|
||||
|
|
|
@ -160,19 +160,17 @@ describe('Streaming', () => {
|
|||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
/* なんか失敗する
|
||||
test('フォローしているユーザーの visibility: followers な投稿への返信が流れる', async () => {
|
||||
const note = await api('notes/create', { text: 'foo', visibility: 'followers' }, kyoko);
|
||||
const note = await post(kyoko, { text: 'foo', visibility: 'followers' });
|
||||
|
||||
const fired = await waitFire(
|
||||
ayano, 'homeTimeline', // ayano:home
|
||||
() => api('notes/create', { text: 'bar', visibility: 'followers', replyId: note.body.id }, kyoko), // kyoko posts
|
||||
() => api('notes/create', { text: 'bar', visibility: 'followers', replyId: note.id }, kyoko), // kyoko posts
|
||||
msg => msg.type === 'note' && msg.body.userId === kyoko.id && msg.body.reply.text === 'foo',
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
*/
|
||||
|
||||
test('フォローしているユーザーのフォローしていないユーザーの visibility: followers な投稿への返信が流れない', async () => {
|
||||
const chitoseNote = await post(chitose, { text: 'followers-only post', visibility: 'followers' });
|
||||
|
|
|
@ -29,6 +29,7 @@ const users = ref<Misskey.entities.UserLite[]>([]);
|
|||
onMounted(async () => {
|
||||
users.value = await misskeyApi('users/show', {
|
||||
userIds: props.userIds,
|
||||
detailed: false,
|
||||
}) as unknown as Misskey.entities.UserLite[];
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -94,7 +94,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<dd class="value">{{ dateString(user.createdAt) }} (<MkTime :time="user.createdAt"/>)</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div v-if="user.fields.length > 0" class="fields">
|
||||
<div v-if="user.fields.length > 0 || userSkebStatus" class="fields">
|
||||
<dl v-for="(field, i) in user.fields" :key="i" class="field">
|
||||
<dt class="name">
|
||||
<Mfm :text="field.name" :plain="true" :colored="false"/>
|
||||
|
@ -104,6 +104,44 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<i v-if="user.verifiedLinks.includes(field.value)" v-tooltip:dialog="i18n.ts.verifiedLink" class="ti ti-circle-check" :class="$style.verifiedLink"></i>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl v-if="userSkebStatus" class="field">
|
||||
<dt class="name">
|
||||
<a href="https://skeb.jp/" target="_blank" rel="noopener" style="display: flex; gap: 2px; align-items: center; justify-content: center;">
|
||||
<!--
|
||||
*** LICENSE NOTICE ***
|
||||
* This SVG is derived from the https://skeb.jp/ website, All rights reserved to *Skeb Inc.* https://skeb.co.jp/
|
||||
* This resource SHOULD NOT be considered as a part of the this project that has licensed under AGPL-3.0-only
|
||||
-->
|
||||
<svg class="ti ti-fw" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
||||
<linearGradient id="a" x1="14.645309%" x2="85.354691%" y1="14.645309%" y2="85.363492%">
|
||||
<stop offset=".2" stop-color="#30b396"/>
|
||||
<stop offset="1" stop-color="#1e5e71"/>
|
||||
</linearGradient>
|
||||
<g fill="none">
|
||||
<circle cx="19.99375" cy="19.98125" fill="url(#a)" r="17.753125"/>
|
||||
<path d="m18.784375 13.9875-7.35625 21.546875c-.8125-.446875-1.58125-.959375-2.309375-1.525l1.134375-3.26875-2.528125-.86875.859375-2.56875 2.528125.86875 1.728125-5.05625-2.528125-.86875.86875-2.5375 2.528125.86875 1.728125-5.05625-2.528125-.86875.86875-2.5375 2.528125.86875 1.6875-4.99375-2.496875-.909375.440625-1.26875 5.05625 1.728125-.378125 1.1-1.76875 5.184375-.059375.159375zm-12.996875 16.640625c.6625.884375 1.40625 1.703125 2.21875 2.446875l.51875-1.515625zm3.29375-13.01875 3.8 1.296875.865625-2.534375-3.8-1.296875zm-2.590625 7.590625 3.8 1.296875.865625-2.534375-3.8-1.296875zm31.259375-5.21875c0-2.534375-.534375-4.940625-1.490625-7.11875l-13.325-4.559375-9.603125 28.134375c2.059375.834375 4.30625 1.3 6.665625 1.3 9.80625 0 17.753125-7.946875 17.753125-17.753125zm-26.071875-9.971875 3.8 1.296875.865625-2.534375-3.8-1.296875z" fill="#fff"/><path d="m19.99375 2.23125c9.80625 0 17.753125 7.946875 17.753125 17.753125s-7.946875 17.753125-17.753125 17.753125-17.753125-7.946875-17.753125-17.753125 7.946875-17.753125 17.753125-17.753125m0-2.228125c-11.034375 0-19.98125 8.946875-19.98125 19.98125s8.946875 19.98125 19.98125 19.98125 19.98125-8.946875 19.98125-19.98125-8.946875-19.98125-19.98125-19.98125z" fill="#1e5e71"/>
|
||||
</g>
|
||||
</svg>
|
||||
<span>Skeb</span>
|
||||
<i class="ti ti-external-link" style="font-size: .9em;"></i>
|
||||
</a>
|
||||
</dt>
|
||||
<dd class="value">
|
||||
<a :href="`https://skeb.jp/@${userSkebStatus.screenName}`" target="_blank" rel="noopener" style="display: flex; gap: 2px; align-items: center;">
|
||||
<span v-if="userSkebStatus.isAcceptable" :class="$style.skebAcceptable">
|
||||
{{ i18n.ts._skebStatus.seeking }}
|
||||
</span>
|
||||
<span v-else-if="userSkebStatus.isCreator" :class="$style.skebStopped">
|
||||
{{ i18n.ts._skebStatus.stopped }}
|
||||
</span>
|
||||
<span v-else :class="$style.skebClient">
|
||||
{{ i18n.ts._skebStatus.client }}
|
||||
</span>
|
||||
<Mfm :text="buildSkebStatus()" :author="user" :nyaize="false" :colored="false"/>
|
||||
<i class="ti ti-external-link" style="font-size: .9em;"></i>
|
||||
</a>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="status">
|
||||
<MkA :to="userPage(user)">
|
||||
|
@ -167,10 +205,11 @@ import number from '@/filters/number.js';
|
|||
import { userPage } from '@/filters/user.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { $i, iAmModerator } from '@/account.js';
|
||||
import { dateString } from '@/filters/date.js';
|
||||
import { confetti } from '@/scripts/confetti.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
|
||||
|
@ -204,6 +243,7 @@ const props = withDefaults(defineProps<{
|
|||
const router = useRouter();
|
||||
|
||||
const user = ref(props.user);
|
||||
const userSkebStatus = ref<Misskey.Endpoints['users/get-skeb-status']['res'] | null>(null);
|
||||
const parallaxAnimationId = ref<null | number>(null);
|
||||
const narrow = ref<null | boolean>(null);
|
||||
const rootEl = ref<null | HTMLElement>(null);
|
||||
|
@ -273,8 +313,44 @@ async function updateMemo() {
|
|||
isEditingMemo.value = false;
|
||||
}
|
||||
|
||||
async function fetchSkebStatus() {
|
||||
if (!instance.enableSkebStatus || !props.user.id) {
|
||||
userSkebStatus.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('fetching skeb status');
|
||||
userSkebStatus.value = await misskeyApiGet('users/get-skeb-status', { userId: props.user.id });
|
||||
}
|
||||
|
||||
function buildSkebStatus(): string {
|
||||
if (!userSkebStatus.value) return '';
|
||||
|
||||
if (userSkebStatus.value.isCreator) {
|
||||
let status = '';
|
||||
|
||||
if (userSkebStatus.value.isAcceptable) {
|
||||
status += `${i18n.ts._skebStatus._genres[userSkebStatus.value.skills[0].genre]} ${i18n.tsx._skebStatus.yenX({ x: userSkebStatus.value.skills[0].amount.toLocaleString() })}`;
|
||||
}
|
||||
|
||||
if (userSkebStatus.value.creatorRequestCount > 0) {
|
||||
if (userSkebStatus.value.isAcceptable) {
|
||||
status += ' | ';
|
||||
}
|
||||
status += i18n.tsx._skebStatus.nWorks({ n: userSkebStatus.value.creatorRequestCount.toLocaleString() });
|
||||
}
|
||||
|
||||
return status;
|
||||
} else if (userSkebStatus.value.clientRequestCount > 0) {
|
||||
return i18n.tsx._skebStatus.nRequests({ n: userSkebStatus.value.clientRequestCount.toLocaleString() });
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
watch([props.user], () => {
|
||||
memoDraft.value = props.user.memo;
|
||||
fetchSkebStatus();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -292,6 +368,7 @@ onMounted(() => {
|
|||
});
|
||||
}
|
||||
}
|
||||
fetchSkebStatus();
|
||||
nextTick(() => {
|
||||
adjustMemoTextarea();
|
||||
});
|
||||
|
@ -685,4 +762,30 @@ onUnmounted(() => {
|
|||
margin-left: 4px;
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.skebAcceptable,
|
||||
.skebStopped,
|
||||
.skebClient {
|
||||
display: inline-flex;
|
||||
border: solid 1px;
|
||||
border-radius: 6px;
|
||||
padding: 2px 6px;
|
||||
margin-right: 4px;
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
.skebAcceptable {
|
||||
color: rgb(255, 255, 255);
|
||||
background-color: rgb(241, 70, 104);
|
||||
}
|
||||
|
||||
.skebStopped {
|
||||
color: rgb(255, 255, 255);
|
||||
background-color: rgb(54, 54, 54);
|
||||
}
|
||||
|
||||
.skebClient {
|
||||
color: rgb(255, 255, 255);
|
||||
background-color: rgb(54, 54, 54);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -4,42 +4,76 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<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 #header>{{ i18n.ts._widgets.birthdayFollowings }}</template>
|
||||
<template v-if="widgetProps.period === 'today'" #header>{{ i18n.ts._widgets.birthdayFollowings }}</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">
|
||||
<MkLoading v-if="fetching"/>
|
||||
<div v-else-if="users.length > 0" :class="$style.bdayFGrid">
|
||||
<MkAvatar v-for="user in users" :key="user.id" :user="user.followee" link preview></MkAvatar>
|
||||
</div>
|
||||
<div v-else :class="$style.bdayFFallback">
|
||||
<img :src="infoImageUrl" class="_ghost" :class="$style.bdayFFallbackImage"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<MkPagination ref="paginationEl" :pagination="birthdayUsersPagination">
|
||||
<template #empty>
|
||||
<div :class="$style.empty" :style="`height: ${widgetProps.showHeader ? widgetProps.height - 38 : widgetProps.height}px;`">
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default="{ items: users }">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||
import { GetFormResultType } from '@/scripts/form.js';
|
||||
import MkContainer from '@/components/MkContainer.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { useInterval } from '@/scripts/use-interval.js';
|
||||
import type { MisskeyEntity } from '@/types/date-separated-list.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { userPage } from '@/filters/user.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 = {
|
||||
showHeader: {
|
||||
type: 'boolean' as const,
|
||||
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>;
|
||||
|
@ -47,42 +81,75 @@ type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
|||
const props = defineProps<WidgetComponentProps<WidgetProps>>();
|
||||
const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
|
||||
|
||||
const { widgetProps, configure } = useWidgetPropsManager(name,
|
||||
const { widgetProps, configure } = useWidgetPropsManager(
|
||||
name,
|
||||
widgetPropsDef,
|
||||
props,
|
||||
emit,
|
||||
);
|
||||
|
||||
const users = ref<Misskey.entities.FollowingFolloweePopulated[]>([]);
|
||||
const fetching = ref(true);
|
||||
let lastFetchedAt = '1970-01-01';
|
||||
|
||||
const fetch = () => {
|
||||
if (!$i) {
|
||||
users.value = [];
|
||||
fetching.value = false;
|
||||
return;
|
||||
const begin = ref<Date>(new Date());
|
||||
const end = computed(() => {
|
||||
switch (widgetProps.period) {
|
||||
case '3day':
|
||||
return new Date(begin.value.getTime() + 1000 * 60 * 60 * 24 * 3);
|
||||
case 'week':
|
||||
return new Date(begin.value.getTime() + 1000 * 60 * 60 * 24 * 7);
|
||||
case 'month':
|
||||
return new Date(begin.value.getTime() + 1000 * 60 * 60 * 24 * 30);
|
||||
default:
|
||||
return begin.value;
|
||||
}
|
||||
});
|
||||
|
||||
const lfAtD = new Date(lastFetchedAt);
|
||||
lfAtD.setHours(0, 0, 0, 0);
|
||||
const now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
|
||||
if (now > lfAtD) {
|
||||
misskeyApi('users/following', {
|
||||
limit: 18,
|
||||
birthday: now.toISOString(),
|
||||
userId: $i.id,
|
||||
}).then(res => {
|
||||
users.value = res;
|
||||
fetching.value = false;
|
||||
});
|
||||
|
||||
lastFetchedAt = now.toISOString();
|
||||
}
|
||||
const paginationEl = ref<InstanceType<typeof MkPagination>>();
|
||||
const birthdayUsersPagination = {
|
||||
endpoint: 'users/get-following-birthday-users' as const,
|
||||
limit: 18,
|
||||
offsetMode: true,
|
||||
params: computed(() => {
|
||||
if (widgetProps.period === 'today') {
|
||||
return {
|
||||
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 fetch(force = false) {
|
||||
const now = new Date();
|
||||
if (force || now.getDate() !== begin.value.getDate()) {
|
||||
// computed() で再評価されるので、paginationEl.value!.reload() は不要
|
||||
begin.value = now;
|
||||
}
|
||||
}
|
||||
|
||||
function toMisskeyEntity(items): MisskeyEntity[] {
|
||||
const r = items.map((item: { userId: string, birthday: string, user: Misskey.entities.UserLite }) => ({
|
||||
id: item.user.id,
|
||||
createdAt: item.birthday,
|
||||
user: item.user,
|
||||
}));
|
||||
|
||||
return [{ id: '_', createdAt: begin.value.toISOString() }, ...r];
|
||||
}
|
||||
|
||||
useInterval(fetch, 1000 * 60, {
|
||||
immediate: true,
|
||||
afterMounted: true,
|
||||
|
@ -96,32 +163,39 @@ defineExpose<WidgetComponentExpose>({
|
|||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.bdayFRoot {
|
||||
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%;
|
||||
.empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
> img {
|
||||
height: 96px;
|
||||
width: auto;
|
||||
max-width: 90%;
|
||||
margin-bottom: 8px;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
}
|
||||
|
||||
.bdayFFallbackImage {
|
||||
height: 96px;
|
||||
width: auto;
|
||||
max-width: 90%;
|
||||
margin-bottom: 8px;
|
||||
border-radius: var(--radius);
|
||||
.post {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
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>
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -60,13 +60,17 @@ async function generateEndpoints(
|
|||
// misskey-jsはPOST固定で送っているので、こちらも決め打ちする。別メソッドに対応することがあればこちらも直す必要あり
|
||||
const paths = openApiDocs.paths ?? {};
|
||||
const postPathItems = Object.keys(paths)
|
||||
.map(it => paths[it]?.post)
|
||||
.map(it => ({
|
||||
_path_: it.replace(/^\//, ''),
|
||||
...paths[it]?.post,
|
||||
}))
|
||||
.filter(filterUndefined);
|
||||
|
||||
for (const operation of postPathItems) {
|
||||
const path = operation._path_;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const operationId = operation.operationId!;
|
||||
const endpoint = new Endpoint(operationId);
|
||||
const endpoint = new Endpoint(path);
|
||||
endpoints.push(endpoint);
|
||||
|
||||
if (isRequestBodyObject(operation.requestBody)) {
|
||||
|
@ -76,19 +80,21 @@ async function generateEndpoints(
|
|||
// いまのところ複数のメディアタイプをとるエンドポイントは無いので決め打ちする
|
||||
endpoint.request = new OperationTypeAlias(
|
||||
operationId,
|
||||
path,
|
||||
supportMediaTypes[0],
|
||||
OperationsAliasType.REQUEST,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isResponseObject(operation.responses['200']) && operation.responses['200'].content) {
|
||||
if (operation.responses && isResponseObject(operation.responses['200']) && operation.responses['200'].content) {
|
||||
const resContent = operation.responses['200'].content;
|
||||
const supportMediaTypes = Object.keys(resContent);
|
||||
if (supportMediaTypes.length > 0) {
|
||||
// いまのところ複数のメディアタイプを返すエンドポイントは無いので決め打ちする
|
||||
endpoint.response = new OperationTypeAlias(
|
||||
operationId,
|
||||
path,
|
||||
supportMediaTypes[0],
|
||||
OperationsAliasType.RESPONSE,
|
||||
);
|
||||
|
@ -98,6 +104,8 @@ async function generateEndpoints(
|
|||
|
||||
const entitiesOutputLine: string[] = [];
|
||||
|
||||
entitiesOutputLine.push('/* eslint @typescript-eslint/naming-convention: 0 */');
|
||||
|
||||
entitiesOutputLine.push(`import { operations } from '${toImportPath(typeFileName)}';`);
|
||||
entitiesOutputLine.push('');
|
||||
|
||||
|
@ -138,12 +146,19 @@ async function generateApiClientJSDoc(
|
|||
endpointsFileName: string,
|
||||
warningsOutputPath: string,
|
||||
) {
|
||||
const endpoints: { operationId: string; description: string; }[] = [];
|
||||
const endpoints: {
|
||||
operationId: string;
|
||||
path: string;
|
||||
description: string;
|
||||
}[] = [];
|
||||
|
||||
// misskey-jsはPOST固定で送っているので、こちらも決め打ちする。別メソッドに対応することがあればこちらも直す必要あり
|
||||
const paths = openApiDocs.paths ?? {};
|
||||
const postPathItems = Object.keys(paths)
|
||||
.map(it => paths[it]?.post)
|
||||
.map(it => ({
|
||||
_path_: it.replace(/^\//, ''),
|
||||
...paths[it]?.post,
|
||||
}))
|
||||
.filter(filterUndefined);
|
||||
|
||||
for (const operation of postPathItems) {
|
||||
|
@ -153,6 +168,7 @@ async function generateApiClientJSDoc(
|
|||
if (operation.description) {
|
||||
endpoints.push({
|
||||
operationId: operationId,
|
||||
path: operation._path_,
|
||||
description: operation.description,
|
||||
});
|
||||
}
|
||||
|
@ -173,7 +189,7 @@ async function generateApiClientJSDoc(
|
|||
' /**',
|
||||
` * ${endpoint.description.split('\n').join('\n * ')}`,
|
||||
' */',
|
||||
` request<E extends '${endpoint.operationId}', P extends Endpoints[E][\'req\']>(`,
|
||||
` request<E extends '${endpoint.path}', P extends Endpoints[E][\'req\']>(`,
|
||||
' endpoint: E,',
|
||||
' params: P,',
|
||||
' credential?: string | null,',
|
||||
|
@ -232,21 +248,24 @@ interface IOperationTypeAlias {
|
|||
|
||||
class OperationTypeAlias implements IOperationTypeAlias {
|
||||
public readonly operationId: string;
|
||||
public readonly path: string;
|
||||
public readonly mediaType: string;
|
||||
public readonly type: OperationsAliasType;
|
||||
|
||||
constructor(
|
||||
operationId: string,
|
||||
path: string,
|
||||
mediaType: string,
|
||||
type: OperationsAliasType,
|
||||
) {
|
||||
this.operationId = operationId;
|
||||
this.path = path;
|
||||
this.mediaType = mediaType;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
generateName(): string {
|
||||
const nameBase = this.operationId.replace(/\//g, '-');
|
||||
const nameBase = this.path.replace(/\//g, '-');
|
||||
return toPascal(nameBase + this.type);
|
||||
}
|
||||
|
||||
|
@ -279,19 +298,19 @@ const emptyRequest = new EmptyTypeAlias(OperationsAliasType.REQUEST);
|
|||
const emptyResponse = new EmptyTypeAlias(OperationsAliasType.RESPONSE);
|
||||
|
||||
class Endpoint {
|
||||
public readonly operationId: string;
|
||||
public readonly path: string;
|
||||
public request?: IOperationTypeAlias;
|
||||
public response?: IOperationTypeAlias;
|
||||
|
||||
constructor(operationId: string) {
|
||||
this.operationId = operationId;
|
||||
constructor(path: string) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
toLine(): string {
|
||||
const reqName = this.request?.generateName() ?? emptyRequest.generateName();
|
||||
const resName = this.response?.generateName() ?? emptyResponse.generateName();
|
||||
|
||||
return `'${this.operationId}': { req: ${reqName}; res: ${resName} };`;
|
||||
return `'${this.path}': { req: ${reqName}; res: ${resName} };`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"type": "module",
|
||||
"name": "misskey-js",
|
||||
"version": "2024.3.1-host.3",
|
||||
"version": "2024.3.1-host.4",
|
||||
"description": "Misskey SDK for JavaScript",
|
||||
"types": "./built/dts/index.d.ts",
|
||||
"exports": {
|
||||
|
|
|
@ -3804,6 +3804,17 @@ declare module '../api.js' {
|
|||
credential?: string | null,
|
||||
): 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.
|
||||
*
|
||||
|
@ -3815,6 +3826,17 @@ declare module '../api.js' {
|
|||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *No*
|
||||
*/
|
||||
request<E extends 'users/get-skeb-status', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
|
|
|
@ -506,8 +506,12 @@ import type {
|
|||
UsersFollowingResponse,
|
||||
UsersGalleryPostsRequest,
|
||||
UsersGalleryPostsResponse,
|
||||
UsersGetFollowingBirthdayUsersRequest,
|
||||
UsersGetFollowingBirthdayUsersResponse,
|
||||
UsersGetFrequentlyRepliedUsersRequest,
|
||||
UsersGetFrequentlyRepliedUsersResponse,
|
||||
UsersGetSkebStatusRequest,
|
||||
UsersGetSkebStatusResponse,
|
||||
UsersFeaturedNotesRequest,
|
||||
UsersFeaturedNotesResponse,
|
||||
UsersListsCreateRequest,
|
||||
|
@ -916,7 +920,9 @@ export type Endpoints = {
|
|||
'users/followers': { req: UsersFollowersRequest; res: UsersFollowersResponse };
|
||||
'users/following': { req: UsersFollowingRequest; res: UsersFollowingResponse };
|
||||
'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-skeb-status': { req: UsersGetSkebStatusRequest; res: UsersGetSkebStatusResponse };
|
||||
'users/featured-notes': { req: UsersFeaturedNotesRequest; res: UsersFeaturedNotesResponse };
|
||||
'users/lists/create': { req: UsersListsCreateRequest; res: UsersListsCreateResponse };
|
||||
'users/lists/delete': { req: UsersListsDeleteRequest; res: EmptyResponse };
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue