Merge upstream

This commit is contained in:
ASTRO:? 2024-12-22 12:44:09 +09:00
commit 29c25555b8
No known key found for this signature in database
GPG key ID: 8947F3AF5B0B4BFE
40 changed files with 1183 additions and 667 deletions

8
locales/index.d.ts vendored
View file

@ -5516,6 +5516,14 @@ export interface Locale extends ILocale {
* IDを一行に一つずつ書きます * IDを一行に一つずつ書きます
*/ */
"endingCreditMembersDescription": string; "endingCreditMembersDescription": string;
/**
*
*/
"emailAddressLogin": string;
/**
*
*/
"usernameLogin": string;
"_bubbleGame": { "_bubbleGame": {
/** /**
* *

View file

@ -1372,6 +1372,8 @@ muteNotification: "通知をミュートする"
unmuteNotification: "通知をミュート解除する" unmuteNotification: "通知をミュート解除する"
endingCreditMembers: "スタッフロールに表示するユーザー" endingCreditMembers: "スタッフロールに表示するユーザー"
endingCreditMembersDescription: "サーバー情報ページに表示するユーザーのIDを一行に一つずつ書きます。" endingCreditMembersDescription: "サーバー情報ページに表示するユーザーのIDを一行に一つずつ書きます。"
emailAddressLogin: "メールアドレスでログイン"
usernameLogin: "ユーザー名でログイン"
_bubbleGame: _bubbleGame:
howToPlay: "遊び方" howToPlay: "遊び方"

View file

@ -48,11 +48,11 @@
"resolutions": { "resolutions": {
"@tensorflow/tfjs-core": "4.22.0", "@tensorflow/tfjs-core": "4.22.0",
"axios": "1.7.9", "axios": "1.7.9",
"chokidar": "4.0.2", "chokidar": "4.0.3",
"cookie": "1.0.2", "cookie": "1.0.2",
"cookie-signature": "1.2.2", "cookie-signature": "1.2.2",
"debug": "4.4.0", "debug": "4.4.0",
"esbuild": "0.24.0", "esbuild": "0.24.2",
"jpeg-js": "0.4.4", "jpeg-js": "0.4.4",
"lodash": "4.17.21", "lodash": "4.17.21",
"punycode": "npm:punycode.js@2.3.1", "punycode": "npm:punycode.js@2.3.1",

View file

@ -63,8 +63,8 @@
}, },
"dependencies": { "dependencies": {
"@authenio/samlify-node-xmllint": "2.0.0", "@authenio/samlify-node-xmllint": "2.0.0",
"@aws-sdk/client-s3": "3.714.0", "@aws-sdk/client-s3": "3.717.0",
"@aws-sdk/lib-storage": "3.714.0", "@aws-sdk/lib-storage": "3.717.0",
"@bull-board/api": "6.5.3", "@bull-board/api": "6.5.3",
"@bull-board/fastify": "6.5.3", "@bull-board/fastify": "6.5.3",
"@bull-board/ui": "6.5.3", "@bull-board/ui": "6.5.3",
@ -80,7 +80,7 @@
"@fastify/static": "8.0.3", "@fastify/static": "8.0.3",
"@fastify/view": "10.0.1", "@fastify/view": "10.0.1",
"@misskey-dev/sharp-read-bmp": "1.2.0", "@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", "@napi-rs/canvas": "0.1.65",
"@nestjs/common": "10.4.15", "@nestjs/common": "10.4.15",
"@nestjs/core": "10.4.15", "@nestjs/core": "10.4.15",
@ -99,12 +99,12 @@
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"body-parser": "1.20.3", "body-parser": "1.20.3",
"bullmq": "5.34.2", "bullmq": "5.34.4",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"cbor": "10.0.3", "cbor": "10.0.3",
"chalk": "5.3.0", "chalk": "5.4.0",
"chalk-template": "1.1.0", "chalk-template": "1.1.0",
"chokidar": "4.0.2", "chokidar": "4.0.3",
"cli-highlight": "2.1.11", "cli-highlight": "2.1.11",
"color-convert": "2.0.1", "color-convert": "2.0.1",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
@ -122,7 +122,7 @@
"hpagent": "1.2.0", "hpagent": "1.2.0",
"htmlescape": "1.1.1", "htmlescape": "1.1.1",
"http-link-header": "1.1.3", "http-link-header": "1.1.3",
"ioredis": "5.4.1", "ioredis": "5.4.2",
"ip-cidr": "4.0.2", "ip-cidr": "4.0.2",
"ipaddr.js": "2.2.0", "ipaddr.js": "2.2.0",
"is-svg": "5.1.0", "is-svg": "5.1.0",
@ -169,13 +169,13 @@
"rss-parser": "3.13.0", "rss-parser": "3.13.0",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"samlify": "2.8.11", "samlify": "2.8.11",
"sanitize-html": "2.13.1", "sanitize-html": "2.14.0",
"secure-json-parse": "3.0.1", "secure-json-parse": "3.0.1",
"sharp": "0.33.5", "sharp": "0.33.5",
"slacc": "0.0.10", "slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"systeminformation": "5.23.13", "systeminformation": "5.23.14",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tmp": "0.2.3", "tmp": "0.2.3",
"tsc-alias": "1.8.10", "tsc-alias": "1.8.10",

View file

@ -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_search from './endpoints/users/search.js';
import * as ep___users_show from './endpoints/users/show.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_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_achievements from './endpoints/users/achievements.js';
import * as ep___users_updateMemo from './endpoints/users/update-memo.js'; import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
import * as ep___fetchRss from './endpoints/fetch-rss.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_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_getFollowingBirthdayUsers: Provider = { provide: 'ep:users/get-following-birthday-users', useClass: ep___users_getFollowingBirthdayUsers.default };
const $users_getFrequentlyRepliedUsers: Provider = { provide: 'ep:users/get-frequently-replied-users', useClass: ep___users_getFrequentlyRepliedUsers.default }; const $users_getFrequentlyRepliedUsers: Provider = { provide: 'ep:users/get-frequently-replied-users', useClass: ep___users_getFrequentlyRepliedUsers.default };
const $users_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_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_featuredNotes: Provider = { provide: 'ep:users/featured-notes', useClass: ep___users_featuredNotes.default };
const $users_lists_create: Provider = { provide: 'ep:users/lists/create', useClass: ep___users_lists_create.default }; const $users_lists_create: Provider = { provide: 'ep:users/lists/create', useClass: ep___users_lists_create.default };
@ -1170,6 +1172,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$users_gallery_posts, $users_gallery_posts,
$users_getFollowingBirthdayUsers, $users_getFollowingBirthdayUsers,
$users_getFrequentlyRepliedUsers, $users_getFrequentlyRepliedUsers,
$users_getSecurityInfo,
$users_getSkebStatus, $users_getSkebStatus,
$users_featuredNotes, $users_featuredNotes,
$users_lists_create, $users_lists_create,
@ -1567,6 +1570,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$users_gallery_posts, $users_gallery_posts,
$users_getFollowingBirthdayUsers, $users_getFollowingBirthdayUsers,
$users_getFrequentlyRepliedUsers, $users_getFrequentlyRepliedUsers,
$users_getSecurityInfo,
$users_getSkebStatus, $users_getSkebStatus,
$users_featuredNotes, $users_featuredNotes,
$users_lists_create, $users_lists_create,

View file

@ -3,11 +3,13 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { import type {
MiUserProfile,
SigninsRepository, SigninsRepository,
UserProfilesRepository, UserProfilesRepository,
UsersRepository, UsersRepository,
@ -27,7 +29,6 @@ import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js'; import { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/server'; import type { AuthenticationResponseJSON } from '@simplewebauthn/server';
import type { FastifyReply, FastifyRequest } from 'fastify'; import type { FastifyReply, FastifyRequest } from 'fastify';
import { randomUUID } from 'node:crypto';
@Injectable() @Injectable()
export class SigninApiService { export class SigninApiService {
@ -122,22 +123,34 @@ export class SigninApiService {
} }
// Fetch user // Fetch user
const user = await this.usersRepository.findOneBy({ const profile = await this.userProfilesRepository.findOne({
usernameLower: username.toLowerCase(), relations: ['user'],
host: IsNull(), where: username.includes('@') ? {
}) as MiLocalUser; 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.'); logger.error('No such user.');
return error(404, { return error(403, {
id: '6cc579cc-885d-43d8-95c2-b8c7fc963280', id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
}); });
} }
if (user.isDeleted && user.isSuspended) { if (user.isDeleted && user.isSuspended) {
logger.error('No such user. (logical deletion)'); logger.error('No such user. (logical deletion)');
return error(404, { return error(403, {
id: '6cc579cc-885d-43d8-95c2-b8c7fc963280', id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
}); });
} }
@ -148,8 +161,6 @@ export class SigninApiService {
}); });
} }
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
// Compare password // Compare password
const same = await bcrypt.compare(password, profile.password!); const same = await bcrypt.compare(password, profile.password!);

View file

@ -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_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js';
import * as ep___users_search from './endpoints/users/search.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_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_stats from './endpoints/users/stats.js';
import * as ep___users_achievements from './endpoints/users/achievements.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___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-by-username-and-host', ep___users_searchByUsernameAndHost],
['users/search', ep___users_search], ['users/search', ep___users_search],
['users/show', ep___users_show], ['users/show', ep___users_show],
['users/get-security-info', ep___users_get_security_info],
['users/stats', ep___users_stats], ['users/stats', ep___users_stats],
['users/achievements', ep___users_achievements], ['users/achievements', ep___users_achievements],
['users/update-memo', ep___users_updateMemo], ['users/update-memo', ep___users_updateMemo],

View file

@ -11,6 +11,8 @@ export const meta = {
tags: ['meta'], tags: ['meta'],
requireCredential: false, requireCredential: false,
allowGet: true,
cacheSec: 60,
res: { res: {
type: 'object', type: 'object',

View file

@ -12,6 +12,8 @@ import UsersChart from '@/core/chart/charts/users.js';
export const meta = { export const meta = {
requireCredential: false, requireCredential: false,
allowGet: true,
cacheSec: 60,
tags: ['meta'], tags: ['meta'],

View file

@ -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,
};
});
}
}

View file

@ -15,6 +15,7 @@ class AntennaChannel extends Channel {
public static readonly requireCredential = true as const; public static readonly requireCredential = true as const;
public static readonly kind = 'read:account'; public static readonly kind = 'read:account';
private antennaId: string; private antennaId: string;
private idOnly: boolean;
constructor( constructor(
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
@ -29,6 +30,7 @@ class AntennaChannel extends Channel {
@bindThis @bindThis
public async init(params: any) { public async init(params: any) {
this.antennaId = params.antennaId as string; this.antennaId = params.antennaId as string;
this.idOnly = params.idOnly ?? false;
// Subscribe stream // Subscribe stream
this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent); this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent);
@ -57,9 +59,13 @@ class AntennaChannel extends Channel {
if (this.isNoteMutedOrBlocked(note.reply)) return; if (this.isNoteMutedOrBlocked(note.reply)) return;
} }
this.connection.cacheNote(note); if (this.idOnly && ['public', 'home'].includes(note.visibility)) {
const idOnlyNote = { id: note.id };
this.send('note', note); this.send('note', idOnlyNote);
} else {
this.connection.cacheNote(note);
this.send('note', note);
}
} else { } else {
this.send(data.type, data.body); this.send(data.type, data.body);
} }

View file

@ -15,6 +15,7 @@ class ChannelChannel extends Channel {
public static readonly shouldShare = false; public static readonly shouldShare = false;
public static readonly requireCredential = false as const; public static readonly requireCredential = false as const;
private channelId: string; private channelId: string;
private idOnly: boolean;
constructor( constructor(
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
@ -29,6 +30,7 @@ class ChannelChannel extends Channel {
@bindThis @bindThis
public async init(params: any) { public async init(params: any) {
this.channelId = params.channelId as string; this.channelId = params.channelId as string;
this.idOnly = params.idOnly ?? false;
// Subscribe stream // Subscribe stream
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);
@ -63,9 +65,13 @@ class ChannelChannel extends Channel {
} }
} }
this.connection.cacheNote(note); if (this.idOnly && ['public', 'home'].includes(note.visibility)) {
const idOnlyNote = { id: note.id };
this.send('note', note); this.send('note', idOnlyNote);
} else {
this.connection.cacheNote(note);
this.send('note', note);
}
} }
@bindThis @bindThis

View file

@ -18,6 +18,7 @@ class GlobalTimelineChannel extends Channel {
public static readonly requireCredential = false as const; public static readonly requireCredential = false as const;
private withRenotes: boolean; private withRenotes: boolean;
private withFiles: boolean; private withFiles: boolean;
private idOnly: boolean;
constructor( constructor(
private metaService: MetaService, private metaService: MetaService,
@ -38,6 +39,7 @@ class GlobalTimelineChannel extends Channel {
this.withRenotes = params.withRenotes ?? true; this.withRenotes = params.withRenotes ?? true;
this.withFiles = params.withFiles ?? false; this.withFiles = params.withFiles ?? false;
this.idOnly = params.idOnly ?? false;
// Subscribe events // Subscribe events
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);
@ -93,9 +95,13 @@ class GlobalTimelineChannel extends Channel {
} }
} }
this.connection.cacheNote(note); if (this.idOnly && ['public', 'home'].includes(note.visibility)) {
const idOnlyNote = { id: note.id };
this.send('note', note); this.send('note', idOnlyNote);
} else {
this.connection.cacheNote(note);
this.send('note', note);
}
} }
@bindThis @bindThis

View file

@ -17,6 +17,7 @@ class HomeTimelineChannel extends Channel {
public static readonly kind = 'read:account'; public static readonly kind = 'read:account';
private withRenotes: boolean; private withRenotes: boolean;
private withFiles: boolean; private withFiles: boolean;
private idOnly: boolean;
constructor( constructor(
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
@ -32,6 +33,7 @@ class HomeTimelineChannel extends Channel {
public async init(params: any) { public async init(params: any) {
this.withRenotes = params.withRenotes ?? true; this.withRenotes = params.withRenotes ?? true;
this.withFiles = params.withFiles ?? false; this.withFiles = params.withFiles ?? false;
this.idOnly = params.idOnly ?? false;
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);
} }
@ -94,9 +96,13 @@ class HomeTimelineChannel extends Channel {
} }
} }
this.connection.cacheNote(note); if (this.idOnly && ['public', 'home'].includes(note.visibility)) {
const idOnlyNote = { id: note.id };
this.send('note', note); this.send('note', idOnlyNote);
} else {
this.connection.cacheNote(note);
this.send('note', note);
}
} }
@bindThis @bindThis

View file

@ -20,6 +20,7 @@ class HybridTimelineChannel extends Channel {
private withRenotes: boolean; private withRenotes: boolean;
private withReplies: boolean; private withReplies: boolean;
private withFiles: boolean; private withFiles: boolean;
private idOnly: boolean;
constructor( constructor(
private metaService: MetaService, private metaService: MetaService,
@ -41,6 +42,7 @@ class HybridTimelineChannel extends Channel {
this.withRenotes = params.withRenotes ?? true; this.withRenotes = params.withRenotes ?? true;
this.withReplies = params.withReplies ?? false; this.withReplies = params.withReplies ?? false;
this.withFiles = params.withFiles ?? false; this.withFiles = params.withFiles ?? false;
this.idOnly = params.idOnly ?? false;
// Subscribe events // Subscribe events
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);
@ -110,9 +112,13 @@ class HybridTimelineChannel extends Channel {
} }
} }
this.connection.cacheNote(note); if (this.idOnly && ['public', 'home'].includes(note.visibility)) {
const idOnlyNote = { id: note.id };
this.send('note', note); this.send('note', idOnlyNote);
} else {
this.connection.cacheNote(note);
this.send('note', note);
}
} }
@bindThis @bindThis

View file

@ -19,6 +19,7 @@ class LocalTimelineChannel extends Channel {
private withRenotes: boolean; private withRenotes: boolean;
private withReplies: boolean; private withReplies: boolean;
private withFiles: boolean; private withFiles: boolean;
private idOnly: boolean;
constructor( constructor(
private metaService: MetaService, private metaService: MetaService,
@ -40,6 +41,7 @@ class LocalTimelineChannel extends Channel {
this.withRenotes = params.withRenotes ?? true; this.withRenotes = params.withRenotes ?? true;
this.withReplies = params.withReplies ?? false; this.withReplies = params.withReplies ?? false;
this.withFiles = params.withFiles ?? false; this.withFiles = params.withFiles ?? false;
this.idOnly = params.idOnly ?? false;
// Subscribe events // Subscribe events
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);
@ -93,9 +95,13 @@ class LocalTimelineChannel extends Channel {
} }
} }
this.connection.cacheNote(note); if (this.idOnly && ['public', 'home'].includes(note.visibility)) {
const idOnlyNote = { id: note.id };
this.send('note', note); this.send('note', idOnlyNote);
} else {
this.connection.cacheNote(note);
this.send('note', note);
}
} }
@bindThis @bindThis

View file

@ -16,6 +16,7 @@ class RoleTimelineChannel extends Channel {
public static readonly shouldShare = false; public static readonly shouldShare = false;
public static readonly requireCredential = false as const; public static readonly requireCredential = false as const;
private roleId: string; private roleId: string;
private idOnly: boolean;
constructor( constructor(
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
@ -31,6 +32,7 @@ class RoleTimelineChannel extends Channel {
@bindThis @bindThis
public async init(params: any) { public async init(params: any) {
this.roleId = params.roleId as string; this.roleId = params.roleId as string;
this.idOnly = params.idOnly ?? false;
this.subscriber.on(`roleTimelineStream:${this.roleId}`, this.onEvent); this.subscriber.on(`roleTimelineStream:${this.roleId}`, this.onEvent);
} }
@ -79,9 +81,13 @@ class RoleTimelineChannel extends Channel {
} }
} }
this.connection.cacheNote(note); if (this.idOnly && ['public', 'home'].includes(note.visibility)) {
const idOnlyNote = { id: note.id };
this.send('note', note); this.send('note', idOnlyNote);
} else {
this.connection.cacheNote(note);
this.send('note', note);
}
} else { } else {
this.send(data.type, data.body); this.send(data.type, data.body);
} }

View file

@ -21,6 +21,7 @@ class UserListChannel extends Channel {
private listUsersClock: NodeJS.Timeout; private listUsersClock: NodeJS.Timeout;
private withFiles: boolean; private withFiles: boolean;
private withRenotes: boolean; private withRenotes: boolean;
private idOnly: boolean;
constructor( constructor(
private userListsRepository: UserListsRepository, private userListsRepository: UserListsRepository,
@ -40,6 +41,7 @@ class UserListChannel extends Channel {
this.listId = params.listId as string; this.listId = params.listId as string;
this.withFiles = params.withFiles ?? false; this.withFiles = params.withFiles ?? false;
this.withRenotes = params.withRenotes ?? true; this.withRenotes = params.withRenotes ?? true;
this.idOnly = params.idOnly ?? false;
// Check existence and owner // Check existence and owner
const listExist = await this.userListsRepository.exists({ const listExist = await this.userListsRepository.exists({
@ -128,9 +130,13 @@ class UserListChannel extends Channel {
} }
} }
this.connection.cacheNote(note); if (this.idOnly && ['public', 'home'].includes(note.visibility)) {
const idOnlyNote = { id: note.id };
this.send('note', note); this.send('note', idOnlyNote);
} else {
this.connection.cacheNote(note);
this.send('note', note);
}
} }
@bindThis @bindThis

View file

@ -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 // Page
fastify.get<{ Params: { user: string; page: string; } }>('/@:user/pages/:page', async (request, reply) => { fastify.get<{ Params: { user: string; page: string; } }>('/@:user/pages/:page', async (request, reply) => {
const { username, host } = Acct.parse(request.params.user); const { username, host } = Acct.parse(request.params.user);

View file

@ -32,25 +32,10 @@
renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.') renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.')
} }
const metaRes = await window.fetch('/api/meta', { if (localStorage.getItem('id') === null) {
method: 'POST', localStorage.setItem('id', crypto.randomUUID().replaceAll('-', ''));
body: JSON.stringify({}), }
credentials: 'omit', let id = localStorage.getItem('id');
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;
}
//#region Detect language & fetch translations //#region Detect language & fetch translations
if (!localStorage.hasOwn('locale')) { if (!localStorage.hasOwn('locale')) {
@ -73,6 +58,25 @@
lang = 'ko-KR'; 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`); const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`);
if (localRes.status === 200) { if (localRes.status === 200) {
localStorage.setItem('lang', lang); localStorage.setItem('lang', lang);

View file

@ -400,7 +400,7 @@ export const waitFire = async <C extends keyof misskey.Channels>(user: UserToken
if (timer) clearTimeout(timer); if (timer) clearTimeout(timer);
res(true); res(true);
} }
}, params); }, { ...params, idOnly: false });
} catch (e) { } catch (e) {
rej(e); rej(e);
} }

View file

@ -31,7 +31,7 @@
"@twemoji/parser": "15.1.1", "@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.2.1", "@vitejs/plugin-vue": "5.2.1",
"@vue/compiler-sfc": "3.5.13", "@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", "astring": "1.9.0",
"broadcast-channel": "7.0.0", "broadcast-channel": "7.0.0",
"buraha": "0.0.1", "buraha": "0.0.1",
@ -59,10 +59,10 @@
"misskey-reversi": "workspace:*", "misskey-reversi": "workspace:*",
"photoswipe": "5.4.4", "photoswipe": "5.4.4",
"punycode.js": "2.3.1", "punycode.js": "2.3.1",
"rollup": "4.28.1", "rollup": "4.29.1",
"sanitize-html": "2.13.1", "sanitize-html": "2.14.0",
"sass": "1.83.0", "sass": "1.83.0",
"shiki": "1.24.2", "shiki": "1.24.3",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"three": "0.171.0", "three": "0.171.0",
@ -73,7 +73,7 @@
"typescript": "5.7.2", "typescript": "5.7.2",
"uuid": "11.0.3", "uuid": "11.0.3",
"v-code-diff": "1.13.1", "v-code-diff": "1.13.1",
"vite": "6.0.3", "vite": "6.0.5",
"vue": "3.5.13", "vue": "3.5.13",
"vue-gtag": "2.0.1", "vue-gtag": "2.0.1",
"vuedraggable": "next", "vuedraggable": "next",
@ -81,7 +81,7 @@
}, },
"devDependencies": { "devDependencies": {
"@misskey-dev/eslint-plugin": "1.0.0", "@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-actions": "8.4.7",
"@storybook/addon-essentials": "8.4.7", "@storybook/addon-essentials": "8.4.7",
"@storybook/addon-interactions": "8.4.7", "@storybook/addon-interactions": "8.4.7",

View file

@ -5,16 +5,16 @@
import { defineAsyncComponent, reactive, ref } from 'vue'; import { defineAsyncComponent, reactive, ref } from 'vue';
import * as Misskey from 'misskey-js'; 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 { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { miLocalStorage } from '@/local-storage.js'; import { miLocalStorage } from '@/local-storage.js';
import { MenuButton } from '@/types/menu.js';
import { del, get, set } from '@/scripts/idb-proxy.js'; import { del, get, set } from '@/scripts/idb-proxy.js';
import { apiUrl } from '@/config.js'; import { apiUrl } from '@/config.js';
import { waiting, popup, popupMenu, success, alert } from '@/os.js'; import { waiting, popup, popupMenu, success, alert } from '@/os.js';
import { generateClientTransactionId, misskeyApi } from '@/scripts/misskey-api.js'; import { generateClientTransactionId, misskeyApi } from '@/scripts/misskey-api.js';
import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js'; import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js';
import { set as gtagSet, time as gtagTime } from 'vue-gtag';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
// TODO: 他のタブと永続化されたstateを同期 // TODO: 他のタブと永続化されたstateを同期

View file

@ -63,10 +63,6 @@ export async function common(createVue: () => App<Element>) {
}); });
} }
if (miLocalStorage.getItem('id') === null) {
miLocalStorage.setItem('id', crypto.randomUUID());
}
let isClientUpdated = false; let isClientUpdated = false;
//#region クライアントが更新されたかチェック //#region クライアントが更新されたかチェック

View file

@ -6,24 +6,32 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<form :class="{ signing, totpLogin }" @submit.prevent="onSubmit"> <form :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
<div class="_gaps_m"> <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"> <MkInfo v-if="message">
{{ message }} {{ message }}
</MkInfo> </MkInfo>
<div v-if="!totpLogin" class="normal-signin _gaps_m"> <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"> <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>@</template> <template #prefix>
<template #suffix>@{{ host }}</template> <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>
<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> <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 #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> </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.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.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.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"/> <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>
<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }"> <div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
<div v-if="user && user.securityKeys" class="twofa-group tap-group"> <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'; import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
const signing = ref(false); const signing = ref(false);
const loginWithEmailAddress = ref(false);
const userAbortController = ref<AbortController>(); const userAbortController = ref<AbortController>();
const user = ref<Misskey.entities.UserDetailed | null>(null); const user = ref<Misskey.entities.UserDetailed | null>(null);
const username = ref(''); 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) { if (userAbortController.value) {
userAbortController.value.abort(); userAbortController.value.abort();
} }
@ -168,8 +179,15 @@ async function queryKey(): Promise<void> {
}); });
} }
function onSubmit(): void { async function onSubmit(): Promise<void> {
signing.value = true; 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 (!totpLogin.value && user.value?.twoFactorEnabled) {
if (webAuthnSupported() && user.value.securityKeys) { if (webAuthnSupported() && user.value.securityKeys) {
misskeyApi('signin', { misskeyApi('signin', {

View file

@ -27,7 +27,7 @@ import { i18n } from '@/i18n.js';
withDefaults(defineProps<{ withDefaults(defineProps<{
autoSet?: boolean; autoSet?: boolean;
message?: string, message?: string;
}>(), { }>(), {
autoSet: false, autoSet: false,
message: '', message: '',

View file

@ -18,15 +18,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { computed, watch, onUnmounted, provide, shallowRef } from 'vue'; import { computed, watch, onUnmounted, provide, shallowRef } from 'vue';
import { time as gtagTime } from 'vue-gtag';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkNotes from '@/components/MkNotes.vue'; import MkNotes from '@/components/MkNotes.vue';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
import { useStream } from '@/stream.js'; import { useStream } from '@/stream.js';
import * as sound from '@/scripts/sound.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 { instance } from '@/instance.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { Paging } from '@/components/MkPagination.vue'; import { Paging } from '@/components/MkPagination.vue';
import { generateClientTransactionId } from '@/scripts/misskey-api.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
src: 'home' | 'local' | 'media' | 'social' | 'global' | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role'; 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; let tlNotesCount = 0;
function prepend(note) { async function prepend(data) {
if (tlComponent.value == null) return; if (tlComponent.value == null) return;
let note = data;
//
// idOnlyid
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++; tlNotesCount++;
if (instance.notesPerOneAd > 0 && tlNotesCount % instance.notesPerOneAd === 0) { if (instance.notesPerOneAd > 0 && tlNotesCount % instance.notesPerOneAd === 0) {
@ -89,6 +118,7 @@ function prepend(note) {
let connection: Misskey.ChannelConnection | null = null; let connection: Misskey.ChannelConnection | null = null;
let connection2: Misskey.ChannelConnection | null = null; let connection2: Misskey.ChannelConnection | null = null;
let paginationQuery: Paging | null = null; let paginationQuery: Paging | null = null;
const idOnly = !iAmModerator;
const stream = useStream(); const stream = useStream();
@ -97,11 +127,13 @@ function connectChannel() {
if (props.antenna == null) return; if (props.antenna == null) return;
connection = stream.useChannel('antenna', { connection = stream.useChannel('antenna', {
antennaId: props.antenna, antennaId: props.antenna,
idOnly: idOnly,
}); });
} else if (props.src === 'home') { } else if (props.src === 'home') {
connection = stream.useChannel('homeTimeline', { connection = stream.useChannel('homeTimeline', {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
idOnly: idOnly,
}); });
connection2 = stream.useChannel('main'); connection2 = stream.useChannel('main');
} else if (props.src === 'local') { } else if (props.src === 'local') {
@ -109,23 +141,27 @@ function connectChannel() {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withReplies: props.withReplies, withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
idOnly: idOnly,
}); });
} else if (props.src === 'media') { } else if (props.src === 'media') {
connection = stream.useChannel('hybridTimeline', { connection = stream.useChannel('hybridTimeline', {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withReplies: props.withReplies, withReplies: props.withReplies,
withFiles: true, withFiles: true,
idOnly: idOnly,
}); });
} else if (props.src === 'social') { } else if (props.src === 'social') {
connection = stream.useChannel('hybridTimeline', { connection = stream.useChannel('hybridTimeline', {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withReplies: props.withReplies, withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
idOnly: idOnly,
}); });
} else if (props.src === 'global') { } else if (props.src === 'global') {
connection = stream.useChannel('globalTimeline', { connection = stream.useChannel('globalTimeline', {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
idOnly: idOnly,
}); });
} else if (props.src === 'mentions') { } else if (props.src === 'mentions') {
connection = stream.useChannel('main'); connection = stream.useChannel('main');
@ -144,16 +180,19 @@ function connectChannel() {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
listId: props.list, listId: props.list,
idOnly: idOnly,
}); });
} else if (props.src === 'channel') { } else if (props.src === 'channel') {
if (props.channel == null) return; if (props.channel == null) return;
connection = stream.useChannel('channel', { connection = stream.useChannel('channel', {
channelId: props.channel, channelId: props.channel,
idOnly: idOnly,
}); });
} else if (props.src === 'role') { } else if (props.src === 'role') {
if (props.role == null) return; if (props.role == null) return;
connection = stream.useChannel('roleTimeline', { connection = stream.useChannel('roleTimeline', {
roleId: props.role, roleId: props.role,
idOnly: idOnly,
}); });
} }
if (props.src !== 'directs' && props.src !== 'mentions') connection?.on('note', prepend); if (props.src !== 'directs' && props.src !== 'mentions') connection?.on('note', prepend);

View file

@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onMounted, shallowRef, ref, nextTick } from 'vue'; import { onMounted, shallowRef, ref, nextTick } from 'vue';
import { Chart } from 'chart.js'; import { Chart } from 'chart.js';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApiGet } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
import { chartVLine } from '@/scripts/chart-vline.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; fetching.value = false;

View file

@ -81,7 +81,7 @@ import MkTimeline from '@/components/MkTimeline.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import { instanceName } from '@/config.js'; import { instanceName } from '@/config.js';
import * as os from '@/os.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 { i18n } from '@/i18n.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import { miLocalStorage } from '@/local-storage.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); const stats = ref<Misskey.entities.StatsResponse | null>(null);
misskeyApi('stats', {}).then((res) => { misskeyApiGet('stats').then((res) => {
stats.value = res; stats.value = res;
}); });

View file

@ -5,7 +5,7 @@
import { computed, reactive } from 'vue'; import { computed, reactive } from 'vue';
import * as Misskey from 'misskey-js'; 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 { miLocalStorage } from '@/local-storage.js';
import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERROR_IMAGE_URL } from '@/const.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, detail: true,
}); });

View file

@ -196,7 +196,7 @@ import MkKeyValue from '@/components/MkKeyValue.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import MkInstanceStats from '@/components/MkInstanceStats.vue'; import MkInstanceStats from '@/components/MkInstanceStats.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.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 number from '@/filters/number.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
@ -229,8 +229,7 @@ watch(tab, () => {
} }
}); });
const initStats = () => misskeyApi('stats', { const initStats = () => misskeyApiGet('stats').then((res) => {
}).then((res) => {
stats.value = res; stats.value = res;
}); });

View file

@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js'; 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 MkNumberDiff from '@/components/MkNumberDiff.vue';
import MkNumber from '@/components/MkNumber.vue'; import MkNumber from '@/components/MkNumber.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
@ -78,7 +78,7 @@ const fetching = ref(true);
onMounted(async () => { onMounted(async () => {
const [_stats, _onlineUsersCount] = await Promise.all([ const [_stats, _onlineUsersCount] = await Promise.all([
misskeyApi('stats', {}), misskeyApiGet('stats'),
misskeyApiGet('get-online-users-count').then(res => res.count), misskeyApiGet('get-online-users-count').then(res => res.count),
]); ]);
stats.value = _stats; stats.value = _stats;

View file

@ -15,11 +15,17 @@ export const pendingApiRequestsCount = ref(0);
let id: string | null = miLocalStorage.getItem('id'); let id: string | null = miLocalStorage.getItem('id');
export function generateClientTransactionId(initiator: string) { export function generateClientTransactionId(initiator: string) {
if (id === null) { if (id === null) {
id = crypto.randomUUID(); id = crypto.randomUUID().replaceAll('-', '');
miLocalStorage.setItem('id', id); 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>( function handleResponse<_ResT>(
@ -58,24 +64,22 @@ export function misskeyApi<
if (endpoint.includes('://')) throw new Error('invalid endpoint'); if (endpoint.includes('://')) throw new Error('invalid endpoint');
pendingApiRequestsCount.value++; pendingApiRequestsCount.value++;
const credential = token ? token : $i ? $i.token : undefined;
const onFinally = () => { const onFinally = () => {
pendingApiRequestsCount.value--; pendingApiRequestsCount.value--;
}; };
const promise = new Promise<_ResT>((resolve, reject) => { 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 // Send request
const initiateTime = Date.now(); const initiateTime = Date.now();
window.fetch(`${apiUrl}/${endpoint}`, { window.fetch(`${apiUrl}/${endpoint}`, {
method: 'POST', method: 'POST',
body: JSON.stringify(data), body: JSON.stringify(data),
credentials: 'omit', credentials: 'omit',
cache: 'no-cache',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': credential ? `Bearer ${credential}` : 'anonymous',
'X-Client-Transaction-Id': generateClientTransactionId(initiator), 'X-Client-Transaction-Id': generateClientTransactionId(initiator),
}, },
signal, signal,
@ -121,8 +125,8 @@ export function misskeyApiGet<
window.fetch(`${apiUrl}/${endpoint}?${query}`, { window.fetch(`${apiUrl}/${endpoint}?${query}`, {
method: 'GET', method: 'GET',
credentials: 'omit', credentials: 'omit',
cache: 'default',
headers: { headers: {
'Authorization': 'anonymous',
'X-Client-Transaction-Id': generateClientTransactionId(initiator), 'X-Client-Transaction-Id': generateClientTransactionId(initiator),
}, },
}).then(res => { }).then(res => {

View file

@ -648,6 +648,7 @@ export type Channels = {
params: { params: {
withRenotes?: boolean; withRenotes?: boolean;
withFiles?: boolean; withFiles?: boolean;
idOnly?: boolean;
}; };
events: { events: {
note: (payload: Note) => void; note: (payload: Note) => void;
@ -659,6 +660,7 @@ export type Channels = {
withRenotes?: boolean; withRenotes?: boolean;
withReplies?: boolean; withReplies?: boolean;
withFiles?: boolean; withFiles?: boolean;
idOnly?: boolean;
}; };
events: { events: {
note: (payload: Note) => void; note: (payload: Note) => void;
@ -670,6 +672,7 @@ export type Channels = {
withRenotes?: boolean; withRenotes?: boolean;
withReplies?: boolean; withReplies?: boolean;
withFiles?: boolean; withFiles?: boolean;
idOnly?: boolean;
}; };
events: { events: {
note: (payload: Note) => void; note: (payload: Note) => void;
@ -680,6 +683,7 @@ export type Channels = {
params: { params: {
withRenotes?: boolean; withRenotes?: boolean;
withFiles?: boolean; withFiles?: boolean;
idOnly?: boolean;
}; };
events: { events: {
note: (payload: Note) => void; note: (payload: Note) => void;
@ -691,6 +695,7 @@ export type Channels = {
listId: string; listId: string;
withFiles?: boolean; withFiles?: boolean;
withRenotes?: boolean; withRenotes?: boolean;
idOnly?: boolean;
}; };
events: { events: {
note: (payload: Note) => void; note: (payload: Note) => void;
@ -709,6 +714,7 @@ export type Channels = {
roleTimeline: { roleTimeline: {
params: { params: {
roleId: string; roleId: string;
idOnly?: boolean;
}; };
events: { events: {
note: (payload: Note) => void; note: (payload: Note) => void;
@ -718,6 +724,7 @@ export type Channels = {
antenna: { antenna: {
params: { params: {
antennaId: string; antennaId: string;
idOnly?: boolean;
}; };
events: { events: {
note: (payload: Note) => void; note: (payload: Note) => void;
@ -727,6 +734,7 @@ export type Channels = {
channel: { channel: {
params: { params: {
channelId: string; channelId: string;
idOnly?: boolean;
}; };
events: { events: {
note: (payload: Note) => void; note: (payload: Note) => void;
@ -1788,6 +1796,8 @@ declare namespace entities {
UsersSearchResponse, UsersSearchResponse,
UsersShowRequest, UsersShowRequest,
UsersShowResponse, UsersShowResponse,
UsersGetSecurityInfoRequest,
UsersGetSecurityInfoResponse,
UsersStatsRequest, UsersStatsRequest,
UsersStatsResponse, UsersStatsResponse,
UsersAchievementsRequest, UsersAchievementsRequest,
@ -3194,6 +3204,12 @@ type UsersGetFrequentlyRepliedUsersRequest = operations['users___get-frequently-
// @public (undocumented) // @public (undocumented)
type UsersGetFrequentlyRepliedUsersResponse = operations['users___get-frequently-replied-users']['responses']['200']['content']['application/json']; 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) // @public (undocumented)
type UsersGetSkebStatusRequest = operations['users___get-skeb-status']['requestBody']['content']['application/json']; type UsersGetSkebStatusRequest = operations['users___get-skeb-status']['requestBody']['content']['application/json'];

View file

@ -4248,6 +4248,17 @@ declare module '../api.js' {
credential?: string | null, credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>; ): 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. * Show statistics about a user.
* *

View file

@ -568,6 +568,8 @@ import type {
UsersSearchResponse, UsersSearchResponse,
UsersShowRequest, UsersShowRequest,
UsersShowResponse, UsersShowResponse,
UsersGetSecurityInfoRequest,
UsersGetSecurityInfoResponse,
UsersStatsRequest, UsersStatsRequest,
UsersStatsResponse, UsersStatsResponse,
UsersAchievementsRequest, UsersAchievementsRequest,
@ -977,6 +979,7 @@ export type Endpoints = {
'users/search-by-username-and-host': { req: UsersSearchByUsernameAndHostRequest; res: UsersSearchByUsernameAndHostResponse }; 'users/search-by-username-and-host': { req: UsersSearchByUsernameAndHostRequest; res: UsersSearchByUsernameAndHostResponse };
'users/search': { req: UsersSearchRequest; res: UsersSearchResponse }; 'users/search': { req: UsersSearchRequest; res: UsersSearchResponse };
'users/show': { req: UsersShowRequest; res: UsersShowResponse }; 'users/show': { req: UsersShowRequest; res: UsersShowResponse };
'users/get-security-info': { req: UsersGetSecurityInfoRequest; res: UsersGetSecurityInfoResponse };
'users/stats': { req: UsersStatsRequest; res: UsersStatsResponse }; 'users/stats': { req: UsersStatsRequest; res: UsersStatsResponse };
'users/achievements': { req: UsersAchievementsRequest; res: UsersAchievementsResponse }; 'users/achievements': { req: UsersAchievementsRequest; res: UsersAchievementsResponse };
'users/update-memo': { req: UsersUpdateMemoRequest; res: EmptyResponse }; 'users/update-memo': { req: UsersUpdateMemoRequest; res: EmptyResponse };

View file

@ -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 UsersSearchResponse = operations['users___search']['responses']['200']['content']['application/json'];
export type UsersShowRequest = operations['users___show']['requestBody']['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 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 UsersStatsRequest = operations['users___stats']['requestBody']['content']['application/json'];
export type UsersStatsResponse = operations['users___stats']['responses']['200']['content']['application/json']; export type UsersStatsResponse = operations['users___stats']['responses']['200']['content']['application/json'];
export type UsersAchievementsRequest = operations['users___achievements']['requestBody']['content']['application/json']; export type UsersAchievementsRequest = operations['users___achievements']['requestBody']['content']['application/json'];

View file

@ -2584,6 +2584,13 @@ export type paths = {
post: operations['invite___limit']; post: operations['invite___limit'];
}; };
'/meta': { '/meta': {
/**
* meta
* @description No description provided.
*
* **Credential required**: *No*
*/
get: operations['meta_get'];
/** /**
* meta * meta
* @description No description provided. * @description No description provided.
@ -3287,6 +3294,13 @@ export type paths = {
post: operations['server-info']; post: operations['server-info'];
}; };
'/stats': { '/stats': {
/**
* stats
* @description No description provided.
*
* **Credential required**: *No*
*/
get: operations['stats_get'];
/** /**
* stats * stats
* @description No description provided. * @description No description provided.
@ -3654,6 +3668,15 @@ export type paths = {
*/ */
post: operations['users___show']; 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': {
/** /**
* 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 * meta
* @description No description provided. * @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 * stats
* @description No description provided. * @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 * users/stats
* @description Show statistics about a user. * @description Show statistics about a user.

View file

@ -64,6 +64,7 @@ export type Channels = {
params: { params: {
withRenotes?: boolean; withRenotes?: boolean;
withFiles?: boolean; withFiles?: boolean;
idOnly?: boolean,
}; };
events: { events: {
note: (payload: Note) => void; note: (payload: Note) => void;
@ -75,6 +76,7 @@ export type Channels = {
withRenotes?: boolean; withRenotes?: boolean;
withReplies?: boolean; withReplies?: boolean;
withFiles?: boolean; withFiles?: boolean;
idOnly?: boolean,
}; };
events: { events: {
note: (payload: Note) => void; note: (payload: Note) => void;
@ -86,6 +88,7 @@ export type Channels = {
withRenotes?: boolean; withRenotes?: boolean;
withReplies?: boolean; withReplies?: boolean;
withFiles?: boolean; withFiles?: boolean;
idOnly?: boolean,
}; };
events: { events: {
note: (payload: Note) => void; note: (payload: Note) => void;
@ -96,6 +99,7 @@ export type Channels = {
params: { params: {
withRenotes?: boolean; withRenotes?: boolean;
withFiles?: boolean; withFiles?: boolean;
idOnly?: boolean,
}; };
events: { events: {
note: (payload: Note) => void; note: (payload: Note) => void;
@ -107,6 +111,7 @@ export type Channels = {
listId: string; listId: string;
withFiles?: boolean; withFiles?: boolean;
withRenotes?: boolean; withRenotes?: boolean;
idOnly?: boolean,
}; };
events: { events: {
note: (payload: Note) => void; note: (payload: Note) => void;
@ -125,6 +130,7 @@ export type Channels = {
roleTimeline: { roleTimeline: {
params: { params: {
roleId: string; roleId: string;
idOnly?: boolean,
}; };
events: { events: {
note: (payload: Note) => void; note: (payload: Note) => void;
@ -134,6 +140,7 @@ export type Channels = {
antenna: { antenna: {
params: { params: {
antennaId: string; antennaId: string;
idOnly?: boolean,
}; };
events: { events: {
note: (payload: Note) => void; note: (payload: Note) => void;
@ -143,6 +150,7 @@ export type Channels = {
channel: { channel: {
params: { params: {
channelId: string; channelId: string;
idOnly?: boolean,
}; };
events: { events: {
note: (payload: Note) => void; note: (payload: Note) => void;

1111
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff