diff options
Diffstat (limited to 'packages/backend/src/server/api/endpoints/i')
51 files changed, 1692 insertions, 1035 deletions
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 35806b2bc3..bcf3931b04 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts @@ -1,6 +1,8 @@ import * as speakeasy from 'speakeasy'; -import define from '../../../define.js'; -import { UserProfiles } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserProfilesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -17,27 +19,35 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const token = ps.token.replace(/\s/g, ''); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const token = ps.token.replace(/\s/g, ''); - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - if (profile.twoFactorTempSecret == null) { - throw new Error('二段階認証の設定が開始されていません'); - } + if (profile.twoFactorTempSecret == null) { + throw new Error('二段階認証の設定が開始されていません'); + } - const verified = (speakeasy as any).totp.verify({ - secret: profile.twoFactorTempSecret, - encoding: 'base32', - token: token, - }); + const verified = (speakeasy as any).totp.verify({ + secret: profile.twoFactorTempSecret, + encoding: 'base32', + token: token, + }); - if (!verified) { - throw new Error('not verified'); - } + if (!verified) { + throw new Error('not verified'); + } - await UserProfiles.update(user.id, { - twoFactorSecret: profile.twoFactorTempSecret, - twoFactorEnabled: true, - }); -}); + await this.userProfilesRepository.update(me.id, { + twoFactorSecret: profile.twoFactorTempSecret, + twoFactorEnabled: 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 1afb34bfda..f2f4c2044e 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 @@ -1,19 +1,16 @@ -import bcrypt from 'bcryptjs'; import { promisify } from 'node:util'; +import bcrypt from 'bcryptjs'; import * as cbor from 'cbor'; -import define from '../../../define.js'; -import { - UserProfiles, - UserSecurityKeys, - AttestationChallenges, - Users, -} from '@/models/index.js'; -import config from '@/config/index.js'; -import { procedures, hash } from '../../../2fa.js'; -import { publishMainStream } from '@/services/stream.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js'; +import { AttestationChallengesRepository, UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js'; const cborDecodeFirst = promisify(cbor.decodeFirst) as any; -const rpIdHashReal = hash(Buffer.from(config.hostname, 'utf-8')); export const meta = { requireCredential: true, @@ -34,110 +31,135 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.config) + private config: Config, - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, - if (!same) { - throw new Error('incorrect password'); - } + @Inject(DI.userSecurityKeysRepository) + private userSecurityKeysRepository: UserSecurityKeysRepository, - if (!profile.twoFactorEnabled) { - throw new Error('2fa not enabled'); - } + @Inject(DI.attestationChallengesRepository) + private attestationChallengesRepository: AttestationChallengesRepository, - const clientData = JSON.parse(ps.clientDataJSON); + private userEntityService: UserEntityService, + private globalEventService: GlobalEventService, + private twoFactorAuthenticationService: TwoFactorAuthenticationService, + ) { + super(meta, paramDef, async (ps, me) => { + const rpIdHashReal = this.twoFactorAuthenticationService.hash(Buffer.from(this.config.hostname, 'utf-8')); - if (clientData.type !== 'webauthn.create') { - throw new Error('not a creation attestation'); - } - if (clientData.origin !== config.scheme + '://' + config.host) { - throw new Error('origin mismatch'); - } + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - const clientDataJSONHash = hash(Buffer.from(ps.clientDataJSON, 'utf-8')); + // Compare password + const same = await bcrypt.compare(ps.password, profile.password!); - const attestation = await cborDecodeFirst(ps.attestationObject); + if (!same) { + throw new Error('incorrect password'); + } - const rpIdHash = attestation.authData.slice(0, 32); - if (!rpIdHashReal.equals(rpIdHash)) { - throw new Error('rpIdHash mismatch'); - } + if (!profile.twoFactorEnabled) { + throw new Error('2fa not enabled'); + } - const flags = attestation.authData[32]; + const clientData = JSON.parse(ps.clientDataJSON); - // eslint:disable-next-line:no-bitwise - if (!(flags & 1)) { - throw new Error('user not present'); - } + if (clientData.type !== 'webauthn.create') { + throw new Error('not a creation attestation'); + } + if (clientData.origin !== this.config.scheme + '://' + this.config.host) { + throw new Error('origin mismatch'); + } - const authData = Buffer.from(attestation.authData); - const credentialIdLength = authData.readUInt16BE(53); - const credentialId = authData.slice(55, 55 + credentialIdLength); - const publicKeyData = authData.slice(55 + credentialIdLength); - const publicKey: Map<number, any> = await cborDecodeFirst(publicKeyData); - if (publicKey.get(3) !== -7) { - throw new Error('alg mismatch'); - } + const clientDataJSONHash = this.twoFactorAuthenticationService.hash(Buffer.from(ps.clientDataJSON, 'utf-8')); - if (!(procedures as any)[attestation.fmt]) { - throw new Error('unsupported fmt'); - } + const attestation = await cborDecodeFirst(ps.attestationObject); - const verificationData = (procedures as any)[attestation.fmt].verify({ - attStmt: attestation.attStmt, - authenticatorData: authData, - clientDataHash: clientDataJSONHash, - credentialId, - publicKey, - rpIdHash, - }); - if (!verificationData.valid) throw new Error('signature invalid'); + const rpIdHash = attestation.authData.slice(0, 32); + if (!rpIdHashReal.equals(rpIdHash)) { + throw new Error('rpIdHash mismatch'); + } - const attestationChallenge = await AttestationChallenges.findOneBy({ - userId: user.id, - id: ps.challengeId, - registrationChallenge: true, - challenge: hash(clientData.challenge).toString('hex'), - }); + const flags = attestation.authData[32]; - if (!attestationChallenge) { - throw new Error('non-existent challenge'); - } + // eslint:disable-next-line:no-bitwise + if (!(flags & 1)) { + throw new Error('user not present'); + } + + const authData = Buffer.from(attestation.authData); + const credentialIdLength = authData.readUInt16BE(53); + const credentialId = authData.slice(55, 55 + credentialIdLength); + const publicKeyData = authData.slice(55 + credentialIdLength); + const publicKey: Map<number, any> = await cborDecodeFirst(publicKeyData); + if (publicKey.get(3) !== -7) { + throw new Error('alg mismatch'); + } + + const procedures = this.twoFactorAuthenticationService.getProcedures(); + + if (!(procedures as any)[attestation.fmt]) { + throw new Error('unsupported fmt'); + } + + const verificationData = (procedures as any)[attestation.fmt].verify({ + attStmt: attestation.attStmt, + authenticatorData: authData, + clientDataHash: clientDataJSONHash, + credentialId, + publicKey, + rpIdHash, + }); + if (!verificationData.valid) throw new Error('signature invalid'); - await AttestationChallenges.delete({ - userId: user.id, - id: ps.challengeId, - }); + const attestationChallenge = await this.attestationChallengesRepository.findOneBy({ + userId: me.id, + id: ps.challengeId, + registrationChallenge: true, + challenge: this.twoFactorAuthenticationService.hash(clientData.challenge).toString('hex'), + }); - // Expired challenge (> 5min old) - if ( - new Date().getTime() - attestationChallenge.createdAt.getTime() >= + if (!attestationChallenge) { + throw new Error('non-existent challenge'); + } + + await this.attestationChallengesRepository.delete({ + userId: me.id, + id: ps.challengeId, + }); + + // Expired challenge (> 5min old) + if ( + new Date().getTime() - attestationChallenge.createdAt.getTime() >= 5 * 60 * 1000 - ) { - throw new Error('expired challenge'); - } + ) { + throw new Error('expired challenge'); + } - const credentialIdString = credentialId.toString('hex'); + const credentialIdString = credentialId.toString('hex'); - await UserSecurityKeys.insert({ - userId: user.id, - id: credentialIdString, - lastUsed: new Date(), - name: ps.name, - publicKey: verificationData.publicKey.toString('hex'), - }); + await this.userSecurityKeysRepository.insert({ + userId: me.id, + id: credentialIdString, + lastUsed: new Date(), + name: ps.name, + publicKey: verificationData.publicKey.toString('hex'), + }); - // Publish meUpdated event - publishMainStream(user.id, 'meUpdated', await Users.pack(user.id, user, { - detail: true, - includeSecrets: true, - })); + // Publish meUpdated event + this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { + detail: true, + includeSecrets: true, + })); - return { - id: credentialIdString, - name: ps.name, - }; -}); + return { + id: credentialIdString, + name: ps.name, + }; + }); + } +} 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 4bfa24f97f..3eb9f43c2b 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,5 +1,7 @@ -import define from '../../../define.js'; -import { UserProfiles } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserProfilesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -16,8 +18,16 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - await UserProfiles.update(user.id, { - usePasswordLessLogin: ps.value, - }); -}); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + await this.userProfilesRepository.update(me.id, { + usePasswordLessLogin: ps.value, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts index e906b82043..df37db4c6a 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts @@ -1,10 +1,12 @@ -import bcrypt from 'bcryptjs'; -import define from '../../../define.js'; -import { UserProfiles, AttestationChallenges } from '@/models/index.js'; import { promisify } from 'node:util'; import * as crypto from 'node:crypto'; -import { genId } from '@/misc/gen-id.js'; -import { hash } from '../../../2fa.js'; +import bcrypt from 'bcryptjs'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserProfilesRepository, AttestationChallengesRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js'; +import { DI } from '@/di-symbols.js'; const randomBytes = promisify(crypto.randomBytes); @@ -23,39 +25,53 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); + @Inject(DI.attestationChallengesRepository) + private attestationChallengesRepository: AttestationChallengesRepository, - if (!same) { - throw new Error('incorrect password'); - } + private idService: IdService, + private twoFactorAuthenticationService: TwoFactorAuthenticationService, + ) { + super(meta, paramDef, async (ps, me) => { + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - if (!profile.twoFactorEnabled) { - throw new Error('2fa not enabled'); - } + // Compare password + const same = await bcrypt.compare(ps.password, profile.password!); - // 32 byte challenge - const entropy = await randomBytes(32); - const challenge = entropy.toString('base64') - .replace(/=/g, '') - .replace(/\+/g, '-') - .replace(/\//g, '_'); + if (!same) { + throw new Error('incorrect password'); + } - const challengeId = genId(); + if (!profile.twoFactorEnabled) { + throw new Error('2fa not enabled'); + } - await AttestationChallenges.insert({ - userId: user.id, - id: challengeId, - challenge: hash(Buffer.from(challenge, 'utf-8')).toString('hex'), - createdAt: new Date(), - registrationChallenge: true, - }); + // 32 byte challenge + const entropy = await randomBytes(32); + const challenge = entropy.toString('base64') + .replace(/=/g, '') + .replace(/\+/g, '-') + .replace(/\//g, '_'); - return { - challengeId, - challenge, - }; -}); + const challengeId = this.idService.genId(); + + await this.attestationChallengesRepository.insert({ + userId: me.id, + id: challengeId, + challenge: this.twoFactorAuthenticationService.hash(Buffer.from(challenge, 'utf-8')).toString('hex'), + createdAt: new Date(), + registrationChallenge: true, + }); + + return { + challengeId, + challenge, + }; + }); + } +} 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 33f5717728..e20911f35e 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts @@ -1,9 +1,11 @@ import bcrypt from 'bcryptjs'; import * as speakeasy from 'speakeasy'; import * as QRCode from 'qrcode'; -import config from '@/config/index.js'; -import { UserProfiles } from '@/models/index.js'; -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UserProfilesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { Config } from '@/config.js'; export const meta = { requireCredential: true, @@ -20,39 +22,50 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.config) + private config: Config, - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - if (!same) { - throw new Error('incorrect password'); - } + // Compare password + const same = await bcrypt.compare(ps.password, profile.password!); + + if (!same) { + throw new Error('incorrect password'); + } - // Generate user's secret key - const secret = speakeasy.generateSecret({ - length: 32, - }); + // Generate user's secret key + const secret = speakeasy.generateSecret({ + length: 32, + }); - await UserProfiles.update(user.id, { - twoFactorTempSecret: secret.base32, - }); + 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', - label: user.username, - issuer: config.host, - }); - const dataUrl = await QRCode.toDataURL(url); + // Get the data URL of the authenticator URL + const url = speakeasy.otpauthURL({ + secret: secret.base32, + encoding: 'base32', + label: me.username, + issuer: this.config.host, + }); + const dataUrl = await QRCode.toDataURL(url); - return { - qr: dataUrl, - url, - secret: secret.base32, - label: user.username, - issuer: config.host, - }; -}); + return { + qr: dataUrl, + url, + secret: secret.base32, + label: me.username, + issuer: this.config.host, + }; + }); + } +} 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 eb2f75308d..1889dd7893 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 @@ -1,7 +1,11 @@ import bcrypt from 'bcryptjs'; -import define from '../../../define.js'; -import { UserProfiles, UserSecurityKeys, Users } from '@/models/index.js'; -import { publishMainStream } from '@/services/stream.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -19,27 +23,41 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.userSecurityKeysRepository) + private userSecurityKeysRepository: UserSecurityKeysRepository, - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, - if (!same) { - throw new Error('incorrect password'); - } + private userEntityService: UserEntityService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); + + // Compare password + const same = await bcrypt.compare(ps.password, profile.password!); - // Make sure we only delete the user's own creds - await UserSecurityKeys.delete({ - userId: user.id, - id: ps.credentialId, - }); + if (!same) { + throw new Error('incorrect password'); + } - // Publish meUpdated event - publishMainStream(user.id, 'meUpdated', await Users.pack(user.id, user, { - detail: true, - includeSecrets: true, - })); + // Make sure we only delete the user's own creds + await this.userSecurityKeysRepository.delete({ + userId: me.id, + id: ps.credentialId, + }); - return {}; -}); + // 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/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts index 45e7a98639..4607e5d981 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts @@ -1,6 +1,8 @@ import bcrypt from 'bcryptjs'; -import define from '../../../define.js'; -import { UserProfiles } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserProfilesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -17,18 +19,26 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); + // Compare password + const same = await bcrypt.compare(ps.password, profile.password!); - if (!same) { - throw new Error('incorrect password'); - } + if (!same) { + throw new Error('incorrect password'); + } - await UserProfiles.update(user.id, { - twoFactorSecret: null, - twoFactorEnabled: false, - }); -}); + await this.userProfilesRepository.update(me.id, { + twoFactorSecret: null, + twoFactorEnabled: false, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts index eca9558847..8d5851659b 100644 --- a/packages/backend/src/server/api/endpoints/i/apps.ts +++ b/packages/backend/src/server/api/endpoints/i/apps.ts @@ -1,5 +1,7 @@ -import define from '../../define.js'; -import { AccessTokens } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AccessTokensRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -16,25 +18,33 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = AccessTokens.createQueryBuilder('token') - .where('token.userId = :userId', { userId: user.id }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.accessTokensRepository) + private accessTokensRepository: AccessTokensRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.accessTokensRepository.createQueryBuilder('token') + .where('token.userId = :userId', { userId: me.id }); - switch (ps.sort) { - case '+createdAt': query.orderBy('token.createdAt', 'DESC'); break; - case '-createdAt': query.orderBy('token.createdAt', 'ASC'); break; - case '+lastUsedAt': query.orderBy('token.lastUsedAt', 'DESC'); break; - case '-lastUsedAt': query.orderBy('token.lastUsedAt', 'ASC'); break; - default: query.orderBy('token.id', 'ASC'); break; - } + switch (ps.sort) { + case '+createdAt': query.orderBy('token.createdAt', 'DESC'); break; + case '-createdAt': query.orderBy('token.createdAt', 'ASC'); break; + case '+lastUsedAt': query.orderBy('token.lastUsedAt', 'DESC'); break; + case '-lastUsedAt': query.orderBy('token.lastUsedAt', 'ASC'); break; + default: query.orderBy('token.id', 'ASC'); break; + } - const tokens = await query.getMany(); + const tokens = await query.getMany(); - return await Promise.all(tokens.map(token => ({ - id: token.id, - name: token.name, - createdAt: token.createdAt, - lastUsedAt: token.lastUsedAt, - permission: token.permission, - }))); -}); + return await Promise.all(tokens.map(token => ({ + id: token.id, + name: token.name, + createdAt: token.createdAt, + lastUsedAt: token.lastUsedAt, + permission: token.permission, + }))); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts index 68bd103a6d..a5592d20de 100644 --- a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts +++ b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts @@ -1,5 +1,8 @@ -import define from '../../define.js'; -import { AccessTokens, Apps } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AccessTokensRepository } from '@/models/index.js'; +import { AppEntityService } from '@/core/entities/AppEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -12,26 +15,36 @@ export const paramDef = { properties: { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, offset: { type: 'integer', default: 0 }, - sort: { type: 'string', enum: ['desc', 'asc'], default: "desc" }, + sort: { type: 'string', enum: ['desc', 'asc'], default: 'desc' }, }, required: [], } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Get tokens - const tokens = await AccessTokens.find({ - where: { - userId: user.id, - }, - take: ps.limit, - skip: ps.offset, - order: { - id: ps.sort === 'asc' ? 1 : -1, - }, - }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.accessTokensRepository) + private accessTokensRepository: AccessTokensRepository, - return await Promise.all(tokens.map(token => Apps.pack(token.appId, user, { - detail: true, - }))); -}); + private appEntityService: AppEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + // Get tokens + const tokens = await this.accessTokensRepository.find({ + where: { + userId: me.id, + }, + take: ps.limit, + skip: ps.offset, + order: { + id: ps.sort === 'asc' ? 1 : -1, + }, + }); + + return await Promise.all(tokens.map(token => this.appEntityService.pack(token.appId, me, { + detail: true, + }))); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/change-password.ts b/packages/backend/src/server/api/endpoints/i/change-password.ts index f9f6a33a80..cc5b712ecf 100644 --- a/packages/backend/src/server/api/endpoints/i/change-password.ts +++ b/packages/backend/src/server/api/endpoints/i/change-password.ts @@ -1,6 +1,8 @@ import bcrypt from 'bcryptjs'; -import define from '../../define.js'; -import { UserProfiles } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserProfilesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -18,21 +20,29 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - // Compare password - const same = await bcrypt.compare(ps.currentPassword, profile.password!); + // Compare password + const same = await bcrypt.compare(ps.currentPassword, profile.password!); - if (!same) { - throw new Error('incorrect password'); - } + if (!same) { + throw new Error('incorrect password'); + } - // Generate hash of password - const salt = await bcrypt.genSalt(8); - const hash = await bcrypt.hash(ps.newPassword, salt); + // Generate hash of password + const salt = await bcrypt.genSalt(8); + const hash = await bcrypt.hash(ps.newPassword, salt); - await UserProfiles.update(user.id, { - password: hash, - }); -}); + await this.userProfilesRepository.update(me.id, { + password: hash, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts index ede4a9d03b..a1804599df 100644 --- a/packages/backend/src/server/api/endpoints/i/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts @@ -1,7 +1,9 @@ import bcrypt from 'bcryptjs'; -import { UserProfiles, Users } from '@/models/index.js'; -import { deleteAccount } from '@/services/delete-account.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UsersRepository, UserProfilesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DeleteAccountService } from '@/core/DeleteAccountService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -18,19 +20,32 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - const userDetailed = await Users.findOneByOrFail({ id: user.id }); - if (userDetailed.isDeleted) { - return; - } +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, - if (!same) { - throw new Error('incorrect password'); - } + private deleteAccountService: DeleteAccountService, + ) { + super(meta, paramDef, async (ps, me) => { + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); + const userDetailed = await this.usersRepository.findOneByOrFail({ id: me.id }); + if (userDetailed.isDeleted) { + return; + } + + // Compare password + const same = await bcrypt.compare(ps.password, profile.password!); - await deleteAccount(user); -}); + if (!same) { + throw new Error('incorrect password'); + } + + await this.deleteAccountService.deleteAccount(me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/export-blocking.ts b/packages/backend/src/server/api/endpoints/i/export-blocking.ts index aed4c2e0a3..770708e685 100644 --- a/packages/backend/src/server/api/endpoints/i/export-blocking.ts +++ b/packages/backend/src/server/api/endpoints/i/export-blocking.ts @@ -1,6 +1,7 @@ -import define from '../../define.js'; -import { createExportBlockingJob } from '@/queue/index.js'; +import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; export const meta = { secure: true, @@ -18,6 +19,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - createExportBlockingJob(user); -}); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + this.queueService.createExportBlockingJob(me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/export-following.ts b/packages/backend/src/server/api/endpoints/i/export-following.ts index 058d77b3c2..fcaa59b12d 100644 --- a/packages/backend/src/server/api/endpoints/i/export-following.ts +++ b/packages/backend/src/server/api/endpoints/i/export-following.ts @@ -1,6 +1,7 @@ -import define from '../../define.js'; -import { createExportFollowingJob } from '@/queue/index.js'; +import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; export const meta = { secure: true, @@ -21,6 +22,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - createExportFollowingJob(user, ps.excludeMuting, ps.excludeInactive); -}); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + this.queueService.createExportFollowingJob(me, ps.excludeMuting, ps.excludeInactive); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/export-mute.ts b/packages/backend/src/server/api/endpoints/i/export-mute.ts index c0216fac0c..37bef0a117 100644 --- a/packages/backend/src/server/api/endpoints/i/export-mute.ts +++ b/packages/backend/src/server/api/endpoints/i/export-mute.ts @@ -1,6 +1,7 @@ -import define from '../../define.js'; -import { createExportMuteJob } from '@/queue/index.js'; +import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; export const meta = { secure: true, @@ -18,6 +19,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - createExportMuteJob(user); -}); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + this.queueService.createExportMuteJob(me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/export-notes.ts b/packages/backend/src/server/api/endpoints/i/export-notes.ts index 4b85a45554..9d2505e403 100644 --- a/packages/backend/src/server/api/endpoints/i/export-notes.ts +++ b/packages/backend/src/server/api/endpoints/i/export-notes.ts @@ -1,6 +1,7 @@ -import define from '../../define.js'; -import { createExportNotesJob } from '@/queue/index.js'; +import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; export const meta = { secure: true, @@ -18,6 +19,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - createExportNotesJob(user); -}); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + this.queueService.createExportNotesJob(me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/export-user-lists.ts b/packages/backend/src/server/api/endpoints/i/export-user-lists.ts index fa5c1f5e5a..0f8e4bca76 100644 --- a/packages/backend/src/server/api/endpoints/i/export-user-lists.ts +++ b/packages/backend/src/server/api/endpoints/i/export-user-lists.ts @@ -1,6 +1,7 @@ -import define from '../../define.js'; -import { createExportUserListsJob } from '@/queue/index.js'; +import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; export const meta = { secure: true, @@ -18,6 +19,13 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - createExportUserListsJob(user); -}); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + this.queueService.createExportUserListsJob(me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/favorites.ts b/packages/backend/src/server/api/endpoints/i/favorites.ts index 3c420e4d0f..350abd9f7b 100644 --- a/packages/backend/src/server/api/endpoints/i/favorites.ts +++ b/packages/backend/src/server/api/endpoints/i/favorites.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; -import { NoteFavorites } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NoteFavoritesRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { NoteFavoriteEntityService } from '@/core/entities/NoteFavoriteEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account', 'notes', 'favorites'], @@ -31,14 +34,25 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(NoteFavorites.createQueryBuilder('favorite'), ps.sinceId, ps.untilId) - .andWhere(`favorite.userId = :meId`, { meId: user.id }) - .leftJoinAndSelect('favorite.note', 'note'); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.noteFavoritesRepository) + private noteFavoritesRepository: NoteFavoritesRepository, - const favorites = await query - .take(ps.limit) - .getMany(); + private noteFavoriteEntityService: NoteFavoriteEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.noteFavoritesRepository.createQueryBuilder('favorite'), ps.sinceId, ps.untilId) + .andWhere('favorite.userId = :meId', { meId: me.id }) + .leftJoinAndSelect('favorite.note', 'note'); - return await NoteFavorites.packMany(favorites, user); -}); + const favorites = await query + .take(ps.limit) + .getMany(); + + return await this.noteFavoriteEntityService.packMany(favorites, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts b/packages/backend/src/server/api/endpoints/i/gallery/likes.ts index a38383f30e..ff6bcc01ab 100644 --- a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts +++ b/packages/backend/src/server/api/endpoints/i/gallery/likes.ts @@ -1,6 +1,9 @@ -import define from '../../../define.js'; -import { GalleryLikes } from '@/models/index.js'; -import { makePaginationQuery } from '../../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GalleryLikesRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { GalleryLikeEntityService } from '@/core/entities/GalleryLikeEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account', 'gallery'], @@ -27,7 +30,7 @@ export const meta = { ref: 'GalleryPost', }, }, - } + }, }, } as const; @@ -42,14 +45,25 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(GalleryLikes.createQueryBuilder('like'), ps.sinceId, ps.untilId) - .andWhere(`like.userId = :meId`, { meId: user.id }) - .leftJoinAndSelect('like.post', 'post'); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.galleryLikesRepository) + private galleryLikesRepository: GalleryLikesRepository, - const likes = await query - .take(ps.limit) - .getMany(); + private galleryLikeEntityService: GalleryLikeEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.galleryLikesRepository.createQueryBuilder('like'), ps.sinceId, ps.untilId) + .andWhere('like.userId = :meId', { meId: me.id }) + .leftJoinAndSelect('like.post', 'post'); - return await GalleryLikes.packMany(likes, user); -}); + const likes = await query + .take(ps.limit) + .getMany(); + + return await this.galleryLikeEntityService.packMany(likes, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts index b4edb5f73d..927be51f79 100644 --- a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts @@ -1,6 +1,9 @@ -import define from '../../../define.js'; -import { GalleryPosts } from '@/models/index.js'; -import { makePaginationQuery } from '../../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GalleryPostsRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account', 'gallery'], @@ -31,13 +34,24 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(GalleryPosts.createQueryBuilder('post'), ps.sinceId, ps.untilId) - .andWhere(`post.userId = :meId`, { meId: user.id }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.galleryPostsRepository) + private galleryPostsRepository: GalleryPostsRepository, - const posts = await query - .take(ps.limit) - .getMany(); + private galleryPostEntityService: GalleryPostEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.galleryPostsRepository.createQueryBuilder('post'), ps.sinceId, ps.untilId) + .andWhere('post.userId = :meId', { meId: me.id }); - return await GalleryPosts.packMany(posts, user); -}); + const posts = await query + .take(ps.limit) + .getMany(); + + return await this.galleryPostEntityService.packMany(posts, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts b/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts index e7d7518c5b..0695abdd85 100644 --- a/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts +++ b/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts @@ -1,5 +1,7 @@ -import define from '../../define.js'; -import { MutedNotes } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { MutedNotesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account'], @@ -27,11 +29,19 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - return { - count: await MutedNotes.countBy({ - userId: user.id, - reason: 'word', - }), - }; -}); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.mutedNotesRepository) + private mutedNotesRepository: MutedNotesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + return { + count: await this.mutedNotesRepository.countBy({ + userId: me.id, + reason: 'word', + }), + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/import-blocking.ts b/packages/backend/src/server/api/endpoints/i/import-blocking.ts index 0bcbf37ddd..bfba1fc36f 100644 --- a/packages/backend/src/server/api/endpoints/i/import-blocking.ts +++ b/packages/backend/src/server/api/endpoints/i/import-blocking.ts @@ -1,8 +1,10 @@ -import define from '../../define.js'; -import { createImportBlockingJob } from '@/queue/index.js'; +import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; +import { DriveFilesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { DriveFiles } from '@/models/index.js'; export const meta = { secure: true, @@ -49,13 +51,23 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ id: ps.fileId }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, - if (file == null) throw new ApiError(meta.errors.noSuchFile); - //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); - if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile); - if (file.size === 0) throw new ApiError(meta.errors.emptyFile); + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); - createImportBlockingJob(user, file.id); -}); + if (file == null) throw new ApiError(meta.errors.noSuchFile); + //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); + if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile); + if (file.size === 0) throw new ApiError(meta.errors.emptyFile); + + this.queueService.createImportBlockingJob(me, file.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/import-following.ts b/packages/backend/src/server/api/endpoints/i/import-following.ts index ee2abbea19..c7cb2e0330 100644 --- a/packages/backend/src/server/api/endpoints/i/import-following.ts +++ b/packages/backend/src/server/api/endpoints/i/import-following.ts @@ -1,8 +1,10 @@ -import define from '../../define.js'; -import { createImportFollowingJob } from '@/queue/index.js'; +import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; +import { DriveFilesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { DriveFiles } from '@/models/index.js'; export const meta = { secure: true, @@ -48,13 +50,23 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ id: ps.fileId }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, - if (file == null) throw new ApiError(meta.errors.noSuchFile); - //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); - if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile); - if (file.size === 0) throw new ApiError(meta.errors.emptyFile); + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); - createImportFollowingJob(user, file.id); -}); + if (file == null) throw new ApiError(meta.errors.noSuchFile); + //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); + if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile); + if (file.size === 0) throw new ApiError(meta.errors.emptyFile); + + this.queueService.createImportFollowingJob(me, file.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/import-muting.ts b/packages/backend/src/server/api/endpoints/i/import-muting.ts index b3b3b39238..060c37c13f 100644 --- a/packages/backend/src/server/api/endpoints/i/import-muting.ts +++ b/packages/backend/src/server/api/endpoints/i/import-muting.ts @@ -1,8 +1,10 @@ -import define from '../../define.js'; -import { createImportMutingJob } from '@/queue/index.js'; +import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; +import { DriveFilesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { DriveFiles } from '@/models/index.js'; export const meta = { secure: true, @@ -49,13 +51,23 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ id: ps.fileId }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, - if (file == null) throw new ApiError(meta.errors.noSuchFile); - //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); - if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile); - if (file.size === 0) throw new ApiError(meta.errors.emptyFile); + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); - createImportMutingJob(user, file.id); -}); + if (file == null) throw new ApiError(meta.errors.noSuchFile); + //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); + if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile); + if (file.size === 0) throw new ApiError(meta.errors.emptyFile); + + this.queueService.createImportMutingJob(me, file.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts index 64f5ec05fd..a5e17283e5 100644 --- a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts +++ b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts @@ -1,8 +1,10 @@ -import define from '../../define.js'; -import { createImportUserListsJob } from '@/queue/index.js'; +import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; +import { DriveFilesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { DriveFiles } from '@/models/index.js'; export const meta = { secure: true, @@ -48,13 +50,23 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ id: ps.fileId }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, - if (file == null) throw new ApiError(meta.errors.noSuchFile); - //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); - if (file.size > 30000) throw new ApiError(meta.errors.tooBigFile); - if (file.size === 0) throw new ApiError(meta.errors.emptyFile); + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); - createImportUserListsJob(user, file.id); -}); + if (file == null) throw new ApiError(meta.errors.noSuchFile); + //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); + if (file.size > 30000) throw new ApiError(meta.errors.tooBigFile); + if (file.size === 0) throw new ApiError(meta.errors.emptyFile); + + this.queueService.createImportUserListsJob(me, file.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts index 2b343dabdd..96927dad49 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications.ts @@ -1,10 +1,13 @@ import { Brackets } from 'typeorm'; -import { Notifications, Followings, Mutings, Users, UserProfiles } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { UsersRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, NotificationsRepository } from '@/models/index.js'; import { notificationTypes } from '@/types.js'; -import read from '@/services/note/read.js'; -import { readNotification } from '../../common/read-notification.js'; -import define from '../../define.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { NoteReadService } from '@/core/NoteReadService.js'; +import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js'; +import { NotificationService } from '@/core/NotificationService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account', 'notifications'], @@ -49,96 +52,121 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // includeTypes が空の場合はクエリしない - if (ps.includeTypes && ps.includeTypes.length === 0) { - return []; - } - // excludeTypes に全指定されている場合はクエリしない - if (notificationTypes.every(type => ps.excludeTypes?.includes(type))) { - return []; - } - const followingQuery = Followings.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: user.id }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - const mutingQuery = Mutings.createQueryBuilder('muting') - .select('muting.muteeId') - .where('muting.muterId = :muterId', { muterId: user.id }); + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, - const mutingInstanceQuery = UserProfiles.createQueryBuilder('user_profile') - .select('user_profile.mutedInstances') - .where('user_profile.userId = :muterId', { muterId: user.id }); + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, - const suspendedQuery = Users.createQueryBuilder('users') - .select('users.id') - .where('users.isSuspended = TRUE'); + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, - const query = makePaginationQuery(Notifications.createQueryBuilder('notification'), ps.sinceId, ps.untilId) - .andWhere('notification.notifieeId = :meId', { meId: user.id }) - .leftJoinAndSelect('notification.notifier', 'notifier') - .leftJoinAndSelect('notification.note', 'note') - .leftJoinAndSelect('notifier.avatar', 'notifierAvatar') - .leftJoinAndSelect('notifier.banner', 'notifierBanner') - .leftJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('user.avatar', 'avatar') - .leftJoinAndSelect('user.banner', 'banner') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') - .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') - .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); + @Inject(DI.notificationsRepository) + private notificationsRepository: NotificationsRepository, - // muted users - query.andWhere(new Brackets(qb => { qb - .where(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`) - .orWhere('notification.notifierId IS NULL'); - })); - query.setParameters(mutingQuery.getParameters()); + private notificationEntityService: NotificationEntityService, + private notificationService: NotificationService, + private queryService: QueryService, + private noteReadService: NoteReadService, + ) { + super(meta, paramDef, async (ps, me) => { + // includeTypes が空の場合はクエリしない + if (ps.includeTypes && ps.includeTypes.length === 0) { + return []; + } + // excludeTypes に全指定されている場合はクエリしない + if (notificationTypes.every(type => ps.excludeTypes?.includes(type))) { + return []; + } + const followingQuery = this.followingsRepository.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: me.id }); - // muted instances - query.andWhere(new Brackets(qb => { qb - .andWhere('notifier.host IS NULL') - .orWhere(`NOT (( ${mutingInstanceQuery.getQuery()} )::jsonb ? notifier.host)`); - })); - query.setParameters(mutingInstanceQuery.getParameters()); + const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') + .select('muting.muteeId') + .where('muting.muterId = :muterId', { muterId: me.id }); - // suspended users - query.andWhere(new Brackets(qb => { qb - .where(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`) - .orWhere('notification.notifierId IS NULL'); - })); + const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile') + .select('user_profile.mutedInstances') + .where('user_profile.userId = :muterId', { muterId: me.id }); - if (ps.following) { - query.andWhere(`((notification.notifierId IN (${ followingQuery.getQuery() })) OR (notification.notifierId = :meId))`, { meId: user.id }); - query.setParameters(followingQuery.getParameters()); - } + const suspendedQuery = this.usersRepository.createQueryBuilder('users') + .select('users.id') + .where('users.isSuspended = TRUE'); - 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 }); - } + const query = this.queryService.makePaginationQuery(this.notificationsRepository.createQueryBuilder('notification'), ps.sinceId, ps.untilId) + .andWhere('notification.notifieeId = :meId', { meId: me.id }) + .leftJoinAndSelect('notification.notifier', 'notifier') + .leftJoinAndSelect('notification.note', 'note') + .leftJoinAndSelect('notifier.avatar', 'notifierAvatar') + .leftJoinAndSelect('notifier.banner', 'notifierBanner') + .leftJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('user.avatar', 'avatar') + .leftJoinAndSelect('user.banner', 'banner') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') + .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') + .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); - if (ps.unreadOnly) { - query.andWhere('notification.isRead = false'); - } + // muted users + query.andWhere(new Brackets(qb => { qb + .where(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`) + .orWhere('notification.notifierId IS NULL'); + })); + query.setParameters(mutingQuery.getParameters()); - const notifications = await query.take(ps.limit).getMany(); + // muted instances + query.andWhere(new Brackets(qb => { qb + .andWhere('notifier.host IS NULL') + .orWhere(`NOT (( ${mutingInstanceQuery.getQuery()} )::jsonb ? notifier.host)`); + })); + query.setParameters(mutingInstanceQuery.getParameters()); - // Mark all as read - if (notifications.length > 0 && ps.markAsRead) { - readNotification(user.id, notifications.map(x => x.id)); - } + // suspended users + query.andWhere(new Brackets(qb => { qb + .where(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`) + .orWhere('notification.notifierId IS NULL'); + })); - const notes = notifications.filter(notification => ['mention', 'reply', 'quote'].includes(notification.type)).map(notification => notification.note!); + if (ps.following) { + query.andWhere(`((notification.notifierId IN (${ followingQuery.getQuery() })) OR (notification.notifierId = :meId))`, { meId: me.id }); + query.setParameters(followingQuery.getParameters()); + } - if (notes.length > 0) { - read(user.id, notes); - } + 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 }); + } - return await Notifications.packMany(notifications, user.id); -}); + if (ps.unreadOnly) { + query.andWhere('notification.isRead = false'); + } + + const notifications = await query.take(ps.limit).getMany(); + + // Mark all as read + if (notifications.length > 0 && ps.markAsRead) { + this.notificationService.readNotification(me.id, notifications.map(x => x.id)); + } + + const notes = notifications.filter(notification => ['mention', 'reply', 'quote'].includes(notification.type)).map(notification => notification.note!); + + if (notes.length > 0) { + this.noteReadService.read(me.id, notes); + } + + return await this.notificationEntityService.packMany(notifications, me.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/page-likes.ts b/packages/backend/src/server/api/endpoints/i/page-likes.ts index 71e326e2f0..9a909eedf4 100644 --- a/packages/backend/src/server/api/endpoints/i/page-likes.ts +++ b/packages/backend/src/server/api/endpoints/i/page-likes.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; -import { PageLikes } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { PageLikesRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { PageLikeEntityService } from '@/core/entities/PageLikeEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account', 'pages'], @@ -26,7 +29,7 @@ export const meta = { ref: 'Page', }, }, - } + }, }, } as const; @@ -41,14 +44,25 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(PageLikes.createQueryBuilder('like'), ps.sinceId, ps.untilId) - .andWhere(`like.userId = :meId`, { meId: user.id }) - .leftJoinAndSelect('like.page', 'page'); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.pageLikesRepository) + private pageLikesRepository: PageLikesRepository, - const likes = await query - .take(ps.limit) - .getMany(); + private pageLikeEntityService: PageLikeEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.pageLikesRepository.createQueryBuilder('like'), ps.sinceId, ps.untilId) + .andWhere('like.userId = :meId', { meId: me.id }) + .leftJoinAndSelect('like.page', 'page'); - return PageLikes.packMany(likes, user); -}); + const likes = await query + .take(ps.limit) + .getMany(); + + return this.pageLikeEntityService.packMany(likes, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/pages.ts b/packages/backend/src/server/api/endpoints/i/pages.ts index f28aed3fd4..7c4e4a6c7d 100644 --- a/packages/backend/src/server/api/endpoints/i/pages.ts +++ b/packages/backend/src/server/api/endpoints/i/pages.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; -import { Pages } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { PagesRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { PageEntityService } from '@/core/entities/PageEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account', 'pages'], @@ -31,13 +34,24 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId) - .andWhere(`page.userId = :meId`, { meId: user.id }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.pagesRepository) + private pagesRepository: PagesRepository, - const pages = await query - .take(ps.limit) - .getMany(); + private pageEntityService: PageEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.pagesRepository.createQueryBuilder('page'), ps.sinceId, ps.untilId) + .andWhere('page.userId = :meId', { meId: me.id }); - return await Pages.packMany(pages); -}); + const pages = await query + .take(ps.limit) + .getMany(); + + return await this.pageEntityService.packMany(pages); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/pin.ts b/packages/backend/src/server/api/endpoints/i/pin.ts index 67b7026be1..f31b0dc35e 100644 --- a/packages/backend/src/server/api/endpoints/i/pin.ts +++ b/packages/backend/src/server/api/endpoints/i/pin.ts @@ -1,7 +1,9 @@ -import { addPinned } from '@/services/i/pin.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { UsersRepository } from '@/models/index.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { NotePiningService } from '@/core/NotePiningService.js'; import { ApiError } from '../../error.js'; -import { Users } from '@/models/index.js'; export const meta = { tags: ['account', 'notes'], @@ -46,15 +48,23 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - await addPinned(user, ps.noteId).catch(e => { - if (e.id === '70c4e51f-5bea-449c-a030-53bee3cce202') throw new ApiError(meta.errors.noSuchNote); - if (e.id === '15a018eb-58e5-4da1-93be-330fcc5e4e1a') throw new ApiError(meta.errors.pinLimitExceeded); - if (e.id === '23f0cf4e-59a3-4276-a91d-61a5891c1514') throw new ApiError(meta.errors.alreadyPinned); - throw e; - }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + private userEntityService: UserEntityService, + private notePiningService: NotePiningService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.notePiningService.addPinned(me, ps.noteId).catch(err => { + if (err.id === '70c4e51f-5bea-449c-a030-53bee3cce202') throw new ApiError(meta.errors.noSuchNote); + if (err.id === '15a018eb-58e5-4da1-93be-330fcc5e4e1a') throw new ApiError(meta.errors.pinLimitExceeded); + if (err.id === '23f0cf4e-59a3-4276-a91d-61a5891c1514') throw new ApiError(meta.errors.alreadyPinned); + throw err; + }); - return await Users.pack<true, true>(user.id, user, { - detail: true, - }); -}); + return await this.userEntityService.pack<true, true>(me.id, me, { + detail: true, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts b/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts index 7ff6409caf..36c3566f55 100644 --- a/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts +++ b/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts @@ -1,6 +1,8 @@ -import { publishMainStream } from '@/services/stream.js'; -import define from '../../define.js'; -import { MessagingMessages, UserGroupJoinings } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { MessagingMessagesRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account', 'messaging'], @@ -17,25 +19,38 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Update documents - await MessagingMessages.update({ - recipientId: user.id, - isRead: false, - }, { - isRead: true, - }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.messagingMessagesRepository) + private messagingMessagesRepository: MessagingMessagesRepository, - const joinings = await UserGroupJoinings.findBy({ userId: user.id }); + @Inject(DI.userGroupJoiningsRepository) + private userGroupJoiningsRepository: UserGroupJoiningsRepository, - await Promise.all(joinings.map(j => MessagingMessages.createQueryBuilder().update() - .set({ - reads: (() => `array_append("reads", '${user.id}')`) as any, - }) - .where(`groupId = :groupId`, { groupId: j.userGroupId }) - .andWhere('userId != :userId', { userId: user.id }) - .andWhere('NOT (:userId = ANY(reads))', { userId: user.id }) - .execute())); + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + // Update documents + await this.messagingMessagesRepository.update({ + recipientId: me.id, + isRead: false, + }, { + isRead: true, + }); - publishMainStream(user.id, 'readAllMessagingMessages'); -}); + const joinings = await this.userGroupJoiningsRepository.findBy({ userId: me.id }); + + await Promise.all(joinings.map(j => this.messagingMessagesRepository.createQueryBuilder().update() + .set({ + reads: (() => `array_append("reads", '${me.id}')`) as any, + }) + .where('groupId = :groupId', { groupId: j.userGroupId }) + .andWhere('userId != :userId', { userId: me.id }) + .andWhere('NOT (:userId = ANY(reads))', { userId: me.id }) + .execute())); + + this.globalEventService.publishMainStream(me.id, 'readAllMessagingMessages'); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts b/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts index 49f3deb331..b4bb83c6eb 100644 --- a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts +++ b/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts @@ -1,6 +1,8 @@ -import { publishMainStream } from '@/services/stream.js'; -import define from '../../define.js'; -import { NoteUnreads } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NoteUnreadsRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account'], @@ -17,13 +19,23 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Remove documents - await NoteUnreads.delete({ - userId: user.id, - }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.noteUnreadsRepository) + private noteUnreadsRepository: NoteUnreadsRepository, - // 全て既読になったイベントを発行 - publishMainStream(user.id, 'readAllUnreadMentions'); - publishMainStream(user.id, 'readAllUnreadSpecifiedNotes'); -}); + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + // Remove documents + await this.noteUnreadsRepository.delete({ + userId: me.id, + }); + + // 全て既読になったイベントを発行 + this.globalEventService.publishMainStream(me.id, 'readAllUnreadMentions'); + this.globalEventService.publishMainStream(me.id, 'readAllUnreadSpecifiedNotes'); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/read-announcement.ts b/packages/backend/src/server/api/endpoints/i/read-announcement.ts index 45b6e98c86..5a7909674f 100644 --- a/packages/backend/src/server/api/endpoints/i/read-announcement.ts +++ b/packages/backend/src/server/api/endpoints/i/read-announcement.ts @@ -1,8 +1,12 @@ -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { IdService } from '@/core/IdService.js'; +import { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { genId } from '@/misc/gen-id.js'; -import { AnnouncementReads, Announcements, Users } from '@/models/index.js'; -import { publishMainStream } from '@/services/stream.js'; export const meta = { tags: ['account'], @@ -29,33 +33,48 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Check if announcement exists - const announcement = await Announcements.findOneBy({ id: ps.announcementId }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.announcementsRepository) + private announcementsRepository: AnnouncementsRepository, - if (announcement == null) { - throw new ApiError(meta.errors.noSuchAnnouncement); - } + @Inject(DI.announcementReadsRepository) + private announcementReadsRepository: AnnouncementReadsRepository, - // Check if already read - const read = await AnnouncementReads.findOneBy({ - announcementId: ps.announcementId, - userId: user.id, - }); + private userEntityService: UserEntityService, + private idService: IdService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + // Check if announcement exists + const announcement = await this.announcementsRepository.findOneBy({ id: ps.announcementId }); - if (read != null) { - return; - } + if (announcement == null) { + throw new ApiError(meta.errors.noSuchAnnouncement); + } + + // Check if already read + const read = await this.announcementReadsRepository.findOneBy({ + announcementId: ps.announcementId, + userId: me.id, + }); + + if (read != null) { + return; + } - // Create read - await AnnouncementReads.insert({ - id: genId(), - createdAt: new Date(), - announcementId: ps.announcementId, - userId: user.id, - }); + // Create read + await this.announcementReadsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + announcementId: ps.announcementId, + userId: me.id, + }); - if (!await Users.getHasUnreadAnnouncement(user.id)) { - publishMainStream(user.id, 'readAllAnnouncements'); + if (!await this.userEntityService.getHasUnreadAnnouncement(me.id)) { + this.globalEventService.publishMainStream(me.id, 'readAllAnnouncements'); + } + }); } -}); +} diff --git a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts index af929b04e8..7796fd97cb 100644 --- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts +++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts @@ -1,8 +1,10 @@ import bcrypt from 'bcryptjs'; -import { publishInternalEvent, publishMainStream, publishUserEvent } from '@/services/stream.js'; -import generateUserToken from '../../common/generate-native-user-token.js'; -import define from '../../define.js'; -import { Users, UserProfiles } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UsersRepository, UserProfilesRepository } from '@/models/index.js'; +import generateUserToken from '@/misc/generate-native-user-token.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -19,31 +21,44 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const freshUser = await Users.findOneByOrFail({ id: user.id }); - const oldToken = freshUser.token; +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + const freshUser = await this.usersRepository.findOneByOrFail({ id: me.id }); + const oldToken = freshUser.token; - if (!same) { - throw new Error('incorrect password'); - } + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); + + // Compare password + const same = await bcrypt.compare(ps.password, profile.password!); - const newToken = generateUserToken(); + if (!same) { + throw new Error('incorrect password'); + } - await Users.update(user.id, { - token: newToken, - }); + const newToken = generateUserToken(); - // Publish event - publishInternalEvent('userTokenRegenerated', { id: user.id, oldToken, newToken }); - publishMainStream(user.id, 'myTokenRegenerated'); + await this.usersRepository.update(me.id, { + token: newToken, + }); - // Terminate streaming - setTimeout(() => { - publishUserEvent(user.id, 'terminate', {}); - }, 5000); -}); + // Publish event + this.globalEventService.publishInternalEvent('userTokenRegenerated', { id: me.id, oldToken, newToken }); + this.globalEventService.publishMainStream(me.id, 'myTokenRegenerated'); + + // Terminate streaming + setTimeout(() => { + this.globalEventService.publishUserEvent(me.id, 'terminate', {}); + }, 5000); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts index d0b16dbc48..3b4db5fae3 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts @@ -1,5 +1,7 @@ -import define from '../../../define.js'; -import { RegistryItems } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { RegistryItemsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -18,19 +20,27 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = RegistryItems.createQueryBuilder('item') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: user.id }) - .andWhere('item.scope = :scope', { scope: ps.scope }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.registryItemsRepository.createQueryBuilder('item') + .where('item.domain IS NULL') + .andWhere('item.userId = :userId', { userId: me.id }) + .andWhere('item.scope = :scope', { scope: ps.scope }); - const items = await query.getMany(); + const items = await query.getMany(); - const res = {} as Record<string, any>; + const res = {} as Record<string, any>; - for (const item of items) { - res[item.key] = item.value; - } + for (const item of items) { + res[item.key] = item.value; + } - return res; -}); + return res; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts index cc5d5a8c6f..d24dff95b0 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts @@ -1,5 +1,7 @@ -import define from '../../../define.js'; -import { RegistryItems } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { RegistryItemsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -28,21 +30,29 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = RegistryItems.createQueryBuilder('item') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: user.id }) - .andWhere('item.key = :key', { key: ps.key }) - .andWhere('item.scope = :scope', { scope: ps.scope }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.registryItemsRepository.createQueryBuilder('item') + .where('item.domain IS NULL') + .andWhere('item.userId = :userId', { userId: me.id }) + .andWhere('item.key = :key', { key: ps.key }) + .andWhere('item.scope = :scope', { scope: ps.scope }); - const item = await query.getOne(); + const item = await query.getOne(); - if (item == null) { - throw new ApiError(meta.errors.noSuchKey); - } + if (item == null) { + throw new ApiError(meta.errors.noSuchKey); + } - return { - updatedAt: item.updatedAt, - value: item.value, - }; -}); + return { + updatedAt: item.updatedAt, + value: item.value, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/registry/get.ts b/packages/backend/src/server/api/endpoints/i/registry/get.ts index a79319744c..98d94a4c02 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get.ts @@ -1,5 +1,7 @@ -import define from '../../../define.js'; -import { RegistryItems } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { RegistryItemsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -28,18 +30,26 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = RegistryItems.createQueryBuilder('item') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: user.id }) - .andWhere('item.key = :key', { key: ps.key }) - .andWhere('item.scope = :scope', { scope: ps.scope }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.registryItemsRepository.createQueryBuilder('item') + .where('item.domain IS NULL') + .andWhere('item.userId = :userId', { userId: me.id }) + .andWhere('item.key = :key', { key: ps.key }) + .andWhere('item.scope = :scope', { scope: ps.scope }); - const item = await query.getOne(); + const item = await query.getOne(); - if (item == null) { - throw new ApiError(meta.errors.noSuchKey); - } + if (item == null) { + throw new ApiError(meta.errors.noSuchKey); + } - return item.value; -}); + return item.value; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts index ac209c06a6..d1a05d9d06 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts @@ -1,5 +1,7 @@ -import define from '../../../define.js'; -import { RegistryItems } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { RegistryItemsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -18,19 +20,25 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = RegistryItems.createQueryBuilder('item') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: user.id }) - .andWhere('item.scope = :scope', { scope: ps.scope }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.registryItemsRepository.createQueryBuilder('item') + .where('item.domain IS NULL') + .andWhere('item.userId = :userId', { userId: me.id }) + .andWhere('item.scope = :scope', { scope: ps.scope }); - const items = await query.getMany(); + const items = await query.getMany(); - const res = {} as Record<string, string>; + const res = {} as Record<string, string>; - for (const item of items) { - const type = typeof item.value; - res[item.key] = + for (const item of items) { + const type = typeof item.value; + res[item.key] = item.value === null ? 'null' : Array.isArray(item.value) ? 'array' : type === 'number' ? 'number' : @@ -38,7 +46,9 @@ export default define(meta, paramDef, async (ps, user) => { type === 'boolean' ? 'boolean' : type === 'object' ? 'object' : null as never; - } + } - return res; -}); + return res; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys.ts b/packages/backend/src/server/api/endpoints/i/registry/keys.ts index 5ea1a9d344..6df5f4ecc3 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys.ts @@ -1,5 +1,7 @@ -import define from '../../../define.js'; -import { RegistryItems } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { RegistryItemsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -18,14 +20,22 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = RegistryItems.createQueryBuilder('item') - .select('item.key') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: user.id }) - .andWhere('item.scope = :scope', { scope: ps.scope }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.registryItemsRepository.createQueryBuilder('item') + .select('item.key') + .where('item.domain IS NULL') + .andWhere('item.userId = :userId', { userId: me.id }) + .andWhere('item.scope = :scope', { scope: ps.scope }); - const items = await query.getMany(); + const items = await query.getMany(); - return items.map(x => x.key); -}); + return items.map(x => x.key); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/registry/remove.ts b/packages/backend/src/server/api/endpoints/i/registry/remove.ts index 92473654c6..b5870f099d 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/remove.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/remove.ts @@ -1,5 +1,7 @@ -import define from '../../../define.js'; -import { RegistryItems } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { RegistryItemsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -28,18 +30,26 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = RegistryItems.createQueryBuilder('item') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: user.id }) - .andWhere('item.key = :key', { key: ps.key }) - .andWhere('item.scope = :scope', { scope: ps.scope }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.registryItemsRepository.createQueryBuilder('item') + .where('item.domain IS NULL') + .andWhere('item.userId = :userId', { userId: me.id }) + .andWhere('item.key = :key', { key: ps.key }) + .andWhere('item.scope = :scope', { scope: ps.scope }); - const item = await query.getOne(); + const item = await query.getOne(); - if (item == null) { - throw new ApiError(meta.errors.noSuchKey); - } + if (item == null) { + throw new ApiError(meta.errors.noSuchKey); + } - await RegistryItems.remove(item); -}); + await this.registryItemsRepository.remove(item); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts b/packages/backend/src/server/api/endpoints/i/registry/scopes.ts index de4b313e25..58085ddbc5 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/scopes.ts @@ -1,5 +1,7 @@ -import define from '../../../define.js'; -import { RegistryItems } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { RegistryItemsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -14,20 +16,28 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = RegistryItems.createQueryBuilder('item') - .select('item.scope') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: user.id }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.registryItemsRepository.createQueryBuilder('item') + .select('item.scope') + .where('item.domain IS NULL') + .andWhere('item.userId = :userId', { userId: me.id }); - const items = await query.getMany(); + const items = await query.getMany(); - const res = [] as string[][]; + const res = [] as string[][]; - for (const item of items) { - if (res.some(scope => scope.join('.') === item.scope.join('.'))) continue; - res.push(item.scope); - } + for (const item of items) { + if (res.some(scope => scope.join('.') === item.scope.join('.'))) continue; + res.push(item.scope); + } - return res; -}); + return res; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/registry/set.ts b/packages/backend/src/server/api/endpoints/i/registry/set.ts index d380b428a3..585aac2e01 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/set.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/set.ts @@ -1,7 +1,9 @@ -import { publishMainStream } from '@/services/stream.js'; -import define from '../../../define.js'; -import { RegistryItems } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { RegistryItemsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -22,37 +24,48 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = RegistryItems.createQueryBuilder('item') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: user.id }) - .andWhere('item.key = :key', { key: ps.key }) - .andWhere('item.scope = :scope', { scope: ps.scope }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, - const existingItem = await query.getOne(); + private idService: IdService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.registryItemsRepository.createQueryBuilder('item') + .where('item.domain IS NULL') + .andWhere('item.userId = :userId', { userId: me.id }) + .andWhere('item.key = :key', { key: ps.key }) + .andWhere('item.scope = :scope', { scope: ps.scope }); - if (existingItem) { - await RegistryItems.update(existingItem.id, { - updatedAt: new Date(), - value: ps.value, - }); - } else { - await RegistryItems.insert({ - id: genId(), - createdAt: new Date(), - updatedAt: new Date(), - userId: user.id, - domain: null, - scope: ps.scope, - key: ps.key, - value: ps.value, + const existingItem = await query.getOne(); + + if (existingItem) { + await this.registryItemsRepository.update(existingItem.id, { + updatedAt: new Date(), + value: ps.value, + }); + } else { + await this.registryItemsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + updatedAt: new Date(), + userId: me.id, + domain: null, + scope: ps.scope, + key: ps.key, + value: ps.value, + }); + } + + // TODO: サードパーティアプリが傍受出来てしまうのでどうにかする + this.globalEventService.publishMainStream(me.id, 'registryUpdated', { + scope: ps.scope, + key: ps.key, + value: ps.value, + }); }); } - - // TODO: サードパーティアプリが傍受出来てしまうのでどうにかする - publishMainStream(user.id, 'registryUpdated', { - scope: ps.scope, - key: ps.key, - value: ps.value, - }); -}); +} diff --git a/packages/backend/src/server/api/endpoints/i/revoke-token.ts b/packages/backend/src/server/api/endpoints/i/revoke-token.ts index c692453794..86a82e6a6c 100644 --- a/packages/backend/src/server/api/endpoints/i/revoke-token.ts +++ b/packages/backend/src/server/api/endpoints/i/revoke-token.ts @@ -1,6 +1,8 @@ -import define from '../../define.js'; -import { AccessTokens } from '@/models/index.js'; -import { publishUserEvent } from '@/services/stream.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AccessTokensRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -17,16 +19,26 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const token = await AccessTokens.findOneBy({ id: ps.tokenId }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.accessTokensRepository) + private accessTokensRepository: AccessTokensRepository, - if (token) { - await AccessTokens.delete({ - id: ps.tokenId, - userId: user.id, - }); + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + const token = await this.accessTokensRepository.findOneBy({ id: ps.tokenId }); + + if (token) { + await this.accessTokensRepository.delete({ + id: ps.tokenId, + userId: me.id, + }); - // Terminate streaming - publishUserEvent(user.id, 'terminate'); + // Terminate streaming + this.globalEventService.publishUserEvent(me.id, 'terminate'); + } + }); } -}); +} diff --git a/packages/backend/src/server/api/endpoints/i/signin-history.ts b/packages/backend/src/server/api/endpoints/i/signin-history.ts index ca37411662..410cd72065 100644 --- a/packages/backend/src/server/api/endpoints/i/signin-history.ts +++ b/packages/backend/src/server/api/endpoints/i/signin-history.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; -import { Signins } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { SigninsRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { SigninEntityService } from '@/core/entities/SigninEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, @@ -19,11 +22,22 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(Signins.createQueryBuilder('signin'), ps.sinceId, ps.untilId) - .andWhere(`signin.userId = :meId`, { meId: user.id }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.signinsRepository) + private signinsRepository: SigninsRepository, - const history = await query.take(ps.limit).getMany(); + private signinEntityService: SigninEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.signinsRepository.createQueryBuilder('signin'), ps.sinceId, ps.untilId) + .andWhere('signin.userId = :meId', { meId: me.id }); - return await Promise.all(history.map(record => Signins.pack(record))); -}); + const history = await query.take(ps.limit).getMany(); + + return await Promise.all(history.map(record => this.signinEntityService.pack(record))); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/unpin.ts b/packages/backend/src/server/api/endpoints/i/unpin.ts index 9912689da5..9a735e1168 100644 --- a/packages/backend/src/server/api/endpoints/i/unpin.ts +++ b/packages/backend/src/server/api/endpoints/i/unpin.ts @@ -1,7 +1,9 @@ -import { removePinned } from '@/services/i/pin.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { UsersRepository } from '@/models/index.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { NotePiningService } from '@/core/NotePiningService.js'; import { ApiError } from '../../error.js'; -import { Users } from '@/models/index.js'; export const meta = { tags: ['account', 'notes'], @@ -34,13 +36,21 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - await removePinned(user, ps.noteId).catch(e => { - if (e.id === 'b302d4cf-c050-400a-bbb3-be208681f40c') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + private userEntityService: UserEntityService, + private notePiningService: NotePiningService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.notePiningService.removePinned(me, ps.noteId).catch(err => { + if (err.id === 'b302d4cf-c050-400a-bbb3-be208681f40c') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); - return await Users.pack<true, true>(user.id, user, { - detail: true, - }); -}); + return await this.userEntityService.pack<true, true>(me.id, me, { + detail: true, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts index 3318078523..719cc14f09 100644 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ b/packages/backend/src/server/api/endpoints/i/update-email.ts @@ -1,13 +1,15 @@ -import { publishMainStream } from '@/services/stream.js'; -import define from '../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; import rndstr from 'rndstr'; -import config from '@/config/index.js'; import ms from 'ms'; import bcrypt from 'bcryptjs'; -import { Users, UserProfiles } from '@/models/index.js'; -import { sendEmail } from '@/services/send-email.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UsersRepository, UserProfilesRepository } from '@/models/index.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { EmailService } from '@/core/EmailService.js'; +import { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; import { ApiError } from '../../error.js'; -import { validateEmailForAccount } from '@/services/validate-email-for-account.js'; export const meta = { requireCredential: true, @@ -44,50 +46,68 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.config) + private config: Config, - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - if (!same) { - throw new ApiError(meta.errors.incorrectPassword); - } + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, - if (ps.email != null) { - const available = await validateEmailForAccount(ps.email); - if (!available) { - throw new ApiError(meta.errors.unavailable); - } - } + private userEntityService: UserEntityService, + private emailService: EmailService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - await UserProfiles.update(user.id, { - email: ps.email, - emailVerified: false, - emailVerifyCode: null, - }); + // Compare password + const same = await bcrypt.compare(ps.password, profile.password!); - const iObj = await Users.pack(user.id, user, { - detail: true, - includeSecrets: true, - }); + if (!same) { + throw new ApiError(meta.errors.incorrectPassword); + } - // Publish meUpdated event - publishMainStream(user.id, 'meUpdated', iObj); + if (ps.email != null) { + const available = await this.emailService.validateEmailForAccount(ps.email); + if (!available) { + throw new ApiError(meta.errors.unavailable); + } + } - if (ps.email != null) { - const code = rndstr('a-z0-9', 16); + await this.userProfilesRepository.update(me.id, { + email: ps.email, + emailVerified: false, + emailVerifyCode: null, + }); - await UserProfiles.update(user.id, { - emailVerifyCode: code, - }); + const iObj = await this.userEntityService.pack(me.id, me, { + detail: true, + includeSecrets: true, + }); - const link = `${config.url}/verify-email/${code}`; + // Publish meUpdated event + this.globalEventService.publishMainStream(me.id, 'meUpdated', iObj); - sendEmail(ps.email, 'Email verification', - `To verify email, please click this link:<br><a href="${link}">${link}</a>`, - `To verify email, please click this link: ${link}`); - } + if (ps.email != null) { + const code = rndstr('a-z0-9', 16); + + await this.userProfilesRepository.update(me.id, { + emailVerifyCode: code, + }); - return iObj; -}); + const link = `${this.config.url}/verify-email/${code}`; + + this.emailService.sendEmail(ps.email, 'Email verification', + `To verify email, please click this link:<br><a href="${link}">${link}</a>`, + `To verify email, please click this link: ${link}`); + } + + return iObj; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 3c2f1cea0d..4b904d4696 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -1,19 +1,23 @@ import RE2 from 're2'; import * as mfm from 'mfm-js'; -import { publishMainStream, publishUserEvent } from '@/services/stream.js'; -import acceptAllFollowRequests from '@/services/following/requests/accept-all.js'; -import { publishToFollowers } from '@/services/i/update.js'; +import { Inject, Injectable } from '@nestjs/common'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractHashtags } from '@/misc/extract-hashtags.js'; -import { updateUsertags } from '@/services/update-hashtag.js'; -import { Users, DriveFiles, UserProfiles, Pages } from '@/models/index.js'; -import { User } from '@/models/entities/user.js'; -import { UserProfile } from '@/models/entities/user-profile.js'; +import { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/index.js'; +import type { User } from '@/models/entities/User.js'; +import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/entities/User.js'; +import type { UserProfile } from '@/models/entities/UserProfile.js'; import { notificationTypes } from '@/types.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { langmap } from '@/misc/langmap.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { UserFollowingService } from '@/core/UserFollowingService.js'; +import { AccountUpdateService } from '@/core/AccountUpdateService.js'; +import { HashtagService } from '@/core/HashtagService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import define from '../../define.js'; export const meta = { tags: ['account'], @@ -70,10 +74,10 @@ export const meta = { export const paramDef = { type: 'object', properties: { - name: { ...Users.nameSchema, nullable: true }, - description: { ...Users.descriptionSchema, nullable: true }, - location: { ...Users.locationSchema, nullable: true }, - birthday: { ...Users.birthdaySchema, nullable: true }, + name: { ...nameSchema, nullable: true }, + description: { ...descriptionSchema, nullable: true }, + location: { ...locationSchema, nullable: true }, + birthday: { ...birthdaySchema, nullable: true }, lang: { type: 'string', enum: [null, ...Object.keys(langmap)], nullable: true }, avatarId: { type: 'string', format: 'misskey:id', nullable: true }, bannerId: { type: 'string', format: 'misskey:id', nullable: true }, @@ -122,134 +126,157 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, _user, token) => { - const user = await Users.findOneByOrFail({ id: _user.id }); - const isSecure = token == null; +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - const updates = {} as Partial<User>; - const profileUpdates = {} as Partial<UserProfile>; + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, - if (ps.name !== undefined) updates.name = ps.name; - if (ps.description !== undefined) profileUpdates.description = ps.description; - if (ps.lang !== undefined) profileUpdates.lang = ps.lang; - if (ps.location !== undefined) profileUpdates.location = ps.location; - if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; - if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility; - if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId; - if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId; - if (ps.mutedWords !== undefined) { - // validate regular expression syntax - ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => { - const regexp = x.match(/^\/(.+)\/(.*)$/); - if (!regexp) throw new ApiError(meta.errors.invalidRegexp); + @Inject(DI.pagesRepository) + private pagesRepository: PagesRepository, - try { - new RE2(regexp[1], regexp[2]); - } catch (err) { - throw new ApiError(meta.errors.invalidRegexp); - } - }); + private userEntityService: UserEntityService, + private globalEventService: GlobalEventService, + private userFollowingService: UserFollowingService, + private accountUpdateService: AccountUpdateService, + private hashtagService: HashtagService, + ) { + super(meta, paramDef, async (ps, _user, token) => { + const user = await this.usersRepository.findOneByOrFail({ id: _user.id }); + const isSecure = token == null; - profileUpdates.mutedWords = ps.mutedWords; - profileUpdates.enableWordMute = ps.mutedWords.length > 0; - } - if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances; - if (ps.mutingNotificationTypes !== undefined) profileUpdates.mutingNotificationTypes = ps.mutingNotificationTypes as typeof notificationTypes[number][]; - if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; - if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable; - if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus; - if (typeof ps.publicReactions === 'boolean') profileUpdates.publicReactions = ps.publicReactions; - if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot; - if (typeof ps.showTimelineReplies === 'boolean') updates.showTimelineReplies = ps.showTimelineReplies; - if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot; - if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; - if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle; - if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat; - if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; - if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; - if (typeof ps.alwaysMarkNsfw === 'boolean') profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw; - if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive; - if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes; + const updates = {} as Partial<User>; + const profileUpdates = {} as Partial<UserProfile>; - if (ps.avatarId) { - const avatar = await DriveFiles.findOneBy({ id: ps.avatarId }); + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar); - if (!avatar.type.startsWith('image/')) throw new ApiError(meta.errors.avatarNotAnImage); - } + if (ps.name !== undefined) updates.name = ps.name; + if (ps.description !== undefined) profileUpdates.description = ps.description; + if (ps.lang !== undefined) profileUpdates.lang = ps.lang; + if (ps.location !== undefined) profileUpdates.location = ps.location; + if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; + if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility; + if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId; + if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId; + if (ps.mutedWords !== undefined) { + // validate regular expression syntax + ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => { + const regexp = x.match(/^\/(.+)\/(.*)$/); + if (!regexp) throw new ApiError(meta.errors.invalidRegexp); - if (ps.bannerId) { - const banner = await DriveFiles.findOneBy({ id: ps.bannerId }); + try { + new RE2(regexp[1], regexp[2]); + } catch (err) { + throw new ApiError(meta.errors.invalidRegexp); + } + }); - if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner); - if (!banner.type.startsWith('image/')) throw new ApiError(meta.errors.bannerNotAnImage); - } + profileUpdates.mutedWords = ps.mutedWords; + profileUpdates.enableWordMute = ps.mutedWords.length > 0; + } + if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances; + if (ps.mutingNotificationTypes !== undefined) profileUpdates.mutingNotificationTypes = ps.mutingNotificationTypes as typeof notificationTypes[number][]; + if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; + if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable; + if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus; + if (typeof ps.publicReactions === 'boolean') profileUpdates.publicReactions = ps.publicReactions; + if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot; + if (typeof ps.showTimelineReplies === 'boolean') updates.showTimelineReplies = ps.showTimelineReplies; + if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot; + if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; + if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle; + if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat; + if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; + if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; + if (typeof ps.alwaysMarkNsfw === 'boolean') profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw; + if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive; + if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes; - if (ps.pinnedPageId) { - const page = await Pages.findOneBy({ id: ps.pinnedPageId }); + if (ps.avatarId) { + const avatar = await this.driveFilesRepository.findOneBy({ id: ps.avatarId }); - if (page == null || page.userId !== user.id) throw new ApiError(meta.errors.noSuchPage); + if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar); + if (!avatar.type.startsWith('image/')) throw new ApiError(meta.errors.avatarNotAnImage); + } - profileUpdates.pinnedPageId = page.id; - } else if (ps.pinnedPageId === null) { - profileUpdates.pinnedPageId = null; - } + if (ps.bannerId) { + const banner = await this.driveFilesRepository.findOneBy({ id: ps.bannerId }); - if (ps.fields) { - profileUpdates.fields = ps.fields - .filter(x => typeof x.name === 'string' && x.name !== '' && typeof x.value === 'string' && x.value !== '') - .map(x => { - return { name: x.name, value: x.value }; - }); - } + if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner); + if (!banner.type.startsWith('image/')) throw new ApiError(meta.errors.bannerNotAnImage); + } - //#region emojis/tags + if (ps.pinnedPageId) { + const page = await this.pagesRepository.findOneBy({ id: ps.pinnedPageId }); - let emojis = [] as string[]; - let tags = [] as string[]; + if (page == null || page.userId !== user.id) throw new ApiError(meta.errors.noSuchPage); - const newName = updates.name === undefined ? user.name : updates.name; - const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description; + profileUpdates.pinnedPageId = page.id; + } else if (ps.pinnedPageId === null) { + profileUpdates.pinnedPageId = null; + } - if (newName != null) { - const tokens = mfm.parseSimple(newName); - emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!)); - } + if (ps.fields) { + profileUpdates.fields = ps.fields + .filter(x => typeof x.name === 'string' && x.name !== '' && typeof x.value === 'string' && x.value !== '') + .map(x => { + return { name: x.name, value: x.value }; + }); + } - if (newDescription != null) { - const tokens = mfm.parse(newDescription); - emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!)); - tags = extractHashtags(tokens!).map(tag => normalizeForSearch(tag)).splice(0, 32); - } + //#region emojis/tags - updates.emojis = emojis; - updates.tags = tags; + let emojis = [] as string[]; + let tags = [] as string[]; - // ハッシュタグ更新 - updateUsertags(user, tags); - //#endregion + const newName = updates.name === undefined ? user.name : updates.name; + const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description; - if (Object.keys(updates).length > 0) await Users.update(user.id, updates); - if (Object.keys(profileUpdates).length > 0) await UserProfiles.update(user.id, profileUpdates); + if (newName != null) { + const tokens = mfm.parseSimple(newName); + emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!)); + } - const iObj = await Users.pack<true, true>(user.id, user, { - detail: true, - includeSecrets: isSecure, - }); + if (newDescription != null) { + const tokens = mfm.parse(newDescription); + emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!)); + tags = extractHashtags(tokens!).map(tag => normalizeForSearch(tag)).splice(0, 32); + } - // Publish meUpdated event - publishMainStream(user.id, 'meUpdated', iObj); - publishUserEvent(user.id, 'updateUserProfile', await UserProfiles.findOneBy({ userId: user.id })); + updates.emojis = emojis; + updates.tags = tags; - // 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認 - if (user.isLocked && ps.isLocked === false) { - acceptAllFollowRequests(user); - } + // ハッシュタグ更新 + this.hashtagService.updateUsertags(user, tags); + //#endregion + + if (Object.keys(updates).length > 0) await this.usersRepository.update(user.id, updates); + if (Object.keys(profileUpdates).length > 0) await this.userProfilesRepository.update(user.id, profileUpdates); - // フォロワーにUpdateを配信 - publishToFollowers(user.id); + const iObj = await this.userEntityService.pack<true, true>(user.id, user, { + detail: true, + includeSecrets: isSecure, + }); - return iObj; -}); + // Publish meUpdated event + this.globalEventService.publishMainStream(user.id, 'meUpdated', iObj); + this.globalEventService.publishUserEvent(user.id, 'updateUserProfile', await this.userProfilesRepository.findOneBy({ userId: user.id })); + + // 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認 + if (user.isLocked && ps.isLocked === false) { + this.userFollowingService.acceptAllFollowRequests(user); + } + + // フォロワーにUpdateを配信 + this.accountUpdateService.publishToFollowers(user.id); + + return iObj; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/user-group-invites.ts b/packages/backend/src/server/api/endpoints/i/user-group-invites.ts index 1d7e4a16b3..6dd1626bb8 100644 --- a/packages/backend/src/server/api/endpoints/i/user-group-invites.ts +++ b/packages/backend/src/server/api/endpoints/i/user-group-invites.ts @@ -1,6 +1,9 @@ -import define from '../../define.js'; -import { UserGroupInvitations } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserGroupInvitationsRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { UserGroupInvitationEntityService } from '@/core/entities/UserGroupInvitationEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['account', 'groups'], @@ -42,14 +45,25 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(UserGroupInvitations.createQueryBuilder('invitation'), ps.sinceId, ps.untilId) - .andWhere(`invitation.userId = :meId`, { meId: user.id }) - .leftJoinAndSelect('invitation.userGroup', 'user_group'); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.userGroupInvitationsRepository) + private userGroupInvitationsRepository: UserGroupInvitationsRepository, - const invitations = await query - .take(ps.limit) - .getMany(); + private userGroupInvitationEntityService: UserGroupInvitationEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.userGroupInvitationsRepository.createQueryBuilder('invitation'), ps.sinceId, ps.untilId) + .andWhere('invitation.userId = :meId', { meId: me.id }) + .leftJoinAndSelect('invitation.userGroup', 'user_group'); - return await UserGroupInvitations.packMany(invitations); -}); + const invitations = await query + .take(ps.limit) + .getMany(); + + return await this.userGroupInvitationEntityService.packMany(invitations); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts index 2e2fd00b8c..016b1b5d6a 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts @@ -1,8 +1,10 @@ -import define from '../../../define.js'; -import { genId } from '@/misc/gen-id.js'; -import { Webhooks } from '@/models/index.js'; -import { publishInternalEvent } from '@/services/stream.js'; -import { webhookEventTypes } from '@/models/entities/webhook.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { IdService } from '@/core/IdService.js'; +import { WebhooksRepository } from '@/models/index.js'; +import { webhookEventTypes } from '@/models/entities/Webhook.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['webhooks'], @@ -25,19 +27,32 @@ export const paramDef = { required: ['name', 'url', 'secret', 'on'], } as const; +// TODO: ロジックをサービスに切り出す + // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const webhook = await Webhooks.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - name: ps.name, - url: ps.url, - secret: ps.secret, - on: ps.on, - }).then(x => Webhooks.findOneByOrFail(x.identifiers[0])); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.webhooksRepository) + private webhooksRepository: WebhooksRepository, + + private idService: IdService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + const webhook = await this.webhooksRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: me.id, + name: ps.name, + url: ps.url, + secret: ps.secret, + on: ps.on, + }).then(x => this.webhooksRepository.findOneByOrFail(x.identifiers[0])); - publishInternalEvent('webhookCreated', webhook); + this.globalEventService.publishInternalEvent('webhookCreated', webhook); - return webhook; -}); + return webhook; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts b/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts index 2821eaa5f1..53b553b43e 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts @@ -1,7 +1,9 @@ -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { WebhooksRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { Webhooks } from '@/models/index.js'; -import { publishInternalEvent } from '@/services/stream.js'; export const meta = { tags: ['webhooks'], @@ -27,18 +29,30 @@ export const paramDef = { required: ['webhookId'], } as const; +// TODO: ロジックをサービスに切り出す + // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const webhook = await Webhooks.findOneBy({ - id: ps.webhookId, - userId: user.id, - }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.webhooksRepository) + private webhooksRepository: WebhooksRepository, - if (webhook == null) { - throw new ApiError(meta.errors.noSuchWebhook); - } + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + const webhook = await this.webhooksRepository.findOneBy({ + id: ps.webhookId, + userId: me.id, + }); - await Webhooks.delete(webhook.id); + if (webhook == null) { + throw new ApiError(meta.errors.noSuchWebhook); + } - publishInternalEvent('webhookDeleted', webhook); -}); + await this.webhooksRepository.delete(webhook.id); + + this.globalEventService.publishInternalEvent('webhookDeleted', webhook); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts index 54e4563732..8e4aff45dd 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts @@ -1,5 +1,7 @@ -import define from '../../../define.js'; -import { Webhooks } from '@/models/index.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { WebhooksRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['webhooks', 'account'], @@ -16,10 +18,18 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const webhooks = await Webhooks.findBy({ - userId: me.id, - }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.webhooksRepository) + private webhooksRepository: WebhooksRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const webhooks = await this.webhooksRepository.findBy({ + userId: me.id, + }); - return webhooks; -}); + return webhooks; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts index 02fa1edb5e..622c2ade98 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts @@ -1,6 +1,8 @@ -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { WebhooksRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { Webhooks } from '@/models/index.js'; export const meta = { tags: ['webhooks'], @@ -27,15 +29,23 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const webhook = await Webhooks.findOneBy({ - id: ps.webhookId, - userId: user.id, - }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.webhooksRepository) + private webhooksRepository: WebhooksRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const webhook = await this.webhooksRepository.findOneBy({ + id: ps.webhookId, + userId: me.id, + }); - if (webhook == null) { - throw new ApiError(meta.errors.noSuchWebhook); - } + if (webhook == null) { + throw new ApiError(meta.errors.noSuchWebhook); + } - return webhook; -}); + return webhook; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts index f87b9753fb..3a0ef1a526 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts @@ -1,8 +1,10 @@ -import define from '../../../define.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { WebhooksRepository } from '@/models/index.js'; +import { webhookEventTypes } from '@/models/entities/Webhook.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { Webhooks } from '@/models/index.js'; -import { publishInternalEvent } from '@/services/stream.js'; -import { webhookEventTypes } from '@/models/entities/webhook.js'; export const meta = { tags: ['webhooks'], @@ -36,24 +38,36 @@ export const paramDef = { required: ['webhookId', 'name', 'url', 'secret', 'on', 'active'], } as const; +// TODO: ロジックをサービスに切り出す + // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const webhook = await Webhooks.findOneBy({ - id: ps.webhookId, - userId: user.id, - }); +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.webhooksRepository) + private webhooksRepository: WebhooksRepository, - if (webhook == null) { - throw new ApiError(meta.errors.noSuchWebhook); - } + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + const webhook = await this.webhooksRepository.findOneBy({ + id: ps.webhookId, + userId: me.id, + }); - await Webhooks.update(webhook.id, { - name: ps.name, - url: ps.url, - secret: ps.secret, - on: ps.on, - active: ps.active, - }); + if (webhook == null) { + throw new ApiError(meta.errors.noSuchWebhook); + } - publishInternalEvent('webhookUpdated', webhook); -}); + await this.webhooksRepository.update(webhook.id, { + name: ps.name, + url: ps.url, + secret: ps.secret, + on: ps.on, + active: ps.active, + }); + + this.globalEventService.publishInternalEvent('webhookUpdated', webhook); + }); + } +} |