なんかもうめっちゃ変えた

This commit is contained in:
syuilo
2022-09-18 03:27:08 +09:00
committed by GitHub
parent d9ab03f086
commit b75184ec8e
946 changed files with 41219 additions and 28839 deletions

View File

@@ -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,
});
if (!verified) {
throw new Error('not verified');
}
await this.userProfilesRepository.update(me.id, {
twoFactorSecret: profile.twoFactorTempSecret,
twoFactorEnabled: true,
});
});
}
const verified = (speakeasy as any).totp.verify({
secret: profile.twoFactorTempSecret,
encoding: 'base32',
token: token,
});
if (!verified) {
throw new Error('not verified');
}
await UserProfiles.update(user.id, {
twoFactorSecret: profile.twoFactorTempSecret,
twoFactorEnabled: true,
});
});
}

View File

@@ -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);
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 clientDataJSONHash = hash(Buffer.from(ps.clientDataJSON, 'utf-8'));
const attestation = await cborDecodeFirst(ps.attestationObject);
const rpIdHash = attestation.authData.slice(0, 32);
if (!rpIdHashReal.equals(rpIdHash)) {
throw new Error('rpIdHash mismatch');
}
const flags = attestation.authData[32];
// 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');
}
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');
const attestationChallenge = await AttestationChallenges.findOneBy({
userId: user.id,
id: ps.challengeId,
registrationChallenge: true,
challenge: hash(clientData.challenge).toString('hex'),
});
if (!attestationChallenge) {
throw new Error('non-existent challenge');
}
await AttestationChallenges.delete({
userId: user.id,
id: ps.challengeId,
});
// Expired challenge (> 5min old)
if (
new Date().getTime() - attestationChallenge.createdAt.getTime() >=
5 * 60 * 1000
private userEntityService: UserEntityService,
private globalEventService: GlobalEventService,
private twoFactorAuthenticationService: TwoFactorAuthenticationService,
) {
throw new Error('expired challenge');
super(meta, paramDef, async (ps, me) => {
const rpIdHashReal = this.twoFactorAuthenticationService.hash(Buffer.from(this.config.hostname, 'utf-8'));
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
// Compare password
const same = await bcrypt.compare(ps.password, profile.password!);
if (!same) {
throw new Error('incorrect password');
}
if (!profile.twoFactorEnabled) {
throw new Error('2fa not enabled');
}
const clientData = JSON.parse(ps.clientDataJSON);
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 clientDataJSONHash = this.twoFactorAuthenticationService.hash(Buffer.from(ps.clientDataJSON, 'utf-8'));
const attestation = await cborDecodeFirst(ps.attestationObject);
const rpIdHash = attestation.authData.slice(0, 32);
if (!rpIdHashReal.equals(rpIdHash)) {
throw new Error('rpIdHash mismatch');
}
const flags = attestation.authData[32];
// 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');
const attestationChallenge = await this.attestationChallengesRepository.findOneBy({
userId: me.id,
id: ps.challengeId,
registrationChallenge: true,
challenge: this.twoFactorAuthenticationService.hash(clientData.challenge).toString('hex'),
});
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');
}
const credentialIdString = credentialId.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
this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
detail: true,
includeSecrets: true,
}));
return {
id: credentialIdString,
name: ps.name,
};
});
}
const credentialIdString = credentialId.toString('hex');
await UserSecurityKeys.insert({
userId: user.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,
}));
return {
id: credentialIdString,
name: ps.name,
};
});
}

View File

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

View File

@@ -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 });
// Compare password
const same = await bcrypt.compare(ps.password, profile.password!);
if (!same) {
throw new Error('incorrect password');
}
if (!profile.twoFactorEnabled) {
throw new Error('2fa not enabled');
}
// 32 byte challenge
const entropy = await randomBytes(32);
const challenge = entropy.toString('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
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,
};
});
}
if (!profile.twoFactorEnabled) {
throw new Error('2fa not enabled');
}
// 32 byte challenge
const entropy = await randomBytes(32);
const challenge = entropy.toString('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
const challengeId = genId();
await AttestationChallenges.insert({
userId: user.id,
id: challengeId,
challenge: hash(Buffer.from(challenge, 'utf-8')).toString('hex'),
createdAt: new Date(),
registrationChallenge: true,
});
return {
challengeId,
challenge,
};
});
}

