summaryrefslogtreecommitdiff
path: root/packages/backend/src
diff options
context:
space:
mode:
authorKagami Sascha Rosylight <saschanaz@outlook.com>2023-02-25 20:04:48 +0100
committerGitHub <noreply@github.com>2023-02-25 20:04:48 +0100
commitb468330ed944cd2aefb93183786855e990bd3df3 (patch)
treeaae515a3d90bc6646854ea718c054540b2b654e9 /packages/backend/src
parentAdd test (diff)
parentrefactor(frontend): fix eslint error (#10084) (diff)
downloadmisskey-b468330ed944cd2aefb93183786855e990bd3df3.tar.gz
misskey-b468330ed944cd2aefb93183786855e990bd3df3.tar.bz2
misskey-b468330ed944cd2aefb93183786855e990bd3df3.zip
Merge branch 'develop' into mkusername-empty
Diffstat (limited to 'packages/backend/src')
-rw-r--r--packages/backend/src/@types/redis-lock.d.ts8
-rw-r--r--packages/backend/src/core/AppLockService.ts2
-rw-r--r--packages/backend/src/core/MfmService.ts14
-rw-r--r--packages/backend/src/core/PushNotificationService.ts8
-rw-r--r--packages/backend/src/core/activitypub/ApInboxService.ts8
-rw-r--r--packages/backend/src/core/activitypub/ApRequestService.ts72
-rw-r--r--packages/backend/src/core/activitypub/type.ts4
-rw-r--r--packages/backend/src/core/entities/NotificationEntityService.ts7
-rw-r--r--packages/backend/src/core/entities/RoleEntityService.ts15
-rw-r--r--packages/backend/src/models/entities/Notification.ts8
-rw-r--r--packages/backend/src/models/entities/UserProfile.ts4
-rw-r--r--packages/backend/src/server/api/ApiCallService.ts4
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts20
-rw-r--r--packages/backend/src/server/api/GetterService.ts4
-rw-r--r--packages/backend/src/server/api/SigninApiService.ts18
-rw-r--r--packages/backend/src/server/api/endpoint-base.ts4
-rw-r--r--packages/backend/src/server/api/endpoints.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/admin/drive/show-file.ts16
-rw-r--r--packages/backend/src/server/api/endpoints/admin/roles/list.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/roles/show.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/roles/users.ts71
-rw-r--r--packages/backend/src/server/api/endpoints/admin/show-user.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/done.ts28
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/key-done.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/password-less.ts47
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/register.ts17
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts18
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/unregister.ts12
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/update-key.ts78
-rw-r--r--packages/backend/src/server/api/endpoints/i/notifications.ts19
-rw-r--r--packages/backend/src/server/api/endpoints/notes/create.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/notes/featured.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/roles/list.ts37
-rw-r--r--packages/backend/src/server/api/endpoints/roles/show.ts52
-rw-r--r--packages/backend/src/server/api/endpoints/roles/users.ts71
-rw-r--r--packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts111
-rw-r--r--packages/backend/src/server/web/manifest.json14
-rw-r--r--packages/backend/src/types.ts3
38 files changed, 642 insertions, 193 deletions
diff --git a/packages/backend/src/@types/redis-lock.d.ts b/packages/backend/src/@types/redis-lock.d.ts
new file mode 100644
index 0000000000..9242656a98
--- /dev/null
+++ b/packages/backend/src/@types/redis-lock.d.ts
@@ -0,0 +1,8 @@
+declare module 'redis-lock' {
+ import type Redis from 'ioredis';
+
+ type Lock = (lockName: string, timeout?: number, taskToPerform?: () => Promise<void>) => void;
+ function redisLock(client: Redis.Redis, retryDelay: number): Lock;
+
+ export = redisLock;
+}
diff --git a/packages/backend/src/core/AppLockService.ts b/packages/backend/src/core/AppLockService.ts
index 5f3072a415..ee179b7f01 100644
--- a/packages/backend/src/core/AppLockService.ts
+++ b/packages/backend/src/core/AppLockService.ts
@@ -12,7 +12,7 @@ const retryDelay = 100;
@Injectable()
export class AppLockService {
- private lock: (key: string, timeout?: number) => Promise<() => void>;
+ private lock: (key: string, timeout?: number, _?: (() => Promise<void>) | undefined) => Promise<() => void>;
constructor(
@Inject(DI.redis)
diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts
index 8d961452c7..9b2d5dc0ff 100644
--- a/packages/backend/src/core/MfmService.ts
+++ b/packages/backend/src/core/MfmService.ts
@@ -1,7 +1,7 @@
import { URL } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
import * as parse5 from 'parse5';
-import { JSDOM } from 'jsdom';
+import { Window } from 'happy-dom';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { intersperse } from '@/misc/prelude/array.js';
@@ -235,7 +235,7 @@ export class MfmService {
return null;
}
- const { window } = new JSDOM('');
+ const { window } = new Window();
const doc = window.document;
@@ -300,7 +300,7 @@ export class MfmService {
hashtag: (node) => {
const a = doc.createElement('a');
- a.href = `${this.config.url}/tags/${node.props.hashtag}`;
+ a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`);
a.textContent = `#${node.props.hashtag}`;
a.setAttribute('rel', 'tag');
return a;
@@ -326,7 +326,7 @@ export class MfmService {
link: (node) => {
const a = doc.createElement('a');
- a.href = node.props.url;
+ a.setAttribute('href', node.props.url);
appendChildren(node.children, a);
return a;
},
@@ -335,7 +335,7 @@ export class MfmService {
const a = doc.createElement('a');
const { username, host, acct } = node.props;
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host);
- a.href = remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${this.config.url}/${acct}`;
+ a.setAttribute('href', remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${this.config.url}/${acct}`);
a.className = 'u-url mention';
a.textContent = acct;
return a;
@@ -360,14 +360,14 @@ export class MfmService {
url: (node) => {
const a = doc.createElement('a');
- a.href = node.props.url;
+ a.setAttribute('href', node.props.url);
a.textContent = node.props.url;
return a;
},
search: (node) => {
const a = doc.createElement('a');
- a.href = `https://www.google.com/search?q=${node.props.query}`;
+ a.setAttribute('href', `https://www.google.com/search?q=${node.props.query}`);
a.textContent = node.props.content;
return a;
},
diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts
index 75bf4b0e01..2cad1bc07e 100644
--- a/packages/backend/src/core/PushNotificationService.ts
+++ b/packages/backend/src/core/PushNotificationService.ts
@@ -9,7 +9,7 @@ import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js';
// Defined also packages/sw/types.ts#L13
-type pushNotificationsTypes = {
+type PushNotificationsTypes = {
'notification': Packed<'Notification'>;
'unreadAntennaNote': {
antenna: { id: string, name: string };
@@ -22,8 +22,8 @@ type pushNotificationsTypes = {
};
// Reduce length because push message servers have character limits
-function truncateBody<T extends keyof pushNotificationsTypes>(type: T, body: pushNotificationsTypes[T]): pushNotificationsTypes[T] {
- if (body === undefined) return body;
+function truncateBody<T extends keyof PushNotificationsTypes>(type: T, body: PushNotificationsTypes[T]): PushNotificationsTypes[T] {
+ if (typeof body !== 'object') return body;
return {
...body,
@@ -56,7 +56,7 @@ export class PushNotificationService {
}
@bindThis
- public async pushNotification<T extends keyof pushNotificationsTypes>(userId: string, type: T, body: pushNotificationsTypes[T]) {
+ public async pushNotification<T extends keyof PushNotificationsTypes>(userId: string, type: T, body: PushNotificationsTypes[T]) {
const meta = await this.metaService.fetch();
if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return;
diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts
index 9bd80b6066..6d9569bce2 100644
--- a/packages/backend/src/core/activitypub/ApInboxService.ts
+++ b/packages/backend/src/core/activitypub/ApInboxService.ts
@@ -450,9 +450,11 @@ export class ApInboxService {
return `skip: delete actor ${actor.uri} !== ${uri}`;
}
- const user = await this.usersRepository.findOneByOrFail({ id: actor.id });
- if (user.isDeleted) {
- this.logger.info('skip: already deleted');
+ const user = await this.usersRepository.findOneBy({ id: actor.id });
+ if (user == null) {
+ return 'skip: actor not found';
+ } else if (user.isDeleted) {
+ return 'skip: already deleted';
}
const job = await this.queueService.createDeleteAccountJob(actor);
diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts
index bfd53dfabf..71fbc29476 100644
--- a/packages/backend/src/core/activitypub/ApRequestService.ts
+++ b/packages/backend/src/core/activitypub/ApRequestService.ts
@@ -28,31 +28,15 @@ type PrivateKey = {
keyId: string;
};
-@Injectable()
-export class ApRequestService {
- private logger: Logger;
-
- constructor(
- @Inject(DI.config)
- private config: Config,
-
- private userKeypairStoreService: UserKeypairStoreService,
- private httpRequestService: HttpRequestService,
- private loggerService: LoggerService,
- ) {
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる
- }
-
- @bindThis
- private createSignedPost(args: { key: PrivateKey, url: string, body: string, additionalHeaders: Record<string, string> }): Signed {
+export class ApRequestCreator {
+ static createSignedPost(args: { key: PrivateKey, url: string, body: string, additionalHeaders: Record<string, string> }): Signed {
const u = new URL(args.url);
const digestHeader = `SHA-256=${crypto.createHash('sha256').update(args.body).digest('base64')}`;
const request: Request = {
url: u.href,
method: 'POST',
- headers: this.objectAssignWithLcKey({
+ headers: this.#objectAssignWithLcKey({
'Date': new Date().toUTCString(),
'Host': u.host,
'Content-Type': 'application/activity+json',
@@ -60,7 +44,7 @@ export class ApRequestService {
}, args.additionalHeaders),
};
- const result = this.signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']);
+ const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']);
return {
request,
@@ -70,21 +54,20 @@ export class ApRequestService {
};
}
- @bindThis
- private createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record<string, string> }): Signed {
+ static createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record<string, string> }): Signed {
const u = new URL(args.url);
const request: Request = {
url: u.href,
method: 'GET',
- headers: this.objectAssignWithLcKey({
+ headers: this.#objectAssignWithLcKey({
'Accept': 'application/activity+json, application/ld+json',
'Date': new Date().toUTCString(),
'Host': new URL(args.url).host,
}, args.additionalHeaders),
};
- const result = this.signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']);
+ const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']);
return {
request,
@@ -94,13 +77,12 @@ export class ApRequestService {
};
}
- @bindThis
- private signToRequest(request: Request, key: PrivateKey, includeHeaders: string[]): Signed {
- const signingString = this.genSigningString(request, includeHeaders);
+ static #signToRequest(request: Request, key: PrivateKey, includeHeaders: string[]): Signed {
+ const signingString = this.#genSigningString(request, includeHeaders);
const signature = crypto.sign('sha256', Buffer.from(signingString), key.privateKeyPem).toString('base64');
const signatureHeader = `keyId="${key.keyId}",algorithm="rsa-sha256",headers="${includeHeaders.join(' ')}",signature="${signature}"`;
- request.headers = this.objectAssignWithLcKey(request.headers, {
+ request.headers = this.#objectAssignWithLcKey(request.headers, {
Signature: signatureHeader,
});
// node-fetch will generate this for us. if we keep 'Host', it won't change with redirects!
@@ -114,9 +96,8 @@ export class ApRequestService {
};
}
- @bindThis
- private genSigningString(request: Request, includeHeaders: string[]): string {
- request.headers = this.lcObjectKey(request.headers);
+ static #genSigningString(request: Request, includeHeaders: string[]): string {
+ request.headers = this.#lcObjectKey(request.headers);
const results: string[] = [];
@@ -131,16 +112,31 @@ export class ApRequestService {
return results.join('\n');
}
- @bindThis
- private lcObjectKey(src: Record<string, string>): Record<string, string> {
+ static #lcObjectKey(src: Record<string, string>): Record<string, string> {
const dst: Record<string, string> = {};
for (const key of Object.keys(src).filter(x => x !== '__proto__' && typeof src[x] === 'string')) dst[key.toLowerCase()] = src[key];
return dst;
}
- @bindThis
- private objectAssignWithLcKey(a: Record<string, string>, b: Record<string, string>): Record<string, string> {
- return Object.assign(this.lcObjectKey(a), this.lcObjectKey(b));
+ static #objectAssignWithLcKey(a: Record<string, string>, b: Record<string, string>): Record<string, string> {
+ return Object.assign(this.#lcObjectKey(a), this.#lcObjectKey(b));
+ }
+}
+
+@Injectable()
+export class ApRequestService {
+ private logger: Logger;
+
+ constructor(
+ @Inject(DI.config)
+ private config: Config,
+
+ private userKeypairStoreService: UserKeypairStoreService,
+ private httpRequestService: HttpRequestService,
+ private loggerService: LoggerService,
+ ) {
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる
}
@bindThis
@@ -149,7 +145,7 @@ export class ApRequestService {
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
- const req = this.createSignedPost({
+ const req = ApRequestCreator.createSignedPost({
key: {
privateKeyPem: keypair.privateKey,
keyId: `${this.config.url}/users/${user.id}#main-key`,
@@ -176,7 +172,7 @@ export class ApRequestService {
public async signedGet(url: string, user: { id: User['id'] }) {
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
- const req = this.createSignedGet({
+ const req = ApRequestCreator.createSignedGet({
key: {
privateKeyPem: keypair.privateKey,
keyId: `${this.config.url}/users/${user.id}#main-key`,
diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts
index 268bf99119..7f2ca9c05e 100644
--- a/packages/backend/src/core/activitypub/type.ts
+++ b/packages/backend/src/core/activitypub/type.ts
@@ -1,8 +1,8 @@
-export type obj = { [x: string]: any };
+export type Obj = { [x: string]: any };
export type ApObject = IObject | string | (IObject | string)[];
export interface IObject {
- '@context'?: string | string[] | obj | obj[];
+ '@context'?: string | string[] | Obj | Obj[];
type: string | string[];
id?: string;
name?: string | null;
diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts
index 34ff52ede8..33c76c6937 100644
--- a/packages/backend/src/core/entities/NotificationEntityService.ts
+++ b/packages/backend/src/core/entities/NotificationEntityService.ts
@@ -94,13 +94,6 @@ export class NotificationEntityService implements OnModuleInit {
}),
reaction: notification.reaction,
} : {}),
- ...(notification.type === 'pollVote' ? { // TODO: そのうち消す
- note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
- detail: true,
- _hint_: options._hintForEachNotes_,
- }),
- choice: notification.choice,
- } : {}),
...(notification.type === 'pollEnded' ? {
note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
detail: true,
diff --git a/packages/backend/src/core/entities/RoleEntityService.ts b/packages/backend/src/core/entities/RoleEntityService.ts
index f2ba642375..80ef5ac1fa 100644
--- a/packages/backend/src/core/entities/RoleEntityService.ts
+++ b/packages/backend/src/core/entities/RoleEntityService.ts
@@ -25,14 +25,7 @@ export class RoleEntityService {
public async pack(
src: Role['id'] | Role,
me?: { id: User['id'] } | null | undefined,
- options?: {
- detail?: boolean;
- },
) {
- const opts = Object.assign({
- detail: true,
- }, options);
-
const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src });
const assigns = await this.roleAssignmentsRepository.findBy({
@@ -65,9 +58,6 @@ export class RoleEntityService {
canEditMembersByModerator: role.canEditMembersByModerator,
policies: policies,
usersCount: assigns.length,
- ...(opts.detail ? {
- users: this.userEntityService.packMany(assigns.map(x => x.userId), me),
- } : {}),
});
}
@@ -75,11 +65,8 @@ export class RoleEntityService {
public packMany(
roles: any[],
me: { id: User['id'] },
- options?: {
- detail?: boolean;
- },
) {
- return Promise.all(roles.map(x => this.pack(x, me, options)));
+ return Promise.all(roles.map(x => this.pack(x, me)));
}
}
diff --git a/packages/backend/src/models/entities/Notification.ts b/packages/backend/src/models/entities/Notification.ts
index 105f0d0407..51117efba5 100644
--- a/packages/backend/src/models/entities/Notification.ts
+++ b/packages/backend/src/models/entities/Notification.ts
@@ -1,5 +1,5 @@
import { Entity, Index, JoinColumn, ManyToOne, Column, PrimaryColumn } from 'typeorm';
-import { notificationTypes } from '@/types.js';
+import { notificationTypes, obsoleteNotificationTypes } from '@/types.js';
import { id } from '../id.js';
import { User } from './User.js';
import { Note } from './Note.js';
@@ -58,7 +58,6 @@ export class Notification {
* renote - 投稿がRenoteされた
* quote - 投稿が引用Renoteされた
* reaction - 投稿にリアクションされた
- * pollVote - 投稿のアンケートに投票された (廃止)
* pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した
* receiveFollowRequest - フォローリクエストされた
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
@@ -67,7 +66,10 @@ export class Notification {
*/
@Index()
@Column('enum', {
- enum: notificationTypes,
+ enum: [
+ ...notificationTypes,
+ ...obsoleteNotificationTypes,
+ ],
comment: 'The type of the Notification.',
})
public type: typeof notificationTypes[number];
diff --git a/packages/backend/src/models/entities/UserProfile.ts b/packages/backend/src/models/entities/UserProfile.ts
index 3d35b4cb5a..60c1c55de5 100644
--- a/packages/backend/src/models/entities/UserProfile.ts
+++ b/packages/backend/src/models/entities/UserProfile.ts
@@ -1,5 +1,5 @@
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
-import { ffVisibility, notificationTypes } from '@/types.js';
+import { obsoleteNotificationTypes, ffVisibility, notificationTypes } from '@/types.js';
import { id } from '../id.js';
import { User } from './User.js';
import { Page } from './Page.js';
@@ -205,7 +205,7 @@ export class UserProfile {
enum: [
...notificationTypes,
// マイグレーションで削除が困難なので古いenumは残しておく
- 'groupInvited',
+ ...obsoleteNotificationTypes,
],
array: true,
default: [],
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index cc27b36966..347fa59d36 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -108,9 +108,9 @@ export class ApiCallService implements OnApplicationShutdown {
const [path] = await createTemp();
await pump(multipartData.file, fs.createWriteStream(path));
- const fields = {} as Record<string, string | undefined>;
+ const fields = {} as Record<string, unknown>;
for (const [k, v] of Object.entries(multipartData.fields)) {
- fields[k] = v.value;
+ fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined;
}
const token = fields['i'];
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 30101a2c60..d3e2219bd5 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -66,6 +66,7 @@ import * as ep___admin_roles_update from './endpoints/admin/roles/update.js';
import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js';
import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
+import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
@@ -170,6 +171,7 @@ import * as ep___i_2fa_keyDone from './endpoints/i/2fa/key-done.js';
import * as ep___i_2fa_passwordLess from './endpoints/i/2fa/password-less.js';
import * as ep___i_2fa_registerKey from './endpoints/i/2fa/register-key.js';
import * as ep___i_2fa_register from './endpoints/i/2fa/register.js';
+import * as ep___i_2fa_updateKey from './endpoints/i/2fa/update-key.js';
import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js';
import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js';
import * as ep___i_apps from './endpoints/i/apps.js';
@@ -276,6 +278,9 @@ import * as ep___flash_myLikes from './endpoints/flash/my-likes.js';
import * as ep___ping from './endpoints/ping.js';
import * as ep___pinnedUsers from './endpoints/pinned-users.js';
import * as ep___promo_read from './endpoints/promo/read.js';
+import * as ep___roles_list from './endpoints/roles/list.js';
+import * as ep___roles_show from './endpoints/roles/show.js';
+import * as ep___roles_users from './endpoints/roles/users.js';
import * as ep___requestResetPassword from './endpoints/request-reset-password.js';
import * as ep___resetDb from './endpoints/reset-db.js';
import * as ep___resetPassword from './endpoints/reset-password.js';
@@ -382,6 +387,7 @@ const $admin_roles_update: Provider = { provide: 'ep:admin/roles/update', useCla
const $admin_roles_assign: Provider = { provide: 'ep:admin/roles/assign', useClass: ep___admin_roles_assign.default };
const $admin_roles_unassign: Provider = { provide: 'ep:admin/roles/unassign', useClass: ep___admin_roles_unassign.default };
const $admin_roles_updateDefaultPolicies: Provider = { provide: 'ep:admin/roles/update-default-policies', useClass: ep___admin_roles_updateDefaultPolicies.default };
+const $admin_roles_users: Provider = { provide: 'ep:admin/roles/users', useClass: ep___admin_roles_users.default };
const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default };
const $antennas_delete: Provider = { provide: 'ep:antennas/delete', useClass: ep___antennas_delete.default };
@@ -486,6 +492,7 @@ const $i_2fa_keyDone: Provider = { provide: 'ep:i/2fa/key-done', useClass: ep___
const $i_2fa_passwordLess: Provider = { provide: 'ep:i/2fa/password-less', useClass: ep___i_2fa_passwordLess.default };
const $i_2fa_registerKey: Provider = { provide: 'ep:i/2fa/register-key', useClass: ep___i_2fa_registerKey.default };
const $i_2fa_register: Provider = { provide: 'ep:i/2fa/register', useClass: ep___i_2fa_register.default };
+const $i_2fa_updateKey: Provider = { provide: 'ep:i/2fa/update-key', useClass: ep___i_2fa_updateKey.default };
const $i_2fa_removeKey: Provider = { provide: 'ep:i/2fa/remove-key', useClass: ep___i_2fa_removeKey.default };
const $i_2fa_unregister: Provider = { provide: 'ep:i/2fa/unregister', useClass: ep___i_2fa_unregister.default };
const $i_apps: Provider = { provide: 'ep:i/apps', useClass: ep___i_apps.default };
@@ -592,6 +599,9 @@ const $flash_myLikes: Provider = { provide: 'ep:flash/my-likes', useClass: ep___
const $ping: Provider = { provide: 'ep:ping', useClass: ep___ping.default };
const $pinnedUsers: Provider = { provide: 'ep:pinned-users', useClass: ep___pinnedUsers.default };
const $promo_read: Provider = { provide: 'ep:promo/read', useClass: ep___promo_read.default };
+const $roles_list: Provider = { provide: 'ep:roles/list', useClass: ep___roles_list.default };
+const $roles_show: Provider = { provide: 'ep:roles/show', useClass: ep___roles_show.default };
+const $roles_users: Provider = { provide: 'ep:roles/users', useClass: ep___roles_users.default };
const $requestResetPassword: Provider = { provide: 'ep:request-reset-password', useClass: ep___requestResetPassword.default };
const $resetDb: Provider = { provide: 'ep:reset-db', useClass: ep___resetDb.default };
const $resetPassword: Provider = { provide: 'ep:reset-password', useClass: ep___resetPassword.default };
@@ -702,6 +712,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$admin_roles_assign,
$admin_roles_unassign,
$admin_roles_updateDefaultPolicies,
+ $admin_roles_users,
$announcements,
$antennas_create,
$antennas_delete,
@@ -806,6 +817,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_2fa_passwordLess,
$i_2fa_registerKey,
$i_2fa_register,
+ $i_2fa_updateKey,
$i_2fa_removeKey,
$i_2fa_unregister,
$i_apps,
@@ -912,6 +924,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$ping,
$pinnedUsers,
$promo_read,
+ $roles_list,
+ $roles_show,
+ $roles_users,
$requestResetPassword,
$resetDb,
$resetPassword,
@@ -1016,6 +1031,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$admin_roles_assign,
$admin_roles_unassign,
$admin_roles_updateDefaultPolicies,
+ $admin_roles_users,
$announcements,
$antennas_create,
$antennas_delete,
@@ -1120,6 +1136,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_2fa_passwordLess,
$i_2fa_registerKey,
$i_2fa_register,
+ $i_2fa_updateKey,
$i_2fa_removeKey,
$i_2fa_unregister,
$i_apps,
@@ -1226,6 +1243,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$ping,
$pinnedUsers,
$promo_read,
+ $roles_list,
+ $roles_show,
+ $roles_users,
$requestResetPassword,
$resetDb,
$resetPassword,
diff --git a/packages/backend/src/server/api/GetterService.ts b/packages/backend/src/server/api/GetterService.ts
index c7f9916f97..c94884a78c 100644
--- a/packages/backend/src/server/api/GetterService.ts
+++ b/packages/backend/src/server/api/GetterService.ts
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { NotesRepository, UsersRepository } from '@/models/index.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
-import type { User } from '@/models/entities/User.js';
+import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
import type { Note } from '@/models/entities/Note.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
@@ -45,7 +45,7 @@ export class GetterService {
throw new IdentifiableError('15348ddd-432d-49c2-8a5a-8069753becff', 'No such user.');
}
- return user;
+ return user as LocalUser | RemoteUser;
}
/**
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index f1164b9957..bd3d8a28da 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -1,7 +1,7 @@
import { randomBytes } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs';
-import * as speakeasy from 'speakeasy';
+import * as OTPAuth from 'otpauth';
import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { UserSecurityKeysRepository, SigninsRepository, UserProfilesRepository, AttestationChallengesRepository, UsersRepository } from '@/models/index.js';
@@ -155,19 +155,19 @@ export class SigninApiService {
});
}
- const verified = (speakeasy as any).totp.verify({
- secret: profile.twoFactorSecret,
- encoding: 'base32',
- token: token,
- window: 2,
+ const delta = OTPAuth.TOTP.validate({
+ secret: OTPAuth.Secret.fromBase32(profile.twoFactorSecret!),
+ digits: 6,
+ token,
+ window: 1,
});
- if (verified) {
- return this.signinService.signin(request, reply, user);
- } else {
+ if (delta === null) {
return await fail(403, {
id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f',
});
+ } else {
+ return this.signinService.signin(request, reply, user);
}
} else if (body.credentialId && body.clientDataJSON && body.authenticatorData && body.signature) {
if (!same && !profile.usePasswordLessLogin) {
diff --git a/packages/backend/src/server/api/endpoint-base.ts b/packages/backend/src/server/api/endpoint-base.ts
index 115526d997..ed283eb834 100644
--- a/packages/backend/src/server/api/endpoint-base.ts
+++ b/packages/backend/src/server/api/endpoint-base.ts
@@ -20,14 +20,14 @@ type File = {
};
// TODO: paramsの型をT['params']のスキーマ定義から推論する
-type executor<T extends IEndpointMeta, Ps extends Schema> =
+type Executor<T extends IEndpointMeta, Ps extends Schema> =
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
public exec: (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => Promise<any>;
- constructor(meta: T, paramDef: Ps, cb: executor<T, Ps>) {
+ constructor(meta: T, paramDef: Ps, cb: Executor<T, Ps>) {
const validate = ajv.compile(paramDef);
this.exec = (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => {
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index d05005b078..4d5ed9fb62 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -66,6 +66,7 @@ import * as ep___admin_roles_update from './endpoints/admin/roles/update.js';
import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js';
import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
+import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
@@ -170,6 +171,7 @@ import * as ep___i_2fa_keyDone from './endpoints/i/2fa/key-done.js';
import * as ep___i_2fa_passwordLess from './endpoints/i/2fa/password-less.js';
import * as ep___i_2fa_registerKey from './endpoints/i/2fa/register-key.js';
import * as ep___i_2fa_register from './endpoints/i/2fa/register.js';
+import * as ep___i_2fa_updateKey from './endpoints/i/2fa/update-key.js';
import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js';
import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js';
import * as ep___i_apps from './endpoints/i/apps.js';
@@ -276,6 +278,9 @@ import * as ep___flash_myLikes from './endpoints/flash/my-likes.js';
import * as ep___ping from './endpoints/ping.js';
import * as ep___pinnedUsers from './endpoints/pinned-users.js';
import * as ep___promo_read from './endpoints/promo/read.js';
+import * as ep___roles_list from './endpoints/roles/list.js';
+import * as ep___roles_show from './endpoints/roles/show.js';
+import * as ep___roles_users from './endpoints/roles/users.js';
import * as ep___requestResetPassword from './endpoints/request-reset-password.js';
import * as ep___resetDb from './endpoints/reset-db.js';
import * as ep___resetPassword from './endpoints/reset-password.js';
@@ -380,6 +385,7 @@ const eps = [
['admin/roles/assign', ep___admin_roles_assign],
['admin/roles/unassign', ep___admin_roles_unassign],
['admin/roles/update-default-policies', ep___admin_roles_updateDefaultPolicies],
+ ['admin/roles/users', ep___admin_roles_users],
['announcements', ep___announcements],
['antennas/create', ep___antennas_create],
['antennas/delete', ep___antennas_delete],
@@ -484,6 +490,7 @@ const eps = [
['i/2fa/password-less', ep___i_2fa_passwordLess],
['i/2fa/register-key', ep___i_2fa_registerKey],
['i/2fa/register', ep___i_2fa_register],
+ ['i/2fa/update-key', ep___i_2fa_updateKey],
['i/2fa/remove-key', ep___i_2fa_removeKey],
['i/2fa/unregister', ep___i_2fa_unregister],
['i/apps', ep___i_apps],
@@ -590,6 +597,9 @@ const eps = [
['ping', ep___ping],
['pinned-users', ep___pinnedUsers],
['promo/read', ep___promo_read],
+ ['roles/list', ep___roles_list],
+ ['roles/show', ep___roles_show],
+ ['roles/users', ep___roles_users],
['request-reset-password', ep___requestResetPassword],
['reset-db', ep___resetDb],
['reset-password', ep___resetPassword],
diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
index 6376cb153c..85b566aabe 100644
--- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
+++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
-import type { DriveFilesRepository } from '@/models/index.js';
+import type { DriveFilesRepository, UsersRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
@@ -161,6 +161,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
+
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -178,7 +181,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchFile);
}
- const isModerator = await this.roleService.isModerator(me);
+ const owner = file.userId ? await this.usersRepository.findOneByOrFail({
+ id: file.userId,
+ }) : null;
+
+ const iAmModerator = await this.roleService.isModerator(me);
+ const ownerIsModerator = owner ? await this.roleService.isModerator(owner) : false;
return {
id: file.id,
@@ -207,8 +215,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
name: file.name,
md5: file.md5,
createdAt: file.createdAt.toISOString(),
- requestIp: isModerator ? file.requestIp : null,
- requestHeaders: isModerator ? file.requestHeaders : null,
+ requestIp: iAmModerator ? file.requestIp : null,
+ requestHeaders: iAmModerator && !ownerIsModerator ? file.requestHeaders : null,
};
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/list.ts b/packages/backend/src/server/api/endpoints/admin/roles/list.ts
index ac56de56b9..edaf638ea9 100644
--- a/packages/backend/src/server/api/endpoints/admin/roles/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/list.ts
@@ -32,7 +32,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const roles = await this.rolesRepository.find({
order: { lastUsedAt: 'DESC' },
});
- return await this.roleEntityService.packMany(roles, me, { detail: false });
+ return await this.roleEntityService.packMany(roles, me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/show.ts b/packages/backend/src/server/api/endpoints/admin/roles/show.ts
index c83f96191d..01028a086f 100644
--- a/packages/backend/src/server/api/endpoints/admin/roles/show.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/show.ts
@@ -39,12 +39,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private roleEntityService: RoleEntityService,
) {
- super(meta, paramDef, async (ps) => {
+ super(meta, paramDef, async (ps, me) => {
const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
if (role == null) {
throw new ApiError(meta.errors.noSuchRole);
}
- return await this.roleEntityService.pack(role);
+ return await this.roleEntityService.pack(role, me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/users.ts b/packages/backend/src/server/api/endpoints/admin/roles/users.ts
new file mode 100644
index 0000000000..bb016a8425
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/roles/users.ts
@@ -0,0 +1,71 @@
+import { Inject, Injectable } from '@nestjs/common';
+import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { QueryService } from '@/core/QueryService.js';
+import { DI } from '@/di-symbols.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { ApiError } from '../../../error.js';
+
+export const meta = {
+ tags: ['admin', 'role', 'users'],
+
+ requireCredential: false,
+ requireAdmin: true,
+
+ errors: {
+ noSuchRole: {
+ message: 'No such role.',
+ code: 'NO_SUCH_ROLE',
+ id: '224eff5e-2488-4b18-b3e7-f50d94421648',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ roleId: { type: 'string', format: 'misskey:id' },
+ sinceId: { type: 'string', format: 'misskey:id' },
+ untilId: { type: 'string', format: 'misskey:id' },
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ },
+ required: ['roleId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.rolesRepository)
+ private rolesRepository: RolesRepository,
+
+ @Inject(DI.roleAssignmentsRepository)
+ private roleAssignmentsRepository: RoleAssignmentsRepository,
+
+ private queryService: QueryService,
+ private userEntityService: UserEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const role = await this.rolesRepository.findOneBy({
+ id: ps.roleId,
+ });
+
+ if (role == null) {
+ throw new ApiError(meta.errors.noSuchRole);
+ }
+
+ const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId)
+ .andWhere('assign.roleId = :roleId', { roleId: role.id })
+ .innerJoinAndSelect('assign.user', 'user');
+
+ const assigns = await query
+ .take(ps.limit)
+ .getMany();
+
+ return await Promise.all(assigns.map(async assign => ({
+ id: assign.id,
+ user: await this.userEntityService.pack(assign.user!, me, { detail: true }),
+ })));
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts
index 823af6d8be..9d19efbbcf 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts
@@ -59,12 +59,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new Error('cannot show info of admin');
}
- if (!await this.roleService.isAdministrator(_me)) {
- return {
- isSuspended: user.isSuspended,
- };
- }
-
const signins = await this.signinsRepository.findBy({ userId: user.id });
const roles = await this.roleService.getUserRoles(user.id);
@@ -89,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
moderationNote: profile.moderationNote,
signins,
policies: await this.roleService.getUserPolicies(user.id),
- roles: await this.roleEntityService.packMany(roles, me, { detail: false }),
+ roles: await this.roleEntityService.packMany(roles, me),
};
});
}
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/done.ts b/packages/backend/src/server/api/endpoints/i/2fa/done.ts
index ec9ac1ef90..6c31075e05 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts
@@ -1,7 +1,10 @@
-import * as speakeasy from 'speakeasy';
+import * as OTPAuth from 'otpauth';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { UserProfilesRepository } from '@/models/index.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
export const meta = {
@@ -22,8 +25,14 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
+ @Inject(DI.config)
+ private config: Config,
+
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
+
+ private userEntityService: UserEntityService,
+ private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const token = ps.token.replace(/\s/g, '');
@@ -34,13 +43,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new Error('二段階認証の設定が開始されていません');
}
- const verified = (speakeasy as any).totp.verify({
- secret: profile.twoFactorTempSecret,
- encoding: 'base32',
- token: token,
+ const delta = OTPAuth.TOTP.validate({
+ secret: OTPAuth.Secret.fromBase32(profile.twoFactorTempSecret),
+ digits: 6,
+ token,
+ window: 1,
});
- if (!verified) {
+ if (delta === null) {
throw new Error('not verified');
}
@@ -48,6 +58,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
twoFactorSecret: profile.twoFactorTempSecret,
twoFactorEnabled: true,
});
+
+ // Publish meUpdated event
+ this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
+ detail: true,
+ includeSecrets: true,
+ }));
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
index 6e0849f2b2..ad33398da6 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
@@ -25,7 +25,7 @@ export const paramDef = {
attestationObject: { type: 'string' },
password: { type: 'string' },
challengeId: { type: 'string' },
- name: { type: 'string' },
+ name: { type: 'string', minLength: 1, maxLength: 30 },
},
required: ['clientDataJSON', 'attestationObject', 'password', 'challengeId', 'name'],
} as const;
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
index 0655a86350..0ee9f556a8 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
@@ -1,12 +1,23 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { UserProfilesRepository } from '@/models/index.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
+import { ApiError } from '../../../error.js';
export const meta = {
requireCredential: true,
secure: true,
+
+ errors: {
+ noKey: {
+ message: 'No security key.',
+ code: 'NO_SECURITY_KEY',
+ id: 'f9c54d7f-d4c2-4d3c-9a8g-a70daac86512',
+ },
+ },
} as const;
export const paramDef = {
@@ -23,11 +34,45 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
+
+ @Inject(DI.userSecurityKeysRepository)
+ private userSecurityKeysRepository: UserSecurityKeysRepository,
+
+ private userEntityService: UserEntityService,
+ private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
+ if (ps.value === true) {
+ // セキュリティキーがなければパスワードレスを有効にはできない
+ const keyCount = await this.userSecurityKeysRepository.count({
+ where: {
+ userId: me.id,
+ },
+ select: {
+ id: true,
+ name: true,
+ lastUsed: true,
+ },
+ });
+
+ if (keyCount === 0) {
+ await this.userProfilesRepository.update(me.id, {
+ usePasswordLessLogin: false,
+ });
+
+ throw new ApiError(meta.errors.noKey);
+ }
+ }
+
await this.userProfilesRepository.update(me.id, {
usePasswordLessLogin: ps.value,
});
+
+ // Publish meUpdated event
+ this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
+ detail: true,
+ includeSecrets: true,
+ }));
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
index a539c5c221..eb4d7f9c14 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
@@ -1,5 +1,5 @@
import bcrypt from 'bcryptjs';
-import * as speakeasy from 'speakeasy';
+import * as OTPAuth from 'otpauth';
import * as QRCode from 'qrcode';
import { Inject, Injectable } from '@nestjs/common';
import type { UserProfilesRepository } from '@/models/index.js';
@@ -42,25 +42,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
// Generate user's secret key
- const secret = speakeasy.generateSecret({
- length: 32,
- });
+ const secret = new OTPAuth.Secret();
await this.userProfilesRepository.update(me.id, {
twoFactorTempSecret: secret.base32,
});
// Get the data URL of the authenticator URL
- const url = speakeasy.otpauthURL({
- secret: secret.base32,
- encoding: 'base32',
+ const totp = new OTPAuth.TOTP({
+ secret,
+ digits: 6,
label: me.username,
issuer: this.config.host,
});
- const dataUrl = await QRCode.toDataURL(url);
+ const url = totp.toString();
+ const qr = await QRCode.toDataURL(url);
return {
- qr: dataUrl,
+ qr,
url,
secret: secret.base32,
label: me.username,
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
index 0f2b0fd7ee..4b726aed80 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
@@ -50,6 +50,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
id: ps.credentialId,
});
+ // 使われているキーがなくなったらパスワードレスログインをやめる
+ const keyCount = await this.userSecurityKeysRepository.count({
+ where: {
+ userId: me.id,
+ },
+ select: {
+ id: true,
+ name: true,
+ lastUsed: true,
+ },
+ });
+
+ if (keyCount === 0) {
+ await this.userProfilesRepository.update(me.id, {
+ usePasswordLessLogin: false,
+ });
+ }
+
// Publish meUpdated event
this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
detail: true,
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
index 4c5b151f78..e0e7ba6658 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
@@ -1,7 +1,9 @@
import bcrypt from 'bcryptjs';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { UserProfilesRepository } from '@/models/index.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
export const meta = {
@@ -24,6 +26,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
+
+ private userEntityService: UserEntityService,
+ private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
@@ -38,7 +43,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
await this.userProfilesRepository.update(me.id, {
twoFactorSecret: null,
twoFactorEnabled: false,
+ usePasswordLessLogin: false,
});
+
+ // Publish meUpdated event
+ this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
+ detail: true,
+ includeSecrets: true,
+ }));
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts
new file mode 100644
index 0000000000..d98f60fa5f
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts
@@ -0,0 +1,78 @@
+import bcrypt from 'bcryptjs';
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '../../../error.js';
+
+export const meta = {
+ requireCredential: true,
+
+ secure: true,
+
+ errors: {
+ noSuchKey: {
+ message: 'No such key.',
+ code: 'NO_SUCH_KEY',
+ id: 'f9c5467f-d492-4d3c-9a8g-a70dacc86512',
+ },
+
+ accessDenied: {
+ message: 'You do not have edit privilege of the channel.',
+ code: 'ACCESS_DENIED',
+ id: '1fb7cb09-d46a-4fff-b8df-057708cce513',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ name: { type: 'string', minLength: 1, maxLength: 30 },
+ credentialId: { type: 'string' },
+ },
+ required: ['name', 'credentialId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.userSecurityKeysRepository)
+ private userSecurityKeysRepository: UserSecurityKeysRepository,
+
+ @Inject(DI.userProfilesRepository)
+ private userProfilesRepository: UserProfilesRepository,
+
+ private userEntityService: UserEntityService,
+ private globalEventService: GlobalEventService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const key = await this.userSecurityKeysRepository.findOneBy({
+ id: ps.credentialId,
+ });
+
+ if (key == null) {
+ throw new ApiError(meta.errors.noSuchKey);
+ }
+
+ if (key.userId !== me.id) {
+ throw new ApiError(meta.errors.accessDenied);
+ }
+
+ await this.userSecurityKeysRepository.update(key.id, {
+ name: ps.name,
+ });
+
+ // Publish meUpdated event
+ this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
+ detail: true,
+ includeSecrets: true,
+ }));
+
+ return {};
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts
index 706e0d2089..e3897d38bd 100644
--- a/packages/backend/src/server/api/endpoints/i/notifications.ts
+++ b/packages/backend/src/server/api/endpoints/i/notifications.ts
@@ -1,7 +1,7 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, NotificationsRepository } from '@/models/index.js';
-import { notificationTypes } from '@/types.js';
+import { obsoleteNotificationTypes, notificationTypes } from '@/types.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteReadService } from '@/core/NoteReadService.js';
@@ -41,11 +41,12 @@ export const paramDef = {
following: { type: 'boolean', default: false },
unreadOnly: { type: 'boolean', default: false },
markAsRead: { type: 'boolean', default: true },
+ // 後方互換のため、廃止された通知タイプも受け付ける
includeTypes: { type: 'array', items: {
- type: 'string', enum: notificationTypes,
+ type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes],
} },
excludeTypes: { type: 'array', items: {
- type: 'string', enum: notificationTypes,
+ type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes],
} },
},
required: [],
@@ -84,6 +85,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (notificationTypes.every(type => ps.excludeTypes?.includes(type))) {
return [];
}
+
+ const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
+ const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
+
const followingQuery = this.followingsRepository.createQueryBuilder('following')
.select('following.followeeId')
.where('following.followerId = :followerId', { followerId: me.id });
@@ -143,10 +148,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
query.setParameters(followingQuery.getParameters());
}
- if (ps.includeTypes && ps.includeTypes.length > 0) {
- query.andWhere('notification.type IN (:...includeTypes)', { includeTypes: ps.includeTypes });
- } else if (ps.excludeTypes && ps.excludeTypes.length > 0) {
- query.andWhere('notification.type NOT IN (:...excludeTypes)', { excludeTypes: ps.excludeTypes });
+ if (includeTypes && includeTypes.length > 0) {
+ query.andWhere('notification.type IN (:...includeTypes)', { includeTypes });
+ } else if (excludeTypes && excludeTypes.length > 0) {
+ query.andWhere('notification.type NOT IN (:...excludeTypes)', { excludeTypes });
}
if (ps.unreadOnly) {
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index 593444968e..f4c5a84a4f 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -79,6 +79,12 @@ export const meta = {
code: 'YOU_HAVE_BEEN_BLOCKED',
id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3',
},
+
+ noSuchFile: {
+ message: 'Some files are not found.',
+ code: 'NO_SUCH_FILE',
+ id: 'b6992544-63e7-67f0-fa7f-32444b1b5306',
+ },
},
} as const;
@@ -207,6 +213,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.orderBy('array_position(ARRAY[:...fileIds], "id"::text)')
.setParameters({ fileIds })
.getMany();
+
+ if (files.length !== fileIds.length) {
+ throw new ApiError(meta.errors.noSuchFile);
+ }
}
let renote: Note | null = null;
diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts
index 8eff8fdb22..cf939f6631 100644
--- a/packages/backend/src/server/api/endpoints/notes/featured.ts
+++ b/packages/backend/src/server/api/endpoints/notes/featured.ts
@@ -28,6 +28,7 @@ export const paramDef = {
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
offset: { type: 'integer', default: 0 },
+ channelId: { type: 'string', nullable: true, format: 'misskey:id' },
},
required: [],
} as const;
@@ -63,12 +64,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
+ if (ps.channelId) query.andWhere('note.channelId = :channelId', { channelId: ps.channelId });
+
if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateBlockedUserQuery(query, me);
let notes = await query
.orderBy('note.score', 'DESC')
- .take(ps.limit)
+ .take(50)
.getMany();
notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
diff --git a/packages/backend/src/server/api/endpoints/roles/list.ts b/packages/backend/src/server/api/endpoints/roles/list.ts
new file mode 100644
index 0000000000..d61c6b8dc6
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/roles/list.ts
@@ -0,0 +1,37 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { RolesRepository } from '@/models/index.js';
+import { DI } from '@/di-symbols.js';
+import { RoleEntityService } from '@/core/entities/RoleEntityService.js';
+
+export const meta = {
+ tags: ['role'],
+
+ requireCredential: true,
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ },
+ required: [
+ ],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.rolesRepository)
+ private rolesRepository: RolesRepository,
+
+ private roleEntityService: RoleEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const roles = await this.rolesRepository.findBy({
+ isPublic: true,
+ });
+ return await this.roleEntityService.packMany(roles, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/roles/show.ts b/packages/backend/src/server/api/endpoints/roles/show.ts
new file mode 100644
index 0000000000..cc755dcc76
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/roles/show.ts
@@ -0,0 +1,52 @@
+import { Inject, Injectable } from '@nestjs/common';
+import type { RolesRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { RoleEntityService } from '@/core/entities/RoleEntityService.js';
+import { ApiError } from '../../error.js';
+
+export const meta = {
+ tags: ['role', 'users'],
+
+ requireCredential: false,
+
+ errors: {
+ noSuchRole: {
+ message: 'No such role.',
+ code: 'NO_SUCH_ROLE',
+ id: 'de5502bf-009a-4639-86c1-fec349e46dcb',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ roleId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['roleId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.rolesRepository)
+ private rolesRepository: RolesRepository,
+
+ private roleEntityService: RoleEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const role = await this.rolesRepository.findOneBy({
+ id: ps.roleId,
+ isPublic: true,
+ });
+
+ if (role == null) {
+ throw new ApiError(meta.errors.noSuchRole);
+ }
+
+ return await this.roleEntityService.pack(role, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/roles/users.ts b/packages/backend/src/server/api/endpoints/roles/users.ts
new file mode 100644
index 0000000000..6e221b6c67
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/roles/users.ts
@@ -0,0 +1,71 @@
+import { Inject, Injectable } from '@nestjs/common';
+import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { QueryService } from '@/core/QueryService.js';
+import { DI } from '@/di-symbols.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { ApiError } from '../../error.js';
+
+export const meta = {
+ tags: ['role', 'users'],
+
+ requireCredential: false,
+
+ errors: {
+ noSuchRole: {
+ message: 'No such role.',
+ code: 'NO_SUCH_ROLE',
+ id: '30aaaee3-4792-48dc-ab0d-cf501a575ac5',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ roleId: { type: 'string', format: 'misskey:id' },
+ sinceId: { type: 'string', format: 'misskey:id' },
+ untilId: { type: 'string', format: 'misskey:id' },
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ },
+ required: ['roleId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.rolesRepository)
+ private rolesRepository: RolesRepository,
+
+ @Inject(DI.roleAssignmentsRepository)
+ private roleAssignmentsRepository: RoleAssignmentsRepository,
+
+ private queryService: QueryService,
+ private userEntityService: UserEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const role = await this.rolesRepository.findOneBy({
+ id: ps.roleId,
+ isPublic: true,
+ });
+
+ if (role == null) {
+ throw new ApiError(meta.errors.noSuchRole);
+ }
+
+ const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId)
+ .andWhere('assign.roleId = :roleId', { roleId: role.id })
+ .innerJoinAndSelect('assign.user', 'user');
+
+ const assigns = await query
+ .take(ps.limit)
+ .getMany();
+
+ return await Promise.all(assigns.map(async assign => ({
+ id: assign.id,
+ user: await this.userEntityService.pack(assign.user!, me, { detail: true }),
+ })));
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
index 8b22f913d2..1cefcf2707 100644
--- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
+++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
@@ -1,6 +1,7 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository, FollowingsRepository } from '@/models/index.js';
+import type { Config } from '@/config.js';
import type { User } from '@/models/entities/User.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
@@ -36,13 +37,13 @@ export const paramDef = {
properties: {
username: { type: 'string', nullable: true },
},
- required: ['username']
+ required: ['username'],
},
{
properties: {
host: { type: 'string', nullable: true },
},
- required: ['host']
+ required: ['host'],
},
],
} as const;
@@ -53,6 +54,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
+ @Inject(DI.config)
+ private config: Config,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -62,79 +66,76 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private userEntityService: UserEntityService,
) {
super(meta, paramDef, async (ps, me) => {
- const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
-
- if (ps.host) {
- const q = this.usersRepository.createQueryBuilder('user')
- .where('user.isSuspended = FALSE')
- .andWhere('user.host LIKE :host', { host: sqlLikeEscape(ps.host.toLowerCase()) + '%' });
-
+ const setUsernameAndHostQuery = (query = this.usersRepository.createQueryBuilder('user')) => {
if (ps.username) {
- q.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' });
+ query.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' });
}
- q.andWhere('user.updatedAt IS NOT NULL');
- q.orderBy('user.updatedAt', 'DESC');
+ if (ps.host) {
+ if (ps.host === this.config.hostname || ps.host === '.') {
+ query.andWhere('user.host IS NULL');
+ } else {
+ query.andWhere('user.host LIKE :host', {
+ host: sqlLikeEscape(ps.host.toLowerCase()) + '%',
+ });
+ }
+ }
- const users = await q.take(ps.limit).getMany();
+ return query;
+ };
- return await this.userEntityService.packMany(users, me, { detail: ps.detail });
- } else if (ps.username) {
- let users: User[] = [];
+ const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
- if (me) {
- const followingQuery = this.followingsRepository.createQueryBuilder('following')
- .select('following.followeeId')
- .where('following.followerId = :followerId', { followerId: me.id });
+ let users: User[] = [];
- const query = this.usersRepository.createQueryBuilder('user')
- .where(`user.id IN (${ followingQuery.getQuery() })`)
- .andWhere('user.id != :meId', { meId: me.id })
- .andWhere('user.isSuspended = FALSE')
- .andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' })
- .andWhere(new Brackets(qb => { qb
- .where('user.updatedAt IS NULL')
- .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
- }));
+ if (me) {
+ const followingQuery = this.followingsRepository.createQueryBuilder('following')
+ .select('following.followeeId')
+ .where('following.followerId = :followerId', { followerId: me.id });
- query.setParameters(followingQuery.getParameters());
+ const query = setUsernameAndHostQuery()
+ .andWhere(`user.id IN (${ followingQuery.getQuery() })`)
+ .andWhere('user.id != :meId', { meId: me.id })
+ .andWhere('user.isSuspended = FALSE')
+ .andWhere(new Brackets(qb => { qb
+ .where('user.updatedAt IS NULL')
+ .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
+ }));
- users = await query
- .orderBy('user.usernameLower', 'ASC')
- .take(ps.limit)
- .getMany();
+ query.setParameters(followingQuery.getParameters());
- if (users.length < ps.limit) {
- const otherQuery = await this.usersRepository.createQueryBuilder('user')
- .where(`user.id NOT IN (${ followingQuery.getQuery() })`)
- .andWhere('user.id != :meId', { meId: me.id })
- .andWhere('user.isSuspended = FALSE')
- .andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' })
- .andWhere('user.updatedAt IS NOT NULL');
+ users = await query
+ .orderBy('user.usernameLower', 'ASC')
+ .take(ps.limit)
+ .getMany();
- otherQuery.setParameters(followingQuery.getParameters());
+ if (users.length < ps.limit) {
+ const otherQuery = setUsernameAndHostQuery()
+ .andWhere(`user.id NOT IN (${ followingQuery.getQuery() })`)
+ .andWhere('user.isSuspended = FALSE')
+ .andWhere('user.updatedAt IS NOT NULL');
- const otherUsers = await otherQuery
- .orderBy('user.updatedAt', 'DESC')
- .take(ps.limit - users.length)
- .getMany();
+ otherQuery.setParameters(followingQuery.getParameters());
- users = users.concat(otherUsers);
- }
- } else {
- users = await this.usersRepository.createQueryBuilder('user')
- .where('user.isSuspended = FALSE')
- .andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' })
- .andWhere('user.updatedAt IS NOT NULL')
+ const otherUsers = await otherQuery
.orderBy('user.updatedAt', 'DESC')
.take(ps.limit - users.length)
.getMany();
+
+ users = users.concat(otherUsers);
}
+ } else {
+ const query = setUsernameAndHostQuery()
+ .andWhere('user.isSuspended = FALSE')
+ .andWhere('user.updatedAt IS NOT NULL');
- return await this.userEntityService.packMany(users, me, { detail: !!ps.detail });
+ users = await query
+ .orderBy('user.updatedAt', 'DESC')
+ .take(ps.limit - users.length)
+ .getMany();
}
- return [];
+ return await this.userEntityService.packMany(users, me, { detail: !!ps.detail });
});
}
}
diff --git a/packages/backend/src/server/web/manifest.json b/packages/backend/src/server/web/manifest.json
index 48030a2980..41171d62a1 100644
--- a/packages/backend/src/server/web/manifest.json
+++ b/packages/backend/src/server/web/manifest.json
@@ -9,16 +9,26 @@
{
"src": "/static-assets/icons/192.png",
"sizes": "192x192",
- "type": "image/png"
+ "type": "image/png",
+ "purpose": "maskable"
},
{
"src": "/static-assets/icons/512.png",
"sizes": "512x512",
- "type": "image/png"
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/static-assets/splash.png",
+ "sizes": "300x300",
+ "type": "image/png",
+ "purpose": "any"
}
],
"share_target": {
"action": "/share/",
+ "method": "GET",
+ "enctype": "application/x-www-form-urlencoded",
"params": {
"title": "title",
"text": "text",
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index 8ac65a021c..7c6a1e5199 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -1,4 +1,5 @@
-export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] as const;
+export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] as const;
+export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;