Merge upstream
This commit is contained in:
commit
29c25555b8
8
locales/index.d.ts
vendored
8
locales/index.d.ts
vendored
@ -5516,6 +5516,14 @@ export interface Locale extends ILocale {
|
||||
* サーバー情報ページに表示するユーザーのIDを一行に一つずつ書きます。
|
||||
*/
|
||||
"endingCreditMembersDescription": string;
|
||||
/**
|
||||
* メールアドレスでログイン
|
||||
*/
|
||||
"emailAddressLogin": string;
|
||||
/**
|
||||
* ユーザー名でログイン
|
||||
*/
|
||||
"usernameLogin": string;
|
||||
"_bubbleGame": {
|
||||
/**
|
||||
* 遊び方
|
||||
|
@ -1372,6 +1372,8 @@ muteNotification: "通知をミュートする"
|
||||
unmuteNotification: "通知をミュート解除する"
|
||||
endingCreditMembers: "スタッフロールに表示するユーザー"
|
||||
endingCreditMembersDescription: "サーバー情報ページに表示するユーザーのIDを一行に一つずつ書きます。"
|
||||
emailAddressLogin: "メールアドレスでログイン"
|
||||
usernameLogin: "ユーザー名でログイン"
|
||||
|
||||
_bubbleGame:
|
||||
howToPlay: "遊び方"
|
||||
|
@ -48,11 +48,11 @@
|
||||
"resolutions": {
|
||||
"@tensorflow/tfjs-core": "4.22.0",
|
||||
"axios": "1.7.9",
|
||||
"chokidar": "4.0.2",
|
||||
"chokidar": "4.0.3",
|
||||
"cookie": "1.0.2",
|
||||
"cookie-signature": "1.2.2",
|
||||
"debug": "4.4.0",
|
||||
"esbuild": "0.24.0",
|
||||
"esbuild": "0.24.2",
|
||||
"jpeg-js": "0.4.4",
|
||||
"lodash": "4.17.21",
|
||||
"punycode": "npm:punycode.js@2.3.1",
|
||||
|
@ -63,8 +63,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@authenio/samlify-node-xmllint": "2.0.0",
|
||||
"@aws-sdk/client-s3": "3.714.0",
|
||||
"@aws-sdk/lib-storage": "3.714.0",
|
||||
"@aws-sdk/client-s3": "3.717.0",
|
||||
"@aws-sdk/lib-storage": "3.717.0",
|
||||
"@bull-board/api": "6.5.3",
|
||||
"@bull-board/fastify": "6.5.3",
|
||||
"@bull-board/ui": "6.5.3",
|
||||
@ -80,7 +80,7 @@
|
||||
"@fastify/static": "8.0.3",
|
||||
"@fastify/view": "10.0.1",
|
||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||
"@misskey-dev/summaly": "MisskeyIO/summaly#5.1.2",
|
||||
"@misskey-dev/summaly": "github:MisskeyIO/summaly#5.1.3",
|
||||
"@napi-rs/canvas": "0.1.65",
|
||||
"@nestjs/common": "10.4.15",
|
||||
"@nestjs/core": "10.4.15",
|
||||
@ -99,12 +99,12 @@
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.5",
|
||||
"body-parser": "1.20.3",
|
||||
"bullmq": "5.34.2",
|
||||
"bullmq": "5.34.4",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"cbor": "10.0.3",
|
||||
"chalk": "5.3.0",
|
||||
"chalk": "5.4.0",
|
||||
"chalk-template": "1.1.0",
|
||||
"chokidar": "4.0.2",
|
||||
"chokidar": "4.0.3",
|
||||
"cli-highlight": "2.1.11",
|
||||
"color-convert": "2.0.1",
|
||||
"content-disposition": "0.5.4",
|
||||
@ -122,7 +122,7 @@
|
||||
"hpagent": "1.2.0",
|
||||
"htmlescape": "1.1.1",
|
||||
"http-link-header": "1.1.3",
|
||||
"ioredis": "5.4.1",
|
||||
"ioredis": "5.4.2",
|
||||
"ip-cidr": "4.0.2",
|
||||
"ipaddr.js": "2.2.0",
|
||||
"is-svg": "5.1.0",
|
||||
@ -169,13 +169,13 @@
|
||||
"rss-parser": "3.13.0",
|
||||
"rxjs": "7.8.1",
|
||||
"samlify": "2.8.11",
|
||||
"sanitize-html": "2.13.1",
|
||||
"sanitize-html": "2.14.0",
|
||||
"secure-json-parse": "3.0.1",
|
||||
"sharp": "0.33.5",
|
||||
"slacc": "0.0.10",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"systeminformation": "5.23.13",
|
||||
"systeminformation": "5.23.14",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.3",
|
||||
"tsc-alias": "1.8.10",
|
||||
|
@ -389,6 +389,7 @@ import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by
|
||||
import * as ep___users_search from './endpoints/users/search.js';
|
||||
import * as ep___users_show from './endpoints/users/show.js';
|
||||
import * as ep___users_stats from './endpoints/users/stats.js';
|
||||
import * as ep___users_get_security_info from './endpoints/users/get-security-info.js';
|
||||
import * as ep___users_achievements from './endpoints/users/achievements.js';
|
||||
import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
|
||||
import * as ep___fetchRss from './endpoints/fetch-rss.js';
|
||||
@ -765,6 +766,7 @@ const $users_following: Provider = { provide: 'ep:users/following', useClass: ep
|
||||
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_getSecurityInfo: Provider = { provide: 'ep:users/get-security-info', useClass: ep___users_get_security_info.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 };
|
||||
@ -1170,6 +1172,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||
$users_gallery_posts,
|
||||
$users_getFollowingBirthdayUsers,
|
||||
$users_getFrequentlyRepliedUsers,
|
||||
$users_getSecurityInfo,
|
||||
$users_getSkebStatus,
|
||||
$users_featuredNotes,
|
||||
$users_lists_create,
|
||||
@ -1567,6 +1570,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||
$users_gallery_posts,
|
||||
$users_getFollowingBirthdayUsers,
|
||||
$users_getFrequentlyRepliedUsers,
|
||||
$users_getSecurityInfo,
|
||||
$users_getSkebStatus,
|
||||
$users_featuredNotes,
|
||||
$users_lists_create,
|
||||
|
@ -3,11 +3,13 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type {
|
||||
MiUserProfile,
|
||||
SigninsRepository,
|
||||
UserProfilesRepository,
|
||||
UsersRepository,
|
||||
@ -27,7 +29,6 @@ import { RateLimiterService } from './RateLimiterService.js';
|
||||
import { SigninService } from './SigninService.js';
|
||||
import type { AuthenticationResponseJSON } from '@simplewebauthn/server';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
@Injectable()
|
||||
export class SigninApiService {
|
||||
@ -122,22 +123,34 @@ export class SigninApiService {
|
||||
}
|
||||
|
||||
// Fetch user
|
||||
const user = await this.usersRepository.findOneBy({
|
||||
usernameLower: username.toLowerCase(),
|
||||
host: IsNull(),
|
||||
}) as MiLocalUser;
|
||||
const profile = await this.userProfilesRepository.findOne({
|
||||
relations: ['user'],
|
||||
where: username.includes('@') ? {
|
||||
email: username,
|
||||
emailVerified: true,
|
||||
user: {
|
||||
host: IsNull(),
|
||||
}
|
||||
} : {
|
||||
user: {
|
||||
usernameLower: username.toLowerCase(),
|
||||
host: IsNull(),
|
||||
}
|
||||
}
|
||||
});
|
||||
const user = (profile?.user as MiLocalUser) ?? null;
|
||||
|
||||
if (user == null) {
|
||||
if (!user || !profile) {
|
||||
logger.error('No such user.');
|
||||
return error(404, {
|
||||
id: '6cc579cc-885d-43d8-95c2-b8c7fc963280',
|
||||
return error(403, {
|
||||
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
|
||||
});
|
||||
}
|
||||
|
||||
if (user.isDeleted && user.isSuspended) {
|
||||
logger.error('No such user. (logical deletion)');
|
||||
return error(404, {
|
||||
id: '6cc579cc-885d-43d8-95c2-b8c7fc963280',
|
||||
return error(403, {
|
||||
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
|
||||
});
|
||||
}
|
||||
|
||||
@ -148,8 +161,6 @@ export class SigninApiService {
|
||||
});
|
||||
}
|
||||
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(password, profile.password!);
|
||||
|
||||
|
@ -388,6 +388,7 @@ import * as ep___users_reportAbuse from './endpoints/users/report-abuse.js';
|
||||
import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js';
|
||||
import * as ep___users_search from './endpoints/users/search.js';
|
||||
import * as ep___users_show from './endpoints/users/show.js';
|
||||
import * as ep___users_get_security_info from './endpoints/users/get-security-info.js';
|
||||
import * as ep___users_stats from './endpoints/users/stats.js';
|
||||
import * as ep___users_achievements from './endpoints/users/achievements.js';
|
||||
import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
|
||||
@ -787,6 +788,7 @@ const eps = [
|
||||
['users/search-by-username-and-host', ep___users_searchByUsernameAndHost],
|
||||
['users/search', ep___users_search],
|
||||
['users/show', ep___users_show],
|
||||
['users/get-security-info', ep___users_get_security_info],
|
||||
['users/stats', ep___users_stats],
|
||||
['users/achievements', ep___users_achievements],
|
||||
['users/update-memo', ep___users_updateMemo],
|
||||
|
@ -11,6 +11,8 @@ export const meta = {
|
||||
tags: ['meta'],
|
||||
|
||||
requireCredential: false,
|
||||
allowGet: true,
|
||||
cacheSec: 60,
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
|
@ -12,6 +12,8 @@ import UsersChart from '@/core/chart/charts/users.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: false,
|
||||
allowGet: true,
|
||||
cacheSec: 60,
|
||||
|
||||
tags: ['meta'],
|
||||
|
||||
|
@ -0,0 +1,72 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import ms from 'ms';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
|
||||
requireCredential: false,
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 30,
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
twoFactorEnabled: { type: 'boolean' },
|
||||
usePasswordLessLogin: { type: 'boolean' },
|
||||
securityKeys: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
email: { type: 'string' },
|
||||
password: { type: 'string' },
|
||||
},
|
||||
required: ['email', 'password'],
|
||||
} 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.userSecurityKeysRepository)
|
||||
private userSecurityKeysRepository: UserSecurityKeysRepository,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const profile = await this.userProfilesRepository.findOneBy({
|
||||
email: ps.email,
|
||||
emailVerified: true,
|
||||
});
|
||||
|
||||
const passwordMatched = await bcrypt.compare(ps.password, profile?.password ?? '');
|
||||
if (!profile || !passwordMatched) {
|
||||
return {
|
||||
twoFactorEnabled: false,
|
||||
usePasswordLessLogin: false,
|
||||
securityKeys: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
twoFactorEnabled: profile.twoFactorEnabled,
|
||||
usePasswordLessLogin: profile.usePasswordLessLogin,
|
||||
securityKeys: profile.twoFactorEnabled
|
||||
? await this.userSecurityKeysRepository.countBy({ userId: profile.userId }).then(result => result >= 1)
|
||||
: false,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ class AntennaChannel extends Channel {
|
||||
public static readonly requireCredential = true as const;
|
||||
public static readonly kind = 'read:account';
|
||||
private antennaId: string;
|
||||
private idOnly: boolean;
|
||||
|
||||
constructor(
|
||||
private noteEntityService: NoteEntityService,
|
||||
@ -29,6 +30,7 @@ class AntennaChannel extends Channel {
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
this.antennaId = params.antennaId as string;
|
||||
this.idOnly = params.idOnly ?? false;
|
||||
|
||||
// Subscribe stream
|
||||
this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent);
|
||||
@ -57,9 +59,13 @@ class AntennaChannel extends Channel {
|
||||
if (this.isNoteMutedOrBlocked(note.reply)) return;
|
||||
}
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
this.send('note', note);
|
||||
if (this.idOnly && ['public', 'home'].includes(note.visibility)) {
|
||||
const idOnlyNote = { id: note.id };
|
||||
this.send('note', idOnlyNote);
|
||||
} else {
|
||||
this.connection.cacheNote(note);
|
||||
this.send('note', note);
|
||||
}
|
||||
} else {
|
||||
this.send(data.type, data.body);
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ class ChannelChannel extends Channel {
|
||||
public static readonly shouldShare = false;
|
||||
public static readonly requireCredential = false as const;
|
||||
private channelId: string;
|
||||
private idOnly: boolean;
|
||||
|
||||
constructor(
|
||||
private noteEntityService: NoteEntityService,
|
||||
@ -29,6 +30,7 @@ class ChannelChannel extends Channel {
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
this.channelId = params.channelId as string;
|
||||
this.idOnly = params.idOnly ?? false;
|
||||
|
||||
// Subscribe stream
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
@ -63,9 +65,13 @@ class ChannelChannel extends Channel {
|
||||
}
|
||||
}
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
this.send('note', note);
|
||||
if (this.idOnly && ['public', 'home'].includes(note.visibility)) {
|
||||
const idOnlyNote = { id: note.id };
|
||||
this.send('note', idOnlyNote);
|
||||
} else {
|
||||
this.connection.cacheNote(note);
|
||||
this.send('note', note);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -18,6 +18,7 @@ class GlobalTimelineChannel extends Channel {
|
||||
public static readonly requireCredential = false as const;
|
||||
private withRenotes: boolean;
|
||||
private withFiles: boolean;
|
||||
private idOnly: boolean;
|
||||
|
||||
constructor(
|
||||
private metaService: MetaService,
|
||||
@ -38,6 +39,7 @@ class GlobalTimelineChannel extends Channel {
|
||||
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
this.withFiles = params.withFiles ?? false;
|
||||
this.idOnly = params.idOnly ?? false;
|
||||
|
||||
// Subscribe events
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
@ -93,9 +95,13 @@ class GlobalTimelineChannel extends Channel {
|
||||
}
|
||||
}
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
this.send('note', note);
|
||||
if (this.idOnly && ['public', 'home'].includes(note.visibility)) {
|
||||
const idOnlyNote = { id: note.id };
|
||||
this.send('note', idOnlyNote);
|
||||
} else {
|
||||
this.connection.cacheNote(note);
|
||||
this.send('note', note);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -17,6 +17,7 @@ class HomeTimelineChannel extends Channel {
|
||||
public static readonly kind = 'read:account';
|
||||
private withRenotes: boolean;
|
||||
private withFiles: boolean;
|
||||
private idOnly: boolean;
|
||||
|
||||
constructor(
|
||||
private noteEntityService: NoteEntityService,
|
||||
@ -32,6 +33,7 @@ class HomeTimelineChannel extends Channel {
|
||||
public async init(params: any) {
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
this.withFiles = params.withFiles ?? false;
|
||||
this.idOnly = params.idOnly ?? false;
|
||||
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
}
|
||||
@ -94,9 +96,13 @@ class HomeTimelineChannel extends Channel {
|
||||
}
|
||||
}
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
this.send('note', note);
|
||||
if (this.idOnly && ['public', 'home'].includes(note.visibility)) {
|
||||
const idOnlyNote = { id: note.id };
|
||||
this.send('note', idOnlyNote);
|
||||
} else {
|
||||
this.connection.cacheNote(note);
|
||||
this.send('note', note);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -20,6 +20,7 @@ class HybridTimelineChannel extends Channel {
|
||||
private withRenotes: boolean;
|
||||
private withReplies: boolean;
|
||||
private withFiles: boolean;
|
||||
private idOnly: boolean;
|
||||
|
||||
constructor(
|
||||
private metaService: MetaService,
|
||||
@ -41,6 +42,7 @@ class HybridTimelineChannel extends Channel {
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
this.withReplies = params.withReplies ?? false;
|
||||
this.withFiles = params.withFiles ?? false;
|
||||
this.idOnly = params.idOnly ?? false;
|
||||
|
||||
// Subscribe events
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
@ -110,9 +112,13 @@ class HybridTimelineChannel extends Channel {
|
||||
}
|
||||
}
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
this.send('note', note);
|
||||
if (this.idOnly && ['public', 'home'].includes(note.visibility)) {
|
||||
const idOnlyNote = { id: note.id };
|
||||
this.send('note', idOnlyNote);
|
||||
} else {
|
||||
this.connection.cacheNote(note);
|
||||
this.send('note', note);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -19,6 +19,7 @@ class LocalTimelineChannel extends Channel {
|
||||
private withRenotes: boolean;
|
||||
private withReplies: boolean;
|
||||
private withFiles: boolean;
|
||||
private idOnly: boolean;
|
||||
|
||||
constructor(
|
||||
private metaService: MetaService,
|
||||
@ -40,6 +41,7 @@ class LocalTimelineChannel extends Channel {
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
this.withReplies = params.withReplies ?? false;
|
||||
this.withFiles = params.withFiles ?? false;
|
||||
this.idOnly = params.idOnly ?? false;
|
||||
|
||||
// Subscribe events
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
@ -93,9 +95,13 @@ class LocalTimelineChannel extends Channel {
|
||||
}
|
||||
}
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
this.send('note', note);
|
||||
if (this.idOnly && ['public', 'home'].includes(note.visibility)) {
|
||||
const idOnlyNote = { id: note.id };
|
||||
this.send('note', idOnlyNote);
|
||||
} else {
|
||||
this.connection.cacheNote(note);
|
||||
this.send('note', note);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -16,6 +16,7 @@ class RoleTimelineChannel extends Channel {
|
||||
public static readonly shouldShare = false;
|
||||
public static readonly requireCredential = false as const;
|
||||
private roleId: string;
|
||||
private idOnly: boolean;
|
||||
|
||||
constructor(
|
||||
private noteEntityService: NoteEntityService,
|
||||
@ -31,6 +32,7 @@ class RoleTimelineChannel extends Channel {
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
this.roleId = params.roleId as string;
|
||||
this.idOnly = params.idOnly ?? false;
|
||||
|
||||
this.subscriber.on(`roleTimelineStream:${this.roleId}`, this.onEvent);
|
||||
}
|
||||
@ -79,9 +81,13 @@ class RoleTimelineChannel extends Channel {
|
||||
}
|
||||
}
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
this.send('note', note);
|
||||
if (this.idOnly && ['public', 'home'].includes(note.visibility)) {
|
||||
const idOnlyNote = { id: note.id };
|
||||
this.send('note', idOnlyNote);
|
||||
} else {
|
||||
this.connection.cacheNote(note);
|
||||
this.send('note', note);
|
||||
}
|
||||
} else {
|
||||
this.send(data.type, data.body);
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ class UserListChannel extends Channel {
|
||||
private listUsersClock: NodeJS.Timeout;
|
||||
private withFiles: boolean;
|
||||
private withRenotes: boolean;
|
||||
private idOnly: boolean;
|
||||
|
||||
constructor(
|
||||
private userListsRepository: UserListsRepository,
|
||||
@ -40,6 +41,7 @@ class UserListChannel extends Channel {
|
||||
this.listId = params.listId as string;
|
||||
this.withFiles = params.withFiles ?? false;
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
this.idOnly = params.idOnly ?? false;
|
||||
|
||||
// Check existence and owner
|
||||
const listExist = await this.userListsRepository.exists({
|
||||
@ -128,9 +130,13 @@ class UserListChannel extends Channel {
|
||||
}
|
||||
}
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
this.send('note', note);
|
||||
if (this.idOnly && ['public', 'home'].includes(note.visibility)) {
|
||||
const idOnlyNote = { id: note.id };
|
||||
this.send('note', idOnlyNote);
|
||||
} else {
|
||||
this.connection.cacheNote(note);
|
||||
this.send('note', note);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -616,6 +616,81 @@ export class ClientServerService {
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<{ Params: { note: string; } }>('/notes/:note.json', async (request, reply) => {
|
||||
const note = await this.notesRepository.findOneBy({
|
||||
id: request.params.note,
|
||||
visibility: In(['public', 'home']),
|
||||
});
|
||||
|
||||
if (note) {
|
||||
try {
|
||||
const _note = await this.noteEntityService.pack(note, null);
|
||||
reply.header('Content-Type', 'application/json; charset=utf-8');
|
||||
reply.header('Cache-Control', 'public, max-age=600');
|
||||
return reply.send(_note);
|
||||
} catch (err) {
|
||||
reply.header('Cache-Control', 'max-age=10, must-revalidate');
|
||||
if (err instanceof IdentifiableError) {
|
||||
this.clientLoggerService.logger.error(`Internal error occurred in ${request.routeOptions.url}: ${err.message}`, {
|
||||
path: request.routeOptions.url,
|
||||
params: request.params,
|
||||
query: request.query,
|
||||
id: err.id,
|
||||
error: {
|
||||
message: err.message,
|
||||
code: 'INTERNAL_ERROR',
|
||||
stack: err.stack,
|
||||
},
|
||||
});
|
||||
const httpStatusCode = err.id === '85ab9bd7-3a41-4530-959d-f07073900109' ? 403 : 500;
|
||||
reply.code(httpStatusCode);
|
||||
return reply.send({
|
||||
message: err.message,
|
||||
code: 'INTERNAL_ERROR',
|
||||
id: err.id,
|
||||
kind: 'server',
|
||||
httpStatusCode,
|
||||
info: {
|
||||
message: err.message,
|
||||
code: err.name,
|
||||
id: err.id,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const error = err as Error;
|
||||
const errId = randomUUID();
|
||||
this.clientLoggerService.logger.error(`Internal error occurred in ${request.routeOptions.url}: ${error.message}`, {
|
||||
path: request.routeOptions.url,
|
||||
params: request.params,
|
||||
query: request.query,
|
||||
id: errId,
|
||||
error: {
|
||||
message: error.message,
|
||||
code: error.name,
|
||||
stack: error.stack,
|
||||
},
|
||||
});
|
||||
reply.code(500);
|
||||
return reply.send({
|
||||
message: 'Internal error occurred. Please contact us if the error persists.',
|
||||
code: 'INTERNAL_ERROR',
|
||||
id: 'b9f2a7f9-fe64-434b-9484-cb1f804d1a80',
|
||||
kind: 'server',
|
||||
httpStatusCode: 500,
|
||||
info: {
|
||||
message: error.message,
|
||||
code: error.name,
|
||||
id: errId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
reply.code(404);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Page
|
||||
fastify.get<{ Params: { user: string; page: string; } }>('/@:user/pages/:page', async (request, reply) => {
|
||||
const { username, host } = Acct.parse(request.params.user);
|
||||
|
@ -32,25 +32,10 @@
|
||||
renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.')
|
||||
}
|
||||
|
||||
const metaRes = await window.fetch('/api/meta', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({}),
|
||||
credentials: 'omit',
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (metaRes.status !== 200) {
|
||||
renderError('META_FETCH');
|
||||
return;
|
||||
}
|
||||
const meta = await metaRes.json();
|
||||
const v = meta.version;
|
||||
if (v == null) {
|
||||
renderError('META_FETCH_V');
|
||||
return;
|
||||
}
|
||||
if (localStorage.getItem('id') === null) {
|
||||
localStorage.setItem('id', crypto.randomUUID().replaceAll('-', ''));
|
||||
}
|
||||
let id = localStorage.getItem('id');
|
||||
|
||||
//#region Detect language & fetch translations
|
||||
if (!localStorage.hasOwn('locale')) {
|
||||
@ -73,6 +58,25 @@
|
||||
lang = 'ko-KR';
|
||||
}
|
||||
|
||||
const metaRes = await window.fetch('/api/meta', {
|
||||
method: 'GET',
|
||||
credentials: 'omit',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Client-Transaction-Id': `${id}-misskey-${crypto.randomUUID().replaceAll('-', '')}`
|
||||
},
|
||||
});
|
||||
if (metaRes.status !== 200) {
|
||||
renderError('META_FETCH');
|
||||
return;
|
||||
}
|
||||
const meta = await metaRes.json();
|
||||
const v = meta.version;
|
||||
if (v == null) {
|
||||
renderError('META_FETCH_V');
|
||||
return;
|
||||
}
|
||||
|
||||
const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`);
|
||||
if (localRes.status === 200) {
|
||||
localStorage.setItem('lang', lang);
|
||||
|
@ -400,7 +400,7 @@ export const waitFire = async <C extends keyof misskey.Channels>(user: UserToken
|
||||
if (timer) clearTimeout(timer);
|
||||
res(true);
|
||||
}
|
||||
}, params);
|
||||
}, { ...params, idOnly: false });
|
||||
} catch (e) {
|
||||
rej(e);
|
||||
}
|
||||
|
@ -31,7 +31,7 @@
|
||||
"@twemoji/parser": "15.1.1",
|
||||
"@vitejs/plugin-vue": "5.2.1",
|
||||
"@vue/compiler-sfc": "3.5.13",
|
||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.9",
|
||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.13",
|
||||
"astring": "1.9.0",
|
||||
"broadcast-channel": "7.0.0",
|
||||
"buraha": "0.0.1",
|
||||
@ -59,10 +59,10 @@
|
||||
"misskey-reversi": "workspace:*",
|
||||
"photoswipe": "5.4.4",
|
||||
"punycode.js": "2.3.1",
|
||||
"rollup": "4.28.1",
|
||||
"sanitize-html": "2.13.1",
|
||||
"rollup": "4.29.1",
|
||||
"sanitize-html": "2.14.0",
|
||||
"sass": "1.83.0",
|
||||
"shiki": "1.24.2",
|
||||
"shiki": "1.24.3",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.171.0",
|
||||
@ -73,7 +73,7 @@
|
||||
"typescript": "5.7.2",
|
||||
"uuid": "11.0.3",
|
||||
"v-code-diff": "1.13.1",
|
||||
"vite": "6.0.3",
|
||||
"vite": "6.0.5",
|
||||
"vue": "3.5.13",
|
||||
"vue-gtag": "2.0.1",
|
||||
"vuedraggable": "next",
|
||||
@ -81,7 +81,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||
"@misskey-dev/summaly": "MisskeyIO/summaly#5.1.2",
|
||||
"@misskey-dev/summaly": "github:MisskeyIO/summaly#5.1.3",
|
||||
"@storybook/addon-actions": "8.4.7",
|
||||
"@storybook/addon-essentials": "8.4.7",
|
||||
"@storybook/addon-interactions": "8.4.7",
|
||||
|
@ -5,16 +5,16 @@
|
||||
|
||||
import { defineAsyncComponent, reactive, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { MenuButton } from '@/types/menu.js';
|
||||
import { set as gtagSet, time as gtagTime } from 'vue-gtag';
|
||||
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { MenuButton } from '@/types/menu.js';
|
||||
import { del, get, set } from '@/scripts/idb-proxy.js';
|
||||
import { apiUrl } from '@/config.js';
|
||||
import { waiting, popup, popupMenu, success, alert } from '@/os.js';
|
||||
import { generateClientTransactionId, misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js';
|
||||
import { set as gtagSet, time as gtagTime } from 'vue-gtag';
|
||||
import { instance } from '@/instance.js';
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
@ -63,10 +63,6 @@ export async function common(createVue: () => App<Element>) {
|
||||
});
|
||||
}
|
||||
|
||||
if (miLocalStorage.getItem('id') === null) {
|
||||
miLocalStorage.setItem('id', crypto.randomUUID());
|
||||
}
|
||||
|
||||
let isClientUpdated = false;
|
||||
|
||||
//#region クライアントが更新されたかチェック
|
||||
|
@ -6,24 +6,32 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template>
|
||||
<form :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
|
||||
<div class="_gaps_m">
|
||||
<div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : undefined, marginBottom: message ? '1.5em' : undefined }"></div>
|
||||
<div v-show="withAvatar && !loginWithEmailAddress" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : undefined, marginBottom: message ? '1.5em' : undefined }"></div>
|
||||
<MkInfo v-if="message">
|
||||
{{ message }}
|
||||
</MkInfo>
|
||||
<div v-if="!totpLogin" class="normal-signin _gaps_m">
|
||||
<MkInput v-model="username" :debounce="true" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
|
||||
<template #prefix>@</template>
|
||||
<template #suffix>@{{ host }}</template>
|
||||
<MkInput v-model="username" :debounce="true" :placeholder="loginWithEmailAddress ? i18n.ts.emailAddress : i18n.ts.username" type="text" :pattern="loginWithEmailAddress ? '^[a-zA-Z0-9_@.]+$' : '^[a-zA-Z0-9_]+$'" :spellcheck="false" :autocomplete="loginWithEmailAddress ? 'email webauthn' : 'username webauthn'" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
|
||||
<template #prefix>
|
||||
<i v-if="loginWithEmailAddress" class="ti ti-mail"></i>
|
||||
<span v-else>@</span>
|
||||
</template>
|
||||
<template v-if="!loginWithEmailAddress" #suffix>@{{ host }}</template>
|
||||
<template #caption>
|
||||
<button class="_textButton" type="button" tabindex="-1" @click="loginWithEmailAddress = !loginWithEmailAddress">{{ loginWithEmailAddress ? i18n.ts.usernameLogin : i18n.ts.emailAddressLogin }}</button>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required data-cy-signin-password>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
|
||||
<template #caption>
|
||||
<button class="_textButton" type="button" tabindex="-1" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkCaptcha v-if="!user?.twoFactorEnabled && instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="!user?.twoFactorEnabled && instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
|
||||
<MkCaptcha v-if="!user?.twoFactorEnabled && instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="!user?.twoFactorEnabled && instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
|
||||
<MkButton type="submit" large primary rounded :disabled="!user || captchaFailed || signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
|
||||
<MkButton type="submit" large primary rounded :disabled="(!loginWithEmailAddress && !user) || captchaFailed || signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
|
||||
</div>
|
||||
<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
|
||||
<div v-if="user && user.securityKeys" class="twofa-group tap-group">
|
||||
@ -70,6 +78,7 @@ import { instance } from '@/instance.js';
|
||||
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
|
||||
|
||||
const signing = ref(false);
|
||||
const loginWithEmailAddress = ref(false);
|
||||
const userAbortController = ref<AbortController>();
|
||||
const user = ref<Misskey.entities.UserDetailed | null>(null);
|
||||
const username = ref('');
|
||||
@ -119,7 +128,9 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
function onUsernameChange(): void {
|
||||
async function onUsernameChange(): Promise<void> {
|
||||
if (loginWithEmailAddress.value) return;
|
||||
|
||||
if (userAbortController.value) {
|
||||
userAbortController.value.abort();
|
||||
}
|
||||
@ -168,8 +179,15 @@ async function queryKey(): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
function onSubmit(): void {
|
||||
async function onSubmit(): Promise<void> {
|
||||
signing.value = true;
|
||||
if (loginWithEmailAddress.value) {
|
||||
user.value = await misskeyApi('users/get-security-info', {
|
||||
email: username.value,
|
||||
password: password.value,
|
||||
});
|
||||
}
|
||||
|
||||
if (!totpLogin.value && user.value?.twoFactorEnabled) {
|
||||
if (webAuthnSupported() && user.value.securityKeys) {
|
||||
misskeyApi('signin', {
|
||||
|
@ -27,7 +27,7 @@ import { i18n } from '@/i18n.js';
|
||||
|
||||
withDefaults(defineProps<{
|
||||
autoSet?: boolean;
|
||||
message?: string,
|
||||
message?: string;
|
||||
}>(), {
|
||||
autoSet: false,
|
||||
message: '',
|
||||
|
@ -18,15 +18,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, onUnmounted, provide, shallowRef } from 'vue';
|
||||
import { time as gtagTime } from 'vue-gtag';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
||||
import { useStream } from '@/stream.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { $i, iAmModerator } from '@/account.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { Paging } from '@/components/MkPagination.vue';
|
||||
import { generateClientTransactionId } from '@/scripts/misskey-api.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
src: 'home' | 'local' | 'media' | 'social' | 'global' | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
|
||||
@ -68,9 +70,36 @@ const tlComponent = shallowRef<InstanceType<typeof MkNotes>>();
|
||||
|
||||
let tlNotesCount = 0;
|
||||
|
||||
function prepend(note) {
|
||||
async function prepend(data) {
|
||||
if (tlComponent.value == null) return;
|
||||
|
||||
let note = data;
|
||||
|
||||
// チェックするプロパティはなんでも良い
|
||||
// idOnlyが有効でid以外が存在しない場合は取得する
|
||||
if (!data.visibility) {
|
||||
const initiateTime = Date.now();
|
||||
const res = await window.fetch(`/notes/${data.id}.json`, {
|
||||
method: 'GET',
|
||||
credentials: 'omit',
|
||||
headers: {
|
||||
'Authorization': 'anonymous',
|
||||
'X-Client-Transaction-Id': generateClientTransactionId('misskey'),
|
||||
},
|
||||
}).then(res => {
|
||||
if (instance.googleAnalyticsId) {
|
||||
gtagTime({
|
||||
name: 'api-get',
|
||||
event_category: `/notes/${data.id}.json`,
|
||||
value: Date.now() - initiateTime,
|
||||
});
|
||||
}
|
||||
return res;
|
||||
});
|
||||
if (!res.ok) return;
|
||||
note = await res.json();
|
||||
}
|
||||
|
||||
tlNotesCount++;
|
||||
|
||||
if (instance.notesPerOneAd > 0 && tlNotesCount % instance.notesPerOneAd === 0) {
|
||||
@ -89,6 +118,7 @@ function prepend(note) {
|
||||
let connection: Misskey.ChannelConnection | null = null;
|
||||
let connection2: Misskey.ChannelConnection | null = null;
|
||||
let paginationQuery: Paging | null = null;
|
||||
const idOnly = !iAmModerator;
|
||||
|
||||
const stream = useStream();
|
||||
|
||||
@ -97,11 +127,13 @@ function connectChannel() {
|
||||
if (props.antenna == null) return;
|
||||
connection = stream.useChannel('antenna', {
|
||||
antennaId: props.antenna,
|
||||
idOnly: idOnly,
|
||||
});
|
||||
} else if (props.src === 'home') {
|
||||
connection = stream.useChannel('homeTimeline', {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
idOnly: idOnly,
|
||||
});
|
||||
connection2 = stream.useChannel('main');
|
||||
} else if (props.src === 'local') {
|
||||
@ -109,23 +141,27 @@ function connectChannel() {
|
||||
withRenotes: props.withRenotes,
|
||||
withReplies: props.withReplies,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
idOnly: idOnly,
|
||||
});
|
||||
} else if (props.src === 'media') {
|
||||
connection = stream.useChannel('hybridTimeline', {
|
||||
withRenotes: props.withRenotes,
|
||||
withReplies: props.withReplies,
|
||||
withFiles: true,
|
||||
idOnly: idOnly,
|
||||
});
|
||||
} else if (props.src === 'social') {
|
||||
connection = stream.useChannel('hybridTimeline', {
|
||||
withRenotes: props.withRenotes,
|
||||
withReplies: props.withReplies,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
idOnly: idOnly,
|
||||
});
|
||||
} else if (props.src === 'global') {
|
||||
connection = stream.useChannel('globalTimeline', {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
idOnly: idOnly,
|
||||
});
|
||||
} else if (props.src === 'mentions') {
|
||||
connection = stream.useChannel('main');
|
||||
@ -144,16 +180,19 @@ function connectChannel() {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
listId: props.list,
|
||||
idOnly: idOnly,
|
||||
});
|
||||
} else if (props.src === 'channel') {
|
||||
if (props.channel == null) return;
|
||||
connection = stream.useChannel('channel', {
|
||||
channelId: props.channel,
|
||||
idOnly: idOnly,
|
||||
});
|
||||
} else if (props.src === 'role') {
|
||||
if (props.role == null) return;
|
||||
connection = stream.useChannel('roleTimeline', {
|
||||
roleId: props.role,
|
||||
idOnly: idOnly,
|
||||
});
|
||||
}
|
||||
if (props.src !== 'directs' && props.src !== 'mentions') connection?.on('note', prepend);
|
||||
|
@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { onMounted, shallowRef, ref, nextTick } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
|
||||
import { chartVLine } from '@/scripts/chart-vline.js';
|
||||
@ -52,7 +52,7 @@ async function renderChart() {
|
||||
}));
|
||||
};
|
||||
|
||||
const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' });
|
||||
const raw = await misskeyApiGet('charts/active-users', { limit: chartLimit, span: 'day' });
|
||||
|
||||
fetching.value = false;
|
||||
|
||||
|
@ -81,7 +81,7 @@ import MkTimeline from '@/components/MkTimeline.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { instanceName } from '@/config.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
@ -91,7 +91,7 @@ import { openInstanceMenu } from '@/ui/_common_/common.js';
|
||||
|
||||
const stats = ref<Misskey.entities.StatsResponse | null>(null);
|
||||
|
||||
misskeyApi('stats', {}).then((res) => {
|
||||
misskeyApiGet('stats').then((res) => {
|
||||
stats.value = res;
|
||||
});
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
import { computed, reactive } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERROR_IMAGE_URL } from '@/const.js';
|
||||
|
||||
@ -47,7 +47,7 @@ export async function fetchInstance(force = false): Promise<Misskey.entities.Met
|
||||
}
|
||||
}
|
||||
|
||||
const meta = await misskeyApi('meta', {
|
||||
const meta = await misskeyApiGet('meta', {
|
||||
detail: true,
|
||||
});
|
||||
|
||||
|
@ -196,7 +196,7 @@ import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkInstanceStats from '@/components/MkInstanceStats.vue';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
import number from '@/filters/number.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
@ -229,8 +229,7 @@ watch(tab, () => {
|
||||
}
|
||||
});
|
||||
|
||||
const initStats = () => misskeyApi('stats', {
|
||||
}).then((res) => {
|
||||
const initStats = () => misskeyApiGet('stats').then((res) => {
|
||||
stats.value = res;
|
||||
});
|
||||
|
||||
|
@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
import MkNumberDiff from '@/components/MkNumberDiff.vue';
|
||||
import MkNumber from '@/components/MkNumber.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
@ -78,7 +78,7 @@ const fetching = ref(true);
|
||||
|
||||
onMounted(async () => {
|
||||
const [_stats, _onlineUsersCount] = await Promise.all([
|
||||
misskeyApi('stats', {}),
|
||||
misskeyApiGet('stats'),
|
||||
misskeyApiGet('get-online-users-count').then(res => res.count),
|
||||
]);
|
||||
stats.value = _stats;
|
||||
|
@ -15,11 +15,17 @@ export const pendingApiRequestsCount = ref(0);
|
||||
let id: string | null = miLocalStorage.getItem('id');
|
||||
export function generateClientTransactionId(initiator: string) {
|
||||
if (id === null) {
|
||||
id = crypto.randomUUID();
|
||||
id = crypto.randomUUID().replaceAll('-', '');
|
||||
miLocalStorage.setItem('id', id);
|
||||
}
|
||||
|
||||
return `${id}-${initiator}-${crypto.randomUUID()}`;
|
||||
// ハイフンが含まれている場合は除去
|
||||
if (id.includes('-')) {
|
||||
id = id.replaceAll('-', '');
|
||||
miLocalStorage.setItem('id', id);
|
||||
}
|
||||
|
||||
return `${id}-${initiator}-${crypto.randomUUID().replaceAll('-', '')}`;
|
||||
}
|
||||
|
||||
function handleResponse<_ResT>(
|
||||
@ -58,24 +64,22 @@ export function misskeyApi<
|
||||
if (endpoint.includes('://')) throw new Error('invalid endpoint');
|
||||
pendingApiRequestsCount.value++;
|
||||
|
||||
const credential = token ? token : $i ? $i.token : undefined;
|
||||
|
||||
const onFinally = () => {
|
||||
pendingApiRequestsCount.value--;
|
||||
};
|
||||
|
||||
const promise = new Promise<_ResT>((resolve, reject) => {
|
||||
// Append a credential
|
||||
if ($i) (data as any).i = $i.token;
|
||||
if (token !== undefined) (data as any).i = token;
|
||||
|
||||
// Send request
|
||||
const initiateTime = Date.now();
|
||||
window.fetch(`${apiUrl}/${endpoint}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'omit',
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': credential ? `Bearer ${credential}` : 'anonymous',
|
||||
'X-Client-Transaction-Id': generateClientTransactionId(initiator),
|
||||
},
|
||||
signal,
|
||||
@ -121,8 +125,8 @@ export function misskeyApiGet<
|
||||
window.fetch(`${apiUrl}/${endpoint}?${query}`, {
|
||||
method: 'GET',
|
||||
credentials: 'omit',
|
||||
cache: 'default',
|
||||
headers: {
|
||||
'Authorization': 'anonymous',
|
||||
'X-Client-Transaction-Id': generateClientTransactionId(initiator),
|
||||
},
|
||||
}).then(res => {
|
||||
|
@ -648,6 +648,7 @@ export type Channels = {
|
||||
params: {
|
||||
withRenotes?: boolean;
|
||||
withFiles?: boolean;
|
||||
idOnly?: boolean;
|
||||
};
|
||||
events: {
|
||||
note: (payload: Note) => void;
|
||||
@ -659,6 +660,7 @@ export type Channels = {
|
||||
withRenotes?: boolean;
|
||||
withReplies?: boolean;
|
||||
withFiles?: boolean;
|
||||
idOnly?: boolean;
|
||||
};
|
||||
events: {
|
||||
note: (payload: Note) => void;
|
||||
@ -670,6 +672,7 @@ export type Channels = {
|
||||
withRenotes?: boolean;
|
||||
withReplies?: boolean;
|
||||
withFiles?: boolean;
|
||||
idOnly?: boolean;
|
||||
};
|
||||
events: {
|
||||
note: (payload: Note) => void;
|
||||
@ -680,6 +683,7 @@ export type Channels = {
|
||||
params: {
|
||||
withRenotes?: boolean;
|
||||
withFiles?: boolean;
|
||||
idOnly?: boolean;
|
||||
};
|
||||
events: {
|
||||
note: (payload: Note) => void;
|
||||
@ -691,6 +695,7 @@ export type Channels = {
|
||||
listId: string;
|
||||
withFiles?: boolean;
|
||||
withRenotes?: boolean;
|
||||
idOnly?: boolean;
|
||||
};
|
||||
events: {
|
||||
note: (payload: Note) => void;
|
||||
@ -709,6 +714,7 @@ export type Channels = {
|
||||
roleTimeline: {
|
||||
params: {
|
||||
roleId: string;
|
||||
idOnly?: boolean;
|
||||
};
|
||||
events: {
|
||||
note: (payload: Note) => void;
|
||||
@ -718,6 +724,7 @@ export type Channels = {
|
||||
antenna: {
|
||||
params: {
|
||||
antennaId: string;
|
||||
idOnly?: boolean;
|
||||
};
|
||||
events: {
|
||||
note: (payload: Note) => void;
|
||||
@ -727,6 +734,7 @@ export type Channels = {
|
||||
channel: {
|
||||
params: {
|
||||
channelId: string;
|
||||
idOnly?: boolean;
|
||||
};
|
||||
events: {
|
||||
note: (payload: Note) => void;
|
||||
@ -1788,6 +1796,8 @@ declare namespace entities {
|
||||
UsersSearchResponse,
|
||||
UsersShowRequest,
|
||||
UsersShowResponse,
|
||||
UsersGetSecurityInfoRequest,
|
||||
UsersGetSecurityInfoResponse,
|
||||
UsersStatsRequest,
|
||||
UsersStatsResponse,
|
||||
UsersAchievementsRequest,
|
||||
@ -3194,6 +3204,12 @@ type UsersGetFrequentlyRepliedUsersRequest = operations['users___get-frequently-
|
||||
// @public (undocumented)
|
||||
type UsersGetFrequentlyRepliedUsersResponse = operations['users___get-frequently-replied-users']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type UsersGetSecurityInfoRequest = operations['users___get-security-info']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type UsersGetSecurityInfoResponse = operations['users___get-security-info']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type UsersGetSkebStatusRequest = operations['users___get-skeb-status']['requestBody']['content']['application/json'];
|
||||
|
||||
|
@ -4248,6 +4248,17 @@ declare module '../api.js' {
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *No*
|
||||
*/
|
||||
request<E extends 'users/get-security-info', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* Show statistics about a user.
|
||||
*
|
||||
|
@ -568,6 +568,8 @@ import type {
|
||||
UsersSearchResponse,
|
||||
UsersShowRequest,
|
||||
UsersShowResponse,
|
||||
UsersGetSecurityInfoRequest,
|
||||
UsersGetSecurityInfoResponse,
|
||||
UsersStatsRequest,
|
||||
UsersStatsResponse,
|
||||
UsersAchievementsRequest,
|
||||
@ -977,6 +979,7 @@ export type Endpoints = {
|
||||
'users/search-by-username-and-host': { req: UsersSearchByUsernameAndHostRequest; res: UsersSearchByUsernameAndHostResponse };
|
||||
'users/search': { req: UsersSearchRequest; res: UsersSearchResponse };
|
||||
'users/show': { req: UsersShowRequest; res: UsersShowResponse };
|
||||
'users/get-security-info': { req: UsersGetSecurityInfoRequest; res: UsersGetSecurityInfoResponse };
|
||||
'users/stats': { req: UsersStatsRequest; res: UsersStatsResponse };
|
||||
'users/achievements': { req: UsersAchievementsRequest; res: UsersAchievementsResponse };
|
||||
'users/update-memo': { req: UsersUpdateMemoRequest; res: EmptyResponse };
|
||||
|
@ -571,6 +571,8 @@ export type UsersSearchRequest = operations['users___search']['requestBody']['co
|
||||
export type UsersSearchResponse = operations['users___search']['responses']['200']['content']['application/json'];
|
||||
export type UsersShowRequest = operations['users___show']['requestBody']['content']['application/json'];
|
||||
export type UsersShowResponse = operations['users___show']['responses']['200']['content']['application/json'];
|
||||
export type UsersGetSecurityInfoRequest = operations['users___get-security-info']['requestBody']['content']['application/json'];
|
||||
export type UsersGetSecurityInfoResponse = operations['users___get-security-info']['responses']['200']['content']['application/json'];
|
||||
export type UsersStatsRequest = operations['users___stats']['requestBody']['content']['application/json'];
|
||||
export type UsersStatsResponse = operations['users___stats']['responses']['200']['content']['application/json'];
|
||||
export type UsersAchievementsRequest = operations['users___achievements']['requestBody']['content']['application/json'];
|
||||
|
@ -2584,6 +2584,13 @@ export type paths = {
|
||||
post: operations['invite___limit'];
|
||||
};
|
||||
'/meta': {
|
||||
/**
|
||||
* meta
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *No*
|
||||
*/
|
||||
get: operations['meta_get'];
|
||||
/**
|
||||
* meta
|
||||
* @description No description provided.
|
||||
@ -3287,6 +3294,13 @@ export type paths = {
|
||||
post: operations['server-info'];
|
||||
};
|
||||
'/stats': {
|
||||
/**
|
||||
* stats
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *No*
|
||||
*/
|
||||
get: operations['stats_get'];
|
||||
/**
|
||||
* stats
|
||||
* @description No description provided.
|
||||
@ -3654,6 +3668,15 @@ export type paths = {
|
||||
*/
|
||||
post: operations['users___show'];
|
||||
};
|
||||
'/users/get-security-info': {
|
||||
/**
|
||||
* users/get-security-info
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *No*
|
||||
*/
|
||||
post: operations['users___get-security-info'];
|
||||
};
|
||||
'/users/stats': {
|
||||
/**
|
||||
* users/stats
|
||||
@ -22607,6 +22630,60 @@ export type operations = {
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* meta
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *No*
|
||||
*/
|
||||
meta_get: {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/** @default true */
|
||||
detail?: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['MetaLite'] | components['schemas']['MetaDetailed'];
|
||||
};
|
||||
};
|
||||
/** @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'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* meta
|
||||
* @description No description provided.
|
||||
@ -27167,6 +27244,60 @@ export type operations = {
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* stats
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *No*
|
||||
*/
|
||||
stats_get: {
|
||||
responses: {
|
||||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
notesCount: number;
|
||||
originalNotesCount: number;
|
||||
usersCount: number;
|
||||
originalUsersCount: number;
|
||||
instances: number;
|
||||
driveUsageLocal: number;
|
||||
driveUsageRemote: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
/** @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'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* stats
|
||||
* @description No description provided.
|
||||
@ -29620,6 +29751,70 @@ export type operations = {
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* users/get-security-info
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *No*
|
||||
*/
|
||||
'users___get-security-info': {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
twoFactorEnabled: boolean;
|
||||
usePasswordLessLogin: boolean;
|
||||
securityKeys: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
/** @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 To many requests */
|
||||
429: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* users/stats
|
||||
* @description Show statistics about a user.
|
||||
|
@ -64,6 +64,7 @@ export type Channels = {
|
||||
params: {
|
||||
withRenotes?: boolean;
|
||||
withFiles?: boolean;
|
||||
idOnly?: boolean,
|
||||
};
|
||||
events: {
|
||||
note: (payload: Note) => void;
|
||||
@ -75,6 +76,7 @@ export type Channels = {
|
||||
withRenotes?: boolean;
|
||||
withReplies?: boolean;
|
||||
withFiles?: boolean;
|
||||
idOnly?: boolean,
|
||||
};
|
||||
events: {
|
||||
note: (payload: Note) => void;
|
||||
@ -86,6 +88,7 @@ export type Channels = {
|
||||
withRenotes?: boolean;
|
||||
withReplies?: boolean;
|
||||
withFiles?: boolean;
|
||||
idOnly?: boolean,
|
||||
};
|
||||
events: {
|
||||
note: (payload: Note) => void;
|
||||
@ -96,6 +99,7 @@ export type Channels = {
|
||||
params: {
|
||||
withRenotes?: boolean;
|
||||
withFiles?: boolean;
|
||||
idOnly?: boolean,
|
||||
};
|
||||
events: {
|
||||
note: (payload: Note) => void;
|
||||
@ -107,6 +111,7 @@ export type Channels = {
|
||||
listId: string;
|
||||
withFiles?: boolean;
|
||||
withRenotes?: boolean;
|
||||
idOnly?: boolean,
|
||||
};
|
||||
events: {
|
||||
note: (payload: Note) => void;
|
||||
@ -125,6 +130,7 @@ export type Channels = {
|
||||
roleTimeline: {
|
||||
params: {
|
||||
roleId: string;
|
||||
idOnly?: boolean,
|
||||
};
|
||||
events: {
|
||||
note: (payload: Note) => void;
|
||||
@ -134,6 +140,7 @@ export type Channels = {
|
||||
antenna: {
|
||||
params: {
|
||||
antennaId: string;
|
||||
idOnly?: boolean,
|
||||
};
|
||||
events: {
|
||||
note: (payload: Note) => void;
|
||||
@ -143,6 +150,7 @@ export type Channels = {
|
||||
channel: {
|
||||
params: {
|
||||
channelId: string;
|
||||
idOnly?: boolean,
|
||||
};
|
||||
events: {
|
||||
note: (payload: Note) => void;
|
||||
|
1111
pnpm-lock.yaml
generated
1111
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user