View File

@@ -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,
});
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: me.username,
issuer: this.config.host,
});
const dataUrl = await QRCode.toDataURL(url);
return {
qr: dataUrl,
url,
secret: secret.base32,
label: me.username,
issuer: this.config.host,
};
});
}
// Generate user's secret key
const secret = speakeasy.generateSecret({
length: 32,
});
await UserProfiles.update(user.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);
return {
qr: dataUrl,
url,
secret: secret.base32,
label: user.username,
issuer: config.host,
};
});
}

View File

@@ -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!);
if (!same) {
throw new Error('incorrect password');
}
// Make sure we only delete the user's own creds
await this.userSecurityKeysRepository.delete({
userId: me.id,
id: ps.credentialId,
});
// Publish meUpdated event
this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
detail: true,
includeSecrets: true,
}));
return {};
});
}
// Make sure we only delete the user's own creds
await UserSecurityKeys.delete({
userId: user.id,
id: ps.credentialId,
});
// Publish meUpdated event
publishMainStream(user.id, 'meUpdated', await Users.pack(user.id, user, {
detail: true,
includeSecrets: true,
}));
return {};
});
}

View File

@@ -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 this.userProfilesRepository.update(me.id, {
twoFactorSecret: null,
twoFactorEnabled: false,
});
});
}
await UserProfiles.update(user.id, {
twoFactorSecret: null,
twoFactorEnabled: false,
});
});
}

View File

@@ -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();
return await Promise.all(tokens.map(token => ({
id: token.id,
name: token.name,
createdAt: token.createdAt,
lastUsedAt: token.lastUsedAt,
permission: token.permission,
})));
});
}
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,
})));
});
}

View File

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

View File

@@ -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);
await this.userProfilesRepository.update(me.id, {
password: hash,
});
});
}
// 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,
});
});
}

View File

@@ -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,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
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!);
if (!same) {
throw new Error('incorrect password');
}
await this.deleteAccountService.deleteAccount(me);
});
}
// Compare password
const same = await bcrypt.compare(ps.password, profile.password!);
if (!same) {
throw new Error('incorrect password');
}
await deleteAccount(user);
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 [];
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
@Inject(DI.notificationsRepository)
private notificationsRepository: NotificationsRepository,
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 });
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
.select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: me.id });
const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile')
.select('user_profile.mutedInstances')
.where('user_profile.userId = :muterId', { muterId: me.id });
const suspendedQuery = this.usersRepository.createQueryBuilder('users')
.select('users.id')
.where('users.isSuspended = TRUE');
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');
// muted users
query.andWhere(new Brackets(qb => { qb
.where(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`)
.orWhere('notification.notifierId IS NULL');
}));
query.setParameters(mutingQuery.getParameters());
// muted instances
query.andWhere(new Brackets(qb => { qb
.andWhere('notifier.host IS NULL')
.orWhere(`NOT (( ${mutingInstanceQuery.getQuery()} )::jsonb ? notifier.host)`);
}));
query.setParameters(mutingInstanceQuery.getParameters());
// suspended users
query.andWhere(new Brackets(qb => { qb
.where(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`)
.orWhere('notification.notifierId IS NULL');
}));
if (ps.following) {
query.andWhere(`((notification.notifierId IN (${ followingQuery.getQuery() })) OR (notification.notifierId = :meId))`, { meId: me.id });
query.setParameters(followingQuery.getParameters());
}
if (ps.includeTypes && ps.includeTypes.length > 0) {
query.andWhere('notification.type IN (:...includeTypes)', { includeTypes: ps.includeTypes });
} else if (ps.excludeTypes && ps.excludeTypes.length > 0) {
query.andWhere('notification.type NOT IN (:...excludeTypes)', { excludeTypes: ps.excludeTypes });
}
if (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);
});
}
// 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 });
const mutingQuery = Mutings.createQueryBuilder('muting')
.select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: user.id });
const mutingInstanceQuery = UserProfiles.createQueryBuilder('user_profile')
.select('user_profile.mutedInstances')
.where('user_profile.userId = :muterId', { muterId: user.id });
const suspendedQuery = Users.createQueryBuilder('users')
.select('users.id')
.where('users.isSuspended = TRUE');
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');
// muted users
query.andWhere(new Brackets(qb => { qb
.where(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`)
.orWhere('notification.notifierId IS NULL');
}));
query.setParameters(mutingQuery.getParameters());
// muted instances
query.andWhere(new Brackets(qb => { qb
.andWhere('notifier.host IS NULL')
.orWhere(`NOT (( ${mutingInstanceQuery.getQuery()} )::jsonb ? notifier.host)`);
}));
query.setParameters(mutingInstanceQuery.getParameters());
// suspended users
query.andWhere(new Brackets(qb => { qb
.where(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`)
.orWhere('notification.notifierId IS NULL');
}));
if (ps.following) {
query.andWhere(`((notification.notifierId IN (${ followingQuery.getQuery() })) OR (notification.notifierId = :meId))`, { meId: user.id });
query.setParameters(followingQuery.getParameters());
}
if (ps.includeTypes && ps.includeTypes.length > 0) {
query.andWhere('notification.type IN (:...includeTypes)', { includeTypes: ps.includeTypes });
} else if (ps.excludeTypes && ps.excludeTypes.length > 0) {
query.andWhere('notification.type NOT IN (:...excludeTypes)', { excludeTypes: ps.excludeTypes });
}
if (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) {
readNotification(user.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) {
read(user.id, notes);
}
return await Notifications.packMany(notifications, user.id);
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
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 (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 this.announcementReadsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
announcementId: ps.announcementId,
userId: me.id,
});
if (!await this.userEntityService.getHasUnreadAnnouncement(me.id)) {
this.globalEventService.publishMainStream(me.id, 'readAllAnnouncements');
}
});
}
// Check if already read
const read = await AnnouncementReads.findOneBy({
announcementId: ps.announcementId,
userId: user.id,
});
if (read != null) {
return;
}
// Create read
await AnnouncementReads.insert({
id: genId(),
createdAt: new Date(),
announcementId: ps.announcementId,
userId: user.id,
});
if (!await Users.getHasUnreadAnnouncement(user.id)) {
publishMainStream(user.id, 'readAllAnnouncements');
}
});
}

View File

@@ -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!);
if (!same) {
throw new Error('incorrect password');
}
const newToken = generateUserToken();
await this.usersRepository.update(me.id, {
token: newToken,
});
// 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);
});
}
const newToken = generateUserToken();
await Users.update(user.id, {
token: newToken,
});
// Publish event
publishInternalEvent('userTokenRegenerated', { id: user.id, oldToken, newToken });
publishMainStream(user.id, 'myTokenRegenerated');
// Terminate streaming
setTimeout(() => {
publishUserEvent(user.id, 'terminate', {});
}, 5000);
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 this.registryItemsRepository.remove(item);
});
}
await RegistryItems.remove(item);
});
}

View File

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

View File

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

View File

@@ -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
this.globalEventService.publishUserEvent(me.id, 'terminate');
}
});
// Terminate streaming
publishUserEvent(user.id, 'terminate');
}
});
}

View File

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

View File

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

View File

@@ -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,
});
// Publish meUpdated event
this.globalEventService.publishMainStream(me.id, 'meUpdated', iObj);
if (ps.email != null) {
const code = rndstr('a-z0-9', 16);
await this.userProfilesRepository.update(me.id, {
emailVerifyCode: code,
});
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;
});
const link = `${config.url}/verify-email/${code}`;
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;
});
}

View File

@@ -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;
const updates = {} as Partial<User>;
const profileUpdates = {} as Partial<UserProfile>;
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
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);
try {
new RE2(regexp[1], regexp[2]);
} catch (err) {
throw new ApiError(meta.errors.invalidRegexp);
}
});
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;
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.avatarId) {
const avatar = await this.driveFilesRepository.findOneBy({ id: ps.avatarId });
if (ps.avatarId) {
const avatar = await DriveFiles.findOneBy({ id: ps.avatarId });
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 (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.bannerId) {
const banner = await this.driveFilesRepository.findOneBy({ id: ps.bannerId });
if (ps.bannerId) {
const banner = await DriveFiles.findOneBy({ id: ps.bannerId });
if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
if (!banner.type.startsWith('image/')) throw new ApiError(meta.errors.bannerNotAnImage);
}
if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
if (!banner.type.startsWith('image/')) throw new ApiError(meta.errors.bannerNotAnImage);
}
if (ps.pinnedPageId) {
const page = await this.pagesRepository.findOneBy({ id: ps.pinnedPageId });
if (ps.pinnedPageId) {
const page = await Pages.findOneBy({ id: ps.pinnedPageId });
if (page == null || page.userId !== user.id) throw new ApiError(meta.errors.noSuchPage);
if (page == null || page.userId !== user.id) throw new ApiError(meta.errors.noSuchPage);
profileUpdates.pinnedPageId = page.id;
} else if (ps.pinnedPageId === null) {
profileUpdates.pinnedPageId = null;
}
profileUpdates.pinnedPageId = page.id;
} else if (ps.pinnedPageId === null) {
profileUpdates.pinnedPageId = null;
}
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 (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 };
//#region emojis/tags
let emojis = [] as string[];
let tags = [] as string[];
const newName = updates.name === undefined ? user.name : updates.name;
const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
if (newName != null) {
const tokens = mfm.parseSimple(newName);
emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!));
}
if (newDescription != null) {
const tokens = mfm.parse(newDescription);
emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!));
tags = extractHashtags(tokens!).map(tag => normalizeForSearch(tag)).splice(0, 32);
}
updates.emojis = emojis;
updates.tags = tags;
// ハッシュタグ更新
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);
const iObj = await this.userEntityService.pack<true, true>(user.id, user, {
detail: true,
includeSecrets: isSecure,
});
// 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;
});
}
//#region emojis/tags
let emojis = [] as string[];
let tags = [] as string[];
const newName = updates.name === undefined ? user.name : updates.name;
const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
if (newName != null) {
const tokens = mfm.parseSimple(newName);
emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!));
}
if (newDescription != null) {
const tokens = mfm.parse(newDescription);
emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!));
tags = extractHashtags(tokens!).map(tag => normalizeForSearch(tag)).splice(0, 32);
}
updates.emojis = emojis;
updates.tags = tags;
// ハッシュタグ更新
updateUsertags(user, tags);
//#endregion
if (Object.keys(updates).length > 0) await Users.update(user.id, updates);
if (Object.keys(profileUpdates).length > 0) await UserProfiles.update(user.id, profileUpdates);
const iObj = await Users.pack<true, true>(user.id, user, {
detail: true,
includeSecrets: isSecure,
});
// Publish meUpdated event
publishMainStream(user.id, 'meUpdated', iObj);
publishUserEvent(user.id, 'updateUserProfile', await UserProfiles.findOneBy({ userId: user.id }));
// 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認
if (user.isLocked && ps.isLocked === false) {
acceptAllFollowRequests(user);
}
// フォロワーにUpdateを配信
publishToFollowers(user.id);
return iObj;
});
}

View File

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

View File

@@ -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,
publishInternalEvent('webhookCreated', webhook);
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]));
return webhook;
});
this.globalEventService.publishInternalEvent('webhookCreated', webhook);
return webhook;
});
}
}

View File

@@ -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,
});
if (webhook == null) {
throw new ApiError(meta.errors.noSuchWebhook);
}
await this.webhooksRepository.delete(webhook.id);
this.globalEventService.publishInternalEvent('webhookDeleted', webhook);
});
}
await Webhooks.delete(webhook.id);
publishInternalEvent('webhookDeleted', webhook);
});
}

View File

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

View File

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

View File

@@ -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,
});
if (webhook == null) {
throw new ApiError(meta.errors.noSuchWebhook);
}
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);
});
}
await Webhooks.update(webhook.id, {
name: ps.name,
url: ps.url,
secret: ps.secret,
on: ps.on,
active: ps.active,
});
publishInternalEvent('webhookUpdated', webhook);
});
}