diff options
Diffstat (limited to 'src/server/api')
365 files changed, 0 insertions, 26078 deletions
diff --git a/src/server/api/2fa.ts b/src/server/api/2fa.ts deleted file mode 100644 index 117446383d..0000000000 --- a/src/server/api/2fa.ts +++ /dev/null @@ -1,422 +0,0 @@ -import * as crypto from 'crypto'; -import config from '@/config/index'; -import * as jsrsasign from 'jsrsasign'; - -const ECC_PRELUDE = Buffer.from([0x04]); -const NULL_BYTE = Buffer.from([0]); -const PEM_PRELUDE = Buffer.from( - '3059301306072a8648ce3d020106082a8648ce3d030107034200', - 'hex' -); - -// Android Safetynet attestations are signed with this cert: -const GSR2 = `-----BEGIN CERTIFICATE----- -MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G -A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp -Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1 -MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG -A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL -v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8 -eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq -tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd -C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa -zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB -mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH -V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n -bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG -3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs -J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO -291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS -ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd -AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 -TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== ------END CERTIFICATE-----\n`; - -function base64URLDecode(source: string) { - return Buffer.from(source.replace(/\-/g, '+').replace(/_/g, '/'), 'base64'); -} - -function getCertSubject(certificate: string) { - const subjectCert = new jsrsasign.X509(); - subjectCert.readCertPEM(certificate); - - const subjectString = subjectCert.getSubjectString(); - const subjectFields = subjectString.slice(1).split('/'); - - const fields = {} as Record<string, string>; - for (const field of subjectFields) { - const eqIndex = field.indexOf('='); - fields[field.substring(0, eqIndex)] = field.substring(eqIndex + 1); - } - - return fields; -} - -function verifyCertificateChain(certificates: string[]) { - let valid = true; - - for (let i = 0; i < certificates.length; i++) { - const Cert = certificates[i]; - const certificate = new jsrsasign.X509(); - certificate.readCertPEM(Cert); - - const CACert = i + 1 >= certificates.length ? Cert : certificates[i + 1]; - - const certStruct = jsrsasign.ASN1HEX.getTLVbyList(certificate.hex!, 0, [0]); - const algorithm = certificate.getSignatureAlgorithmField(); - const signatureHex = certificate.getSignatureValueHex(); - - // Verify against CA - const Signature = new jsrsasign.KJUR.crypto.Signature({alg: algorithm}); - Signature.init(CACert); - Signature.updateHex(certStruct); - valid = valid && !!Signature.verify(signatureHex); // true if CA signed the certificate - } - - return valid; -} - -function PEMString(pemBuffer: Buffer, type = 'CERTIFICATE') { - if (pemBuffer.length === 65 && pemBuffer[0] === 0x04) { - pemBuffer = Buffer.concat([PEM_PRELUDE, pemBuffer], 91); - type = 'PUBLIC KEY'; - } - const cert = pemBuffer.toString('base64'); - - const keyParts = []; - const max = Math.ceil(cert.length / 64); - let start = 0; - for (let i = 0; i < max; i++) { - keyParts.push(cert.substring(start, start + 64)); - start += 64; - } - - return ( - `-----BEGIN ${type}-----\n` + - keyParts.join('\n') + - `\n-----END ${type}-----\n` - ); -} - -export function hash(data: Buffer) { - return crypto - .createHash('sha256') - .update(data) - .digest(); -} - -export function verifyLogin({ - publicKey, - authenticatorData, - clientDataJSON, - clientData, - signature, - challenge -}: { - publicKey: Buffer, - authenticatorData: Buffer, - clientDataJSON: Buffer, - clientData: any, - signature: Buffer, - challenge: string -}) { - if (clientData.type != 'webauthn.get') { - throw new Error('type is not webauthn.get'); - } - - if (hash(clientData.challenge).toString('hex') != challenge) { - throw new Error('challenge mismatch'); - } - if (clientData.origin != config.scheme + '://' + config.host) { - throw new Error('origin mismatch'); - } - - const verificationData = Buffer.concat( - [authenticatorData, hash(clientDataJSON)], - 32 + authenticatorData.length - ); - - return crypto - .createVerify('SHA256') - .update(verificationData) - .verify(PEMString(publicKey), signature); -} - -export const procedures = { - none: { - verify({publicKey}: {publicKey: Map<number, Buffer>}) { - const negTwo = publicKey.get(-2); - - if (!negTwo || negTwo.length != 32) { - throw new Error('invalid or no -2 key given'); - } - const negThree = publicKey.get(-3); - if (!negThree || negThree.length != 32) { - throw new Error('invalid or no -3 key given'); - } - - const publicKeyU2F = Buffer.concat( - [ECC_PRELUDE, negTwo, negThree], - 1 + 32 + 32 - ); - - return { - publicKey: publicKeyU2F, - valid: true - }; - } - }, - 'android-key': { - verify({ - attStmt, - authenticatorData, - clientDataHash, - publicKey, - rpIdHash, - credentialId - }: { - attStmt: any, - authenticatorData: Buffer, - clientDataHash: Buffer, - publicKey: Map<number, any>; - rpIdHash: Buffer, - credentialId: Buffer, - }) { - if (attStmt.alg != -7) { - throw new Error('alg mismatch'); - } - - const verificationData = Buffer.concat([ - authenticatorData, - clientDataHash - ]); - - const attCert: Buffer = attStmt.x5c[0]; - - const negTwo = publicKey.get(-2); - - if (!negTwo || negTwo.length != 32) { - throw new Error('invalid or no -2 key given'); - } - const negThree = publicKey.get(-3); - if (!negThree || negThree.length != 32) { - throw new Error('invalid or no -3 key given'); - } - - const publicKeyData = Buffer.concat( - [ECC_PRELUDE, negTwo, negThree], - 1 + 32 + 32 - ); - - if (!attCert.equals(publicKeyData)) { - throw new Error('public key mismatch'); - } - - const isValid = crypto - .createVerify('SHA256') - .update(verificationData) - .verify(PEMString(attCert), attStmt.sig); - - // TODO: Check 'attestationChallenge' field in extension of cert matches hash(clientDataJSON) - - return { - valid: isValid, - publicKey: publicKeyData - }; - } - }, - // what a stupid attestation - 'android-safetynet': { - verify({ - attStmt, - authenticatorData, - clientDataHash, - publicKey, - rpIdHash, - credentialId - }: { - attStmt: any, - authenticatorData: Buffer, - clientDataHash: Buffer, - publicKey: Map<number, any>; - rpIdHash: Buffer, - credentialId: Buffer, - }) { - const verificationData = hash( - Buffer.concat([authenticatorData, clientDataHash]) - ); - - const jwsParts = attStmt.response.toString('utf-8').split('.'); - - const header = JSON.parse(base64URLDecode(jwsParts[0]).toString('utf-8')); - const response = JSON.parse( - base64URLDecode(jwsParts[1]).toString('utf-8') - ); - const signature = jwsParts[2]; - - if (!verificationData.equals(Buffer.from(response.nonce, 'base64'))) { - throw new Error('invalid nonce'); - } - - const certificateChain = header.x5c - .map((key: any) => PEMString(key)) - .concat([GSR2]); - - if (getCertSubject(certificateChain[0]).CN != 'attest.android.com') { - throw new Error('invalid common name'); - } - - if (!verifyCertificateChain(certificateChain)) { - throw new Error('Invalid certificate chain!'); - } - - const signatureBase = Buffer.from( - jwsParts[0] + '.' + jwsParts[1], - 'utf-8' - ); - - const valid = crypto - .createVerify('sha256') - .update(signatureBase) - .verify(certificateChain[0], base64URLDecode(signature)); - - const negTwo = publicKey.get(-2); - - if (!negTwo || negTwo.length != 32) { - throw new Error('invalid or no -2 key given'); - } - const negThree = publicKey.get(-3); - if (!negThree || negThree.length != 32) { - throw new Error('invalid or no -3 key given'); - } - - const publicKeyData = Buffer.concat( - [ECC_PRELUDE, negTwo, negThree], - 1 + 32 + 32 - ); - return { - valid, - publicKey: publicKeyData - }; - } - }, - packed: { - verify({ - attStmt, - authenticatorData, - clientDataHash, - publicKey, - rpIdHash, - credentialId - }: { - attStmt: any, - authenticatorData: Buffer, - clientDataHash: Buffer, - publicKey: Map<number, any>; - rpIdHash: Buffer, - credentialId: Buffer, - }) { - const verificationData = Buffer.concat([ - authenticatorData, - clientDataHash - ]); - - if (attStmt.x5c) { - const attCert = attStmt.x5c[0]; - - const validSignature = crypto - .createVerify('SHA256') - .update(verificationData) - .verify(PEMString(attCert), attStmt.sig); - - const negTwo = publicKey.get(-2); - - if (!negTwo || negTwo.length != 32) { - throw new Error('invalid or no -2 key given'); - } - const negThree = publicKey.get(-3); - if (!negThree || negThree.length != 32) { - throw new Error('invalid or no -3 key given'); - } - - const publicKeyData = Buffer.concat( - [ECC_PRELUDE, negTwo, negThree], - 1 + 32 + 32 - ); - - return { - valid: validSignature, - publicKey: publicKeyData - }; - } else if (attStmt.ecdaaKeyId) { - // https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-ecdaa-algorithm-v2.0-id-20180227.html#ecdaa-verify-operation - throw new Error('ECDAA-Verify is not supported'); - } else { - if (attStmt.alg != -7) throw new Error('alg mismatch'); - - throw new Error('self attestation is not supported'); - } - } - }, - - 'fido-u2f': { - verify({ - attStmt, - authenticatorData, - clientDataHash, - publicKey, - rpIdHash, - credentialId - }: { - attStmt: any, - authenticatorData: Buffer, - clientDataHash: Buffer, - publicKey: Map<number, any>, - rpIdHash: Buffer, - credentialId: Buffer - }) { - const x5c: Buffer[] = attStmt.x5c; - if (x5c.length != 1) { - throw new Error('x5c length does not match expectation'); - } - - const attCert = x5c[0]; - - // TODO: make sure attCert is an Elliptic Curve (EC) public key over the P-256 curve - - const negTwo: Buffer = publicKey.get(-2); - - if (!negTwo || negTwo.length != 32) { - throw new Error('invalid or no -2 key given'); - } - const negThree: Buffer = publicKey.get(-3); - if (!negThree || negThree.length != 32) { - throw new Error('invalid or no -3 key given'); - } - - const publicKeyU2F = Buffer.concat( - [ECC_PRELUDE, negTwo, negThree], - 1 + 32 + 32 - ); - - const verificationData = Buffer.concat([ - NULL_BYTE, - rpIdHash, - clientDataHash, - credentialId, - publicKeyU2F - ]); - - const validSignature = crypto - .createVerify('SHA256') - .update(verificationData) - .verify(PEMString(attCert), attStmt.sig); - - return { - valid: validSignature, - publicKey: publicKeyU2F - }; - } - } -}; diff --git a/src/server/api/api-handler.ts b/src/server/api/api-handler.ts deleted file mode 100644 index cbace8917e..0000000000 --- a/src/server/api/api-handler.ts +++ /dev/null @@ -1,51 +0,0 @@ -import * as Koa from 'koa'; - -import { IEndpoint } from './endpoints'; -import authenticate, { AuthenticationError } from './authenticate'; -import call from './call'; -import { ApiError } from './error'; - -export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise((res) => { - const body = ctx.request.body; - - const reply = (x?: any, y?: ApiError) => { - if (x == null) { - ctx.status = 204; - } else if (typeof x === 'number' && y) { - ctx.status = x; - ctx.body = { - error: { - message: y!.message, - code: y!.code, - id: y!.id, - kind: y!.kind, - ...(y!.info ? { info: y!.info } : {}) - } - }; - } else { - // 文字列を返す場合は、JSON.stringify通さないとJSONと認識されない - ctx.body = typeof x === 'string' ? JSON.stringify(x) : x; - } - res(); - }; - - // Authentication - authenticate(body['i']).then(([user, app]) => { - // API invoking - call(endpoint.name, user, app, body, (ctx as any).file).then((res: any) => { - reply(res); - }).catch((e: ApiError) => { - reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e); - }); - }).catch(e => { - if (e instanceof AuthenticationError) { - reply(403, new ApiError({ - message: 'Authentication failed. Please ensure your token is correct.', - code: 'AUTHENTICATION_FAILED', - id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14' - })); - } else { - reply(500, new ApiError()); - } - }); -}); diff --git a/src/server/api/authenticate.ts b/src/server/api/authenticate.ts deleted file mode 100644 index b8e216edc4..0000000000 --- a/src/server/api/authenticate.ts +++ /dev/null @@ -1,62 +0,0 @@ -import isNativeToken from './common/is-native-token'; -import { User } from '@/models/entities/user'; -import { Users, AccessTokens, Apps } from '@/models/index'; -import { AccessToken } from '@/models/entities/access-token'; - -export class AuthenticationError extends Error { - constructor(message: string) { - super(message); - this.name = 'AuthenticationError'; - } -} - -export default async (token: string): Promise<[User | null | undefined, App | null | undefined]> => { - if (token == null) { - return [null, null]; - } - - if (isNativeToken(token)) { - // Fetch user - const user = await Users - .findOne({ token }); - - if (user == null) { - throw new AuthenticationError('user not found'); - } - - return [user, null]; - } else { - const accessToken = await AccessTokens.findOne({ - where: [{ - hash: token.toLowerCase() // app - }, { - token: token // miauth - }], - }); - - if (accessToken == null) { - throw new AuthenticationError('invalid signature'); - } - - AccessTokens.update(accessToken.id, { - lastUsedAt: new Date(), - }); - - const user = await Users - .findOne({ - id: accessToken.userId // findOne(accessToken.userId) のように書かないのは後方互換性のため - }); - - if (accessToken.appId) { - const app = await Apps - .findOneOrFail(accessToken.appId); - - return [user, { - id: accessToken.id, - permission: app.permission - } as AccessToken]; - } else { - return [user, accessToken]; - } - } -}; diff --git a/src/server/api/call.ts b/src/server/api/call.ts deleted file mode 100644 index bd86ffdc35..0000000000 --- a/src/server/api/call.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { performance } from 'perf_hooks'; -import limiter from './limiter'; -import { User } from '@/models/entities/user'; -import endpoints from './endpoints'; -import { ApiError } from './error'; -import { apiLogger } from './logger'; -import { AccessToken } from '@/models/entities/access-token'; - -const accessDenied = { - message: 'Access denied.', - code: 'ACCESS_DENIED', - id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e' -}; - -export default async (endpoint: string, user: User | null | undefined, token: AccessToken | null | undefined, data: any, file?: any) => { - const isSecure = user != null && token == null; - - const ep = endpoints.find(e => e.name === endpoint); - - if (ep == null) { - throw new ApiError({ - message: 'No such endpoint.', - code: 'NO_SUCH_ENDPOINT', - id: 'f8080b67-5f9c-4eb7-8c18-7f1eeae8f709', - httpStatusCode: 404 - }); - } - - if (ep.meta.secure && !isSecure) { - throw new ApiError(accessDenied); - } - - if (ep.meta.requireCredential && user == null) { - throw new ApiError({ - message: 'Credential required.', - code: 'CREDENTIAL_REQUIRED', - id: '1384574d-a912-4b81-8601-c7b1c4085df1', - httpStatusCode: 401 - }); - } - - if (ep.meta.requireCredential && user!.isSuspended) { - throw new ApiError({ - message: 'Your account has been suspended.', - code: 'YOUR_ACCOUNT_SUSPENDED', - id: 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370', - httpStatusCode: 403 - }); - } - - if (ep.meta.requireAdmin && !user!.isAdmin) { - throw new ApiError(accessDenied, { reason: 'You are not the admin.' }); - } - - if (ep.meta.requireModerator && !user!.isAdmin && !user!.isModerator) { - throw new ApiError(accessDenied, { reason: 'You are not a moderator.' }); - } - - if (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) { - throw new ApiError({ - message: 'Your app does not have the necessary permissions to use this endpoint.', - code: 'PERMISSION_DENIED', - id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838', - }); - } - - if (ep.meta.requireCredential && ep.meta.limit && !user!.isAdmin && !user!.isModerator) { - // Rate limit - await limiter(ep, user!).catch(e => { - throw new ApiError({ - message: 'Rate limit exceeded. Please try again later.', - code: 'RATE_LIMIT_EXCEEDED', - id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', - httpStatusCode: 429 - }); - }); - } - - // API invoking - const before = performance.now(); - return await ep.exec(data, user, token, file).catch((e: Error) => { - if (e instanceof ApiError) { - throw e; - } else { - apiLogger.error(`Internal error occurred in ${ep.name}: ${e?.message}`, { - ep: ep.name, - ps: data, - e: { - message: e?.message, - code: e?.name, - stack: e?.stack - } - }); - throw new ApiError(null, { - e: { - message: e?.message, - code: e?.name, - stack: e?.stack - } - }); - } - }).finally(() => { - const after = performance.now(); - const time = after - before; - if (time > 1000) { - apiLogger.warn(`SLOW API CALL DETECTED: ${ep.name} (${time}ms)`); - } - }); -}; diff --git a/src/server/api/common/generate-block-query.ts b/src/server/api/common/generate-block-query.ts deleted file mode 100644 index 4fd6184738..0000000000 --- a/src/server/api/common/generate-block-query.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { User } from '@/models/entities/user'; -import { Blockings } from '@/models/index'; -import { Brackets, SelectQueryBuilder } from 'typeorm'; - -// ここでいうBlockedは被Blockedの意 -export function generateBlockedUserQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) { - const blockingQuery = Blockings.createQueryBuilder('blocking') - .select('blocking.blockerId') - .where('blocking.blockeeId = :blockeeId', { blockeeId: me.id }); - - // 投稿の作者にブロックされていない かつ - // 投稿の返信先の作者にブロックされていない かつ - // 投稿の引用元の作者にブロックされていない - q - .andWhere(`note.userId NOT IN (${ blockingQuery.getQuery() })`) - .andWhere(new Brackets(qb => { qb - .where(`note.replyUserId IS NULL`) - .orWhere(`note.replyUserId NOT IN (${ blockingQuery.getQuery() })`); - })) - .andWhere(new Brackets(qb => { qb - .where(`note.renoteUserId IS NULL`) - .orWhere(`note.renoteUserId NOT IN (${ blockingQuery.getQuery() })`); - })); - - q.setParameters(blockingQuery.getParameters()); -} - -export function generateBlockQueryForUsers(q: SelectQueryBuilder<any>, me: { id: User['id'] }) { - const blockingQuery = Blockings.createQueryBuilder('blocking') - .select('blocking.blockeeId') - .where('blocking.blockerId = :blockerId', { blockerId: me.id }); - - const blockedQuery = Blockings.createQueryBuilder('blocking') - .select('blocking.blockerId') - .where('blocking.blockeeId = :blockeeId', { blockeeId: me.id }); - - q.andWhere(`user.id NOT IN (${ blockingQuery.getQuery() })`); - q.setParameters(blockingQuery.getParameters()); - - q.andWhere(`user.id NOT IN (${ blockedQuery.getQuery() })`); - q.setParameters(blockedQuery.getParameters()); -} diff --git a/src/server/api/common/generate-channel-query.ts b/src/server/api/common/generate-channel-query.ts deleted file mode 100644 index 80a0acf7f9..0000000000 --- a/src/server/api/common/generate-channel-query.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { User } from '@/models/entities/user'; -import { ChannelFollowings } from '@/models/index'; -import { Brackets, SelectQueryBuilder } from 'typeorm'; - -export function generateChannelQuery(q: SelectQueryBuilder<any>, me?: { id: User['id'] } | null) { - if (me == null) { - q.andWhere('note.channelId IS NULL'); - } else { - q.leftJoinAndSelect('note.channel', 'channel'); - - const channelFollowingQuery = ChannelFollowings.createQueryBuilder('channelFollowing') - .select('channelFollowing.followeeId') - .where('channelFollowing.followerId = :followerId', { followerId: me.id }); - - q.andWhere(new Brackets(qb => { qb - // チャンネルのノートではない - .where('note.channelId IS NULL') - // または自分がフォローしているチャンネルのノート - .orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`); - })); - - q.setParameters(channelFollowingQuery.getParameters()); - } -} diff --git a/src/server/api/common/generate-muted-note-query.ts b/src/server/api/common/generate-muted-note-query.ts deleted file mode 100644 index 0737842613..0000000000 --- a/src/server/api/common/generate-muted-note-query.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { User } from '@/models/entities/user'; -import { MutedNotes } from '@/models/index'; -import { SelectQueryBuilder } from 'typeorm'; - -export function generateMutedNoteQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) { - const mutedQuery = MutedNotes.createQueryBuilder('muted') - .select('muted.noteId') - .where('muted.userId = :userId', { userId: me.id }); - - q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`); - - q.setParameters(mutedQuery.getParameters()); -} diff --git a/src/server/api/common/generate-muted-note-thread-query.ts b/src/server/api/common/generate-muted-note-thread-query.ts deleted file mode 100644 index 7e2cbd498b..0000000000 --- a/src/server/api/common/generate-muted-note-thread-query.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { User } from '@/models/entities/user'; -import { NoteThreadMutings } from '@/models/index'; -import { Brackets, SelectQueryBuilder } from 'typeorm'; - -export function generateMutedNoteThreadQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) { - const mutedQuery = NoteThreadMutings.createQueryBuilder('threadMuted') - .select('threadMuted.threadId') - .where('threadMuted.userId = :userId', { userId: me.id }); - - q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`); - q.andWhere(new Brackets(qb => { qb - .where(`note.threadId IS NULL`) - .orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`); - })); - - q.setParameters(mutedQuery.getParameters()); -} diff --git a/src/server/api/common/generate-muted-user-query.ts b/src/server/api/common/generate-muted-user-query.ts deleted file mode 100644 index 7e200b87ef..0000000000 --- a/src/server/api/common/generate-muted-user-query.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { User } from '@/models/entities/user'; -import { Mutings } from '@/models/index'; -import { SelectQueryBuilder, Brackets } from 'typeorm'; - -export function generateMutedUserQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }, exclude?: User) { - const mutingQuery = Mutings.createQueryBuilder('muting') - .select('muting.muteeId') - .where('muting.muterId = :muterId', { muterId: me.id }); - - if (exclude) { - mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id }); - } - - // 投稿の作者をミュートしていない かつ - // 投稿の返信先の作者をミュートしていない かつ - // 投稿の引用元の作者をミュートしていない - q - .andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`) - .andWhere(new Brackets(qb => { qb - .where(`note.replyUserId IS NULL`) - .orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`); - })) - .andWhere(new Brackets(qb => { qb - .where(`note.renoteUserId IS NULL`) - .orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`); - })); - - q.setParameters(mutingQuery.getParameters()); -} - -export function generateMutedUserQueryForUsers(q: SelectQueryBuilder<any>, me: { id: User['id'] }) { - const mutingQuery = Mutings.createQueryBuilder('muting') - .select('muting.muteeId') - .where('muting.muterId = :muterId', { muterId: me.id }); - - q - .andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`); - - q.setParameters(mutingQuery.getParameters()); -} diff --git a/src/server/api/common/generate-native-user-token.ts b/src/server/api/common/generate-native-user-token.ts deleted file mode 100644 index 1f791c57ce..0000000000 --- a/src/server/api/common/generate-native-user-token.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { secureRndstr } from '@/misc/secure-rndstr'; - -export default () => secureRndstr(16, true); diff --git a/src/server/api/common/generate-replies-query.ts b/src/server/api/common/generate-replies-query.ts deleted file mode 100644 index fbc41b2c25..0000000000 --- a/src/server/api/common/generate-replies-query.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { User } from '@/models/entities/user'; -import { Brackets, SelectQueryBuilder } from 'typeorm'; - -export function generateRepliesQuery(q: SelectQueryBuilder<any>, me?: { id: User['id'] } | null) { - if (me == null) { - q.andWhere(new Brackets(qb => { qb - .where(`note.replyId IS NULL`) // 返信ではない - .orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信 - .where(`note.replyId IS NOT NULL`) - .andWhere('note.replyUserId = note.userId'); - })); - })); - } else { - q.andWhere(new Brackets(qb => { qb - .where(`note.replyId IS NULL`) // 返信ではない - .orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信だけど自分のノートへの返信 - .orWhere(new Brackets(qb => { qb // 返信だけど自分の行った返信 - .where(`note.replyId IS NOT NULL`) - .andWhere('note.userId = :meId', { meId: me.id }); - })) - .orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信 - .where(`note.replyId IS NOT NULL`) - .andWhere('note.replyUserId = note.userId'); - })); - })); - } -} diff --git a/src/server/api/common/generate-visibility-query.ts b/src/server/api/common/generate-visibility-query.ts deleted file mode 100644 index 813e8b6c09..0000000000 --- a/src/server/api/common/generate-visibility-query.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { User } from '@/models/entities/user'; -import { Followings } from '@/models/index'; -import { Brackets, SelectQueryBuilder } from 'typeorm'; - -export function generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: { id: User['id'] } | null) { - if (me == null) { - q.andWhere(new Brackets(qb => { qb - .where(`note.visibility = 'public'`) - .orWhere(`note.visibility = 'home'`); - })); - } else { - const followingQuery = Followings.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: me.id }); - - q.andWhere(new Brackets(qb => { qb - // 公開投稿である - .where(new Brackets(qb => { qb - .where(`note.visibility = 'public'`) - .orWhere(`note.visibility = 'home'`); - })) - // または 自分自身 - .orWhere('note.userId = :userId1', { userId1: me.id }) - // または 自分宛て - .orWhere(`'{"${me.id}"}' <@ note.visibleUserIds`) - .orWhere(new Brackets(qb => { qb - // または フォロワー宛ての投稿であり、 - .where('note.visibility = \'followers\'') - .andWhere(new Brackets(qb => { qb - // 自分がフォロワーである - .where(`note.userId IN (${ followingQuery.getQuery() })`) - // または 自分の投稿へのリプライ - .orWhere('note.replyUserId = :userId3', { userId3: me.id }); - })); - })); - })); - - q.setParameters(followingQuery.getParameters()); - } -} diff --git a/src/server/api/common/getters.ts b/src/server/api/common/getters.ts deleted file mode 100644 index 4b2ee8f1da..0000000000 --- a/src/server/api/common/getters.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { IdentifiableError } from '@/misc/identifiable-error'; -import { User } from '@/models/entities/user'; -import { Note } from '@/models/entities/note'; -import { Notes, Users } from '@/models/index'; - -/** - * Get note for API processing - */ -export async function getNote(noteId: Note['id']) { - const note = await Notes.findOne(noteId); - - if (note == null) { - throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.'); - } - - return note; -} - -/** - * Get user for API processing - */ -export async function getUser(userId: User['id']) { - const user = await Users.findOne(userId); - - if (user == null) { - throw new IdentifiableError('15348ddd-432d-49c2-8a5a-8069753becff', 'No such user.'); - } - - return user; -} - -/** - * Get remote user for API processing - */ -export async function getRemoteUser(userId: User['id']) { - const user = await getUser(userId); - - if (!Users.isRemoteUser(user)) { - throw new Error('user is not a remote user'); - } - - return user; -} - -/** - * Get local user for API processing - */ -export async function getLocalUser(userId: User['id']) { - const user = await getUser(userId); - - if (!Users.isLocalUser(user)) { - throw new Error('user is not a local user'); - } - - return user; -} diff --git a/src/server/api/common/inject-featured.ts b/src/server/api/common/inject-featured.ts deleted file mode 100644 index 1dc13c83ef..0000000000 --- a/src/server/api/common/inject-featured.ts +++ /dev/null @@ -1,56 +0,0 @@ -import rndstr from 'rndstr'; -import { Note } from '@/models/entities/note'; -import { User } from '@/models/entities/user'; -import { Notes, UserProfiles, NoteReactions } from '@/models/index'; -import { generateMutedUserQuery } from './generate-muted-user-query'; -import { generateBlockedUserQuery } from './generate-block-query'; - -// TODO: リアクション、Renote、返信などをしたノートは除外する - -export async function injectFeatured(timeline: Note[], user?: User | null) { - if (timeline.length < 5) return; - - if (user) { - const profile = await UserProfiles.findOneOrFail(user.id); - if (!profile.injectFeaturedNote) return; - } - - const max = 30; - const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで - - const query = Notes.createQueryBuilder('note') - .addSelect('note.score') - .where('note.userHost IS NULL') - .andWhere(`note.score > 0`) - .andWhere(`note.createdAt > :date`, { date: new Date(Date.now() - day) }) - .andWhere(`note.visibility = 'public'`) - .innerJoinAndSelect('note.user', 'user'); - - if (user) { - query.andWhere('note.userId != :userId', { userId: user.id }); - - generateMutedUserQuery(query, user); - generateBlockedUserQuery(query, user); - - const reactionQuery = NoteReactions.createQueryBuilder('reaction') - .select('reaction.noteId') - .where('reaction.userId = :userId', { userId: user.id }); - - query.andWhere(`note.id NOT IN (${ reactionQuery.getQuery() })`); - } - - const notes = await query - .orderBy('note.score', 'DESC') - .take(max) - .getMany(); - - if (notes.length === 0) return; - - // Pick random one - const featured = notes[Math.floor(Math.random() * notes.length)]; - - (featured as any)._featuredId_ = rndstr('a-z0-9', 8); - - // Inject featured - timeline.splice(3, 0, featured); -} diff --git a/src/server/api/common/inject-promo.ts b/src/server/api/common/inject-promo.ts deleted file mode 100644 index 87767a65bf..0000000000 --- a/src/server/api/common/inject-promo.ts +++ /dev/null @@ -1,34 +0,0 @@ -import rndstr from 'rndstr'; -import { Note } from '@/models/entities/note'; -import { User } from '@/models/entities/user'; -import { PromoReads, PromoNotes, Notes, Users } from '@/models/index'; - -export async function injectPromo(timeline: Note[], user?: User | null) { - if (timeline.length < 5) return; - - // TODO: readやexpireフィルタはクエリ側でやる - - const reads = user ? await PromoReads.find({ - userId: user.id - }) : []; - - let promos = await PromoNotes.find(); - - promos = promos.filter(n => n.expiresAt.getTime() > Date.now()); - promos = promos.filter(n => !reads.map(r => r.noteId).includes(n.noteId)); - - if (promos.length === 0) return; - - // Pick random promo - const promo = promos[Math.floor(Math.random() * promos.length)]; - - const note = await Notes.findOneOrFail(promo.noteId); - - // Join - note.user = await Users.findOneOrFail(note.userId); - - (note as any)._prId_ = rndstr('a-z0-9', 8); - - // Inject promo - timeline.splice(3, 0, note); -} diff --git a/src/server/api/common/is-native-token.ts b/src/server/api/common/is-native-token.ts deleted file mode 100644 index 2833c570c8..0000000000 --- a/src/server/api/common/is-native-token.ts +++ /dev/null @@ -1 +0,0 @@ -export default (token: string) => token.length === 16; diff --git a/src/server/api/common/make-pagination-query.ts b/src/server/api/common/make-pagination-query.ts deleted file mode 100644 index 51c11e5dff..0000000000 --- a/src/server/api/common/make-pagination-query.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { SelectQueryBuilder } from 'typeorm'; - -export function makePaginationQuery<T>(q: SelectQueryBuilder<T>, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number) { - if (sinceId && untilId) { - q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId }); - q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId }); - q.orderBy(`${q.alias}.id`, 'DESC'); - } else if (sinceId) { - q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId }); - q.orderBy(`${q.alias}.id`, 'ASC'); - } else if (untilId) { - q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId }); - q.orderBy(`${q.alias}.id`, 'DESC'); - } else if (sinceDate && untilDate) { - q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) }); - q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) }); - q.orderBy(`${q.alias}.createdAt`, 'DESC'); - } else if (sinceDate) { - q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) }); - q.orderBy(`${q.alias}.createdAt`, 'ASC'); - } else if (untilDate) { - q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) }); - q.orderBy(`${q.alias}.createdAt`, 'DESC'); - } else { - q.orderBy(`${q.alias}.id`, 'DESC'); - } - return q; -} diff --git a/src/server/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts deleted file mode 100644 index 33f41b2770..0000000000 --- a/src/server/api/common/read-messaging-message.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { publishMainStream, publishGroupMessagingStream } from '@/services/stream'; -import { publishMessagingStream } from '@/services/stream'; -import { publishMessagingIndexStream } from '@/services/stream'; -import { User, IRemoteUser } from '@/models/entities/user'; -import { MessagingMessage } from '@/models/entities/messaging-message'; -import { MessagingMessages, UserGroupJoinings, Users } from '@/models/index'; -import { In } from 'typeorm'; -import { IdentifiableError } from '@/misc/identifiable-error'; -import { UserGroup } from '@/models/entities/user-group'; -import { toArray } from '@/prelude/array'; -import { renderReadActivity } from '@/remote/activitypub/renderer/read'; -import { renderActivity } from '@/remote/activitypub/renderer/index'; -import { deliver } from '@/queue/index'; -import orderedCollection from '@/remote/activitypub/renderer/ordered-collection'; - -/** - * Mark messages as read - */ -export async function readUserMessagingMessage( - userId: User['id'], - otherpartyId: User['id'], - messageIds: MessagingMessage['id'][] -) { - if (messageIds.length === 0) return; - - const messages = await MessagingMessages.find({ - id: In(messageIds) - }); - - for (const message of messages) { - if (message.recipientId !== userId) { - throw new IdentifiableError('e140a4bf-49ce-4fb6-b67c-b78dadf6b52f', 'Access denied (user).'); - } - } - - // Update documents - await MessagingMessages.update({ - id: In(messageIds), - userId: otherpartyId, - recipientId: userId, - isRead: false - }, { - isRead: true - }); - - // Publish event - publishMessagingStream(otherpartyId, userId, 'read', messageIds); - publishMessagingIndexStream(userId, 'read', messageIds); - - if (!await Users.getHasUnreadMessagingMessage(userId)) { - // 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行 - publishMainStream(userId, 'readAllMessagingMessages'); - } -} - -/** - * Mark messages as read - */ -export async function readGroupMessagingMessage( - userId: User['id'], - groupId: UserGroup['id'], - messageIds: MessagingMessage['id'][] -) { - if (messageIds.length === 0) return; - - // check joined - const joining = await UserGroupJoinings.findOne({ - userId: userId, - userGroupId: groupId - }); - - if (joining == null) { - throw new IdentifiableError('930a270c-714a-46b2-b776-ad27276dc569', 'Access denied (group).'); - } - - const messages = await MessagingMessages.find({ - id: In(messageIds) - }); - - const reads: MessagingMessage['id'][] = []; - - for (const message of messages) { - if (message.userId === userId) continue; - if (message.reads.includes(userId)) continue; - - // Update document - await MessagingMessages.createQueryBuilder().update() - .set({ - reads: (() => `array_append("reads", '${joining.userId}')`) as any - }) - .where('id = :id', { id: message.id }) - .execute(); - - reads.push(message.id); - } - - // Publish event - publishGroupMessagingStream(groupId, 'read', { - ids: reads, - userId: userId - }); - publishMessagingIndexStream(userId, 'read', reads); - - if (!await Users.getHasUnreadMessagingMessage(userId)) { - // 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行 - publishMainStream(userId, 'readAllMessagingMessages'); - } -} - -export async function deliverReadActivity(user: { id: User['id']; host: null; }, recipient: IRemoteUser, messages: MessagingMessage | MessagingMessage[]) { - messages = toArray(messages).filter(x => x.uri); - const contents = messages.map(x => renderReadActivity(user, x)); - - if (contents.length > 1) { - const collection = orderedCollection(null, contents.length, undefined, undefined, contents); - deliver(user, renderActivity(collection), recipient.inbox); - } else { - for (const content of contents) { - deliver(user, renderActivity(content), recipient.inbox); - } - } -} diff --git a/src/server/api/common/read-notification.ts b/src/server/api/common/read-notification.ts deleted file mode 100644 index a4406c9eeb..0000000000 --- a/src/server/api/common/read-notification.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { publishMainStream } from '@/services/stream'; -import { User } from '@/models/entities/user'; -import { Notification } from '@/models/entities/notification'; -import { Notifications, Users } from '@/models/index'; -import { In } from 'typeorm'; - -export async function readNotification( - userId: User['id'], - notificationIds: Notification['id'][] -) { - // Update documents - await Notifications.update({ - id: In(notificationIds), - isRead: false - }, { - isRead: true - }); - - post(userId); -} - -export async function readNotificationByQuery( - userId: User['id'], - query: Record<string, any> -) { - // Update documents - await Notifications.update({ - ...query, - notifieeId: userId, - isRead: false - }, { - isRead: true - }); - - post(userId); -} - -async function post(userId: User['id']) { - if (!await Users.getHasUnreadNotification(userId)) { - // 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行 - publishMainStream(userId, 'readAllNotifications'); - } -} diff --git a/src/server/api/common/signin.ts b/src/server/api/common/signin.ts deleted file mode 100644 index 4c7aacf1cd..0000000000 --- a/src/server/api/common/signin.ts +++ /dev/null @@ -1,44 +0,0 @@ -import * as Koa from 'koa'; - -import config from '@/config/index'; -import { ILocalUser } from '@/models/entities/user'; -import { Signins } from '@/models/index'; -import { genId } from '@/misc/gen-id'; -import { publishMainStream } from '@/services/stream'; - -export default function(ctx: Koa.Context, user: ILocalUser, redirect = false) { - if (redirect) { - //#region Cookie - ctx.cookies.set('igi', user.token, { - path: '/', - // SEE: https://github.com/koajs/koa/issues/974 - // When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header - secure: config.url.startsWith('https'), - httpOnly: false - }); - //#endregion - - ctx.redirect(config.url); - } else { - ctx.body = { - id: user.id, - i: user.token - }; - ctx.status = 200; - } - - (async () => { - // Append signin history - const record = await Signins.save({ - id: genId(), - createdAt: new Date(), - userId: user.id, - ip: ctx.ip, - headers: ctx.headers, - success: true - }); - - // Publish signin event - publishMainStream(user.id, 'signin', await Signins.pack(record)); - })(); -} diff --git a/src/server/api/common/signup.ts b/src/server/api/common/signup.ts deleted file mode 100644 index 2ba0d8e479..0000000000 --- a/src/server/api/common/signup.ts +++ /dev/null @@ -1,113 +0,0 @@ -import * as bcrypt from 'bcryptjs'; -import { generateKeyPair } from 'crypto'; -import generateUserToken from './generate-native-user-token'; -import { User } from '@/models/entities/user'; -import { Users, UsedUsernames } from '@/models/index'; -import { UserProfile } from '@/models/entities/user-profile'; -import { getConnection } from 'typeorm'; -import { genId } from '@/misc/gen-id'; -import { toPunyNullable } from '@/misc/convert-host'; -import { UserKeypair } from '@/models/entities/user-keypair'; -import { usersChart } from '@/services/chart/index'; -import { UsedUsername } from '@/models/entities/used-username'; - -export async function signup(opts: { - username: User['username']; - password?: string | null; - passwordHash?: UserProfile['password'] | null; - host?: string | null; -}) { - const { username, password, passwordHash, host } = opts; - let hash = passwordHash; - - // Validate username - if (!Users.validateLocalUsername.ok(username)) { - throw new Error('INVALID_USERNAME'); - } - - if (password != null && passwordHash == null) { - // Validate password - if (!Users.validatePassword.ok(password)) { - throw new Error('INVALID_PASSWORD'); - } - - // Generate hash of password - const salt = await bcrypt.genSalt(8); - hash = await bcrypt.hash(password, salt); - } - - // Generate secret - const secret = generateUserToken(); - - // Check username duplication - if (await Users.findOne({ usernameLower: username.toLowerCase(), host: null })) { - throw new Error('DUPLICATED_USERNAME'); - } - - // Check deleted username duplication - if (await UsedUsernames.findOne({ username: username.toLowerCase() })) { - throw new Error('USED_USERNAME'); - } - - const keyPair = await new Promise<string[]>((res, rej) => - generateKeyPair('rsa', { - modulusLength: 4096, - publicKeyEncoding: { - type: 'spki', - format: 'pem' - }, - privateKeyEncoding: { - type: 'pkcs8', - format: 'pem', - cipher: undefined, - passphrase: undefined - } - } as any, (err, publicKey, privateKey) => - err ? rej(err) : res([publicKey, privateKey]) - )); - - let account!: User; - - // Start transaction - await getConnection().transaction(async transactionalEntityManager => { - const exist = await transactionalEntityManager.findOne(User, { - usernameLower: username.toLowerCase(), - host: null - }); - - if (exist) throw new Error(' the username is already used'); - - account = await transactionalEntityManager.save(new User({ - id: genId(), - createdAt: new Date(), - username: username, - usernameLower: username.toLowerCase(), - host: toPunyNullable(host), - token: secret, - isAdmin: (await Users.count({ - host: null, - })) === 0, - })); - - await transactionalEntityManager.save(new UserKeypair({ - publicKey: keyPair[0], - privateKey: keyPair[1], - userId: account.id - })); - - await transactionalEntityManager.save(new UserProfile({ - userId: account.id, - autoAcceptFollowed: true, - password: hash, - })); - - await transactionalEntityManager.save(new UsedUsername({ - createdAt: new Date(), - username: username.toLowerCase(), - })); - }); - - usersChart.update(account, true); - - return { account, secret }; -} diff --git a/src/server/api/define.ts b/src/server/api/define.ts deleted file mode 100644 index 4bd8f95e31..0000000000 --- a/src/server/api/define.ts +++ /dev/null @@ -1,87 +0,0 @@ -import * as fs from 'fs'; -import { ILocalUser } from '@/models/entities/user'; -import { IEndpointMeta } from './endpoints'; -import { ApiError } from './error'; -import { SchemaType } from '@/misc/schema'; -import { AccessToken } from '@/models/entities/access-token'; - -type NonOptional<T> = T extends undefined ? never : T; - -type SimpleUserInfo = { - id: ILocalUser['id']; - host: ILocalUser['host']; - username: ILocalUser['username']; - uri: ILocalUser['uri']; - inbox: ILocalUser['inbox']; - sharedInbox: ILocalUser['sharedInbox']; - isAdmin: ILocalUser['isAdmin']; - isModerator: ILocalUser['isModerator']; - isSilenced: ILocalUser['isSilenced']; -}; - -type Params<T extends IEndpointMeta> = { - [P in keyof T['params']]: NonNullable<T['params']>[P]['transform'] extends Function - ? ReturnType<NonNullable<T['params']>[P]['transform']> - : NonNullable<T['params']>[P]['default'] extends null | number | string - ? NonOptional<ReturnType<NonNullable<T['params']>[P]['validator']['get']>[0]> - : ReturnType<NonNullable<T['params']>[P]['validator']['get']>[0]; -}; - -export type Response = Record<string, any> | void; - -type executor<T extends IEndpointMeta> = - (params: Params<T>, user: T['requireCredential'] extends true ? SimpleUserInfo : SimpleUserInfo | null, token: AccessToken | null, file?: any, cleanup?: Function) => - Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>; - -export default function <T extends IEndpointMeta>(meta: T, cb: executor<T>) - : (params: any, user: T['requireCredential'] extends true ? SimpleUserInfo : SimpleUserInfo | null, token: AccessToken | null, file?: any) => Promise<any> { - return (params: any, user: T['requireCredential'] extends true ? SimpleUserInfo : SimpleUserInfo | null, token: AccessToken | null, file?: any) => { - function cleanup() { - fs.unlink(file.path, () => {}); - } - - if (meta.requireFile && file == null) return Promise.reject(new ApiError({ - message: 'File required.', - code: 'FILE_REQUIRED', - id: '4267801e-70d1-416a-b011-4ee502885d8b', - })); - - const [ps, pserr] = getParams(meta, params); - if (pserr) { - if (file) cleanup(); - return Promise.reject(pserr); - } - - return cb(ps, user, token, file, cleanup); - }; -} - -function getParams<T extends IEndpointMeta>(defs: T, params: any): [Params<T>, ApiError | null] { - if (defs.params == null) return [params, null]; - - const x: any = {}; - let err: ApiError | null = null; - Object.entries(defs.params).some(([k, def]) => { - const [v, e] = def.validator.get(params[k]); - if (e) { - err = new ApiError({ - message: 'Invalid param.', - code: 'INVALID_PARAM', - id: '3d81ceae-475f-4600-b2a8-2bc116157532', - }, { - param: k, - reason: e.message - }); - return true; - } else { - if (v === undefined && def.hasOwnProperty('default')) { - x[k] = def.default; - } else { - x[k] = v; - } - if (def.transform) x[k] = def.transform(x[k]); - return false; - } - }); - return [x, err]; -} diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts deleted file mode 100644 index 6d9d2b0782..0000000000 --- a/src/server/api/endpoints.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; -import { Context } from 'cafy'; -import * as path from 'path'; -import * as glob from 'glob'; -import { SimpleSchema } from '@/misc/simple-schema'; - -//const _filename = fileURLToPath(import.meta.url); -const _filename = __filename; -const _dirname = dirname(_filename); - -export type Param = { - validator: Context<any>; - transform?: any; - default?: any; - deprecated?: boolean; - ref?: string; -}; - -export interface IEndpointMeta { - stability?: string; //'deprecated' | 'experimental' | 'stable'; - - tags?: string[]; - - params?: { - [key: string]: Param; - }; - - errors?: { - [key: string]: { - message: string; - code: string; - id: string; - }; - }; - - res?: SimpleSchema; - - /** - * このエンドポイントにリクエストするのにユーザー情報が必須か否か - * 省略した場合は false として解釈されます。 - */ - requireCredential?: boolean; - - /** - * 管理者のみ使えるエンドポイントか否か - */ - requireAdmin?: boolean; - - /** - * 管理者またはモデレーターのみ使えるエンドポイントか否か - */ - requireModerator?: boolean; - - /** - * エンドポイントのリミテーションに関するやつ - * 省略した場合はリミテーションは無いものとして解釈されます。 - * また、withCredential が false の場合はリミテーションを行うことはできません。 - */ - limit?: { - - /** - * 複数のエンドポイントでリミットを共有したい場合に指定するキー - */ - key?: string; - - /** - * リミットを適用する期間(ms) - * このプロパティを設定する場合、max プロパティも設定する必要があります。 - */ - duration?: number; - - /** - * durationで指定した期間内にいくつまでリクエストできるのか - * このプロパティを設定する場合、duration プロパティも設定する必要があります。 - */ - max?: number; - - /** - * 最低でもどれくらいの間隔を開けてリクエストしなければならないか(ms) - */ - minInterval?: number; - }; - - /** - * ファイルの添付を必要とするか否か - * 省略した場合は false として解釈されます。 - */ - requireFile?: boolean; - - /** - * サードパーティアプリからはリクエストすることができないか否か - * 省略した場合は false として解釈されます。 - */ - secure?: boolean; - - /** - * エンドポイントの種類 - * パーミッションの実現に利用されます。 - */ - kind?: string; -} - -export interface IEndpoint { - name: string; - exec: any; - meta: IEndpointMeta; -} - -const files = glob.sync('**/*.js', { - cwd: path.resolve(_dirname + '/endpoints/') -}); - -const endpoints: IEndpoint[] = files.map(f => { - const ep = require(`./endpoints/${f}`); - - return { - name: f.replace('.js', ''), - exec: ep.default, - meta: ep.meta || {} - }; -}); - -export default endpoints; diff --git a/src/server/api/endpoints/admin/abuse-user-reports.ts b/src/server/api/endpoints/admin/abuse-user-reports.ts deleted file mode 100644 index 403eb24191..0000000000 --- a/src/server/api/endpoints/admin/abuse-user-reports.ts +++ /dev/null @@ -1,134 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { AbuseUserReports } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - state: { - validator: $.optional.nullable.str, - default: null, - }, - - reporterOrigin: { - validator: $.optional.str.or([ - 'combined', - 'local', - 'remote', - ]), - default: 'combined' - }, - - targetUserOrigin: { - validator: $.optional.str.or([ - 'combined', - 'local', - 'remote', - ]), - default: 'combined' - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - nullable: false as const, optional: false as const, - format: 'id', - example: 'xxxxxxxxxx', - }, - createdAt: { - type: 'string' as const, - nullable: false as const, optional: false as const, - format: 'date-time', - }, - comment: { - type: 'string' as const, - nullable: false as const, optional: false as const, - }, - resolved: { - type: 'boolean' as const, - nullable: false as const, optional: false as const, - example: false - }, - reporterId: { - type: 'string' as const, - nullable: false as const, optional: false as const, - format: 'id', - }, - targetUserId: { - type: 'string' as const, - nullable: false as const, optional: false as const, - format: 'id', - }, - assigneeId: { - type: 'string' as const, - nullable: true as const, optional: false as const, - format: 'id', - }, - reporter: { - type: 'object' as const, - nullable: false as const, optional: false as const, - ref: 'User' - }, - targetUser: { - type: 'object' as const, - nullable: false as const, optional: false as const, - ref: 'User' - }, - assignee: { - type: 'object' as const, - nullable: true as const, optional: true as const, - ref: 'User' - } - } - } - } -}; - -export default define(meta, async (ps) => { - const query = makePaginationQuery(AbuseUserReports.createQueryBuilder('report'), ps.sinceId, ps.untilId); - - switch (ps.state) { - case 'resolved': query.andWhere('report.resolved = TRUE'); break; - case 'unresolved': query.andWhere('report.resolved = FALSE'); break; - } - - switch (ps.reporterOrigin) { - case 'local': query.andWhere('report.reporterHost IS NULL'); break; - case 'remote': query.andWhere('report.reporterHost IS NOT NULL'); break; - } - - switch (ps.targetUserOrigin) { - case 'local': query.andWhere('report.targetUserHost IS NULL'); break; - case 'remote': query.andWhere('report.targetUserHost IS NOT NULL'); break; - } - - const reports = await query.take(ps.limit!).getMany(); - - return await AbuseUserReports.packMany(reports); -}); diff --git a/src/server/api/endpoints/admin/accounts/create.ts b/src/server/api/endpoints/admin/accounts/create.ts deleted file mode 100644 index fa15e84f77..0000000000 --- a/src/server/api/endpoints/admin/accounts/create.ts +++ /dev/null @@ -1,51 +0,0 @@ -import define from '../../../define'; -import { Users } from '@/models/index'; -import { signup } from '../../../common/signup'; - -export const meta = { - tags: ['admin'], - - params: { - username: { - validator: Users.validateLocalUsername, - }, - - password: { - validator: Users.validatePassword, - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User', - properties: { - token: { - type: 'string' as const, - optional: false as const, nullable: false as const, - } - } - } -}; - -export default define(meta, async (ps, _me) => { - const me = _me ? await Users.findOneOrFail(_me.id) : null; - const noUsers = (await Users.count({ - host: null, - })) === 0; - if (!noUsers && !me?.isAdmin) throw new Error('access denied'); - - const { account, secret } = await signup({ - username: ps.username, - password: ps.password, - }); - - const res = await Users.pack(account, account, { - detail: true, - includeSecrets: true - }); - - (res as any).token = secret; - - return res; -}); diff --git a/src/server/api/endpoints/admin/accounts/delete.ts b/src/server/api/endpoints/admin/accounts/delete.ts deleted file mode 100644 index 4e8a559805..0000000000 --- a/src/server/api/endpoints/admin/accounts/delete.ts +++ /dev/null @@ -1,58 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { Users } from '@/models/index'; -import { doPostSuspend } from '@/services/suspend-user'; -import { publishUserEvent } from '@/services/stream'; -import { createDeleteAccountJob } from '@/queue'; -import { ID } from '@/misc/cafy-id'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - userId: { - validator: $.type(ID), - }, - } -}; - -export default define(meta, async (ps, me) => { - const user = await Users.findOne(ps.userId); - - if (user == null) { - throw new Error('user not found'); - } - - if (user.isAdmin) { - throw new Error('cannot suspend admin'); - } - - if (user.isModerator) { - throw new Error('cannot suspend moderator'); - } - - if (Users.isLocalUser(user)) { - // 物理削除する前にDelete activityを送信する - await doPostSuspend(user).catch(e => {}); - - createDeleteAccountJob(user, { - soft: false - }); - } else { - createDeleteAccountJob(user, { - soft: true // リモートユーザーの削除は、完全にDBから物理削除してしまうと再度連合してきてアカウントが復活する可能性があるため、soft指定する - }); - } - - await Users.update(user.id, { - isDeleted: true, - }); - - if (Users.isLocalUser(user)) { - // Terminate streaming - publishUserEvent(user.id, 'terminate', {}); - } -}); diff --git a/src/server/api/endpoints/admin/ad/create.ts b/src/server/api/endpoints/admin/ad/create.ts deleted file mode 100644 index 27c7b5d318..0000000000 --- a/src/server/api/endpoints/admin/ad/create.ts +++ /dev/null @@ -1,49 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { Ads } from '@/models/index'; -import { genId } from '@/misc/gen-id'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - url: { - validator: $.str.min(1) - }, - memo: { - validator: $.str - }, - place: { - validator: $.str - }, - priority: { - validator: $.str - }, - ratio: { - validator: $.num.int().min(0) - }, - expiresAt: { - validator: $.num.int() - }, - imageUrl: { - validator: $.str.min(1) - } - }, -}; - -export default define(meta, async (ps) => { - await Ads.insert({ - id: genId(), - createdAt: new Date(), - expiresAt: new Date(ps.expiresAt), - url: ps.url, - imageUrl: ps.imageUrl, - priority: ps.priority, - ratio: ps.ratio, - place: ps.place, - memo: ps.memo, - }); -}); diff --git a/src/server/api/endpoints/admin/ad/delete.ts b/src/server/api/endpoints/admin/ad/delete.ts deleted file mode 100644 index 91934e1aab..0000000000 --- a/src/server/api/endpoints/admin/ad/delete.ts +++ /dev/null @@ -1,34 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { ID } from '@/misc/cafy-id'; -import { Ads } from '@/models/index'; -import { ApiError } from '../../../error'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - id: { - validator: $.type(ID) - } - }, - - errors: { - noSuchAd: { - message: 'No such ad.', - code: 'NO_SUCH_AD', - id: 'ccac9863-3a03-416e-b899-8a64041118b1' - } - } -}; - -export default define(meta, async (ps, me) => { - const ad = await Ads.findOne(ps.id); - - if (ad == null) throw new ApiError(meta.errors.noSuchAd); - - await Ads.delete(ad.id); -}); diff --git a/src/server/api/endpoints/admin/ad/list.ts b/src/server/api/endpoints/admin/ad/list.ts deleted file mode 100644 index 000aaaba9d..0000000000 --- a/src/server/api/endpoints/admin/ad/list.ts +++ /dev/null @@ -1,36 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { Ads } from '@/models/index'; -import { makePaginationQuery } from '../../../common/make-pagination-query'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - }, -}; - -export default define(meta, async (ps) => { - const query = makePaginationQuery(Ads.createQueryBuilder('ad'), ps.sinceId, ps.untilId) - .andWhere('ad.expiresAt > :now', { now: new Date() }); - - const ads = await query.take(ps.limit!).getMany(); - - return ads; -}); diff --git a/src/server/api/endpoints/admin/ad/update.ts b/src/server/api/endpoints/admin/ad/update.ts deleted file mode 100644 index 36c87895c2..0000000000 --- a/src/server/api/endpoints/admin/ad/update.ts +++ /dev/null @@ -1,63 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { ID } from '@/misc/cafy-id'; -import { Ads } from '@/models/index'; -import { ApiError } from '../../../error'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - id: { - validator: $.type(ID) - }, - memo: { - validator: $.str - }, - url: { - validator: $.str.min(1) - }, - imageUrl: { - validator: $.str.min(1) - }, - place: { - validator: $.str - }, - priority: { - validator: $.str - }, - ratio: { - validator: $.num.int().min(0) - }, - expiresAt: { - validator: $.num.int() - }, - }, - - errors: { - noSuchAd: { - message: 'No such ad.', - code: 'NO_SUCH_AD', - id: 'b7aa1727-1354-47bc-a182-3a9c3973d300' - } - } -}; - -export default define(meta, async (ps, me) => { - const ad = await Ads.findOne(ps.id); - - if (ad == null) throw new ApiError(meta.errors.noSuchAd); - - await Ads.update(ad.id, { - url: ps.url, - place: ps.place, - priority: ps.priority, - ratio: ps.ratio, - memo: ps.memo, - imageUrl: ps.imageUrl, - expiresAt: new Date(ps.expiresAt), - }); -}); diff --git a/src/server/api/endpoints/admin/announcements/create.ts b/src/server/api/endpoints/admin/announcements/create.ts deleted file mode 100644 index f1c07745f9..0000000000 --- a/src/server/api/endpoints/admin/announcements/create.ts +++ /dev/null @@ -1,71 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { Announcements } from '@/models/index'; -import { genId } from '@/misc/gen-id'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - title: { - validator: $.str.min(1) - }, - text: { - validator: $.str.min(1) - }, - imageUrl: { - validator: $.nullable.str.min(1) - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id', - example: 'xxxxxxxxxx', - }, - createdAt: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'date-time', - }, - updatedAt: { - type: 'string' as const, - optional: false as const, nullable: true as const, - format: 'date-time', - }, - title: { - type: 'string' as const, - optional: false as const, nullable: false as const, - }, - text: { - type: 'string' as const, - optional: false as const, nullable: false as const, - }, - imageUrl: { - type: 'string' as const, - optional: false as const, nullable: true as const, - } - } - } -}; - -export default define(meta, async (ps) => { - const announcement = await Announcements.save({ - id: genId(), - createdAt: new Date(), - updatedAt: null, - title: ps.title, - text: ps.text, - imageUrl: ps.imageUrl, - }); - - return announcement; -}); diff --git a/src/server/api/endpoints/admin/announcements/delete.ts b/src/server/api/endpoints/admin/announcements/delete.ts deleted file mode 100644 index 7dbc05b4c9..0000000000 --- a/src/server/api/endpoints/admin/announcements/delete.ts +++ /dev/null @@ -1,34 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { ID } from '@/misc/cafy-id'; -import { Announcements } from '@/models/index'; -import { ApiError } from '../../../error'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - id: { - validator: $.type(ID) - } - }, - - errors: { - noSuchAnnouncement: { - message: 'No such announcement.', - code: 'NO_SUCH_ANNOUNCEMENT', - id: 'ecad8040-a276-4e85-bda9-015a708d291e' - } - } -}; - -export default define(meta, async (ps, me) => { - const announcement = await Announcements.findOne(ps.id); - - if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement); - - await Announcements.delete(announcement.id); -}); diff --git a/src/server/api/endpoints/admin/announcements/list.ts b/src/server/api/endpoints/admin/announcements/list.ts deleted file mode 100644 index 4039bcd88f..0000000000 --- a/src/server/api/endpoints/admin/announcements/list.ts +++ /dev/null @@ -1,84 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { Announcements, AnnouncementReads } from '@/models/index'; -import { makePaginationQuery } from '../../../common/make-pagination-query'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id', - example: 'xxxxxxxxxx', - }, - createdAt: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'date-time', - }, - updatedAt: { - type: 'string' as const, - optional: false as const, nullable: true as const, - format: 'date-time', - }, - text: { - type: 'string' as const, - optional: false as const, nullable: false as const, - }, - title: { - type: 'string' as const, - optional: false as const, nullable: false as const, - }, - imageUrl: { - type: 'string' as const, - optional: false as const, nullable: true as const, - }, - reads: { - type: 'number' as const, - optional: false as const, nullable: false as const, - } - } - } - } -}; - -export default define(meta, async (ps) => { - const query = makePaginationQuery(Announcements.createQueryBuilder('announcement'), ps.sinceId, ps.untilId); - - const announcements = await query.take(ps.limit!).getMany(); - - for (const announcement of announcements) { - (announcement as any).reads = await AnnouncementReads.count({ - announcementId: announcement.id - }); - } - - return announcements; -}); diff --git a/src/server/api/endpoints/admin/announcements/update.ts b/src/server/api/endpoints/admin/announcements/update.ts deleted file mode 100644 index 343f37d626..0000000000 --- a/src/server/api/endpoints/admin/announcements/update.ts +++ /dev/null @@ -1,48 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { ID } from '@/misc/cafy-id'; -import { Announcements } from '@/models/index'; -import { ApiError } from '../../../error'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - id: { - validator: $.type(ID) - }, - title: { - validator: $.str.min(1) - }, - text: { - validator: $.str.min(1) - }, - imageUrl: { - validator: $.nullable.str.min(1) - } - }, - - errors: { - noSuchAnnouncement: { - message: 'No such announcement.', - code: 'NO_SUCH_ANNOUNCEMENT', - id: 'd3aae5a7-6372-4cb4-b61c-f511ffc2d7cc' - } - } -}; - -export default define(meta, async (ps, me) => { - const announcement = await Announcements.findOne(ps.id); - - if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement); - - await Announcements.update(announcement.id, { - updatedAt: new Date(), - title: ps.title, - text: ps.text, - imageUrl: ps.imageUrl, - }); -}); diff --git a/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts b/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts deleted file mode 100644 index 988ab29558..0000000000 --- a/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts +++ /dev/null @@ -1,28 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { deleteFile } from '@/services/drive/delete-file'; -import { DriveFiles } from '@/models/index'; -import { ID } from '@/misc/cafy-id'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - userId: { - validator: $.type(ID), - }, - } -}; - -export default define(meta, async (ps, me) => { - const files = await DriveFiles.find({ - userId: ps.userId - }); - - for (const file of files) { - deleteFile(file); - } -}); diff --git a/src/server/api/endpoints/admin/delete-logs.ts b/src/server/api/endpoints/admin/delete-logs.ts deleted file mode 100644 index 9d37ceb434..0000000000 --- a/src/server/api/endpoints/admin/delete-logs.ts +++ /dev/null @@ -1,13 +0,0 @@ -import define from '../../define'; -import { Logs } from '@/models/index'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, -}; - -export default define(meta, async (ps) => { - await Logs.clear(); // TRUNCATE -}); diff --git a/src/server/api/endpoints/admin/drive/clean-remote-files.ts b/src/server/api/endpoints/admin/drive/clean-remote-files.ts deleted file mode 100644 index 76a6acff59..0000000000 --- a/src/server/api/endpoints/admin/drive/clean-remote-files.ts +++ /dev/null @@ -1,13 +0,0 @@ -import define from '../../../define'; -import { createCleanRemoteFilesJob } from '@/queue/index'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, -}; - -export default define(meta, async (ps, me) => { - createCleanRemoteFilesJob(); -}); diff --git a/src/server/api/endpoints/admin/drive/cleanup.ts b/src/server/api/endpoints/admin/drive/cleanup.ts deleted file mode 100644 index 8497478da9..0000000000 --- a/src/server/api/endpoints/admin/drive/cleanup.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { IsNull } from 'typeorm'; -import define from '../../../define'; -import { deleteFile } from '@/services/drive/delete-file'; -import { DriveFiles } from '@/models/index'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, -}; - -export default define(meta, async (ps, me) => { - const files = await DriveFiles.find({ - userId: IsNull() - }); - - for (const file of files) { - deleteFile(file); - } -}); diff --git a/src/server/api/endpoints/admin/drive/files.ts b/src/server/api/endpoints/admin/drive/files.ts deleted file mode 100644 index fe1c799805..0000000000 --- a/src/server/api/endpoints/admin/drive/files.ts +++ /dev/null @@ -1,81 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { DriveFiles } from '@/models/index'; -import { makePaginationQuery } from '../../../common/make-pagination-query'; -import { ID } from '@/misc/cafy-id'; - -export const meta = { - tags: ['admin'], - - requireCredential: false as const, - requireModerator: true, - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - type: { - validator: $.optional.nullable.str.match(/^[a-zA-Z0-9\/\-*]+$/) - }, - - origin: { - validator: $.optional.str.or([ - 'combined', - 'local', - 'remote', - ]), - default: 'local' - }, - - hostname: { - validator: $.optional.nullable.str, - default: null - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'DriveFile' - } - } -}; - -export default define(meta, async (ps, me) => { - const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId); - - if (ps.origin === 'local') { - query.andWhere('file.userHost IS NULL'); - } else if (ps.origin === 'remote') { - query.andWhere('file.userHost IS NOT NULL'); - } - - if (ps.hostname) { - query.andWhere('file.userHost = :hostname', { hostname: ps.hostname }); - } - - if (ps.type) { - if (ps.type.endsWith('/*')) { - query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' }); - } else { - query.andWhere('file.type = :type', { type: ps.type }); - } - } - - const files = await query.take(ps.limit!).getMany(); - - return await DriveFiles.packMany(files, { detail: true, withUser: true, self: true }); -}); diff --git a/src/server/api/endpoints/admin/drive/show-file.ts b/src/server/api/endpoints/admin/drive/show-file.ts deleted file mode 100644 index 270b89c4fa..0000000000 --- a/src/server/api/endpoints/admin/drive/show-file.ts +++ /dev/null @@ -1,180 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { DriveFiles } from '@/models/index'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - fileId: { - validator: $.optional.type(ID), - }, - - url: { - validator: $.optional.str, - }, - }, - - errors: { - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: 'caf3ca38-c6e5-472e-a30c-b05377dcc240' - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id', - example: 'xxxxxxxxxx', - }, - createdAt: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'date-time', - }, - userId: { - type: 'string' as const, - optional: false as const, nullable: true as const, - format: 'id', - example: 'xxxxxxxxxx', - }, - userHost: { - type: 'string' as const, - optional: false as const, nullable: true as const - }, - md5: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'md5', - example: '15eca7fba0480996e2245f5185bf39f2' - }, - name: { - type: 'string' as const, - optional: false as const, nullable: false as const, - example: 'lenna.jpg' - }, - type: { - type: 'string' as const, - optional: false as const, nullable: false as const, - example: 'image/jpeg' - }, - size: { - type: 'number' as const, - optional: false as const, nullable: false as const, - example: 51469 - }, - comment: { - type: 'string' as const, - optional: false as const, nullable: true as const - }, - blurhash: { - type: 'string' as const, - optional: false as const, nullable: true as const - }, - properties: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - width: { - type: 'number' as const, - optional: false as const, nullable: false as const, - example: 1280 - }, - height: { - type: 'number' as const, - optional: false as const, nullable: false as const, - example: 720 - }, - avgColor: { - type: 'string' as const, - optional: true as const, nullable: false as const, - example: 'rgb(40,65,87)' - } - } - }, - storedInternal: { - type: 'boolean' as const, - optional: false as const, nullable: true as const, - example: true - }, - url: { - type: 'string' as const, - optional: false as const, nullable: true as const, - format: 'url', - }, - thumbnailUrl: { - type: 'string' as const, - optional: false as const, nullable: true as const, - format: 'url', - }, - webpublicUrl: { - type: 'string' as const, - optional: false as const, nullable: true as const, - format: 'url', - }, - accessKey: { - type: 'string' as const, - optional: false as const, nullable: false as const, - }, - thumbnailAccessKey: { - type: 'string' as const, - optional: false as const, nullable: false as const, - }, - webpublicAccessKey: { - type: 'string' as const, - optional: false as const, nullable: false as const, - }, - uri: { - type: 'string' as const, - optional: false as const, nullable: true as const - }, - src: { - type: 'string' as const, - optional: false as const, nullable: true as const - }, - folderId: { - type: 'string' as const, - optional: false as const, nullable: true as const, - format: 'id', - example: 'xxxxxxxxxx', - }, - isSensitive: { - type: 'boolean' as const, - optional: false as const, nullable: false as const, - }, - isLink: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - } - } - } -}; - -export default define(meta, async (ps, me) => { - const file = ps.fileId ? await DriveFiles.findOne(ps.fileId) : await DriveFiles.findOne({ - where: [{ - url: ps.url - }, { - thumbnailUrl: ps.url - }, { - webpublicUrl: ps.url - }] - }); - - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } - - return file; -}); diff --git a/src/server/api/endpoints/admin/emoji/add.ts b/src/server/api/endpoints/admin/emoji/add.ts deleted file mode 100644 index 1af81fe46d..0000000000 --- a/src/server/api/endpoints/admin/emoji/add.ts +++ /dev/null @@ -1,64 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { Emojis, DriveFiles } from '@/models/index'; -import { genId } from '@/misc/gen-id'; -import { getConnection } from 'typeorm'; -import { insertModerationLog } from '@/services/insert-moderation-log'; -import { ApiError } from '../../../error'; -import { ID } from '@/misc/cafy-id'; -import rndstr from 'rndstr'; -import { publishBroadcastStream } from '@/services/stream'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - fileId: { - validator: $.type(ID) - }, - }, - - errors: { - noSuchFile: { - message: 'No such file.', - code: 'MO_SUCH_FILE', - id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf' - } - } -}; - -export default define(meta, async (ps, me) => { - const file = await DriveFiles.findOne(ps.fileId); - - if (file == null) throw new ApiError(meta.errors.noSuchFile); - - const name = file.name.split('.')[0].match(/^[a-z0-9_]+$/) ? file.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`; - - const emoji = await Emojis.save({ - id: genId(), - updatedAt: new Date(), - name: name, - category: null, - host: null, - aliases: [], - url: file.url, - type: file.type, - }); - - await getConnection().queryResultCache!.remove(['meta_emojis']); - - publishBroadcastStream('emojiAdded', { - emoji: await Emojis.pack(emoji.id) - }); - - insertModerationLog(me, 'addEmoji', { - emojiId: emoji.id - }); - - return { - id: emoji.id - }; -}); diff --git a/src/server/api/endpoints/admin/emoji/copy.ts b/src/server/api/endpoints/admin/emoji/copy.ts deleted file mode 100644 index 4c8ab99f7c..0000000000 --- a/src/server/api/endpoints/admin/emoji/copy.ts +++ /dev/null @@ -1,81 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { Emojis } from '@/models/index'; -import { genId } from '@/misc/gen-id'; -import { getConnection } from 'typeorm'; -import { ApiError } from '../../../error'; -import { DriveFile } from '@/models/entities/drive-file'; -import { ID } from '@/misc/cafy-id'; -import uploadFromUrl from '@/services/drive/upload-from-url'; -import { publishBroadcastStream } from '@/services/stream'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - emojiId: { - validator: $.type(ID) - }, - }, - - errors: { - noSuchEmoji: { - message: 'No such emoji.', - code: 'NO_SUCH_EMOJI', - id: 'e2785b66-dca3-4087-9cac-b93c541cc425' - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id', - } - } - } -}; - -export default define(meta, async (ps, me) => { - const emoji = await Emojis.findOne(ps.emojiId); - - if (emoji == null) { - throw new ApiError(meta.errors.noSuchEmoji); - } - - let driveFile: DriveFile; - - try { - // Create file - driveFile = await uploadFromUrl(emoji.url, null, null, null, false, true); - } catch (e) { - throw new ApiError(); - } - - const copied = await Emojis.insert({ - id: genId(), - updatedAt: new Date(), - name: emoji.name, - host: null, - aliases: [], - url: driveFile.url, - type: driveFile.type, - fileId: driveFile.id, - }).then(x => Emojis.findOneOrFail(x.identifiers[0])); - - await getConnection().queryResultCache!.remove(['meta_emojis']); - - publishBroadcastStream('emojiAdded', { - emoji: await Emojis.pack(copied.id) - }); - - return { - id: copied.id - }; -}); diff --git a/src/server/api/endpoints/admin/emoji/list-remote.ts b/src/server/api/endpoints/admin/emoji/list-remote.ts deleted file mode 100644 index 3c8ca22170..0000000000 --- a/src/server/api/endpoints/admin/emoji/list-remote.ts +++ /dev/null @@ -1,99 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { Emojis } from '@/models/index'; -import { toPuny } from '@/misc/convert-host'; -import { makePaginationQuery } from '../../../common/make-pagination-query'; -import { ID } from '@/misc/cafy-id'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - query: { - validator: $.optional.nullable.str, - default: null - }, - - host: { - validator: $.optional.nullable.str, - default: null - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - } - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id', - }, - aliases: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'string' as const, - optional: false as const, nullable: false as const - } - }, - name: { - type: 'string' as const, - optional: false as const, nullable: false as const, - }, - category: { - type: 'string' as const, - optional: false as const, nullable: true as const, - }, - host: { - type: 'string' as const, - optional: false as const, nullable: true as const, - }, - url: { - type: 'string' as const, - optional: false as const, nullable: false as const, - } - } - } - } -}; - -export default define(meta, async (ps) => { - const q = makePaginationQuery(Emojis.createQueryBuilder('emoji'), ps.sinceId, ps.untilId); - - if (ps.host == null) { - q.andWhere(`emoji.host IS NOT NULL`); - } else { - q.andWhere(`emoji.host = :host`, { host: toPuny(ps.host) }); - } - - if (ps.query) { - q.andWhere('emoji.name like :query', { query: '%' + ps.query + '%' }); - } - - const emojis = await q - .orderBy('emoji.id', 'DESC') - .take(ps.limit!) - .getMany(); - - return Emojis.packMany(emojis); -}); diff --git a/src/server/api/endpoints/admin/emoji/list.ts b/src/server/api/endpoints/admin/emoji/list.ts deleted file mode 100644 index cb1e79e0fe..0000000000 --- a/src/server/api/endpoints/admin/emoji/list.ts +++ /dev/null @@ -1,98 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { Emojis } from '@/models/index'; -import { makePaginationQuery } from '../../../common/make-pagination-query'; -import { ID } from '@/misc/cafy-id'; -import { Emoji } from '@/models/entities/emoji'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - query: { - validator: $.optional.nullable.str, - default: null - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - } - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id', - }, - aliases: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'string' as const, - optional: false as const, nullable: false as const - } - }, - name: { - type: 'string' as const, - optional: false as const, nullable: false as const, - }, - category: { - type: 'string' as const, - optional: false as const, nullable: true as const, - }, - host: { - type: 'string' as const, - optional: false as const, nullable: true as const, - }, - url: { - type: 'string' as const, - optional: false as const, nullable: false as const, - } - } - } - } -}; - -export default define(meta, async (ps) => { - const q = makePaginationQuery(Emojis.createQueryBuilder('emoji'), ps.sinceId, ps.untilId) - .andWhere(`emoji.host IS NULL`); - - let emojis: Emoji[]; - - if (ps.query) { - //q.andWhere('emoji.name ILIKE :q', { q: `%${ps.query}%` }); - //const emojis = await q.take(ps.limit!).getMany(); - - emojis = await q.getMany(); - - emojis = emojis.filter(emoji => - emoji.name.includes(ps.query!) || - emoji.aliases.some(a => a.includes(ps.query!)) || - emoji.category?.includes(ps.query!)); - - emojis.splice(ps.limit! + 1); - } else { - emojis = await q.take(ps.limit!).getMany(); - } - - return Emojis.packMany(emojis); -}); diff --git a/src/server/api/endpoints/admin/emoji/remove.ts b/src/server/api/endpoints/admin/emoji/remove.ts deleted file mode 100644 index 259950e362..0000000000 --- a/src/server/api/endpoints/admin/emoji/remove.ts +++ /dev/null @@ -1,42 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { ID } from '@/misc/cafy-id'; -import { Emojis } from '@/models/index'; -import { getConnection } from 'typeorm'; -import { insertModerationLog } from '@/services/insert-moderation-log'; -import { ApiError } from '../../../error'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - id: { - validator: $.type(ID) - } - }, - - errors: { - noSuchEmoji: { - message: 'No such emoji.', - code: 'NO_SUCH_EMOJI', - id: 'be83669b-773a-44b7-b1f8-e5e5170ac3c2' - } - } -}; - -export default define(meta, async (ps, me) => { - const emoji = await Emojis.findOne(ps.id); - - if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji); - - await Emojis.delete(emoji.id); - - await getConnection().queryResultCache!.remove(['meta_emojis']); - - insertModerationLog(me, 'removeEmoji', { - emoji: emoji - }); -}); diff --git a/src/server/api/endpoints/admin/emoji/update.ts b/src/server/api/endpoints/admin/emoji/update.ts deleted file mode 100644 index 3fd547d7e5..0000000000 --- a/src/server/api/endpoints/admin/emoji/update.ts +++ /dev/null @@ -1,54 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { ID } from '@/misc/cafy-id'; -import { Emojis } from '@/models/index'; -import { getConnection } from 'typeorm'; -import { ApiError } from '../../../error'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - id: { - validator: $.type(ID) - }, - - name: { - validator: $.str - }, - - category: { - validator: $.optional.nullable.str - }, - - aliases: { - validator: $.arr($.str) - } - }, - - errors: { - noSuchEmoji: { - message: 'No such emoji.', - code: 'NO_SUCH_EMOJI', - id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8' - } - } -}; - -export default define(meta, async (ps) => { - const emoji = await Emojis.findOne(ps.id); - - if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji); - - await Emojis.update(emoji.id, { - updatedAt: new Date(), - name: ps.name, - category: ps.category, - aliases: ps.aliases, - }); - - await getConnection().queryResultCache!.remove(['meta_emojis']); -}); diff --git a/src/server/api/endpoints/admin/federation/delete-all-files.ts b/src/server/api/endpoints/admin/federation/delete-all-files.ts deleted file mode 100644 index 82540c5447..0000000000 --- a/src/server/api/endpoints/admin/federation/delete-all-files.ts +++ /dev/null @@ -1,27 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { deleteFile } from '@/services/drive/delete-file'; -import { DriveFiles } from '@/models/index'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - host: { - validator: $.str - } - } -}; - -export default define(meta, async (ps, me) => { - const files = await DriveFiles.find({ - userHost: ps.host - }); - - for (const file of files) { - deleteFile(file); - } -}); diff --git a/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts b/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts deleted file mode 100644 index 65a6947ba0..0000000000 --- a/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts +++ /dev/null @@ -1,28 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { Instances } from '@/models/index'; -import { toPuny } from '@/misc/convert-host'; -import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - host: { - validator: $.str - }, - } -}; - -export default define(meta, async (ps, me) => { - const instance = await Instances.findOne({ host: toPuny(ps.host) }); - - if (instance == null) { - throw new Error('instance not found'); - } - - fetchInstanceMetadata(instance, true); -}); diff --git a/src/server/api/endpoints/admin/federation/remove-all-following.ts b/src/server/api/endpoints/admin/federation/remove-all-following.ts deleted file mode 100644 index 7935eaa631..0000000000 --- a/src/server/api/endpoints/admin/federation/remove-all-following.ts +++ /dev/null @@ -1,32 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import deleteFollowing from '@/services/following/delete'; -import { Followings, Users } from '@/models/index'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - host: { - validator: $.str - } - } -}; - -export default define(meta, async (ps, me) => { - const followings = await Followings.find({ - followerHost: ps.host - }); - - const pairs = await Promise.all(followings.map(f => Promise.all([ - Users.findOneOrFail(f.followerId), - Users.findOneOrFail(f.followeeId) - ]))); - - for (const pair of pairs) { - deleteFollowing(pair[0], pair[1]); - } -}); diff --git a/src/server/api/endpoints/admin/federation/update-instance.ts b/src/server/api/endpoints/admin/federation/update-instance.ts deleted file mode 100644 index 34eab27c78..0000000000 --- a/src/server/api/endpoints/admin/federation/update-instance.ts +++ /dev/null @@ -1,33 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { Instances } from '@/models/index'; -import { toPuny } from '@/misc/convert-host'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - host: { - validator: $.str - }, - - isSuspended: { - validator: $.bool - }, - } -}; - -export default define(meta, async (ps, me) => { - const instance = await Instances.findOne({ host: toPuny(ps.host) }); - - if (instance == null) { - throw new Error('instance not found'); - } - - Instances.update({ host: toPuny(ps.host) }, { - isSuspended: ps.isSuspended - }); -}); diff --git a/src/server/api/endpoints/admin/get-index-stats.ts b/src/server/api/endpoints/admin/get-index-stats.ts deleted file mode 100644 index f2b06d0ef2..0000000000 --- a/src/server/api/endpoints/admin/get-index-stats.ts +++ /dev/null @@ -1,26 +0,0 @@ -import define from '../../define'; -import { getConnection } from 'typeorm'; - -export const meta = { - requireCredential: true as const, - requireModerator: true, - - tags: ['admin'], - - params: { - }, -}; - -export default define(meta, async () => { - const stats = await - getConnection().query(`SELECT * FROM pg_indexes;`) - .then(recs => { - const res = [] as { tablename: string; indexname: string; }[]; - for (const rec of recs) { - res.push(rec); - } - return res; - }); - - return stats; -}); diff --git a/src/server/api/endpoints/admin/get-table-stats.ts b/src/server/api/endpoints/admin/get-table-stats.ts deleted file mode 100644 index bce813232b..0000000000 --- a/src/server/api/endpoints/admin/get-table-stats.ts +++ /dev/null @@ -1,45 +0,0 @@ -import define from '../../define'; -import { getConnection } from 'typeorm'; - -export const meta = { - requireCredential: true as const, - requireModerator: true, - - tags: ['admin'], - - params: { - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - example: { - migrations: { - count: 66, - size: 32768 - }, - } - } -}; - -export default define(meta, async () => { - const sizes = await - getConnection().query(` - SELECT relname AS "table", reltuples as "count", pg_total_relation_size(C.oid) AS "size" - FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) - WHERE nspname NOT IN ('pg_catalog', 'information_schema') - AND C.relkind <> 'i' - AND nspname !~ '^pg_toast';`) - .then(recs => { - const res = {} as Record<string, { count: number; size: number; }>; - for (const rec of recs) { - res[rec.table] = { - count: parseInt(rec.count, 10), - size: parseInt(rec.size, 10), - }; - } - return res; - }); - - return sizes; -}); diff --git a/src/server/api/endpoints/admin/invite.ts b/src/server/api/endpoints/admin/invite.ts deleted file mode 100644 index 2c69eec535..0000000000 --- a/src/server/api/endpoints/admin/invite.ts +++ /dev/null @@ -1,44 +0,0 @@ -import rndstr from 'rndstr'; -import define from '../../define'; -import { RegistrationTickets } from '@/models/index'; -import { genId } from '@/misc/gen-id'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: {}, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - code: { - type: 'string' as const, - optional: false as const, nullable: false as const, - example: '2ERUA5VR', - maxLength: 8, - minLength: 8 - } - } - } -}; - -export default define(meta, async () => { - const code = rndstr({ - length: 8, - chars: '2-9A-HJ-NP-Z', // [0-9A-Z] w/o [01IO] (32 patterns) - }); - - await RegistrationTickets.insert({ - id: genId(), - createdAt: new Date(), - code, - }); - - return { - code, - }; -}); diff --git a/src/server/api/endpoints/admin/moderators/add.ts b/src/server/api/endpoints/admin/moderators/add.ts deleted file mode 100644 index 2b87fc217f..0000000000 --- a/src/server/api/endpoints/admin/moderators/add.ts +++ /dev/null @@ -1,33 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { Users } from '@/models/index'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireAdmin: true, - - params: { - userId: { - validator: $.type(ID), - }, - } -}; - -export default define(meta, async (ps) => { - const user = await Users.findOne(ps.userId as string); - - if (user == null) { - throw new Error('user not found'); - } - - if (user.isAdmin) { - throw new Error('cannot mark as moderator if admin user'); - } - - await Users.update(user.id, { - isModerator: true - }); -}); diff --git a/src/server/api/endpoints/admin/moderators/remove.ts b/src/server/api/endpoints/admin/moderators/remove.ts deleted file mode 100644 index cbb0625224..0000000000 --- a/src/server/api/endpoints/admin/moderators/remove.ts +++ /dev/null @@ -1,29 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { Users } from '@/models/index'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireAdmin: true, - - params: { - userId: { - validator: $.type(ID), - }, - } -}; - -export default define(meta, async (ps) => { - const user = await Users.findOne(ps.userId as string); - - if (user == null) { - throw new Error('user not found'); - } - - await Users.update(user.id, { - isModerator: false - }); -}); diff --git a/src/server/api/endpoints/admin/promo/create.ts b/src/server/api/endpoints/admin/promo/create.ts deleted file mode 100644 index 3bdaaad4d9..0000000000 --- a/src/server/api/endpoints/admin/promo/create.ts +++ /dev/null @@ -1,57 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { getNote } from '../../../common/getters'; -import { PromoNotes } from '@/models/index'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - noteId: { - validator: $.type(ID), - }, - - expiresAt: { - validator: $.num.int() - }, - }, - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: 'ee449fbe-af2a-453b-9cae-cf2fe7c895fc' - }, - - alreadyPromoted: { - message: 'The note has already promoted.', - code: 'ALREADY_PROMOTED', - id: 'ae427aa2-7a41-484f-a18c-2c1104051604' - }, - } -}; - -export default define(meta, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - const exist = await PromoNotes.findOne(note.id); - - if (exist != null) { - throw new ApiError(meta.errors.alreadyPromoted); - } - - await PromoNotes.insert({ - noteId: note.id, - createdAt: new Date(), - expiresAt: new Date(ps.expiresAt), - userId: note.userId, - }); -}); diff --git a/src/server/api/endpoints/admin/queue/clear.ts b/src/server/api/endpoints/admin/queue/clear.ts deleted file mode 100644 index fedb7065ab..0000000000 --- a/src/server/api/endpoints/admin/queue/clear.ts +++ /dev/null @@ -1,18 +0,0 @@ -import define from '../../../define'; -import { destroy } from '@/queue/index'; -import { insertModerationLog } from '@/services/insert-moderation-log'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: {} -}; - -export default define(meta, async (ps, me) => { - destroy(); - - insertModerationLog(me, 'clearQueue'); -}); diff --git a/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/src/server/api/endpoints/admin/queue/deliver-delayed.ts deleted file mode 100644 index cd7b640983..0000000000 --- a/src/server/api/endpoints/admin/queue/deliver-delayed.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { deliverQueue } from '@/queue/queues'; -import { URL } from 'url'; -import define from '../../../define'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - anyOf: [ - { - type: 'string' as const, - }, - { - type: 'number' as const, - } - ] - } - }, - example: [[ - 'example.com', - 12 - ]] - } -}; - -export default define(meta, async (ps) => { - const jobs = await deliverQueue.getJobs(['delayed']); - - const res = [] as [string, number][]; - - for (const job of jobs) { - const host = new URL(job.data.to).host; - if (res.find(x => x[0] === host)) { - res.find(x => x[0] === host)![1]++; - } else { - res.push([host, 1]); - } - } - - res.sort((a, b) => b[1] - a[1]); - - return res; -}); diff --git a/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/src/server/api/endpoints/admin/queue/inbox-delayed.ts deleted file mode 100644 index 1925906c28..0000000000 --- a/src/server/api/endpoints/admin/queue/inbox-delayed.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { URL } from 'url'; -import define from '../../../define'; -import { inboxQueue } from '@/queue/queues'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - anyOf: [ - { - type: 'string' as const, - }, - { - type: 'number' as const, - } - ] - } - }, - example: [[ - 'example.com', - 12 - ]] - } -}; - -export default define(meta, async (ps) => { - const jobs = await inboxQueue.getJobs(['delayed']); - - const res = [] as [string, number][]; - - for (const job of jobs) { - const host = new URL(job.data.signature.keyId).host; - if (res.find(x => x[0] === host)) { - res.find(x => x[0] === host)![1]++; - } else { - res.push([host, 1]); - } - } - - res.sort((a, b) => b[1] - a[1]); - - return res; -}); diff --git a/src/server/api/endpoints/admin/queue/jobs.ts b/src/server/api/endpoints/admin/queue/jobs.ts deleted file mode 100644 index c426e5f39b..0000000000 --- a/src/server/api/endpoints/admin/queue/jobs.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { deliverQueue, inboxQueue, dbQueue, objectStorageQueue } from '@/queue/queues'; -import $ from 'cafy'; -import define from '../../../define'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - domain: { - validator: $.str.or(['deliver', 'inbox', 'db', 'objectStorage']), - }, - - state: { - validator: $.str.or(['active', 'waiting', 'delayed']), - }, - - limit: { - validator: $.optional.num, - default: 50 - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id' - }, - data: { - type: 'object' as const, - optional: false as const, nullable: false as const - }, - attempts: { - type: 'number' as const, - optional: false as const, nullable: false as const - }, - maxAttempts: { - type: 'number' as const, - optional: false as const, nullable: false as const - }, - timestamp: { - type: 'number' as const, - optional: false as const, nullable: false as const - } - } - } - } -}; - -export default define(meta, async (ps) => { - const queue = - ps.domain === 'deliver' ? deliverQueue : - ps.domain === 'inbox' ? inboxQueue : - ps.domain === 'db' ? dbQueue : - ps.domain === 'objectStorage' ? objectStorageQueue : - null as never; - - const jobs = await queue.getJobs([ps.state], 0, ps.limit!); - - return jobs.map(job => { - const data = job.data; - delete data.content; - delete data.user; - return { - id: job.id, - data, - attempts: job.attemptsMade, - maxAttempts: job.opts ? job.opts.attempts : 0, - timestamp: job.timestamp, - }; - }); -}); diff --git a/src/server/api/endpoints/admin/queue/stats.ts b/src/server/api/endpoints/admin/queue/stats.ts deleted file mode 100644 index 38f18459dd..0000000000 --- a/src/server/api/endpoints/admin/queue/stats.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { deliverQueue, inboxQueue, dbQueue, objectStorageQueue } from '@/queue/queues'; -import define from '../../../define'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: {}, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - deliver: { - ref: 'QueueCount' - }, - inbox: { - ref: 'QueueCount' - }, - db: { - ref: 'QueueCount' - }, - objectStorage: { - ref: 'QueueCount' - } - } - } -}; - -export default define(meta, async (ps) => { - const deliverJobCounts = await deliverQueue.getJobCounts(); - const inboxJobCounts = await inboxQueue.getJobCounts(); - const dbJobCounts = await dbQueue.getJobCounts(); - const objectStorageJobCounts = await objectStorageQueue.getJobCounts(); - - return { - deliver: deliverJobCounts, - inbox: inboxJobCounts, - db: dbJobCounts, - objectStorage: objectStorageJobCounts, - }; -}); diff --git a/src/server/api/endpoints/admin/relays/add.ts b/src/server/api/endpoints/admin/relays/add.ts deleted file mode 100644 index 567035fd3a..0000000000 --- a/src/server/api/endpoints/admin/relays/add.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { URL } from 'url'; -import $ from 'cafy'; -import define from '../../../define'; -import { addRelay } from '@/services/relay'; -import { ApiError } from '../../../error'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true as const, - - params: { - inbox: { - validator: $.str - }, - }, - - errors: { - invalidUrl: { - message: 'Invalid URL', - code: 'INVALID_URL', - id: 'fb8c92d3-d4e5-44e7-b3d4-800d5cef8b2c' - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id' - }, - inbox: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'url' - }, - status: { - type: 'string' as const, - optional: false as const, nullable: false as const, - default: 'requesting', - enum: [ - 'requesting', - 'accepted', - 'rejected' - ] - } - } - } -}; - -export default define(meta, async (ps, user) => { - try { - if (new URL(ps.inbox).protocol !== 'https:') throw 'https only'; - } catch { - throw new ApiError(meta.errors.invalidUrl); - } - - return await addRelay(ps.inbox); -}); diff --git a/src/server/api/endpoints/admin/relays/list.ts b/src/server/api/endpoints/admin/relays/list.ts deleted file mode 100644 index 031ebe85d0..0000000000 --- a/src/server/api/endpoints/admin/relays/list.ts +++ /dev/null @@ -1,47 +0,0 @@ -import define from '../../../define'; -import { listRelay } from '@/services/relay'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true as const, - - params: { - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id' - }, - inbox: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'url' - }, - status: { - type: 'string' as const, - optional: false as const, nullable: false as const, - default: 'requesting', - enum: [ - 'requesting', - 'accepted', - 'rejected' - ] - } - } - } - } -}; - -export default define(meta, async (ps, user) => { - return await listRelay(); -}); diff --git a/src/server/api/endpoints/admin/relays/remove.ts b/src/server/api/endpoints/admin/relays/remove.ts deleted file mode 100644 index c1c50f5dc0..0000000000 --- a/src/server/api/endpoints/admin/relays/remove.ts +++ /dev/null @@ -1,20 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { removeRelay } from '@/services/relay'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true as const, - - params: { - inbox: { - validator: $.str - }, - }, -}; - -export default define(meta, async (ps, user) => { - return await removeRelay(ps.inbox); -}); diff --git a/src/server/api/endpoints/admin/reset-password.ts b/src/server/api/endpoints/admin/reset-password.ts deleted file mode 100644 index 0fc2c6a868..0000000000 --- a/src/server/api/endpoints/admin/reset-password.ts +++ /dev/null @@ -1,59 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import * as bcrypt from 'bcryptjs'; -import rndstr from 'rndstr'; -import { Users, UserProfiles } from '@/models/index'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - userId: { - validator: $.type(ID), - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - password: { - type: 'string' as const, - optional: false as const, nullable: false as const, - minLength: 8, - maxLength: 8 - } - } - } -}; - -export default define(meta, async (ps) => { - const user = await Users.findOne(ps.userId as string); - - if (user == null) { - throw new Error('user not found'); - } - - if (user.isAdmin) { - throw new Error('cannot reset password of admin'); - } - - const passwd = rndstr('a-zA-Z0-9', 8); - - // Generate hash of password - const hash = bcrypt.hashSync(passwd); - - await UserProfiles.update({ - userId: user.id - }, { - password: hash - }); - - return { - password: passwd - }; -}); diff --git a/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/src/server/api/endpoints/admin/resolve-abuse-user-report.ts deleted file mode 100644 index 7b71f8e000..0000000000 --- a/src/server/api/endpoints/admin/resolve-abuse-user-report.ts +++ /dev/null @@ -1,30 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { AbuseUserReports } from '@/models/index'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - reportId: { - validator: $.type(ID), - }, - } -}; - -export default define(meta, async (ps, me) => { - const report = await AbuseUserReports.findOne(ps.reportId); - - if (report == null) { - throw new Error('report not found'); - } - - await AbuseUserReports.update(report.id, { - resolved: true, - assigneeId: me.id, - }); -}); diff --git a/src/server/api/endpoints/admin/resync-chart.ts b/src/server/api/endpoints/admin/resync-chart.ts deleted file mode 100644 index e01dfce1b6..0000000000 --- a/src/server/api/endpoints/admin/resync-chart.ts +++ /dev/null @@ -1,21 +0,0 @@ -import define from '../../define'; -import { driveChart, notesChart, usersChart } from '@/services/chart/index'; -import { insertModerationLog } from '@/services/insert-moderation-log'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, -}; - -export default define(meta, async (ps, me) => { - insertModerationLog(me, 'chartResync'); - - driveChart.resync(); - notesChart.resync(); - usersChart.resync(); - - // TODO: ユーザーごとのチャートもキューに入れて更新する - // TODO: インスタンスごとのチャートもキューに入れて更新する -}); diff --git a/src/server/api/endpoints/admin/send-email.ts b/src/server/api/endpoints/admin/send-email.ts deleted file mode 100644 index 6f67b78542..0000000000 --- a/src/server/api/endpoints/admin/send-email.ts +++ /dev/null @@ -1,26 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { sendEmail } from '@/services/send-email'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - to: { - validator: $.str, - }, - subject: { - validator: $.str, - }, - text: { - validator: $.str, - }, - } -}; - -export default define(meta, async (ps) => { - await sendEmail(ps.to, ps.subject, ps.text, ps.text); -}); diff --git a/src/server/api/endpoints/admin/server-info.ts b/src/server/api/endpoints/admin/server-info.ts deleted file mode 100644 index bb2d35e397..0000000000 --- a/src/server/api/endpoints/admin/server-info.ts +++ /dev/null @@ -1,119 +0,0 @@ -import * as os from 'os'; -import * as si from 'systeminformation'; -import { getConnection } from 'typeorm'; -import define from '../../define'; -import { redisClient } from '../../../../db/redis'; - -export const meta = { - requireCredential: true as const, - requireModerator: true, - - tags: ['admin', 'meta'], - - params: { - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - machine: { - type: 'string' as const, - optional: false as const, nullable: false as const, - }, - os: { - type: 'string' as const, - optional: false as const, nullable: false as const, - example: 'linux' - }, - node: { - type: 'string' as const, - optional: false as const, nullable: false as const, - }, - psql: { - type: 'string' as const, - optional: false as const, nullable: false as const, - }, - cpu: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - model: { - type: 'string' as const, - optional: false as const, nullable: false as const, - }, - cores: { - type: 'number' as const, - optional: false as const, nullable: false as const, - } - } - }, - mem: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - total: { - type: 'number' as const, - optional: false as const, nullable: false as const, - format: 'bytes', - } - } - }, - fs: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - total: { - type: 'number' as const, - optional: false as const, nullable: false as const, - format: 'bytes', - }, - used: { - type: 'number' as const, - optional: false as const, nullable: false as const, - format: 'bytes', - } - } - }, - net: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - interface: { - type: 'string' as const, - optional: false as const, nullable: false as const, - example: 'eth0' - } - } - } - } - } -}; - -export default define(meta, async () => { - const memStats = await si.mem(); - const fsStats = await si.fsSize(); - const netInterface = await si.networkInterfaceDefault(); - - return { - machine: os.hostname(), - os: os.platform(), - node: process.version, - psql: await getConnection().query('SHOW server_version').then(x => x[0].server_version), - redis: redisClient.server_info.redis_version, - cpu: { - model: os.cpus()[0].model, - cores: os.cpus().length - }, - mem: { - total: memStats.total - }, - fs: { - total: fsStats[0].size, - used: fsStats[0].used, - }, - net: { - interface: netInterface - } - }; -}); diff --git a/src/server/api/endpoints/admin/show-moderation-logs.ts b/src/server/api/endpoints/admin/show-moderation-logs.ts deleted file mode 100644 index e9509568d0..0000000000 --- a/src/server/api/endpoints/admin/show-moderation-logs.ts +++ /dev/null @@ -1,74 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ModerationLogs } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id' - }, - createdAt: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'date-time' - }, - type: { - type: 'string' as const, - optional: false as const, nullable: false as const - }, - info: { - type: 'object' as const, - optional: false as const, nullable: false as const - }, - userId: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id' - }, - user: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User' - } - } - } - } -}; - -export default define(meta, async (ps) => { - const query = makePaginationQuery(ModerationLogs.createQueryBuilder('report'), ps.sinceId, ps.untilId); - - const reports = await query.take(ps.limit!).getMany(); - - return await ModerationLogs.packMany(reports); -}); diff --git a/src/server/api/endpoints/admin/show-user.ts b/src/server/api/endpoints/admin/show-user.ts deleted file mode 100644 index 963c123255..0000000000 --- a/src/server/api/endpoints/admin/show-user.ts +++ /dev/null @@ -1,177 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { Users } from '@/models/index'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - userId: { - validator: $.type(ID), - }, - }, - - res: { - type: 'object' as const, - nullable: false as const, optional: false as const, - properties: { - id: { - type: 'string' as const, - nullable: false as const, optional: false as const, - format: 'id' - }, - createdAt: { - type: 'string' as const, - nullable: false as const, optional: false as const, - format: 'date-time' - }, - updatedAt: { - type: 'string' as const, - nullable: true as const, optional: false as const, - format: 'date-time' - }, - lastFetchedAt: { - type: 'string' as const, - nullable: true as const, optional: false as const - }, - username: { - type: 'string' as const, - nullable: false as const, optional: false as const - }, - name: { - type: 'string' as const, - nullable: false as const, optional: false as const - }, - folowersCount: { - type: 'number' as const, - nullable: false as const, optional: false as const - }, - followingCount: { - type: 'number' as const, - nullable: false as const, optional: false as const - }, - notesCount: { - type: 'number' as const, - nullable: false as const, optional: false as const - }, - avatarId: { - type: 'string' as const, - nullable: true as const, optional: false as const - }, - bannerId: { - type: 'string' as const, - nullable: true as const, optional: false as const - }, - tags: { - type: 'array' as const, - nullable: false as const, optional: false as const, - items: { - type: 'string' as const, - nullable: false as const, optional: false as const - } - }, - avatarUrl: { - type: 'string' as const, - nullable: true as const, optional: false as const, - format: 'url' - }, - bannerUrl: { - type: 'string' as const, - nullable: true as const, optional: false as const, - format: 'url' - }, - avatarBlurhash: { - type: 'any' as const, - nullable: true as const, optional: false as const, - default: null - }, - bannerBlurhash: { - type: 'any' as const, - nullable: true as const, optional: false as const, - default: null - }, - isSuspended: { - type: 'boolean' as const, - nullable: false as const, optional: false as const - }, - isSilenced: { - type: 'boolean' as const, - nullable: false as const, optional: false as const - }, - isLocked: { - type: 'boolean' as const, - nullable: false as const, optional: false as const, - }, - isBot: { - type: 'boolean' as const, - nullable: false as const, optional: false as const - }, - isCat: { - type: 'boolean' as const, - nullable: false as const, optional: false as const - }, - isAdmin: { - type: 'boolean' as const, - nullable: false as const, optional: false as const - }, - isModerator: { - type: 'boolean' as const, - nullable: false as const, optional: false as const - }, - emojis: { - type: 'array' as const, - nullable: false as const, optional: false as const, - items: { - type: 'string' as const, - nullable: false as const, optional: false as const - } - }, - host: { - type: 'string' as const, - nullable: true as const, optional: false as const - }, - inbox: { - type: 'string' as const, - nullable: true as const, optional: false as const - }, - sharedInbox: { - type: 'string' as const, - nullable: true as const, optional: false as const - }, - featured: { - type: 'string' as const, - nullable: true as const, optional: false as const - }, - uri: { - type: 'string' as const, - nullable: true as const, optional: false as const - }, - token: { - type: 'string' as const, - nullable: false as const, optional: false as const, - default: '<MASKED>' - } - } - } -}; - -export default define(meta, async (ps, me) => { - const user = await Users.findOne(ps.userId as string); - - if (user == null) { - throw new Error('user not found'); - } - - if ((me.isModerator && !me.isAdmin) && user.isAdmin) { - throw new Error('cannot show info of admin'); - } - - return { - ...user, - token: user.token != null ? '<MASKED>' : user.token, - }; -}); diff --git a/src/server/api/endpoints/admin/show-users.ts b/src/server/api/endpoints/admin/show-users.ts deleted file mode 100644 index 20b63e7be6..0000000000 --- a/src/server/api/endpoints/admin/show-users.ts +++ /dev/null @@ -1,119 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { Users } from '@/models/index'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - offset: { - validator: $.optional.num.min(0), - default: 0 - }, - - sort: { - validator: $.optional.str.or([ - '+follower', - '-follower', - '+createdAt', - '-createdAt', - '+updatedAt', - '-updatedAt', - ]), - }, - - state: { - validator: $.optional.str.or([ - 'all', - 'available', - 'admin', - 'moderator', - 'adminOrModerator', - 'silenced', - 'suspended', - ]), - default: 'all' - }, - - origin: { - validator: $.optional.str.or([ - 'combined', - 'local', - 'remote', - ]), - default: 'local' - }, - - username: { - validator: $.optional.str, - default: null - }, - - hostname: { - validator: $.optional.str, - default: null - } - }, - - res: { - type: 'array' as const, - nullable: false as const, optional: false as const, - items: { - type: 'object' as const, - nullable: false as const, optional: false as const, - ref: 'User' - } - } -}; - -export default define(meta, async (ps, me) => { - const query = Users.createQueryBuilder('user'); - - switch (ps.state) { - case 'available': query.where('user.isSuspended = FALSE'); break; - case 'admin': query.where('user.isAdmin = TRUE'); break; - case 'moderator': query.where('user.isModerator = TRUE'); break; - case 'adminOrModerator': query.where('user.isAdmin = TRUE OR user.isModerator = TRUE'); break; - case 'alive': query.where('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break; - case 'silenced': query.where('user.isSilenced = TRUE'); break; - case 'suspended': query.where('user.isSuspended = TRUE'); break; - } - - switch (ps.origin) { - case 'local': query.andWhere('user.host IS NULL'); break; - case 'remote': query.andWhere('user.host IS NOT NULL'); break; - } - - if (ps.username) { - query.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' }); - } - - if (ps.hostname) { - query.andWhere('user.host like :hostname', { hostname: '%' + ps.hostname.toLowerCase() + '%' }); - } - - switch (ps.sort) { - case '+follower': query.orderBy('user.followersCount', 'DESC'); break; - case '-follower': query.orderBy('user.followersCount', 'ASC'); break; - case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break; - case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break; - case '+updatedAt': query.orderBy('user.updatedAt', 'DESC', 'NULLS LAST'); break; - case '-updatedAt': query.orderBy('user.updatedAt', 'ASC', 'NULLS FIRST'); break; - default: query.orderBy('user.id', 'ASC'); break; - } - - query.take(ps.limit!); - query.skip(ps.offset); - - const users = await query.getMany(); - - return await Users.packMany(users, me, { detail: true }); -}); diff --git a/src/server/api/endpoints/admin/silence-user.ts b/src/server/api/endpoints/admin/silence-user.ts deleted file mode 100644 index 9bfed2310a..0000000000 --- a/src/server/api/endpoints/admin/silence-user.ts +++ /dev/null @@ -1,38 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { Users } from '@/models/index'; -import { insertModerationLog } from '@/services/insert-moderation-log'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - userId: { - validator: $.type(ID), - }, - } -}; - -export default define(meta, async (ps, me) => { - const user = await Users.findOne(ps.userId as string); - - if (user == null) { - throw new Error('user not found'); - } - - if (user.isAdmin) { - throw new Error('cannot silence admin'); - } - - await Users.update(user.id, { - isSilenced: true - }); - - insertModerationLog(me, 'silence', { - targetId: user.id, - }); -}); diff --git a/src/server/api/endpoints/admin/suspend-user.ts b/src/server/api/endpoints/admin/suspend-user.ts deleted file mode 100644 index 364f258ce8..0000000000 --- a/src/server/api/endpoints/admin/suspend-user.ts +++ /dev/null @@ -1,84 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import deleteFollowing from '@/services/following/delete'; -import { Users, Followings, Notifications } from '@/models/index'; -import { User } from '@/models/entities/user'; -import { insertModerationLog } from '@/services/insert-moderation-log'; -import { doPostSuspend } from '@/services/suspend-user'; -import { publishUserEvent } from '@/services/stream'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - userId: { - validator: $.type(ID), - }, - } -}; - -export default define(meta, async (ps, me) => { - const user = await Users.findOne(ps.userId as string); - - if (user == null) { - throw new Error('user not found'); - } - - if (user.isAdmin) { - throw new Error('cannot suspend admin'); - } - - if (user.isModerator) { - throw new Error('cannot suspend moderator'); - } - - await Users.update(user.id, { - isSuspended: true - }); - - insertModerationLog(me, 'suspend', { - targetId: user.id, - }); - - // Terminate streaming - if (Users.isLocalUser(user)) { - publishUserEvent(user.id, 'terminate', {}); - } - - (async () => { - await doPostSuspend(user).catch(e => {}); - await unFollowAll(user).catch(e => {}); - await readAllNotify(user).catch(e => {}); - })(); -}); - -async function unFollowAll(follower: User) { - const followings = await Followings.find({ - followerId: follower.id - }); - - for (const following of followings) { - const followee = await Users.findOne({ - id: following.followeeId - }); - - if (followee == null) { - throw `Cant find followee ${following.followeeId}`; - } - - await deleteFollowing(follower, followee, true); - } -} - -async function readAllNotify(notifier: User) { - await Notifications.update({ - notifierId: notifier.id, - isRead: false, - }, { - isRead: true - }); -} diff --git a/src/server/api/endpoints/admin/unsilence-user.ts b/src/server/api/endpoints/admin/unsilence-user.ts deleted file mode 100644 index 9994fbf462..0000000000 --- a/src/server/api/endpoints/admin/unsilence-user.ts +++ /dev/null @@ -1,34 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { Users } from '@/models/index'; -import { insertModerationLog } from '@/services/insert-moderation-log'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - userId: { - validator: $.type(ID), - }, - } -}; - -export default define(meta, async (ps, me) => { - const user = await Users.findOne(ps.userId as string); - - if (user == null) { - throw new Error('user not found'); - } - - await Users.update(user.id, { - isSilenced: false - }); - - insertModerationLog(me, 'unsilence', { - targetId: user.id, - }); -}); diff --git a/src/server/api/endpoints/admin/unsuspend-user.ts b/src/server/api/endpoints/admin/unsuspend-user.ts deleted file mode 100644 index ab4c2d3dfe..0000000000 --- a/src/server/api/endpoints/admin/unsuspend-user.ts +++ /dev/null @@ -1,37 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { Users } from '@/models/index'; -import { insertModerationLog } from '@/services/insert-moderation-log'; -import { doPostUnsuspend } from '@/services/unsuspend-user'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - userId: { - validator: $.type(ID), - }, - } -}; - -export default define(meta, async (ps, me) => { - const user = await Users.findOne(ps.userId as string); - - if (user == null) { - throw new Error('user not found'); - } - - await Users.update(user.id, { - isSuspended: false - }); - - insertModerationLog(me, 'unsuspend', { - targetId: user.id, - }); - - doPostUnsuspend(user); -}); diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts deleted file mode 100644 index 55447098dc..0000000000 --- a/src/server/api/endpoints/admin/update-meta.ts +++ /dev/null @@ -1,608 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { getConnection } from 'typeorm'; -import { Meta } from '@/models/entities/meta'; -import { insertModerationLog } from '@/services/insert-moderation-log'; -import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits'; -import { ID } from '@/misc/cafy-id'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireAdmin: true, - - params: { - disableRegistration: { - validator: $.optional.nullable.bool, - }, - - disableLocalTimeline: { - validator: $.optional.nullable.bool, - }, - - disableGlobalTimeline: { - validator: $.optional.nullable.bool, - }, - - useStarForReactionFallback: { - validator: $.optional.nullable.bool, - }, - - pinnedUsers: { - validator: $.optional.nullable.arr($.str), - }, - - hiddenTags: { - validator: $.optional.nullable.arr($.str), - }, - - blockedHosts: { - validator: $.optional.nullable.arr($.str), - }, - - mascotImageUrl: { - validator: $.optional.nullable.str, - }, - - bannerUrl: { - validator: $.optional.nullable.str, - }, - - errorImageUrl: { - validator: $.optional.nullable.str, - }, - - iconUrl: { - validator: $.optional.nullable.str, - }, - - backgroundImageUrl: { - validator: $.optional.nullable.str, - }, - - logoImageUrl: { - validator: $.optional.nullable.str, - }, - - name: { - validator: $.optional.nullable.str, - }, - - description: { - validator: $.optional.nullable.str, - }, - - maxNoteTextLength: { - validator: $.optional.num.min(0).max(DB_MAX_NOTE_TEXT_LENGTH), - }, - - localDriveCapacityMb: { - validator: $.optional.num.min(0), - }, - - remoteDriveCapacityMb: { - validator: $.optional.num.min(0), - }, - - cacheRemoteFiles: { - validator: $.optional.bool, - }, - - proxyRemoteFiles: { - validator: $.optional.bool, - }, - - emailRequiredForSignup: { - validator: $.optional.bool, - }, - - enableHcaptcha: { - validator: $.optional.bool, - }, - - hcaptchaSiteKey: { - validator: $.optional.nullable.str, - }, - - hcaptchaSecretKey: { - validator: $.optional.nullable.str, - }, - - enableRecaptcha: { - validator: $.optional.bool, - }, - - recaptchaSiteKey: { - validator: $.optional.nullable.str, - }, - - recaptchaSecretKey: { - validator: $.optional.nullable.str, - }, - - proxyAccountId: { - validator: $.optional.nullable.type(ID), - }, - - maintainerName: { - validator: $.optional.nullable.str, - }, - - maintainerEmail: { - validator: $.optional.nullable.str, - }, - - pinnedPages: { - validator: $.optional.arr($.str), - }, - - pinnedClipId: { - validator: $.optional.nullable.type(ID), - }, - - langs: { - validator: $.optional.arr($.str), - }, - - summalyProxy: { - validator: $.optional.nullable.str, - }, - - deeplAuthKey: { - validator: $.optional.nullable.str, - }, - - deeplIsPro: { - validator: $.optional.bool, - }, - - enableTwitterIntegration: { - validator: $.optional.bool, - }, - - twitterConsumerKey: { - validator: $.optional.nullable.str, - }, - - twitterConsumerSecret: { - validator: $.optional.nullable.str, - }, - - enableGithubIntegration: { - validator: $.optional.bool, - }, - - githubClientId: { - validator: $.optional.nullable.str, - }, - - githubClientSecret: { - validator: $.optional.nullable.str, - }, - - enableDiscordIntegration: { - validator: $.optional.bool, - }, - - discordClientId: { - validator: $.optional.nullable.str, - }, - - discordClientSecret: { - validator: $.optional.nullable.str, - }, - - enableEmail: { - validator: $.optional.bool, - }, - - email: { - validator: $.optional.nullable.str, - }, - - smtpSecure: { - validator: $.optional.bool, - }, - - smtpHost: { - validator: $.optional.nullable.str, - }, - - smtpPort: { - validator: $.optional.nullable.num, - }, - - smtpUser: { - validator: $.optional.nullable.str, - }, - - smtpPass: { - validator: $.optional.nullable.str, - }, - - enableServiceWorker: { - validator: $.optional.bool, - }, - - swPublicKey: { - validator: $.optional.nullable.str, - }, - - swPrivateKey: { - validator: $.optional.nullable.str, - }, - - tosUrl: { - validator: $.optional.nullable.str, - }, - - repositoryUrl: { - validator: $.optional.str, - }, - - feedbackUrl: { - validator: $.optional.str, - }, - - useObjectStorage: { - validator: $.optional.bool - }, - - objectStorageBaseUrl: { - validator: $.optional.nullable.str - }, - - objectStorageBucket: { - validator: $.optional.nullable.str - }, - - objectStoragePrefix: { - validator: $.optional.nullable.str - }, - - objectStorageEndpoint: { - validator: $.optional.nullable.str - }, - - objectStorageRegion: { - validator: $.optional.nullable.str - }, - - objectStoragePort: { - validator: $.optional.nullable.num - }, - - objectStorageAccessKey: { - validator: $.optional.nullable.str - }, - - objectStorageSecretKey: { - validator: $.optional.nullable.str - }, - - objectStorageUseSSL: { - validator: $.optional.bool - }, - - objectStorageUseProxy: { - validator: $.optional.bool - }, - - objectStorageSetPublicRead: { - validator: $.optional.bool - }, - - objectStorageS3ForcePathStyle: { - validator: $.optional.bool - }, - } -}; - -export default define(meta, async (ps, me) => { - const set = {} as Partial<Meta>; - - if (typeof ps.disableRegistration === 'boolean') { - set.disableRegistration = ps.disableRegistration; - } - - if (typeof ps.disableLocalTimeline === 'boolean') { - set.disableLocalTimeline = ps.disableLocalTimeline; - } - - if (typeof ps.disableGlobalTimeline === 'boolean') { - set.disableGlobalTimeline = ps.disableGlobalTimeline; - } - - if (typeof ps.useStarForReactionFallback === 'boolean') { - set.useStarForReactionFallback = ps.useStarForReactionFallback; - } - - if (Array.isArray(ps.pinnedUsers)) { - set.pinnedUsers = ps.pinnedUsers.filter(Boolean); - } - - if (Array.isArray(ps.hiddenTags)) { - set.hiddenTags = ps.hiddenTags.filter(Boolean); - } - - if (Array.isArray(ps.blockedHosts)) { - set.blockedHosts = ps.blockedHosts.filter(Boolean); - } - - if (ps.mascotImageUrl !== undefined) { - set.mascotImageUrl = ps.mascotImageUrl; - } - - if (ps.bannerUrl !== undefined) { - set.bannerUrl = ps.bannerUrl; - } - - if (ps.iconUrl !== undefined) { - set.iconUrl = ps.iconUrl; - } - - if (ps.backgroundImageUrl !== undefined) { - set.backgroundImageUrl = ps.backgroundImageUrl; - } - - if (ps.logoImageUrl !== undefined) { - set.logoImageUrl = ps.logoImageUrl; - } - - if (ps.name !== undefined) { - set.name = ps.name; - } - - if (ps.description !== undefined) { - set.description = ps.description; - } - - if (ps.maxNoteTextLength) { - set.maxNoteTextLength = ps.maxNoteTextLength; - } - - if (ps.localDriveCapacityMb !== undefined) { - set.localDriveCapacityMb = ps.localDriveCapacityMb; - } - - if (ps.remoteDriveCapacityMb !== undefined) { - set.remoteDriveCapacityMb = ps.remoteDriveCapacityMb; - } - - if (ps.cacheRemoteFiles !== undefined) { - set.cacheRemoteFiles = ps.cacheRemoteFiles; - } - - if (ps.proxyRemoteFiles !== undefined) { - set.proxyRemoteFiles = ps.proxyRemoteFiles; - } - - if (ps.emailRequiredForSignup !== undefined) { - set.emailRequiredForSignup = ps.emailRequiredForSignup; - } - - if (ps.enableHcaptcha !== undefined) { - set.enableHcaptcha = ps.enableHcaptcha; - } - - if (ps.hcaptchaSiteKey !== undefined) { - set.hcaptchaSiteKey = ps.hcaptchaSiteKey; - } - - if (ps.hcaptchaSecretKey !== undefined) { - set.hcaptchaSecretKey = ps.hcaptchaSecretKey; - } - - if (ps.enableRecaptcha !== undefined) { - set.enableRecaptcha = ps.enableRecaptcha; - } - - if (ps.recaptchaSiteKey !== undefined) { - set.recaptchaSiteKey = ps.recaptchaSiteKey; - } - - if (ps.recaptchaSecretKey !== undefined) { - set.recaptchaSecretKey = ps.recaptchaSecretKey; - } - - if (ps.proxyAccountId !== undefined) { - set.proxyAccountId = ps.proxyAccountId; - } - - if (ps.maintainerName !== undefined) { - set.maintainerName = ps.maintainerName; - } - - if (ps.maintainerEmail !== undefined) { - set.maintainerEmail = ps.maintainerEmail; - } - - if (Array.isArray(ps.langs)) { - set.langs = ps.langs.filter(Boolean); - } - - if (Array.isArray(ps.pinnedPages)) { - set.pinnedPages = ps.pinnedPages.filter(Boolean); - } - - if (ps.pinnedClipId !== undefined) { - set.pinnedClipId = ps.pinnedClipId; - } - - if (ps.summalyProxy !== undefined) { - set.summalyProxy = ps.summalyProxy; - } - - if (ps.enableTwitterIntegration !== undefined) { - set.enableTwitterIntegration = ps.enableTwitterIntegration; - } - - if (ps.twitterConsumerKey !== undefined) { - set.twitterConsumerKey = ps.twitterConsumerKey; - } - - if (ps.twitterConsumerSecret !== undefined) { - set.twitterConsumerSecret = ps.twitterConsumerSecret; - } - - if (ps.enableGithubIntegration !== undefined) { - set.enableGithubIntegration = ps.enableGithubIntegration; - } - - if (ps.githubClientId !== undefined) { - set.githubClientId = ps.githubClientId; - } - - if (ps.githubClientSecret !== undefined) { - set.githubClientSecret = ps.githubClientSecret; - } - - if (ps.enableDiscordIntegration !== undefined) { - set.enableDiscordIntegration = ps.enableDiscordIntegration; - } - - if (ps.discordClientId !== undefined) { - set.discordClientId = ps.discordClientId; - } - - if (ps.discordClientSecret !== undefined) { - set.discordClientSecret = ps.discordClientSecret; - } - - if (ps.enableEmail !== undefined) { - set.enableEmail = ps.enableEmail; - } - - if (ps.email !== undefined) { - set.email = ps.email; - } - - if (ps.smtpSecure !== undefined) { - set.smtpSecure = ps.smtpSecure; - } - - if (ps.smtpHost !== undefined) { - set.smtpHost = ps.smtpHost; - } - - if (ps.smtpPort !== undefined) { - set.smtpPort = ps.smtpPort; - } - - if (ps.smtpUser !== undefined) { - set.smtpUser = ps.smtpUser; - } - - if (ps.smtpPass !== undefined) { - set.smtpPass = ps.smtpPass; - } - - if (ps.errorImageUrl !== undefined) { - set.errorImageUrl = ps.errorImageUrl; - } - - if (ps.enableServiceWorker !== undefined) { - set.enableServiceWorker = ps.enableServiceWorker; - } - - if (ps.swPublicKey !== undefined) { - set.swPublicKey = ps.swPublicKey; - } - - if (ps.swPrivateKey !== undefined) { - set.swPrivateKey = ps.swPrivateKey; - } - - if (ps.tosUrl !== undefined) { - set.ToSUrl = ps.tosUrl; - } - - if (ps.repositoryUrl !== undefined) { - set.repositoryUrl = ps.repositoryUrl; - } - - if (ps.feedbackUrl !== undefined) { - set.feedbackUrl = ps.feedbackUrl; - } - - if (ps.useObjectStorage !== undefined) { - set.useObjectStorage = ps.useObjectStorage; - } - - if (ps.objectStorageBaseUrl !== undefined) { - set.objectStorageBaseUrl = ps.objectStorageBaseUrl; - } - - if (ps.objectStorageBucket !== undefined) { - set.objectStorageBucket = ps.objectStorageBucket; - } - - if (ps.objectStoragePrefix !== undefined) { - set.objectStoragePrefix = ps.objectStoragePrefix; - } - - if (ps.objectStorageEndpoint !== undefined) { - set.objectStorageEndpoint = ps.objectStorageEndpoint; - } - - if (ps.objectStorageRegion !== undefined) { - set.objectStorageRegion = ps.objectStorageRegion; - } - - if (ps.objectStoragePort !== undefined) { - set.objectStoragePort = ps.objectStoragePort; - } - - if (ps.objectStorageAccessKey !== undefined) { - set.objectStorageAccessKey = ps.objectStorageAccessKey; - } - - if (ps.objectStorageSecretKey !== undefined) { - set.objectStorageSecretKey = ps.objectStorageSecretKey; - } - - if (ps.objectStorageUseSSL !== undefined) { - set.objectStorageUseSSL = ps.objectStorageUseSSL; - } - - if (ps.objectStorageUseProxy !== undefined) { - set.objectStorageUseProxy = ps.objectStorageUseProxy; - } - - if (ps.objectStorageSetPublicRead !== undefined) { - set.objectStorageSetPublicRead = ps.objectStorageSetPublicRead; - } - - if (ps.objectStorageS3ForcePathStyle !== undefined) { - set.objectStorageS3ForcePathStyle = ps.objectStorageS3ForcePathStyle; - } - - if (ps.deeplAuthKey !== undefined) { - if (ps.deeplAuthKey === '') { - set.deeplAuthKey = null; - } else { - set.deeplAuthKey = ps.deeplAuthKey; - } - } - - if (ps.deeplIsPro !== undefined) { - set.deeplIsPro = ps.deeplIsPro; - } - - await getConnection().transaction(async transactionalEntityManager => { - const meta = await transactionalEntityManager.findOne(Meta, { - order: { - id: 'DESC' - } - }); - - if (meta) { - await transactionalEntityManager.update(Meta, meta.id, set); - } else { - await transactionalEntityManager.save(Meta, set); - } - }); - - insertModerationLog(me, 'updateMeta'); -}); diff --git a/src/server/api/endpoints/admin/vacuum.ts b/src/server/api/endpoints/admin/vacuum.ts deleted file mode 100644 index 9a80d88c44..0000000000 --- a/src/server/api/endpoints/admin/vacuum.ts +++ /dev/null @@ -1,36 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { getConnection } from 'typeorm'; -import { insertModerationLog } from '@/services/insert-moderation-log'; - -export const meta = { - tags: ['admin'], - - requireCredential: true as const, - requireModerator: true, - - params: { - full: { - validator: $.bool, - }, - analyze: { - validator: $.bool, - }, - } -}; - -export default define(meta, async (ps, me) => { - const params: string[] = []; - - if (ps.full) { - params.push('FULL'); - } - - if (ps.analyze) { - params.push('ANALYZE'); - } - - getConnection().query('VACUUM ' + params.join(' ')); - - insertModerationLog(me, 'vacuum', ps); -}); diff --git a/src/server/api/endpoints/announcements.ts b/src/server/api/endpoints/announcements.ts deleted file mode 100644 index a67737b2ff..0000000000 --- a/src/server/api/endpoints/announcements.ts +++ /dev/null @@ -1,92 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../define'; -import { Announcements, AnnouncementReads } from '@/models/index'; -import { makePaginationQuery } from '../common/make-pagination-query'; - -export const meta = { - tags: ['meta'], - - requireCredential: false as const, - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - withUnreads: { - validator: $.optional.boolean, - default: false - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id', - example: 'xxxxxxxxxx', - }, - createdAt: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'date-time', - }, - updatedAt: { - type: 'string' as const, - optional: false as const, nullable: true as const, - format: 'date-time', - }, - text: { - type: 'string' as const, - optional: false as const, nullable: false as const, - }, - title: { - type: 'string' as const, - optional: false as const, nullable: false as const, - }, - imageUrl: { - type: 'string' as const, - optional: false as const, nullable: true as const, - }, - isRead: { - type: 'boolean' as const, - optional: false as const, nullable: false as const, - } - } - } - } -}; - -export default define(meta, async (ps, user) => { - const query = makePaginationQuery(Announcements.createQueryBuilder('announcement'), ps.sinceId, ps.untilId); - - const announcements = await query.take(ps.limit!).getMany(); - - if (user) { - const reads = (await AnnouncementReads.find({ - userId: user.id - })).map(x => x.announcementId); - - for (const announcement of announcements) { - (announcement as any).isRead = reads.includes(announcement.id); - } - } - - return ps.withUnreads ? announcements.filter((a: any) => !a.isRead) : announcements; -}); diff --git a/src/server/api/endpoints/antennas/create.ts b/src/server/api/endpoints/antennas/create.ts deleted file mode 100644 index 4bdae8cc33..0000000000 --- a/src/server/api/endpoints/antennas/create.ts +++ /dev/null @@ -1,127 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { genId } from '@/misc/gen-id'; -import { Antennas, UserLists, UserGroupJoinings } from '@/models/index'; -import { ID } from '@/misc/cafy-id'; -import { ApiError } from '../../error'; -import { publishInternalEvent } from '@/services/stream'; - -export const meta = { - tags: ['antennas'], - - requireCredential: true as const, - - kind: 'write:account', - - params: { - name: { - validator: $.str.range(1, 100) - }, - - src: { - validator: $.str.or(['home', 'all', 'users', 'list', 'group']) - }, - - userListId: { - validator: $.nullable.optional.type(ID), - }, - - userGroupId: { - validator: $.nullable.optional.type(ID), - }, - - keywords: { - validator: $.arr($.arr($.str)) - }, - - excludeKeywords: { - validator: $.arr($.arr($.str)) - }, - - users: { - validator: $.arr($.str) - }, - - caseSensitive: { - validator: $.bool - }, - - withReplies: { - validator: $.bool - }, - - withFile: { - validator: $.bool - }, - - notify: { - validator: $.bool - } - }, - - errors: { - noSuchUserList: { - message: 'No such user list.', - code: 'NO_SUCH_USER_LIST', - id: '95063e93-a283-4b8b-9aa5-bcdb8df69a7f' - }, - - noSuchUserGroup: { - message: 'No such user group.', - code: 'NO_SUCH_USER_GROUP', - id: 'aa3c0b9a-8cae-47c0-92ac-202ce5906682' - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Antenna' - } -}; - -export default define(meta, async (ps, user) => { - let userList; - let userGroupJoining; - - if (ps.src === 'list' && ps.userListId) { - userList = await UserLists.findOne({ - id: ps.userListId, - userId: user.id, - }); - - if (userList == null) { - throw new ApiError(meta.errors.noSuchUserList); - } - } else if (ps.src === 'group' && ps.userGroupId) { - userGroupJoining = await UserGroupJoinings.findOne({ - userGroupId: ps.userGroupId, - userId: user.id, - }); - - if (userGroupJoining == null) { - throw new ApiError(meta.errors.noSuchUserGroup); - } - } - - const antenna = await Antennas.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - name: ps.name, - src: ps.src, - userListId: userList ? userList.id : null, - userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null, - keywords: ps.keywords, - excludeKeywords: ps.excludeKeywords, - users: ps.users, - caseSensitive: ps.caseSensitive, - withReplies: ps.withReplies, - withFile: ps.withFile, - notify: ps.notify, - }).then(x => Antennas.findOneOrFail(x.identifiers[0])); - - publishInternalEvent('antennaCreated', antenna); - - return await Antennas.pack(antenna); -}); diff --git a/src/server/api/endpoints/antennas/delete.ts b/src/server/api/endpoints/antennas/delete.ts deleted file mode 100644 index 1cd136183e..0000000000 --- a/src/server/api/endpoints/antennas/delete.ts +++ /dev/null @@ -1,43 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { Antennas } from '@/models/index'; -import { publishInternalEvent } from '@/services/stream'; - -export const meta = { - tags: ['antennas'], - - requireCredential: true as const, - - kind: 'write:account', - - params: { - antennaId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchAntenna: { - message: 'No such antenna.', - code: 'NO_SUCH_ANTENNA', - id: 'b34dcf9d-348f-44bb-99d0-6c9314cfe2df' - } - } -}; - -export default define(meta, async (ps, user) => { - const antenna = await Antennas.findOne({ - id: ps.antennaId, - userId: user.id - }); - - if (antenna == null) { - throw new ApiError(meta.errors.noSuchAntenna); - } - - await Antennas.delete(antenna.id); - - publishInternalEvent('antennaDeleted', antenna); -}); diff --git a/src/server/api/endpoints/antennas/list.ts b/src/server/api/endpoints/antennas/list.ts deleted file mode 100644 index 8baae8435b..0000000000 --- a/src/server/api/endpoints/antennas/list.ts +++ /dev/null @@ -1,28 +0,0 @@ -import define from '../../define'; -import { Antennas } from '@/models/index'; - -export const meta = { - tags: ['antennas', 'account'], - - requireCredential: true as const, - - kind: 'read:account', - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Antenna' - } - } -}; - -export default define(meta, async (ps, me) => { - const antennas = await Antennas.find({ - userId: me.id, - }); - - return await Promise.all(antennas.map(x => Antennas.pack(x))); -}); diff --git a/src/server/api/endpoints/antennas/notes.ts b/src/server/api/endpoints/antennas/notes.ts deleted file mode 100644 index 1759e95b4c..0000000000 --- a/src/server/api/endpoints/antennas/notes.ts +++ /dev/null @@ -1,93 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import readNote from '@/services/note/read'; -import { Antennas, Notes, AntennaNotes } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; -import { ApiError } from '../../error'; -import { generateBlockedUserQuery } from '../../common/generate-block-query'; - -export const meta = { - tags: ['antennas', 'account', 'notes'], - - requireCredential: true as const, - - kind: 'read:account', - - params: { - antennaId: { - validator: $.type(ID), - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - }, - - errors: { - noSuchAntenna: { - message: 'No such antenna.', - code: 'NO_SUCH_ANTENNA', - id: '850926e0-fd3b-49b6-b69a-b28a5dbd82fe' - } - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Note' - } - } -}; - -export default define(meta, async (ps, user) => { - const antenna = await Antennas.findOne({ - id: ps.antennaId, - userId: user.id - }); - - if (antenna == null) { - throw new ApiError(meta.errors.noSuchAntenna); - } - - const antennaQuery = AntennaNotes.createQueryBuilder('joining') - .select('joining.noteId') - .where('joining.antennaId = :antennaId', { antennaId: antenna.id }); - - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere(`note.id IN (${ antennaQuery.getQuery() })`) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .setParameters(antennaQuery.getParameters()); - - generateVisibilityQuery(query, user); - generateMutedUserQuery(query, user); - generateBlockedUserQuery(query, user); - - const notes = await query - .take(ps.limit!) - .getMany(); - - if (notes.length > 0) { - readNote(user.id, notes); - } - - return await Notes.packMany(notes, user); -}); diff --git a/src/server/api/endpoints/antennas/show.ts b/src/server/api/endpoints/antennas/show.ts deleted file mode 100644 index 3cdf4dcb61..0000000000 --- a/src/server/api/endpoints/antennas/show.ts +++ /dev/null @@ -1,47 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { Antennas } from '@/models/index'; - -export const meta = { - tags: ['antennas', 'account'], - - requireCredential: true as const, - - kind: 'read:account', - - params: { - antennaId: { - validator: $.type(ID), - }, - }, - - errors: { - noSuchAntenna: { - message: 'No such antenna.', - code: 'NO_SUCH_ANTENNA', - id: 'c06569fb-b025-4f23-b22d-1fcd20d2816b' - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Antenna' - } -}; - -export default define(meta, async (ps, me) => { - // Fetch the antenna - const antenna = await Antennas.findOne({ - id: ps.antennaId, - userId: me.id, - }); - - if (antenna == null) { - throw new ApiError(meta.errors.noSuchAntenna); - } - - return await Antennas.pack(antenna); -}); diff --git a/src/server/api/endpoints/antennas/update.ts b/src/server/api/endpoints/antennas/update.ts deleted file mode 100644 index d69b4feee6..0000000000 --- a/src/server/api/endpoints/antennas/update.ts +++ /dev/null @@ -1,143 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { Antennas, UserLists, UserGroupJoinings } from '@/models/index'; -import { publishInternalEvent } from '@/services/stream'; - -export const meta = { - tags: ['antennas'], - - requireCredential: true as const, - - kind: 'write:account', - - params: { - antennaId: { - validator: $.type(ID), - }, - - name: { - validator: $.str.range(1, 100) - }, - - src: { - validator: $.str.or(['home', 'all', 'users', 'list', 'group']) - }, - - userListId: { - validator: $.nullable.optional.type(ID), - }, - - userGroupId: { - validator: $.nullable.optional.type(ID), - }, - - keywords: { - validator: $.arr($.arr($.str)) - }, - - excludeKeywords: { - validator: $.arr($.arr($.str)) - }, - - users: { - validator: $.arr($.str) - }, - - caseSensitive: { - validator: $.bool - }, - - withReplies: { - validator: $.bool - }, - - withFile: { - validator: $.bool - }, - - notify: { - validator: $.bool - } - }, - - errors: { - noSuchAntenna: { - message: 'No such antenna.', - code: 'NO_SUCH_ANTENNA', - id: '10c673ac-8852-48eb-aa1f-f5b67f069290' - }, - - noSuchUserList: { - message: 'No such user list.', - code: 'NO_SUCH_USER_LIST', - id: '1c6b35c9-943e-48c2-81e4-2844989407f7' - }, - - noSuchUserGroup: { - message: 'No such user group.', - code: 'NO_SUCH_USER_GROUP', - id: '109ed789-b6eb-456e-b8a9-6059d567d385' - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Antenna' - } -}; - -export default define(meta, async (ps, user) => { - // Fetch the antenna - const antenna = await Antennas.findOne({ - id: ps.antennaId, - userId: user.id - }); - - if (antenna == null) { - throw new ApiError(meta.errors.noSuchAntenna); - } - - let userList; - let userGroupJoining; - - if (ps.src === 'list' && ps.userListId) { - userList = await UserLists.findOne({ - id: ps.userListId, - userId: user.id, - }); - - if (userList == null) { - throw new ApiError(meta.errors.noSuchUserList); - } - } else if (ps.src === 'group' && ps.userGroupId) { - userGroupJoining = await UserGroupJoinings.findOne({ - userGroupId: ps.userGroupId, - userId: user.id, - }); - - if (userGroupJoining == null) { - throw new ApiError(meta.errors.noSuchUserGroup); - } - } - - await Antennas.update(antenna.id, { - name: ps.name, - src: ps.src, - userListId: userList ? userList.id : null, - userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null, - keywords: ps.keywords, - excludeKeywords: ps.excludeKeywords, - users: ps.users, - caseSensitive: ps.caseSensitive, - withReplies: ps.withReplies, - withFile: ps.withFile, - notify: ps.notify, - }); - - publishInternalEvent('antennaUpdated', await Antennas.findOneOrFail(antenna.id)); - - return await Antennas.pack(antenna.id); -}); diff --git a/src/server/api/endpoints/ap/get.ts b/src/server/api/endpoints/ap/get.ts deleted file mode 100644 index 78919f43b0..0000000000 --- a/src/server/api/endpoints/ap/get.ts +++ /dev/null @@ -1,36 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import Resolver from '@/remote/activitypub/resolver'; -import { ApiError } from '../../error'; -import * as ms from 'ms'; - -export const meta = { - tags: ['federation'], - - requireCredential: true as const, - - limit: { - duration: ms('1hour'), - max: 30 - }, - - params: { - uri: { - validator: $.str, - }, - }, - - errors: { - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - } -}; - -export default define(meta, async (ps) => { - const resolver = new Resolver(); - const object = await resolver.resolve(ps.uri); - return object; -}); diff --git a/src/server/api/endpoints/ap/show.ts b/src/server/api/endpoints/ap/show.ts deleted file mode 100644 index 2280d93724..0000000000 --- a/src/server/api/endpoints/ap/show.ts +++ /dev/null @@ -1,190 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import config from '@/config/index'; -import { createPerson } from '@/remote/activitypub/models/person'; -import { createNote } from '@/remote/activitypub/models/note'; -import Resolver from '@/remote/activitypub/resolver'; -import { ApiError } from '../../error'; -import { extractDbHost } from '@/misc/convert-host'; -import { Users, Notes } from '@/models/index'; -import { Note } from '@/models/entities/note'; -import { User } from '@/models/entities/user'; -import { fetchMeta } from '@/misc/fetch-meta'; -import { isActor, isPost, getApId } from '@/remote/activitypub/type'; -import * as ms from 'ms'; - -export const meta = { - tags: ['federation'], - - requireCredential: true as const, - - limit: { - duration: ms('1hour'), - max: 30 - }, - - params: { - uri: { - validator: $.str, - }, - }, - - errors: { - noSuchObject: { - message: 'No such object.', - code: 'NO_SUCH_OBJECT', - id: 'dc94d745-1262-4e63-a17d-fecaa57efc82' - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - type: { - type: 'string' as const, - optional: false as const, nullable: false as const, - enum: ['User', 'Note'] - }, - object: { - type: 'object' as const, - optional: false as const, nullable: false as const - } - } - } -}; - -export default define(meta, async (ps) => { - const object = await fetchAny(ps.uri); - if (object) { - return object; - } else { - throw new ApiError(meta.errors.noSuchObject); - } -}); - -/*** - * URIからUserかNoteを解決する - */ -async function fetchAny(uri: string) { - // URIがこのサーバーを指しているなら、ローカルユーザーIDとしてDBからフェッチ - if (uri.startsWith(config.url + '/')) { - const parts = uri.split('/'); - const id = parts.pop(); - const type = parts.pop(); - - if (type === 'notes') { - const note = await Notes.findOne(id); - - if (note) { - return { - type: 'Note', - object: await Notes.pack(note, null, { detail: true }) - }; - } - } else if (type === 'users') { - const user = await Users.findOne(id); - - if (user) { - return { - type: 'User', - object: await Users.pack(user, null, { detail: true }) - }; - } - } - } - - // ブロックしてたら中断 - const meta = await fetchMeta(); - if (meta.blockedHosts.includes(extractDbHost(uri))) return null; - - // URI(AP Object id)としてDB検索 - { - const [user, note] = await Promise.all([ - Users.findOne({ uri: uri }), - Notes.findOne({ uri: uri }) - ]); - - const packed = await mergePack(user, note); - if (packed !== null) return packed; - } - - // リモートから一旦オブジェクトフェッチ - const resolver = new Resolver(); - const object = await resolver.resolve(uri) as any; - - // /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する - // これはDBに存在する可能性があるため再度DB検索 - if (uri !== object.id) { - if (object.id.startsWith(config.url + '/')) { - const parts = object.id.split('/'); - const id = parts.pop(); - const type = parts.pop(); - - if (type === 'notes') { - const note = await Notes.findOne(id); - - if (note) { - return { - type: 'Note', - object: await Notes.pack(note, null, { detail: true }) - }; - } - } else if (type === 'users') { - const user = await Users.findOne(id); - - if (user) { - return { - type: 'User', - object: await Users.pack(user, null, { detail: true }) - }; - } - } - } - - const [user, note] = await Promise.all([ - Users.findOne({ uri: object.id }), - Notes.findOne({ uri: object.id }) - ]); - - const packed = await mergePack(user, note); - if (packed !== null) return packed; - } - - // それでもみつからなければ新規であるため登録 - if (isActor(object)) { - const user = await createPerson(getApId(object)); - return { - type: 'User', - object: await Users.pack(user, null, { detail: true }) - }; - } - - if (isPost(object)) { - const note = await createNote(getApId(object), undefined, true); - return { - type: 'Note', - object: await Notes.pack(note!, null, { detail: true }) - }; - } - - return null; -} - -async function mergePack(user: User | null | undefined, note: Note | null | undefined) { - if (user != null) { - return { - type: 'User', - object: await Users.pack(user, null, { detail: true }) - }; - } - - if (note != null) { - return { - type: 'Note', - object: await Notes.pack(note, null, { detail: true }) - }; - } - - return null; -} diff --git a/src/server/api/endpoints/app/create.ts b/src/server/api/endpoints/app/create.ts deleted file mode 100644 index c2ce943dcc..0000000000 --- a/src/server/api/endpoints/app/create.ts +++ /dev/null @@ -1,63 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { Apps } from '@/models/index'; -import { genId } from '@/misc/gen-id'; -import { unique } from '@/prelude/array'; -import { secureRndstr } from '@/misc/secure-rndstr'; - -export const meta = { - tags: ['app'], - - requireCredential: false as const, - - params: { - name: { - validator: $.str, - }, - - description: { - validator: $.str, - }, - - permission: { - validator: $.arr($.str).unique(), - }, - - // TODO: Check it is valid url - callbackUrl: { - validator: $.optional.nullable.str, - default: null, - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'App', - }, -}; - -export default define(meta, async (ps, user) => { - // Generate secret - const secret = secureRndstr(32, true); - - // for backward compatibility - const permission = unique(ps.permission.map(v => v.replace(/^(.+)(\/|-)(read|write)$/, '$3:$1'))); - - // Create account - const app = await Apps.save({ - id: genId(), - createdAt: new Date(), - userId: user ? user.id : null, - name: ps.name, - description: ps.description, - permission, - callbackUrl: ps.callbackUrl, - secret: secret - }); - - return await Apps.pack(app, null, { - detail: true, - includeSecret: true - }); -}); diff --git a/src/server/api/endpoints/app/show.ts b/src/server/api/endpoints/app/show.ts deleted file mode 100644 index 27f12eb44f..0000000000 --- a/src/server/api/endpoints/app/show.ts +++ /dev/null @@ -1,51 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { Apps } from '@/models/index'; - -export const meta = { - tags: ['app'], - - params: { - appId: { - validator: $.type(ID), - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'App', - }, - - errors: { - noSuchApp: { - message: 'No such app.', - code: 'NO_SUCH_APP', - id: 'dce83913-2dc6-4093-8a7b-71dbb11718a3' - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'App' - } -}; - -export default define(meta, async (ps, user, token) => { - const isSecure = user != null && token == null; - - // Lookup app - const ap = await Apps.findOne(ps.appId); - - if (ap == null) { - throw new ApiError(meta.errors.noSuchApp); - } - - return await Apps.pack(ap, user, { - detail: true, - includeSecret: isSecure && (ap.userId === user!.id) - }); -}); diff --git a/src/server/api/endpoints/auth/accept.ts b/src/server/api/endpoints/auth/accept.ts deleted file mode 100644 index 1d1d8ac227..0000000000 --- a/src/server/api/endpoints/auth/accept.ts +++ /dev/null @@ -1,76 +0,0 @@ -import * as crypto from 'crypto'; -import $ from 'cafy'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { AuthSessions, AccessTokens, Apps } from '@/models/index'; -import { genId } from '@/misc/gen-id'; -import { secureRndstr } from '@/misc/secure-rndstr'; - -export const meta = { - tags: ['auth'], - - requireCredential: true as const, - - secure: true, - - params: { - token: { - validator: $.str - } - }, - - errors: { - noSuchSession: { - message: 'No such session.', - code: 'NO_SUCH_SESSION', - id: '9c72d8de-391a-43c1-9d06-08d29efde8df' - }, - } -}; - -export default define(meta, async (ps, user) => { - // Fetch token - const session = await AuthSessions - .findOne({ token: ps.token }); - - if (session == null) { - throw new ApiError(meta.errors.noSuchSession); - } - - // Generate access token - const accessToken = secureRndstr(32, true); - - // Fetch exist access token - const exist = await AccessTokens.findOne({ - appId: session.appId, - userId: user.id, - }); - - if (exist == null) { - // Lookup app - const app = await Apps.findOneOrFail(session.appId); - - // Generate Hash - const sha256 = crypto.createHash('sha256'); - sha256.update(accessToken + app.secret); - const hash = sha256.digest('hex'); - - const now = new Date(); - - // Insert access token doc - await AccessTokens.insert({ - id: genId(), - createdAt: now, - lastUsedAt: now, - appId: session.appId, - userId: user.id, - token: accessToken, - hash: hash - }); - } - - // Update session - await AuthSessions.update(session.id, { - userId: user.id - }); -}); diff --git a/src/server/api/endpoints/auth/session/generate.ts b/src/server/api/endpoints/auth/session/generate.ts deleted file mode 100644 index 859cf52ed3..0000000000 --- a/src/server/api/endpoints/auth/session/generate.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { v4 as uuid } from 'uuid'; -import $ from 'cafy'; -import config from '@/config/index'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { Apps, AuthSessions } from '@/models/index'; -import { genId } from '@/misc/gen-id'; - -export const meta = { - tags: ['auth'], - - requireCredential: false as const, - - params: { - appSecret: { - validator: $.str, - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - token: { - type: 'string' as const, - optional: false as const, nullable: false as const, - }, - url: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'url', - }, - } - }, - - errors: { - noSuchApp: { - message: 'No such app.', - code: 'NO_SUCH_APP', - id: '92f93e63-428e-4f2f-a5a4-39e1407fe998' - } - } -}; - -export default define(meta, async (ps) => { - // Lookup app - const app = await Apps.findOne({ - secret: ps.appSecret - }); - - if (app == null) { - throw new ApiError(meta.errors.noSuchApp); - } - - // Generate token - const token = uuid(); - - // Create session token document - const doc = await AuthSessions.save({ - id: genId(), - createdAt: new Date(), - appId: app.id, - token: token - }); - - return { - token: doc.token, - url: `${config.authUrl}/${doc.token}` - }; -}); diff --git a/src/server/api/endpoints/auth/session/show.ts b/src/server/api/endpoints/auth/session/show.ts deleted file mode 100644 index 23f1a56a37..0000000000 --- a/src/server/api/endpoints/auth/session/show.ts +++ /dev/null @@ -1,58 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { AuthSessions } from '@/models/index'; - -export const meta = { - tags: ['auth'], - - requireCredential: false as const, - - params: { - token: { - validator: $.str, - } - }, - - errors: { - noSuchSession: { - message: 'No such session.', - code: 'NO_SUCH_SESSION', - id: 'bd72c97d-eba7-4adb-a467-f171b8847250' - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id' - }, - app: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'App' - }, - token: { - type: 'string' as const, - optional: false as const, nullable: false as const - } - } - } -}; - -export default define(meta, async (ps, user) => { - // Lookup session - const session = await AuthSessions.findOne({ - token: ps.token - }); - - if (session == null) { - throw new ApiError(meta.errors.noSuchSession); - } - - return await AuthSessions.pack(session, user); -}); diff --git a/src/server/api/endpoints/auth/session/userkey.ts b/src/server/api/endpoints/auth/session/userkey.ts deleted file mode 100644 index 72201cb207..0000000000 --- a/src/server/api/endpoints/auth/session/userkey.ts +++ /dev/null @@ -1,98 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { Apps, AuthSessions, AccessTokens, Users } from '@/models/index'; - -export const meta = { - tags: ['auth'], - - requireCredential: false as const, - - params: { - appSecret: { - validator: $.str, - }, - - token: { - validator: $.str, - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - accessToken: { - type: 'string' as const, - optional: false as const, nullable: false as const, - }, - - user: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User', - }, - } - }, - - errors: { - noSuchApp: { - message: 'No such app.', - code: 'NO_SUCH_APP', - id: 'fcab192a-2c5a-43b7-8ad8-9b7054d8d40d' - }, - - noSuchSession: { - message: 'No such session.', - code: 'NO_SUCH_SESSION', - id: '5b5a1503-8bc8-4bd0-8054-dc189e8cdcb3' - }, - - pendingSession: { - message: 'This session is not completed yet.', - code: 'PENDING_SESSION', - id: '8c8a4145-02cc-4cca-8e66-29ba60445a8e' - } - } -}; - -export default define(meta, async (ps) => { - // Lookup app - const app = await Apps.findOne({ - secret: ps.appSecret - }); - - if (app == null) { - throw new ApiError(meta.errors.noSuchApp); - } - - // Fetch token - const session = await AuthSessions.findOne({ - token: ps.token, - appId: app.id - }); - - if (session == null) { - throw new ApiError(meta.errors.noSuchSession); - } - - if (session.userId == null) { - throw new ApiError(meta.errors.pendingSession); - } - - // Lookup access token - const accessToken = await AccessTokens.findOneOrFail({ - appId: app.id, - userId: session.userId - }); - - // Delete session - AuthSessions.delete(session.id); - - return { - accessToken: accessToken.token, - user: await Users.pack(session.userId, null, { - detail: true - }) - }; -}); diff --git a/src/server/api/endpoints/blocking/create.ts b/src/server/api/endpoints/blocking/create.ts deleted file mode 100644 index 2953252394..0000000000 --- a/src/server/api/endpoints/blocking/create.ts +++ /dev/null @@ -1,89 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import * as ms from 'ms'; -import create from '@/services/blocking/create'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { getUser } from '../../common/getters'; -import { Blockings, NoteWatchings, Users } from '@/models/index'; - -export const meta = { - tags: ['account'], - - limit: { - duration: ms('1hour'), - max: 100 - }, - - requireCredential: true as const, - - kind: 'write:blocks', - - params: { - userId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '7cc4f851-e2f1-4621-9633-ec9e1d00c01e' - }, - - blockeeIsYourself: { - message: 'Blockee is yourself.', - code: 'BLOCKEE_IS_YOURSELF', - id: '88b19138-f28d-42c0-8499-6a31bbd0fdc6' - }, - - alreadyBlocking: { - message: 'You are already blocking that user.', - code: 'ALREADY_BLOCKING', - id: '787fed64-acb9-464a-82eb-afbd745b9614' - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User' - } -}; - -export default define(meta, async (ps, user) => { - const blocker = await Users.findOneOrFail(user.id); - - // 自分自身 - if (user.id === ps.userId) { - throw new ApiError(meta.errors.blockeeIsYourself); - } - - // Get blockee - const blockee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - // Check if already blocking - const exist = await Blockings.findOne({ - blockerId: blocker.id, - blockeeId: blockee.id - }); - - if (exist != null) { - throw new ApiError(meta.errors.alreadyBlocking); - } - - await create(blocker, blockee); - - NoteWatchings.delete({ - userId: blocker.id, - noteUserId: blockee.id - }); - - return await Users.pack(blockee.id, blocker, { - detail: true - }); -}); diff --git a/src/server/api/endpoints/blocking/delete.ts b/src/server/api/endpoints/blocking/delete.ts deleted file mode 100644 index a66e46fdf0..0000000000 --- a/src/server/api/endpoints/blocking/delete.ts +++ /dev/null @@ -1,85 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import * as ms from 'ms'; -import deleteBlocking from '@/services/blocking/delete'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { getUser } from '../../common/getters'; -import { Blockings, Users } from '@/models/index'; - -export const meta = { - tags: ['account'], - - limit: { - duration: ms('1hour'), - max: 100 - }, - - requireCredential: true as const, - - kind: 'write:blocks', - - params: { - userId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '8621d8bf-c358-4303-a066-5ea78610eb3f' - }, - - blockeeIsYourself: { - message: 'Blockee is yourself.', - code: 'BLOCKEE_IS_YOURSELF', - id: '06f6fac6-524b-473c-a354-e97a40ae6eac' - }, - - notBlocking: { - message: 'You are not blocking that user.', - code: 'NOT_BLOCKING', - id: '291b2efa-60c6-45c0-9f6a-045c8f9b02cd' - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User', - }, -}; - -export default define(meta, async (ps, user) => { - const blocker = await Users.findOneOrFail(user.id); - - // Check if the blockee is yourself - if (user.id === ps.userId) { - throw new ApiError(meta.errors.blockeeIsYourself); - } - - // Get blockee - const blockee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - // Check not blocking - const exist = await Blockings.findOne({ - blockerId: blocker.id, - blockeeId: blockee.id - }); - - if (exist == null) { - throw new ApiError(meta.errors.notBlocking); - } - - // Delete blocking - await deleteBlocking(blocker, blockee); - - return await Users.pack(blockee.id, blocker, { - detail: true - }); -}); diff --git a/src/server/api/endpoints/blocking/list.ts b/src/server/api/endpoints/blocking/list.ts deleted file mode 100644 index fe25fdaba1..0000000000 --- a/src/server/api/endpoints/blocking/list.ts +++ /dev/null @@ -1,49 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { Blockings } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; - -export const meta = { - tags: ['account'], - - requireCredential: true as const, - - kind: 'read:blocks', - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 30 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Blocking', - } - }, -}; - -export default define(meta, async (ps, me) => { - const query = makePaginationQuery(Blockings.createQueryBuilder('blocking'), ps.sinceId, ps.untilId) - .andWhere(`blocking.blockerId = :meId`, { meId: me.id }); - - const blockings = await query - .take(ps.limit!) - .getMany(); - - return await Blockings.packMany(blockings, me); -}); diff --git a/src/server/api/endpoints/channels/create.ts b/src/server/api/endpoints/channels/create.ts deleted file mode 100644 index 0cedfd6c6a..0000000000 --- a/src/server/api/endpoints/channels/create.ts +++ /dev/null @@ -1,68 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { Channels, DriveFiles } from '@/models/index'; -import { Channel } from '@/models/entities/channel'; -import { genId } from '@/misc/gen-id'; -import { ID } from '@/misc/cafy-id'; - -export const meta = { - tags: ['channels'], - - requireCredential: true as const, - - kind: 'write:channels', - - params: { - name: { - validator: $.str.range(1, 128) - }, - - description: { - validator: $.nullable.optional.str.range(1, 2048) - }, - - bannerId: { - validator: $.nullable.optional.type(ID), - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Channel', - }, - - errors: { - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: 'cd1e9f3e-5a12-4ab4-96f6-5d0a2cc32050' - }, - } -}; - -export default define(meta, async (ps, user) => { - let banner = null; - if (ps.bannerId != null) { - banner = await DriveFiles.findOne({ - id: ps.bannerId, - userId: user.id - }); - - if (banner == null) { - throw new ApiError(meta.errors.noSuchFile); - } - } - - const channel = await Channels.save({ - id: genId(), - createdAt: new Date(), - userId: user.id, - name: ps.name, - description: ps.description || null, - bannerId: banner ? banner.id : null, - } as Channel); - - return await Channels.pack(channel, user); -}); diff --git a/src/server/api/endpoints/channels/featured.ts b/src/server/api/endpoints/channels/featured.ts deleted file mode 100644 index dc1f49f960..0000000000 --- a/src/server/api/endpoints/channels/featured.ts +++ /dev/null @@ -1,28 +0,0 @@ -import define from '../../define'; -import { Channels } from '@/models/index'; - -export const meta = { - tags: ['channels'], - - requireCredential: false as const, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Channel', - } - }, -}; - -export default define(meta, async (ps, me) => { - const query = Channels.createQueryBuilder('channel') - .where('channel.lastNotedAt IS NOT NULL') - .orderBy('channel.lastNotedAt', 'DESC'); - - const channels = await query.take(10).getMany(); - - return await Promise.all(channels.map(x => Channels.pack(x, me))); -}); diff --git a/src/server/api/endpoints/channels/follow.ts b/src/server/api/endpoints/channels/follow.ts deleted file mode 100644 index d4664e6996..0000000000 --- a/src/server/api/endpoints/channels/follow.ts +++ /dev/null @@ -1,48 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { Channels, ChannelFollowings } from '@/models/index'; -import { genId } from '@/misc/gen-id'; -import { publishUserEvent } from '@/services/stream'; - -export const meta = { - tags: ['channels'], - - requireCredential: true as const, - - kind: 'write:channels', - - params: { - channelId: { - validator: $.type(ID), - }, - }, - - errors: { - noSuchChannel: { - message: 'No such channel.', - code: 'NO_SUCH_CHANNEL', - id: 'c0031718-d573-4e85-928e-10039f1fbb68' - }, - } -}; - -export default define(meta, async (ps, user) => { - const channel = await Channels.findOne({ - id: ps.channelId, - }); - - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); - } - - await ChannelFollowings.insert({ - id: genId(), - createdAt: new Date(), - followerId: user.id, - followeeId: channel.id, - }); - - publishUserEvent(user.id, 'followChannel', channel); -}); diff --git a/src/server/api/endpoints/channels/followed.ts b/src/server/api/endpoints/channels/followed.ts deleted file mode 100644 index be239a01d6..0000000000 --- a/src/server/api/endpoints/channels/followed.ts +++ /dev/null @@ -1,49 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { Channels, ChannelFollowings } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; - -export const meta = { - tags: ['channels', 'account'], - - requireCredential: true as const, - - kind: 'read:channels', - - params: { - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 5 - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Channel', - } - }, -}; - -export default define(meta, async (ps, me) => { - const query = makePaginationQuery(ChannelFollowings.createQueryBuilder(), ps.sinceId, ps.untilId) - .andWhere({ followerId: me.id }); - - const followings = await query - .take(ps.limit!) - .getMany(); - - return await Promise.all(followings.map(x => Channels.pack(x.followeeId, me))); -}); diff --git a/src/server/api/endpoints/channels/owned.ts b/src/server/api/endpoints/channels/owned.ts deleted file mode 100644 index 4a2e9db17b..0000000000 --- a/src/server/api/endpoints/channels/owned.ts +++ /dev/null @@ -1,49 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { Channels } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; - -export const meta = { - tags: ['channels', 'account'], - - requireCredential: true as const, - - kind: 'read:channels', - - params: { - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 5 - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Channel', - } - }, -}; - -export default define(meta, async (ps, me) => { - const query = makePaginationQuery(Channels.createQueryBuilder(), ps.sinceId, ps.untilId) - .andWhere({ userId: me.id }); - - const channels = await query - .take(ps.limit!) - .getMany(); - - return await Promise.all(channels.map(x => Channels.pack(x, me))); -}); diff --git a/src/server/api/endpoints/channels/pin-note.ts b/src/server/api/endpoints/channels/pin-note.ts deleted file mode 100644 index e69de29bb2..0000000000 --- a/src/server/api/endpoints/channels/pin-note.ts +++ /dev/null diff --git a/src/server/api/endpoints/channels/show.ts b/src/server/api/endpoints/channels/show.ts deleted file mode 100644 index 803ce6363d..0000000000 --- a/src/server/api/endpoints/channels/show.ts +++ /dev/null @@ -1,43 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { Channels } from '@/models/index'; - -export const meta = { - tags: ['channels'], - - requireCredential: false as const, - - params: { - channelId: { - validator: $.type(ID), - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Channel', - }, - - errors: { - noSuchChannel: { - message: 'No such channel.', - code: 'NO_SUCH_CHANNEL', - id: '6f6c314b-7486-4897-8966-c04a66a02923' - }, - } -}; - -export default define(meta, async (ps, me) => { - const channel = await Channels.findOne({ - id: ps.channelId, - }); - - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); - } - - return await Channels.pack(channel, me); -}); diff --git a/src/server/api/endpoints/channels/timeline.ts b/src/server/api/endpoints/channels/timeline.ts deleted file mode 100644 index 0ed057a11e..0000000000 --- a/src/server/api/endpoints/channels/timeline.ts +++ /dev/null @@ -1,85 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { Notes, Channels } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; -import { activeUsersChart } from '@/services/chart/index'; - -export const meta = { - tags: ['notes', 'channels'], - - requireCredential: false as const, - - params: { - channelId: { - validator: $.type(ID), - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10, - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - sinceDate: { - validator: $.optional.num, - }, - - untilDate: { - validator: $.optional.num, - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Note', - } - }, - - errors: { - noSuchChannel: { - message: 'No such channel.', - code: 'NO_SUCH_CHANNEL', - id: '4d0eeeba-a02c-4c3c-9966-ef60d38d2e7f' - } - } -}; - -export default define(meta, async (ps, user) => { - const channel = await Channels.findOne({ - id: ps.channelId, - }); - - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); - } - - //#region Construct query - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.channelId = :channelId', { channelId: channel.id }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); - //#endregion - - const timeline = await query.take(ps.limit!).getMany(); - - if (user) activeUsersChart.update(user); - - return await Notes.packMany(timeline, user); -}); diff --git a/src/server/api/endpoints/channels/unfollow.ts b/src/server/api/endpoints/channels/unfollow.ts deleted file mode 100644 index 700f8e93ba..0000000000 --- a/src/server/api/endpoints/channels/unfollow.ts +++ /dev/null @@ -1,45 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { Channels, ChannelFollowings } from '@/models/index'; -import { publishUserEvent } from '@/services/stream'; - -export const meta = { - tags: ['channels'], - - requireCredential: true as const, - - kind: 'write:channels', - - params: { - channelId: { - validator: $.type(ID), - }, - }, - - errors: { - noSuchChannel: { - message: 'No such channel.', - code: 'NO_SUCH_CHANNEL', - id: '19959ee9-0153-4c51-bbd9-a98c49dc59d6' - }, - } -}; - -export default define(meta, async (ps, user) => { - const channel = await Channels.findOne({ - id: ps.channelId, - }); - - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); - } - - await ChannelFollowings.delete({ - followerId: user.id, - followeeId: channel.id, - }); - - publishUserEvent(user.id, 'unfollowChannel', channel); -}); diff --git a/src/server/api/endpoints/channels/update.ts b/src/server/api/endpoints/channels/update.ts deleted file mode 100644 index 9b447bd04b..0000000000 --- a/src/server/api/endpoints/channels/update.ts +++ /dev/null @@ -1,94 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { Channels, DriveFiles } from '@/models/index'; - -export const meta = { - tags: ['channels'], - - requireCredential: true as const, - - kind: 'write:channels', - - params: { - channelId: { - validator: $.type(ID), - }, - - name: { - validator: $.optional.str.range(1, 128) - }, - - description: { - validator: $.nullable.optional.str.range(1, 2048) - }, - - bannerId: { - validator: $.nullable.optional.type(ID), - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Channel', - }, - - errors: { - noSuchChannel: { - message: 'No such channel.', - code: 'NO_SUCH_CHANNEL', - id: 'f9c5467f-d492-4c3c-9a8d-a70dacc86512' - }, - - accessDenied: { - message: 'You do not have edit privilege of the channel.', - code: 'ACCESS_DENIED', - id: '1fb7cb09-d46a-4fdf-b8df-057788cce513' - }, - - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: 'e86c14a4-0da2-4032-8df3-e737a04c7f3b' - }, - } -}; - -export default define(meta, async (ps, me) => { - const channel = await Channels.findOne({ - id: ps.channelId, - }); - - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); - } - - if (channel.userId !== me.id) { - throw new ApiError(meta.errors.accessDenied); - } - - // tslint:disable-next-line:no-unnecessary-initializer - let banner = undefined; - if (ps.bannerId != null) { - banner = await DriveFiles.findOne({ - id: ps.bannerId, - userId: me.id - }); - - if (banner == null) { - throw new ApiError(meta.errors.noSuchFile); - } - } else if (ps.bannerId === null) { - banner = null; - } - - await Channels.update(channel.id, { - ...(ps.name !== undefined ? { name: ps.name } : {}), - ...(ps.description !== undefined ? { description: ps.description } : {}), - ...(banner ? { bannerId: banner.id } : {}), - }); - - return await Channels.pack(channel.id, me); -}); diff --git a/src/server/api/endpoints/charts/active-users.ts b/src/server/api/endpoints/charts/active-users.ts deleted file mode 100644 index c4878f7d61..0000000000 --- a/src/server/api/endpoints/charts/active-users.ts +++ /dev/null @@ -1,30 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { convertLog } from '@/services/chart/core'; -import { activeUsersChart } from '@/services/chart/index'; - -export const meta = { - tags: ['charts', 'users'], - - params: { - span: { - validator: $.str.or(['day', 'hour']), - }, - - limit: { - validator: $.optional.num.range(1, 500), - default: 30, - }, - - offset: { - validator: $.optional.nullable.num, - default: null, - }, - }, - - res: convertLog(activeUsersChart.schema), -}; - -export default define(meta, async (ps) => { - return await activeUsersChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null); -}); diff --git a/src/server/api/endpoints/charts/drive.ts b/src/server/api/endpoints/charts/drive.ts deleted file mode 100644 index 07bff82cf4..0000000000 --- a/src/server/api/endpoints/charts/drive.ts +++ /dev/null @@ -1,30 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { convertLog } from '@/services/chart/core'; -import { driveChart } from '@/services/chart/index'; - -export const meta = { - tags: ['charts', 'drive'], - - params: { - span: { - validator: $.str.or(['day', 'hour']), - }, - - limit: { - validator: $.optional.num.range(1, 500), - default: 30, - }, - - offset: { - validator: $.optional.nullable.num, - default: null, - }, - }, - - res: convertLog(driveChart.schema), -}; - -export default define(meta, async (ps) => { - return await driveChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null); -}); diff --git a/src/server/api/endpoints/charts/federation.ts b/src/server/api/endpoints/charts/federation.ts deleted file mode 100644 index 9575f9a7b7..0000000000 --- a/src/server/api/endpoints/charts/federation.ts +++ /dev/null @@ -1,30 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { convertLog } from '@/services/chart/core'; -import { federationChart } from '@/services/chart/index'; - -export const meta = { - tags: ['charts'], - - params: { - span: { - validator: $.str.or(['day', 'hour']), - }, - - limit: { - validator: $.optional.num.range(1, 500), - default: 30, - }, - - offset: { - validator: $.optional.nullable.num, - default: null, - }, - }, - - res: convertLog(federationChart.schema), -}; - -export default define(meta, async (ps) => { - return await federationChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null); -}); diff --git a/src/server/api/endpoints/charts/hashtag.ts b/src/server/api/endpoints/charts/hashtag.ts deleted file mode 100644 index 53dc61e51e..0000000000 --- a/src/server/api/endpoints/charts/hashtag.ts +++ /dev/null @@ -1,34 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { convertLog } from '@/services/chart/core'; -import { hashtagChart } from '@/services/chart/index'; - -export const meta = { - tags: ['charts', 'hashtags'], - - params: { - span: { - validator: $.str.or(['day', 'hour']), - }, - - limit: { - validator: $.optional.num.range(1, 500), - default: 30, - }, - - offset: { - validator: $.optional.nullable.num, - default: null, - }, - - tag: { - validator: $.str, - }, - }, - - res: convertLog(hashtagChart.schema), -}; - -export default define(meta, async (ps) => { - return await hashtagChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.tag); -}); diff --git a/src/server/api/endpoints/charts/instance.ts b/src/server/api/endpoints/charts/instance.ts deleted file mode 100644 index 1835023188..0000000000 --- a/src/server/api/endpoints/charts/instance.ts +++ /dev/null @@ -1,34 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { convertLog } from '@/services/chart/core'; -import { instanceChart } from '@/services/chart/index'; - -export const meta = { - tags: ['charts'], - - params: { - span: { - validator: $.str.or(['day', 'hour']), - }, - - limit: { - validator: $.optional.num.range(1, 500), - default: 30, - }, - - offset: { - validator: $.optional.nullable.num, - default: null, - }, - - host: { - validator: $.str, - } - }, - - res: convertLog(instanceChart.schema), -}; - -export default define(meta, async (ps) => { - return await instanceChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.host); -}); diff --git a/src/server/api/endpoints/charts/network.ts b/src/server/api/endpoints/charts/network.ts deleted file mode 100644 index b524df93be..0000000000 --- a/src/server/api/endpoints/charts/network.ts +++ /dev/null @@ -1,30 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { convertLog } from '@/services/chart/core'; -import { networkChart } from '@/services/chart/index'; - -export const meta = { - tags: ['charts'], - - params: { - span: { - validator: $.str.or(['day', 'hour']), - }, - - limit: { - validator: $.optional.num.range(1, 500), - default: 30, - }, - - offset: { - validator: $.optional.nullable.num, - default: null, - }, - }, - - res: convertLog(networkChart.schema), -}; - -export default define(meta, async (ps) => { - return await networkChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null); -}); diff --git a/src/server/api/endpoints/charts/notes.ts b/src/server/api/endpoints/charts/notes.ts deleted file mode 100644 index 676f302939..0000000000 --- a/src/server/api/endpoints/charts/notes.ts +++ /dev/null @@ -1,30 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { convertLog } from '@/services/chart/core'; -import { notesChart } from '@/services/chart/index'; - -export const meta = { - tags: ['charts', 'notes'], - - params: { - span: { - validator: $.str.or(['day', 'hour']), - }, - - limit: { - validator: $.optional.num.range(1, 500), - default: 30, - }, - - offset: { - validator: $.optional.nullable.num, - default: null, - }, - }, - - res: convertLog(notesChart.schema), -}; - -export default define(meta, async (ps) => { - return await notesChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null); -}); diff --git a/src/server/api/endpoints/charts/user/drive.ts b/src/server/api/endpoints/charts/user/drive.ts deleted file mode 100644 index f2770e2df8..0000000000 --- a/src/server/api/endpoints/charts/user/drive.ts +++ /dev/null @@ -1,35 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { ID } from '@/misc/cafy-id'; -import { convertLog } from '@/services/chart/core'; -import { perUserDriveChart } from '@/services/chart/index'; - -export const meta = { - tags: ['charts', 'drive', 'users'], - - params: { - span: { - validator: $.str.or(['day', 'hour']), - }, - - limit: { - validator: $.optional.num.range(1, 500), - default: 30, - }, - - offset: { - validator: $.optional.nullable.num, - default: null, - }, - - userId: { - validator: $.type(ID), - } - }, - - res: convertLog(perUserDriveChart.schema), -}; - -export default define(meta, async (ps) => { - return await perUserDriveChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.userId); -}); diff --git a/src/server/api/endpoints/charts/user/following.ts b/src/server/api/endpoints/charts/user/following.ts deleted file mode 100644 index 8c97b63e89..0000000000 --- a/src/server/api/endpoints/charts/user/following.ts +++ /dev/null @@ -1,35 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { ID } from '@/misc/cafy-id'; -import { convertLog } from '@/services/chart/core'; -import { perUserFollowingChart } from '@/services/chart/index'; - -export const meta = { - tags: ['charts', 'users', 'following'], - - params: { - span: { - validator: $.str.or(['day', 'hour']), - }, - - limit: { - validator: $.optional.num.range(1, 500), - default: 30, - }, - - offset: { - validator: $.optional.nullable.num, - default: null, - }, - - userId: { - validator: $.type(ID), - } - }, - - res: convertLog(perUserFollowingChart.schema), -}; - -export default define(meta, async (ps) => { - return await perUserFollowingChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.userId); -}); diff --git a/src/server/api/endpoints/charts/user/notes.ts b/src/server/api/endpoints/charts/user/notes.ts deleted file mode 100644 index 0d5f5a8b6a..0000000000 --- a/src/server/api/endpoints/charts/user/notes.ts +++ /dev/null @@ -1,35 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { ID } from '@/misc/cafy-id'; -import { convertLog } from '@/services/chart/core'; -import { perUserNotesChart } from '@/services/chart/index'; - -export const meta = { - tags: ['charts', 'users', 'notes'], - - params: { - span: { - validator: $.str.or(['day', 'hour']), - }, - - limit: { - validator: $.optional.num.range(1, 500), - default: 30, - }, - - offset: { - validator: $.optional.nullable.num, - default: null, - }, - - userId: { - validator: $.type(ID), - } - }, - - res: convertLog(perUserNotesChart.schema), -}; - -export default define(meta, async (ps) => { - return await perUserNotesChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.userId); -}); diff --git a/src/server/api/endpoints/charts/user/reactions.ts b/src/server/api/endpoints/charts/user/reactions.ts deleted file mode 100644 index 3cabe40d56..0000000000 --- a/src/server/api/endpoints/charts/user/reactions.ts +++ /dev/null @@ -1,35 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { ID } from '@/misc/cafy-id'; -import { convertLog } from '@/services/chart/core'; -import { perUserReactionsChart } from '@/services/chart/index'; - -export const meta = { - tags: ['charts', 'users', 'reactions'], - - params: { - span: { - validator: $.str.or(['day', 'hour']), - }, - - limit: { - validator: $.optional.num.range(1, 500), - default: 30, - }, - - offset: { - validator: $.optional.nullable.num, - default: null, - }, - - userId: { - validator: $.type(ID), - } - }, - - res: convertLog(perUserReactionsChart.schema), -}; - -export default define(meta, async (ps) => { - return await perUserReactionsChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.userId); -}); diff --git a/src/server/api/endpoints/charts/users.ts b/src/server/api/endpoints/charts/users.ts deleted file mode 100644 index deac89b59d..0000000000 --- a/src/server/api/endpoints/charts/users.ts +++ /dev/null @@ -1,30 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { convertLog } from '@/services/chart/core'; -import { usersChart } from '@/services/chart/index'; - -export const meta = { - tags: ['charts', 'users'], - - params: { - span: { - validator: $.str.or(['day', 'hour']), - }, - - limit: { - validator: $.optional.num.range(1, 500), - default: 30, - }, - - offset: { - validator: $.optional.nullable.num, - default: null, - }, - }, - - res: convertLog(usersChart.schema), -}; - -export default define(meta, async (ps) => { - return await usersChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null); -}); diff --git a/src/server/api/endpoints/clips/add-note.ts b/src/server/api/endpoints/clips/add-note.ts deleted file mode 100644 index 79d7b8adde..0000000000 --- a/src/server/api/endpoints/clips/add-note.ts +++ /dev/null @@ -1,76 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ClipNotes, Clips } from '@/models/index'; -import { ApiError } from '../../error'; -import { genId } from '@/misc/gen-id'; -import { getNote } from '../../common/getters'; - -export const meta = { - tags: ['account', 'notes', 'clips'], - - requireCredential: true as const, - - kind: 'write:account', - - params: { - clipId: { - validator: $.type(ID), - }, - - noteId: { - validator: $.type(ID), - }, - }, - - errors: { - noSuchClip: { - message: 'No such clip.', - code: 'NO_SUCH_CLIP', - id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf' - }, - - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: 'fc8c0b49-c7a3-4664-a0a6-b418d386bb8b' - }, - - alreadyClipped: { - message: 'The note has already been clipped.', - code: 'ALREADY_CLIPPED', - id: '734806c4-542c-463a-9311-15c512803965' - }, - } -}; - -export default define(meta, async (ps, user) => { - const clip = await Clips.findOne({ - id: ps.clipId, - userId: user.id - }); - - if (clip == null) { - throw new ApiError(meta.errors.noSuchClip); - } - - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - const exist = await ClipNotes.findOne({ - noteId: note.id, - clipId: clip.id - }); - - if (exist != null) { - throw new ApiError(meta.errors.alreadyClipped); - } - - await ClipNotes.insert({ - id: genId(), - noteId: note.id, - clipId: clip.id - }); -}); diff --git a/src/server/api/endpoints/clips/create.ts b/src/server/api/endpoints/clips/create.ts deleted file mode 100644 index 02d2773709..0000000000 --- a/src/server/api/endpoints/clips/create.ts +++ /dev/null @@ -1,45 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { genId } from '@/misc/gen-id'; -import { Clips } from '@/models/index'; - -export const meta = { - tags: ['clips'], - - requireCredential: true as const, - - kind: 'write:account', - - params: { - name: { - validator: $.str.range(1, 100) - }, - - isPublic: { - validator: $.optional.bool - }, - - description: { - validator: $.optional.nullable.str.range(1, 2048) - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Clip' - } -}; - -export default define(meta, async (ps, user) => { - const clip = await Clips.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - name: ps.name, - isPublic: ps.isPublic, - description: ps.description, - }).then(x => Clips.findOneOrFail(x.identifiers[0])); - - return await Clips.pack(clip); -}); diff --git a/src/server/api/endpoints/clips/delete.ts b/src/server/api/endpoints/clips/delete.ts deleted file mode 100644 index ca489af3bf..0000000000 --- a/src/server/api/endpoints/clips/delete.ts +++ /dev/null @@ -1,40 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { Clips } from '@/models/index'; - -export const meta = { - tags: ['clips'], - - requireCredential: true as const, - - kind: 'write:account', - - params: { - clipId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchClip: { - message: 'No such clip.', - code: 'NO_SUCH_CLIP', - id: '70ca08ba-6865-4630-b6fb-8494759aa754' - } - } -}; - -export default define(meta, async (ps, user) => { - const clip = await Clips.findOne({ - id: ps.clipId, - userId: user.id - }); - - if (clip == null) { - throw new ApiError(meta.errors.noSuchClip); - } - - await Clips.delete(clip.id); -}); diff --git a/src/server/api/endpoints/clips/list.ts b/src/server/api/endpoints/clips/list.ts deleted file mode 100644 index 1f6db9b979..0000000000 --- a/src/server/api/endpoints/clips/list.ts +++ /dev/null @@ -1,28 +0,0 @@ -import define from '../../define'; -import { Clips } from '@/models/index'; - -export const meta = { - tags: ['clips', 'account'], - - requireCredential: true as const, - - kind: 'read:account', - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Clip' - } - } -}; - -export default define(meta, async (ps, me) => { - const clips = await Clips.find({ - userId: me.id, - }); - - return await Promise.all(clips.map(x => Clips.pack(x))); -}); diff --git a/src/server/api/endpoints/clips/notes.ts b/src/server/api/endpoints/clips/notes.ts deleted file mode 100644 index 5a9fed52fa..0000000000 --- a/src/server/api/endpoints/clips/notes.ts +++ /dev/null @@ -1,93 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ClipNotes, Clips, Notes } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; -import { ApiError } from '../../error'; -import { generateBlockedUserQuery } from '../../common/generate-block-query'; - -export const meta = { - tags: ['account', 'notes', 'clips'], - - requireCredential: false as const, - - kind: 'read:account', - - params: { - clipId: { - validator: $.type(ID), - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - }, - - errors: { - noSuchClip: { - message: 'No such clip.', - code: 'NO_SUCH_CLIP', - id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00' - } - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Note' - } - } -}; - -export default define(meta, async (ps, user) => { - const clip = await Clips.findOne({ - id: ps.clipId, - }); - - if (clip == null) { - throw new ApiError(meta.errors.noSuchClip); - } - - if (!clip.isPublic && (user == null || (clip.userId !== user.id))) { - throw new ApiError(meta.errors.noSuchClip); - } - - const clipQuery = ClipNotes.createQueryBuilder('joining') - .select('joining.noteId') - .where('joining.clipId = :clipId', { clipId: clip.id }); - - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere(`note.id IN (${ clipQuery.getQuery() })`) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .setParameters(clipQuery.getParameters()); - - if (user) { - generateVisibilityQuery(query, user); - generateMutedUserQuery(query, user); - generateBlockedUserQuery(query, user); - } - - const notes = await query - .take(ps.limit!) - .getMany(); - - return await Notes.packMany(notes, user); -}); diff --git a/src/server/api/endpoints/clips/show.ts b/src/server/api/endpoints/clips/show.ts deleted file mode 100644 index 8f245cd18e..0000000000 --- a/src/server/api/endpoints/clips/show.ts +++ /dev/null @@ -1,50 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { Clips } from '@/models/index'; - -export const meta = { - tags: ['clips', 'account'], - - requireCredential: false as const, - - kind: 'read:account', - - params: { - clipId: { - validator: $.type(ID), - }, - }, - - errors: { - noSuchClip: { - message: 'No such clip.', - code: 'NO_SUCH_CLIP', - id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20' - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Clip' - } -}; - -export default define(meta, async (ps, me) => { - // Fetch the clip - const clip = await Clips.findOne({ - id: ps.clipId, - }); - - if (clip == null) { - throw new ApiError(meta.errors.noSuchClip); - } - - if (!clip.isPublic && (me == null || (clip.userId !== me.id))) { - throw new ApiError(meta.errors.noSuchClip); - } - - return await Clips.pack(clip); -}); diff --git a/src/server/api/endpoints/clips/update.ts b/src/server/api/endpoints/clips/update.ts deleted file mode 100644 index 7f645560bb..0000000000 --- a/src/server/api/endpoints/clips/update.ts +++ /dev/null @@ -1,65 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { Clips } from '@/models/index'; - -export const meta = { - tags: ['clips'], - - requireCredential: true as const, - - kind: 'write:account', - - params: { - clipId: { - validator: $.type(ID), - }, - - name: { - validator: $.str.range(1, 100), - }, - - isPublic: { - validator: $.optional.bool - }, - - description: { - validator: $.optional.nullable.str.range(1, 2048) - } - }, - - errors: { - noSuchClip: { - message: 'No such clip.', - code: 'NO_SUCH_CLIP', - id: 'b4d92d70-b216-46fa-9a3f-a8c811699257' - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Clip' - } -}; - -export default define(meta, async (ps, user) => { - // Fetch the clip - const clip = await Clips.findOne({ - id: ps.clipId, - userId: user.id - }); - - if (clip == null) { - throw new ApiError(meta.errors.noSuchClip); - } - - await Clips.update(clip.id, { - name: ps.name, - description: ps.description, - isPublic: ps.isPublic, - }); - - return await Clips.pack(clip.id); -}); diff --git a/src/server/api/endpoints/drive.ts b/src/server/api/endpoints/drive.ts deleted file mode 100644 index 2974ccfab9..0000000000 --- a/src/server/api/endpoints/drive.ts +++ /dev/null @@ -1,38 +0,0 @@ -import define from '../define'; -import { fetchMeta } from '@/misc/fetch-meta'; -import { DriveFiles } from '@/models/index'; - -export const meta = { - tags: ['drive', 'account'], - - requireCredential: true as const, - - kind: 'read:drive', - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - capacity: { - type: 'number' as const, - optional: false as const, nullable: false as const, - }, - usage: { - type: 'number' as const, - optional: false as const, nullable: false as const, - } - } - } -}; - -export default define(meta, async (ps, user) => { - const instance = await fetchMeta(true); - - // Calculate drive usage - const usage = await DriveFiles.calcDriveUsageOf(user.id); - - return { - capacity: 1024 * 1024 * instance.localDriveCapacityMb, - usage: usage - }; -}); diff --git a/src/server/api/endpoints/drive/files.ts b/src/server/api/endpoints/drive/files.ts deleted file mode 100644 index 95435e1e43..0000000000 --- a/src/server/api/endpoints/drive/files.ts +++ /dev/null @@ -1,70 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { DriveFiles } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; - -export const meta = { - tags: ['drive'], - - requireCredential: true as const, - - kind: 'read:drive', - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - folderId: { - validator: $.optional.nullable.type(ID), - default: null, - }, - - type: { - validator: $.optional.nullable.str.match(/^[a-zA-Z\/\-*]+$/) - } - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'DriveFile', - } - }, -}; - -export default define(meta, async (ps, user) => { - const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId) - .andWhere('file.userId = :userId', { userId: user.id }); - - if (ps.folderId) { - query.andWhere('file.folderId = :folderId', { folderId: ps.folderId }); - } else { - query.andWhere('file.folderId IS NULL'); - } - - if (ps.type) { - if (ps.type.endsWith('/*')) { - query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' }); - } else { - query.andWhere('file.type = :type', { type: ps.type }); - } - } - - const files = await query.take(ps.limit!).getMany(); - - return await DriveFiles.packMany(files, { detail: false, self: true }); -}); diff --git a/src/server/api/endpoints/drive/files/attached-notes.ts b/src/server/api/endpoints/drive/files/attached-notes.ts deleted file mode 100644 index eec7d7877e..0000000000 --- a/src/server/api/endpoints/drive/files/attached-notes.ts +++ /dev/null @@ -1,57 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { DriveFiles, Notes } from '@/models/index'; - -export const meta = { - tags: ['drive', 'notes'], - - requireCredential: true as const, - - kind: 'read:drive', - - params: { - fileId: { - validator: $.type(ID), - } - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Note', - } - }, - - errors: { - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: 'c118ece3-2e4b-4296-99d1-51756e32d232', - } - } -}; - -export default define(meta, async (ps, user) => { - // Fetch file - const file = await DriveFiles.findOne({ - id: ps.fileId, - userId: user.id, - }); - - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } - - const notes = await Notes.createQueryBuilder('note') - .where(':file = ANY(note.fileIds)', { file: file.id }) - .getMany(); - - return await Notes.packMany(notes, user, { - detail: true - }); -}); diff --git a/src/server/api/endpoints/drive/files/check-existence.ts b/src/server/api/endpoints/drive/files/check-existence.ts deleted file mode 100644 index 2c36078421..0000000000 --- a/src/server/api/endpoints/drive/files/check-existence.ts +++ /dev/null @@ -1,31 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { DriveFiles } from '@/models/index'; - -export const meta = { - tags: ['drive'], - - requireCredential: true as const, - - kind: 'read:drive', - - params: { - md5: { - validator: $.str, - } - }, - - res: { - type: 'boolean' as const, - optional: false as const, nullable: false as const, - }, -}; - -export default define(meta, async (ps, user) => { - const file = await DriveFiles.findOne({ - md5: ps.md5, - userId: user.id, - }); - - return file != null; -}); diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts deleted file mode 100644 index 2abc104e6c..0000000000 --- a/src/server/api/endpoints/drive/files/create.ts +++ /dev/null @@ -1,89 +0,0 @@ -import * as ms from 'ms'; -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import create from '@/services/drive/add-file'; -import define from '../../../define'; -import { apiLogger } from '../../../logger'; -import { ApiError } from '../../../error'; -import { DriveFiles } from '@/models/index'; - -export const meta = { - tags: ['drive'], - - requireCredential: true as const, - - limit: { - duration: ms('1hour'), - max: 120 - }, - - requireFile: true, - - kind: 'write:drive', - - params: { - folderId: { - validator: $.optional.nullable.type(ID), - default: null, - }, - - name: { - validator: $.optional.nullable.str, - default: null, - }, - - isSensitive: { - validator: $.optional.either($.bool, $.str), - default: false, - transform: (v: any): boolean => v === true || v === 'true', - }, - - force: { - validator: $.optional.either($.bool, $.str), - default: false, - transform: (v: any): boolean => v === true || v === 'true', - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'DriveFile', - }, - - errors: { - invalidFileName: { - message: 'Invalid file name.', - code: 'INVALID_FILE_NAME', - id: 'f449b209-0c60-4e51-84d5-29486263bfd4' - } - } -}; - -export default define(meta, async (ps, user, _, file, cleanup) => { - // Get 'name' parameter - let name = ps.name || file.originalname; - if (name !== undefined && name !== null) { - name = name.trim(); - if (name.length === 0) { - name = null; - } else if (name === 'blob') { - name = null; - } else if (!DriveFiles.validateFileName(name)) { - throw new ApiError(meta.errors.invalidFileName); - } - } else { - name = null; - } - - try { - // Create file - const driveFile = await create(user, file.path, name, null, ps.folderId, ps.force, false, null, null, ps.isSensitive); - return await DriveFiles.pack(driveFile, { self: true }); - } catch (e) { - apiLogger.error(e); - throw new ApiError(); - } finally { - cleanup!(); - } -}); diff --git a/src/server/api/endpoints/drive/files/delete.ts b/src/server/api/endpoints/drive/files/delete.ts deleted file mode 100644 index 038325694d..0000000000 --- a/src/server/api/endpoints/drive/files/delete.ts +++ /dev/null @@ -1,53 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import { deleteFile } from '@/services/drive/delete-file'; -import { publishDriveStream } from '@/services/stream'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { DriveFiles } from '@/models/index'; - -export const meta = { - tags: ['drive'], - - requireCredential: true as const, - - kind: 'write:drive', - - params: { - fileId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: '908939ec-e52b-4458-b395-1025195cea58' - }, - - accessDenied: { - message: 'Access denied.', - code: 'ACCESS_DENIED', - id: '5eb8d909-2540-4970-90b8-dd6f86088121' - }, - } -}; - -export default define(meta, async (ps, user) => { - const file = await DriveFiles.findOne(ps.fileId); - - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } - - if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) { - throw new ApiError(meta.errors.accessDenied); - } - - // Delete - await deleteFile(file); - - // Publish fileDeleted event - publishDriveStream(user.id, 'fileDeleted', file.id); -}); diff --git a/src/server/api/endpoints/drive/files/find-by-hash.ts b/src/server/api/endpoints/drive/files/find-by-hash.ts deleted file mode 100644 index 5fea7bbbb0..0000000000 --- a/src/server/api/endpoints/drive/files/find-by-hash.ts +++ /dev/null @@ -1,36 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { DriveFiles } from '@/models/index'; - -export const meta = { - tags: ['drive'], - - requireCredential: true as const, - - kind: 'read:drive', - - params: { - md5: { - validator: $.str, - } - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'DriveFile', - } - }, -}; - -export default define(meta, async (ps, user) => { - const files = await DriveFiles.find({ - md5: ps.md5, - userId: user.id, - }); - - return await DriveFiles.packMany(files, { self: true }); -}); diff --git a/src/server/api/endpoints/drive/files/find.ts b/src/server/api/endpoints/drive/files/find.ts deleted file mode 100644 index dd419f4c04..0000000000 --- a/src/server/api/endpoints/drive/files/find.ts +++ /dev/null @@ -1,43 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { DriveFiles } from '@/models/index'; - -export const meta = { - requireCredential: true as const, - - tags: ['drive'], - - kind: 'read:drive', - - params: { - name: { - validator: $.str - }, - - folderId: { - validator: $.optional.nullable.type(ID), - default: null, - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'DriveFile', - } - }, -}; - -export default define(meta, async (ps, user) => { - const files = await DriveFiles.find({ - name: ps.name, - userId: user.id, - folderId: ps.folderId - }); - - return await Promise.all(files.map(file => DriveFiles.pack(file, { self: true }))); -}); diff --git a/src/server/api/endpoints/drive/files/show.ts b/src/server/api/endpoints/drive/files/show.ts deleted file mode 100644 index a96ebaa123..0000000000 --- a/src/server/api/endpoints/drive/files/show.ts +++ /dev/null @@ -1,84 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { DriveFile } from '@/models/entities/drive-file'; -import { DriveFiles } from '@/models/index'; - -export const meta = { - tags: ['drive'], - - requireCredential: true as const, - - kind: 'read:drive', - - params: { - fileId: { - validator: $.optional.type(ID), - }, - - url: { - validator: $.optional.str, - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'DriveFile', - }, - - errors: { - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: '067bc436-2718-4795-b0fb-ecbe43949e31' - }, - - accessDenied: { - message: 'Access denied.', - code: 'ACCESS_DENIED', - id: '25b73c73-68b1-41d0-bad1-381cfdf6579f' - }, - - fileIdOrUrlRequired: { - message: 'fileId or url required.', - code: 'INVALID_PARAM', - id: '89674805-722c-440c-8d88-5641830dc3e4' - } - } -}; - -export default define(meta, async (ps, user) => { - let file: DriveFile | undefined; - - if (ps.fileId) { - file = await DriveFiles.findOne(ps.fileId); - } else if (ps.url) { - file = await DriveFiles.findOne({ - where: [{ - url: ps.url - }, { - webpublicUrl: ps.url - }, { - thumbnailUrl: ps.url - }], - }); - } else { - throw new ApiError(meta.errors.fileIdOrUrlRequired); - } - - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } - - if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) { - throw new ApiError(meta.errors.accessDenied); - } - - return await DriveFiles.pack(file, { - detail: true, - withUser: true, - self: true - }); -}); diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts deleted file mode 100644 index f277a9c3dc..0000000000 --- a/src/server/api/endpoints/drive/files/update.ts +++ /dev/null @@ -1,116 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import { publishDriveStream } from '@/services/stream'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { DriveFiles, DriveFolders } from '@/models/index'; -import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits'; - -export const meta = { - tags: ['drive'], - - requireCredential: true as const, - - kind: 'write:drive', - - params: { - fileId: { - validator: $.type(ID), - }, - - folderId: { - validator: $.optional.nullable.type(ID), - default: undefined as any, - }, - - name: { - validator: $.optional.str.pipe(DriveFiles.validateFileName), - default: undefined as any, - }, - - isSensitive: { - validator: $.optional.bool, - default: undefined as any, - }, - - comment: { - validator: $.optional.nullable.str.max(DB_MAX_IMAGE_COMMENT_LENGTH), - default: undefined as any, - } - }, - - errors: { - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: 'e7778c7e-3af9-49cd-9690-6dbc3e6c972d' - }, - - accessDenied: { - message: 'Access denied.', - code: 'ACCESS_DENIED', - id: '01a53b27-82fc-445b-a0c1-b558465a8ed2' - }, - - noSuchFolder: { - message: 'No such folder.', - code: 'NO_SUCH_FOLDER', - id: 'ea8fb7a5-af77-4a08-b608-c0218176cd73' - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'DriveFile' - } -}; - -export default define(meta, async (ps, user) => { - const file = await DriveFiles.findOne(ps.fileId); - - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } - - if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) { - throw new ApiError(meta.errors.accessDenied); - } - - if (ps.name) file.name = ps.name; - - if (ps.comment !== undefined) file.comment = ps.comment; - - if (ps.isSensitive !== undefined) file.isSensitive = ps.isSensitive; - - if (ps.folderId !== undefined) { - if (ps.folderId === null) { - file.folderId = null; - } else { - const folder = await DriveFolders.findOne({ - id: ps.folderId, - userId: user.id - }); - - if (folder == null) { - throw new ApiError(meta.errors.noSuchFolder); - } - - file.folderId = folder.id; - } - } - - await DriveFiles.update(file.id, { - name: file.name, - comment: file.comment, - folderId: file.folderId, - isSensitive: file.isSensitive - }); - - const fileObj = await DriveFiles.pack(file, { self: true }); - - // Publish fileUpdated event - publishDriveStream(user.id, 'fileUpdated', fileObj); - - return fileObj; -}); diff --git a/src/server/api/endpoints/drive/files/upload-from-url.ts b/src/server/api/endpoints/drive/files/upload-from-url.ts deleted file mode 100644 index 9f10a42d24..0000000000 --- a/src/server/api/endpoints/drive/files/upload-from-url.ts +++ /dev/null @@ -1,64 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import * as ms from 'ms'; -import uploadFromUrl from '@/services/drive/upload-from-url'; -import define from '../../../define'; -import { DriveFiles } from '@/models/index'; -import { publishMainStream } from '@/services/stream'; -import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits'; - -export const meta = { - tags: ['drive'], - - limit: { - duration: ms('1hour'), - max: 60 - }, - - requireCredential: true as const, - - kind: 'write:drive', - - params: { - url: { - // TODO: Validate this url - validator: $.str, - }, - - folderId: { - validator: $.optional.nullable.type(ID), - default: null, - }, - - isSensitive: { - validator: $.optional.bool, - default: false, - }, - - comment: { - validator: $.optional.nullable.str.max(DB_MAX_IMAGE_COMMENT_LENGTH), - default: null, - }, - - marker: { - validator: $.optional.nullable.str, - default: null, - }, - - force: { - validator: $.optional.bool, - default: false, - } - } -}; - -export default define(meta, async (ps, user) => { - uploadFromUrl(ps.url, user, ps.folderId, null, ps.isSensitive, ps.force, false, ps.comment).then(file => { - DriveFiles.pack(file, { self: true }).then(packedFile => { - publishMainStream(user.id, 'urlUploadFinished', { - marker: ps.marker, - file: packedFile - }); - }); - }); -}); diff --git a/src/server/api/endpoints/drive/folders.ts b/src/server/api/endpoints/drive/folders.ts deleted file mode 100644 index 6f16878b13..0000000000 --- a/src/server/api/endpoints/drive/folders.ts +++ /dev/null @@ -1,58 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { DriveFolders } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; - -export const meta = { - tags: ['drive'], - - requireCredential: true as const, - - kind: 'read:drive', - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - folderId: { - validator: $.optional.nullable.type(ID), - default: null, - } - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'DriveFolder', - } - }, -}; - -export default define(meta, async (ps, user) => { - const query = makePaginationQuery(DriveFolders.createQueryBuilder('folder'), ps.sinceId, ps.untilId) - .andWhere('folder.userId = :userId', { userId: user.id }); - - if (ps.folderId) { - query.andWhere('folder.parentId = :parentId', { parentId: ps.folderId }); - } else { - query.andWhere('folder.parentId IS NULL'); - } - - const folders = await query.take(ps.limit!).getMany(); - - return await Promise.all(folders.map(folder => DriveFolders.pack(folder))); -}); diff --git a/src/server/api/endpoints/drive/folders/create.ts b/src/server/api/endpoints/drive/folders/create.ts deleted file mode 100644 index 80f96bd641..0000000000 --- a/src/server/api/endpoints/drive/folders/create.ts +++ /dev/null @@ -1,72 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import { publishDriveStream } from '@/services/stream'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { DriveFolders } from '@/models/index'; -import { genId } from '@/misc/gen-id'; - -export const meta = { - tags: ['drive'], - - requireCredential: true as const, - - kind: 'write:drive', - - params: { - name: { - validator: $.optional.str.pipe(DriveFolders.validateFolderName), - default: 'Untitled', - }, - - parentId: { - validator: $.optional.nullable.type(ID), - } - }, - - errors: { - noSuchFolder: { - message: 'No such folder.', - code: 'NO_SUCH_FOLDER', - id: '53326628-a00d-40a6-a3cd-8975105c0f95' - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'DriveFolder' - } -}; - -export default define(meta, async (ps, user) => { - // If the parent folder is specified - let parent = null; - if (ps.parentId) { - // Fetch parent folder - parent = await DriveFolders.findOne({ - id: ps.parentId, - userId: user.id - }); - - if (parent == null) { - throw new ApiError(meta.errors.noSuchFolder); - } - } - - // Create folder - const folder = await DriveFolders.insert({ - id: genId(), - createdAt: new Date(), - name: ps.name, - parentId: parent !== null ? parent.id : null, - userId: user.id - }).then(x => DriveFolders.findOneOrFail(x.identifiers[0])); - - const folderObj = await DriveFolders.pack(folder); - - // Publish folderCreated event - publishDriveStream(user.id, 'folderCreated', folderObj); - - return folderObj; -}); diff --git a/src/server/api/endpoints/drive/folders/delete.ts b/src/server/api/endpoints/drive/folders/delete.ts deleted file mode 100644 index 38b4aef103..0000000000 --- a/src/server/api/endpoints/drive/folders/delete.ts +++ /dev/null @@ -1,60 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { publishDriveStream } from '@/services/stream'; -import { ApiError } from '../../../error'; -import { DriveFolders, DriveFiles } from '@/models/index'; - -export const meta = { - tags: ['drive'], - - requireCredential: true as const, - - kind: 'write:drive', - - params: { - folderId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchFolder: { - message: 'No such folder.', - code: 'NO_SUCH_FOLDER', - id: '1069098f-c281-440f-b085-f9932edbe091' - }, - - hasChildFilesOrFolders: { - message: 'This folder has child files or folders.', - code: 'HAS_CHILD_FILES_OR_FOLDERS', - id: 'b0fc8a17-963c-405d-bfbc-859a487295e1' - }, - } -}; - -export default define(meta, async (ps, user) => { - // Get folder - const folder = await DriveFolders.findOne({ - id: ps.folderId, - userId: user.id - }); - - if (folder == null) { - throw new ApiError(meta.errors.noSuchFolder); - } - - const [childFoldersCount, childFilesCount] = await Promise.all([ - DriveFolders.count({ parentId: folder.id }), - DriveFiles.count({ folderId: folder.id }) - ]); - - if (childFoldersCount !== 0 || childFilesCount !== 0) { - throw new ApiError(meta.errors.hasChildFilesOrFolders); - } - - await DriveFolders.delete(folder.id); - - // Publish folderCreated event - publishDriveStream(user.id, 'folderDeleted', folder.id); -}); diff --git a/src/server/api/endpoints/drive/folders/find.ts b/src/server/api/endpoints/drive/folders/find.ts deleted file mode 100644 index a6c5a49988..0000000000 --- a/src/server/api/endpoints/drive/folders/find.ts +++ /dev/null @@ -1,43 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { DriveFolders } from '@/models/index'; - -export const meta = { - tags: ['drive'], - - requireCredential: true as const, - - kind: 'read:drive', - - params: { - name: { - validator: $.str - }, - - parentId: { - validator: $.optional.nullable.type(ID), - default: null, - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'DriveFolder', - } - }, -}; - -export default define(meta, async (ps, user) => { - const folders = await DriveFolders.find({ - name: ps.name, - userId: user.id, - parentId: ps.parentId - }); - - return await Promise.all(folders.map(folder => DriveFolders.pack(folder))); -}); diff --git a/src/server/api/endpoints/drive/folders/show.ts b/src/server/api/endpoints/drive/folders/show.ts deleted file mode 100644 index e907a24f05..0000000000 --- a/src/server/api/endpoints/drive/folders/show.ts +++ /dev/null @@ -1,49 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { DriveFolders } from '@/models/index'; - -export const meta = { - tags: ['drive'], - - requireCredential: true as const, - - kind: 'read:drive', - - params: { - folderId: { - validator: $.type(ID), - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'DriveFolder', - }, - - errors: { - noSuchFolder: { - message: 'No such folder.', - code: 'NO_SUCH_FOLDER', - id: 'd74ab9eb-bb09-4bba-bf24-fb58f761e1e9' - }, - } -}; - -export default define(meta, async (ps, user) => { - // Get folder - const folder = await DriveFolders.findOne({ - id: ps.folderId, - userId: user.id - }); - - if (folder == null) { - throw new ApiError(meta.errors.noSuchFolder); - } - - return await DriveFolders.pack(folder, { - detail: true - }); -}); diff --git a/src/server/api/endpoints/drive/folders/update.ts b/src/server/api/endpoints/drive/folders/update.ts deleted file mode 100644 index 612252e6df..0000000000 --- a/src/server/api/endpoints/drive/folders/update.ts +++ /dev/null @@ -1,123 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import { publishDriveStream } from '@/services/stream'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { DriveFolders } from '@/models/index'; - -export const meta = { - tags: ['drive'], - - requireCredential: true as const, - - kind: 'write:drive', - - params: { - folderId: { - validator: $.type(ID), - }, - - name: { - validator: $.optional.str.pipe(DriveFolders.validateFolderName), - }, - - parentId: { - validator: $.optional.nullable.type(ID), - } - }, - - errors: { - noSuchFolder: { - message: 'No such folder.', - code: 'NO_SUCH_FOLDER', - id: 'f7974dac-2c0d-4a27-926e-23583b28e98e' - }, - - noSuchParentFolder: { - message: 'No such parent folder.', - code: 'NO_SUCH_PARENT_FOLDER', - id: 'ce104e3a-faaf-49d5-b459-10ff0cbbcaa1' - }, - - recursiveNesting: { - message: 'It can not be structured like nesting folders recursively.', - code: 'NO_SUCH_PARENT_FOLDER', - id: 'ce104e3a-faaf-49d5-b459-10ff0cbbcaa1' - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'DriveFolder' - } -}; - -export default define(meta, async (ps, user) => { - // Fetch folder - const folder = await DriveFolders.findOne({ - id: ps.folderId, - userId: user.id - }); - - if (folder == null) { - throw new ApiError(meta.errors.noSuchFolder); - } - - if (ps.name) folder.name = ps.name; - - if (ps.parentId !== undefined) { - if (ps.parentId === folder.id) { - throw new ApiError(meta.errors.recursiveNesting); - } else if (ps.parentId === null) { - folder.parentId = null; - } else { - // Get parent folder - const parent = await DriveFolders.findOne({ - id: ps.parentId, - userId: user.id - }); - - if (parent == null) { - throw new ApiError(meta.errors.noSuchParentFolder); - } - - // Check if the circular reference will occur - async function checkCircle(folderId: any): Promise<boolean> { - // Fetch folder - const folder2 = await DriveFolders.findOne({ - id: folderId - }); - - if (folder2!.id === folder!.id) { - return true; - } else if (folder2!.parentId) { - return await checkCircle(folder2!.parentId); - } else { - return false; - } - } - - if (parent.parentId !== null) { - if (await checkCircle(parent.parentId)) { - throw new ApiError(meta.errors.recursiveNesting); - } - } - - folder.parentId = parent.id; - } - } - - // Update - DriveFolders.update(folder.id, { - name: folder.name, - parentId: folder.parentId - }); - - const folderObj = await DriveFolders.pack(folder); - - // Publish folderUpdated event - publishDriveStream(user.id, 'folderUpdated', folderObj); - - return folderObj; -}); diff --git a/src/server/api/endpoints/drive/stream.ts b/src/server/api/endpoints/drive/stream.ts deleted file mode 100644 index 141e02f748..0000000000 --- a/src/server/api/endpoints/drive/stream.ts +++ /dev/null @@ -1,59 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { DriveFiles } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; - -export const meta = { - tags: ['drive'], - - requireCredential: true as const, - - kind: 'read:drive', - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - type: { - validator: $.optional.str.match(/^[a-zA-Z\/\-*]+$/) - } - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'DriveFile', - } - }, -}; - -export default define(meta, async (ps, user) => { - const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId) - .andWhere('file.userId = :userId', { userId: user.id }); - - if (ps.type) { - if (ps.type.endsWith('/*')) { - query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' }); - } else { - query.andWhere('file.type = :type', { type: ps.type }); - } - } - - const files = await query.take(ps.limit!).getMany(); - - return await DriveFiles.packMany(files, { detail: false, self: true }); -}); diff --git a/src/server/api/endpoints/email-address/available.ts b/src/server/api/endpoints/email-address/available.ts deleted file mode 100644 index f6fccd59b0..0000000000 --- a/src/server/api/endpoints/email-address/available.ts +++ /dev/null @@ -1,34 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { validateEmailForAccount } from '@/services/validate-email-for-account'; - -export const meta = { - tags: ['users'], - - requireCredential: false as const, - - params: { - emailAddress: { - validator: $.str - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - available: { - type: 'boolean' as const, - optional: false as const, nullable: false as const, - }, - reason: { - type: 'string' as const, - optional: false as const, nullable: true as const, - }, - } - } -}; - -export default define(meta, async (ps) => { - return await validateEmailForAccount(ps.emailAddress); -}); diff --git a/src/server/api/endpoints/endpoint.ts b/src/server/api/endpoints/endpoint.ts deleted file mode 100644 index 1a04d8bee8..0000000000 --- a/src/server/api/endpoints/endpoint.ts +++ /dev/null @@ -1,26 +0,0 @@ -import $ from 'cafy'; -import define from '../define'; -import endpoints from '../endpoints'; - -export const meta = { - requireCredential: false as const, - - tags: ['meta'], - - params: { - endpoint: { - validator: $.str, - } - }, -}; - -export default define(meta, async (ps) => { - const ep = endpoints.find(x => x.name === ps.endpoint); - if (ep == null) return null; - return { - params: Object.entries(ep.meta.params || {}).map(([k, v]) => ({ - name: k, - type: v.validator.name === 'ID' ? 'String' : v.validator.name - })) - }; -}); diff --git a/src/server/api/endpoints/endpoints.ts b/src/server/api/endpoints/endpoints.ts deleted file mode 100644 index f7b9757d8d..0000000000 --- a/src/server/api/endpoints/endpoints.ts +++ /dev/null @@ -1,30 +0,0 @@ -import define from '../define'; -import endpoints from '../endpoints'; - -export const meta = { - requireCredential: false as const, - - tags: ['meta'], - - params: { - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'string' as const, - optional: false as const, nullable: false as const - }, - example: [ - 'admin/abuse-user-reports', - 'admin/accounts/create', - 'admin/announcements/create', - '...' - ] - } -}; - -export default define(meta, async () => { - return endpoints.map(x => x.name); -}); diff --git a/src/server/api/endpoints/federation/dns.ts b/src/server/api/endpoints/federation/dns.ts deleted file mode 100644 index 7ba566301a..0000000000 --- a/src/server/api/endpoints/federation/dns.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { promises as dns } from 'dns'; -import $ from 'cafy'; -import define from '../../define'; -import { Instances } from '@/models/index'; -import { toPuny } from '@/misc/convert-host'; - -const resolver = new dns.Resolver(); -resolver.setServers(['1.1.1.1']); - -export const meta = { - tags: ['federation'], - - requireCredential: false as const, - - params: { - host: { - validator: $.str - } - }, -}; - -export default define(meta, async (ps, me) => { - const instance = await Instances.findOneOrFail({ host: toPuny(ps.host) }); - - const [ - resolved4, - resolved6, - resolvedCname, - resolvedTxt, - ] = await Promise.all([ - resolver.resolve4(instance.host).catch(() => []), - resolver.resolve6(instance.host).catch(() => []), - resolver.resolveCname(instance.host).catch(() => []), - resolver.resolveTxt(instance.host).catch(() => []), - ]); - - return { - a: resolved4, - aaaa: resolved6, - cname: resolvedCname, - txt: resolvedTxt, - }; -}); diff --git a/src/server/api/endpoints/federation/followers.ts b/src/server/api/endpoints/federation/followers.ts deleted file mode 100644 index 655e7b7b9a..0000000000 --- a/src/server/api/endpoints/federation/followers.ts +++ /dev/null @@ -1,51 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { Followings } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; - -export const meta = { - tags: ['federation'], - - requireCredential: false as const, - - params: { - host: { - validator: $.str - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Following', - } - }, -}; - -export default define(meta, async (ps, me) => { - const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) - .andWhere(`following.followeeHost = :host`, { host: ps.host }); - - const followings = await query - .take(ps.limit!) - .getMany(); - - return await Followings.packMany(followings, me, { populateFollowee: true }); -}); diff --git a/src/server/api/endpoints/federation/following.ts b/src/server/api/endpoints/federation/following.ts deleted file mode 100644 index 5b283581a6..0000000000 --- a/src/server/api/endpoints/federation/following.ts +++ /dev/null @@ -1,51 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { Followings } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; - -export const meta = { - tags: ['federation'], - - requireCredential: false as const, - - params: { - host: { - validator: $.str - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Following', - } - }, -}; - -export default define(meta, async (ps, me) => { - const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) - .andWhere(`following.followerHost = :host`, { host: ps.host }); - - const followings = await query - .take(ps.limit!) - .getMany(); - - return await Followings.packMany(followings, me, { populateFollowee: true }); -}); diff --git a/src/server/api/endpoints/federation/instances.ts b/src/server/api/endpoints/federation/instances.ts deleted file mode 100644 index cf5e44ebd5..0000000000 --- a/src/server/api/endpoints/federation/instances.ts +++ /dev/null @@ -1,149 +0,0 @@ -import $ from 'cafy'; -import config from '@/config/index'; -import define from '../../define'; -import { Instances } from '@/models/index'; -import { fetchMeta } from '@/misc/fetch-meta'; - -export const meta = { - tags: ['federation'], - - requireCredential: false as const, - - params: { - host: { - validator: $.optional.nullable.str, - }, - - blocked: { - validator: $.optional.nullable.bool, - }, - - notResponding: { - validator: $.optional.nullable.bool, - }, - - suspended: { - validator: $.optional.nullable.bool, - }, - - federating: { - validator: $.optional.nullable.bool, - }, - - subscribing: { - validator: $.optional.nullable.bool, - }, - - publishing: { - validator: $.optional.nullable.bool, - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 30 - }, - - offset: { - validator: $.optional.num.min(0), - default: 0 - }, - - sort: { - validator: $.optional.str, - } - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'FederationInstance' - } - } -}; - -export default define(meta, async (ps, me) => { - const query = Instances.createQueryBuilder('instance'); - - switch (ps.sort) { - case '+pubSub': query.orderBy('instance.followingCount', 'DESC').orderBy('instance.followersCount', 'DESC'); break; - case '-pubSub': query.orderBy('instance.followingCount', 'ASC').orderBy('instance.followersCount', 'ASC'); break; - case '+notes': query.orderBy('instance.notesCount', 'DESC'); break; - case '-notes': query.orderBy('instance.notesCount', 'ASC'); break; - case '+users': query.orderBy('instance.usersCount', 'DESC'); break; - case '-users': query.orderBy('instance.usersCount', 'ASC'); break; - case '+following': query.orderBy('instance.followingCount', 'DESC'); break; - case '-following': query.orderBy('instance.followingCount', 'ASC'); break; - case '+followers': query.orderBy('instance.followersCount', 'DESC'); break; - case '-followers': query.orderBy('instance.followersCount', 'ASC'); break; - case '+caughtAt': query.orderBy('instance.caughtAt', 'DESC'); break; - case '-caughtAt': query.orderBy('instance.caughtAt', 'ASC'); break; - case '+lastCommunicatedAt': query.orderBy('instance.lastCommunicatedAt', 'DESC'); break; - case '-lastCommunicatedAt': query.orderBy('instance.lastCommunicatedAt', 'ASC'); break; - case '+driveUsage': query.orderBy('instance.driveUsage', 'DESC'); break; - case '-driveUsage': query.orderBy('instance.driveUsage', 'ASC'); break; - case '+driveFiles': query.orderBy('instance.driveFiles', 'DESC'); break; - case '-driveFiles': query.orderBy('instance.driveFiles', 'ASC'); break; - - default: query.orderBy('instance.id', 'DESC'); break; - } - - if (typeof ps.blocked === 'boolean') { - const meta = await fetchMeta(true); - if (ps.blocked) { - query.andWhere('instance.host IN (:...blocks)', { blocks: meta.blockedHosts }); - } else { - query.andWhere('instance.host NOT IN (:...blocks)', { blocks: meta.blockedHosts }); - } - } - - if (typeof ps.notResponding === 'boolean') { - if (ps.notResponding) { - query.andWhere('instance.isNotResponding = TRUE'); - } else { - query.andWhere('instance.isNotResponding = FALSE'); - } - } - - if (typeof ps.suspended === 'boolean') { - if (ps.suspended) { - query.andWhere('instance.isSuspended = TRUE'); - } else { - query.andWhere('instance.isSuspended = FALSE'); - } - } - - if (typeof ps.federating === 'boolean') { - if (ps.federating) { - query.andWhere('((instance.followingCount > 0) OR (instance.followersCount > 0))'); - } else { - query.andWhere('((instance.followingCount = 0) AND (instance.followersCount = 0))'); - } - } - - if (typeof ps.subscribing === 'boolean') { - if (ps.subscribing) { - query.andWhere('instance.followersCount > 0'); - } else { - query.andWhere('instance.followersCount = 0'); - } - } - - if (typeof ps.publishing === 'boolean') { - if (ps.publishing) { - query.andWhere('instance.followingCount > 0'); - } else { - query.andWhere('instance.followingCount = 0'); - } - } - - if (ps.host) { - query.andWhere('instance.host like :host', { host: '%' + ps.host.toLowerCase() + '%' }); - } - - const instances = await query.take(ps.limit!).skip(ps.offset).getMany(); - - return instances; -}); diff --git a/src/server/api/endpoints/federation/show-instance.ts b/src/server/api/endpoints/federation/show-instance.ts deleted file mode 100644 index f8352aefb3..0000000000 --- a/src/server/api/endpoints/federation/show-instance.ts +++ /dev/null @@ -1,29 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { Instances } from '@/models/index'; -import { toPuny } from '@/misc/convert-host'; - -export const meta = { - tags: ['federation'], - - requireCredential: false as const, - - params: { - host: { - validator: $.str - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'FederationInstance' - } -}; - -export default define(meta, async (ps, me) => { - const instance = await Instances - .findOne({ host: toPuny(ps.host) }); - - return instance; -}); diff --git a/src/server/api/endpoints/federation/update-remote-user.ts b/src/server/api/endpoints/federation/update-remote-user.ts deleted file mode 100644 index 580c3cb3d9..0000000000 --- a/src/server/api/endpoints/federation/update-remote-user.ts +++ /dev/null @@ -1,22 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { getRemoteUser } from '../../common/getters'; -import { updatePerson } from '@/remote/activitypub/models/person'; - -export const meta = { - tags: ['federation'], - - requireCredential: true as const, - - params: { - userId: { - validator: $.type(ID), - }, - } -}; - -export default define(meta, async (ps) => { - const user = await getRemoteUser(ps.userId); - await updatePerson(user.uri!); -}); diff --git a/src/server/api/endpoints/federation/users.ts b/src/server/api/endpoints/federation/users.ts deleted file mode 100644 index 0e35df3e1c..0000000000 --- a/src/server/api/endpoints/federation/users.ts +++ /dev/null @@ -1,51 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { Users } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; - -export const meta = { - tags: ['federation'], - - requireCredential: false as const, - - params: { - host: { - validator: $.str - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User', - } - }, -}; - -export default define(meta, async (ps, me) => { - const query = makePaginationQuery(Users.createQueryBuilder('user'), ps.sinceId, ps.untilId) - .andWhere(`user.host = :host`, { host: ps.host }); - - const users = await query - .take(ps.limit!) - .getMany(); - - return await Users.packMany(users, me, { detail: true }); -}); diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts deleted file mode 100644 index ba9ca1092d..0000000000 --- a/src/server/api/endpoints/following/create.ts +++ /dev/null @@ -1,100 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import * as ms from 'ms'; -import create from '@/services/following/create'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { getUser } from '../../common/getters'; -import { Followings, Users } from '@/models/index'; - -export const meta = { - tags: ['following', 'users'], - - limit: { - duration: ms('1hour'), - max: 100 - }, - - requireCredential: true as const, - - kind: 'write:following', - - params: { - userId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5' - }, - - followeeIsYourself: { - message: 'Followee is yourself.', - code: 'FOLLOWEE_IS_YOURSELF', - id: '26fbe7bb-a331-4857-af17-205b426669a9' - }, - - alreadyFollowing: { - message: 'You are already following that user.', - code: 'ALREADY_FOLLOWING', - id: '35387507-38c7-4cb9-9197-300b93783fa0' - }, - - blocking: { - message: 'You are blocking that user.', - code: 'BLOCKING', - id: '4e2206ec-aa4f-4960-b865-6c23ac38e2d9' - }, - - blocked: { - message: 'You are blocked by that user.', - code: 'BLOCKED', - id: 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0' - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User' - } -}; - -export default define(meta, async (ps, user) => { - const follower = user; - - // 自分自身 - if (user.id === ps.userId) { - throw new ApiError(meta.errors.followeeIsYourself); - } - - // Get followee - const followee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - // Check if already following - const exist = await Followings.findOne({ - followerId: follower.id, - followeeId: followee.id - }); - - if (exist != null) { - throw new ApiError(meta.errors.alreadyFollowing); - } - - try { - await create(follower, followee); - } catch (e) { - if (e.id === '710e8fb0-b8c3-4922-be49-d5d93d8e6a6e') throw new ApiError(meta.errors.blocking); - if (e.id === '3338392a-f764-498d-8855-db939dcf8c48') throw new ApiError(meta.errors.blocked); - throw e; - } - - return await Users.pack(followee.id, user); -}); diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts deleted file mode 100644 index 0b0158b86e..0000000000 --- a/src/server/api/endpoints/following/delete.ts +++ /dev/null @@ -1,82 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import * as ms from 'ms'; -import deleteFollowing from '@/services/following/delete'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { getUser } from '../../common/getters'; -import { Followings, Users } from '@/models/index'; - -export const meta = { - tags: ['following', 'users'], - - limit: { - duration: ms('1hour'), - max: 100 - }, - - requireCredential: true as const, - - kind: 'write:following', - - params: { - userId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '5b12c78d-2b28-4dca-99d2-f56139b42ff8' - }, - - followeeIsYourself: { - message: 'Followee is yourself.', - code: 'FOLLOWEE_IS_YOURSELF', - id: 'd9e400b9-36b0-4808-b1d8-79e707f1296c' - }, - - notFollowing: { - message: 'You are not following that user.', - code: 'NOT_FOLLOWING', - id: '5dbf82f5-c92b-40b1-87d1-6c8c0741fd09' - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User' - } -}; - -export default define(meta, async (ps, user) => { - const follower = user; - - // Check if the followee is yourself - if (user.id === ps.userId) { - throw new ApiError(meta.errors.followeeIsYourself); - } - - // Get followee - const followee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - // Check not following - const exist = await Followings.findOne({ - followerId: follower.id, - followeeId: followee.id - }); - - if (exist == null) { - throw new ApiError(meta.errors.notFollowing); - } - - await deleteFollowing(follower, followee); - - return await Users.pack(followee.id, user); -}); diff --git a/src/server/api/endpoints/following/requests/accept.ts b/src/server/api/endpoints/following/requests/accept.ts deleted file mode 100644 index af39ea1d90..0000000000 --- a/src/server/api/endpoints/following/requests/accept.ts +++ /dev/null @@ -1,48 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import acceptFollowRequest from '@/services/following/requests/accept'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { getUser } from '../../../common/getters'; - -export const meta = { - tags: ['following', 'account'], - - requireCredential: true as const, - - kind: 'write:following', - - params: { - userId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '66ce1645-d66c-46bb-8b79-96739af885bd' - }, - noFollowRequest: { - message: 'No follow request.', - code: 'NO_FOLLOW_REQUEST', - id: 'bcde4f8b-0913-4614-8881-614e522fb041' - }, - } -}; - -export default define(meta, async (ps, user) => { - // Fetch follower - const follower = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - await acceptFollowRequest(user, follower).catch(e => { - if (e.id === '8884c2dd-5795-4ac9-b27e-6a01d38190f9') throw new ApiError(meta.errors.noFollowRequest); - throw e; - }); - - return; -}); diff --git a/src/server/api/endpoints/following/requests/cancel.ts b/src/server/api/endpoints/following/requests/cancel.ts deleted file mode 100644 index b69c9d2fe1..0000000000 --- a/src/server/api/endpoints/following/requests/cancel.ts +++ /dev/null @@ -1,58 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import cancelFollowRequest from '@/services/following/requests/cancel'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { getUser } from '../../../common/getters'; -import { Users } from '@/models/index'; - -export const meta = { - tags: ['following', 'account'], - - requireCredential: true as const, - - kind: 'write:following', - - params: { - userId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '4e68c551-fc4c-4e46-bb41-7d4a37bf9dab' - }, - - followRequestNotFound: { - message: 'Follow request not found.', - code: 'FOLLOW_REQUEST_NOT_FOUND', - id: '089b125b-d338-482a-9a09-e2622ac9f8d4' - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User' - } -}; - -export default define(meta, async (ps, user) => { - // Fetch followee - const followee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - try { - await cancelFollowRequest(followee, user); - } catch (e) { - if (e.id === '17447091-ce07-46dd-b331-c1fd4f15b1e7') throw new ApiError(meta.errors.followRequestNotFound); - throw e; - } - - return await Users.pack(followee.id, user); -}); diff --git a/src/server/api/endpoints/following/requests/list.ts b/src/server/api/endpoints/following/requests/list.ts deleted file mode 100644 index 84440ccac7..0000000000 --- a/src/server/api/endpoints/following/requests/list.ts +++ /dev/null @@ -1,44 +0,0 @@ -import define from '../../../define'; -import { FollowRequests } from '@/models/index'; - -export const meta = { - tags: ['following', 'account'], - - requireCredential: true as const, - - kind: 'read:following', - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id' - }, - follower: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User' - }, - followee: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User' - } - } - } - } -}; - -export default define(meta, async (ps, user) => { - const reqs = await FollowRequests.find({ - followeeId: user.id - }); - - return await Promise.all(reqs.map(req => FollowRequests.pack(req))); -}); diff --git a/src/server/api/endpoints/following/requests/reject.ts b/src/server/api/endpoints/following/requests/reject.ts deleted file mode 100644 index 620324361f..0000000000 --- a/src/server/api/endpoints/following/requests/reject.ts +++ /dev/null @@ -1,40 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import rejectFollowRequest from '@/services/following/requests/reject'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { getUser } from '../../../common/getters'; - -export const meta = { - tags: ['following', 'account'], - - requireCredential: true as const, - - kind: 'write:following', - - params: { - userId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: 'abc2ffa6-25b2-4380-ba99-321ff3a94555' - }, - } -}; - -export default define(meta, async (ps, user) => { - // Fetch follower - const follower = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - await rejectFollowRequest(user, follower); - - return; -}); diff --git a/src/server/api/endpoints/gallery/featured.ts b/src/server/api/endpoints/gallery/featured.ts deleted file mode 100644 index 30ef8cedec..0000000000 --- a/src/server/api/endpoints/gallery/featured.ts +++ /dev/null @@ -1,29 +0,0 @@ -import define from '../../define'; -import { GalleryPosts } from '@/models/index'; - -export const meta = { - tags: ['gallery'], - - requireCredential: false as const, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'GalleryPost', - } - }, -}; - -export default define(meta, async (ps, me) => { - const query = GalleryPosts.createQueryBuilder('post') - .andWhere('post.createdAt > :date', { date: new Date(Date.now() - (1000 * 60 * 60 * 24 * 3)) }) - .andWhere('post.likedCount > 0') - .orderBy('post.likedCount', 'DESC'); - - const posts = await query.take(10).getMany(); - - return await GalleryPosts.packMany(posts, me); -}); diff --git a/src/server/api/endpoints/gallery/popular.ts b/src/server/api/endpoints/gallery/popular.ts deleted file mode 100644 index 18449b9654..0000000000 --- a/src/server/api/endpoints/gallery/popular.ts +++ /dev/null @@ -1,28 +0,0 @@ -import define from '../../define'; -import { GalleryPosts } from '@/models/index'; - -export const meta = { - tags: ['gallery'], - - requireCredential: false as const, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'GalleryPost', - } - }, -}; - -export default define(meta, async (ps, me) => { - const query = GalleryPosts.createQueryBuilder('post') - .andWhere('post.likedCount > 0') - .orderBy('post.likedCount', 'DESC'); - - const posts = await query.take(10).getMany(); - - return await GalleryPosts.packMany(posts, me); -}); diff --git a/src/server/api/endpoints/gallery/posts.ts b/src/server/api/endpoints/gallery/posts.ts deleted file mode 100644 index 53d3236d2d..0000000000 --- a/src/server/api/endpoints/gallery/posts.ts +++ /dev/null @@ -1,43 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { makePaginationQuery } from '../../common/make-pagination-query'; -import { GalleryPosts } from '@/models/index'; - -export const meta = { - tags: ['gallery'], - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'GalleryPost', - } - }, -}; - -export default define(meta, async (ps, me) => { - const query = makePaginationQuery(GalleryPosts.createQueryBuilder('post'), ps.sinceId, ps.untilId) - .innerJoinAndSelect('post.user', 'user'); - - const posts = await query.take(ps.limit!).getMany(); - - return await GalleryPosts.packMany(posts, me); -}); diff --git a/src/server/api/endpoints/gallery/posts/create.ts b/src/server/api/endpoints/gallery/posts/create.ts deleted file mode 100644 index 38b487e6ea..0000000000 --- a/src/server/api/endpoints/gallery/posts/create.ts +++ /dev/null @@ -1,77 +0,0 @@ -import $ from 'cafy'; -import * as ms from 'ms'; -import define from '../../../define'; -import { ID } from '../../../../../misc/cafy-id'; -import { DriveFiles, GalleryPosts } from '@/models/index'; -import { genId } from '../../../../../misc/gen-id'; -import { GalleryPost } from '@/models/entities/gallery-post'; -import { ApiError } from '../../../error'; -import { DriveFile } from '@/models/entities/drive-file'; - -export const meta = { - tags: ['gallery'], - - requireCredential: true as const, - - kind: 'write:gallery', - - limit: { - duration: ms('1hour'), - max: 300 - }, - - params: { - title: { - validator: $.str.min(1), - }, - - description: { - validator: $.optional.nullable.str, - }, - - fileIds: { - validator: $.arr($.type(ID)).unique().range(1, 32), - }, - - isSensitive: { - validator: $.optional.bool, - default: false, - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'GalleryPost', - }, - - errors: { - - } -}; - -export default define(meta, async (ps, user) => { - const files = (await Promise.all(ps.fileIds.map(fileId => - DriveFiles.findOne({ - id: fileId, - userId: user.id - }) - ))).filter((file): file is DriveFile => file != null); - - if (files.length === 0) { - throw new Error(); - } - - const post = await GalleryPosts.insert(new GalleryPost({ - id: genId(), - createdAt: new Date(), - updatedAt: new Date(), - title: ps.title, - description: ps.description, - userId: user.id, - isSensitive: ps.isSensitive, - fileIds: files.map(file => file.id) - })).then(x => GalleryPosts.findOneOrFail(x.identifiers[0])); - - return await GalleryPosts.pack(post, user); -}); diff --git a/src/server/api/endpoints/gallery/posts/delete.ts b/src/server/api/endpoints/gallery/posts/delete.ts deleted file mode 100644 index e5b7c07f2f..0000000000 --- a/src/server/api/endpoints/gallery/posts/delete.ts +++ /dev/null @@ -1,40 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { GalleryPosts } from '@/models/index'; -import { ID } from '@/misc/cafy-id'; - -export const meta = { - tags: ['gallery'], - - requireCredential: true as const, - - kind: 'write:gallery', - - params: { - postId: { - validator: $.type(ID), - }, - }, - - errors: { - noSuchPost: { - message: 'No such post.', - code: 'NO_SUCH_POST', - id: 'ae52f367-4bd7-4ecd-afc6-5672fff427f5' - }, - } -}; - -export default define(meta, async (ps, user) => { - const post = await GalleryPosts.findOne({ - id: ps.postId, - userId: user.id, - }); - - if (post == null) { - throw new ApiError(meta.errors.noSuchPost); - } - - await GalleryPosts.delete(post.id); -}); diff --git a/src/server/api/endpoints/gallery/posts/like.ts b/src/server/api/endpoints/gallery/posts/like.ts deleted file mode 100644 index 81a25c0ad1..0000000000 --- a/src/server/api/endpoints/gallery/posts/like.ts +++ /dev/null @@ -1,71 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { GalleryPosts, GalleryLikes } from '@/models/index'; -import { genId } from '@/misc/gen-id'; - -export const meta = { - tags: ['gallery'], - - requireCredential: true as const, - - kind: 'write:gallery-likes', - - params: { - postId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchPost: { - message: 'No such post.', - code: 'NO_SUCH_POST', - id: '56c06af3-1287-442f-9701-c93f7c4a62ff' - }, - - yourPost: { - message: 'You cannot like your post.', - code: 'YOUR_POST', - id: 'f78f1511-5ebc-4478-a888-1198d752da68' - }, - - alreadyLiked: { - message: 'The post has already been liked.', - code: 'ALREADY_LIKED', - id: '40e9ed56-a59c-473a-bf3f-f289c54fb5a7' - }, - } -}; - -export default define(meta, async (ps, user) => { - const post = await GalleryPosts.findOne(ps.postId); - if (post == null) { - throw new ApiError(meta.errors.noSuchPost); - } - - if (post.userId === user.id) { - throw new ApiError(meta.errors.yourPost); - } - - // if already liked - const exist = await GalleryLikes.findOne({ - postId: post.id, - userId: user.id - }); - - if (exist != null) { - throw new ApiError(meta.errors.alreadyLiked); - } - - // Create like - await GalleryLikes.insert({ - id: genId(), - createdAt: new Date(), - postId: post.id, - userId: user.id - }); - - GalleryPosts.increment({ id: post.id }, 'likedCount', 1); -}); diff --git a/src/server/api/endpoints/gallery/posts/show.ts b/src/server/api/endpoints/gallery/posts/show.ts deleted file mode 100644 index 93852a5f8d..0000000000 --- a/src/server/api/endpoints/gallery/posts/show.ts +++ /dev/null @@ -1,43 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { GalleryPosts } from '@/models/index'; - -export const meta = { - tags: ['gallery'], - - requireCredential: false as const, - - params: { - postId: { - validator: $.type(ID), - }, - }, - - errors: { - noSuchPost: { - message: 'No such post.', - code: 'NO_SUCH_POST', - id: '1137bf14-c5b0-4604-85bb-5b5371b1cd45' - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'GalleryPost' - } -}; - -export default define(meta, async (ps, me) => { - const post = await GalleryPosts.findOne({ - id: ps.postId, - }); - - if (post == null) { - throw new ApiError(meta.errors.noSuchPost); - } - - return await GalleryPosts.pack(post, me); -}); diff --git a/src/server/api/endpoints/gallery/posts/unlike.ts b/src/server/api/endpoints/gallery/posts/unlike.ts deleted file mode 100644 index 0347cdf79e..0000000000 --- a/src/server/api/endpoints/gallery/posts/unlike.ts +++ /dev/null @@ -1,54 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { GalleryPosts, GalleryLikes } from '@/models/index'; - -export const meta = { - tags: ['gallery'], - - requireCredential: true as const, - - kind: 'write:gallery-likes', - - params: { - postId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchPost: { - message: 'No such post.', - code: 'NO_SUCH_POST', - id: 'c32e6dd0-b555-4413-925e-b3757d19ed84' - }, - - notLiked: { - message: 'You have not liked that post.', - code: 'NOT_LIKED', - id: 'e3e8e06e-be37-41f7-a5b4-87a8250288f0' - }, - } -}; - -export default define(meta, async (ps, user) => { - const post = await GalleryPosts.findOne(ps.postId); - if (post == null) { - throw new ApiError(meta.errors.noSuchPost); - } - - const exist = await GalleryLikes.findOne({ - postId: post.id, - userId: user.id - }); - - if (exist == null) { - throw new ApiError(meta.errors.notLiked); - } - - // Delete like - await GalleryLikes.delete(exist.id); - - GalleryPosts.decrement({ id: post.id }, 'likedCount', 1); -}); diff --git a/src/server/api/endpoints/gallery/posts/update.ts b/src/server/api/endpoints/gallery/posts/update.ts deleted file mode 100644 index 54eea130d3..0000000000 --- a/src/server/api/endpoints/gallery/posts/update.ts +++ /dev/null @@ -1,82 +0,0 @@ -import $ from 'cafy'; -import * as ms from 'ms'; -import define from '../../../define'; -import { ID } from '../../../../../misc/cafy-id'; -import { DriveFiles, GalleryPosts } from '@/models/index'; -import { GalleryPost } from '@/models/entities/gallery-post'; -import { ApiError } from '../../../error'; -import { DriveFile } from '@/models/entities/drive-file'; - -export const meta = { - tags: ['gallery'], - - requireCredential: true as const, - - kind: 'write:gallery', - - limit: { - duration: ms('1hour'), - max: 300 - }, - - params: { - postId: { - validator: $.type(ID), - }, - - title: { - validator: $.str.min(1), - }, - - description: { - validator: $.optional.nullable.str, - }, - - fileIds: { - validator: $.arr($.type(ID)).unique().range(1, 32), - }, - - isSensitive: { - validator: $.optional.bool, - default: false, - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'GalleryPost', - }, - - errors: { - - } -}; - -export default define(meta, async (ps, user) => { - const files = (await Promise.all(ps.fileIds.map(fileId => - DriveFiles.findOne({ - id: fileId, - userId: user.id - }) - ))).filter((file): file is DriveFile => file != null); - - if (files.length === 0) { - throw new Error(); - } - - await GalleryPosts.update({ - id: ps.postId, - userId: user.id, - }, { - updatedAt: new Date(), - title: ps.title, - description: ps.description, - isSensitive: ps.isSensitive, - fileIds: files.map(file => file.id) - }); - - const post = await GalleryPosts.findOneOrFail(ps.postId); - - return await GalleryPosts.pack(post, user); -}); diff --git a/src/server/api/endpoints/games/reversi/games.ts b/src/server/api/endpoints/games/reversi/games.ts deleted file mode 100644 index 4db9ecb69f..0000000000 --- a/src/server/api/endpoints/games/reversi/games.ts +++ /dev/null @@ -1,156 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { ReversiGames } from '@/models/index'; -import { makePaginationQuery } from '../../../common/make-pagination-query'; -import { Brackets } from 'typeorm'; - -export const meta = { - tags: ['games'], - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - my: { - validator: $.optional.bool, - default: false - } - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id' - }, - createdAt: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'date-time' - }, - startedAt: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'date-time' - }, - isStarted: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - isEnded: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - form1: { - type: 'any' as const, - optional: false as const, nullable: true as const - }, - form2: { - type: 'any' as const, - optional: false as const, nullable: true as const - }, - user1Accepted: { - type: 'boolean' as const, - optional: false as const, nullable: false as const, - default: false - }, - user2Accepted: { - type: 'boolean' as const, - optional: false as const, nullable: false as const, - default: false - }, - user1Id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id' - }, - user2Id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id' - }, - user1: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User' - }, - user2: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User' - }, - winnerId: { - type: 'string' as const, - optional: false as const, nullable: true as const, - format: 'id' - }, - winner: { - type: 'object' as const, - optional: false as const, nullable: true as const, - ref: 'User' - }, - surrendered: { - type: 'string' as const, - optional: false as const, nullable: true as const, - format: 'id' - }, - black: { - type: 'number' as const, - optional: false as const, nullable: true as const - }, - bw: { - type: 'string' as const, - optional: false as const, nullable: false as const - }, - isLlotheo: { - type: 'boolean' as const, - optional: false as const, nullable: false as const, - }, - canPutEverywhere: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - loopedBoard: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - } - } - } - } -}; - -export default define(meta, async (ps, user) => { - const query = makePaginationQuery(ReversiGames.createQueryBuilder('game'), ps.sinceId, ps.untilId) - .andWhere('game.isStarted = TRUE'); - - if (ps.my && user) { - query.andWhere(new Brackets(qb => { qb - .where('game.user1Id = :userId', { userId: user.id }) - .orWhere('game.user2Id = :userId', { userId: user.id }); - })); - } - - // Fetch games - const games = await query.take(ps.limit!).getMany(); - - return await Promise.all(games.map((g) => ReversiGames.pack(g, user, { - detail: false - }))); -}); diff --git a/src/server/api/endpoints/games/reversi/games/show.ts b/src/server/api/endpoints/games/reversi/games/show.ts deleted file mode 100644 index 93afffdb1f..0000000000 --- a/src/server/api/endpoints/games/reversi/games/show.ts +++ /dev/null @@ -1,168 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import Reversi from '../../../../../../games/reversi/core'; -import define from '../../../../define'; -import { ApiError } from '../../../../error'; -import { ReversiGames } from '@/models/index'; - -export const meta = { - tags: ['games'], - - params: { - gameId: { - validator: $.type(ID), - }, - }, - - errors: { - noSuchGame: { - message: 'No such game.', - code: 'NO_SUCH_GAME', - id: 'f13a03db-fae1-46c9-87f3-43c8165419e1' - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id' - }, - createdAt: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'date-time' - }, - startedAt: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'date-time' - }, - isStarted: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - isEnded: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - form1: { - type: 'any' as const, - optional: false as const, nullable: true as const - }, - form2: { - type: 'any' as const, - optional: false as const, nullable: true as const - }, - user1Accepted: { - type: 'boolean' as const, - optional: false as const, nullable: false as const, - default: false - }, - user2Accepted: { - type: 'boolean' as const, - optional: false as const, nullable: false as const, - default: false - }, - user1Id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id' - }, - user2Id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id' - }, - user1: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User' - }, - user2: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User' - }, - winnerId: { - type: 'string' as const, - optional: false as const, nullable: true as const, - format: 'id' - }, - winner: { - type: 'object' as const, - optional: false as const, nullable: true as const, - ref: 'User' - }, - surrendered: { - type: 'string' as const, - optional: false as const, nullable: true as const, - format: 'id' - }, - black: { - type: 'number' as const, - optional: false as const, nullable: true as const - }, - bw: { - type: 'string' as const, - optional: false as const, nullable: false as const - }, - isLlotheo: { - type: 'boolean' as const, - optional: false as const, nullable: false as const, - }, - canPutEverywhere: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - loopedBoard: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - board: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'any' as const, - optional: false as const, nullable: false as const - } - }, - turn: { - type: 'any' as const, - optional: false as const, nullable: false as const - } - } - } - } -}; - -export default define(meta, async (ps, user) => { - const game = await ReversiGames.findOne(ps.gameId); - - if (game == null) { - throw new ApiError(meta.errors.noSuchGame); - } - - const o = new Reversi(game.map, { - isLlotheo: game.isLlotheo, - canPutEverywhere: game.canPutEverywhere, - loopedBoard: game.loopedBoard - }); - - for (const log of game.logs) { - o.put(log.color, log.pos); - } - - const packed = await ReversiGames.pack(game, user); - - return Object.assign({ - board: o.board, - turn: o.turn - }, packed); -}); diff --git a/src/server/api/endpoints/games/reversi/games/surrender.ts b/src/server/api/endpoints/games/reversi/games/surrender.ts deleted file mode 100644 index 00d58b19e3..0000000000 --- a/src/server/api/endpoints/games/reversi/games/surrender.ts +++ /dev/null @@ -1,67 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import { publishReversiGameStream } from '@/services/stream'; -import define from '../../../../define'; -import { ApiError } from '../../../../error'; -import { ReversiGames } from '@/models/index'; - -export const meta = { - tags: ['games'], - - requireCredential: true as const, - - params: { - gameId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchGame: { - message: 'No such game.', - code: 'NO_SUCH_GAME', - id: 'ace0b11f-e0a6-4076-a30d-e8284c81b2df' - }, - - alreadyEnded: { - message: 'That game has already ended.', - code: 'ALREADY_ENDED', - id: '6c2ad4a6-cbf1-4a5b-b187-b772826cfc6d' - }, - - accessDenied: { - message: 'Access denied.', - code: 'ACCESS_DENIED', - id: '6e04164b-a992-4c93-8489-2123069973e1' - }, - } -}; - -export default define(meta, async (ps, user) => { - const game = await ReversiGames.findOne(ps.gameId); - - if (game == null) { - throw new ApiError(meta.errors.noSuchGame); - } - - if (game.isEnded) { - throw new ApiError(meta.errors.alreadyEnded); - } - - if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) { - throw new ApiError(meta.errors.accessDenied); - } - - const winnerId = game.user1Id === user.id ? game.user2Id : game.user1Id; - - await ReversiGames.update(game.id, { - surrendered: user.id, - isEnded: true, - winnerId: winnerId - }); - - publishReversiGameStream(game.id, 'ended', { - winnerId: winnerId, - game: await ReversiGames.pack(game.id, user) - }); -}); diff --git a/src/server/api/endpoints/games/reversi/invitations.ts b/src/server/api/endpoints/games/reversi/invitations.ts deleted file mode 100644 index c8629377b2..0000000000 --- a/src/server/api/endpoints/games/reversi/invitations.ts +++ /dev/null @@ -1,58 +0,0 @@ -import define from '../../../define'; -import { ReversiMatchings } from '@/models/index'; - -export const meta = { - tags: ['games'], - - requireCredential: true as const, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id' - }, - createdAt: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'date-time' - }, - parentId: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id' - }, - parent: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User' - }, - childId: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id' - }, - child: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User' - } - } - } - } -}; - -export default define(meta, async (ps, user) => { - // Find session - const invitations = await ReversiMatchings.find({ - childId: user.id - }); - - return await Promise.all(invitations.map((i) => ReversiMatchings.pack(i, user))); -}); diff --git a/src/server/api/endpoints/games/reversi/match.ts b/src/server/api/endpoints/games/reversi/match.ts deleted file mode 100644 index 5ceb16c7d7..0000000000 --- a/src/server/api/endpoints/games/reversi/match.ts +++ /dev/null @@ -1,108 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import { publishMainStream, publishReversiStream } from '@/services/stream'; -import { eighteight } from '../../../../../games/reversi/maps'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { getUser } from '../../../common/getters'; -import { genId } from '@/misc/gen-id'; -import { ReversiMatchings, ReversiGames } from '@/models/index'; -import { ReversiGame } from '@/models/entities/games/reversi/game'; -import { ReversiMatching } from '@/models/entities/games/reversi/matching'; - -export const meta = { - tags: ['games'], - - requireCredential: true as const, - - params: { - userId: { - validator: $.type(ID), - }, - }, - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '0b4f0559-b484-4e31-9581-3f73cee89b28' - }, - - isYourself: { - message: 'Target user is yourself.', - code: 'TARGET_IS_YOURSELF', - id: '96fd7bd6-d2bc-426c-a865-d055dcd2828e' - }, - } -}; - -export default define(meta, async (ps, user) => { - // Myself - if (ps.userId === user.id) { - throw new ApiError(meta.errors.isYourself); - } - - // Find session - const exist = await ReversiMatchings.findOne({ - parentId: ps.userId, - childId: user.id - }); - - if (exist) { - // Destroy session - ReversiMatchings.delete(exist.id); - - // Create game - const game = await ReversiGames.save({ - id: genId(), - createdAt: new Date(), - user1Id: exist.parentId, - user2Id: user.id, - user1Accepted: false, - user2Accepted: false, - isStarted: false, - isEnded: false, - logs: [], - map: eighteight.data, - bw: 'random', - isLlotheo: false - } as Partial<ReversiGame>); - - publishReversiStream(exist.parentId, 'matched', await ReversiGames.pack(game, { id: exist.parentId })); - - const other = await ReversiMatchings.count({ - childId: user.id - }); - - if (other == 0) { - publishMainStream(user.id, 'reversiNoInvites'); - } - - return await ReversiGames.pack(game, user); - } else { - // Fetch child - const child = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - // 以前のセッションはすべて削除しておく - await ReversiMatchings.delete({ - parentId: user.id - }); - - // セッションを作成 - const matching = await ReversiMatchings.save({ - id: genId(), - createdAt: new Date(), - parentId: user.id, - childId: child.id - } as ReversiMatching); - - const packed = await ReversiMatchings.pack(matching, child); - publishReversiStream(child.id, 'invited', packed); - publishMainStream(child.id, 'reversiInvited', packed); - - return; - } -}); diff --git a/src/server/api/endpoints/games/reversi/match/cancel.ts b/src/server/api/endpoints/games/reversi/match/cancel.ts deleted file mode 100644 index e4a138bb87..0000000000 --- a/src/server/api/endpoints/games/reversi/match/cancel.ts +++ /dev/null @@ -1,14 +0,0 @@ -import define from '../../../../define'; -import { ReversiMatchings } from '@/models/index'; - -export const meta = { - tags: ['games'], - - requireCredential: true as const -}; - -export default define(meta, async (ps, user) => { - await ReversiMatchings.delete({ - parentId: user.id - }); -}); diff --git a/src/server/api/endpoints/get-online-users-count.ts b/src/server/api/endpoints/get-online-users-count.ts deleted file mode 100644 index 5c80d588d3..0000000000 --- a/src/server/api/endpoints/get-online-users-count.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { USER_ONLINE_THRESHOLD } from '@/const'; -import { Users } from '@/models/index'; -import { MoreThan } from 'typeorm'; -import define from '../define'; - -export const meta = { - tags: ['meta'], - - requireCredential: false as const, - - params: { - } -}; - -export default define(meta, async () => { - const count = await Users.count({ - lastActiveDate: MoreThan(new Date(Date.now() - USER_ONLINE_THRESHOLD)) - }); - - return { - count - }; -}); diff --git a/src/server/api/endpoints/hashtags/list.ts b/src/server/api/endpoints/hashtags/list.ts deleted file mode 100644 index 821016a50c..0000000000 --- a/src/server/api/endpoints/hashtags/list.ts +++ /dev/null @@ -1,95 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { Hashtags } from '@/models/index'; - -export const meta = { - tags: ['hashtags'], - - requireCredential: false as const, - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - attachedToUserOnly: { - validator: $.optional.bool, - default: false - }, - - attachedToLocalUserOnly: { - validator: $.optional.bool, - default: false - }, - - attachedToRemoteUserOnly: { - validator: $.optional.bool, - default: false - }, - - sort: { - validator: $.str.or([ - '+mentionedUsers', - '-mentionedUsers', - '+mentionedLocalUsers', - '-mentionedLocalUsers', - '+mentionedRemoteUsers', - '-mentionedRemoteUsers', - '+attachedUsers', - '-attachedUsers', - '+attachedLocalUsers', - '-attachedLocalUsers', - '+attachedRemoteUsers', - '-attachedRemoteUsers', - ]), - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Hashtag', - } - }, -}; - -export default define(meta, async (ps, me) => { - const query = Hashtags.createQueryBuilder('tag'); - - if (ps.attachedToUserOnly) query.andWhere('tag.attachedUsersCount != 0'); - if (ps.attachedToLocalUserOnly) query.andWhere('tag.attachedLocalUsersCount != 0'); - if (ps.attachedToRemoteUserOnly) query.andWhere('tag.attachedRemoteUsersCount != 0'); - - switch (ps.sort) { - case '+mentionedUsers': query.orderBy('tag.mentionedUsersCount', 'DESC'); break; - case '-mentionedUsers': query.orderBy('tag.mentionedUsersCount', 'ASC'); break; - case '+mentionedLocalUsers': query.orderBy('tag.mentionedLocalUsersCount', 'DESC'); break; - case '-mentionedLocalUsers': query.orderBy('tag.mentionedLocalUsersCount', 'ASC'); break; - case '+mentionedRemoteUsers': query.orderBy('tag.mentionedRemoteUsersCount', 'DESC'); break; - case '-mentionedRemoteUsers': query.orderBy('tag.mentionedRemoteUsersCount', 'ASC'); break; - case '+attachedUsers': query.orderBy('tag.attachedUsersCount', 'DESC'); break; - case '-attachedUsers': query.orderBy('tag.attachedUsersCount', 'ASC'); break; - case '+attachedLocalUsers': query.orderBy('tag.attachedLocalUsersCount', 'DESC'); break; - case '-attachedLocalUsers': query.orderBy('tag.attachedLocalUsersCount', 'ASC'); break; - case '+attachedRemoteUsers': query.orderBy('tag.attachedRemoteUsersCount', 'DESC'); break; - case '-attachedRemoteUsers': query.orderBy('tag.attachedRemoteUsersCount', 'ASC'); break; - } - - query.select([ - 'tag.name', - 'tag.mentionedUsersCount', - 'tag.mentionedLocalUsersCount', - 'tag.mentionedRemoteUsersCount', - 'tag.attachedUsersCount', - 'tag.attachedLocalUsersCount', - 'tag.attachedRemoteUsersCount', - ]); - - const tags = await query.take(ps.limit!).getMany(); - - return Hashtags.packMany(tags); -}); diff --git a/src/server/api/endpoints/hashtags/search.ts b/src/server/api/endpoints/hashtags/search.ts deleted file mode 100644 index fd0cac3983..0000000000 --- a/src/server/api/endpoints/hashtags/search.ts +++ /dev/null @@ -1,46 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { Hashtags } from '@/models/index'; - -export const meta = { - tags: ['hashtags'], - - requireCredential: false as const, - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10, - }, - - query: { - validator: $.str, - }, - - offset: { - validator: $.optional.num.min(0), - default: 0, - } - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'string' as const, - optional: false as const, nullable: false as const, - } - }, -}; - -export default define(meta, async (ps) => { - const hashtags = await Hashtags.createQueryBuilder('tag') - .where('tag.name like :q', { q: ps.query.toLowerCase() + '%' }) - .orderBy('tag.count', 'DESC') - .groupBy('tag.id') - .take(ps.limit!) - .skip(ps.offset) - .getMany(); - - return hashtags.map(tag => tag.name); -}); diff --git a/src/server/api/endpoints/hashtags/show.ts b/src/server/api/endpoints/hashtags/show.ts deleted file mode 100644 index f22edbfffd..0000000000 --- a/src/server/api/endpoints/hashtags/show.ts +++ /dev/null @@ -1,40 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { Hashtags } from '@/models/index'; -import { normalizeForSearch } from '@/misc/normalize-for-search'; - -export const meta = { - tags: ['hashtags'], - - requireCredential: false as const, - - params: { - tag: { - validator: $.str, - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Hashtag', - }, - - errors: { - noSuchHashtag: { - message: 'No such hashtag.', - code: 'NO_SUCH_HASHTAG', - id: '110ee688-193e-4a3a-9ecf-c167b2e6981e' - } - } -}; - -export default define(meta, async (ps, user) => { - const hashtag = await Hashtags.findOne({ name: normalizeForSearch(ps.tag) }); - if (hashtag == null) { - throw new ApiError(meta.errors.noSuchHashtag); - } - - return await Hashtags.pack(hashtag); -}); diff --git a/src/server/api/endpoints/hashtags/trend.ts b/src/server/api/endpoints/hashtags/trend.ts deleted file mode 100644 index 3d67241ab6..0000000000 --- a/src/server/api/endpoints/hashtags/trend.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { Brackets } from 'typeorm'; -import define from '../../define'; -import { fetchMeta } from '@/misc/fetch-meta'; -import { Notes } from '@/models/index'; -import { Note } from '@/models/entities/note'; -import { safeForSql } from '@/misc/safe-for-sql'; -import { normalizeForSearch } from '@/misc/normalize-for-search'; - -/* -トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前~今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要 -ユニーク投稿数とはそのハッシュタグと投稿ユーザーのペアのカウントで、例えば同じユーザーが複数回同じハッシュタグを投稿してもそのハッシュタグのユニーク投稿数は1とカウントされる - -..が理想だけどPostgreSQLでどうするのか分からないので単に「直近Aの内に投稿されたユニーク投稿数が多いハッシュタグ」で妥協する -*/ - -const rangeA = 1000 * 60 * 60; // 60分 -//const rangeB = 1000 * 60 * 120; // 2時間 -//const coefficient = 1.25; // 「n倍」の部分 -//const requiredUsers = 3; // 最低何人がそのタグを投稿している必要があるか - -const max = 5; - -export const meta = { - tags: ['hashtags'], - - requireCredential: false as const, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - tag: { - type: 'string' as const, - optional: false as const, nullable: false as const, - }, - chart: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'number' as const, - optional: false as const, nullable: false as const, - } - }, - usersCount: { - type: 'number' as const, - optional: false as const, nullable: false as const, - } - } - } - } -}; - -export default define(meta, async () => { - const instance = await fetchMeta(true); - const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t)); - - const now = new Date(); // 5分単位で丸めた現在日時 - now.setMinutes(Math.round(now.getMinutes() / 5) * 5, 0, 0); - - const tagNotes = await Notes.createQueryBuilder('note') - .where(`note.createdAt > :date`, { date: new Date(now.getTime() - rangeA) }) - .andWhere(new Brackets(qb => { qb - .where(`note.visibility = 'public'`) - .orWhere(`note.visibility = 'home'`); - })) - .andWhere(`note.tags != '{}'`) - .select(['note.tags', 'note.userId']) - .cache(60000) // 1 min - .getMany(); - - if (tagNotes.length === 0) { - return []; - } - - const tags: { - name: string; - users: Note['userId'][]; - }[] = []; - - for (const note of tagNotes) { - for (const tag of note.tags) { - if (hiddenTags.includes(tag)) continue; - - const x = tags.find(x => x.name === tag); - if (x) { - if (!x.users.includes(note.userId)) { - x.users.push(note.userId); - } - } else { - tags.push({ - name: tag, - users: [note.userId] - }); - } - } - } - - // タグを人気順に並べ替え - const hots = tags - .sort((a, b) => b.users.length - a.users.length) - .map(tag => tag.name) - .slice(0, max); - - //#region 2(または3)で話題と判定されたタグそれぞれについて過去の投稿数グラフを取得する - const countPromises: Promise<number[]>[] = []; - - const range = 20; - - // 10分 - const interval = 1000 * 60 * 10; - - for (let i = 0; i < range; i++) { - countPromises.push(Promise.all(hots.map(tag => Notes.createQueryBuilder('note') - .select('count(distinct note.userId)') - .where(`'{"${safeForSql(tag) ? tag : 'aichan_kawaii'}"}' <@ note.tags`) - .andWhere('note.createdAt < :lt', { lt: new Date(now.getTime() - (interval * i)) }) - .andWhere('note.createdAt > :gt', { gt: new Date(now.getTime() - (interval * (i + 1))) }) - .cache(60000) // 1 min - .getRawOne() - .then(x => parseInt(x.count, 10)) - ))); - } - - const countsLog = await Promise.all(countPromises); - //#endregion - - const totalCounts = await Promise.all(hots.map(tag => Notes.createQueryBuilder('note') - .select('count(distinct note.userId)') - .where(`'{"${safeForSql(tag) ? tag : 'aichan_kawaii'}"}' <@ note.tags`) - .andWhere('note.createdAt > :gt', { gt: new Date(now.getTime() - rangeA) }) - .cache(60000 * 60) // 60 min - .getRawOne() - .then(x => parseInt(x.count, 10)) - )); - - const stats = hots.map((tag, i) => ({ - tag, - chart: countsLog.map(counts => counts[i]), - usersCount: totalCounts[i] - })); - - return stats; -}); diff --git a/src/server/api/endpoints/hashtags/users.ts b/src/server/api/endpoints/hashtags/users.ts deleted file mode 100644 index 8c8cd1510b..0000000000 --- a/src/server/api/endpoints/hashtags/users.ts +++ /dev/null @@ -1,89 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { Users } from '@/models/index'; -import { normalizeForSearch } from '@/misc/normalize-for-search'; - -export const meta = { - requireCredential: false as const, - - tags: ['hashtags', 'users'], - - params: { - tag: { - validator: $.str, - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sort: { - validator: $.str.or([ - '+follower', - '-follower', - '+createdAt', - '-createdAt', - '+updatedAt', - '-updatedAt', - ]), - }, - - state: { - validator: $.optional.str.or([ - 'all', - 'alive' - ]), - default: 'all' - }, - - origin: { - validator: $.optional.str.or([ - 'combined', - 'local', - 'remote', - ]), - default: 'local' - } - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User', - } - }, -}; - -export default define(meta, async (ps, me) => { - const query = Users.createQueryBuilder('user') - .where(':tag = ANY(user.tags)', { tag: normalizeForSearch(ps.tag) }); - - const recent = new Date(Date.now() - (1000 * 60 * 60 * 24 * 5)); - - if (ps.state === 'alive') { - query.andWhere('user.updatedAt > :date', { date: recent }); - } - - if (ps.origin === 'local') { - query.andWhere('user.host IS NULL'); - } else if (ps.origin === 'remote') { - query.andWhere('user.host IS NOT NULL'); - } - - switch (ps.sort) { - case '+follower': query.orderBy('user.followersCount', 'DESC'); break; - case '-follower': query.orderBy('user.followersCount', 'ASC'); break; - case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break; - case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break; - case '+updatedAt': query.orderBy('user.updatedAt', 'DESC'); break; - case '-updatedAt': query.orderBy('user.updatedAt', 'ASC'); break; - } - - const users = await query.take(ps.limit!).getMany(); - - return await Users.packMany(users, me, { detail: true }); -}); diff --git a/src/server/api/endpoints/i.ts b/src/server/api/endpoints/i.ts deleted file mode 100644 index 0568a962d8..0000000000 --- a/src/server/api/endpoints/i.ts +++ /dev/null @@ -1,26 +0,0 @@ -import define from '../define'; -import { Users } from '@/models/index'; - -export const meta = { - tags: ['account'], - - requireCredential: true as const, - - params: {}, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User', - }, -}; - -export default define(meta, async (ps, user, token) => { - const isSecure = token == null; - - // ここで渡ってきている user はキャッシュされていて古い可能性もあるので id だけ渡す - return await Users.pack(user.id, user, { - detail: true, - includeSecrets: isSecure - }); -}); diff --git a/src/server/api/endpoints/i/2fa/done.ts b/src/server/api/endpoints/i/2fa/done.ts deleted file mode 100644 index 2bd2128cce..0000000000 --- a/src/server/api/endpoints/i/2fa/done.ts +++ /dev/null @@ -1,41 +0,0 @@ -import $ from 'cafy'; -import * as speakeasy from 'speakeasy'; -import define from '../../../define'; -import { UserProfiles } from '@/models/index'; - -export const meta = { - requireCredential: true as const, - - secure: true, - - params: { - token: { - validator: $.str - } - } -}; - -export default define(meta, async (ps, user) => { - const token = ps.token.replace(/\s/g, ''); - - const profile = await UserProfiles.findOneOrFail(user.id); - - 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 UserProfiles.update(user.id, { - twoFactorSecret: profile.twoFactorTempSecret, - twoFactorEnabled: true - }); -}); diff --git a/src/server/api/endpoints/i/2fa/key-done.ts b/src/server/api/endpoints/i/2fa/key-done.ts deleted file mode 100644 index b4d3af235a..0000000000 --- a/src/server/api/endpoints/i/2fa/key-done.ts +++ /dev/null @@ -1,150 +0,0 @@ -import $ from 'cafy'; -import * as bcrypt from 'bcryptjs'; -import { promisify } from 'util'; -import * as cbor from 'cbor'; -import define from '../../../define'; -import { - UserProfiles, - UserSecurityKeys, - AttestationChallenges, - Users -} from '@/models/index'; -import config from '@/config/index'; -import { procedures, hash } from '../../../2fa'; -import { publishMainStream } from '@/services/stream'; - -const cborDecodeFirst = promisify(cbor.decodeFirst) as any; - -export const meta = { - requireCredential: true as const, - - secure: true, - - params: { - clientDataJSON: { - validator: $.str - }, - attestationObject: { - validator: $.str - }, - password: { - validator: $.str - }, - challengeId: { - validator: $.str - }, - name: { - validator: $.str - } - } -}; - -const rpIdHashReal = hash(Buffer.from(config.hostname, 'utf-8')); - -export default define(meta, async (ps, user) => { - const profile = await UserProfiles.findOneOrFail(user.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 != 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]; - - // tslint: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.findOne({ - 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 - ) { - throw new Error('expired challenge'); - } - - const credentialIdString = credentialId.toString('hex'); - - await UserSecurityKeys.save({ - 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 - }; -}); diff --git a/src/server/api/endpoints/i/2fa/password-less.ts b/src/server/api/endpoints/i/2fa/password-less.ts deleted file mode 100644 index 064828b638..0000000000 --- a/src/server/api/endpoints/i/2fa/password-less.ts +++ /dev/null @@ -1,21 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { UserProfiles } from '@/models/index'; - -export const meta = { - requireCredential: true as const, - - secure: true, - - params: { - value: { - validator: $.boolean - } - } -}; - -export default define(meta, async (ps, user) => { - await UserProfiles.update(user.id, { - usePasswordLessLogin: ps.value - }); -}); diff --git a/src/server/api/endpoints/i/2fa/register-key.ts b/src/server/api/endpoints/i/2fa/register-key.ts deleted file mode 100644 index 1b385a10ee..0000000000 --- a/src/server/api/endpoints/i/2fa/register-key.ts +++ /dev/null @@ -1,59 +0,0 @@ -import $ from 'cafy'; -import * as bcrypt from 'bcryptjs'; -import define from '../../../define'; -import { UserProfiles, AttestationChallenges } from '@/models/index'; -import { promisify } from 'util'; -import * as crypto from 'crypto'; -import { genId } from '@/misc/gen-id'; -import { hash } from '../../../2fa'; - -const randomBytes = promisify(crypto.randomBytes); - -export const meta = { - requireCredential: true as const, - - secure: true, - - params: { - password: { - validator: $.str - } - } -}; - -export default define(meta, async (ps, user) => { - const profile = await UserProfiles.findOneOrFail(user.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 = genId(); - - await AttestationChallenges.save({ - userId: user.id, - id: challengeId, - challenge: hash(Buffer.from(challenge, 'utf-8')).toString('hex'), - createdAt: new Date(), - registrationChallenge: true - }); - - return { - challengeId, - challenge - }; -}); diff --git a/src/server/api/endpoints/i/2fa/register.ts b/src/server/api/endpoints/i/2fa/register.ts deleted file mode 100644 index b03b98188a..0000000000 --- a/src/server/api/endpoints/i/2fa/register.ts +++ /dev/null @@ -1,54 +0,0 @@ -import $ from 'cafy'; -import * as bcrypt from 'bcryptjs'; -import * as speakeasy from 'speakeasy'; -import * as QRCode from 'qrcode'; -import config from '@/config/index'; -import define from '../../../define'; -import { UserProfiles } from '@/models/index'; - -export const meta = { - requireCredential: true as const, - - secure: true, - - params: { - password: { - validator: $.str - } - } -}; - -export default define(meta, async (ps, user) => { - const profile = await UserProfiles.findOneOrFail(user.id); - - // 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 UserProfiles.update(user.id, { - twoFactorTempSecret: secret.base32 - }); - - // Get the data URL of the authenticator URL - const dataUrl = await QRCode.toDataURL(speakeasy.otpauthURL({ - secret: secret.base32, - encoding: 'base32', - label: user.username, - issuer: config.host - })); - - return { - qr: dataUrl, - secret: secret.base32, - label: user.username, - issuer: config.host - }; -}); diff --git a/src/server/api/endpoints/i/2fa/remove-key.ts b/src/server/api/endpoints/i/2fa/remove-key.ts deleted file mode 100644 index dea56301ab..0000000000 --- a/src/server/api/endpoints/i/2fa/remove-key.ts +++ /dev/null @@ -1,45 +0,0 @@ -import $ from 'cafy'; -import * as bcrypt from 'bcryptjs'; -import define from '../../../define'; -import { UserProfiles, UserSecurityKeys, Users } from '@/models/index'; -import { publishMainStream } from '@/services/stream'; - -export const meta = { - requireCredential: true as const, - - secure: true, - - params: { - password: { - validator: $.str - }, - credentialId: { - validator: $.str - }, - } -}; - -export default define(meta, async (ps, user) => { - const profile = await UserProfiles.findOneOrFail(user.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 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 {}; -}); diff --git a/src/server/api/endpoints/i/2fa/unregister.ts b/src/server/api/endpoints/i/2fa/unregister.ts deleted file mode 100644 index af53033daa..0000000000 --- a/src/server/api/endpoints/i/2fa/unregister.ts +++ /dev/null @@ -1,32 +0,0 @@ -import $ from 'cafy'; -import * as bcrypt from 'bcryptjs'; -import define from '../../../define'; -import { UserProfiles } from '@/models/index'; - -export const meta = { - requireCredential: true as const, - - secure: true, - - params: { - password: { - validator: $.str - } - } -}; - -export default define(meta, async (ps, user) => { - const profile = await UserProfiles.findOneOrFail(user.id); - - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); - - if (!same) { - throw new Error('incorrect password'); - } - - await UserProfiles.update(user.id, { - twoFactorSecret: null, - twoFactorEnabled: false - }); -}); diff --git a/src/server/api/endpoints/i/apps.ts b/src/server/api/endpoints/i/apps.ts deleted file mode 100644 index 994528e5c9..0000000000 --- a/src/server/api/endpoints/i/apps.ts +++ /dev/null @@ -1,43 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { AccessTokens } from '@/models/index'; - -export const meta = { - requireCredential: true as const, - - secure: true, - - params: { - sort: { - validator: $.optional.str.or([ - '+createdAt', - '-createdAt', - '+lastUsedAt', - '-lastUsedAt', - ]), - }, - } -}; - -export default define(meta, async (ps, user) => { - const query = AccessTokens.createQueryBuilder('token') - .where('token.userId = :userId', { userId: user.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; - } - - 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, - }))); -}); diff --git a/src/server/api/endpoints/i/authorized-apps.ts b/src/server/api/endpoints/i/authorized-apps.ts deleted file mode 100644 index 042fcd14e8..0000000000 --- a/src/server/api/endpoints/i/authorized-apps.ts +++ /dev/null @@ -1,44 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { AccessTokens, Apps } from '@/models/index'; - -export const meta = { - requireCredential: true as const, - - secure: true, - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10, - }, - - offset: { - validator: $.optional.num.min(0), - default: 0, - }, - - sort: { - validator: $.optional.str.or('desc|asc'), - default: 'desc', - } - } -}; - -export default define(meta, 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 - } - }); - - return await Promise.all(tokens.map(token => Apps.pack(token.appId, user, { - detail: true - }))); -}); diff --git a/src/server/api/endpoints/i/change-password.ts b/src/server/api/endpoints/i/change-password.ts deleted file mode 100644 index 7ea5f8c488..0000000000 --- a/src/server/api/endpoints/i/change-password.ts +++ /dev/null @@ -1,39 +0,0 @@ -import $ from 'cafy'; -import * as bcrypt from 'bcryptjs'; -import define from '../../define'; -import { UserProfiles } from '@/models/index'; - -export const meta = { - requireCredential: true as const, - - secure: true, - - params: { - currentPassword: { - validator: $.str - }, - - newPassword: { - validator: $.str - } - } -}; - -export default define(meta, async (ps, user) => { - const profile = await UserProfiles.findOneOrFail(user.id); - - // Compare password - const same = await bcrypt.compare(ps.currentPassword, profile.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 UserProfiles.update(user.id, { - password: hash - }); -}); diff --git a/src/server/api/endpoints/i/delete-account.ts b/src/server/api/endpoints/i/delete-account.ts deleted file mode 100644 index 10e5adf64a..0000000000 --- a/src/server/api/endpoints/i/delete-account.ts +++ /dev/null @@ -1,48 +0,0 @@ -import $ from 'cafy'; -import * as bcrypt from 'bcryptjs'; -import define from '../../define'; -import { UserProfiles, Users } from '@/models/index'; -import { doPostSuspend } from '@/services/suspend-user'; -import { publishUserEvent } from '@/services/stream'; -import { createDeleteAccountJob } from '@/queue'; - -export const meta = { - requireCredential: true as const, - - secure: true, - - params: { - password: { - validator: $.str - }, - } -}; - -export default define(meta, async (ps, user) => { - const profile = await UserProfiles.findOneOrFail(user.id); - const userDetailed = await Users.findOneOrFail(user.id); - if (userDetailed.isDeleted) { - return; - } - - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); - - if (!same) { - throw new Error('incorrect password'); - } - - // 物理削除する前にDelete activityを送信する - await doPostSuspend(user).catch(e => {}); - - createDeleteAccountJob(user, { - soft: false - }); - - await Users.update(user.id, { - isDeleted: true, - }); - - // Terminate streaming - publishUserEvent(user.id, 'terminate', {}); -}); diff --git a/src/server/api/endpoints/i/export-blocking.ts b/src/server/api/endpoints/i/export-blocking.ts deleted file mode 100644 index e4797da0c1..0000000000 --- a/src/server/api/endpoints/i/export-blocking.ts +++ /dev/null @@ -1,16 +0,0 @@ -import define from '../../define'; -import { createExportBlockingJob } from '@/queue/index'; -import * as ms from 'ms'; - -export const meta = { - secure: true, - requireCredential: true as const, - limit: { - duration: ms('1hour'), - max: 1, - }, -}; - -export default define(meta, async (ps, user) => { - createExportBlockingJob(user); -}); diff --git a/src/server/api/endpoints/i/export-following.ts b/src/server/api/endpoints/i/export-following.ts deleted file mode 100644 index b0f154cda8..0000000000 --- a/src/server/api/endpoints/i/export-following.ts +++ /dev/null @@ -1,16 +0,0 @@ -import define from '../../define'; -import { createExportFollowingJob } from '@/queue/index'; -import * as ms from 'ms'; - -export const meta = { - secure: true, - requireCredential: true as const, - limit: { - duration: ms('1hour'), - max: 1, - }, -}; - -export default define(meta, async (ps, user) => { - createExportFollowingJob(user); -}); diff --git a/src/server/api/endpoints/i/export-mute.ts b/src/server/api/endpoints/i/export-mute.ts deleted file mode 100644 index 46d547fa53..0000000000 --- a/src/server/api/endpoints/i/export-mute.ts +++ /dev/null @@ -1,16 +0,0 @@ -import define from '../../define'; -import { createExportMuteJob } from '@/queue/index'; -import * as ms from 'ms'; - -export const meta = { - secure: true, - requireCredential: true as const, - limit: { - duration: ms('1hour'), - max: 1, - }, -}; - -export default define(meta, async (ps, user) => { - createExportMuteJob(user); -}); diff --git a/src/server/api/endpoints/i/export-notes.ts b/src/server/api/endpoints/i/export-notes.ts deleted file mode 100644 index 441bf16896..0000000000 --- a/src/server/api/endpoints/i/export-notes.ts +++ /dev/null @@ -1,16 +0,0 @@ -import define from '../../define'; -import { createExportNotesJob } from '@/queue/index'; -import * as ms from 'ms'; - -export const meta = { - secure: true, - requireCredential: true as const, - limit: { - duration: ms('1day'), - max: 1, - }, -}; - -export default define(meta, async (ps, user) => { - createExportNotesJob(user); -}); diff --git a/src/server/api/endpoints/i/export-user-lists.ts b/src/server/api/endpoints/i/export-user-lists.ts deleted file mode 100644 index 24043a862a..0000000000 --- a/src/server/api/endpoints/i/export-user-lists.ts +++ /dev/null @@ -1,16 +0,0 @@ -import define from '../../define'; -import { createExportUserListsJob } from '@/queue/index'; -import * as ms from 'ms'; - -export const meta = { - secure: true, - requireCredential: true as const, - limit: { - duration: ms('1min'), - max: 1, - }, -}; - -export default define(meta, async (ps, user) => { - createExportUserListsJob(user); -}); diff --git a/src/server/api/endpoints/i/favorites.ts b/src/server/api/endpoints/i/favorites.ts deleted file mode 100644 index b79d68ae73..0000000000 --- a/src/server/api/endpoints/i/favorites.ts +++ /dev/null @@ -1,50 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { NoteFavorites } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; - -export const meta = { - tags: ['account', 'notes', 'favorites'], - - requireCredential: true as const, - - kind: 'read:favorites', - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'NoteFavorite', - } - }, -}; - -export default define(meta, async (ps, user) => { - const query = makePaginationQuery(NoteFavorites.createQueryBuilder('favorite'), ps.sinceId, ps.untilId) - .andWhere(`favorite.userId = :meId`, { meId: user.id }) - .leftJoinAndSelect('favorite.note', 'note'); - - const favorites = await query - .take(ps.limit!) - .getMany(); - - return await NoteFavorites.packMany(favorites, user); -}); diff --git a/src/server/api/endpoints/i/gallery/likes.ts b/src/server/api/endpoints/i/gallery/likes.ts deleted file mode 100644 index 7a2935a5ec..0000000000 --- a/src/server/api/endpoints/i/gallery/likes.ts +++ /dev/null @@ -1,57 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { GalleryLikes } from '@/models/index'; -import { makePaginationQuery } from '../../../common/make-pagination-query'; - -export const meta = { - tags: ['account', 'gallery'], - - requireCredential: true as const, - - kind: 'read:gallery-likes', - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id' - }, - page: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'GalleryPost' - } - } - } -}; - -export default define(meta, async (ps, user) => { - const query = makePaginationQuery(GalleryLikes.createQueryBuilder('like'), ps.sinceId, ps.untilId) - .andWhere(`like.userId = :meId`, { meId: user.id }) - .leftJoinAndSelect('like.post', 'post'); - - const likes = await query - .take(ps.limit!) - .getMany(); - - return await GalleryLikes.packMany(likes, user); -}); diff --git a/src/server/api/endpoints/i/gallery/posts.ts b/src/server/api/endpoints/i/gallery/posts.ts deleted file mode 100644 index 21bb8759fc..0000000000 --- a/src/server/api/endpoints/i/gallery/posts.ts +++ /dev/null @@ -1,49 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { GalleryPosts } from '@/models/index'; -import { makePaginationQuery } from '../../../common/make-pagination-query'; - -export const meta = { - tags: ['account', 'gallery'], - - requireCredential: true as const, - - kind: 'read:gallery', - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'GalleryPost' - } - } -}; - -export default define(meta, async (ps, user) => { - const query = makePaginationQuery(GalleryPosts.createQueryBuilder('post'), ps.sinceId, ps.untilId) - .andWhere(`post.userId = :meId`, { meId: user.id }); - - const posts = await query - .take(ps.limit!) - .getMany(); - - return await GalleryPosts.packMany(posts, user); -}); diff --git a/src/server/api/endpoints/i/get-word-muted-notes-count.ts b/src/server/api/endpoints/i/get-word-muted-notes-count.ts deleted file mode 100644 index 6b9be98582..0000000000 --- a/src/server/api/endpoints/i/get-word-muted-notes-count.ts +++ /dev/null @@ -1,33 +0,0 @@ -import define from '../../define'; -import { MutedNotes } from '@/models/index'; - -export const meta = { - tags: ['account'], - - requireCredential: true as const, - - kind: 'read:account', - - params: { - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - count: { - type: 'number' as const, - optional: false as const, nullable: false as const - } - } - } -}; - -export default define(meta, async (ps, user) => { - return { - count: await MutedNotes.count({ - userId: user.id, - reason: 'word' - }) - }; -}); diff --git a/src/server/api/endpoints/i/import-blocking.ts b/src/server/api/endpoints/i/import-blocking.ts deleted file mode 100644 index d44d0b6077..0000000000 --- a/src/server/api/endpoints/i/import-blocking.ts +++ /dev/null @@ -1,60 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { createImportBlockingJob } from '@/queue/index'; -import * as ms from 'ms'; -import { ApiError } from '../../error'; -import { DriveFiles } from '@/models/index'; - -export const meta = { - secure: true, - requireCredential: true as const, - - limit: { - duration: ms('1hour'), - max: 1, - }, - - params: { - fileId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: 'ebb53e5f-6574-9c0c-0b92-7ca6def56d7e' - }, - - unexpectedFileType: { - message: 'We need csv file.', - code: 'UNEXPECTED_FILE_TYPE', - id: 'b6fab7d6-d945-d67c-dfdb-32da1cd12cfe' - }, - - tooBigFile: { - message: 'That file is too big.', - code: 'TOO_BIG_FILE', - id: 'b7fbf0b1-aeef-3b21-29ef-fadd4cb72ccf' - }, - - emptyFile: { - message: 'That file is empty.', - code: 'EMPTY_FILE', - id: '6f3a4dcc-f060-a707-4950-806fbdbe60d6' - }, - } -}; - -export default define(meta, async (ps, user) => { - const file = await DriveFiles.findOne(ps.fileId); - - 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); - - createImportBlockingJob(user, file.id); -}); diff --git a/src/server/api/endpoints/i/import-following.ts b/src/server/api/endpoints/i/import-following.ts deleted file mode 100644 index b3de397661..0000000000 --- a/src/server/api/endpoints/i/import-following.ts +++ /dev/null @@ -1,59 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { createImportFollowingJob } from '@/queue/index'; -import * as ms from 'ms'; -import { ApiError } from '../../error'; -import { DriveFiles } from '@/models/index'; - -export const meta = { - secure: true, - requireCredential: true as const, - limit: { - duration: ms('1hour'), - max: 1, - }, - - params: { - fileId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: 'b98644cf-a5ac-4277-a502-0b8054a709a3' - }, - - unexpectedFileType: { - message: 'We need csv file.', - code: 'UNEXPECTED_FILE_TYPE', - id: '660f3599-bce0-4f95-9dde-311fd841c183' - }, - - tooBigFile: { - message: 'That file is too big.', - code: 'TOO_BIG_FILE', - id: 'dee9d4ed-ad07-43ed-8b34-b2856398bc60' - }, - - emptyFile: { - message: 'That file is empty.', - code: 'EMPTY_FILE', - id: '31a1b42c-06f7-42ae-8a38-a661c5c9f691' - }, - } -}; - -export default define(meta, async (ps, user) => { - const file = await DriveFiles.findOne(ps.fileId); - - 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); - - createImportFollowingJob(user, file.id); -}); diff --git a/src/server/api/endpoints/i/import-muting.ts b/src/server/api/endpoints/i/import-muting.ts deleted file mode 100644 index c17434c587..0000000000 --- a/src/server/api/endpoints/i/import-muting.ts +++ /dev/null @@ -1,60 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { createImportMutingJob } from '@/queue/index'; -import * as ms from 'ms'; -import { ApiError } from '../../error'; -import { DriveFiles } from '@/models/index'; - -export const meta = { - secure: true, - requireCredential: true as const, - - limit: { - duration: ms('1hour'), - max: 1, - }, - - params: { - fileId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: 'e674141e-bd2a-ba85-e616-aefb187c9c2a' - }, - - unexpectedFileType: { - message: 'We need csv file.', - code: 'UNEXPECTED_FILE_TYPE', - id: '568c6e42-c86c-ba09-c004-517f83f9f1a8' - }, - - tooBigFile: { - message: 'That file is too big.', - code: 'TOO_BIG_FILE', - id: '9b4ada6d-d7f7-0472-0713-4f558bd1ec9c' - }, - - emptyFile: { - message: 'That file is empty.', - code: 'EMPTY_FILE', - id: 'd2f12af1-e7b4-feac-86a3-519548f2728e' - }, - } -}; - -export default define(meta, async (ps, user) => { - const file = await DriveFiles.findOne(ps.fileId); - - 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); - - createImportMutingJob(user, file.id); -}); diff --git a/src/server/api/endpoints/i/import-user-lists.ts b/src/server/api/endpoints/i/import-user-lists.ts deleted file mode 100644 index 9069a019a9..0000000000 --- a/src/server/api/endpoints/i/import-user-lists.ts +++ /dev/null @@ -1,59 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { createImportUserListsJob } from '@/queue/index'; -import * as ms from 'ms'; -import { ApiError } from '../../error'; -import { DriveFiles } from '@/models/index'; - -export const meta = { - secure: true, - requireCredential: true as const, - limit: { - duration: ms('1hour'), - max: 1, - }, - - params: { - fileId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: 'ea9cc34f-c415-4bc6-a6fe-28ac40357049' - }, - - unexpectedFileType: { - message: 'We need csv file.', - code: 'UNEXPECTED_FILE_TYPE', - id: 'a3c9edda-dd9b-4596-be6a-150ef813745c' - }, - - tooBigFile: { - message: 'That file is too big.', - code: 'TOO_BIG_FILE', - id: 'ae6e7a22-971b-4b52-b2be-fc0b9b121fe9' - }, - - emptyFile: { - message: 'That file is empty.', - code: 'EMPTY_FILE', - id: '99efe367-ce6e-4d44-93f8-5fae7b040356' - }, - } -}; - -export default define(meta, async (ps, user) => { - const file = await DriveFiles.findOne(ps.fileId); - - 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); - - createImportUserListsJob(user, file.id); -}); diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts deleted file mode 100644 index 56668d03b7..0000000000 --- a/src/server/api/endpoints/i/notifications.ts +++ /dev/null @@ -1,138 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import { readNotification } from '../../common/read-notification'; -import define from '../../define'; -import { makePaginationQuery } from '../../common/make-pagination-query'; -import { Notifications, Followings, Mutings, Users } from '@/models/index'; -import { notificationTypes } from '@/types'; -import read from '@/services/note/read'; -import { Brackets } from 'typeorm'; - -export const meta = { - tags: ['account', 'notifications'], - - requireCredential: true as const, - - kind: 'read:notifications', - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - following: { - validator: $.optional.bool, - default: false - }, - - unreadOnly: { - validator: $.optional.bool, - default: false - }, - - markAsRead: { - validator: $.optional.bool, - default: true - }, - - includeTypes: { - validator: $.optional.arr($.str.or(notificationTypes as unknown as string[])), - }, - - excludeTypes: { - validator: $.optional.arr($.str.or(notificationTypes as unknown as string[])), - } - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Notification', - } - }, -}; - -export default define(meta, async (ps, user) => { - // includeTypes が空の場合はクエリしない - if (ps.includeTypes && ps.includeTypes.length === 0) { - return []; - } - // excludeTypes に全指定されている場合はクエリしない - if (notificationTypes.every(type => ps.excludeTypes?.includes(type))) { - return []; - } - const followingQuery = Followings.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: user.id }); - - const mutingQuery = Mutings.createQueryBuilder('muting') - .select('muting.muteeId') - .where('muting.muterId = :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('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - query.andWhere(new Brackets(qb => { qb - .where(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`) - .orWhere('notification.notifierId IS NULL'); - })); - query.setParameters(mutingQuery.getParameters()); - - 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); -}); diff --git a/src/server/api/endpoints/i/page-likes.ts b/src/server/api/endpoints/i/page-likes.ts deleted file mode 100644 index fa2bc31730..0000000000 --- a/src/server/api/endpoints/i/page-likes.ts +++ /dev/null @@ -1,57 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { PageLikes } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; - -export const meta = { - tags: ['account', 'pages'], - - requireCredential: true as const, - - kind: 'read:page-likes', - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id' - }, - page: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Page' - } - } - } -}; - -export default define(meta, async (ps, user) => { - const query = makePaginationQuery(PageLikes.createQueryBuilder('like'), ps.sinceId, ps.untilId) - .andWhere(`like.userId = :meId`, { meId: user.id }) - .leftJoinAndSelect('like.page', 'page'); - - const likes = await query - .take(ps.limit!) - .getMany(); - - return await PageLikes.packMany(likes, user); -}); diff --git a/src/server/api/endpoints/i/pages.ts b/src/server/api/endpoints/i/pages.ts deleted file mode 100644 index ee87fffa2d..0000000000 --- a/src/server/api/endpoints/i/pages.ts +++ /dev/null @@ -1,49 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { Pages } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; - -export const meta = { - tags: ['account', 'pages'], - - requireCredential: true as const, - - kind: 'read:pages', - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Page' - } - } -}; - -export default define(meta, async (ps, user) => { - const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId) - .andWhere(`page.userId = :meId`, { meId: user.id }); - - const pages = await query - .take(ps.limit!) - .getMany(); - - return await Pages.packMany(pages); -}); diff --git a/src/server/api/endpoints/i/pin.ts b/src/server/api/endpoints/i/pin.ts deleted file mode 100644 index de94220ba9..0000000000 --- a/src/server/api/endpoints/i/pin.ts +++ /dev/null @@ -1,59 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import { addPinned } from '@/services/i/pin'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { Users } from '@/models/index'; - -export const meta = { - tags: ['account', 'notes'], - - requireCredential: true as const, - - kind: 'write:account', - - params: { - noteId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '56734f8b-3928-431e-bf80-6ff87df40cb3' - }, - - pinLimitExceeded: { - message: 'You can not pin notes any more.', - code: 'PIN_LIMIT_EXCEEDED', - id: '72dab508-c64d-498f-8740-a8eec1ba385a' - }, - - alreadyPinned: { - message: 'That note has already been pinned.', - code: 'ALREADY_PINNED', - id: '8b18c2b7-68fe-4edb-9892-c0cbaeb6c913' - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User' - } -}; - -export default define(meta, 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; - }); - - return await Users.pack(user.id, user, { - detail: true - }); -}); diff --git a/src/server/api/endpoints/i/read-all-messaging-messages.ts b/src/server/api/endpoints/i/read-all-messaging-messages.ts deleted file mode 100644 index 9aca7611c9..0000000000 --- a/src/server/api/endpoints/i/read-all-messaging-messages.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { publishMainStream } from '@/services/stream'; -import define from '../../define'; -import { MessagingMessages, UserGroupJoinings } from '@/models/index'; - -export const meta = { - tags: ['account', 'messaging'], - - requireCredential: true as const, - - kind: 'write:account', - - params: { - } -}; - -export default define(meta, async (ps, user) => { - // Update documents - await MessagingMessages.update({ - recipientId: user.id, - isRead: false - }, { - isRead: true - }); - - const joinings = await UserGroupJoinings.find({ userId: user.id }); - - 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())); - - publishMainStream(user.id, 'readAllMessagingMessages'); -}); diff --git a/src/server/api/endpoints/i/read-all-unread-notes.ts b/src/server/api/endpoints/i/read-all-unread-notes.ts deleted file mode 100644 index 2a7102a590..0000000000 --- a/src/server/api/endpoints/i/read-all-unread-notes.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { publishMainStream } from '@/services/stream'; -import define from '../../define'; -import { NoteUnreads } from '@/models/index'; - -export const meta = { - tags: ['account'], - - requireCredential: true as const, - - kind: 'write:account', - - params: { - } -}; - -export default define(meta, async (ps, user) => { - // Remove documents - await NoteUnreads.delete({ - userId: user.id - }); - - // 全て既読になったイベントを発行 - publishMainStream(user.id, 'readAllUnreadMentions'); - publishMainStream(user.id, 'readAllUnreadSpecifiedNotes'); -}); diff --git a/src/server/api/endpoints/i/read-announcement.ts b/src/server/api/endpoints/i/read-announcement.ts deleted file mode 100644 index 2f5036f953..0000000000 --- a/src/server/api/endpoints/i/read-announcement.ts +++ /dev/null @@ -1,60 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { genId } from '@/misc/gen-id'; -import { AnnouncementReads, Announcements, Users } from '@/models/index'; -import { publishMainStream } from '@/services/stream'; - -export const meta = { - tags: ['account'], - - requireCredential: true as const, - - kind: 'write:account', - - params: { - announcementId: { - validator: $.type(ID), - }, - }, - - errors: { - noSuchAnnouncement: { - message: 'No such announcement.', - code: 'NO_SUCH_ANNOUNCEMENT', - id: '184663db-df88-4bc2-8b52-fb85f0681939' - }, - } -}; - -export default define(meta, async (ps, user) => { - // Check if announcement exists - const announcement = await Announcements.findOne(ps.announcementId); - - if (announcement == null) { - throw new ApiError(meta.errors.noSuchAnnouncement); - } - - // Check if already read - const read = await AnnouncementReads.findOne({ - 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'); - } -}); diff --git a/src/server/api/endpoints/i/regenerate-token.ts b/src/server/api/endpoints/i/regenerate-token.ts deleted file mode 100644 index 1cce2d37be..0000000000 --- a/src/server/api/endpoints/i/regenerate-token.ts +++ /dev/null @@ -1,44 +0,0 @@ -import $ from 'cafy'; -import * as bcrypt from 'bcryptjs'; -import { publishMainStream, publishUserEvent } from '@/services/stream'; -import generateUserToken from '../../common/generate-native-user-token'; -import define from '../../define'; -import { Users, UserProfiles } from '@/models/index'; - -export const meta = { - requireCredential: true as const, - - secure: true, - - params: { - password: { - validator: $.str - } - } -}; - -export default define(meta, async (ps, user) => { - const profile = await UserProfiles.findOneOrFail(user.id); - - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); - - if (!same) { - throw new Error('incorrect password'); - } - - // Generate secret - const secret = generateUserToken(); - - await Users.update(user.id, { - token: secret - }); - - // Publish event - publishMainStream(user.id, 'myTokenRegenerated'); - - // Terminate streaming - setTimeout(() => { - publishUserEvent(user.id, 'terminate', {}); - }, 5000); -}); diff --git a/src/server/api/endpoints/i/registry/get-all.ts b/src/server/api/endpoints/i/registry/get-all.ts deleted file mode 100644 index c8eaf83a25..0000000000 --- a/src/server/api/endpoints/i/registry/get-all.ts +++ /dev/null @@ -1,33 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { RegistryItems } from '@/models/index'; - -export const meta = { - requireCredential: true as const, - - secure: true, - - params: { - scope: { - validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)), - default: [], - }, - } -}; - -export default define(meta, 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 }); - - const items = await query.getMany(); - - const res = {} as Record<string, any>; - - for (const item of items) { - res[item.key] = item.value; - } - - return res; -}); diff --git a/src/server/api/endpoints/i/registry/get-detail.ts b/src/server/api/endpoints/i/registry/get-detail.ts deleted file mode 100644 index 992800c44c..0000000000 --- a/src/server/api/endpoints/i/registry/get-detail.ts +++ /dev/null @@ -1,48 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { RegistryItems } from '@/models/index'; -import { ApiError } from '../../../error'; - -export const meta = { - requireCredential: true as const, - - secure: true, - - params: { - key: { - validator: $.str - }, - - scope: { - validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)), - default: [], - }, - }, - - errors: { - noSuchKey: { - message: 'No such key.', - code: 'NO_SUCH_KEY', - id: '97a1e8e7-c0f7-47d2-957a-92e61256e01a' - }, - }, -}; - -export default define(meta, 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 }); - - const item = await query.getOne(); - - if (item == null) { - throw new ApiError(meta.errors.noSuchKey); - } - - return { - updatedAt: item.updatedAt, - value: item.value, - }; -}); diff --git a/src/server/api/endpoints/i/registry/get.ts b/src/server/api/endpoints/i/registry/get.ts deleted file mode 100644 index 569c3a9280..0000000000 --- a/src/server/api/endpoints/i/registry/get.ts +++ /dev/null @@ -1,45 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { RegistryItems } from '@/models/index'; -import { ApiError } from '../../../error'; - -export const meta = { - requireCredential: true as const, - - secure: true, - - params: { - key: { - validator: $.str - }, - - scope: { - validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)), - default: [], - }, - }, - - errors: { - noSuchKey: { - message: 'No such key.', - code: 'NO_SUCH_KEY', - id: 'ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a' - }, - }, -}; - -export default define(meta, 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 }); - - const item = await query.getOne(); - - if (item == null) { - throw new ApiError(meta.errors.noSuchKey); - } - - return item.value; -}); diff --git a/src/server/api/endpoints/i/registry/keys-with-type.ts b/src/server/api/endpoints/i/registry/keys-with-type.ts deleted file mode 100644 index 16a4fee374..0000000000 --- a/src/server/api/endpoints/i/registry/keys-with-type.ts +++ /dev/null @@ -1,41 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { RegistryItems } from '@/models/index'; - -export const meta = { - requireCredential: true as const, - - secure: true, - - params: { - scope: { - validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)), - default: [], - }, - } -}; - -export default define(meta, 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 }); - - const items = await query.getMany(); - - const res = {} as Record<string, string>; - - 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' : - type === 'string' ? 'string' : - type === 'boolean' ? 'boolean' : - type === 'object' ? 'object' : - null as never; - } - - return res; -}); diff --git a/src/server/api/endpoints/i/registry/keys.ts b/src/server/api/endpoints/i/registry/keys.ts deleted file mode 100644 index 3a8aeaa195..0000000000 --- a/src/server/api/endpoints/i/registry/keys.ts +++ /dev/null @@ -1,28 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { RegistryItems } from '@/models/index'; - -export const meta = { - requireCredential: true as const, - - secure: true, - - params: { - scope: { - validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)), - default: [], - }, - } -}; - -export default define(meta, 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 }); - - const items = await query.getMany(); - - return items.map(x => x.key); -}); diff --git a/src/server/api/endpoints/i/registry/remove.ts b/src/server/api/endpoints/i/registry/remove.ts deleted file mode 100644 index 07bc23d4a6..0000000000 --- a/src/server/api/endpoints/i/registry/remove.ts +++ /dev/null @@ -1,45 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { RegistryItems } from '@/models/index'; -import { ApiError } from '../../../error'; - -export const meta = { - requireCredential: true as const, - - secure: true, - - params: { - key: { - validator: $.str - }, - - scope: { - validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)), - default: [], - }, - }, - - errors: { - noSuchKey: { - message: 'No such key.', - code: 'NO_SUCH_KEY', - id: '1fac4e8a-a6cd-4e39-a4a5-3a7e11f1b019' - }, - }, -}; - -export default define(meta, 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 }); - - const item = await query.getOne(); - - if (item == null) { - throw new ApiError(meta.errors.noSuchKey); - } - - await RegistryItems.remove(item); -}); diff --git a/src/server/api/endpoints/i/registry/scopes.ts b/src/server/api/endpoints/i/registry/scopes.ts deleted file mode 100644 index ecbdb05a8e..0000000000 --- a/src/server/api/endpoints/i/registry/scopes.ts +++ /dev/null @@ -1,29 +0,0 @@ -import define from '../../../define'; -import { RegistryItems } from '@/models/index'; - -export const meta = { - requireCredential: true as const, - - secure: true, - - params: { - } -}; - -export default define(meta, async (ps, user) => { - const query = RegistryItems.createQueryBuilder('item') - .select('item.scope') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: user.id }); - - const items = await query.getMany(); - - const res = [] as string[][]; - - for (const item of items) { - if (res.some(scope => scope.join('.') === item.scope.join('.'))) continue; - res.push(item.scope); - } - - return res; -}); diff --git a/src/server/api/endpoints/i/registry/set.ts b/src/server/api/endpoints/i/registry/set.ts deleted file mode 100644 index f129ee1b70..0000000000 --- a/src/server/api/endpoints/i/registry/set.ts +++ /dev/null @@ -1,61 +0,0 @@ -import $ from 'cafy'; -import { publishMainStream } from '@/services/stream'; -import define from '../../../define'; -import { RegistryItems } from '@/models/index'; -import { genId } from '@/misc/gen-id'; - -export const meta = { - requireCredential: true as const, - - secure: true, - - params: { - key: { - validator: $.str.min(1) - }, - - value: { - validator: $.nullable.any - }, - - scope: { - validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)), - default: [], - }, - } -}; - -export default define(meta, 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 }); - - const existingItem = await query.getOne(); - - 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 - }); - } - - // TODO: サードパーティアプリが傍受出来てしまうのでどうにかする - publishMainStream(user.id, 'registryUpdated', { - scope: ps.scope, - key: ps.key, - value: ps.value - }); -}); diff --git a/src/server/api/endpoints/i/revoke-token.ts b/src/server/api/endpoints/i/revoke-token.ts deleted file mode 100644 index bed868def4..0000000000 --- a/src/server/api/endpoints/i/revoke-token.ts +++ /dev/null @@ -1,31 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { AccessTokens } from '@/models/index'; -import { ID } from '@/misc/cafy-id'; -import { publishUserEvent } from '@/services/stream'; - -export const meta = { - requireCredential: true as const, - - secure: true, - - params: { - tokenId: { - validator: $.type(ID) - } - } -}; - -export default define(meta, async (ps, user) => { - const token = await AccessTokens.findOne(ps.tokenId); - - if (token) { - await AccessTokens.delete({ - id: ps.tokenId, - userId: user.id, - }); - - // Terminate streaming - publishUserEvent(user.id, 'terminate'); - } -}); diff --git a/src/server/api/endpoints/i/signin-history.ts b/src/server/api/endpoints/i/signin-history.ts deleted file mode 100644 index a2c10148c6..0000000000 --- a/src/server/api/endpoints/i/signin-history.ts +++ /dev/null @@ -1,35 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { Signins } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; - -export const meta = { - requireCredential: true as const, - - secure: true, - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - } - } -}; - -export default define(meta, async (ps, user) => { - const query = makePaginationQuery(Signins.createQueryBuilder('signin'), ps.sinceId, ps.untilId) - .andWhere(`signin.userId = :meId`, { meId: user.id }); - - const history = await query.take(ps.limit!).getMany(); - - return await Promise.all(history.map(record => Signins.pack(record))); -}); diff --git a/src/server/api/endpoints/i/unpin.ts b/src/server/api/endpoints/i/unpin.ts deleted file mode 100644 index dc79e255ab..0000000000 --- a/src/server/api/endpoints/i/unpin.ts +++ /dev/null @@ -1,45 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import { removePinned } from '@/services/i/pin'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { Users } from '@/models/index'; - -export const meta = { - tags: ['account', 'notes'], - - requireCredential: true as const, - - kind: 'write:account', - - params: { - noteId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '454170ce-9d63-4a43-9da1-ea10afe81e21' - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User' - } -}; - -export default define(meta, 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; - }); - - return await Users.pack(user.id, user, { - detail: true - }); -}); diff --git a/src/server/api/endpoints/i/update-email.ts b/src/server/api/endpoints/i/update-email.ts deleted file mode 100644 index 9b6fb9c410..0000000000 --- a/src/server/api/endpoints/i/update-email.ts +++ /dev/null @@ -1,94 +0,0 @@ -import $ from 'cafy'; -import { publishMainStream } from '@/services/stream'; -import define from '../../define'; -import rndstr from 'rndstr'; -import config from '@/config/index'; -import * as ms from 'ms'; -import * as bcrypt from 'bcryptjs'; -import { Users, UserProfiles } from '@/models/index'; -import { sendEmail } from '@/services/send-email'; -import { ApiError } from '../../error'; -import { validateEmailForAccount } from '@/services/validate-email-for-account'; - -export const meta = { - requireCredential: true as const, - - secure: true, - - limit: { - duration: ms('1hour'), - max: 3 - }, - - params: { - password: { - validator: $.str - }, - - email: { - validator: $.optional.nullable.str - }, - }, - - errors: { - incorrectPassword: { - message: 'Incorrect password.', - code: 'INCORRECT_PASSWORD', - id: 'e54c1d7e-e7d6-4103-86b6-0a95069b4ad3' - }, - - unavailable: { - message: 'Unavailable email address.', - code: 'UNAVAILABLE', - id: 'a2defefb-f220-8849-0af6-17f816099323' - }, - } -}; - -export default define(meta, async (ps, user) => { - const profile = await UserProfiles.findOneOrFail(user.id); - - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); - - if (!same) { - throw new ApiError(meta.errors.incorrectPassword); - } - - if (ps.email != null) { - const available = await validateEmailForAccount(ps.email); - if (!available) { - throw new ApiError(meta.errors.unavailable); - } - } - - await UserProfiles.update(user.id, { - email: ps.email, - emailVerified: false, - emailVerifyCode: null - }); - - const iObj = await Users.pack(user.id, user, { - detail: true, - includeSecrets: true - }); - - // Publish meUpdated event - publishMainStream(user.id, 'meUpdated', iObj); - - if (ps.email != null) { - const code = rndstr('a-z0-9', 16); - - await UserProfiles.update(user.id, { - emailVerifyCode: code - }); - - 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; -}); diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts deleted file mode 100644 index d0f201ab60..0000000000 --- a/src/server/api/endpoints/i/update.ts +++ /dev/null @@ -1,294 +0,0 @@ -import $ from 'cafy'; -import * as mfm from 'mfm-js'; -import { ID } from '@/misc/cafy-id'; -import { publishMainStream, publishUserEvent } from '@/services/stream'; -import acceptAllFollowRequests from '@/services/following/requests/accept-all'; -import { publishToFollowers } from '@/services/i/update'; -import define from '../../define'; -import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm'; -import { extractHashtags } from '@/misc/extract-hashtags'; -import * as langmap from 'langmap'; -import { updateUsertags } from '@/services/update-hashtag'; -import { ApiError } from '../../error'; -import { Users, DriveFiles, UserProfiles, Pages } from '@/models/index'; -import { User } from '@/models/entities/user'; -import { UserProfile } from '@/models/entities/user-profile'; -import { notificationTypes } from '@/types'; -import { normalizeForSearch } from '@/misc/normalize-for-search'; - -export const meta = { - tags: ['account'], - - requireCredential: true as const, - - kind: 'write:account', - - params: { - name: { - validator: $.optional.nullable.use(Users.validateName), - }, - - description: { - validator: $.optional.nullable.use(Users.validateDescription), - }, - - lang: { - validator: $.optional.nullable.str.or(Object.keys(langmap)), - }, - - location: { - validator: $.optional.nullable.use(Users.validateLocation), - }, - - birthday: { - validator: $.optional.nullable.use(Users.validateBirthday), - }, - - avatarId: { - validator: $.optional.nullable.type(ID), - }, - - bannerId: { - validator: $.optional.nullable.type(ID), - }, - - fields: { - validator: $.optional.arr($.object()).range(1, 4), - }, - - isLocked: { - validator: $.optional.bool, - }, - - isExplorable: { - validator: $.optional.bool, - }, - - hideOnlineStatus: { - validator: $.optional.bool, - }, - - publicReactions: { - validator: $.optional.bool, - }, - - ffVisibility: { - validator: $.optional.str, - }, - - carefulBot: { - validator: $.optional.bool, - }, - - autoAcceptFollowed: { - validator: $.optional.bool, - }, - - noCrawle: { - validator: $.optional.bool, - }, - - isBot: { - validator: $.optional.bool, - }, - - isCat: { - validator: $.optional.bool, - }, - - injectFeaturedNote: { - validator: $.optional.bool, - }, - - receiveAnnouncementEmail: { - validator: $.optional.bool, - }, - - alwaysMarkNsfw: { - validator: $.optional.bool, - }, - - pinnedPageId: { - validator: $.optional.nullable.type(ID), - }, - - mutedWords: { - validator: $.optional.arr($.arr($.str)) - }, - - mutingNotificationTypes: { - validator: $.optional.arr($.str.or(notificationTypes as unknown as string[])) - }, - - emailNotificationTypes: { - validator: $.optional.arr($.str) - }, - }, - - errors: { - noSuchAvatar: { - message: 'No such avatar file.', - code: 'NO_SUCH_AVATAR', - id: '539f3a45-f215-4f81-a9a8-31293640207f' - }, - - noSuchBanner: { - message: 'No such banner file.', - code: 'NO_SUCH_BANNER', - id: '0d8f5629-f210-41c2-9433-735831a58595' - }, - - avatarNotAnImage: { - message: 'The file specified as an avatar is not an image.', - code: 'AVATAR_NOT_AN_IMAGE', - id: 'f419f9f8-2f4d-46b1-9fb4-49d3a2fd7191' - }, - - bannerNotAnImage: { - message: 'The file specified as a banner is not an image.', - code: 'BANNER_NOT_AN_IMAGE', - id: '75aedb19-2afd-4e6d-87fc-67941256fa60' - }, - - noSuchPage: { - message: 'No such page.', - code: 'NO_SUCH_PAGE', - id: '8e01b590-7eb9-431b-a239-860e086c408e' - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User' - } -}; - -export default define(meta, async (ps, _user, token) => { - const user = await Users.findOneOrFail(_user.id); - const isSecure = token == null; - - const updates = {} as Partial<User>; - const profileUpdates = {} as Partial<UserProfile>; - - const profile = await UserProfiles.findOneOrFail(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) { - profileUpdates.mutedWords = ps.mutedWords; - profileUpdates.enableWordMute = ps.mutedWords.length > 0; - } - 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.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 (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes; - - if (ps.avatarId) { - const avatar = await DriveFiles.findOne(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); - - updates.avatarUrl = DriveFiles.getPublicUrl(avatar, true); - - if (avatar.blurhash) { - updates.avatarBlurhash = avatar.blurhash; - } - } - - if (ps.bannerId) { - const banner = await DriveFiles.findOne(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); - - updates.bannerUrl = DriveFiles.getPublicUrl(banner, false); - - if (banner.blurhash) { - updates.bannerBlurhash = banner.blurhash; - } - } - - if (ps.pinnedPageId) { - const page = await Pages.findOne(ps.pinnedPageId); - - 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; - } - - 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.parsePlain(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(user.id, user, { - detail: true, - includeSecrets: isSecure - }); - - // Publish meUpdated event - publishMainStream(user.id, 'meUpdated', iObj); - publishUserEvent(user.id, 'updateUserProfile', await UserProfiles.findOne(user.id)); - - // 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認 - if (user.isLocked && ps.isLocked === false) { - acceptAllFollowRequests(user); - } - - // フォロワーにUpdateを配信 - publishToFollowers(user.id); - - return iObj; -}); diff --git a/src/server/api/endpoints/i/user-group-invites.ts b/src/server/api/endpoints/i/user-group-invites.ts deleted file mode 100644 index 1ebde243ca..0000000000 --- a/src/server/api/endpoints/i/user-group-invites.ts +++ /dev/null @@ -1,61 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { UserGroupInvitations } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; - -export const meta = { - tags: ['account', 'groups'], - - requireCredential: true as const, - - kind: 'read:user-groups', - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id' - }, - group: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'UserGroup' - } - } - } - } -}; - -export default define(meta, 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'); - - const invitations = await query - .take(ps.limit!) - .getMany(); - - return await UserGroupInvitations.packMany(invitations); -}); diff --git a/src/server/api/endpoints/messaging/history.ts b/src/server/api/endpoints/messaging/history.ts deleted file mode 100644 index e447703546..0000000000 --- a/src/server/api/endpoints/messaging/history.ts +++ /dev/null @@ -1,94 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { MessagingMessage } from '@/models/entities/messaging-message'; -import { MessagingMessages, Mutings, UserGroupJoinings } from '@/models/index'; -import { Brackets } from 'typeorm'; - -export const meta = { - tags: ['messaging'], - - requireCredential: true as const, - - kind: 'read:messaging', - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - group: { - validator: $.optional.bool, - default: false - } - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'MessagingMessage', - } - }, -}; - -export default define(meta, async (ps, user) => { - const mute = await Mutings.find({ - muterId: user.id, - }); - - const groups = ps.group ? await UserGroupJoinings.find({ - userId: user.id, - }).then(xs => xs.map(x => x.userGroupId)) : []; - - if (ps.group && groups.length === 0) { - return []; - } - - const history: MessagingMessage[] = []; - - for (let i = 0; i < ps.limit!; i++) { - const found = ps.group - ? history.map(m => m.groupId!) - : history.map(m => (m.userId === user.id) ? m.recipientId! : m.userId!); - - const query = MessagingMessages.createQueryBuilder('message') - .orderBy('message.createdAt', 'DESC'); - - if (ps.group) { - query.where(`message.groupId IN (:...groups)`, { groups: groups }); - - if (found.length > 0) { - query.andWhere(`message.groupId NOT IN (:...found)`, { found: found }); - } - } else { - query.where(new Brackets(qb => { qb - .where(`message.userId = :userId`, { userId: user.id }) - .orWhere(`message.recipientId = :userId`, { userId: user.id }); - })); - query.andWhere(`message.groupId IS NULL`); - - if (found.length > 0) { - query.andWhere(`message.userId NOT IN (:...found)`, { found: found }); - query.andWhere(`message.recipientId NOT IN (:...found)`, { found: found }); - } - - if (mute.length > 0) { - query.andWhere(`message.userId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) }); - query.andWhere(`message.recipientId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) }); - } - } - - const message = await query.getOne(); - - if (message) { - history.push(message); - } else { - break; - } - } - - return await Promise.all(history.map(h => MessagingMessages.pack(h.id, user))); -}); diff --git a/src/server/api/endpoints/messaging/messages.ts b/src/server/api/endpoints/messaging/messages.ts deleted file mode 100644 index 6baa24609e..0000000000 --- a/src/server/api/endpoints/messaging/messages.ts +++ /dev/null @@ -1,148 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { getUser } from '../../common/getters'; -import { MessagingMessages, UserGroups, UserGroupJoinings, Users } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; -import { Brackets } from 'typeorm'; -import { readUserMessagingMessage, readGroupMessagingMessage, deliverReadActivity } from '../../common/read-messaging-message'; - -export const meta = { - tags: ['messaging'], - - requireCredential: true as const, - - kind: 'read:messaging', - - params: { - userId: { - validator: $.optional.type(ID), - }, - - groupId: { - validator: $.optional.type(ID), - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - markAsRead: { - validator: $.optional.bool, - default: true - } - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'MessagingMessage', - } - }, - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '11795c64-40ea-4198-b06e-3c873ed9039d' - }, - - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: 'c4d9f88c-9270-4632-b032-6ed8cee36f7f' - }, - - groupAccessDenied: { - message: 'You can not read messages of groups that you have not joined.', - code: 'GROUP_ACCESS_DENIED', - id: 'a053a8dd-a491-4718-8f87-50775aad9284' - }, - } -}; - -export default define(meta, async (ps, user) => { - if (ps.userId != null) { - // Fetch recipient (user) - const recipient = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId) - .andWhere(new Brackets(qb => { qb - .where(new Brackets(qb => { qb - .where('message.userId = :meId') - .andWhere('message.recipientId = :recipientId'); - })) - .orWhere(new Brackets(qb => { qb - .where('message.userId = :recipientId') - .andWhere('message.recipientId = :meId'); - })); - })) - .setParameter('meId', user.id) - .setParameter('recipientId', recipient.id); - - const messages = await query.take(ps.limit!).getMany(); - - // Mark all as read - if (ps.markAsRead) { - readUserMessagingMessage(user.id, recipient.id, messages.filter(m => m.recipientId === user.id).map(x => x.id)); - - // リモートユーザーとのメッセージだったら既読配信 - if (Users.isLocalUser(user) && Users.isRemoteUser(recipient)) { - deliverReadActivity(user, recipient, messages); - } - } - - return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, { - populateRecipient: false - }))); - } else if (ps.groupId != null) { - // Fetch recipient (group) - const recipientGroup = await UserGroups.findOne(ps.groupId); - - if (recipientGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - // check joined - const joining = await UserGroupJoinings.findOne({ - userId: user.id, - userGroupId: recipientGroup.id - }); - - if (joining == null) { - throw new ApiError(meta.errors.groupAccessDenied); - } - - const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId) - .andWhere(`message.groupId = :groupId`, { groupId: recipientGroup.id }); - - const messages = await query.take(ps.limit!).getMany(); - - // Mark all as read - if (ps.markAsRead) { - readGroupMessagingMessage(user.id, recipientGroup.id, messages.map(x => x.id)); - } - - return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, { - populateGroup: false - }))); - } else { - throw new Error(); - } -}); diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts deleted file mode 100644 index df0b455cbe..0000000000 --- a/src/server/api/endpoints/messaging/messages/create.ts +++ /dev/null @@ -1,148 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { getUser } from '../../../common/getters'; -import { MessagingMessages, DriveFiles, UserGroups, UserGroupJoinings, Blockings } from '@/models/index'; -import { User } from '@/models/entities/user'; -import { UserGroup } from '@/models/entities/user-group'; -import { createMessage } from '@/services/messages/create'; - -export const meta = { - tags: ['messaging'], - - requireCredential: true as const, - - kind: 'write:messaging', - - params: { - userId: { - validator: $.optional.type(ID), - }, - - groupId: { - validator: $.optional.type(ID), - }, - - text: { - validator: $.optional.str.pipe(MessagingMessages.validateText) - }, - - fileId: { - validator: $.optional.type(ID), - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'MessagingMessage', - }, - - errors: { - recipientIsYourself: { - message: 'You can not send a message to yourself.', - code: 'RECIPIENT_IS_YOURSELF', - id: '17e2ba79-e22a-4cbc-bf91-d327643f4a7e' - }, - - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '11795c64-40ea-4198-b06e-3c873ed9039d' - }, - - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: 'c94e2a5d-06aa-4914-8fa6-6a42e73d6537' - }, - - groupAccessDenied: { - message: 'You can not send messages to groups that you have not joined.', - code: 'GROUP_ACCESS_DENIED', - id: 'd96b3cca-5ad1-438b-ad8b-02f931308fbd' - }, - - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: '4372b8e2-185d-4146-8749-2f68864a3e5f' - }, - - contentRequired: { - message: 'Content required. You need to set text or fileId.', - code: 'CONTENT_REQUIRED', - id: '25587321-b0e6-449c-9239-f8925092942c' - }, - - youHaveBeenBlocked: { - message: 'You cannot send a message because you have been blocked by this user.', - code: 'YOU_HAVE_BEEN_BLOCKED', - id: 'c15a5199-7422-4968-941a-2a462c478f7d' - }, - } -}; - -export default define(meta, async (ps, user) => { - let recipientUser: User | undefined; - let recipientGroup: UserGroup | undefined; - - if (ps.userId != null) { - // Myself - if (ps.userId === user.id) { - throw new ApiError(meta.errors.recipientIsYourself); - } - - // Fetch recipient (user) - recipientUser = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - // Check blocking - const block = await Blockings.findOne({ - blockerId: recipientUser.id, - blockeeId: user.id, - }); - if (block) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } - } else if (ps.groupId != null) { - // Fetch recipient (group) - recipientGroup = await UserGroups.findOne(ps.groupId); - - if (recipientGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - // check joined - const joining = await UserGroupJoinings.findOne({ - userId: user.id, - userGroupId: recipientGroup.id - }); - - if (joining == null) { - throw new ApiError(meta.errors.groupAccessDenied); - } - } - - let file = null; - if (ps.fileId != null) { - file = await DriveFiles.findOne({ - id: ps.fileId, - userId: user.id - }); - - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } - } - - // テキストが無いかつ添付ファイルも無かったらエラー - if (ps.text == null && file == null) { - throw new ApiError(meta.errors.contentRequired); - } - - return await createMessage(user, recipientUser, recipientGroup, ps.text, file); -}); diff --git a/src/server/api/endpoints/messaging/messages/delete.ts b/src/server/api/endpoints/messaging/messages/delete.ts deleted file mode 100644 index bd4890fc8a..0000000000 --- a/src/server/api/endpoints/messaging/messages/delete.ts +++ /dev/null @@ -1,48 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import * as ms from 'ms'; -import { ApiError } from '../../../error'; -import { MessagingMessages } from '@/models/index'; -import { deleteMessage } from '@/services/messages/delete'; - -export const meta = { - tags: ['messaging'], - - requireCredential: true as const, - - kind: 'write:messaging', - - limit: { - duration: ms('1hour'), - max: 300, - minInterval: ms('1sec') - }, - - params: { - messageId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchMessage: { - message: 'No such message.', - code: 'NO_SUCH_MESSAGE', - id: '54b5b326-7925-42cf-8019-130fda8b56af' - }, - } -}; - -export default define(meta, async (ps, user) => { - const message = await MessagingMessages.findOne({ - id: ps.messageId, - userId: user.id - }); - - if (message == null) { - throw new ApiError(meta.errors.noSuchMessage); - } - - await deleteMessage(message); -}); diff --git a/src/server/api/endpoints/messaging/messages/read.ts b/src/server/api/endpoints/messaging/messages/read.ts deleted file mode 100644 index a1747310d3..0000000000 --- a/src/server/api/endpoints/messaging/messages/read.ts +++ /dev/null @@ -1,48 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { MessagingMessages } from '@/models/index'; -import { readUserMessagingMessage, readGroupMessagingMessage } from '../../../common/read-messaging-message'; - -export const meta = { - tags: ['messaging'], - - requireCredential: true as const, - - kind: 'write:messaging', - - params: { - messageId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchMessage: { - message: 'No such message.', - code: 'NO_SUCH_MESSAGE', - id: '86d56a2f-a9c3-4afb-b13c-3e9bfef9aa14' - }, - } -}; - -export default define(meta, async (ps, user) => { - const message = await MessagingMessages.findOne(ps.messageId); - - if (message == null) { - throw new ApiError(meta.errors.noSuchMessage); - } - - if (message.recipientId) { - await readUserMessagingMessage(user.id, message.userId, [message.id]).catch(e => { - if (e.id === 'e140a4bf-49ce-4fb6-b67c-b78dadf6b52f') throw new ApiError(meta.errors.noSuchMessage); - throw e; - }); - } else if (message.groupId) { - await readGroupMessagingMessage(user.id, message.groupId, [message.id]).catch(e => { - if (e.id === '930a270c-714a-46b2-b776-ad27276dc569') throw new ApiError(meta.errors.noSuchMessage); - throw e; - }); - } -}); diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts deleted file mode 100644 index ce21556243..0000000000 --- a/src/server/api/endpoints/meta.ts +++ /dev/null @@ -1,598 +0,0 @@ -import $ from 'cafy'; -import config from '@/config/index'; -import define from '../define'; -import { fetchMeta } from '@/misc/fetch-meta'; -import { Ads, Emojis, Users } from '@/models/index'; -import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits'; -import { MoreThan } from 'typeorm'; - -export const meta = { - tags: ['meta'], - - requireCredential: false as const, - - params: { - detail: { - validator: $.optional.bool, - default: true - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - maintainerName: { - type: 'string' as const, - optional: false as const, nullable: true as const - }, - maintainerEmail: { - type: 'string' as const, - optional: false as const, nullable: true as const - }, - version: { - type: 'string' as const, - optional: false as const, nullable: false as const, - example: config.version - }, - name: { - type: 'string' as const, - optional: false as const, nullable: false as const, - }, - uri: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'url', - example: 'https://misskey.example.com' - }, - description: { - type: 'string' as const, - optional: false as const, nullable: true as const, - }, - langs: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'string' as const, - optional: false as const, nullable: false as const - } - }, - tosUrl: { - type: 'string' as const, - optional: false as const, nullable: true as const - }, - repositoryUrl: { - type: 'string' as const, - optional: false as const, nullable: false as const, - default: 'https://github.com/misskey-dev/misskey' - }, - feedbackUrl: { - type: 'string' as const, - optional: false as const, nullable: false as const, - default: 'https://github.com/misskey-dev/misskey/issues/new' - }, - secure: { - type: 'boolean' as const, - optional: false as const, nullable: false as const, - default: false - }, - disableRegistration: { - type: 'boolean' as const, - optional: false as const, nullable: false as const, - }, - disableLocalTimeline: { - type: 'boolean' as const, - optional: false as const, nullable: false as const, - }, - disableGlobalTimeline: { - type: 'boolean' as const, - optional: false as const, nullable: false as const, - }, - driveCapacityPerLocalUserMb: { - type: 'number' as const, - optional: false as const, nullable: false as const - }, - driveCapacityPerRemoteUserMb: { - type: 'number' as const, - optional: false as const, nullable: false as const - }, - cacheRemoteFiles: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - proxyRemoteFiles: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - emailRequiredForSignup: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - enableHcaptcha: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - hcaptchaSiteKey: { - type: 'string' as const, - optional: false as const, nullable: true as const - }, - enableRecaptcha: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - recaptchaSiteKey: { - type: 'string' as const, - optional: false as const, nullable: true as const - }, - swPublickey: { - type: 'string' as const, - optional: false as const, nullable: true as const - }, - mascotImageUrl: { - type: 'string' as const, - optional: false as const, nullable: false as const, - default: '/assets/ai.png' - }, - bannerUrl: { - type: 'string' as const, - optional: false as const, nullable: false as const - }, - errorImageUrl: { - type: 'string' as const, - optional: false as const, nullable: false as const, - default: 'https://xn--931a.moe/aiart/yubitun.png' - }, - iconUrl: { - type: 'string' as const, - optional: false as const, nullable: true as const - }, - maxNoteTextLength: { - type: 'number' as const, - optional: false as const, nullable: false as const, - default: 500 - }, - emojis: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id' - }, - aliases: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'string' as const, - optional: false as const, nullable: false as const - } - }, - category: { - type: 'string' as const, - optional: false as const, nullable: true as const - }, - host: { - type: 'string' as const, - optional: false as const, nullable: true as const - }, - url: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'url' - } - } - } - }, - ads: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - place: { - type: 'string' as const, - optional: false as const, nullable: false as const - }, - url: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'url' - }, - imageUrl: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'url' - }, - } - } - }, - requireSetup: { - type: 'boolean' as const, - optional: false as const, nullable: false as const, - example: false - }, - enableEmail: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - enableTwitterIntegration: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - enableGithubIntegration: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - enableDiscordIntegration: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - enableServiceWorker: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - translatorAvailable: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - proxyAccountName: { - type: 'string' as const, - optional: false as const, nullable: true as const - }, - features: { - type: 'object' as const, - optional: true as const, nullable: false as const, - properties: { - registration: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - localTimeLine: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - globalTimeLine: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - elasticsearch: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - hcaptcha: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - recaptcha: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - objectStorage: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - twitter: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - github: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - discord: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - serviceWorker: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - miauth: { - type: 'boolean' as const, - optional: true as const, nullable: false as const, - default: true - }, - } - }, - userStarForReactionFallback: { - type: 'boolean' as const, - optional: true as const, nullable: false as const, - }, - pinnedUsers: { - type: 'array' as const, - optional: true as const, nullable: false as const, - items: { - type: 'string' as const, - optional: false as const, nullable: false as const - } - }, - hiddenTags: { - type: 'array' as const, - optional: true as const, nullable: false as const, - items: { - type: 'string' as const, - optional: false as const, nullable: false as const - } - }, - blockedHosts: { - type: 'array' as const, - optional: true as const, nullable: false as const, - items: { - type: 'string' as const, - optional: false as const, nullable: false as const - } - }, - hcaptchaSecretKey: { - type: 'string' as const, - optional: true as const, nullable: true as const - }, - recaptchaSecretKey: { - type: 'string' as const, - optional: true as const, nullable: true as const - }, - proxyAccountId: { - type: 'string' as const, - optional: true as const, nullable: true as const, - format: 'id' - }, - twitterConsumerKey: { - type: 'string' as const, - optional: true as const, nullable: true as const - }, - twitterConsumerSecret: { - type: 'string' as const, - optional: true as const, nullable: true as const - }, - githubClientId: { - type: 'string' as const, - optional: true as const, nullable: true as const - }, - githubClientSecret: { - type: 'string' as const, - optional: true as const, nullable: true as const - }, - discordClientId: { - type: 'string' as const, - optional: true as const, nullable: true as const - }, - discordClientSecret: { - type: 'string' as const, - optional: true as const, nullable: true as const - }, - summaryProxy: { - type: 'string' as const, - optional: true as const, nullable: true as const - }, - email: { - type: 'string' as const, - optional: true as const, nullable: true as const - }, - smtpSecure: { - type: 'boolean' as const, - optional: true as const, nullable: false as const - }, - smtpHost: { - type: 'string' as const, - optional: true as const, nullable: true as const - }, - smtpPort: { - type: 'string' as const, - optional: true as const, nullable: true as const - }, - smtpUser: { - type: 'string' as const, - optional: true as const, nullable: true as const - }, - smtpPass: { - type: 'string' as const, - optional: true as const, nullable: true as const - }, - swPrivateKey: { - type: 'string' as const, - optional: true as const, nullable: true as const - }, - useObjectStorage: { - type: 'boolean' as const, - optional: true as const, nullable: false as const - }, - objectStorageBaseUrl: { - type: 'string' as const, - optional: true as const, nullable: true as const - }, - objectStorageBucket: { - type: 'string' as const, - optional: true as const, nullable: true as const - }, - objectStoragePrefix: { - type: 'string' as const, - optional: true as const, nullable: true as const - }, - objectStorageEndpoint: { - type: 'string' as const, - optional: true as const, nullable: true as const - }, - objectStorageRegion: { - type: 'string' as const, - optional: true as const, nullable: true as const - }, - objectStoragePort: { - type: 'number' as const, - optional: true as const, nullable: true as const - }, - objectStorageAccessKey: { - type: 'string' as const, - optional: true as const, nullable: true as const - }, - objectStorageSecretKey: { - type: 'string' as const, - optional: true as const, nullable: true as const - }, - objectStorageUseSSL: { - type: 'boolean' as const, - optional: true as const, nullable: false as const - }, - objectStorageUseProxy: { - type: 'boolean' as const, - optional: true as const, nullable: false as const - }, - objectStorageSetPublicRead: { - type: 'boolean' as const, - optional: true as const, nullable: false as const - } - } - } -}; - -export default define(meta, async (ps, me) => { - const instance = await fetchMeta(true); - - const emojis = await Emojis.find({ - where: { - host: null - }, - order: { - category: 'ASC', - name: 'ASC' - }, - cache: { - id: 'meta_emojis', - milliseconds: 3600000 // 1 hour - } - }); - - const ads = await Ads.find({ - where: { - expiresAt: MoreThan(new Date()) - }, - }); - - const response: any = { - maintainerName: instance.maintainerName, - maintainerEmail: instance.maintainerEmail, - - version: config.version, - - name: instance.name, - uri: config.url, - description: instance.description, - langs: instance.langs, - tosUrl: instance.ToSUrl, - repositoryUrl: instance.repositoryUrl, - feedbackUrl: instance.feedbackUrl, - - secure: config.https != null, - - disableRegistration: instance.disableRegistration, - disableLocalTimeline: instance.disableLocalTimeline, - disableGlobalTimeline: instance.disableGlobalTimeline, - driveCapacityPerLocalUserMb: instance.localDriveCapacityMb, - driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb, - emailRequiredForSignup: instance.emailRequiredForSignup, - enableHcaptcha: instance.enableHcaptcha, - hcaptchaSiteKey: instance.hcaptchaSiteKey, - enableRecaptcha: instance.enableRecaptcha, - recaptchaSiteKey: instance.recaptchaSiteKey, - swPublickey: instance.swPublicKey, - mascotImageUrl: instance.mascotImageUrl, - bannerUrl: instance.bannerUrl, - errorImageUrl: instance.errorImageUrl, - iconUrl: instance.iconUrl, - backgroundImageUrl: instance.backgroundImageUrl, - logoImageUrl: instance.logoImageUrl, - maxNoteTextLength: Math.min(instance.maxNoteTextLength, DB_MAX_NOTE_TEXT_LENGTH), - emojis: await Emojis.packMany(emojis), - ads: ads.map(ad => ({ - id: ad.id, - url: ad.url, - place: ad.place, - ratio: ad.ratio, - imageUrl: ad.imageUrl, - })), - enableEmail: instance.enableEmail, - - enableTwitterIntegration: instance.enableTwitterIntegration, - enableGithubIntegration: instance.enableGithubIntegration, - enableDiscordIntegration: instance.enableDiscordIntegration, - - enableServiceWorker: instance.enableServiceWorker, - - translatorAvailable: instance.deeplAuthKey != null, - - ...(ps.detail ? { - pinnedPages: instance.pinnedPages, - pinnedClipId: instance.pinnedClipId, - cacheRemoteFiles: instance.cacheRemoteFiles, - proxyRemoteFiles: instance.proxyRemoteFiles, - requireSetup: (await Users.count({ - host: null, - })) === 0, - } : {}) - }; - - if (ps.detail) { - const proxyAccount = instance.proxyAccountId ? await Users.pack(instance.proxyAccountId).catch(() => null) : null; - - response.proxyAccountName = proxyAccount ? proxyAccount.username : null; - response.features = { - registration: !instance.disableRegistration, - localTimeLine: !instance.disableLocalTimeline, - globalTimeLine: !instance.disableGlobalTimeline, - emailRequiredForSignup: instance.emailRequiredForSignup, - elasticsearch: config.elasticsearch ? true : false, - hcaptcha: instance.enableHcaptcha, - recaptcha: instance.enableRecaptcha, - objectStorage: instance.useObjectStorage, - twitter: instance.enableTwitterIntegration, - github: instance.enableGithubIntegration, - discord: instance.enableDiscordIntegration, - serviceWorker: instance.enableServiceWorker, - miauth: true, - }; - - if (me && me.isAdmin) { - response.useStarForReactionFallback = instance.useStarForReactionFallback; - response.pinnedUsers = instance.pinnedUsers; - response.hiddenTags = instance.hiddenTags; - response.blockedHosts = instance.blockedHosts; - response.hcaptchaSecretKey = instance.hcaptchaSecretKey; - response.recaptchaSecretKey = instance.recaptchaSecretKey; - response.proxyAccountId = instance.proxyAccountId; - response.twitterConsumerKey = instance.twitterConsumerKey; - response.twitterConsumerSecret = instance.twitterConsumerSecret; - response.githubClientId = instance.githubClientId; - response.githubClientSecret = instance.githubClientSecret; - response.discordClientId = instance.discordClientId; - response.discordClientSecret = instance.discordClientSecret; - response.summalyProxy = instance.summalyProxy; - response.email = instance.email; - response.smtpSecure = instance.smtpSecure; - response.smtpHost = instance.smtpHost; - response.smtpPort = instance.smtpPort; - response.smtpUser = instance.smtpUser; - response.smtpPass = instance.smtpPass; - response.swPrivateKey = instance.swPrivateKey; - response.useObjectStorage = instance.useObjectStorage; - response.objectStorageBaseUrl = instance.objectStorageBaseUrl; - response.objectStorageBucket = instance.objectStorageBucket; - response.objectStoragePrefix = instance.objectStoragePrefix; - response.objectStorageEndpoint = instance.objectStorageEndpoint; - response.objectStorageRegion = instance.objectStorageRegion; - response.objectStoragePort = instance.objectStoragePort; - response.objectStorageAccessKey = instance.objectStorageAccessKey; - response.objectStorageSecretKey = instance.objectStorageSecretKey; - response.objectStorageUseSSL = instance.objectStorageUseSSL; - response.objectStorageUseProxy = instance.objectStorageUseProxy; - response.objectStorageSetPublicRead = instance.objectStorageSetPublicRead; - response.objectStorageS3ForcePathStyle = instance.objectStorageS3ForcePathStyle; - response.deeplAuthKey = instance.deeplAuthKey; - response.deeplIsPro = instance.deeplIsPro; - } - } - - return response; -}); diff --git a/src/server/api/endpoints/miauth/gen-token.ts b/src/server/api/endpoints/miauth/gen-token.ts deleted file mode 100644 index 321fa42fc9..0000000000 --- a/src/server/api/endpoints/miauth/gen-token.ts +++ /dev/null @@ -1,72 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { AccessTokens } from '@/models/index'; -import { genId } from '@/misc/gen-id'; -import { secureRndstr } from '@/misc/secure-rndstr'; - -export const meta = { - tags: ['auth'], - - requireCredential: true as const, - - secure: true, - - params: { - session: { - validator: $.nullable.str - }, - - name: { - validator: $.nullable.optional.str - }, - - description: { - validator: $.nullable.optional.str, - }, - - iconUrl: { - validator: $.nullable.optional.str, - }, - - permission: { - validator: $.arr($.str).unique(), - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - token: { - type: 'string' as const, - optional: false as const, nullable: false as const - } - } - } -}; - -export default define(meta, async (ps, user) => { - // Generate access token - const accessToken = secureRndstr(32, true); - - const now = new Date(); - - // Insert access token doc - await AccessTokens.insert({ - id: genId(), - createdAt: now, - lastUsedAt: now, - session: ps.session, - userId: user.id, - token: accessToken, - hash: accessToken, - name: ps.name, - description: ps.description, - iconUrl: ps.iconUrl, - permission: ps.permission, - }); - - return { - token: accessToken - }; -}); diff --git a/src/server/api/endpoints/mute/create.ts b/src/server/api/endpoints/mute/create.ts deleted file mode 100644 index 3fc64d3eba..0000000000 --- a/src/server/api/endpoints/mute/create.ts +++ /dev/null @@ -1,83 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { getUser } from '../../common/getters'; -import { genId } from '@/misc/gen-id'; -import { Mutings, NoteWatchings } from '@/models/index'; -import { Muting } from '@/models/entities/muting'; -import { publishUserEvent } from '@/services/stream'; - -export const meta = { - tags: ['account'], - - requireCredential: true as const, - - kind: 'write:mutes', - - params: { - userId: { - validator: $.type(ID), - }, - }, - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '6fef56f3-e765-4957-88e5-c6f65329b8a5' - }, - - muteeIsYourself: { - message: 'Mutee is yourself.', - code: 'MUTEE_IS_YOURSELF', - id: 'a4619cb2-5f23-484b-9301-94c903074e10' - }, - - alreadyMuting: { - message: 'You are already muting that user.', - code: 'ALREADY_MUTING', - id: '7e7359cb-160c-4956-b08f-4d1c653cd007' - }, - } -}; - -export default define(meta, async (ps, user) => { - const muter = user; - - // 自分自身 - if (user.id === ps.userId) { - throw new ApiError(meta.errors.muteeIsYourself); - } - - // Get mutee - const mutee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - // Check if already muting - const exist = await Mutings.findOne({ - muterId: muter.id, - muteeId: mutee.id - }); - - if (exist != null) { - throw new ApiError(meta.errors.alreadyMuting); - } - - // Create mute - await Mutings.insert({ - id: genId(), - createdAt: new Date(), - muterId: muter.id, - muteeId: mutee.id, - } as Muting); - - publishUserEvent(user.id, 'mute', mutee); - - NoteWatchings.delete({ - userId: muter.id, - noteUserId: mutee.id - }); -}); diff --git a/src/server/api/endpoints/mute/delete.ts b/src/server/api/endpoints/mute/delete.ts deleted file mode 100644 index 3ffd1f4562..0000000000 --- a/src/server/api/endpoints/mute/delete.ts +++ /dev/null @@ -1,73 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { getUser } from '../../common/getters'; -import { Mutings } from '@/models/index'; -import { publishUserEvent } from '@/services/stream'; - -export const meta = { - tags: ['account'], - - requireCredential: true as const, - - kind: 'write:mutes', - - params: { - userId: { - validator: $.type(ID), - }, - }, - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: 'b851d00b-8ab1-4a56-8b1b-e24187cb48ef' - }, - - muteeIsYourself: { - message: 'Mutee is yourself.', - code: 'MUTEE_IS_YOURSELF', - id: 'f428b029-6b39-4d48-a1d2-cc1ae6dd5cf9' - }, - - notMuting: { - message: 'You are not muting that user.', - code: 'NOT_MUTING', - id: '5467d020-daa9-4553-81e1-135c0c35a96d' - }, - } -}; - -export default define(meta, async (ps, user) => { - const muter = user; - - // Check if the mutee is yourself - if (user.id === ps.userId) { - throw new ApiError(meta.errors.muteeIsYourself); - } - - // Get mutee - const mutee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - // Check not muting - const exist = await Mutings.findOne({ - muterId: muter.id, - muteeId: mutee.id - }); - - if (exist == null) { - throw new ApiError(meta.errors.notMuting); - } - - // Delete mute - await Mutings.delete({ - id: exist.id - }); - - publishUserEvent(user.id, 'unmute', mutee); -}); diff --git a/src/server/api/endpoints/mute/list.ts b/src/server/api/endpoints/mute/list.ts deleted file mode 100644 index ae4c3a719d..0000000000 --- a/src/server/api/endpoints/mute/list.ts +++ /dev/null @@ -1,49 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { makePaginationQuery } from '../../common/make-pagination-query'; -import { Mutings } from '@/models/index'; - -export const meta = { - tags: ['account'], - - requireCredential: true as const, - - kind: 'read:mutes', - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 30 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Muting', - } - }, -}; - -export default define(meta, async (ps, me) => { - const query = makePaginationQuery(Mutings.createQueryBuilder('muting'), ps.sinceId, ps.untilId) - .andWhere(`muting.muterId = :meId`, { meId: me.id }); - - const mutings = await query - .take(ps.limit!) - .getMany(); - - return await Mutings.packMany(mutings, me); -}); diff --git a/src/server/api/endpoints/my/apps.ts b/src/server/api/endpoints/my/apps.ts deleted file mode 100644 index d91562b62f..0000000000 --- a/src/server/api/endpoints/my/apps.ts +++ /dev/null @@ -1,86 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { Apps } from '@/models/index'; - -export const meta = { - tags: ['account', 'app'], - - requireCredential: true as const, - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - offset: { - validator: $.optional.num.min(0), - default: 0 - } - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - optional: false as const, nullable: false as const - }, - name: { - type: 'string' as const, - optional: false as const, nullable: false as const - }, - callbackUrl: { - type: 'string' as const, - optional: false as const, nullable: false as const - }, - permission: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'string' as const, - optional: false as const, nullable: false as const - } - }, - secret: { - type: 'string' as const, - optional: true as const, nullable: false as const - }, - isAuthorized: { - type: 'object' as const, - optional: true as const, nullable: false as const, - properties: { - appId: { - type: 'string' as const, - optional: false as const, nullable: false as const - }, - userId: { - type: 'string' as const, - optional: false as const, nullable: false as const - } - } - } - } - } - } -}; - -export default define(meta, async (ps, user) => { - const query = { - userId: user.id - }; - - const apps = await Apps.find({ - where: query, - take: ps.limit!, - skip: ps.offset, - }); - - return await Promise.all(apps.map(app => Apps.pack(app, user, { - detail: true - }))); -}); diff --git a/src/server/api/endpoints/notes.ts b/src/server/api/endpoints/notes.ts deleted file mode 100644 index a3f6e187f2..0000000000 --- a/src/server/api/endpoints/notes.ts +++ /dev/null @@ -1,94 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../define'; -import { makePaginationQuery } from '../common/make-pagination-query'; -import { Notes } from '@/models/index'; - -export const meta = { - tags: ['notes'], - - params: { - local: { - validator: $.optional.bool, - }, - - reply: { - validator: $.optional.bool, - }, - - renote: { - validator: $.optional.bool, - }, - - withFiles: { - validator: $.optional.bool, - }, - - poll: { - validator: $.optional.bool, - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Note', - } - }, -}; - -export default define(meta, async (ps) => { - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere(`note.visibility = 'public'`) - .andWhere(`note.localOnly = FALSE`) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - if (ps.local) { - query.andWhere('note.userHost IS NULL'); - } - - if (ps.reply != undefined) { - query.andWhere(ps.reply ? 'note.replyId IS NOT NULL' : 'note.replyId IS NULL'); - } - - if (ps.renote != undefined) { - query.andWhere(ps.renote ? 'note.renoteId IS NOT NULL' : 'note.renoteId IS NULL'); - } - - if (ps.withFiles != undefined) { - query.andWhere(ps.withFiles ? `note.fileIds != '{}'` : `note.fileIds = '{}'`); - } - - if (ps.poll != undefined) { - query.andWhere(ps.poll ? 'note.hasPoll = TRUE' : 'note.hasPoll = FALSE'); - } - - // TODO - //if (bot != undefined) { - // query.isBot = bot; - //} - - const notes = await query.take(ps.limit!).getMany(); - - return await Notes.packMany(notes); -}); diff --git a/src/server/api/endpoints/notes/children.ts b/src/server/api/endpoints/notes/children.ts deleted file mode 100644 index 68881fda9e..0000000000 --- a/src/server/api/endpoints/notes/children.ts +++ /dev/null @@ -1,72 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { makePaginationQuery } from '../../common/make-pagination-query'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; -import { Brackets } from 'typeorm'; -import { Notes } from '@/models/index'; -import { generateBlockedUserQuery } from '../../common/generate-block-query'; - -export const meta = { - tags: ['notes'], - - requireCredential: false as const, - - params: { - noteId: { - validator: $.type(ID), - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Note', - } - }, -}; - -export default define(meta, async (ps, user) => { - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere(new Brackets(qb => { qb - .where(`note.replyId = :noteId`, { noteId: ps.noteId }) - .orWhere(new Brackets(qb => { qb - .where(`note.renoteId = :noteId`, { noteId: ps.noteId }) - .andWhere(new Brackets(qb => { qb - .where(`note.text IS NOT NULL`) - .orWhere(`note.fileIds != '{}'`) - .orWhere(`note.hasPoll = TRUE`); - })); - })); - })) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - generateVisibilityQuery(query, user); - if (user) generateMutedUserQuery(query, user); - if (user) generateBlockedUserQuery(query, user); - - const notes = await query.take(ps.limit!).getMany(); - - return await Notes.packMany(notes, user); -}); diff --git a/src/server/api/endpoints/notes/clips.ts b/src/server/api/endpoints/notes/clips.ts deleted file mode 100644 index 6b303d87ec..0000000000 --- a/src/server/api/endpoints/notes/clips.ts +++ /dev/null @@ -1,55 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ClipNotes, Clips } from '@/models/index'; -import { getNote } from '../../common/getters'; -import { ApiError } from '../../error'; -import { In } from 'typeorm'; - -export const meta = { - tags: ['clips', 'notes'], - - requireCredential: false as const, - - params: { - noteId: { - validator: $.type(ID), - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Note', - } - }, - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '47db1a1c-b0af-458d-8fb4-986e4efafe1e' - } - } -}; - -export default define(meta, async (ps, me) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - const clipNotes = await ClipNotes.find({ - noteId: note.id, - }); - - const clips = await Clips.find({ - id: In(clipNotes.map(x => x.clipId)), - isPublic: true - }); - - return await Promise.all(clips.map(x => Clips.pack(x))); -}); diff --git a/src/server/api/endpoints/notes/conversation.ts b/src/server/api/endpoints/notes/conversation.ts deleted file mode 100644 index 0fe323ea00..0000000000 --- a/src/server/api/endpoints/notes/conversation.ts +++ /dev/null @@ -1,81 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { getNote } from '../../common/getters'; -import { Note } from '@/models/entities/note'; -import { Notes } from '@/models/index'; - -export const meta = { - tags: ['notes'], - - requireCredential: false as const, - - params: { - noteId: { - validator: $.type(ID), - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - offset: { - validator: $.optional.num.min(0), - default: 0 - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Note', - } - }, - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: 'e1035875-9551-45ec-afa8-1ded1fcb53c8' - } - } -}; - -export default define(meta, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - const conversation: Note[] = []; - let i = 0; - - async function get(id: any) { - i++; - const p = await Notes.findOne(id); - if (p == null) return; - - if (i > ps.offset!) { - conversation.push(p); - } - - if (conversation.length == ps.limit!) { - return; - } - - if (p.replyId) { - await get(p.replyId); - } - } - - if (note.replyId) { - await get(note.replyId); - } - - return await Notes.packMany(conversation, user); -}); diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts deleted file mode 100644 index 751673f955..0000000000 --- a/src/server/api/endpoints/notes/create.ts +++ /dev/null @@ -1,299 +0,0 @@ -import $ from 'cafy'; -import * as ms from 'ms'; -import { length } from 'stringz'; -import create from '@/services/note/create'; -import define from '../../define'; -import { fetchMeta } from '@/misc/fetch-meta'; -import { ApiError } from '../../error'; -import { ID } from '@/misc/cafy-id'; -import { User } from '@/models/entities/user'; -import { Users, DriveFiles, Notes, Channels, Blockings } from '@/models/index'; -import { DriveFile } from '@/models/entities/drive-file'; -import { Note } from '@/models/entities/note'; -import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits'; -import { noteVisibilities } from '../../../../types'; -import { Channel } from '@/models/entities/channel'; - -let maxNoteTextLength = 500; - -setInterval(() => { - fetchMeta().then(m => { - maxNoteTextLength = m.maxNoteTextLength; - }); -}, 3000); - -export const meta = { - tags: ['notes'], - - requireCredential: true as const, - - limit: { - duration: ms('1hour'), - max: 300 - }, - - kind: 'write:notes', - - params: { - visibility: { - validator: $.optional.str.or(noteVisibilities as unknown as string[]), - default: 'public', - }, - - visibleUserIds: { - validator: $.optional.arr($.type(ID)).unique().min(0), - }, - - text: { - validator: $.optional.nullable.str.pipe(text => - text.trim() != '' - && length(text.trim()) <= maxNoteTextLength - && Array.from(text.trim()).length <= DB_MAX_NOTE_TEXT_LENGTH // DB limit - ), - default: null, - }, - - cw: { - validator: $.optional.nullable.str.pipe(Notes.validateCw), - }, - - viaMobile: { - validator: $.optional.bool, - default: false, - }, - - localOnly: { - validator: $.optional.bool, - default: false, - }, - - noExtractMentions: { - validator: $.optional.bool, - default: false, - }, - - noExtractHashtags: { - validator: $.optional.bool, - default: false, - }, - - noExtractEmojis: { - validator: $.optional.bool, - default: false, - }, - - fileIds: { - validator: $.optional.arr($.type(ID)).unique().range(1, 4), - }, - - mediaIds: { - validator: $.optional.arr($.type(ID)).unique().range(1, 4), - deprecated: true, - }, - - replyId: { - validator: $.optional.nullable.type(ID), - }, - - renoteId: { - validator: $.optional.nullable.type(ID), - }, - - channelId: { - validator: $.optional.nullable.type(ID), - }, - - poll: { - validator: $.optional.nullable.obj({ - choices: $.arr($.str) - .unique() - .range(2, 10) - .each(c => c.length > 0 && c.length < 50), - multiple: $.optional.bool, - expiresAt: $.optional.nullable.num.int(), - expiredAfter: $.optional.nullable.num.int().min(1) - }).strict(), - ref: 'poll' - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - createdNote: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Note', - } - } - }, - - errors: { - noSuchRenoteTarget: { - message: 'No such renote target.', - code: 'NO_SUCH_RENOTE_TARGET', - id: 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4' - }, - - cannotReRenote: { - message: 'You can not Renote a pure Renote.', - code: 'CANNOT_RENOTE_TO_A_PURE_RENOTE', - id: 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a' - }, - - noSuchReplyTarget: { - message: 'No such reply target.', - code: 'NO_SUCH_REPLY_TARGET', - id: '749ee0f6-d3da-459a-bf02-282e2da4292c' - }, - - cannotReplyToPureRenote: { - message: 'You can not reply to a pure Renote.', - code: 'CANNOT_REPLY_TO_A_PURE_RENOTE', - id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15' - }, - - contentRequired: { - message: 'Content required. You need to set text, fileIds, renoteId or poll.', - code: 'CONTENT_REQUIRED', - id: '6f57e42b-c348-439b-bc45-993995cc515a' - }, - - cannotCreateAlreadyExpiredPoll: { - message: 'Poll is already expired.', - code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL', - id: '04da457d-b083-4055-9082-955525eda5a5' - }, - - noSuchChannel: { - message: 'No such channel.', - code: 'NO_SUCH_CHANNEL', - id: 'b1653923-5453-4edc-b786-7c4f39bb0bbb' - }, - - youHaveBeenBlocked: { - message: 'You have been blocked by this user.', - code: 'YOU_HAVE_BEEN_BLOCKED', - id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3' - }, - } -}; - -export default define(meta, async (ps, user) => { - let visibleUsers: User[] = []; - if (ps.visibleUserIds) { - visibleUsers = (await Promise.all(ps.visibleUserIds.map(id => Users.findOne(id)))) - .filter(x => x != null) as User[]; - } - - let files: DriveFile[] = []; - const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null; - if (fileIds != null) { - files = (await Promise.all(fileIds.map(fileId => - DriveFiles.findOne({ - id: fileId, - userId: user.id - }) - ))).filter(file => file != null) as DriveFile[]; - } - - let renote: Note | undefined; - if (ps.renoteId != null) { - // Fetch renote to note - renote = await Notes.findOne(ps.renoteId); - - if (renote == null) { - throw new ApiError(meta.errors.noSuchRenoteTarget); - } else if (renote.renoteId && !renote.text && !renote.fileIds) { - throw new ApiError(meta.errors.cannotReRenote); - } - - // Check blocking - if (renote.userId !== user.id) { - const block = await Blockings.findOne({ - blockerId: renote.userId, - blockeeId: user.id, - }); - if (block) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } - } - } - - let reply: Note | undefined; - if (ps.replyId != null) { - // Fetch reply - reply = await Notes.findOne(ps.replyId); - - if (reply == null) { - throw new ApiError(meta.errors.noSuchReplyTarget); - } - - // 返信対象が引用でないRenoteだったらエラー - if (reply.renoteId && !reply.text && !reply.fileIds) { - throw new ApiError(meta.errors.cannotReplyToPureRenote); - } - - // Check blocking - if (reply.userId !== user.id) { - const block = await Blockings.findOne({ - blockerId: reply.userId, - blockeeId: user.id, - }); - if (block) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } - } - } - - if (ps.poll) { - if (typeof ps.poll.expiresAt === 'number') { - if (ps.poll.expiresAt < Date.now()) - throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll); - } else if (typeof ps.poll.expiredAfter === 'number') { - ps.poll.expiresAt = Date.now() + ps.poll.expiredAfter; - } - } - - // テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー - if (!(ps.text || files.length || renote || ps.poll)) { - throw new ApiError(meta.errors.contentRequired); - } - - let channel: Channel | undefined; - if (ps.channelId != null) { - channel = await Channels.findOne(ps.channelId); - - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); - } - } - - // 投稿を作成 - const note = await create(user, { - createdAt: new Date(), - files: files, - poll: ps.poll ? { - choices: ps.poll.choices, - multiple: ps.poll.multiple || false, - expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null - } : undefined, - text: ps.text || undefined, - reply, - renote, - cw: ps.cw, - viaMobile: ps.viaMobile, - localOnly: ps.localOnly, - visibility: ps.visibility, - visibleUsers, - channel, - apMentions: ps.noExtractMentions ? [] : undefined, - apHashtags: ps.noExtractHashtags ? [] : undefined, - apEmojis: ps.noExtractEmojis ? [] : undefined, - }); - - return { - createdNote: await Notes.pack(note, user) - }; -}); diff --git a/src/server/api/endpoints/notes/delete.ts b/src/server/api/endpoints/notes/delete.ts deleted file mode 100644 index 7163a2b9d2..0000000000 --- a/src/server/api/endpoints/notes/delete.ts +++ /dev/null @@ -1,56 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import deleteNote from '@/services/note/delete'; -import define from '../../define'; -import * as ms from 'ms'; -import { getNote } from '../../common/getters'; -import { ApiError } from '../../error'; -import { Users } from '@/models/index'; - -export const meta = { - tags: ['notes'], - - requireCredential: true as const, - - kind: 'write:notes', - - limit: { - duration: ms('1hour'), - max: 300, - minInterval: ms('1sec') - }, - - params: { - noteId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '490be23f-8c1f-4796-819f-94cb4f9d1630' - }, - - accessDenied: { - message: 'Access denied.', - code: 'ACCESS_DENIED', - id: 'fe8d7103-0ea8-4ec3-814d-f8b401dc69e9' - } - } -}; - -export default define(meta, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - if (!user.isAdmin && !user.isModerator && (note.userId !== user.id)) { - throw new ApiError(meta.errors.accessDenied); - } - - // この操作を行うのが投稿者とは限らない(例えばモデレーター)ため - await deleteNote(await Users.findOneOrFail(note.userId), note); -}); diff --git a/src/server/api/endpoints/notes/favorites/create.ts b/src/server/api/endpoints/notes/favorites/create.ts deleted file mode 100644 index 1bb25edd7f..0000000000 --- a/src/server/api/endpoints/notes/favorites/create.ts +++ /dev/null @@ -1,61 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { getNote } from '../../../common/getters'; -import { NoteFavorites } from '@/models/index'; -import { genId } from '@/misc/gen-id'; - -export const meta = { - tags: ['notes', 'favorites'], - - requireCredential: true as const, - - kind: 'write:favorites', - - params: { - noteId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '6dd26674-e060-4816-909a-45ba3f4da458' - }, - - alreadyFavorited: { - message: 'The note has already been marked as a favorite.', - code: 'ALREADY_FAVORITED', - id: 'a402c12b-34dd-41d2-97d8-4d2ffd96a1a6' - }, - } -}; - -export default define(meta, async (ps, user) => { - // Get favoritee - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - // if already favorited - const exist = await NoteFavorites.findOne({ - noteId: note.id, - userId: user.id - }); - - if (exist != null) { - throw new ApiError(meta.errors.alreadyFavorited); - } - - // Create favorite - await NoteFavorites.insert({ - id: genId(), - createdAt: new Date(), - noteId: note.id, - userId: user.id - }); -}); diff --git a/src/server/api/endpoints/notes/favorites/delete.ts b/src/server/api/endpoints/notes/favorites/delete.ts deleted file mode 100644 index 75eb9a359a..0000000000 --- a/src/server/api/endpoints/notes/favorites/delete.ts +++ /dev/null @@ -1,55 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { getNote } from '../../../common/getters'; -import { NoteFavorites } from '@/models/index'; - -export const meta = { - tags: ['notes', 'favorites'], - - requireCredential: true as const, - - kind: 'write:favorites', - - params: { - noteId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '80848a2c-398f-4343-baa9-df1d57696c56' - }, - - notFavorited: { - message: 'You have not marked that note a favorite.', - code: 'NOT_FAVORITED', - id: 'b625fc69-635e-45e9-86f4-dbefbef35af5' - }, - } -}; - -export default define(meta, async (ps, user) => { - // Get favoritee - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - // if already favorited - const exist = await NoteFavorites.findOne({ - noteId: note.id, - userId: user.id - }); - - if (exist == null) { - throw new ApiError(meta.errors.notFavorited); - } - - // Delete favorite - await NoteFavorites.delete(exist.id); -}); diff --git a/src/server/api/endpoints/notes/featured.ts b/src/server/api/endpoints/notes/featured.ts deleted file mode 100644 index 8d33c0e73d..0000000000 --- a/src/server/api/endpoints/notes/featured.ts +++ /dev/null @@ -1,64 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; -import { Notes } from '@/models/index'; -import { generateBlockedUserQuery } from '../../common/generate-block-query'; - -export const meta = { - tags: ['notes'], - - requireCredential: false as const, - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10, - }, - - offset: { - validator: $.optional.num.min(0), - default: 0 - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Note', - } - }, -}; - -export default define(meta, async (ps, user) => { - const max = 30; - const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで - - const query = Notes.createQueryBuilder('note') - .addSelect('note.score') - .where('note.userHost IS NULL') - .andWhere(`note.score > 0`) - .andWhere(`note.createdAt > :date`, { date: new Date(Date.now() - day) }) - .andWhere(`note.visibility = 'public'`) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - if (user) generateMutedUserQuery(query, user); - if (user) generateBlockedUserQuery(query, user); - - let notes = await query - .orderBy('note.score', 'DESC') - .take(max) - .getMany(); - - notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); - - notes = notes.slice(ps.offset, ps.offset + ps.limit); - - return await Notes.packMany(notes, user); -}); diff --git a/src/server/api/endpoints/notes/global-timeline.ts b/src/server/api/endpoints/notes/global-timeline.ts deleted file mode 100644 index 5902c0415c..0000000000 --- a/src/server/api/endpoints/notes/global-timeline.ts +++ /dev/null @@ -1,101 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { fetchMeta } from '@/misc/fetch-meta'; -import { ApiError } from '../../error'; -import { makePaginationQuery } from '../../common/make-pagination-query'; -import { Notes } from '@/models/index'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; -import { activeUsersChart } from '@/services/chart/index'; -import { generateRepliesQuery } from '../../common/generate-replies-query'; -import { generateMutedNoteQuery } from '../../common/generate-muted-note-query'; -import { generateBlockedUserQuery } from '../../common/generate-block-query'; - -export const meta = { - tags: ['notes'], - - params: { - withFiles: { - validator: $.optional.bool, - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - sinceDate: { - validator: $.optional.num - }, - - untilDate: { - validator: $.optional.num - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Note', - } - }, - - errors: { - gtlDisabled: { - message: 'Global timeline has been disabled.', - code: 'GTL_DISABLED', - id: '0332fc13-6ab2-4427-ae80-a9fadffd1a6b' - }, - } -}; - -export default define(meta, async (ps, user) => { - const m = await fetchMeta(); - if (m.disableGlobalTimeline) { - if (user == null || (!user.isAdmin && !user.isModerator)) { - throw new ApiError(meta.errors.gtlDisabled); - } - } - - //#region Construct query - const query = makePaginationQuery(Notes.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.visibility = \'public\'') - .andWhere('note.channelId IS NULL') - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - generateRepliesQuery(query, user); - if (user) generateMutedUserQuery(query, user); - if (user) generateMutedNoteQuery(query, user); - if (user) generateBlockedUserQuery(query, user); - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - //#endregion - - const timeline = await query.take(ps.limit!).getMany(); - - process.nextTick(() => { - if (user) { - activeUsersChart.update(user); - } - }); - - return await Notes.packMany(timeline, user); -}); diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/hybrid-timeline.ts deleted file mode 100644 index 47f08f208b..0000000000 --- a/src/server/api/endpoints/notes/hybrid-timeline.ts +++ /dev/null @@ -1,158 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { fetchMeta } from '@/misc/fetch-meta'; -import { ApiError } from '../../error'; -import { makePaginationQuery } from '../../common/make-pagination-query'; -import { Followings, Notes } from '@/models/index'; -import { Brackets } from 'typeorm'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; -import { activeUsersChart } from '@/services/chart/index'; -import { generateRepliesQuery } from '../../common/generate-replies-query'; -import { generateMutedNoteQuery } from '../../common/generate-muted-note-query'; -import { generateChannelQuery } from '../../common/generate-channel-query'; -import { generateBlockedUserQuery } from '../../common/generate-block-query'; - -export const meta = { - tags: ['notes'], - - requireCredential: true as const, - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10, - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - sinceDate: { - validator: $.optional.num, - }, - - untilDate: { - validator: $.optional.num, - }, - - includeMyRenotes: { - validator: $.optional.bool, - default: true, - }, - - includeRenotedMyNotes: { - validator: $.optional.bool, - default: true, - }, - - includeLocalRenotes: { - validator: $.optional.bool, - default: true, - }, - - withFiles: { - validator: $.optional.bool, - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Note', - } - }, - - errors: { - stlDisabled: { - message: 'Hybrid timeline has been disabled.', - code: 'STL_DISABLED', - id: '620763f4-f621-4533-ab33-0577a1a3c342' - }, - } -}; - -export default define(meta, async (ps, user) => { - const m = await fetchMeta(); - if (m.disableLocalTimeline && !user.isAdmin && !user.isModerator) { - throw new ApiError(meta.errors.stlDisabled); - } - - //#region Construct query - const followingQuery = Followings.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: user.id }); - - const query = makePaginationQuery(Notes.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere(new Brackets(qb => { - qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: user.id }) - .orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)'); - })) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .setParameters(followingQuery.getParameters()); - - generateChannelQuery(query, user); - generateRepliesQuery(query, user); - generateVisibilityQuery(query, user); - generateMutedUserQuery(query, user); - generateMutedNoteQuery(query, user); - generateBlockedUserQuery(query, user); - - if (ps.includeMyRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :meId', { meId: user.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeRenotedMyNotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserId != :meId', { meId: user.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeLocalRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserHost IS NOT NULL'); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - //#endregion - - const timeline = await query.take(ps.limit!).getMany(); - - process.nextTick(() => { - if (user) { - activeUsersChart.update(user); - } - }); - - return await Notes.packMany(timeline, user); -}); diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts deleted file mode 100644 index f670d478bf..0000000000 --- a/src/server/api/endpoints/notes/local-timeline.ts +++ /dev/null @@ -1,129 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { fetchMeta } from '@/misc/fetch-meta'; -import { ApiError } from '../../error'; -import { Notes } from '@/models/index'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; -import { makePaginationQuery } from '../../common/make-pagination-query'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query'; -import { activeUsersChart } from '@/services/chart/index'; -import { Brackets } from 'typeorm'; -import { generateRepliesQuery } from '../../common/generate-replies-query'; -import { generateMutedNoteQuery } from '../../common/generate-muted-note-query'; -import { generateChannelQuery } from '../../common/generate-channel-query'; -import { generateBlockedUserQuery } from '../../common/generate-block-query'; - -export const meta = { - tags: ['notes'], - - params: { - withFiles: { - validator: $.optional.bool, - }, - - fileType: { - validator: $.optional.arr($.str), - }, - - excludeNsfw: { - validator: $.optional.bool, - default: false, - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - sinceDate: { - validator: $.optional.num, - }, - - untilDate: { - validator: $.optional.num, - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Note', - } - }, - - errors: { - ltlDisabled: { - message: 'Local timeline has been disabled.', - code: 'LTL_DISABLED', - id: '45a6eb02-7695-4393-b023-dd3be9aaaefd' - }, - } -}; - -export default define(meta, async (ps, user) => { - const m = await fetchMeta(); - if (m.disableLocalTimeline) { - if (user == null || (!user.isAdmin && !user.isModerator)) { - throw new ApiError(meta.errors.ltlDisabled); - } - } - - //#region Construct query - const query = makePaginationQuery(Notes.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)') - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - generateChannelQuery(query, user); - generateRepliesQuery(query, user); - generateVisibilityQuery(query, user); - if (user) generateMutedUserQuery(query, user); - if (user) generateMutedNoteQuery(query, user); - if (user) generateBlockedUserQuery(query, user); - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - - if (ps.fileType != null) { - query.andWhere('note.fileIds != \'{}\''); - query.andWhere(new Brackets(qb => { - for (const type of ps.fileType!) { - const i = ps.fileType!.indexOf(type); - qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type }); - } - })); - - if (ps.excludeNsfw) { - query.andWhere('note.cw IS NULL'); - query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)'); - } - } - //#endregion - - const timeline = await query.take(ps.limit!).getMany(); - - process.nextTick(() => { - if (user) { - activeUsersChart.update(user); - } - }); - - return await Notes.packMany(timeline, user); -}); diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts deleted file mode 100644 index ffaebd6c95..0000000000 --- a/src/server/api/endpoints/notes/mentions.ts +++ /dev/null @@ -1,88 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import read from '@/services/note/read'; -import { Notes, Followings } from '@/models/index'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; -import { makePaginationQuery } from '../../common/make-pagination-query'; -import { Brackets } from 'typeorm'; -import { generateBlockedUserQuery } from '../../common/generate-block-query'; -import { generateMutedNoteThreadQuery } from '../../common/generate-muted-note-thread-query'; - -export const meta = { - tags: ['notes'], - - requireCredential: true as const, - - params: { - following: { - validator: $.optional.bool, - default: false - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - visibility: { - validator: $.optional.str, - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Note', - } - }, -}; - -export default define(meta, async (ps, user) => { - const followingQuery = Followings.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: user.id }); - - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere(new Brackets(qb => { qb - .where(`'{"${user.id}"}' <@ note.mentions`) - .orWhere(`'{"${user.id}"}' <@ note.visibleUserIds`); - })) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - generateVisibilityQuery(query, user); - generateMutedUserQuery(query, user); - generateMutedNoteThreadQuery(query, user); - generateBlockedUserQuery(query, user); - - if (ps.visibility) { - query.andWhere('note.visibility = :visibility', { visibility: ps.visibility }); - } - - if (ps.following) { - query.andWhere(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: user.id }); - query.setParameters(followingQuery.getParameters()); - } - - const mentions = await query.take(ps.limit!).getMany(); - - read(user.id, mentions); - - return await Notes.packMany(mentions, user); -}); diff --git a/src/server/api/endpoints/notes/polls/recommendation.ts b/src/server/api/endpoints/notes/polls/recommendation.ts deleted file mode 100644 index 0763f0c8fd..0000000000 --- a/src/server/api/endpoints/notes/polls/recommendation.ts +++ /dev/null @@ -1,77 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { Polls, Mutings, Notes, PollVotes } from '@/models/index'; -import { Brackets, In } from 'typeorm'; - -export const meta = { - tags: ['notes'], - - requireCredential: true as const, - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - offset: { - validator: $.optional.num.min(0), - default: 0 - } - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Note' - } - } -}; - -export default define(meta, async (ps, user) => { - const query = Polls.createQueryBuilder('poll') - .where('poll.userHost IS NULL') - .andWhere(`poll.userId != :meId`, { meId: user.id }) - .andWhere(`poll.noteVisibility = 'public'`) - .andWhere(new Brackets(qb => { qb - .where('poll.expiresAt IS NULL') - .orWhere('poll.expiresAt > :now', { now: new Date() }); - })); - - //#region exclude arleady voted polls - const votedQuery = PollVotes.createQueryBuilder('vote') - .select('vote.noteId') - .where('vote.userId = :meId', { meId: user.id }); - - query - .andWhere(`poll.noteId NOT IN (${ votedQuery.getQuery() })`); - - query.setParameters(votedQuery.getParameters()); - //#endregion - - //#region mute - const mutingQuery = Mutings.createQueryBuilder('muting') - .select('muting.muteeId') - .where('muting.muterId = :muterId', { muterId: user.id }); - - query - .andWhere(`poll.userId NOT IN (${ mutingQuery.getQuery() })`); - - query.setParameters(mutingQuery.getParameters()); - //#endregion - - const polls = await query.take(ps.limit!).skip(ps.offset).getMany(); - - if (polls.length === 0) return []; - - const notes = await Notes.find({ - id: In(polls.map(poll => poll.noteId)) - }); - - return await Notes.packMany(notes, user, { - detail: true - }); -}); diff --git a/src/server/api/endpoints/notes/polls/vote.ts b/src/server/api/endpoints/notes/polls/vote.ts deleted file mode 100644 index f670501385..0000000000 --- a/src/server/api/endpoints/notes/polls/vote.ts +++ /dev/null @@ -1,170 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import { publishNoteStream } from '@/services/stream'; -import { createNotification } from '@/services/create-notification'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { getNote } from '../../../common/getters'; -import { deliver } from '@/queue/index'; -import { renderActivity } from '@/remote/activitypub/renderer/index'; -import renderVote from '@/remote/activitypub/renderer/vote'; -import { deliverQuestionUpdate } from '@/services/note/polls/update'; -import { PollVotes, NoteWatchings, Users, Polls, Blockings } from '@/models/index'; -import { Not } from 'typeorm'; -import { IRemoteUser } from '@/models/entities/user'; -import { genId } from '@/misc/gen-id'; - -export const meta = { - tags: ['notes'], - - requireCredential: true as const, - - kind: 'write:votes', - - params: { - noteId: { - validator: $.type(ID), - }, - - choice: { - validator: $.num - }, - }, - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: 'ecafbd2e-c283-4d6d-aecb-1a0a33b75396' - }, - - noPoll: { - message: 'The note does not attach a poll.', - code: 'NO_POLL', - id: '5f979967-52d9-4314-a911-1c673727f92f' - }, - - invalidChoice: { - message: 'Choice ID is invalid.', - code: 'INVALID_CHOICE', - id: 'e0cc9a04-f2e8-41e4-a5f1-4127293260cc' - }, - - alreadyVoted: { - message: 'You have already voted.', - code: 'ALREADY_VOTED', - id: '0963fc77-efac-419b-9424-b391608dc6d8' - }, - - alreadyExpired: { - message: 'The poll is already expired.', - code: 'ALREADY_EXPIRED', - id: '1022a357-b085-4054-9083-8f8de358337e' - }, - - youHaveBeenBlocked: { - message: 'You cannot vote this poll because you have been blocked by this user.', - code: 'YOU_HAVE_BEEN_BLOCKED', - id: '85a5377e-b1e9-4617-b0b9-5bea73331e49' - }, - } -}; - -export default define(meta, async (ps, user) => { - const createdAt = new Date(); - - // Get votee - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - if (!note.hasPoll) { - throw new ApiError(meta.errors.noPoll); - } - - // Check blocking - if (note.userId !== user.id) { - const block = await Blockings.findOne({ - blockerId: note.userId, - blockeeId: user.id, - }); - if (block) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } - } - - const poll = await Polls.findOneOrFail({ noteId: note.id }); - - if (poll.expiresAt && poll.expiresAt < createdAt) { - throw new ApiError(meta.errors.alreadyExpired); - } - - if (poll.choices[ps.choice] == null) { - throw new ApiError(meta.errors.invalidChoice); - } - - // if already voted - const exist = await PollVotes.find({ - noteId: note.id, - userId: user.id - }); - - if (exist.length) { - if (poll.multiple) { - if (exist.some(x => x.choice == ps.choice)) - throw new ApiError(meta.errors.alreadyVoted); - } else { - throw new ApiError(meta.errors.alreadyVoted); - } - } - - // Create vote - const vote = await PollVotes.insert({ - id: genId(), - createdAt, - noteId: note.id, - userId: user.id, - choice: ps.choice - }).then(x => PollVotes.findOneOrFail(x.identifiers[0])); - - // Increment votes count - const index = ps.choice + 1; // In SQL, array index is 1 based - await Polls.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`); - - publishNoteStream(note.id, 'pollVoted', { - choice: ps.choice, - userId: user.id - }); - - // Notify - createNotification(note.userId, 'pollVote', { - notifierId: user.id, - noteId: note.id, - choice: ps.choice - }); - - // Fetch watchers - NoteWatchings.find({ - noteId: note.id, - userId: Not(user.id), - }).then(watchers => { - for (const watcher of watchers) { - createNotification(watcher.userId, 'pollVote', { - notifierId: user.id, - noteId: note.id, - choice: ps.choice - }); - } - }); - - // リモート投票の場合リプライ送信 - if (note.userHost != null) { - const pollOwner = await Users.findOneOrFail(note.userId) as IRemoteUser; - - deliver(user, renderActivity(await renderVote(user, vote, note, poll, pollOwner)), pollOwner.inbox); - } - - // リモートフォロワーにUpdate配信 - deliverQuestionUpdate(note.id); -}); diff --git a/src/server/api/endpoints/notes/reactions.ts b/src/server/api/endpoints/notes/reactions.ts deleted file mode 100644 index 09dd6b600b..0000000000 --- a/src/server/api/endpoints/notes/reactions.ts +++ /dev/null @@ -1,90 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { getNote } from '../../common/getters'; -import { ApiError } from '../../error'; -import { NoteReactions } from '@/models/index'; -import { DeepPartial } from 'typeorm'; -import { NoteReaction } from '@/models/entities/note-reaction'; - -export const meta = { - tags: ['notes', 'reactions'], - - requireCredential: false as const, - - params: { - noteId: { - validator: $.type(ID), - }, - - type: { - validator: $.optional.nullable.str, - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - offset: { - validator: $.optional.num, - default: 0 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'NoteReaction', - } - }, - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '263fff3d-d0e1-4af4-bea7-8408059b451a' - } - } -}; - -export default define(meta, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - const query = { - noteId: note.id - } as DeepPartial<NoteReaction>; - - if (ps.type) { - // ローカルリアクションはホスト名が . とされているが - // DB 上ではそうではないので、必要に応じて変換 - const suffix = '@.:'; - const type = ps.type.endsWith(suffix) ? ps.type.slice(0, ps.type.length - suffix.length) + ':' : ps.type; - query.reaction = type; - } - - const reactions = await NoteReactions.find({ - where: query, - take: ps.limit!, - skip: ps.offset, - order: { - id: -1 - } - }); - - return await Promise.all(reactions.map(reaction => NoteReactions.pack(reaction, user))); -}); diff --git a/src/server/api/endpoints/notes/reactions/create.ts b/src/server/api/endpoints/notes/reactions/create.ts deleted file mode 100644 index 24a73a8d4f..0000000000 --- a/src/server/api/endpoints/notes/reactions/create.ts +++ /dev/null @@ -1,57 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import createReaction from '@/services/note/reaction/create'; -import define from '../../../define'; -import { getNote } from '../../../common/getters'; -import { ApiError } from '../../../error'; - -export const meta = { - tags: ['reactions', 'notes'], - - requireCredential: true as const, - - kind: 'write:reactions', - - params: { - noteId: { - validator: $.type(ID), - }, - - reaction: { - validator: $.str, - } - }, - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '033d0620-5bfe-4027-965d-980b0c85a3ea' - }, - - alreadyReacted: { - message: 'You are already reacting to that note.', - code: 'ALREADY_REACTED', - id: '71efcf98-86d6-4e2b-b2ad-9d032369366b' - }, - - youHaveBeenBlocked: { - message: 'You cannot react this note because you have been blocked by this user.', - code: 'YOU_HAVE_BEEN_BLOCKED', - id: '20ef5475-9f38-4e4c-bd33-de6d979498ec' - }, - } -}; - -export default define(meta, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - await createReaction(user, note, ps.reaction).catch(e => { - if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted); - if (e.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError(meta.errors.youHaveBeenBlocked); - throw e; - }); - return; -}); diff --git a/src/server/api/endpoints/notes/reactions/delete.ts b/src/server/api/endpoints/notes/reactions/delete.ts deleted file mode 100644 index 69550f96de..0000000000 --- a/src/server/api/endpoints/notes/reactions/delete.ts +++ /dev/null @@ -1,52 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import * as ms from 'ms'; -import deleteReaction from '@/services/note/reaction/delete'; -import { getNote } from '../../../common/getters'; -import { ApiError } from '../../../error'; - -export const meta = { - tags: ['reactions', 'notes'], - - requireCredential: true as const, - - kind: 'write:reactions', - - limit: { - duration: ms('1hour'), - max: 60, - minInterval: ms('3sec') - }, - - params: { - noteId: { - validator: $.type(ID), - }, - }, - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '764d9fce-f9f2-4a0e-92b1-6ceac9a7ad37' - }, - - notReacted: { - message: 'You are not reacting to that note.', - code: 'NOT_REACTED', - id: '92f4426d-4196-4125-aa5b-02943e2ec8fc' - }, - } -}; - -export default define(meta, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - await deleteReaction(user, note).catch(e => { - if (e.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') throw new ApiError(meta.errors.notReacted); - throw e; - }); -}); diff --git a/src/server/api/endpoints/notes/renotes.ts b/src/server/api/endpoints/notes/renotes.ts deleted file mode 100644 index 26bfc1657d..0000000000 --- a/src/server/api/endpoints/notes/renotes.ts +++ /dev/null @@ -1,76 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { getNote } from '../../common/getters'; -import { ApiError } from '../../error'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; -import { makePaginationQuery } from '../../common/make-pagination-query'; -import { Notes } from '@/models/index'; -import { generateBlockedUserQuery } from '../../common/generate-block-query'; - -export const meta = { - tags: ['notes'], - - requireCredential: false as const, - - params: { - noteId: { - validator: $.type(ID), - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - } - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Note', - } - }, - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '12908022-2e21-46cd-ba6a-3edaf6093f46' - } - } -}; - -export default define(meta, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere(`note.renoteId = :renoteId`, { renoteId: note.id }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - generateVisibilityQuery(query, user); - if (user) generateMutedUserQuery(query, user); - if (user) generateBlockedUserQuery(query, user); - - const renotes = await query.take(ps.limit!).getMany(); - - return await Notes.packMany(renotes, user); -}); diff --git a/src/server/api/endpoints/notes/replies.ts b/src/server/api/endpoints/notes/replies.ts deleted file mode 100644 index 0bb62413ae..0000000000 --- a/src/server/api/endpoints/notes/replies.ts +++ /dev/null @@ -1,61 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { Notes } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; -import { generateBlockedUserQuery } from '../../common/generate-block-query'; - -export const meta = { - tags: ['notes'], - - requireCredential: false as const, - - params: { - noteId: { - validator: $.type(ID), - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Note', - } - }, -}; - -export default define(meta, async (ps, user) => { - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere('note.replyId = :replyId', { replyId: ps.noteId }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - generateVisibilityQuery(query, user); - if (user) generateMutedUserQuery(query, user); - if (user) generateBlockedUserQuery(query, user); - - const timeline = await query.take(ps.limit!).getMany(); - - return await Notes.packMany(timeline, user); -}); diff --git a/src/server/api/endpoints/notes/search-by-tag.ts b/src/server/api/endpoints/notes/search-by-tag.ts deleted file mode 100644 index 40e1499736..0000000000 --- a/src/server/api/endpoints/notes/search-by-tag.ts +++ /dev/null @@ -1,134 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { makePaginationQuery } from '../../common/make-pagination-query'; -import { Notes } from '@/models/index'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query'; -import { Brackets } from 'typeorm'; -import { safeForSql } from '@/misc/safe-for-sql'; -import { normalizeForSearch } from '@/misc/normalize-for-search'; -import { generateBlockedUserQuery } from '../../common/generate-block-query'; - -export const meta = { - tags: ['notes', 'hashtags'], - - params: { - tag: { - validator: $.optional.str, - }, - - query: { - validator: $.optional.arr($.arr($.str)), - }, - - reply: { - validator: $.optional.nullable.bool, - default: null, - }, - - renote: { - validator: $.optional.nullable.bool, - default: null, - }, - - withFiles: { - validator: $.optional.bool, - }, - - poll: { - validator: $.optional.nullable.bool, - default: null, - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Note', - } - }, -}; - -export default define(meta, async (ps, me) => { - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - generateVisibilityQuery(query, me); - if (me) generateMutedUserQuery(query, me); - if (me) generateBlockedUserQuery(query, me); - - try { - if (ps.tag) { - if (!safeForSql(ps.tag)) throw 'Injection'; - query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`); - } else { - query.andWhere(new Brackets(qb => { - for (const tags of ps.query!) { - qb.orWhere(new Brackets(qb => { - for (const tag of tags) { - if (!safeForSql(tag)) throw 'Injection'; - qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`); - } - })); - } - })); - } - } catch (e) { - if (e === 'Injection') return []; - throw e; - } - - if (ps.reply != null) { - if (ps.reply) { - query.andWhere('note.replyId IS NOT NULL'); - } else { - query.andWhere('note.replyId IS NULL'); - } - } - - if (ps.renote != null) { - if (ps.renote) { - query.andWhere('note.renoteId IS NOT NULL'); - } else { - query.andWhere('note.renoteId IS NULL'); - } - } - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - - if (ps.poll != null) { - if (ps.poll) { - query.andWhere('note.hasPoll = TRUE'); - } else { - query.andWhere('note.hasPoll = FALSE'); - } - } - - // Search notes - const notes = await query.take(ps.limit!).getMany(); - - return await Notes.packMany(notes, me); -}); diff --git a/src/server/api/endpoints/notes/search.ts b/src/server/api/endpoints/notes/search.ts deleted file mode 100644 index eb832a6b31..0000000000 --- a/src/server/api/endpoints/notes/search.ts +++ /dev/null @@ -1,152 +0,0 @@ -import $ from 'cafy'; -import es from '../../../../db/elasticsearch'; -import define from '../../define'; -import { Notes } from '@/models/index'; -import { In } from 'typeorm'; -import { ID } from '@/misc/cafy-id'; -import config from '@/config/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; -import { generateBlockedUserQuery } from '../../common/generate-block-query'; - -export const meta = { - tags: ['notes'], - - requireCredential: false as const, - - params: { - query: { - validator: $.str - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - host: { - validator: $.optional.nullable.str, - default: undefined - }, - - userId: { - validator: $.optional.nullable.type(ID), - default: null - }, - - channelId: { - validator: $.optional.nullable.type(ID), - default: null - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Note', - } - }, - - errors: { - } -}; - -export default define(meta, async (ps, me) => { - if (es == null) { - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId); - - if (ps.userId) { - query.andWhere('note.userId = :userId', { userId: ps.userId }); - } else if (ps.channelId) { - query.andWhere('note.channelId = :channelId', { channelId: ps.channelId }); - } - - query - .andWhere('note.text ILIKE :q', { q: `%${ps.query}%` }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - generateVisibilityQuery(query, me); - if (me) generateMutedUserQuery(query, me); - if (me) generateBlockedUserQuery(query, me); - - const notes = await query.take(ps.limit!).getMany(); - - return await Notes.packMany(notes, me); - } else { - const userQuery = ps.userId != null ? [{ - term: { - userId: ps.userId - } - }] : []; - - const hostQuery = ps.userId == null ? - ps.host === null ? [{ - bool: { - must_not: { - exists: { - field: 'userHost' - } - } - } - }] : ps.host !== undefined ? [{ - term: { - userHost: ps.host - } - }] : [] - : []; - - const result = await es.search({ - index: config.elasticsearch.index || 'misskey_note', - body: { - size: ps.limit!, - from: ps.offset, - query: { - bool: { - must: [{ - simple_query_string: { - fields: ['text'], - query: ps.query.toLowerCase(), - default_operator: 'and' - }, - }, ...hostQuery, ...userQuery] - } - }, - sort: [{ - _doc: 'desc' - }] - } - }); - - const hits = result.body.hits.hits.map((hit: any) => hit._id); - - if (hits.length === 0) return []; - - // Fetch found notes - const notes = await Notes.find({ - where: { - id: In(hits) - }, - order: { - id: -1 - } - }); - - return await Notes.packMany(notes, me); - } -}); diff --git a/src/server/api/endpoints/notes/show.ts b/src/server/api/endpoints/notes/show.ts deleted file mode 100644 index fad63d6483..0000000000 --- a/src/server/api/endpoints/notes/show.ts +++ /dev/null @@ -1,43 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { getNote } from '../../common/getters'; -import { ApiError } from '../../error'; -import { Notes } from '@/models/index'; - -export const meta = { - tags: ['notes'], - - requireCredential: false as const, - - params: { - noteId: { - validator: $.type(ID), - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Note', - }, - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d' - } - } -}; - -export default define(meta, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - return await Notes.pack(note, user, { - detail: true - }); -}); diff --git a/src/server/api/endpoints/notes/state.ts b/src/server/api/endpoints/notes/state.ts deleted file mode 100644 index b3913a5e79..0000000000 --- a/src/server/api/endpoints/notes/state.ts +++ /dev/null @@ -1,69 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { NoteFavorites, Notes, NoteThreadMutings, NoteWatchings } from '@/models/index'; - -export const meta = { - tags: ['notes'], - - requireCredential: true as const, - - params: { - noteId: { - validator: $.type(ID), - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - isFavorited: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - isWatching: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - isMutedThread: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - } - } -}; - -export default define(meta, async (ps, user) => { - const note = await Notes.findOneOrFail(ps.noteId); - - const [favorite, watching, threadMuting] = await Promise.all([ - NoteFavorites.count({ - where: { - userId: user.id, - noteId: note.id, - }, - take: 1 - }), - NoteWatchings.count({ - where: { - userId: user.id, - noteId: note.id, - }, - take: 1 - }), - NoteThreadMutings.count({ - where: { - userId: user.id, - threadId: note.threadId || note.id, - }, - take: 1 - }), - ]); - - return { - isFavorited: favorite !== 0, - isWatching: watching !== 0, - isMutedThread: threadMuting !== 0, - }; -}); diff --git a/src/server/api/endpoints/notes/thread-muting/create.ts b/src/server/api/endpoints/notes/thread-muting/create.ts deleted file mode 100644 index 2010d54331..0000000000 --- a/src/server/api/endpoints/notes/thread-muting/create.ts +++ /dev/null @@ -1,54 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { getNote } from '../../../common/getters'; -import { ApiError } from '../../../error'; -import { Notes, NoteThreadMutings } from '@/models'; -import { genId } from '@/misc/gen-id'; -import readNote from '@/services/note/read'; - -export const meta = { - tags: ['notes'], - - requireCredential: true as const, - - kind: 'write:account', - - params: { - noteId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '5ff67ada-ed3b-2e71-8e87-a1a421e177d2' - } - } -}; - -export default define(meta, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - const mutedNotes = await Notes.find({ - where: [{ - id: note.threadId || note.id, - }, { - threadId: note.threadId || note.id, - }], - }); - - await readNote(user.id, mutedNotes); - - await NoteThreadMutings.insert({ - id: genId(), - createdAt: new Date(), - threadId: note.threadId || note.id, - userId: user.id, - }); -}); diff --git a/src/server/api/endpoints/notes/thread-muting/delete.ts b/src/server/api/endpoints/notes/thread-muting/delete.ts deleted file mode 100644 index 05d5691870..0000000000 --- a/src/server/api/endpoints/notes/thread-muting/delete.ts +++ /dev/null @@ -1,40 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { getNote } from '../../../common/getters'; -import { ApiError } from '../../../error'; -import { NoteThreadMutings } from '@/models'; - -export const meta = { - tags: ['notes'], - - requireCredential: true as const, - - kind: 'write:account', - - params: { - noteId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: 'bddd57ac-ceb3-b29d-4334-86ea5fae481a' - } - } -}; - -export default define(meta, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - await NoteThreadMutings.delete({ - threadId: note.threadId || note.id, - userId: user.id, - }); -}); diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts deleted file mode 100644 index 1bd0e57d34..0000000000 --- a/src/server/api/endpoints/notes/timeline.ts +++ /dev/null @@ -1,150 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { makePaginationQuery } from '../../common/make-pagination-query'; -import { Notes, Followings } from '@/models/index'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; -import { activeUsersChart } from '@/services/chart/index'; -import { Brackets } from 'typeorm'; -import { generateRepliesQuery } from '../../common/generate-replies-query'; -import { generateMutedNoteQuery } from '../../common/generate-muted-note-query'; -import { generateChannelQuery } from '../../common/generate-channel-query'; -import { generateBlockedUserQuery } from '../../common/generate-block-query'; - -export const meta = { - tags: ['notes'], - - requireCredential: true as const, - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10, - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - sinceDate: { - validator: $.optional.num, - }, - - untilDate: { - validator: $.optional.num, - }, - - includeMyRenotes: { - validator: $.optional.bool, - default: true, - }, - - includeRenotedMyNotes: { - validator: $.optional.bool, - default: true, - }, - - includeLocalRenotes: { - validator: $.optional.bool, - default: true, - }, - - withFiles: { - validator: $.optional.bool, - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Note', - } - }, -}; - -export default define(meta, async (ps, user) => { - const hasFollowing = (await Followings.count({ - where: { - followerId: user.id, - }, - take: 1 - })) !== 0; - - //#region Construct query - const followingQuery = Followings.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: user.id }); - - const query = makePaginationQuery(Notes.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere(new Brackets(qb => { qb - .where('note.userId = :meId', { meId: user.id }); - if (hasFollowing) qb.orWhere(`note.userId IN (${ followingQuery.getQuery() })`); - })) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .setParameters(followingQuery.getParameters()); - - generateChannelQuery(query, user); - generateRepliesQuery(query, user); - generateVisibilityQuery(query, user); - generateMutedUserQuery(query, user); - generateMutedNoteQuery(query, user); - generateBlockedUserQuery(query, user); - - if (ps.includeMyRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :meId', { meId: user.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeRenotedMyNotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserId != :meId', { meId: user.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeLocalRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserHost IS NOT NULL'); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - //#endregion - - const timeline = await query.take(ps.limit!).getMany(); - - process.nextTick(() => { - if (user) { - activeUsersChart.update(user); - } - }); - - return await Notes.packMany(timeline, user); -}); diff --git a/src/server/api/endpoints/notes/translate.ts b/src/server/api/endpoints/notes/translate.ts deleted file mode 100644 index b56b1debdd..0000000000 --- a/src/server/api/endpoints/notes/translate.ts +++ /dev/null @@ -1,89 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { getNote } from '../../common/getters'; -import { ApiError } from '../../error'; -import fetch from 'node-fetch'; -import config from '@/config/index'; -import { getAgentByUrl } from '@/misc/fetch'; -import { URLSearchParams } from 'url'; -import { fetchMeta } from '@/misc/fetch-meta'; -import { Notes } from '@/models'; - -export const meta = { - tags: ['notes'], - - requireCredential: false as const, - - params: { - noteId: { - validator: $.type(ID), - }, - targetLang: { - validator: $.str, - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - }, - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: 'bea9b03f-36e0-49c5-a4db-627a029f8971' - } - } -}; - -export default define(meta, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - if (!(await Notes.isVisibleForMe(note, user ? user.id : null))) { - return 204; // TODO: 良い感じのエラー返す - } - - if (note.text == null) { - return 204; - } - - const instance = await fetchMeta(); - - if (instance.deeplAuthKey == null) { - return 204; // TODO: 良い感じのエラー返す - } - - let targetLang = ps.targetLang; - if (targetLang.includes('-')) targetLang = targetLang.split('-')[0]; - - const params = new URLSearchParams(); - params.append('auth_key', instance.deeplAuthKey); - params.append('text', note.text); - params.append('target_lang', targetLang); - - const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate'; - - const res = await fetch(endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'User-Agent': config.userAgent, - Accept: 'application/json, */*' - }, - body: params, - timeout: 10000, - agent: getAgentByUrl, - }); - - const json = await res.json(); - - return { - sourceLang: json.translations[0].detected_source_language, - text: json.translations[0].text - }; -}); diff --git a/src/server/api/endpoints/notes/unrenote.ts b/src/server/api/endpoints/notes/unrenote.ts deleted file mode 100644 index dce43d9d9c..0000000000 --- a/src/server/api/endpoints/notes/unrenote.ts +++ /dev/null @@ -1,52 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import deleteNote from '@/services/note/delete'; -import define from '../../define'; -import * as ms from 'ms'; -import { getNote } from '../../common/getters'; -import { ApiError } from '../../error'; -import { Notes, Users } from '@/models/index'; - -export const meta = { - tags: ['notes'], - - requireCredential: true as const, - - kind: 'write:notes', - - limit: { - duration: ms('1hour'), - max: 300, - minInterval: ms('1sec') - }, - - params: { - noteId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: 'efd4a259-2442-496b-8dd7-b255aa1a160f' - }, - } -}; - -export default define(meta, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - const renotes = await Notes.find({ - userId: user.id, - renoteId: note.id - }); - - for (const note of renotes) { - deleteNote(await Users.findOneOrFail(user.id), note); - } -}); diff --git a/src/server/api/endpoints/notes/user-list-timeline.ts b/src/server/api/endpoints/notes/user-list-timeline.ts deleted file mode 100644 index 32c370004c..0000000000 --- a/src/server/api/endpoints/notes/user-list-timeline.ts +++ /dev/null @@ -1,147 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { UserLists, UserListJoinings, Notes } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query'; -import { activeUsersChart } from '@/services/chart/index'; -import { Brackets } from 'typeorm'; - -export const meta = { - tags: ['notes', 'lists'], - - requireCredential: true as const, - - params: { - listId: { - validator: $.type(ID), - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10, - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - sinceDate: { - validator: $.optional.num, - }, - - untilDate: { - validator: $.optional.num, - }, - - includeMyRenotes: { - validator: $.optional.bool, - default: true, - }, - - includeRenotedMyNotes: { - validator: $.optional.bool, - default: true, - }, - - includeLocalRenotes: { - validator: $.optional.bool, - default: true, - }, - - withFiles: { - validator: $.optional.bool, - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Note', - } - }, - - errors: { - noSuchList: { - message: 'No such list.', - code: 'NO_SUCH_LIST', - id: '8fb1fbd5-e476-4c37-9fb0-43d55b63a2ff' - } - } -}; - -export default define(meta, async (ps, user) => { - const list = await UserLists.findOne({ - id: ps.listId, - userId: user.id - }); - - if (list == null) { - throw new ApiError(meta.errors.noSuchList); - } - - //#region Construct query - const listQuery = UserListJoinings.createQueryBuilder('joining') - .select('joining.userId') - .where('joining.userListId = :userListId', { userListId: list.id }); - - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere(`note.userId IN (${ listQuery.getQuery() })`) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .setParameters(listQuery.getParameters()); - - generateVisibilityQuery(query, user); - - if (ps.includeMyRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :meId', { meId: user.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeRenotedMyNotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserId != :meId', { meId: user.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeLocalRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserHost IS NOT NULL'); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - //#endregion - - const timeline = await query.take(ps.limit!).getMany(); - - activeUsersChart.update(user); - - return await Notes.packMany(timeline, user); -}); diff --git a/src/server/api/endpoints/notes/watching/create.ts b/src/server/api/endpoints/notes/watching/create.ts deleted file mode 100644 index 4d182d3715..0000000000 --- a/src/server/api/endpoints/notes/watching/create.ts +++ /dev/null @@ -1,37 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import watch from '@/services/note/watch'; -import { getNote } from '../../../common/getters'; -import { ApiError } from '../../../error'; - -export const meta = { - tags: ['notes'], - - requireCredential: true as const, - - kind: 'write:account', - - params: { - noteId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: 'ea0e37a6-90a3-4f58-ba6b-c328ca206fc7' - } - } -}; - -export default define(meta, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - await watch(user.id, note); -}); diff --git a/src/server/api/endpoints/notes/watching/delete.ts b/src/server/api/endpoints/notes/watching/delete.ts deleted file mode 100644 index dd58c52b57..0000000000 --- a/src/server/api/endpoints/notes/watching/delete.ts +++ /dev/null @@ -1,37 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import unwatch from '@/services/note/unwatch'; -import { getNote } from '../../../common/getters'; -import { ApiError } from '../../../error'; - -export const meta = { - tags: ['notes'], - - requireCredential: true as const, - - kind: 'write:account', - - params: { - noteId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '09b3695c-f72c-4731-a428-7cff825fc82e' - } - } -}; - -export default define(meta, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - await unwatch(user.id, note); -}); diff --git a/src/server/api/endpoints/notifications/create.ts b/src/server/api/endpoints/notifications/create.ts deleted file mode 100644 index 8003c497ee..0000000000 --- a/src/server/api/endpoints/notifications/create.ts +++ /dev/null @@ -1,37 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { createNotification } from '@/services/create-notification'; - -export const meta = { - tags: ['notifications'], - - requireCredential: true as const, - - kind: 'write:notifications', - - params: { - body: { - validator: $.str - }, - - header: { - validator: $.optional.nullable.str - }, - - icon: { - validator: $.optional.nullable.str - }, - }, - - errors: { - } -}; - -export default define(meta, async (ps, user, token) => { - createNotification(user.id, 'app', { - appAccessTokenId: token ? token.id : null, - customBody: ps.body, - customHeader: ps.header, - customIcon: ps.icon, - }); -}); diff --git a/src/server/api/endpoints/notifications/mark-all-as-read.ts b/src/server/api/endpoints/notifications/mark-all-as-read.ts deleted file mode 100644 index 8d4e512750..0000000000 --- a/src/server/api/endpoints/notifications/mark-all-as-read.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { publishMainStream } from '@/services/stream'; -import define from '../../define'; -import { Notifications } from '@/models/index'; - -export const meta = { - tags: ['notifications', 'account'], - - requireCredential: true as const, - - kind: 'write:notifications' -}; - -export default define(meta, async (ps, user) => { - // Update documents - await Notifications.update({ - notifieeId: user.id, - isRead: false, - }, { - isRead: true - }); - - // 全ての通知を読みましたよというイベントを発行 - publishMainStream(user.id, 'readAllNotifications'); -}); diff --git a/src/server/api/endpoints/notifications/read.ts b/src/server/api/endpoints/notifications/read.ts deleted file mode 100644 index 66bbc4efd7..0000000000 --- a/src/server/api/endpoints/notifications/read.ts +++ /dev/null @@ -1,42 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import { publishMainStream } from '@/services/stream'; -import define from '../../define'; -import { Notifications } from '@/models/index'; -import { readNotification } from '../../common/read-notification'; -import { ApiError } from '../../error'; - -export const meta = { - tags: ['notifications', 'account'], - - requireCredential: true as const, - - kind: 'write:notifications', - - params: { - notificationId: { - validator: $.type(ID), - }, - }, - - errors: { - noSuchNotification: { - message: 'No such notification.', - code: 'NO_SUCH_NOTIFICATION', - id: 'efa929d5-05b5-47d1-beec-e6a4dbed011e' - }, - }, -}; - -export default define(meta, async (ps, user) => { - const notification = await Notifications.findOne({ - notifieeId: user.id, - id: ps.notificationId, - }); - - if (notification == null) { - throw new ApiError(meta.errors.noSuchNotification); - } - - readNotification(user.id, [notification.id]); -}); diff --git a/src/server/api/endpoints/page-push.ts b/src/server/api/endpoints/page-push.ts deleted file mode 100644 index a0412e89f1..0000000000 --- a/src/server/api/endpoints/page-push.ts +++ /dev/null @@ -1,50 +0,0 @@ -import $ from 'cafy'; -import define from '../define'; -import { ID } from '@/misc/cafy-id'; -import { publishMainStream } from '@/services/stream'; -import { Users, Pages } from '@/models/index'; -import { ApiError } from '../error'; - -export const meta = { - requireCredential: true as const, - secure: true, - - params: { - pageId: { - validator: $.type(ID) - }, - - event: { - validator: $.str - }, - - var: { - validator: $.optional.nullable.any - } - }, - - errors: { - noSuchPage: { - message: 'No such page.', - code: 'NO_SUCH_PAGE', - id: '4a13ad31-6729-46b4-b9af-e86b265c2e74' - } - } -}; - -export default define(meta, async (ps, user) => { - const page = await Pages.findOne(ps.pageId); - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); - } - - publishMainStream(page.userId, 'pageEvent', { - pageId: ps.pageId, - event: ps.event, - var: ps.var, - userId: user.id, - user: await Users.pack(user.id, { id: page.userId }, { - detail: true - }) - }); -}); diff --git a/src/server/api/endpoints/pages/create.ts b/src/server/api/endpoints/pages/create.ts deleted file mode 100644 index c23978f093..0000000000 --- a/src/server/api/endpoints/pages/create.ts +++ /dev/null @@ -1,128 +0,0 @@ -import $ from 'cafy'; -import * as ms from 'ms'; -import define from '../../define'; -import { ID } from '@/misc/cafy-id'; -import { Pages, DriveFiles } from '@/models/index'; -import { genId } from '@/misc/gen-id'; -import { Page } from '@/models/entities/page'; -import { ApiError } from '../../error'; - -export const meta = { - tags: ['pages'], - - requireCredential: true as const, - - kind: 'write:pages', - - limit: { - duration: ms('1hour'), - max: 300 - }, - - params: { - title: { - validator: $.str, - }, - - name: { - validator: $.str.min(1), - }, - - summary: { - validator: $.optional.nullable.str, - }, - - content: { - validator: $.arr($.obj()) - }, - - variables: { - validator: $.arr($.obj()) - }, - - script: { - validator: $.str, - }, - - eyeCatchingImageId: { - validator: $.optional.nullable.type(ID), - }, - - font: { - validator: $.optional.str.or(['serif', 'sans-serif']), - default: 'sans-serif' - }, - - alignCenter: { - validator: $.optional.bool, - default: false - }, - - hideTitleWhenPinned: { - validator: $.optional.bool, - default: false - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Page', - }, - - errors: { - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: 'b7b97489-0f66-4b12-a5ff-b21bd63f6e1c' - }, - nameAlreadyExists: { - message: 'Specified name already exists.', - code: 'NAME_ALREADY_EXISTS', - id: '4650348e-301c-499a-83c9-6aa988c66bc1' - } - } -}; - -export default define(meta, async (ps, user) => { - let eyeCatchingImage = null; - if (ps.eyeCatchingImageId != null) { - eyeCatchingImage = await DriveFiles.findOne({ - id: ps.eyeCatchingImageId, - userId: user.id - }); - - if (eyeCatchingImage == null) { - throw new ApiError(meta.errors.noSuchFile); - } - } - - await Pages.find({ - userId: user.id, - name: ps.name - }).then(result => { - if (result.length > 0) { - throw new ApiError(meta.errors.nameAlreadyExists); - } - }); - - const page = await Pages.save(new Page({ - id: genId(), - createdAt: new Date(), - updatedAt: new Date(), - title: ps.title, - name: ps.name, - summary: ps.summary, - content: ps.content, - variables: ps.variables, - script: ps.script, - eyeCatchingImageId: eyeCatchingImage ? eyeCatchingImage.id : null, - userId: user.id, - visibility: 'public', - alignCenter: ps.alignCenter, - hideTitleWhenPinned: ps.hideTitleWhenPinned, - font: ps.font - })); - - return await Pages.pack(page); -}); diff --git a/src/server/api/endpoints/pages/delete.ts b/src/server/api/endpoints/pages/delete.ts deleted file mode 100644 index b1f8c8a709..0000000000 --- a/src/server/api/endpoints/pages/delete.ts +++ /dev/null @@ -1,45 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { Pages } from '@/models/index'; -import { ID } from '@/misc/cafy-id'; - -export const meta = { - tags: ['pages'], - - requireCredential: true as const, - - kind: 'write:pages', - - params: { - pageId: { - validator: $.type(ID), - }, - }, - - errors: { - noSuchPage: { - message: 'No such page.', - code: 'NO_SUCH_PAGE', - id: 'eb0c6e1d-d519-4764-9486-52a7e1c6392a' - }, - - accessDenied: { - message: 'Access denied.', - code: 'ACCESS_DENIED', - id: '8b741b3e-2c22-44b3-a15f-29949aa1601e' - }, - } -}; - -export default define(meta, async (ps, user) => { - const page = await Pages.findOne(ps.pageId); - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); - } - if (page.userId !== user.id) { - throw new ApiError(meta.errors.accessDenied); - } - - await Pages.delete(page.id); -}); diff --git a/src/server/api/endpoints/pages/featured.ts b/src/server/api/endpoints/pages/featured.ts deleted file mode 100644 index f891c45f05..0000000000 --- a/src/server/api/endpoints/pages/featured.ts +++ /dev/null @@ -1,29 +0,0 @@ -import define from '../../define'; -import { Pages } from '@/models/index'; - -export const meta = { - tags: ['pages'], - - requireCredential: false as const, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Page', - } - }, -}; - -export default define(meta, async (ps, me) => { - const query = Pages.createQueryBuilder('page') - .where('page.visibility = \'public\'') - .andWhere('page.likedCount > 0') - .orderBy('page.likedCount', 'DESC'); - - const pages = await query.take(10).getMany(); - - return await Pages.packMany(pages, me); -}); diff --git a/src/server/api/endpoints/pages/like.ts b/src/server/api/endpoints/pages/like.ts deleted file mode 100644 index a95a377802..0000000000 --- a/src/server/api/endpoints/pages/like.ts +++ /dev/null @@ -1,71 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { Pages, PageLikes } from '@/models/index'; -import { genId } from '@/misc/gen-id'; - -export const meta = { - tags: ['pages'], - - requireCredential: true as const, - - kind: 'write:page-likes', - - params: { - pageId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchPage: { - message: 'No such page.', - code: 'NO_SUCH_PAGE', - id: 'cc98a8a2-0dc3-4123-b198-62c71df18ed3' - }, - - yourPage: { - message: 'You cannot like your page.', - code: 'YOUR_PAGE', - id: '28800466-e6db-40f2-8fae-bf9e82aa92b8' - }, - - alreadyLiked: { - message: 'The page has already been liked.', - code: 'ALREADY_LIKED', - id: 'cc98a8a2-0dc3-4123-b198-62c71df18ed3' - }, - } -}; - -export default define(meta, async (ps, user) => { - const page = await Pages.findOne(ps.pageId); - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); - } - - if (page.userId === user.id) { - throw new ApiError(meta.errors.yourPage); - } - - // if already liked - const exist = await PageLikes.findOne({ - pageId: page.id, - userId: user.id - }); - - if (exist != null) { - throw new ApiError(meta.errors.alreadyLiked); - } - - // Create like - await PageLikes.insert({ - id: genId(), - createdAt: new Date(), - pageId: page.id, - userId: user.id - }); - - Pages.increment({ id: page.id }, 'likedCount', 1); -}); diff --git a/src/server/api/endpoints/pages/show.ts b/src/server/api/endpoints/pages/show.ts deleted file mode 100644 index 7c55d4a9e6..0000000000 --- a/src/server/api/endpoints/pages/show.ts +++ /dev/null @@ -1,65 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { Pages, Users } from '@/models/index'; -import { ID } from '@/misc/cafy-id'; -import { Page } from '@/models/entities/page'; - -export const meta = { - tags: ['pages'], - - requireCredential: false as const, - - params: { - pageId: { - validator: $.optional.type(ID), - }, - - name: { - validator: $.optional.str, - }, - - username: { - validator: $.optional.str, - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Page', - }, - - errors: { - noSuchPage: { - message: 'No such page.', - code: 'NO_SUCH_PAGE', - id: '222120c0-3ead-4528-811b-b96f233388d7' - } - } -}; - -export default define(meta, async (ps, user) => { - let page: Page | undefined; - - if (ps.pageId) { - page = await Pages.findOne(ps.pageId); - } else if (ps.name && ps.username) { - const author = await Users.findOne({ - host: null, - usernameLower: ps.username.toLowerCase() - }); - if (author) { - page = await Pages.findOne({ - name: ps.name, - userId: author.id - }); - } - } - - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); - } - - return await Pages.pack(page, user); -}); diff --git a/src/server/api/endpoints/pages/unlike.ts b/src/server/api/endpoints/pages/unlike.ts deleted file mode 100644 index facf2d6d5f..0000000000 --- a/src/server/api/endpoints/pages/unlike.ts +++ /dev/null @@ -1,54 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { Pages, PageLikes } from '@/models/index'; - -export const meta = { - tags: ['pages'], - - requireCredential: true as const, - - kind: 'write:page-likes', - - params: { - pageId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchPage: { - message: 'No such page.', - code: 'NO_SUCH_PAGE', - id: 'a0d41e20-1993-40bd-890e-f6e560ae648e' - }, - - notLiked: { - message: 'You have not liked that page.', - code: 'NOT_LIKED', - id: 'f5e586b0-ce93-4050-b0e3-7f31af5259ee' - }, - } -}; - -export default define(meta, async (ps, user) => { - const page = await Pages.findOne(ps.pageId); - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); - } - - const exist = await PageLikes.findOne({ - pageId: page.id, - userId: user.id - }); - - if (exist == null) { - throw new ApiError(meta.errors.notLiked); - } - - // Delete like - await PageLikes.delete(exist.id); - - Pages.decrement({ id: page.id }, 'likedCount', 1); -}); diff --git a/src/server/api/endpoints/pages/update.ts b/src/server/api/endpoints/pages/update.ts deleted file mode 100644 index b3a7f26963..0000000000 --- a/src/server/api/endpoints/pages/update.ts +++ /dev/null @@ -1,141 +0,0 @@ -import $ from 'cafy'; -import * as ms from 'ms'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { Pages, DriveFiles } from '@/models/index'; -import { ID } from '@/misc/cafy-id'; -import { Not } from 'typeorm'; - -export const meta = { - tags: ['pages'], - - requireCredential: true as const, - - kind: 'write:pages', - - limit: { - duration: ms('1hour'), - max: 300 - }, - - params: { - pageId: { - validator: $.type(ID), - }, - - title: { - validator: $.str, - }, - - name: { - validator: $.str.min(1), - }, - - summary: { - validator: $.optional.nullable.str, - }, - - content: { - validator: $.arr($.obj()) - }, - - variables: { - validator: $.arr($.obj()) - }, - - script: { - validator: $.str, - }, - - eyeCatchingImageId: { - validator: $.optional.nullable.type(ID), - }, - - font: { - validator: $.optional.str.or(['serif', 'sans-serif']), - }, - - alignCenter: { - validator: $.optional.bool, - }, - - hideTitleWhenPinned: { - validator: $.optional.bool, - }, - }, - - errors: { - noSuchPage: { - message: 'No such page.', - code: 'NO_SUCH_PAGE', - id: '21149b9e-3616-4778-9592-c4ce89f5a864' - }, - - accessDenied: { - message: 'Access denied.', - code: 'ACCESS_DENIED', - id: '3c15cd52-3b4b-4274-967d-6456fc4f792b' - }, - - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: 'cfc23c7c-3887-490e-af30-0ed576703c82' - }, - nameAlreadyExists: { - message: 'Specified name already exists.', - code: 'NAME_ALREADY_EXISTS', - id: '2298a392-d4a1-44c5-9ebb-ac1aeaa5a9ab' - } - } -}; - -export default define(meta, async (ps, user) => { - const page = await Pages.findOne(ps.pageId); - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); - } - if (page.userId !== user.id) { - throw new ApiError(meta.errors.accessDenied); - } - - let eyeCatchingImage = null; - if (ps.eyeCatchingImageId != null) { - eyeCatchingImage = await DriveFiles.findOne({ - id: ps.eyeCatchingImageId, - userId: user.id - }); - - if (eyeCatchingImage == null) { - throw new ApiError(meta.errors.noSuchFile); - } - } - - await Pages.find({ - id: Not(ps.pageId), - userId: user.id, - name: ps.name - }).then(result => { - if (result.length > 0) { - throw new ApiError(meta.errors.nameAlreadyExists); - } - }); - - await Pages.update(page.id, { - updatedAt: new Date(), - title: ps.title, - name: ps.name === undefined ? page.name : ps.name, - summary: ps.name === undefined ? page.summary : ps.summary, - content: ps.content, - variables: ps.variables, - script: ps.script, - alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter, - hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned, - font: ps.font === undefined ? page.font : ps.font, - eyeCatchingImageId: ps.eyeCatchingImageId === null - ? null - : ps.eyeCatchingImageId === undefined - ? page.eyeCatchingImageId - : eyeCatchingImage!.id, - }); -}); diff --git a/src/server/api/endpoints/ping.ts b/src/server/api/endpoints/ping.ts deleted file mode 100644 index 0b1bb6e164..0000000000 --- a/src/server/api/endpoints/ping.ts +++ /dev/null @@ -1,27 +0,0 @@ -import define from '../define'; - -export const meta = { - requireCredential: false as const, - - tags: ['meta'], - - params: { - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - pong: { - type: 'number' as const, - optional: false as const, nullable: false as const, - }, - } - } -}; - -export default define(meta, async () => { - return { - pong: Date.now(), - }; -}); diff --git a/src/server/api/endpoints/pinned-users.ts b/src/server/api/endpoints/pinned-users.ts deleted file mode 100644 index e88dfbd535..0000000000 --- a/src/server/api/endpoints/pinned-users.ts +++ /dev/null @@ -1,32 +0,0 @@ -import define from '../define'; -import { Users } from '@/models/index'; -import { fetchMeta } from '@/misc/fetch-meta'; -import { parseAcct } from '@/misc/acct'; -import { User } from '@/models/entities/user'; - -export const meta = { - tags: ['users'], - - requireCredential: false as const, - - params: { - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User', - } - }, -}; - -export default define(meta, async (ps, me) => { - const meta = await fetchMeta(); - - const users = await Promise.all(meta.pinnedUsers.map(acct => Users.findOne(parseAcct(acct)))); - - return await Users.packMany(users.filter(x => x !== undefined) as User[], me, { detail: true }); -}); diff --git a/src/server/api/endpoints/promo/read.ts b/src/server/api/endpoints/promo/read.ts deleted file mode 100644 index ae57bf9cf1..0000000000 --- a/src/server/api/endpoints/promo/read.ts +++ /dev/null @@ -1,50 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { getNote } from '../../common/getters'; -import { PromoReads } from '@/models/index'; -import { genId } from '@/misc/gen-id'; - -export const meta = { - tags: ['notes'], - - requireCredential: true as const, - - params: { - noteId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: 'd785b897-fcd3-4fe9-8fc3-b85c26e6c932' - }, - } -}; - -export default define(meta, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - const exist = await PromoReads.findOne({ - noteId: note.id, - userId: user.id - }); - - if (exist != null) { - return; - } - - await PromoReads.insert({ - id: genId(), - createdAt: new Date(), - noteId: note.id, - userId: user.id - }); -}); diff --git a/src/server/api/endpoints/request-reset-password.ts b/src/server/api/endpoints/request-reset-password.ts deleted file mode 100644 index f9928c2ee6..0000000000 --- a/src/server/api/endpoints/request-reset-password.ts +++ /dev/null @@ -1,73 +0,0 @@ -import $ from 'cafy'; -import { publishMainStream } from '@/services/stream'; -import define from '../define'; -import rndstr from 'rndstr'; -import config from '@/config/index'; -import * as ms from 'ms'; -import { Users, UserProfiles, PasswordResetRequests } from '@/models/index'; -import { sendEmail } from '@/services/send-email'; -import { ApiError } from '../error'; -import { genId } from '@/misc/gen-id'; -import { IsNull } from 'typeorm'; - -export const meta = { - requireCredential: false as const, - - limit: { - duration: ms('1hour'), - max: 3 - }, - - params: { - username: { - validator: $.str - }, - - email: { - validator: $.str - }, - }, - - errors: { - - } -}; - -export default define(meta, async (ps) => { - const user = await Users.findOne({ - usernameLower: ps.username.toLowerCase(), - host: IsNull() - }); - - // 合致するユーザーが登録されていなかったら無視 - if (user == null) { - return; - } - - const profile = await UserProfiles.findOneOrFail(user.id); - - // 合致するメアドが登録されていなかったら無視 - if (profile.email !== ps.email) { - return; - } - - // メアドが認証されていなかったら無視 - if (!profile.emailVerified) { - return; - } - - const token = rndstr('a-z0-9', 64); - - await PasswordResetRequests.insert({ - id: genId(), - createdAt: new Date(), - userId: profile.userId, - token - }); - - const link = `${config.url}/reset-password/${token}`; - - sendEmail(ps.email, 'Password reset requested', - `To reset password, please click this link:<br><a href="${link}">${link}</a>`, - `To reset password, please click this link: ${link}`); -}); diff --git a/src/server/api/endpoints/reset-db.ts b/src/server/api/endpoints/reset-db.ts deleted file mode 100644 index f0a9dae4ff..0000000000 --- a/src/server/api/endpoints/reset-db.ts +++ /dev/null @@ -1,23 +0,0 @@ -import $ from 'cafy'; -import define from '../define'; -import { ApiError } from '../error'; -import { resetDb } from '@/db/postgre'; - -export const meta = { - requireCredential: false as const, - - params: { - }, - - errors: { - - } -}; - -export default define(meta, async (ps, user) => { - if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test'; - - await resetDb(); - - await new Promise(resolve => setTimeout(resolve, 1000)); -}); diff --git a/src/server/api/endpoints/reset-password.ts b/src/server/api/endpoints/reset-password.ts deleted file mode 100644 index 53b0bfde0b..0000000000 --- a/src/server/api/endpoints/reset-password.ts +++ /dev/null @@ -1,45 +0,0 @@ -import $ from 'cafy'; -import * as bcrypt from 'bcryptjs'; -import { publishMainStream } from '@/services/stream'; -import define from '../define'; -import { Users, UserProfiles, PasswordResetRequests } from '@/models/index'; -import { ApiError } from '../error'; - -export const meta = { - requireCredential: false as const, - - params: { - token: { - validator: $.str - }, - - password: { - validator: $.str - } - }, - - errors: { - - } -}; - -export default define(meta, async (ps, user) => { - const req = await PasswordResetRequests.findOneOrFail({ - token: ps.token, - }); - - // 発行してから30分以上経過していたら無効 - if (Date.now() - req.createdAt.getTime() > 1000 * 60 * 30) { - throw new Error(); // TODO - } - - // Generate hash of password - const salt = await bcrypt.genSalt(8); - const hash = await bcrypt.hash(ps.password, salt); - - await UserProfiles.update(req.userId, { - password: hash - }); - - PasswordResetRequests.delete(req.id); -}); diff --git a/src/server/api/endpoints/room/show.ts b/src/server/api/endpoints/room/show.ts deleted file mode 100644 index a6461d4a6e..0000000000 --- a/src/server/api/endpoints/room/show.ts +++ /dev/null @@ -1,159 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { Users, UserProfiles } from '@/models/index'; -import { ID } from '@/misc/cafy-id'; -import { toPunyNullable } from '@/misc/convert-host'; - -export const meta = { - tags: ['room'], - - requireCredential: false as const, - - params: { - userId: { - validator: $.optional.type(ID), - }, - - username: { - validator: $.optional.str - }, - - host: { - validator: $.optional.nullable.str - }, - }, - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '7ad3fa3e-5e12-42f0-b23a-f3d13f10ee4b' - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - roomType: { - type: 'string' as const, - optional: false as const, nullable: false as const, - enum: ['default', 'washitsu'] - }, - furnitures: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - optional: false as const, nullable: false as const - }, - type: { - type: 'string' as const, - optional: false as const, nullable: false as const - }, - props: { - type: 'object' as const, - optional: true as const, nullable: false as const, - }, - position: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - x: { - type: 'number' as const, - optional: false as const, nullable: false as const - }, - y: { - type: 'number' as const, - optional: false as const, nullable: false as const - }, - z: { - type: 'number' as const, - optional: false as const, nullable: false as const - } - } - }, - rotation: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - x: { - type: 'number' as const, - optional: false as const, nullable: false as const - }, - y: { - type: 'number' as const, - optional: false as const, nullable: false as const - }, - z: { - type: 'number' as const, - optional: false as const, nullable: false as const - } - } - } - } - } - }, - carpetColor: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'hex', - example: '#85CAF0' - } - } - } -}; - -export default define(meta, async (ps, me) => { - const user = await Users.findOne(ps.userId != null - ? { id: ps.userId } - : { usernameLower: ps.username!.toLowerCase(), host: toPunyNullable(ps.host) }); - - if (user == null) { - throw new ApiError(meta.errors.noSuchUser); - } - - const profile = await UserProfiles.findOneOrFail(user.id); - - if (profile.room.furnitures == null) { - await UserProfiles.update(user.id, { - room: { - furnitures: [], - ...profile.room - } - }); - - profile.room.furnitures = []; - } - - if (profile.room.roomType == null) { - const initialType = 'default'; - await UserProfiles.update(user.id, { - room: { - roomType: initialType as any, - ...profile.room - } - }); - - profile.room.roomType = initialType; - } - - if (profile.room.carpetColor == null) { - const initialColor = '#85CAF0'; - await UserProfiles.update(user.id, { - room: { - carpetColor: initialColor as any, - ...profile.room - } - }); - - profile.room.carpetColor = initialColor; - } - - return profile.room; -}); diff --git a/src/server/api/endpoints/room/update.ts b/src/server/api/endpoints/room/update.ts deleted file mode 100644 index 8c4cfbdea6..0000000000 --- a/src/server/api/endpoints/room/update.ts +++ /dev/null @@ -1,51 +0,0 @@ -import $ from 'cafy'; -import { publishMainStream } from '@/services/stream'; -import define from '../../define'; -import { Users, UserProfiles } from '@/models/index'; - -export const meta = { - tags: ['room'], - - requireCredential: true as const, - - params: { - room: { - validator: $.obj({ - furnitures: $.arr($.obj({ - id: $.str, - type: $.str, - position: $.obj({ - x: $.num, - y: $.num, - z: $.num, - }), - rotation: $.obj({ - x: $.num, - y: $.num, - z: $.num, - }), - props: $.optional.nullable.obj(), - })), - roomType: $.str, - carpetColor: $.str - }) - }, - }, -}; - -export default define(meta, async (ps, user) => { - await UserProfiles.update(user.id, { - room: ps.room as any - }); - - const iObj = await Users.pack(user.id, user, { - detail: true, - includeSecrets: true - }); - - // Publish meUpdated event - publishMainStream(user.id, 'meUpdated', iObj); - - // TODO: レスポンスがおかしいと思う by YuzuRyo61 - return iObj; -}); diff --git a/src/server/api/endpoints/server-info.ts b/src/server/api/endpoints/server-info.ts deleted file mode 100644 index 4e636d331c..0000000000 --- a/src/server/api/endpoints/server-info.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as os from 'os'; -import * as si from 'systeminformation'; -import define from '../define'; - -export const meta = { - requireCredential: false as const, - - desc: { - }, - - tags: ['meta'], - - params: { - }, -}; - -export default define(meta, async () => { - const memStats = await si.mem(); - const fsStats = await si.fsSize(); - - return { - machine: os.hostname(), - cpu: { - model: os.cpus()[0].model, - cores: os.cpus().length - }, - mem: { - total: memStats.total - }, - fs: { - total: fsStats[0].size, - used: fsStats[0].used, - }, - }; -}); diff --git a/src/server/api/endpoints/stats.ts b/src/server/api/endpoints/stats.ts deleted file mode 100644 index 15c8001742..0000000000 --- a/src/server/api/endpoints/stats.ts +++ /dev/null @@ -1,83 +0,0 @@ -import define from '../define'; -import { NoteReactions, Notes, Users } from '@/models/index'; -import { federationChart, driveChart } from '@/services/chart/index'; - -export const meta = { - requireCredential: false as const, - - tags: ['meta'], - - params: { - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - notesCount: { - type: 'number' as const, - optional: false as const, nullable: false as const, - }, - originalNotesCount: { - type: 'number' as const, - optional: false as const, nullable: false as const, - }, - usersCount: { - type: 'number' as const, - optional: false as const, nullable: false as const, - }, - originalUsersCount: { - type: 'number' as const, - optional: false as const, nullable: false as const, - }, - instances: { - type: 'number' as const, - optional: false as const, nullable: false as const, - }, - driveUsageLocal: { - type: 'number' as const, - optional: false as const, nullable: false as const - }, - driveUsageRemote: { - type: 'number' as const, - optional: false as const, nullable: false as const - } - } - } -}; - -export default define(meta, async () => { - const [ - notesCount, - originalNotesCount, - usersCount, - originalUsersCount, - reactionsCount, - //originalReactionsCount, - instances, - driveUsageLocal, - driveUsageRemote - ] = await Promise.all([ - Notes.count({ cache: 3600000 }), // 1 hour - Notes.count({ where: { userHost: null }, cache: 3600000 }), - Users.count({ cache: 3600000 }), - Users.count({ where: { host: null }, cache: 3600000 }), - NoteReactions.count({ cache: 3600000 }), // 1 hour - //NoteReactions.count({ where: { userHost: null }, cache: 3600000 }), - federationChart.getChart('hour', 1, null).then(chart => chart.instance.total[0]), - driveChart.getChart('hour', 1, null).then(chart => chart.local.totalSize[0]), - driveChart.getChart('hour', 1, null).then(chart => chart.remote.totalSize[0]), - ]); - - return { - notesCount, - originalNotesCount, - usersCount, - originalUsersCount, - reactionsCount, - //originalReactionsCount, - instances, - driveUsageLocal, - driveUsageRemote - }; -}); diff --git a/src/server/api/endpoints/sw/register.ts b/src/server/api/endpoints/sw/register.ts deleted file mode 100644 index 6e14ba2669..0000000000 --- a/src/server/api/endpoints/sw/register.ts +++ /dev/null @@ -1,74 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { fetchMeta } from '@/misc/fetch-meta'; -import { genId } from '@/misc/gen-id'; -import { SwSubscriptions } from '@/models/index'; - -export const meta = { - tags: ['account'], - - requireCredential: true as const, - - params: { - endpoint: { - validator: $.str - }, - - auth: { - validator: $.str - }, - - publickey: { - validator: $.str - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - state: { - type: 'string' as const, - optional: false as const, nullable: false as const, - enum: ['already-subscribed', 'subscribed'] - }, - key: { - type: 'string' as const, - optional: false as const, nullable: false as const - } - } - } -}; - -export default define(meta, async (ps, user) => { - // if already subscribed - const exist = await SwSubscriptions.findOne({ - userId: user.id, - endpoint: ps.endpoint, - auth: ps.auth, - publickey: ps.publickey, - }); - - const instance = await fetchMeta(true); - - if (exist != null) { - return { - state: 'already-subscribed', - key: instance.swPublicKey - }; - } - - await SwSubscriptions.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - endpoint: ps.endpoint, - auth: ps.auth, - publickey: ps.publickey - }); - - return { - state: 'subscribed', - key: instance.swPublicKey - }; -}); diff --git a/src/server/api/endpoints/sw/unregister.ts b/src/server/api/endpoints/sw/unregister.ts deleted file mode 100644 index 817ad1f517..0000000000 --- a/src/server/api/endpoints/sw/unregister.ts +++ /dev/null @@ -1,22 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { SwSubscriptions } from '../../../../models'; - -export const meta = { - tags: ['account'], - - requireCredential: true as const, - - params: { - endpoint: { - validator: $.str - }, - } -}; - -export default define(meta, async (ps, user) => { - await SwSubscriptions.delete({ - userId: user.id, - endpoint: ps.endpoint, - }); -}); diff --git a/src/server/api/endpoints/username/available.ts b/src/server/api/endpoints/username/available.ts deleted file mode 100644 index 1ae75448ea..0000000000 --- a/src/server/api/endpoints/username/available.ts +++ /dev/null @@ -1,40 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { Users, UsedUsernames } from '@/models/index'; - -export const meta = { - tags: ['users'], - - requireCredential: false as const, - - params: { - username: { - validator: $.use(Users.validateLocalUsername) - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - available: { - type: 'boolean' as const, - optional: false as const, nullable: false as const, - } - } - } -}; - -export default define(meta, async (ps) => { - // Get exist - const exist = await Users.count({ - host: null, - usernameLower: ps.username.toLowerCase() - }); - - const exist2 = await UsedUsernames.count({ username: ps.username.toLowerCase() }); - - return { - available: exist === 0 && exist2 === 0 - }; -}); diff --git a/src/server/api/endpoints/users.ts b/src/server/api/endpoints/users.ts deleted file mode 100644 index 930dcc7616..0000000000 --- a/src/server/api/endpoints/users.ts +++ /dev/null @@ -1,101 +0,0 @@ -import $ from 'cafy'; -import define from '../define'; -import { Users } from '@/models/index'; -import { generateMutedUserQueryForUsers } from '../common/generate-muted-user-query'; -import { generateBlockQueryForUsers } from '../common/generate-block-query'; - -export const meta = { - tags: ['users'], - - requireCredential: false as const, - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - offset: { - validator: $.optional.num.min(0), - default: 0 - }, - - sort: { - validator: $.optional.str.or([ - '+follower', - '-follower', - '+createdAt', - '-createdAt', - '+updatedAt', - '-updatedAt', - ]), - }, - - state: { - validator: $.optional.str.or([ - 'all', - 'admin', - 'moderator', - 'adminOrModerator', - 'alive' - ]), - default: 'all' - }, - - origin: { - validator: $.optional.str.or([ - 'combined', - 'local', - 'remote', - ]), - default: 'local' - } - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User', - } - }, -}; - -export default define(meta, async (ps, me) => { - const query = Users.createQueryBuilder('user'); - query.where('user.isExplorable = TRUE'); - - switch (ps.state) { - case 'admin': query.andWhere('user.isAdmin = TRUE'); break; - case 'moderator': query.andWhere('user.isModerator = TRUE'); break; - case 'adminOrModerator': query.andWhere('user.isAdmin = TRUE OR user.isModerator = TRUE'); break; - case 'alive': query.andWhere('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break; - } - - switch (ps.origin) { - case 'local': query.andWhere('user.host IS NULL'); break; - case 'remote': query.andWhere('user.host IS NOT NULL'); break; - } - - switch (ps.sort) { - case '+follower': query.orderBy('user.followersCount', 'DESC'); break; - case '-follower': query.orderBy('user.followersCount', 'ASC'); break; - case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break; - case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break; - case '+updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'DESC'); break; - case '-updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'ASC'); break; - default: query.orderBy('user.id', 'ASC'); break; - } - - if (me) generateMutedUserQueryForUsers(query, me); - if (me) generateBlockQueryForUsers(query, me); - - query.take(ps.limit!); - query.skip(ps.offset); - - const users = await query.getMany(); - - return await Users.packMany(users, me, { detail: true }); -}); diff --git a/src/server/api/endpoints/users/clips.ts b/src/server/api/endpoints/users/clips.ts deleted file mode 100644 index 8feca9422a..0000000000 --- a/src/server/api/endpoints/users/clips.ts +++ /dev/null @@ -1,40 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { Clips } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; - -export const meta = { - tags: ['users', 'clips'], - - params: { - userId: { - validator: $.type(ID), - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - } -}; - -export default define(meta, async (ps, user) => { - const query = makePaginationQuery(Clips.createQueryBuilder('clip'), ps.sinceId, ps.untilId) - .andWhere(`clip.userId = :userId`, { userId: ps.userId }) - .andWhere('clip.isPublic = true'); - - const clips = await query - .take(ps.limit!) - .getMany(); - - return await Clips.packMany(clips); -}); diff --git a/src/server/api/endpoints/users/followers.ts b/src/server/api/endpoints/users/followers.ts deleted file mode 100644 index 6d042a2861..0000000000 --- a/src/server/api/endpoints/users/followers.ts +++ /dev/null @@ -1,104 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { Users, Followings, UserProfiles } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; -import { toPunyNullable } from '@/misc/convert-host'; - -export const meta = { - tags: ['users'], - - requireCredential: false as const, - - params: { - userId: { - validator: $.optional.type(ID), - }, - - username: { - validator: $.optional.str - }, - - host: { - validator: $.optional.nullable.str - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Following', - } - }, - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '27fa5435-88ab-43de-9360-387de88727cd' - }, - - forbidden: { - message: 'Forbidden.', - code: 'FORBIDDEN', - id: '3c6a84db-d619-26af-ca14-06232a21df8a' - }, - } -}; - -export default define(meta, async (ps, me) => { - const user = await Users.findOne(ps.userId != null - ? { id: ps.userId } - : { usernameLower: ps.username!.toLowerCase(), host: toPunyNullable(ps.host) }); - - if (user == null) { - throw new ApiError(meta.errors.noSuchUser); - } - - const profile = await UserProfiles.findOneOrFail(user.id); - - if (profile.ffVisibility === 'private') { - if (me == null || (me.id !== user.id)) { - throw new ApiError(meta.errors.forbidden); - } - } else if (profile.ffVisibility === 'followers') { - if (me == null) { - throw new ApiError(meta.errors.forbidden); - } else if (me.id !== user.id) { - const following = await Followings.findOne({ - followeeId: user.id, - followerId: me.id, - }); - if (following == null) { - throw new ApiError(meta.errors.forbidden); - } - } - } - - const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) - .andWhere(`following.followeeId = :userId`, { userId: user.id }) - .innerJoinAndSelect('following.follower', 'follower'); - - const followings = await query - .take(ps.limit!) - .getMany(); - - return await Followings.packMany(followings, me, { populateFollower: true }); -}); diff --git a/src/server/api/endpoints/users/following.ts b/src/server/api/endpoints/users/following.ts deleted file mode 100644 index 1033117ef8..0000000000 --- a/src/server/api/endpoints/users/following.ts +++ /dev/null @@ -1,104 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { Users, Followings, UserProfiles } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; -import { toPunyNullable } from '@/misc/convert-host'; - -export const meta = { - tags: ['users'], - - requireCredential: false as const, - - params: { - userId: { - validator: $.optional.type(ID), - }, - - username: { - validator: $.optional.str - }, - - host: { - validator: $.optional.nullable.str - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Following', - } - }, - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '63e4aba4-4156-4e53-be25-c9559e42d71b' - }, - - forbidden: { - message: 'Forbidden.', - code: 'FORBIDDEN', - id: 'f6cdb0df-c19f-ec5c-7dbb-0ba84a1f92ba' - }, - } -}; - -export default define(meta, async (ps, me) => { - const user = await Users.findOne(ps.userId != null - ? { id: ps.userId } - : { usernameLower: ps.username!.toLowerCase(), host: toPunyNullable(ps.host) }); - - if (user == null) { - throw new ApiError(meta.errors.noSuchUser); - } - - const profile = await UserProfiles.findOneOrFail(user.id); - - if (profile.ffVisibility === 'private') { - if (me == null || (me.id !== user.id)) { - throw new ApiError(meta.errors.forbidden); - } - } else if (profile.ffVisibility === 'followers') { - if (me == null) { - throw new ApiError(meta.errors.forbidden); - } else if (me.id !== user.id) { - const following = await Followings.findOne({ - followeeId: user.id, - followerId: me.id, - }); - if (following == null) { - throw new ApiError(meta.errors.forbidden); - } - } - } - - const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) - .andWhere(`following.followerId = :userId`, { userId: user.id }) - .innerJoinAndSelect('following.followee', 'followee'); - - const followings = await query - .take(ps.limit!) - .getMany(); - - return await Followings.packMany(followings, me, { populateFollowee: true }); -}); diff --git a/src/server/api/endpoints/users/gallery/posts.ts b/src/server/api/endpoints/users/gallery/posts.ts deleted file mode 100644 index 845de1089c..0000000000 --- a/src/server/api/endpoints/users/gallery/posts.ts +++ /dev/null @@ -1,39 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { GalleryPosts } from '@/models/index'; -import { makePaginationQuery } from '../../../common/make-pagination-query'; - -export const meta = { - tags: ['users', 'gallery'], - - params: { - userId: { - validator: $.type(ID), - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - } -}; - -export default define(meta, async (ps, user) => { - const query = makePaginationQuery(GalleryPosts.createQueryBuilder('post'), ps.sinceId, ps.untilId) - .andWhere(`post.userId = :userId`, { userId: ps.userId }); - - const posts = await query - .take(ps.limit!) - .getMany(); - - return await GalleryPosts.packMany(posts, user); -}); diff --git a/src/server/api/endpoints/users/get-frequently-replied-users.ts b/src/server/api/endpoints/users/get-frequently-replied-users.ts deleted file mode 100644 index 32ebfd683a..0000000000 --- a/src/server/api/endpoints/users/get-frequently-replied-users.ts +++ /dev/null @@ -1,105 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { maximum } from '@/prelude/array'; -import { ApiError } from '../../error'; -import { getUser } from '../../common/getters'; -import { Not, In, IsNull } from 'typeorm'; -import { Notes, Users } from '@/models/index'; - -export const meta = { - tags: ['users'], - - requireCredential: false as const, - - params: { - userId: { - validator: $.type(ID), - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User', - } - }, - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: 'e6965129-7b2a-40a4-bae2-cd84cd434822' - } - } -}; - -export default define(meta, async (ps, me) => { - // Lookup user - const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - // Fetch recent notes - const recentNotes = await Notes.find({ - where: { - userId: user.id, - replyId: Not(IsNull()) - }, - order: { - id: -1 - }, - take: 1000, - select: ['replyId'] - }); - - // 投稿が少なかったら中断 - if (recentNotes.length === 0) { - return []; - } - - // TODO ミュートを考慮 - const replyTargetNotes = await Notes.find({ - where: { - id: In(recentNotes.map(p => p.replyId)), - }, - select: ['userId'] - }); - - const repliedUsers: any = {}; - - // Extract replies from recent notes - for (const userId of replyTargetNotes.map(x => x.userId.toString())) { - if (repliedUsers[userId]) { - repliedUsers[userId]++; - } else { - repliedUsers[userId] = 1; - } - } - - // Calc peak - const peak = maximum(Object.values(repliedUsers)); - - // Sort replies by frequency - const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]); - - // Extract top replied users - const topRepliedUsers = repliedUsersSorted.slice(0, ps.limit!); - - // Make replies object (includes weights) - const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({ - user: await Users.pack(user, me, { detail: true }), - weight: repliedUsers[user] / peak - }))); - - return repliesObj; -}); diff --git a/src/server/api/endpoints/users/groups/create.ts b/src/server/api/endpoints/users/groups/create.ts deleted file mode 100644 index dc1ee3879e..0000000000 --- a/src/server/api/endpoints/users/groups/create.ts +++ /dev/null @@ -1,45 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { UserGroups, UserGroupJoinings } from '@/models/index'; -import { genId } from '@/misc/gen-id'; -import { UserGroup } from '@/models/entities/user-group'; -import { UserGroupJoining } from '@/models/entities/user-group-joining'; - -export const meta = { - tags: ['groups'], - - requireCredential: true as const, - - kind: 'write:user-groups', - - params: { - name: { - validator: $.str.range(1, 100) - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'UserGroup', - }, -}; - -export default define(meta, async (ps, user) => { - const userGroup = await UserGroups.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - name: ps.name, - } as UserGroup).then(x => UserGroups.findOneOrFail(x.identifiers[0])); - - // Push the owner - await UserGroupJoinings.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - userGroupId: userGroup.id - } as UserGroupJoining); - - return await UserGroups.pack(userGroup); -}); diff --git a/src/server/api/endpoints/users/groups/delete.ts b/src/server/api/endpoints/users/groups/delete.ts deleted file mode 100644 index 7da1b4a273..0000000000 --- a/src/server/api/endpoints/users/groups/delete.ts +++ /dev/null @@ -1,40 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { UserGroups } from '@/models/index'; - -export const meta = { - tags: ['groups'], - - requireCredential: true as const, - - kind: 'write:user-groups', - - params: { - groupId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: '63dbd64c-cd77-413f-8e08-61781e210b38' - } - } -}; - -export default define(meta, async (ps, user) => { - const userGroup = await UserGroups.findOne({ - id: ps.groupId, - userId: user.id - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - await UserGroups.delete(userGroup.id); -}); diff --git a/src/server/api/endpoints/users/groups/invitations/accept.ts b/src/server/api/endpoints/users/groups/invitations/accept.ts deleted file mode 100644 index 09e6ae2647..0000000000 --- a/src/server/api/endpoints/users/groups/invitations/accept.ts +++ /dev/null @@ -1,54 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../../define'; -import { ApiError } from '../../../../error'; -import { UserGroupJoinings, UserGroupInvitations } from '@/models/index'; -import { genId } from '@/misc/gen-id'; -import { UserGroupJoining } from '@/models/entities/user-group-joining'; - -export const meta = { - tags: ['groups', 'users'], - - requireCredential: true as const, - - kind: 'write:user-groups', - - params: { - invitationId: { - validator: $.type(ID), - }, - }, - - errors: { - noSuchInvitation: { - message: 'No such invitation.', - code: 'NO_SUCH_INVITATION', - id: '98c11eca-c890-4f42-9806-c8c8303ebb5e' - }, - } -}; - -export default define(meta, async (ps, user) => { - // Fetch the invitation - const invitation = await UserGroupInvitations.findOne({ - id: ps.invitationId, - }); - - if (invitation == null) { - throw new ApiError(meta.errors.noSuchInvitation); - } - - if (invitation.userId !== user.id) { - throw new ApiError(meta.errors.noSuchInvitation); - } - - // Push the user - await UserGroupJoinings.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - userGroupId: invitation.userGroupId - } as UserGroupJoining); - - UserGroupInvitations.delete(invitation.id); -}); diff --git a/src/server/api/endpoints/users/groups/invitations/reject.ts b/src/server/api/endpoints/users/groups/invitations/reject.ts deleted file mode 100644 index 741fcefb35..0000000000 --- a/src/server/api/endpoints/users/groups/invitations/reject.ts +++ /dev/null @@ -1,44 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../../define'; -import { ApiError } from '../../../../error'; -import { UserGroupInvitations } from '@/models/index'; - -export const meta = { - tags: ['groups', 'users'], - - requireCredential: true as const, - - kind: 'write:user-groups', - - params: { - invitationId: { - validator: $.type(ID), - }, - }, - - errors: { - noSuchInvitation: { - message: 'No such invitation.', - code: 'NO_SUCH_INVITATION', - id: 'ad7471d4-2cd9-44b4-ac68-e7136b4ce656' - }, - } -}; - -export default define(meta, async (ps, user) => { - // Fetch the invitation - const invitation = await UserGroupInvitations.findOne({ - id: ps.invitationId, - }); - - if (invitation == null) { - throw new ApiError(meta.errors.noSuchInvitation); - } - - if (invitation.userId !== user.id) { - throw new ApiError(meta.errors.noSuchInvitation); - } - - await UserGroupInvitations.delete(invitation.id); -}); diff --git a/src/server/api/endpoints/users/groups/invite.ts b/src/server/api/endpoints/users/groups/invite.ts deleted file mode 100644 index f1ee8bf8b7..0000000000 --- a/src/server/api/endpoints/users/groups/invite.ts +++ /dev/null @@ -1,102 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { getUser } from '../../../common/getters'; -import { UserGroups, UserGroupJoinings, UserGroupInvitations } from '@/models/index'; -import { genId } from '@/misc/gen-id'; -import { UserGroupInvitation } from '@/models/entities/user-group-invitation'; -import { createNotification } from '@/services/create-notification'; - -export const meta = { - tags: ['groups', 'users'], - - requireCredential: true as const, - - kind: 'write:user-groups', - - params: { - groupId: { - validator: $.type(ID), - }, - - userId: { - validator: $.type(ID), - }, - }, - - errors: { - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: '583f8bc0-8eee-4b78-9299-1e14fc91e409' - }, - - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: 'da52de61-002c-475b-90e1-ba64f9cf13a8' - }, - - alreadyAdded: { - message: 'That user has already been added to that group.', - code: 'ALREADY_ADDED', - id: '7e35c6a0-39b2-4488-aea6-6ee20bd5da2c' - }, - - alreadyInvited: { - message: 'That user has already been invited to that group.', - code: 'ALREADY_INVITED', - id: 'ee0f58b4-b529-4d13-b761-b9a3e69f97e6' - } - } -}; - -export default define(meta, async (ps, me) => { - // Fetch the group - const userGroup = await UserGroups.findOne({ - id: ps.groupId, - userId: me.id, - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - // Fetch the user - const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - const joining = await UserGroupJoinings.findOne({ - userGroupId: userGroup.id, - userId: user.id - }); - - if (joining) { - throw new ApiError(meta.errors.alreadyAdded); - } - - const existInvitation = await UserGroupInvitations.findOne({ - userGroupId: userGroup.id, - userId: user.id - }); - - if (existInvitation) { - throw new ApiError(meta.errors.alreadyInvited); - } - - const invitation = await UserGroupInvitations.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - userGroupId: userGroup.id - } as UserGroupInvitation).then(x => UserGroupInvitations.findOneOrFail(x.identifiers[0])); - - // 通知を作成 - createNotification(user.id, 'groupInvited', { - notifierId: me.id, - userGroupInvitationId: invitation.id - }); -}); diff --git a/src/server/api/endpoints/users/groups/joined.ts b/src/server/api/endpoints/users/groups/joined.ts deleted file mode 100644 index d5e8fe4032..0000000000 --- a/src/server/api/endpoints/users/groups/joined.ts +++ /dev/null @@ -1,36 +0,0 @@ -import define from '../../../define'; -import { UserGroups, UserGroupJoinings } from '@/models/index'; -import { Not, In } from 'typeorm'; - -export const meta = { - tags: ['groups', 'account'], - - requireCredential: true as const, - - kind: 'read:user-groups', - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'UserGroup', - } - }, -}; - -export default define(meta, async (ps, me) => { - const ownedGroups = await UserGroups.find({ - userId: me.id, - }); - - const joinings = await UserGroupJoinings.find({ - userId: me.id, - ...(ownedGroups.length > 0 ? { - userGroupId: Not(In(ownedGroups.map(x => x.id))) - } : {}) - }); - - return await Promise.all(joinings.map(x => UserGroups.pack(x.userGroupId))); -}); diff --git a/src/server/api/endpoints/users/groups/leave.ts b/src/server/api/endpoints/users/groups/leave.ts deleted file mode 100644 index 0e52f2abdf..0000000000 --- a/src/server/api/endpoints/users/groups/leave.ts +++ /dev/null @@ -1,50 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { UserGroups, UserGroupJoinings } from '@/models/index'; - -export const meta = { - tags: ['groups', 'users'], - - requireCredential: true as const, - - kind: 'write:user-groups', - - params: { - groupId: { - validator: $.type(ID), - }, - }, - - errors: { - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: '62780270-1f67-5dc0-daca-3eb510612e31' - }, - - youAreOwner: { - message: 'Your are the owner.', - code: 'YOU_ARE_OWNER', - id: 'b6d6e0c2-ef8a-9bb8-653d-79f4a3107c69' - }, - } -}; - -export default define(meta, async (ps, me) => { - // Fetch the group - const userGroup = await UserGroups.findOne({ - id: ps.groupId, - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - if (me.id === userGroup.userId) { - throw new ApiError(meta.errors.youAreOwner); - } - - await UserGroupJoinings.delete({ userGroupId: userGroup.id, userId: me.id }); -}); diff --git a/src/server/api/endpoints/users/groups/owned.ts b/src/server/api/endpoints/users/groups/owned.ts deleted file mode 100644 index 17de370dbc..0000000000 --- a/src/server/api/endpoints/users/groups/owned.ts +++ /dev/null @@ -1,28 +0,0 @@ -import define from '../../../define'; -import { UserGroups } from '@/models/index'; - -export const meta = { - tags: ['groups', 'account'], - - requireCredential: true as const, - - kind: 'read:user-groups', - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'UserGroup', - } - }, -}; - -export default define(meta, async (ps, me) => { - const userGroups = await UserGroups.find({ - userId: me.id, - }); - - return await Promise.all(userGroups.map(x => UserGroups.pack(x))); -}); diff --git a/src/server/api/endpoints/users/groups/pull.ts b/src/server/api/endpoints/users/groups/pull.ts deleted file mode 100644 index ce4d2e2881..0000000000 --- a/src/server/api/endpoints/users/groups/pull.ts +++ /dev/null @@ -1,69 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { getUser } from '../../../common/getters'; -import { UserGroups, UserGroupJoinings } from '@/models/index'; - -export const meta = { - tags: ['groups', 'users'], - - requireCredential: true as const, - - kind: 'write:user-groups', - - params: { - groupId: { - validator: $.type(ID), - }, - - userId: { - validator: $.type(ID), - }, - }, - - errors: { - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: '4662487c-05b1-4b78-86e5-fd46998aba74' - }, - - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '0b5cc374-3681-41da-861e-8bc1146f7a55' - }, - - isOwner: { - message: 'The user is the owner.', - code: 'IS_OWNER', - id: '1546eed5-4414-4dea-81c1-b0aec4f6d2af' - }, - } -}; - -export default define(meta, async (ps, me) => { - // Fetch the group - const userGroup = await UserGroups.findOne({ - id: ps.groupId, - userId: me.id, - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - // Fetch the user - const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - if (user.id === userGroup.userId) { - throw new ApiError(meta.errors.isOwner); - } - - // Pull the user - await UserGroupJoinings.delete({ userGroupId: userGroup.id, userId: user.id }); -}); diff --git a/src/server/api/endpoints/users/groups/show.ts b/src/server/api/endpoints/users/groups/show.ts deleted file mode 100644 index 3c030bf3a5..0000000000 --- a/src/server/api/endpoints/users/groups/show.ts +++ /dev/null @@ -1,55 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { UserGroups, UserGroupJoinings } from '@/models/index'; - -export const meta = { - tags: ['groups', 'account'], - - requireCredential: true as const, - - kind: 'read:user-groups', - - params: { - groupId: { - validator: $.type(ID), - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'UserGroup', - }, - - errors: { - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: 'ea04751e-9b7e-487b-a509-330fb6bd6b9b' - }, - } -}; - -export default define(meta, async (ps, me) => { - // Fetch the group - const userGroup = await UserGroups.findOne({ - id: ps.groupId, - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - const joining = await UserGroupJoinings.findOne({ - userId: me.id, - userGroupId: userGroup.id - }); - - if (joining == null && userGroup.userId !== me.id) { - throw new ApiError(meta.errors.noSuchGroup); - } - - return await UserGroups.pack(userGroup); -}); diff --git a/src/server/api/endpoints/users/groups/transfer.ts b/src/server/api/endpoints/users/groups/transfer.ts deleted file mode 100644 index 17c42e1127..0000000000 --- a/src/server/api/endpoints/users/groups/transfer.ts +++ /dev/null @@ -1,83 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { getUser } from '../../../common/getters'; -import { UserGroups, UserGroupJoinings } from '@/models/index'; - -export const meta = { - tags: ['groups', 'users'], - - requireCredential: true as const, - - kind: 'write:user-groups', - - params: { - groupId: { - validator: $.type(ID), - }, - - userId: { - validator: $.type(ID), - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'UserGroup', - }, - - errors: { - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: '8e31d36b-2f88-4ccd-a438-e2d78a9162db' - }, - - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '711f7ebb-bbb9-4dfa-b540-b27809fed5e9' - }, - - noSuchGroupMember: { - message: 'No such group member.', - code: 'NO_SUCH_GROUP_MEMBER', - id: 'd31bebee-196d-42c2-9a3e-9474d4be6cc4' - }, - } -}; - -export default define(meta, async (ps, me) => { - // Fetch the group - const userGroup = await UserGroups.findOne({ - id: ps.groupId, - userId: me.id, - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - // Fetch the user - const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - const joining = await UserGroupJoinings.findOne({ - userGroupId: userGroup.id, - userId: user.id - }); - - if (joining == null) { - throw new ApiError(meta.errors.noSuchGroupMember); - } - - await UserGroups.update(userGroup.id, { - userId: ps.userId - }); - - return await UserGroups.pack(userGroup.id); -}); diff --git a/src/server/api/endpoints/users/groups/update.ts b/src/server/api/endpoints/users/groups/update.ts deleted file mode 100644 index 127bbc47a1..0000000000 --- a/src/server/api/endpoints/users/groups/update.ts +++ /dev/null @@ -1,55 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { UserGroups } from '@/models/index'; - -export const meta = { - tags: ['groups'], - - requireCredential: true as const, - - kind: 'write:user-groups', - - params: { - groupId: { - validator: $.type(ID), - }, - - name: { - validator: $.str.range(1, 100), - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'UserGroup', - }, - - errors: { - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: '9081cda3-7a9e-4fac-a6ce-908d70f282f6' - }, - } -}; - -export default define(meta, async (ps, me) => { - // Fetch the group - const userGroup = await UserGroups.findOne({ - id: ps.groupId, - userId: me.id - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - await UserGroups.update(userGroup.id, { - name: ps.name - }); - - return await UserGroups.pack(userGroup.id); -}); diff --git a/src/server/api/endpoints/users/lists/create.ts b/src/server/api/endpoints/users/lists/create.ts deleted file mode 100644 index e0bfe611fc..0000000000 --- a/src/server/api/endpoints/users/lists/create.ts +++ /dev/null @@ -1,36 +0,0 @@ -import $ from 'cafy'; -import define from '../../../define'; -import { UserLists } from '@/models/index'; -import { genId } from '@/misc/gen-id'; -import { UserList } from '@/models/entities/user-list'; - -export const meta = { - tags: ['lists'], - - requireCredential: true as const, - - kind: 'write:account', - - params: { - name: { - validator: $.str.range(1, 100) - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'UserList', - }, -}; - -export default define(meta, async (ps, user) => { - const userList = await UserLists.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - name: ps.name, - } as UserList).then(x => UserLists.findOneOrFail(x.identifiers[0])); - - return await UserLists.pack(userList); -}); diff --git a/src/server/api/endpoints/users/lists/delete.ts b/src/server/api/endpoints/users/lists/delete.ts deleted file mode 100644 index 5fe3bfb03d..0000000000 --- a/src/server/api/endpoints/users/lists/delete.ts +++ /dev/null @@ -1,40 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { UserLists } from '@/models/index'; - -export const meta = { - tags: ['lists'], - - requireCredential: true as const, - - kind: 'write:account', - - params: { - listId: { - validator: $.type(ID), - } - }, - - errors: { - noSuchList: { - message: 'No such list.', - code: 'NO_SUCH_LIST', - id: '78436795-db79-42f5-b1e2-55ea2cf19166' - } - } -}; - -export default define(meta, async (ps, user) => { - const userList = await UserLists.findOne({ - id: ps.listId, - userId: user.id - }); - - if (userList == null) { - throw new ApiError(meta.errors.noSuchList); - } - - await UserLists.delete(userList.id); -}); diff --git a/src/server/api/endpoints/users/lists/list.ts b/src/server/api/endpoints/users/lists/list.ts deleted file mode 100644 index cf0c92bb84..0000000000 --- a/src/server/api/endpoints/users/lists/list.ts +++ /dev/null @@ -1,28 +0,0 @@ -import define from '../../../define'; -import { UserLists } from '@/models/index'; - -export const meta = { - tags: ['lists', 'account'], - - requireCredential: true as const, - - kind: 'read:account', - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'UserList', - } - }, -}; - -export default define(meta, async (ps, me) => { - const userLists = await UserLists.find({ - userId: me.id, - }); - - return await Promise.all(userLists.map(x => UserLists.pack(x))); -}); diff --git a/src/server/api/endpoints/users/lists/pull.ts b/src/server/api/endpoints/users/lists/pull.ts deleted file mode 100644 index d4357fc5e7..0000000000 --- a/src/server/api/endpoints/users/lists/pull.ts +++ /dev/null @@ -1,62 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import { publishUserListStream } from '@/services/stream'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { getUser } from '../../../common/getters'; -import { UserLists, UserListJoinings, Users } from '@/models/index'; - -export const meta = { - tags: ['lists', 'users'], - - requireCredential: true as const, - - kind: 'write:account', - - params: { - listId: { - validator: $.type(ID), - }, - - userId: { - validator: $.type(ID), - }, - }, - - errors: { - noSuchList: { - message: 'No such list.', - code: 'NO_SUCH_LIST', - id: '7f44670e-ab16-43b8-b4c1-ccd2ee89cc02' - }, - - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '588e7f72-c744-4a61-b180-d354e912bda2' - } - } -}; - -export default define(meta, async (ps, me) => { - // Fetch the list - const userList = await UserLists.findOne({ - id: ps.listId, - userId: me.id, - }); - - if (userList == null) { - throw new ApiError(meta.errors.noSuchList); - } - - // Fetch the user - const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - // Pull the user - await UserListJoinings.delete({ userListId: userList.id, userId: user.id }); - - publishUserListStream(userList.id, 'userRemoved', await Users.pack(user)); -}); diff --git a/src/server/api/endpoints/users/lists/push.ts b/src/server/api/endpoints/users/lists/push.ts deleted file mode 100644 index 8e21059d3d..0000000000 --- a/src/server/api/endpoints/users/lists/push.ts +++ /dev/null @@ -1,92 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { getUser } from '../../../common/getters'; -import { pushUserToUserList } from '@/services/user-list/push'; -import { UserLists, UserListJoinings, Blockings } from '@/models/index'; - -export const meta = { - tags: ['lists', 'users'], - - requireCredential: true as const, - - kind: 'write:account', - - params: { - listId: { - validator: $.type(ID), - }, - - userId: { - validator: $.type(ID), - }, - }, - - errors: { - noSuchList: { - message: 'No such list.', - code: 'NO_SUCH_LIST', - id: '2214501d-ac96-4049-b717-91e42272a711' - }, - - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: 'a89abd3d-f0bc-4cce-beb1-2f446f4f1e6a' - }, - - alreadyAdded: { - message: 'That user has already been added to that list.', - code: 'ALREADY_ADDED', - id: '1de7c884-1595-49e9-857e-61f12f4d4fc5' - }, - - youHaveBeenBlocked: { - message: 'You cannot push this user because you have been blocked by this user.', - code: 'YOU_HAVE_BEEN_BLOCKED', - id: '990232c5-3f9d-4d83-9f3f-ef27b6332a4b' - }, - } -}; - -export default define(meta, async (ps, me) => { - // Fetch the list - const userList = await UserLists.findOne({ - id: ps.listId, - userId: me.id, - }); - - if (userList == null) { - throw new ApiError(meta.errors.noSuchList); - } - - // Fetch the user - const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - // Check blocking - if (user.id !== me.id) { - const block = await Blockings.findOne({ - blockerId: user.id, - blockeeId: me.id, - }); - if (block) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } - } - - const exist = await UserListJoinings.findOne({ - userListId: userList.id, - userId: user.id - }); - - if (exist) { - throw new ApiError(meta.errors.alreadyAdded); - } - - // Push the user - await pushUserToUserList(user, userList); -}); diff --git a/src/server/api/endpoints/users/lists/show.ts b/src/server/api/endpoints/users/lists/show.ts deleted file mode 100644 index f9a35cdab3..0000000000 --- a/src/server/api/endpoints/users/lists/show.ts +++ /dev/null @@ -1,47 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { UserLists } from '@/models/index'; - -export const meta = { - tags: ['lists', 'account'], - - requireCredential: true as const, - - kind: 'read:account', - - params: { - listId: { - validator: $.type(ID), - }, - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'UserList', - }, - - errors: { - noSuchList: { - message: 'No such list.', - code: 'NO_SUCH_LIST', - id: '7bc05c21-1d7a-41ae-88f1-66820f4dc686' - }, - } -}; - -export default define(meta, async (ps, me) => { - // Fetch the list - const userList = await UserLists.findOne({ - id: ps.listId, - userId: me.id, - }); - - if (userList == null) { - throw new ApiError(meta.errors.noSuchList); - } - - return await UserLists.pack(userList); -}); diff --git a/src/server/api/endpoints/users/lists/update.ts b/src/server/api/endpoints/users/lists/update.ts deleted file mode 100644 index 1185af5043..0000000000 --- a/src/server/api/endpoints/users/lists/update.ts +++ /dev/null @@ -1,55 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../../define'; -import { ApiError } from '../../../error'; -import { UserLists } from '@/models/index'; - -export const meta = { - tags: ['lists'], - - requireCredential: true as const, - - kind: 'write:account', - - params: { - listId: { - validator: $.type(ID), - }, - - name: { - validator: $.str.range(1, 100), - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'UserList', - }, - - errors: { - noSuchList: { - message: 'No such list.', - code: 'NO_SUCH_LIST', - id: '796666fe-3dff-4d39-becb-8a5932c1d5b7' - }, - } -}; - -export default define(meta, async (ps, user) => { - // Fetch the list - const userList = await UserLists.findOne({ - id: ps.listId, - userId: user.id - }); - - if (userList == null) { - throw new ApiError(meta.errors.noSuchList); - } - - await UserLists.update(userList.id, { - name: ps.name - }); - - return await UserLists.pack(userList.id); -}); diff --git a/src/server/api/endpoints/users/notes.ts b/src/server/api/endpoints/users/notes.ts deleted file mode 100644 index 0afbad9d04..0000000000 --- a/src/server/api/endpoints/users/notes.ts +++ /dev/null @@ -1,144 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { getUser } from '../../common/getters'; -import { makePaginationQuery } from '../../common/make-pagination-query'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query'; -import { Notes } from '@/models/index'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; -import { Brackets } from 'typeorm'; -import { generateBlockedUserQuery } from '../../common/generate-block-query'; - -export const meta = { - tags: ['users', 'notes'], - - params: { - userId: { - validator: $.type(ID), - }, - - includeReplies: { - validator: $.optional.bool, - default: true, - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10, - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - sinceDate: { - validator: $.optional.num, - }, - - untilDate: { - validator: $.optional.num, - }, - - includeMyRenotes: { - validator: $.optional.bool, - default: true, - }, - - withFiles: { - validator: $.optional.bool, - default: false, - }, - - fileType: { - validator: $.optional.arr($.str), - }, - - excludeNsfw: { - validator: $.optional.bool, - default: false, - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'Note', - } - }, - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '27e494ba-2ac2-48e8-893b-10d4d8c2387b' - } - } -}; - -export default define(meta, async (ps, me) => { - // Lookup user - const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - //#region Construct query - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.userId = :userId', { userId: user.id }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - generateVisibilityQuery(query, me); - if (me) generateMutedUserQuery(query, me, user); - if (me) generateBlockedUserQuery(query, me); - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - - if (ps.fileType != null) { - query.andWhere('note.fileIds != \'{}\''); - query.andWhere(new Brackets(qb => { - for (const type of ps.fileType!) { - const i = ps.fileType!.indexOf(type); - qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type }); - } - })); - - if (ps.excludeNsfw) { - query.andWhere('note.cw IS NULL'); - query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)'); - } - } - - if (!ps.includeReplies) { - query.andWhere('note.replyId IS NULL'); - } - - if (ps.includeMyRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :userId', { userId: user.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - //#endregion - - const timeline = await query.take(ps.limit!).getMany(); - - return await Notes.packMany(timeline, me); -}); diff --git a/src/server/api/endpoints/users/pages.ts b/src/server/api/endpoints/users/pages.ts deleted file mode 100644 index 24e9e207fd..0000000000 --- a/src/server/api/endpoints/users/pages.ts +++ /dev/null @@ -1,40 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { Pages } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; - -export const meta = { - tags: ['users', 'pages'], - - params: { - userId: { - validator: $.type(ID), - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - } -}; - -export default define(meta, async (ps, user) => { - const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId) - .andWhere(`page.userId = :userId`, { userId: ps.userId }) - .andWhere('page.visibility = \'public\''); - - const pages = await query - .take(ps.limit!) - .getMany(); - - return await Pages.packMany(pages); -}); diff --git a/src/server/api/endpoints/users/reactions.ts b/src/server/api/endpoints/users/reactions.ts deleted file mode 100644 index fe5e4d84a9..0000000000 --- a/src/server/api/endpoints/users/reactions.ts +++ /dev/null @@ -1,79 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { NoteReactions, UserProfiles } from '@/models/index'; -import { makePaginationQuery } from '../../common/make-pagination-query'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query'; -import { ApiError } from '../../error'; - -export const meta = { - tags: ['users', 'reactions'], - - requireCredential: false as const, - - params: { - userId: { - validator: $.type(ID), - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10, - }, - - sinceId: { - validator: $.optional.type(ID), - }, - - untilId: { - validator: $.optional.type(ID), - }, - - sinceDate: { - validator: $.optional.num, - }, - - untilDate: { - validator: $.optional.num, - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'NoteReaction', - } - }, - - errors: { - reactionsNotPublic: { - message: 'Reactions of the user is not public.', - code: 'REACTIONS_NOT_PUBLIC', - id: '673a7dd2-6924-1093-e0c0-e68456ceae5c' - }, - } -}; - -export default define(meta, async (ps, me) => { - const profile = await UserProfiles.findOneOrFail(ps.userId); - - if (me == null || (me.id !== ps.userId && !profile.publicReactions)) { - throw new ApiError(meta.errors.reactionsNotPublic); - } - - const query = makePaginationQuery(NoteReactions.createQueryBuilder('reaction'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere(`reaction.userId = :userId`, { userId: ps.userId }) - .leftJoinAndSelect('reaction.note', 'note'); - - generateVisibilityQuery(query, me); - - const reactions = await query - .take(ps.limit!) - .getMany(); - - return await Promise.all(reactions.map(reaction => NoteReactions.pack(reaction, me, { withNote: true }))); -}); diff --git a/src/server/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts deleted file mode 100644 index dde6bb1037..0000000000 --- a/src/server/api/endpoints/users/recommendation.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as ms from 'ms'; -import $ from 'cafy'; -import define from '../../define'; -import { Users, Followings } from '@/models/index'; -import { generateMutedUserQueryForUsers } from '../../common/generate-muted-user-query'; -import { generateBlockedUserQuery, generateBlockQueryForUsers } from '../../common/generate-block-query'; - -export const meta = { - tags: ['users'], - - requireCredential: true as const, - - kind: 'read:account', - - params: { - limit: { - validator: $.optional.num.range(1, 100), - default: 10 - }, - - offset: { - validator: $.optional.num.min(0), - default: 0 - } - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User', - } - }, -}; - -export default define(meta, async (ps, me) => { - const query = Users.createQueryBuilder('user') - .where('user.isLocked = FALSE') - .andWhere('user.isExplorable = TRUE') - .andWhere('user.host IS NULL') - .andWhere('user.updatedAt >= :date', { date: new Date(Date.now() - ms('7days')) }) - .andWhere('user.id != :meId', { meId: me.id }) - .orderBy('user.followersCount', 'DESC'); - - generateMutedUserQueryForUsers(query, me); - generateBlockQueryForUsers(query, me); - generateBlockedUserQuery(query, me); - - const followingQuery = Followings.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: me.id }); - - query - .andWhere(`user.id NOT IN (${ followingQuery.getQuery() })`); - - query.setParameters(followingQuery.getParameters()); - - const users = await query.take(ps.limit!).skip(ps.offset).getMany(); - - return await Users.packMany(users, me, { detail: true }); -}); diff --git a/src/server/api/endpoints/users/relation.ts b/src/server/api/endpoints/users/relation.ts deleted file mode 100644 index 32d76a5322..0000000000 --- a/src/server/api/endpoints/users/relation.ts +++ /dev/null @@ -1,111 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { ID } from '@/misc/cafy-id'; -import { Users } from '@/models/index'; - -export const meta = { - tags: ['users'], - - requireCredential: true as const, - - params: { - userId: { - validator: $.either($.type(ID), $.arr($.type(ID)).unique()), - } - }, - - res: { - oneOf: [ - { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id' - }, - isFollowing: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - hasPendingFollowRequestFromYou: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - hasPendingFollowRequestToYou: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - isFollowed: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - isBlocking: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - isBlocked: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - isMuted: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - } - } - }, - { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - properties: { - id: { - type: 'string' as const, - optional: false as const, nullable: false as const, - format: 'id' - }, - isFollowing: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - hasPendingFollowRequestFromYou: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - hasPendingFollowRequestToYou: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - isFollowed: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - isBlocking: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - isBlocked: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - }, - isMuted: { - type: 'boolean' as const, - optional: false as const, nullable: false as const - } - } - } - } - ] - } -}; - -export default define(meta, async (ps, me) => { - const ids = Array.isArray(ps.userId) ? ps.userId : [ps.userId]; - - const relations = await Promise.all(ids.map(id => Users.getRelation(me.id, id))); - - return Array.isArray(ps.userId) ? relations : relations[0]; -}); diff --git a/src/server/api/endpoints/users/report-abuse.ts b/src/server/api/endpoints/users/report-abuse.ts deleted file mode 100644 index 2c8672cd47..0000000000 --- a/src/server/api/endpoints/users/report-abuse.ts +++ /dev/null @@ -1,90 +0,0 @@ -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id'; -import define from '../../define'; -import { publishAdminStream } from '@/services/stream'; -import { ApiError } from '../../error'; -import { getUser } from '../../common/getters'; -import { AbuseUserReports, Users } from '@/models/index'; -import { genId } from '@/misc/gen-id'; - -export const meta = { - tags: ['users'], - - requireCredential: true as const, - - params: { - userId: { - validator: $.type(ID), - }, - - comment: { - validator: $.str.range(1, 2048), - }, - }, - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '1acefcb5-0959-43fd-9685-b48305736cb5' - }, - - cannotReportYourself: { - message: 'Cannot report yourself.', - code: 'CANNOT_REPORT_YOURSELF', - id: '1e13149e-b1e8-43cf-902e-c01dbfcb202f' - }, - - cannotReportAdmin: { - message: 'Cannot report the admin.', - code: 'CANNOT_REPORT_THE_ADMIN', - id: '35e166f5-05fb-4f87-a2d5-adb42676d48f' - } - } -}; - -export default define(meta, async (ps, me) => { - // Lookup user - const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - if (user.id === me.id) { - throw new ApiError(meta.errors.cannotReportYourself); - } - - if (user.isAdmin) { - throw new ApiError(meta.errors.cannotReportAdmin); - } - - const report = await AbuseUserReports.save({ - id: genId(), - createdAt: new Date(), - targetUserId: user.id, - targetUserHost: user.host, - reporterId: me.id, - reporterHost: null, - comment: ps.comment, - }); - - // Publish event to moderators - setTimeout(async () => { - const moderators = await Users.find({ - where: [{ - isAdmin: true - }, { - isModerator: true - }] - }); - - for (const moderator of moderators) { - publishAdminStream(moderator.id, 'newAbuseUserReport', { - id: report.id, - targetUserId: report.targetUserId, - reporterId: report.reporterId, - comment: report.comment - }); - } - }, 1); -}); diff --git a/src/server/api/endpoints/users/search-by-username-and-host.ts b/src/server/api/endpoints/users/search-by-username-and-host.ts deleted file mode 100644 index 1ec5e1a743..0000000000 --- a/src/server/api/endpoints/users/search-by-username-and-host.ts +++ /dev/null @@ -1,116 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { Followings, Users } from '@/models/index'; -import { Brackets } from 'typeorm'; -import { USER_ACTIVE_THRESHOLD } from '@/const'; -import { User } from '@/models/entities/user'; - -export const meta = { - tags: ['users'], - - requireCredential: false as const, - - params: { - username: { - validator: $.optional.nullable.str, - }, - - host: { - validator: $.optional.nullable.str, - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10, - }, - - detail: { - validator: $.optional.bool, - default: true, - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User', - } - }, -}; - -export default define(meta, async (ps, me) => { - const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日 - - if (ps.host) { - const q = Users.createQueryBuilder('user') - .where('user.isSuspended = FALSE') - .andWhere('user.host LIKE :host', { host: ps.host.toLowerCase() + '%' }); - - if (ps.username) { - q.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }); - } - - q.andWhere('user.updatedAt IS NOT NULL'); - q.orderBy('user.updatedAt', 'DESC'); - - const users = await q.take(ps.limit!).getMany(); - - return await Users.packMany(users, me, { detail: ps.detail }); - } else if (ps.username) { - let users: User[] = []; - - if (me) { - const followingQuery = Followings.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: me.id }); - - const query = Users.createQueryBuilder('user') - .where(`user.id IN (${ followingQuery.getQuery() })`) - .andWhere(`user.id != :meId`, { meId: me.id }) - .andWhere('user.isSuspended = FALSE') - .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) - .andWhere(new Brackets(qb => { qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); - })); - - query.setParameters(followingQuery.getParameters()); - - users = await query - .orderBy('user.usernameLower', 'ASC') - .take(ps.limit!) - .getMany(); - - if (users.length < ps.limit!) { - const otherQuery = await Users.createQueryBuilder('user') - .where(`user.id NOT IN (${ followingQuery.getQuery() })`) - .andWhere(`user.id != :meId`, { meId: me.id }) - .andWhere('user.isSuspended = FALSE') - .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) - .andWhere('user.updatedAt IS NOT NULL'); - - otherQuery.setParameters(followingQuery.getParameters()); - - const otherUsers = await otherQuery - .orderBy('user.updatedAt', 'DESC') - .take(ps.limit! - users.length) - .getMany(); - - users = users.concat(otherUsers); - } - } else { - users = await Users.createQueryBuilder('user') - .where('user.isSuspended = FALSE') - .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) - .andWhere('user.updatedAt IS NOT NULL') - .orderBy('user.updatedAt', 'DESC') - .take(ps.limit! - users.length) - .getMany(); - } - - return await Users.packMany(users, me, { detail: ps.detail }); - } -}); diff --git a/src/server/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts deleted file mode 100644 index 9aa988d9ed..0000000000 --- a/src/server/api/endpoints/users/search.ts +++ /dev/null @@ -1,127 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { UserProfiles, Users } from '@/models/index'; -import { User } from '@/models/entities/user'; -import { Brackets } from 'typeorm'; - -export const meta = { - tags: ['users'], - - requireCredential: false as const, - - params: { - query: { - validator: $.str, - }, - - offset: { - validator: $.optional.num.min(0), - default: 0, - }, - - limit: { - validator: $.optional.num.range(1, 100), - default: 10, - }, - - origin: { - validator: $.optional.str.or(['local', 'remote', 'combined']), - default: 'combined', - }, - - detail: { - validator: $.optional.bool, - default: true, - }, - }, - - res: { - type: 'array' as const, - optional: false as const, nullable: false as const, - items: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User', - } - }, -}; - -export default define(meta, async (ps, me) => { - const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日 - - const isUsername = ps.query.startsWith('@'); - - let users: User[] = []; - - if (isUsername) { - const usernameQuery = Users.createQueryBuilder('user') - .where('user.usernameLower LIKE :username', { username: ps.query.replace('@', '').toLowerCase() + '%' }) - .andWhere(new Brackets(qb => { qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); - })) - .andWhere('user.isSuspended = FALSE'); - - if (ps.origin === 'local') { - usernameQuery.andWhere('user.host IS NULL'); - } else if (ps.origin === 'remote') { - usernameQuery.andWhere('user.host IS NOT NULL'); - } - - users = await usernameQuery - .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') - .take(ps.limit!) - .skip(ps.offset) - .getMany(); - } else { - const nameQuery = Users.createQueryBuilder('user') - .where('user.name ILIKE :query', { query: '%' + ps.query + '%' }) - .andWhere(new Brackets(qb => { qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); - })) - .andWhere('user.isSuspended = FALSE'); - - if (ps.origin === 'local') { - nameQuery.andWhere('user.host IS NULL'); - } else if (ps.origin === 'remote') { - nameQuery.andWhere('user.host IS NOT NULL'); - } - - users = await nameQuery - .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') - .take(ps.limit!) - .skip(ps.offset) - .getMany(); - - if (users.length < ps.limit!) { - const profQuery = UserProfiles.createQueryBuilder('prof') - .select('prof.userId') - .where('prof.description ILIKE :query', { query: '%' + ps.query + '%' }); - - if (ps.origin === 'local') { - profQuery.andWhere('prof.userHost IS NULL'); - } else if (ps.origin === 'remote') { - profQuery.andWhere('prof.userHost IS NOT NULL'); - } - - const query = Users.createQueryBuilder('user') - .where(`user.id IN (${ profQuery.getQuery() })`) - .andWhere(new Brackets(qb => { qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); - })) - .andWhere('user.isSuspended = FALSE') - .setParameters(profQuery.getParameters()); - - users = users.concat(await query - .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') - .take(ps.limit!) - .skip(ps.offset) - .getMany() - ); - } - } - - return await Users.packMany(users, me, { detail: ps.detail }); -}); diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts deleted file mode 100644 index f056983636..0000000000 --- a/src/server/api/endpoints/users/show.ts +++ /dev/null @@ -1,105 +0,0 @@ -import $ from 'cafy'; -import { resolveUser } from '@/remote/resolve-user'; -import define from '../../define'; -import { apiLogger } from '../../logger'; -import { ApiError } from '../../error'; -import { ID } from '@/misc/cafy-id'; -import { Users } from '@/models/index'; -import { In } from 'typeorm'; -import { User } from '@/models/entities/user'; - -export const meta = { - tags: ['users'], - - requireCredential: false as const, - - params: { - userId: { - validator: $.optional.type(ID), - }, - - userIds: { - validator: $.optional.arr($.type(ID)).unique(), - }, - - username: { - validator: $.optional.str - }, - - host: { - validator: $.optional.nullable.str - } - }, - - res: { - type: 'object' as const, - optional: false as const, nullable: false as const, - ref: 'User', - }, - - errors: { - failedToResolveRemoteUser: { - message: 'Failed to resolve remote user.', - code: 'FAILED_TO_RESOLVE_REMOTE_USER', - id: 'ef7b9be4-9cba-4e6f-ab41-90ed171c7d3c', - kind: 'server' as const - }, - - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '4362f8dc-731f-4ad8-a694-be5a88922a24' - }, - } -}; - -export default define(meta, async (ps, me) => { - let user; - - const isAdminOrModerator = me && (me.isAdmin || me.isModerator); - - if (ps.userIds) { - if (ps.userIds.length === 0) { - return []; - } - - const users = await Users.find(isAdminOrModerator ? { - id: In(ps.userIds) - } : { - id: In(ps.userIds), - isSuspended: false - }); - - // リクエストされた通りに並べ替え - const _users: User[] = []; - for (const id of ps.userIds) { - _users.push(users.find(x => x.id === id)!); - } - - return await Promise.all(_users.map(u => Users.pack(u, me, { - detail: true - }))); - } else { - // Lookup user - if (typeof ps.host === 'string' && typeof ps.username === 'string') { - user = await resolveUser(ps.username, ps.host).catch(e => { - apiLogger.warn(`failed to resolve remote user: ${e}`); - throw new ApiError(meta.errors.failedToResolveRemoteUser); - }); - } else { - const q: any = ps.userId != null - ? { id: ps.userId } - : { usernameLower: ps.username!.toLowerCase(), host: null }; - - user = await Users.findOne(q); - } - - if (user == null || (!isAdminOrModerator && user.isSuspended)) { - throw new ApiError(meta.errors.noSuchUser); - } - - return await Users.pack(user, me, { - detail: true - }); - } -}); diff --git a/src/server/api/endpoints/users/stats.ts b/src/server/api/endpoints/users/stats.ts deleted file mode 100644 index ef8afd5625..0000000000 --- a/src/server/api/endpoints/users/stats.ts +++ /dev/null @@ -1,144 +0,0 @@ -import $ from 'cafy'; -import define from '../../define'; -import { ApiError } from '../../error'; -import { ID } from '@/misc/cafy-id'; -import { DriveFiles, Followings, NoteFavorites, NoteReactions, Notes, PageLikes, PollVotes, ReversiGames, Users } from '@/models/index'; - -export const meta = { - tags: ['users'], - - requireCredential: false as const, - - params: { - userId: { - validator: $.type(ID), - }, - }, - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '9e638e45-3b25-4ef7-8f95-07e8498f1819' - }, - } -}; - -export default define(meta, async (ps, me) => { - const user = await Users.findOne(ps.userId); - if (user == null) { - throw new ApiError(meta.errors.noSuchUser); - } - - const [ - notesCount, - repliesCount, - renotesCount, - repliedCount, - renotedCount, - pollVotesCount, - pollVotedCount, - localFollowingCount, - remoteFollowingCount, - localFollowersCount, - remoteFollowersCount, - sentReactionsCount, - receivedReactionsCount, - noteFavoritesCount, - pageLikesCount, - pageLikedCount, - driveFilesCount, - driveUsage, - reversiCount, - ] = await Promise.all([ - Notes.createQueryBuilder('note') - .where('note.userId = :userId', { userId: user.id }) - .getCount(), - Notes.createQueryBuilder('note') - .where('note.userId = :userId', { userId: user.id }) - .andWhere('note.replyId IS NOT NULL') - .getCount(), - Notes.createQueryBuilder('note') - .where('note.userId = :userId', { userId: user.id }) - .andWhere('note.renoteId IS NOT NULL') - .getCount(), - Notes.createQueryBuilder('note') - .where('note.replyUserId = :userId', { userId: user.id }) - .getCount(), - Notes.createQueryBuilder('note') - .where('note.renoteUserId = :userId', { userId: user.id }) - .getCount(), - PollVotes.createQueryBuilder('vote') - .where('vote.userId = :userId', { userId: user.id }) - .getCount(), - PollVotes.createQueryBuilder('vote') - .innerJoin('vote.note', 'note') - .where('note.userId = :userId', { userId: user.id }) - .getCount(), - Followings.createQueryBuilder('following') - .where('following.followerId = :userId', { userId: user.id }) - .andWhere('following.followeeHost IS NULL') - .getCount(), - Followings.createQueryBuilder('following') - .where('following.followerId = :userId', { userId: user.id }) - .andWhere('following.followeeHost IS NOT NULL') - .getCount(), - Followings.createQueryBuilder('following') - .where('following.followeeId = :userId', { userId: user.id }) - .andWhere('following.followerHost IS NULL') - .getCount(), - Followings.createQueryBuilder('following') - .where('following.followeeId = :userId', { userId: user.id }) - .andWhere('following.followerHost IS NOT NULL') - .getCount(), - NoteReactions.createQueryBuilder('reaction') - .where('reaction.userId = :userId', { userId: user.id }) - .getCount(), - NoteReactions.createQueryBuilder('reaction') - .innerJoin('reaction.note', 'note') - .where('note.userId = :userId', { userId: user.id }) - .getCount(), - NoteFavorites.createQueryBuilder('favorite') - .where('favorite.userId = :userId', { userId: user.id }) - .getCount(), - PageLikes.createQueryBuilder('like') - .where('like.userId = :userId', { userId: user.id }) - .getCount(), - PageLikes.createQueryBuilder('like') - .innerJoin('like.page', 'page') - .where('page.userId = :userId', { userId: user.id }) - .getCount(), - DriveFiles.createQueryBuilder('file') - .where('file.userId = :userId', { userId: user.id }) - .getCount(), - DriveFiles.calcDriveUsageOf(user), - ReversiGames.createQueryBuilder('game') - .where('game.user1Id = :userId', { userId: user.id }) - .orWhere('game.user2Id = :userId', { userId: user.id }) - .getCount(), - ]); - - return { - notesCount, - repliesCount, - renotesCount, - repliedCount, - renotedCount, - pollVotesCount, - pollVotedCount, - localFollowingCount, - remoteFollowingCount, - localFollowersCount, - remoteFollowersCount, - followingCount: localFollowingCount + remoteFollowingCount, - followersCount: localFollowersCount + remoteFollowersCount, - sentReactionsCount, - receivedReactionsCount, - noteFavoritesCount, - pageLikesCount, - pageLikedCount, - driveFilesCount, - driveUsage, - reversiCount, - }; -}); diff --git a/src/server/api/error.ts b/src/server/api/error.ts deleted file mode 100644 index cb0bdc9f47..0000000000 --- a/src/server/api/error.ts +++ /dev/null @@ -1,28 +0,0 @@ -type E = { message: string, code: string, id: string, kind?: 'client' | 'server', httpStatusCode?: number }; - -export class ApiError extends Error { - public message: string; - public code: string; - public id: string; - public kind: string; - public httpStatusCode?: number; - public info?: any; - - constructor(e?: E | null | undefined, info?: any | null | undefined) { - if (e == null) e = { - message: 'Internal error occurred. Please contact us if the error persists.', - code: 'INTERNAL_ERROR', - id: '5d37dbcb-891e-41ca-a3d6-e690c97775ac', - kind: 'server', - httpStatusCode: 500 - }; - - super(e.message); - this.message = e.message; - this.code = e.code; - this.id = e.id; - this.kind = e.kind || 'client'; - this.httpStatusCode = e.httpStatusCode; - this.info = info; - } -} diff --git a/src/server/api/index.ts b/src/server/api/index.ts deleted file mode 100644 index 82579075eb..0000000000 --- a/src/server/api/index.ts +++ /dev/null @@ -1,113 +0,0 @@ -/** - * API Server - */ - -import * as Koa from 'koa'; -import * as Router from '@koa/router'; -import * as multer from '@koa/multer'; -import * as bodyParser from 'koa-bodyparser'; -import * as cors from '@koa/cors'; - -import endpoints from './endpoints'; -import handler from './api-handler'; -import signup from './private/signup'; -import signin from './private/signin'; -import signupPending from './private/signup-pending'; -import discord from './service/discord'; -import github from './service/github'; -import twitter from './service/twitter'; -import { Instances, AccessTokens, Users } from '@/models/index'; -import config from '@/config'; - -// Init app -const app = new Koa(); - -app.use(cors({ - origin: '*' -})); - -// No caching -app.use(async (ctx, next) => { - ctx.set('Cache-Control', 'private, max-age=0, must-revalidate'); - await next(); -}); - -app.use(bodyParser({ - // リクエストが multipart/form-data でない限りはJSONだと見なす - detectJSON: ctx => !ctx.is('multipart/form-data') -})); - -// Init multer instance -const upload = multer({ - storage: multer.diskStorage({}), - limits: { - fileSize: config.maxFileSize || 262144000, - files: 1, - } -}); - -// Init router -const router = new Router(); - -/** - * Register endpoint handlers - */ -for (const endpoint of endpoints) { - if (endpoint.meta.requireFile) { - router.post(`/${endpoint.name}`, upload.single('file'), handler.bind(null, endpoint)); - } else { - if (endpoint.name.includes('-')) { - // 後方互換性のため - router.post(`/${endpoint.name.replace(/-/g, '_')}`, handler.bind(null, endpoint)); - } - router.post(`/${endpoint.name}`, handler.bind(null, endpoint)); - } -} - -router.post('/signup', signup); -router.post('/signin', signin); -router.post('/signup-pending', signupPending); - -router.use(discord.routes()); -router.use(github.routes()); -router.use(twitter.routes()); - -router.get('/v1/instance/peers', async ctx => { - const instances = await Instances.find({ - select: ['host'] - }); - - ctx.body = instances.map(instance => instance.host); -}); - -router.post('/miauth/:session/check', async ctx => { - const token = await AccessTokens.findOne({ - session: ctx.params.session - }); - - if (token && token.session != null && !token.fetched) { - AccessTokens.update(token.id, { - fetched: true - }); - - ctx.body = { - ok: true, - token: token.token, - user: await Users.pack(token.userId, null, { detail: true }) - }; - } else { - ctx.body = { - ok: false, - }; - } -}); - -// Return 404 for unknown API -router.all('(.*)', async ctx => { - ctx.status = 404; -}); - -// Register router -app.use(router.routes()); - -export default app; diff --git a/src/server/api/limiter.ts b/src/server/api/limiter.ts deleted file mode 100644 index e677aad0b6..0000000000 --- a/src/server/api/limiter.ts +++ /dev/null @@ -1,83 +0,0 @@ -import * as Limiter from 'ratelimiter'; -import { redisClient } from '../../db/redis'; -import { IEndpoint } from './endpoints'; -import { getAcct } from '@/misc/acct'; -import { User } from '@/models/entities/user'; -import Logger from '@/services/logger'; - -const logger = new Logger('limiter'); - -export default (endpoint: IEndpoint, user: User) => new Promise<void>((ok, reject) => { - const limitation = endpoint.meta.limit!; - - const key = limitation.hasOwnProperty('key') - ? limitation.key - : endpoint.name; - - const hasShortTermLimit = - limitation.hasOwnProperty('minInterval'); - - const hasLongTermLimit = - limitation.hasOwnProperty('duration') && - limitation.hasOwnProperty('max'); - - if (hasShortTermLimit) { - min(); - } else if (hasLongTermLimit) { - max(); - } else { - ok(); - } - - // Short-term limit - function min() { - const minIntervalLimiter = new Limiter({ - id: `${user.id}:${key}:min`, - duration: limitation.minInterval, - max: 1, - db: redisClient - }); - - minIntervalLimiter.get((err, info) => { - if (err) { - return reject('ERR'); - } - - logger.debug(`@${getAcct(user)} ${endpoint.name} min remaining: ${info.remaining}`); - - if (info.remaining === 0) { - reject('BRIEF_REQUEST_INTERVAL'); - } else { - if (hasLongTermLimit) { - max(); - } else { - ok(); - } - } - }); - } - - // Long term limit - function max() { - const limiter = new Limiter({ - id: `${user.id}:${key}`, - duration: limitation.duration, - max: limitation.max, - db: redisClient - }); - - limiter.get((err, info) => { - if (err) { - return reject('ERR'); - } - - logger.debug(`@${getAcct(user)} ${endpoint.name} max remaining: ${info.remaining}`); - - if (info.remaining === 0) { - reject('RATE_LIMIT_EXCEEDED'); - } else { - ok(); - } - }); - } -}); diff --git a/src/server/api/logger.ts b/src/server/api/logger.ts deleted file mode 100644 index 750defe547..0000000000 --- a/src/server/api/logger.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Logger from '@/services/logger'; - -export const apiLogger = new Logger('api'); diff --git a/src/server/api/openapi/description.ts b/src/server/api/openapi/description.ts deleted file mode 100644 index e51b312259..0000000000 --- a/src/server/api/openapi/description.ts +++ /dev/null @@ -1,51 +0,0 @@ -import endpoints from '../endpoints'; -import * as locale from '../../../../locales/index'; -import { kinds as kindsList } from '@/misc/api-permissions'; - -export interface IKindInfo { - endpoints: string[]; - descs: { [x: string]: string; }; -} - -export function kinds() { - const kinds = Object.fromEntries( - kindsList - .map(k => [k, { - endpoints: [], - descs: Object.fromEntries( - Object.keys(locale) - .map(l => [l, locale[l]._permissions[k] as string]) - ) - } as IKindInfo]) - ); - - const errors = [] as string[][]; - - for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) { - if (endpoint.meta.kind) { - const kind = endpoint.meta.kind; - if (kind in kinds) kinds[kind].endpoints.push(endpoint.name); - else errors.push([kind, endpoint.name]); - } - } - - if (errors.length > 0) throw Error('\n ' + errors.map((e) => `Unknown kind (permission) "${e[0]}" found at ${e[1]}.`).join('\n ')); - - return kinds; -} - -export function getDescription(lang = 'ja-JP'): string { - const permissionTable = Object.entries(kinds()) - .map(e => `|${e[0]}|${e[1].descs[lang]}|${e[1].endpoints.map(f => `[${f}](#operation/${f})`).join(', ')}|`) - .join('\n'); - - const descriptions: { [x: string]: string } = { - 'ja-JP': ` -# Permissions -|Permisson (kind)|Description|Endpoints| -|:--|:--|:--| -${permissionTable} -` - }; - return lang in descriptions ? descriptions[lang] : descriptions['ja-JP']; -} diff --git a/src/server/api/openapi/errors.ts b/src/server/api/openapi/errors.ts deleted file mode 100644 index 43bcc323ba..0000000000 --- a/src/server/api/openapi/errors.ts +++ /dev/null @@ -1,69 +0,0 @@ - -export const errors = { - '400': { - 'INVALID_PARAM': { - value: { - error: { - message: 'Invalid param.', - code: 'INVALID_PARAM', - id: '3d81ceae-475f-4600-b2a8-2bc116157532', - } - } - } - }, - '401': { - 'CREDENTIAL_REQUIRED': { - value: { - error: { - message: 'Credential required.', - code: 'CREDENTIAL_REQUIRED', - id: '1384574d-a912-4b81-8601-c7b1c4085df1', - } - } - } - }, - '403': { - 'AUTHENTICATION_FAILED': { - value: { - error: { - message: 'Authentication failed. Please ensure your token is correct.', - code: 'AUTHENTICATION_FAILED', - id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', - } - } - } - }, - '418': { - 'I_AM_AI': { - value: { - error: { - message: 'You sent a request to Ai-chan, Misskey\'s showgirl, instead of the server.', - code: 'I_AM_AI', - id: '60c46cd1-f23a-46b1-bebe-5d2b73951a84', - } - } - } - }, - '429': { - 'RATE_LIMIT_EXCEEDED': { - value: { - error: { - message: 'Rate limit exceeded. Please try again later.', - code: 'RATE_LIMIT_EXCEEDED', - id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', - } - } - } - }, - '500': { - 'INTERNAL_ERROR': { - value: { - error: { - message: 'Internal error occurred. Please contact us if the error persists.', - code: 'INTERNAL_ERROR', - id: '5d37dbcb-891e-41ca-a3d6-e690c97775ac', - } - } - } - } -}; diff --git a/src/server/api/openapi/gen-spec.ts b/src/server/api/openapi/gen-spec.ts deleted file mode 100644 index 9db47c6dfc..0000000000 --- a/src/server/api/openapi/gen-spec.ts +++ /dev/null @@ -1,237 +0,0 @@ -import endpoints from '../endpoints'; -import { Context } from 'cafy'; -import config from '@/config/index'; -import { errors as basicErrors } from './errors'; -import { schemas, convertSchemaToOpenApiSchema } from './schemas'; -import { getDescription } from './description'; - -export function genOpenapiSpec(lang = 'ja-JP') { - const spec = { - openapi: '3.0.0', - - info: { - version: 'v1', - title: 'Misskey API', - description: getDescription(lang), - 'x-logo': { url: '/static-assets/api-doc.png' } - }, - - externalDocs: { - description: 'Repository', - url: 'https://github.com/misskey-dev/misskey' - }, - - servers: [{ - url: config.apiUrl - }], - - paths: {} as any, - - components: { - schemas: schemas, - - securitySchemes: { - ApiKeyAuth: { - type: 'apiKey', - in: 'body', - name: 'i' - } - } - } - }; - - function genProps(props: { [key: string]: Context; }) { - const properties = {} as any; - - for (const [k, v] of Object.entries(props)) { - properties[k] = genProp(v); - } - - return properties; - } - - function genProp(param: Context): any { - const required = param.name === 'Object' ? (param as any).props ? Object.entries((param as any).props).filter(([k, v]: any) => !v.isOptional).map(([k, v]) => k) : [] : []; - return { - description: (param.data || {}).desc, - default: (param.data || {}).default, - deprecated: (param.data || {}).deprecated, - ...((param.data || {}).default ? { default: (param.data || {}).default } : {}), - type: param.name === 'ID' ? 'string' : param.name.toLowerCase(), - ...(param.name === 'ID' ? { example: 'xxxxxxxxxx', format: 'id' } : {}), - nullable: param.isNullable, - ...(param.name === 'String' ? { - ...((param as any).enum ? { enum: (param as any).enum } : {}), - ...((param as any).minLength ? { minLength: (param as any).minLength } : {}), - ...((param as any).maxLength ? { maxLength: (param as any).maxLength } : {}), - } : {}), - ...(param.name === 'Number' ? { - ...((param as any).minimum ? { minimum: (param as any).minimum } : {}), - ...((param as any).maximum ? { maximum: (param as any).maximum } : {}), - } : {}), - ...(param.name === 'Object' ? { - ...(required.length > 0 ? { required } : {}), - properties: (param as any).props ? genProps((param as any).props) : {} - } : {}), - ...(param.name === 'Array' ? { - items: (param as any).ctx ? genProp((param as any).ctx) : {} - } : {}) - }; - } - - for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) { - const porops = {} as any; - const errors = {} as any; - - if (endpoint.meta.errors) { - for (const e of Object.values(endpoint.meta.errors)) { - errors[e.code] = { - value: { - error: e - } - }; - } - } - - if (endpoint.meta.params) { - for (const [k, v] of Object.entries(endpoint.meta.params)) { - if (v.validator.data == null) v.validator.data = {}; - if (v.desc) v.validator.data.desc = v.desc[lang]; - if (v.deprecated) v.validator.data.deprecated = v.deprecated; - if (v.default) v.validator.data.default = v.default; - porops[k] = v.validator; - } - } - - const required = endpoint.meta.params ? Object.entries(endpoint.meta.params).filter(([k, v]) => !v.validator.isOptional).map(([k, v]) => k) : []; - - const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res) : {}; - - let desc = (endpoint.meta.desc ? endpoint.meta.desc[lang] : 'No description provided.') + '\n\n'; - desc += `**Credential required**: *${endpoint.meta.requireCredential ? 'Yes' : 'No'}*`; - if (endpoint.meta.kind) { - const kind = endpoint.meta.kind; - desc += ` / **Permission**: *${kind}*`; - } - - const info = { - operationId: endpoint.name, - summary: endpoint.name, - description: desc, - externalDocs: { - description: 'Source code', - url: `https://github.com/misskey-dev/misskey/blob/develop/src/server/api/endpoints/${endpoint.name}.ts` - }, - ...(endpoint.meta.tags ? { - tags: [endpoint.meta.tags[0]] - } : {}), - ...(endpoint.meta.requireCredential ? { - security: [{ - ApiKeyAuth: [] - }] - } : {}), - requestBody: { - required: true, - content: { - 'application/json': { - schema: { - type: 'object', - ...(required.length > 0 ? { required } : {}), - properties: endpoint.meta.params ? genProps(porops) : {} - } - } - } - }, - responses: { - ...(endpoint.meta.res ? { - '200': { - description: 'OK (with results)', - content: { - 'application/json': { - schema: resSchema - } - } - } - } : { - '204': { - description: 'OK (without any results)', - } - }), - '400': { - description: 'Client error', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error' - }, - examples: { ...errors, ...basicErrors['400'] } - } - } - }, - '401': { - description: 'Authentication error', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error' - }, - examples: basicErrors['401'] - } - } - }, - '403': { - description: 'Forbidden error', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error' - }, - examples: basicErrors['403'] - } - } - }, - '418': { - description: 'I\'m Ai', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error' - }, - examples: basicErrors['418'] - } - } - }, - ...(endpoint.meta.limit ? { - '429': { - description: 'To many requests', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error' - }, - examples: basicErrors['429'] - } - } - } - } : {}), - '500': { - description: 'Internal server error', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error' - }, - examples: basicErrors['500'] - } - } - }, - } - }; - - spec.paths['/' + endpoint.name] = { - post: info - }; - } - - return spec; -} diff --git a/src/server/api/openapi/schemas.ts b/src/server/api/openapi/schemas.ts deleted file mode 100644 index 12fc207c47..0000000000 --- a/src/server/api/openapi/schemas.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { refs, Schema } from '@/misc/schema'; - -export function convertSchemaToOpenApiSchema(schema: Schema) { - const res: any = schema; - - if (schema.type === 'object' && schema.properties) { - res.required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k); - - for (const k of Object.keys(schema.properties)) { - res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]); - } - } - - if (schema.type === 'array' && schema.items) { - res.items = convertSchemaToOpenApiSchema(schema.items); - } - - if (schema.ref) { - res.$ref = `#/components/schemas/${schema.ref}`; - } - - return res; -} - -export const schemas = { - Error: { - type: 'object', - properties: { - error: { - type: 'object', - description: 'An error object.', - properties: { - code: { - type: 'string', - description: 'An error code. Unique within the endpoint.', - }, - message: { - type: 'string', - description: 'An error message.', - }, - id: { - type: 'string', - format: 'uuid', - description: 'An error ID. This ID is static.', - } - }, - required: ['code', 'id', 'message'] - }, - }, - required: ['error'] - }, - - ...Object.fromEntries( - Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema)]) - ), -}; diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts deleted file mode 100644 index 83c3dfee94..0000000000 --- a/src/server/api/private/signin.ts +++ /dev/null @@ -1,232 +0,0 @@ -import * as Koa from 'koa'; -import * as bcrypt from 'bcryptjs'; -import * as speakeasy from 'speakeasy'; -import signin from '../common/signin'; -import config from '@/config/index'; -import { Users, Signins, UserProfiles, UserSecurityKeys, AttestationChallenges } from '@/models/index'; -import { ILocalUser } from '@/models/entities/user'; -import { genId } from '@/misc/gen-id'; -import { verifyLogin, hash } from '../2fa'; -import { randomBytes } from 'crypto'; - -export default async (ctx: Koa.Context) => { - ctx.set('Access-Control-Allow-Origin', config.url); - ctx.set('Access-Control-Allow-Credentials', 'true'); - - const body = ctx.request.body as any; - const username = body['username']; - const password = body['password']; - const token = body['token']; - - function error(status: number, error: { id: string }) { - ctx.status = status; - ctx.body = { error }; - } - - if (typeof username != 'string') { - ctx.status = 400; - return; - } - - if (typeof password != 'string') { - ctx.status = 400; - return; - } - - if (token != null && typeof token != 'string') { - ctx.status = 400; - return; - } - - // Fetch user - const user = await Users.findOne({ - usernameLower: username.toLowerCase(), - host: null - }) as ILocalUser; - - if (user == null) { - error(404, { - id: '6cc579cc-885d-43d8-95c2-b8c7fc963280', - }); - return; - } - - if (user.isSuspended) { - error(403, { - id: 'e03a5f46-d309-4865-9b69-56282d94e1eb', - }); - return; - } - - const profile = await UserProfiles.findOneOrFail(user.id); - - // Compare password - const same = await bcrypt.compare(password, profile.password!); - - async function fail(status?: number, failure?: { id: string }) { - // Append signin history - await Signins.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - ip: ctx.ip, - headers: ctx.headers, - success: false - }); - - error(status || 500, failure || { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' }); - } - - if (!profile.twoFactorEnabled) { - if (same) { - signin(ctx, user); - return; - } else { - await fail(403, { - id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c' - }); - return; - } - } - - if (token) { - if (!same) { - await fail(403, { - id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c' - }); - return; - } - - const verified = (speakeasy as any).totp.verify({ - secret: profile.twoFactorSecret, - encoding: 'base32', - token: token, - window: 2 - }); - - if (verified) { - signin(ctx, user); - return; - } else { - await fail(403, { - id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f' - }); - return; - } - } else if (body.credentialId) { - if (!same && !profile.usePasswordLessLogin) { - await fail(403, { - id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c' - }); - return; - } - - const clientDataJSON = Buffer.from(body.clientDataJSON, 'hex'); - const clientData = JSON.parse(clientDataJSON.toString('utf-8')); - const challenge = await AttestationChallenges.findOne({ - userId: user.id, - id: body.challengeId, - registrationChallenge: false, - challenge: hash(clientData.challenge).toString('hex') - }); - - if (!challenge) { - await fail(403, { - id: '2715a88a-2125-4013-932f-aa6fe72792da' - }); - return; - } - - await AttestationChallenges.delete({ - userId: user.id, - id: body.challengeId - }); - - if (new Date().getTime() - challenge.createdAt.getTime() >= 5 * 60 * 1000) { - await fail(403, { - id: '2715a88a-2125-4013-932f-aa6fe72792da' - }); - return; - } - - const securityKey = await UserSecurityKeys.findOne({ - id: Buffer.from( - body.credentialId - .replace(/-/g, '+') - .replace(/_/g, '/'), - 'base64' - ).toString('hex') - }); - - if (!securityKey) { - await fail(403, { - id: '66269679-aeaf-4474-862b-eb761197e046' - }); - return; - } - - const isValid = verifyLogin({ - publicKey: Buffer.from(securityKey.publicKey, 'hex'), - authenticatorData: Buffer.from(body.authenticatorData, 'hex'), - clientDataJSON, - clientData, - signature: Buffer.from(body.signature, 'hex'), - challenge: challenge.challenge - }); - - if (isValid) { - signin(ctx, user); - return; - } else { - await fail(403, { - id: '93b86c4b-72f9-40eb-9815-798928603d1e' - }); - return; - } - } else { - if (!same && !profile.usePasswordLessLogin) { - await fail(403, { - id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c' - }); - return; - } - - const keys = await UserSecurityKeys.find({ - userId: user.id - }); - - if (keys.length === 0) { - await fail(403, { - id: 'f27fd449-9af4-4841-9249-1f989b9fa4a4' - }); - return; - } - - // 32 byte challenge - const challenge = randomBytes(32).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: false - }); - - ctx.body = { - challenge, - challengeId, - securityKeys: keys.map(key => ({ - id: key.id - })) - }; - ctx.status = 200; - return; - } - // never get here -}; diff --git a/src/server/api/private/signup-pending.ts b/src/server/api/private/signup-pending.ts deleted file mode 100644 index c0638a1cda..0000000000 --- a/src/server/api/private/signup-pending.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as Koa from 'koa'; -import { Users, UserPendings, UserProfiles } from '@/models/index'; -import { signup } from '../common/signup'; -import signin from '../common/signin'; - -export default async (ctx: Koa.Context) => { - const body = ctx.request.body; - - const code = body['code']; - - try { - const pendingUser = await UserPendings.findOneOrFail({ code }); - - const { account, secret } = await signup({ - username: pendingUser.username, - passwordHash: pendingUser.password, - }); - - UserPendings.delete({ - id: pendingUser.id, - }); - - const profile = await UserProfiles.findOneOrFail(account.id); - - await UserProfiles.update({ userId: profile.userId }, { - email: pendingUser.email, - emailVerified: true, - emailVerifyCode: null, - }); - - signin(ctx, account); - } catch (e) { - ctx.throw(400, e); - } -}; diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts deleted file mode 100644 index 2b6a3eb00c..0000000000 --- a/src/server/api/private/signup.ts +++ /dev/null @@ -1,112 +0,0 @@ -import * as Koa from 'koa'; -import rndstr from 'rndstr'; -import * as bcrypt from 'bcryptjs'; -import { fetchMeta } from '@/misc/fetch-meta'; -import { verifyHcaptcha, verifyRecaptcha } from '@/misc/captcha'; -import { Users, RegistrationTickets, UserPendings } from '@/models/index'; -import { signup } from '../common/signup'; -import config from '@/config'; -import { sendEmail } from '@/services/send-email'; -import { genId } from '@/misc/gen-id'; -import { validateEmailForAccount } from '@/services/validate-email-for-account'; - -export default async (ctx: Koa.Context) => { - const body = ctx.request.body; - - const instance = await fetchMeta(true); - - // Verify *Captcha - // ただしテスト時はこの機構は障害となるため無効にする - if (process.env.NODE_ENV !== 'test') { - if (instance.enableHcaptcha && instance.hcaptchaSecretKey) { - await verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(e => { - ctx.throw(400, e); - }); - } - - if (instance.enableRecaptcha && instance.recaptchaSecretKey) { - await verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(e => { - ctx.throw(400, e); - }); - } - } - - const username = body['username']; - const password = body['password']; - const host: string | null = process.env.NODE_ENV === 'test' ? (body['host'] || null) : null; - const invitationCode = body['invitationCode']; - const emailAddress = body['emailAddress']; - - if (instance.emailRequiredForSignup) { - if (emailAddress == null || typeof emailAddress != 'string') { - ctx.status = 400; - return; - } - - const available = await validateEmailForAccount(emailAddress); - if (!available) { - ctx.status = 400; - return; - } - } - - if (instance.disableRegistration) { - if (invitationCode == null || typeof invitationCode != 'string') { - ctx.status = 400; - return; - } - - const ticket = await RegistrationTickets.findOne({ - code: invitationCode - }); - - if (ticket == null) { - ctx.status = 400; - return; - } - - RegistrationTickets.delete(ticket.id); - } - - if (instance.emailRequiredForSignup) { - const code = rndstr('a-z0-9', 16); - - // Generate hash of password - const salt = await bcrypt.genSalt(8); - const hash = await bcrypt.hash(password, salt); - - await UserPendings.insert({ - id: genId(), - createdAt: new Date(), - code, - email: emailAddress, - username: username, - password: hash, - }); - - const link = `${config.url}/signup-complete/${code}`; - - sendEmail(emailAddress, 'Signup', - `To complete signup, please click this link:<br><a href="${link}">${link}</a>`, - `To complete signup, please click this link: ${link}`); - - ctx.status = 204; - } else { - try { - const { account, secret } = await signup({ - username, password, host - }); - - const res = await Users.pack(account, account, { - detail: true, - includeSecrets: true - }); - - (res as any).token = secret; - - ctx.body = res; - } catch (e) { - ctx.throw(400, e); - } - } -}; diff --git a/src/server/api/service/discord.ts b/src/server/api/service/discord.ts deleted file mode 100644 index dd52a23376..0000000000 --- a/src/server/api/service/discord.ts +++ /dev/null @@ -1,286 +0,0 @@ -import * as Koa from 'koa'; -import * as Router from '@koa/router'; -import { getJson } from '@/misc/fetch'; -import { OAuth2 } from 'oauth'; -import config from '@/config/index'; -import { publishMainStream } from '@/services/stream'; -import { redisClient } from '../../../db/redis'; -import { v4 as uuid } from 'uuid'; -import signin from '../common/signin'; -import { fetchMeta } from '@/misc/fetch-meta'; -import { Users, UserProfiles } from '@/models/index'; -import { ILocalUser } from '@/models/entities/user'; - -function getUserToken(ctx: Koa.Context) { - return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1]; -} - -function compareOrigin(ctx: Koa.Context) { - function normalizeUrl(url: string) { - return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : ''; - } - - const referer = ctx.headers['referer']; - - return (normalizeUrl(referer) == normalizeUrl(config.url)); -} - -// Init router -const router = new Router(); - -router.get('/disconnect/discord', async ctx => { - if (!compareOrigin(ctx)) { - ctx.throw(400, 'invalid origin'); - return; - } - - const userToken = getUserToken(ctx); - if (!userToken) { - ctx.throw(400, 'signin required'); - return; - } - - const user = await Users.findOneOrFail({ - host: null, - token: userToken - }); - - const profile = await UserProfiles.findOneOrFail(user.id); - - delete profile.integrations.discord; - - await UserProfiles.update(user.id, { - integrations: profile.integrations, - }); - - ctx.body = `Discordの連携を解除しました :v:`; - - // Publish i updated event - publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { - detail: true, - includeSecrets: true - })); -}); - -async function getOAuth2() { - const meta = await fetchMeta(true); - - if (meta.enableDiscordIntegration) { - return new OAuth2( - meta.discordClientId!, - meta.discordClientSecret!, - 'https://discord.com/', - 'api/oauth2/authorize', - 'api/oauth2/token'); - } else { - return null; - } -} - -router.get('/connect/discord', async ctx => { - if (!compareOrigin(ctx)) { - ctx.throw(400, 'invalid origin'); - return; - } - - const userToken = getUserToken(ctx); - if (!userToken) { - ctx.throw(400, 'signin required'); - return; - } - - const params = { - redirect_uri: `${config.url}/api/dc/cb`, - scope: ['identify'], - state: uuid(), - response_type: 'code' - }; - - redisClient.set(userToken, JSON.stringify(params)); - - const oauth2 = await getOAuth2(); - ctx.redirect(oauth2!.getAuthorizeUrl(params)); -}); - -router.get('/signin/discord', async ctx => { - const sessid = uuid(); - - const params = { - redirect_uri: `${config.url}/api/dc/cb`, - scope: ['identify'], - state: uuid(), - response_type: 'code' - }; - - ctx.cookies.set('signin_with_discord_sid', sessid, { - path: '/', - secure: config.url.startsWith('https'), - httpOnly: true - }); - - redisClient.set(sessid, JSON.stringify(params)); - - const oauth2 = await getOAuth2(); - ctx.redirect(oauth2!.getAuthorizeUrl(params)); -}); - -router.get('/dc/cb', async ctx => { - const userToken = getUserToken(ctx); - - const oauth2 = await getOAuth2(); - - if (!userToken) { - const sessid = ctx.cookies.get('signin_with_discord_sid'); - - if (!sessid) { - ctx.throw(400, 'invalid session'); - return; - } - - const code = ctx.query.code; - - if (!code) { - ctx.throw(400, 'invalid session'); - return; - } - - const { redirect_uri, state } = await new Promise<any>((res, rej) => { - redisClient.get(sessid, async (_, state) => { - res(JSON.parse(state)); - }); - }); - - if (ctx.query.state !== state) { - ctx.throw(400, 'invalid session'); - return; - } - - const { accessToken, refreshToken, expiresDate } = await new Promise<any>((res, rej) => - oauth2!.getOAuthAccessToken(code, { - grant_type: 'authorization_code', - redirect_uri - }, (err, accessToken, refreshToken, result) => { - if (err) { - rej(err); - } else if (result.error) { - rej(result.error); - } else { - res({ - accessToken, - refreshToken, - expiresDate: Date.now() + Number(result.expires_in) * 1000 - }); - } - })); - - const { id, username, discriminator } = await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, { - 'Authorization': `Bearer ${accessToken}`, - }); - - if (!id || !username || !discriminator) { - ctx.throw(400, 'invalid session'); - return; - } - - const profile = await UserProfiles.createQueryBuilder() - .where(`"integrations"->'discord'->>'id' = :id`, { id: id }) - .andWhere('"userHost" IS NULL') - .getOne(); - - if (profile == null) { - ctx.throw(404, `@${username}#${discriminator}と連携しているMisskeyアカウントはありませんでした...`); - return; - } - - await UserProfiles.update(profile.userId, { - integrations: { - ...profile.integrations, - discord: { - id: id, - accessToken: accessToken, - refreshToken: refreshToken, - expiresDate: expiresDate, - username: username, - discriminator: discriminator - } - }, - }); - - signin(ctx, await Users.findOne(profile.userId) as ILocalUser, true); - } else { - const code = ctx.query.code; - - if (!code) { - ctx.throw(400, 'invalid session'); - return; - } - - const { redirect_uri, state } = await new Promise<any>((res, rej) => { - redisClient.get(userToken, async (_, state) => { - res(JSON.parse(state)); - }); - }); - - if (ctx.query.state !== state) { - ctx.throw(400, 'invalid session'); - return; - } - - const { accessToken, refreshToken, expiresDate } = await new Promise<any>((res, rej) => - oauth2!.getOAuthAccessToken(code, { - grant_type: 'authorization_code', - redirect_uri - }, (err, accessToken, refreshToken, result) => { - if (err) { - rej(err); - } else if (result.error) { - rej(result.error); - } else { - res({ - accessToken, - refreshToken, - expiresDate: Date.now() + Number(result.expires_in) * 1000 - }); - } - })); - - const { id, username, discriminator } = await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, { - 'Authorization': `Bearer ${accessToken}`, - }); - if (!id || !username || !discriminator) { - ctx.throw(400, 'invalid session'); - return; - } - - const user = await Users.findOneOrFail({ - host: null, - token: userToken - }); - - const profile = await UserProfiles.findOneOrFail(user.id); - - await UserProfiles.update(user.id, { - integrations: { - ...profile.integrations, - discord: { - accessToken: accessToken, - refreshToken: refreshToken, - expiresDate: expiresDate, - id: id, - username: username, - discriminator: discriminator - } - } - }); - - ctx.body = `Discord: @${username}#${discriminator} を、Misskey: @${user.username} に接続しました!`; - - // Publish i updated event - publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { - detail: true, - includeSecrets: true - })); - } -}); - -export default router; diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts deleted file mode 100644 index 0616f3f773..0000000000 --- a/src/server/api/service/github.ts +++ /dev/null @@ -1,257 +0,0 @@ -import * as Koa from 'koa'; -import * as Router from '@koa/router'; -import { getJson } from '@/misc/fetch'; -import { OAuth2 } from 'oauth'; -import config from '@/config/index'; -import { publishMainStream } from '@/services/stream'; -import { redisClient } from '../../../db/redis'; -import { v4 as uuid } from 'uuid'; -import signin from '../common/signin'; -import { fetchMeta } from '@/misc/fetch-meta'; -import { Users, UserProfiles } from '@/models/index'; -import { ILocalUser } from '@/models/entities/user'; - -function getUserToken(ctx: Koa.Context) { - return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1]; -} - -function compareOrigin(ctx: Koa.Context) { - function normalizeUrl(url: string) { - return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : ''; - } - - const referer = ctx.headers['referer']; - - return (normalizeUrl(referer) == normalizeUrl(config.url)); -} - -// Init router -const router = new Router(); - -router.get('/disconnect/github', async ctx => { - if (!compareOrigin(ctx)) { - ctx.throw(400, 'invalid origin'); - return; - } - - const userToken = getUserToken(ctx); - if (!userToken) { - ctx.throw(400, 'signin required'); - return; - } - - const user = await Users.findOneOrFail({ - host: null, - token: userToken - }); - - const profile = await UserProfiles.findOneOrFail(user.id); - - delete profile.integrations.github; - - await UserProfiles.update(user.id, { - integrations: profile.integrations, - }); - - ctx.body = `GitHubの連携を解除しました :v:`; - - // Publish i updated event - publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { - detail: true, - includeSecrets: true - })); -}); - -async function getOath2() { - const meta = await fetchMeta(true); - - if (meta.enableGithubIntegration && meta.githubClientId && meta.githubClientSecret) { - return new OAuth2( - meta.githubClientId, - meta.githubClientSecret, - 'https://github.com/', - 'login/oauth/authorize', - 'login/oauth/access_token'); - } else { - return null; - } -} - -router.get('/connect/github', async ctx => { - if (!compareOrigin(ctx)) { - ctx.throw(400, 'invalid origin'); - return; - } - - const userToken = getUserToken(ctx); - if (!userToken) { - ctx.throw(400, 'signin required'); - return; - } - - const params = { - redirect_uri: `${config.url}/api/gh/cb`, - scope: ['read:user'], - state: uuid() - }; - - redisClient.set(userToken, JSON.stringify(params)); - - const oauth2 = await getOath2(); - ctx.redirect(oauth2!.getAuthorizeUrl(params)); -}); - -router.get('/signin/github', async ctx => { - const sessid = uuid(); - - const params = { - redirect_uri: `${config.url}/api/gh/cb`, - scope: ['read:user'], - state: uuid() - }; - - ctx.cookies.set('signin_with_github_sid', sessid, { - path: '/', - secure: config.url.startsWith('https'), - httpOnly: true - }); - - redisClient.set(sessid, JSON.stringify(params)); - - const oauth2 = await getOath2(); - ctx.redirect(oauth2!.getAuthorizeUrl(params)); -}); - -router.get('/gh/cb', async ctx => { - const userToken = getUserToken(ctx); - - const oauth2 = await getOath2(); - - if (!userToken) { - const sessid = ctx.cookies.get('signin_with_github_sid'); - - if (!sessid) { - ctx.throw(400, 'invalid session'); - return; - } - - const code = ctx.query.code; - - if (!code) { - ctx.throw(400, 'invalid session'); - return; - } - - const { redirect_uri, state } = await new Promise<any>((res, rej) => { - redisClient.get(sessid, async (_, state) => { - res(JSON.parse(state)); - }); - }); - - if (ctx.query.state !== state) { - ctx.throw(400, 'invalid session'); - return; - } - - const { accessToken } = await new Promise<any>((res, rej) => - oauth2!.getOAuthAccessToken(code, { - redirect_uri - }, (err, accessToken, refresh, result) => { - if (err) { - rej(err); - } else if (result.error) { - rej(result.error); - } else { - res({ accessToken }); - } - })); - - const { login, id } = await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, { - 'Authorization': `bearer ${accessToken}` - }); - if (!login || !id) { - ctx.throw(400, 'invalid session'); - return; - } - - const link = await UserProfiles.createQueryBuilder() - .where(`"integrations"->'github'->>'id' = :id`, { id: id }) - .andWhere('"userHost" IS NULL') - .getOne(); - - if (link == null) { - ctx.throw(404, `@${login}と連携しているMisskeyアカウントはありませんでした...`); - return; - } - - signin(ctx, await Users.findOne(link.userId) as ILocalUser, true); - } else { - const code = ctx.query.code; - - if (!code) { - ctx.throw(400, 'invalid session'); - return; - } - - const { redirect_uri, state } = await new Promise<any>((res, rej) => { - redisClient.get(userToken, async (_, state) => { - res(JSON.parse(state)); - }); - }); - - if (ctx.query.state !== state) { - ctx.throw(400, 'invalid session'); - return; - } - - const { accessToken } = await new Promise<any>((res, rej) => - oauth2!.getOAuthAccessToken( - code, - { redirect_uri }, - (err, accessToken, refresh, result) => { - if (err) - rej(err); - else if (result.error) - rej(result.error); - else - res({ accessToken }); - })); - - const { login, id } = await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, { - 'Authorization': `bearer ${accessToken}` - }); - - if (!login || !id) { - ctx.throw(400, 'invalid session'); - return; - } - - const user = await Users.findOneOrFail({ - host: null, - token: userToken - }); - - const profile = await UserProfiles.findOneOrFail(user.id); - - await UserProfiles.update(user.id, { - integrations: { - ...profile.integrations, - github: { - accessToken: accessToken, - id: id, - login: login, - } - } - }); - - ctx.body = `GitHub: @${login} を、Misskey: @${user.username} に接続しました!`; - - // Publish i updated event - publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { - detail: true, - includeSecrets: true - })); - } -}); - -export default router; diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts deleted file mode 100644 index 8a6a58aeee..0000000000 --- a/src/server/api/service/twitter.ts +++ /dev/null @@ -1,194 +0,0 @@ -import * as Koa from 'koa'; -import * as Router from '@koa/router'; -import { v4 as uuid } from 'uuid'; -import autwh from 'autwh'; -import { redisClient } from '../../../db/redis'; -import { publishMainStream } from '@/services/stream'; -import config from '@/config/index'; -import signin from '../common/signin'; -import { fetchMeta } from '@/misc/fetch-meta'; -import { Users, UserProfiles } from '@/models/index'; -import { ILocalUser } from '@/models/entities/user'; - -function getUserToken(ctx: Koa.Context) { - return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1]; -} - -function compareOrigin(ctx: Koa.Context) { - function normalizeUrl(url: string) { - return url.endsWith('/') ? url.substr(0, url.length - 1) : url; - } - - const referer = ctx.headers['referer']; - - return (normalizeUrl(referer) == normalizeUrl(config.url)); -} - -// Init router -const router = new Router(); - -router.get('/disconnect/twitter', async ctx => { - if (!compareOrigin(ctx)) { - ctx.throw(400, 'invalid origin'); - return; - } - - const userToken = getUserToken(ctx); - if (userToken == null) { - ctx.throw(400, 'signin required'); - return; - } - - const user = await Users.findOneOrFail({ - host: null, - token: userToken - }); - - const profile = await UserProfiles.findOneOrFail(user.id); - - delete profile.integrations.twitter; - - await UserProfiles.update(user.id, { - integrations: profile.integrations, - }); - - ctx.body = `Twitterの連携を解除しました :v:`; - - // Publish i updated event - publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { - detail: true, - includeSecrets: true - })); -}); - -async function getTwAuth() { - const meta = await fetchMeta(true); - - if (meta.enableTwitterIntegration && meta.twitterConsumerKey && meta.twitterConsumerSecret) { - return autwh({ - consumerKey: meta.twitterConsumerKey, - consumerSecret: meta.twitterConsumerSecret, - callbackUrl: `${config.url}/api/tw/cb` - }); - } else { - return null; - } -} - -router.get('/connect/twitter', async ctx => { - if (!compareOrigin(ctx)) { - ctx.throw(400, 'invalid origin'); - return; - } - - const userToken = getUserToken(ctx); - if (userToken == null) { - ctx.throw(400, 'signin required'); - return; - } - - const twAuth = await getTwAuth(); - const twCtx = await twAuth!.begin(); - redisClient.set(userToken, JSON.stringify(twCtx)); - ctx.redirect(twCtx.url); -}); - -router.get('/signin/twitter', async ctx => { - const twAuth = await getTwAuth(); - const twCtx = await twAuth!.begin(); - - const sessid = uuid(); - - redisClient.set(sessid, JSON.stringify(twCtx)); - - ctx.cookies.set('signin_with_twitter_sid', sessid, { - path: '/', - secure: config.url.startsWith('https'), - httpOnly: true - }); - - ctx.redirect(twCtx.url); -}); - -router.get('/tw/cb', async ctx => { - const userToken = getUserToken(ctx); - - const twAuth = await getTwAuth(); - - if (userToken == null) { - const sessid = ctx.cookies.get('signin_with_twitter_sid'); - - if (sessid == null) { - ctx.throw(400, 'invalid session'); - return; - } - - const get = new Promise<any>((res, rej) => { - redisClient.get(sessid, async (_, twCtx) => { - res(twCtx); - }); - }); - - const twCtx = await get; - - const result = await twAuth!.done(JSON.parse(twCtx), ctx.query.oauth_verifier); - - const link = await UserProfiles.createQueryBuilder() - .where(`"integrations"->'twitter'->>'userId' = :id`, { id: result.userId }) - .andWhere('"userHost" IS NULL') - .getOne(); - - if (link == null) { - ctx.throw(404, `@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`); - return; - } - - signin(ctx, await Users.findOne(link.userId) as ILocalUser, true); - } else { - const verifier = ctx.query.oauth_verifier; - - if (verifier == null) { - ctx.throw(400, 'invalid session'); - return; - } - - const get = new Promise<any>((res, rej) => { - redisClient.get(userToken, async (_, twCtx) => { - res(twCtx); - }); - }); - - const twCtx = await get; - - const result = await twAuth!.done(JSON.parse(twCtx), verifier); - - const user = await Users.findOneOrFail({ - host: null, - token: userToken - }); - - const profile = await UserProfiles.findOneOrFail(user.id); - - await UserProfiles.update(user.id, { - integrations: { - ...profile.integrations, - twitter: { - accessToken: result.accessToken, - accessTokenSecret: result.accessTokenSecret, - userId: result.userId, - screenName: result.screenName, - } - }, - }); - - ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`; - - // Publish i updated event - publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { - detail: true, - includeSecrets: true - })); - } -}); - -export default router; diff --git a/src/server/api/stream/channel.ts b/src/server/api/stream/channel.ts deleted file mode 100644 index 2824d7d1b8..0000000000 --- a/src/server/api/stream/channel.ts +++ /dev/null @@ -1,62 +0,0 @@ -import autobind from 'autobind-decorator'; -import Connection from '.'; - -/** - * Stream channel - */ -export default abstract class Channel { - protected connection: Connection; - public id: string; - public abstract readonly chName: string; - public static readonly shouldShare: boolean; - public static readonly requireCredential: boolean; - - protected get user() { - return this.connection.user; - } - - protected get userProfile() { - return this.connection.userProfile; - } - - protected get following() { - return this.connection.following; - } - - protected get muting() { - return this.connection.muting; - } - - protected get blocking() { - return this.connection.blocking; - } - - protected get followingChannels() { - return this.connection.followingChannels; - } - - protected get subscriber() { - return this.connection.subscriber; - } - - constructor(id: string, connection: Connection) { - this.id = id; - this.connection = connection; - } - - @autobind - public send(typeOrPayload: any, payload?: any) { - const type = payload === undefined ? typeOrPayload.type : typeOrPayload; - const body = payload === undefined ? typeOrPayload.body : payload; - - this.connection.sendMessageToWs('channel', { - id: this.id, - type: type, - body: body - }); - } - - public abstract init(params: any): void; - public dispose?(): void; - public onMessage?(type: string, body: any): void; -} diff --git a/src/server/api/stream/channels/admin.ts b/src/server/api/stream/channels/admin.ts deleted file mode 100644 index 1ff932d1dd..0000000000 --- a/src/server/api/stream/channels/admin.ts +++ /dev/null @@ -1,16 +0,0 @@ -import autobind from 'autobind-decorator'; -import Channel from '../channel'; - -export default class extends Channel { - public readonly chName = 'admin'; - public static shouldShare = true; - public static requireCredential = true; - - @autobind - public async init(params: any) { - // Subscribe admin stream - this.subscriber.on(`adminStream:${this.user!.id}`, data => { - this.send(data); - }); - } -} diff --git a/src/server/api/stream/channels/antenna.ts b/src/server/api/stream/channels/antenna.ts deleted file mode 100644 index 3cbdfebb43..0000000000 --- a/src/server/api/stream/channels/antenna.ts +++ /dev/null @@ -1,45 +0,0 @@ -import autobind from 'autobind-decorator'; -import Channel from '../channel'; -import { Notes } from '@/models/index'; -import { isMutedUserRelated } from '@/misc/is-muted-user-related'; -import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; -import { StreamMessages } from '../types'; - -export default class extends Channel { - public readonly chName = 'antenna'; - public static shouldShare = false; - public static requireCredential = false; - private antennaId: string; - - @autobind - public async init(params: any) { - this.antennaId = params.antennaId as string; - - // Subscribe stream - this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent); - } - - @autobind - private async onEvent(data: StreamMessages['antenna']['payload']) { - if (data.type === 'note') { - const note = await Notes.pack(data.body.id, this.user, { detail: true }); - - // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isMutedUserRelated(note, this.muting)) return; - // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isBlockerUserRelated(note, this.blocking)) return; - - this.connection.cacheNote(note); - - this.send('note', note); - } else { - this.send(data.type, data.body); - } - } - - @autobind - public dispose() { - // Unsubscribe events - this.subscriber.off(`antennaStream:${this.antennaId}`, this.onEvent); - } -} diff --git a/src/server/api/stream/channels/channel.ts b/src/server/api/stream/channels/channel.ts deleted file mode 100644 index bf7942f522..0000000000 --- a/src/server/api/stream/channels/channel.ts +++ /dev/null @@ -1,92 +0,0 @@ -import autobind from 'autobind-decorator'; -import Channel from '../channel'; -import { Notes, Users } from '@/models/index'; -import { isMutedUserRelated } from '@/misc/is-muted-user-related'; -import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; -import { User } from '@/models/entities/user'; -import { StreamMessages } from '../types'; -import { Packed } from '@/misc/schema'; - -export default class extends Channel { - public readonly chName = 'channel'; - public static shouldShare = false; - public static requireCredential = false; - private channelId: string; - private typers: Record<User['id'], Date> = {}; - private emitTypersIntervalId: ReturnType<typeof setInterval>; - - @autobind - public async init(params: any) { - this.channelId = params.channelId as string; - - // Subscribe stream - this.subscriber.on('notesStream', this.onNote); - this.subscriber.on(`channelStream:${this.channelId}`, this.onEvent); - this.emitTypersIntervalId = setInterval(this.emitTypers, 5000); - } - - @autobind - private async onNote(note: Packed<'Note'>) { - if (note.channelId !== this.channelId) return; - - // リプライなら再pack - if (note.replyId != null) { - note.reply = await Notes.pack(note.replyId, this.user, { - detail: true - }); - } - // Renoteなら再pack - if (note.renoteId != null) { - note.renote = await Notes.pack(note.renoteId, this.user, { - detail: true - }); - } - - // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isMutedUserRelated(note, this.muting)) return; - // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isBlockerUserRelated(note, this.blocking)) return; - - this.connection.cacheNote(note); - - this.send('note', note); - } - - @autobind - private onEvent(data: StreamMessages['channel']['payload']) { - if (data.type === 'typing') { - const id = data.body; - const begin = this.typers[id] == null; - this.typers[id] = new Date(); - if (begin) { - this.emitTypers(); - } - } - } - - @autobind - private async emitTypers() { - const now = new Date(); - - // Remove not typing users - for (const [userId, date] of Object.entries(this.typers)) { - if (now.getTime() - date.getTime() > 5000) delete this.typers[userId]; - } - - const users = await Users.packMany(Object.keys(this.typers), null, { detail: false }); - - this.send({ - type: 'typers', - body: users, - }); - } - - @autobind - public dispose() { - // Unsubscribe events - this.subscriber.off('notesStream', this.onNote); - this.subscriber.off(`channelStream:${this.channelId}`, this.onEvent); - - clearInterval(this.emitTypersIntervalId); - } -} diff --git a/src/server/api/stream/channels/drive.ts b/src/server/api/stream/channels/drive.ts deleted file mode 100644 index 4112dd9b04..0000000000 --- a/src/server/api/stream/channels/drive.ts +++ /dev/null @@ -1,16 +0,0 @@ -import autobind from 'autobind-decorator'; -import Channel from '../channel'; - -export default class extends Channel { - public readonly chName = 'drive'; - public static shouldShare = true; - public static requireCredential = true; - - @autobind - public async init(params: any) { - // Subscribe drive stream - this.subscriber.on(`driveStream:${this.user!.id}`, data => { - this.send(data); - }); - } -} diff --git a/src/server/api/stream/channels/games/reversi-game.ts b/src/server/api/stream/channels/games/reversi-game.ts deleted file mode 100644 index bfdbf1d266..0000000000 --- a/src/server/api/stream/channels/games/reversi-game.ts +++ /dev/null @@ -1,372 +0,0 @@ -import autobind from 'autobind-decorator'; -import * as CRC32 from 'crc-32'; -import { publishReversiGameStream } from '@/services/stream'; -import Reversi from '../../../../../games/reversi/core'; -import * as maps from '../../../../../games/reversi/maps'; -import Channel from '../../channel'; -import { ReversiGame } from '@/models/entities/games/reversi/game'; -import { ReversiGames, Users } from '@/models/index'; -import { User } from '@/models/entities/user'; - -export default class extends Channel { - public readonly chName = 'gamesReversiGame'; - public static shouldShare = false; - public static requireCredential = false; - - private gameId: ReversiGame['id'] | null = null; - private watchers: Record<User['id'], Date> = {}; - private emitWatchersIntervalId: ReturnType<typeof setInterval>; - - @autobind - public async init(params: any) { - this.gameId = params.gameId; - - // Subscribe game stream - this.subscriber.on(`reversiGameStream:${this.gameId}`, this.onEvent); - this.emitWatchersIntervalId = setInterval(this.emitWatchers, 5000); - - const game = await ReversiGames.findOne(this.gameId!); - if (game == null) throw new Error('game not found'); - - // 観戦者イベント - this.watch(game); - } - - @autobind - private onEvent(data: any) { - if (data.type === 'watching') { - const id = data.body; - this.watchers[id] = new Date(); - } else { - this.send(data); - } - } - - @autobind - private async emitWatchers() { - const now = new Date(); - - // Remove not watching users - for (const [userId, date] of Object.entries(this.watchers)) { - if (now.getTime() - date.getTime() > 5000) delete this.watchers[userId]; - } - - const users = await Users.packMany(Object.keys(this.watchers), null, { detail: false }); - - this.send({ - type: 'watchers', - body: users, - }); - } - - @autobind - public dispose() { - // Unsubscribe events - this.subscriber.off(`reversiGameStream:${this.gameId}`, this.onEvent); - clearInterval(this.emitWatchersIntervalId); - } - - @autobind - public onMessage(type: string, body: any) { - switch (type) { - case 'accept': this.accept(true); break; - case 'cancelAccept': this.accept(false); break; - case 'updateSettings': this.updateSettings(body.key, body.value); break; - case 'initForm': this.initForm(body); break; - case 'updateForm': this.updateForm(body.id, body.value); break; - case 'message': this.message(body); break; - case 'set': this.set(body.pos); break; - case 'check': this.check(body.crc32); break; - } - } - - @autobind - private async updateSettings(key: string, value: any) { - if (this.user == null) return; - - const game = await ReversiGames.findOne(this.gameId!); - if (game == null) throw new Error('game not found'); - - if (game.isStarted) return; - if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return; - if ((game.user1Id === this.user.id) && game.user1Accepted) return; - if ((game.user2Id === this.user.id) && game.user2Accepted) return; - - if (!['map', 'bw', 'isLlotheo', 'canPutEverywhere', 'loopedBoard'].includes(key)) return; - - await ReversiGames.update(this.gameId!, { - [key]: value - }); - - publishReversiGameStream(this.gameId!, 'updateSettings', { - key: key, - value: value - }); - } - - @autobind - private async initForm(form: any) { - if (this.user == null) return; - - const game = await ReversiGames.findOne(this.gameId!); - if (game == null) throw new Error('game not found'); - - if (game.isStarted) return; - if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return; - - const set = game.user1Id === this.user.id ? { - form1: form - } : { - form2: form - }; - - await ReversiGames.update(this.gameId!, set); - - publishReversiGameStream(this.gameId!, 'initForm', { - userId: this.user.id, - form - }); - } - - @autobind - private async updateForm(id: string, value: any) { - if (this.user == null) return; - - const game = await ReversiGames.findOne(this.gameId!); - if (game == null) throw new Error('game not found'); - - if (game.isStarted) return; - if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return; - - const form = game.user1Id === this.user.id ? game.form2 : game.form1; - - const item = form.find((i: any) => i.id == id); - - if (item == null) return; - - item.value = value; - - const set = game.user1Id === this.user.id ? { - form2: form - } : { - form1: form - }; - - await ReversiGames.update(this.gameId!, set); - - publishReversiGameStream(this.gameId!, 'updateForm', { - userId: this.user.id, - id, - value - }); - } - - @autobind - private async message(message: any) { - if (this.user == null) return; - - message.id = Math.random(); - publishReversiGameStream(this.gameId!, 'message', { - userId: this.user.id, - message - }); - } - - @autobind - private async accept(accept: boolean) { - if (this.user == null) return; - - const game = await ReversiGames.findOne(this.gameId!); - if (game == null) throw new Error('game not found'); - - if (game.isStarted) return; - - let bothAccepted = false; - - if (game.user1Id === this.user.id) { - await ReversiGames.update(this.gameId!, { - user1Accepted: accept - }); - - publishReversiGameStream(this.gameId!, 'changeAccepts', { - user1: accept, - user2: game.user2Accepted - }); - - if (accept && game.user2Accepted) bothAccepted = true; - } else if (game.user2Id === this.user.id) { - await ReversiGames.update(this.gameId!, { - user2Accepted: accept - }); - - publishReversiGameStream(this.gameId!, 'changeAccepts', { - user1: game.user1Accepted, - user2: accept - }); - - if (accept && game.user1Accepted) bothAccepted = true; - } else { - return; - } - - if (bothAccepted) { - // 3秒後、まだacceptされていたらゲーム開始 - setTimeout(async () => { - const freshGame = await ReversiGames.findOne(this.gameId!); - if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return; - if (!freshGame.user1Accepted || !freshGame.user2Accepted) return; - - let bw: number; - if (freshGame.bw == 'random') { - bw = Math.random() > 0.5 ? 1 : 2; - } else { - bw = parseInt(freshGame.bw, 10); - } - - function getRandomMap() { - const mapCount = Object.entries(maps).length; - const rnd = Math.floor(Math.random() * mapCount); - return Object.values(maps)[rnd].data; - } - - const map = freshGame.map != null ? freshGame.map : getRandomMap(); - - await ReversiGames.update(this.gameId!, { - startedAt: new Date(), - isStarted: true, - black: bw, - map: map - }); - - //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 - const o = new Reversi(map, { - isLlotheo: freshGame.isLlotheo, - canPutEverywhere: freshGame.canPutEverywhere, - loopedBoard: freshGame.loopedBoard - }); - - if (o.isEnded) { - let winner; - if (o.winner === true) { - winner = freshGame.black == 1 ? freshGame.user1Id : freshGame.user2Id; - } else if (o.winner === false) { - winner = freshGame.black == 1 ? freshGame.user2Id : freshGame.user1Id; - } else { - winner = null; - } - - await ReversiGames.update(this.gameId!, { - isEnded: true, - winnerId: winner - }); - - publishReversiGameStream(this.gameId!, 'ended', { - winnerId: winner, - game: await ReversiGames.pack(this.gameId!, this.user) - }); - } - //#endregion - - publishReversiGameStream(this.gameId!, 'started', - await ReversiGames.pack(this.gameId!, this.user)); - }, 3000); - } - } - - // 石を打つ - @autobind - private async set(pos: number) { - if (this.user == null) return; - - const game = await ReversiGames.findOne(this.gameId!); - if (game == null) throw new Error('game not found'); - - if (!game.isStarted) return; - if (game.isEnded) return; - if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return; - - const myColor = - ((game.user1Id === this.user.id) && game.black == 1) || ((game.user2Id === this.user.id) && game.black == 2) - ? true - : false; - - const o = new Reversi(game.map, { - isLlotheo: game.isLlotheo, - canPutEverywhere: game.canPutEverywhere, - loopedBoard: game.loopedBoard - }); - - // 盤面の状態を再生 - for (const log of game.logs) { - o.put(log.color, log.pos); - } - - if (o.turn !== myColor) return; - - if (!o.canPut(myColor, pos)) return; - o.put(myColor, pos); - - let winner; - if (o.isEnded) { - if (o.winner === true) { - winner = game.black == 1 ? game.user1Id : game.user2Id; - } else if (o.winner === false) { - winner = game.black == 1 ? game.user2Id : game.user1Id; - } else { - winner = null; - } - } - - const log = { - at: new Date(), - color: myColor, - pos - }; - - const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()).toString(); - - game.logs.push(log); - - await ReversiGames.update(this.gameId!, { - crc32, - isEnded: o.isEnded, - winnerId: winner, - logs: game.logs - }); - - publishReversiGameStream(this.gameId!, 'set', Object.assign(log, { - next: o.turn - })); - - if (o.isEnded) { - publishReversiGameStream(this.gameId!, 'ended', { - winnerId: winner, - game: await ReversiGames.pack(this.gameId!, this.user) - }); - } - } - - @autobind - private async check(crc32: string | number) { - const game = await ReversiGames.findOne(this.gameId!); - if (game == null) throw new Error('game not found'); - - if (!game.isStarted) return; - - if (crc32.toString() !== game.crc32) { - this.send('rescue', await ReversiGames.pack(game, this.user)); - } - - // ついでに観戦者イベントを発行 - this.watch(game); - } - - @autobind - private watch(game: ReversiGame) { - if (this.user != null) { - if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) { - publishReversiGameStream(this.gameId!, 'watching', this.user.id); - } - } - } -} diff --git a/src/server/api/stream/channels/games/reversi.ts b/src/server/api/stream/channels/games/reversi.ts deleted file mode 100644 index 3b89aac35c..0000000000 --- a/src/server/api/stream/channels/games/reversi.ts +++ /dev/null @@ -1,33 +0,0 @@ -import autobind from 'autobind-decorator'; -import { publishMainStream } from '@/services/stream'; -import Channel from '../../channel'; -import { ReversiMatchings } from '@/models/index'; - -export default class extends Channel { - public readonly chName = 'gamesReversi'; - public static shouldShare = true; - public static requireCredential = true; - - @autobind - public async init(params: any) { - // Subscribe reversi stream - this.subscriber.on(`reversiStream:${this.user!.id}`, data => { - this.send(data); - }); - } - - @autobind - public async onMessage(type: string, body: any) { - switch (type) { - case 'ping': - if (body.id == null) return; - const matching = await ReversiMatchings.findOne({ - parentId: this.user!.id, - childId: body.id - }); - if (matching == null) return; - publishMainStream(matching.childId, 'reversiInvited', await ReversiMatchings.pack(matching, { id: matching.childId })); - break; - } - } -} diff --git a/src/server/api/stream/channels/global-timeline.ts b/src/server/api/stream/channels/global-timeline.ts deleted file mode 100644 index f5983ab472..0000000000 --- a/src/server/api/stream/channels/global-timeline.ts +++ /dev/null @@ -1,73 +0,0 @@ -import autobind from 'autobind-decorator'; -import { isMutedUserRelated } from '@/misc/is-muted-user-related'; -import Channel from '../channel'; -import { fetchMeta } from '@/misc/fetch-meta'; -import { Notes } from '@/models/index'; -import { checkWordMute } from '@/misc/check-word-mute'; -import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; -import { Packed } from '@/misc/schema'; - -export default class extends Channel { - public readonly chName = 'globalTimeline'; - public static shouldShare = true; - public static requireCredential = false; - - @autobind - public async init(params: any) { - const meta = await fetchMeta(); - if (meta.disableGlobalTimeline) { - if (this.user == null || (!this.user.isAdmin && !this.user.isModerator)) return; - } - - // Subscribe events - this.subscriber.on('notesStream', this.onNote); - } - - @autobind - private async onNote(note: Packed<'Note'>) { - if (note.visibility !== 'public') return; - if (note.channelId != null) return; - - // リプライなら再pack - if (note.replyId != null) { - note.reply = await Notes.pack(note.replyId, this.user, { - detail: true - }); - } - // Renoteなら再pack - if (note.renoteId != null) { - note.renote = await Notes.pack(note.renoteId, this.user, { - detail: true - }); - } - - // 関係ない返信は除外 - if (note.reply) { - const reply = note.reply; - // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 - if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; - } - - // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isMutedUserRelated(note, this.muting)) return; - // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isBlockerUserRelated(note, this.blocking)) return; - - // 流れてきたNoteがミュートすべきNoteだったら無視する - // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) - // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 - // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 - // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる - if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; - - this.connection.cacheNote(note); - - this.send('note', note); - } - - @autobind - public dispose() { - // Unsubscribe events - this.subscriber.off('notesStream', this.onNote); - } -} diff --git a/src/server/api/stream/channels/hashtag.ts b/src/server/api/stream/channels/hashtag.ts deleted file mode 100644 index 281be4f2eb..0000000000 --- a/src/server/api/stream/channels/hashtag.ts +++ /dev/null @@ -1,53 +0,0 @@ -import autobind from 'autobind-decorator'; -import { isMutedUserRelated } from '@/misc/is-muted-user-related'; -import Channel from '../channel'; -import { Notes } from '@/models/index'; -import { normalizeForSearch } from '@/misc/normalize-for-search'; -import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; -import { Packed } from '@/misc/schema'; - -export default class extends Channel { - public readonly chName = 'hashtag'; - public static shouldShare = false; - public static requireCredential = false; - private q: string[][]; - - @autobind - public async init(params: any) { - this.q = params.q; - - if (this.q == null) return; - - // Subscribe stream - this.subscriber.on('notesStream', this.onNote); - } - - @autobind - private async onNote(note: Packed<'Note'>) { - const noteTags = note.tags ? note.tags.map((t: string) => t.toLowerCase()) : []; - const matched = this.q.some(tags => tags.every(tag => noteTags.includes(normalizeForSearch(tag)))); - if (!matched) return; - - // Renoteなら再pack - if (note.renoteId != null) { - note.renote = await Notes.pack(note.renoteId, this.user, { - detail: true - }); - } - - // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isMutedUserRelated(note, this.muting)) return; - // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isBlockerUserRelated(note, this.blocking)) return; - - this.connection.cacheNote(note); - - this.send('note', note); - } - - @autobind - public dispose() { - // Unsubscribe events - this.subscriber.off('notesStream', this.onNote); - } -} diff --git a/src/server/api/stream/channels/home-timeline.ts b/src/server/api/stream/channels/home-timeline.ts deleted file mode 100644 index 52e9aec250..0000000000 --- a/src/server/api/stream/channels/home-timeline.ts +++ /dev/null @@ -1,81 +0,0 @@ -import autobind from 'autobind-decorator'; -import { isMutedUserRelated } from '@/misc/is-muted-user-related'; -import Channel from '../channel'; -import { Notes } from '@/models/index'; -import { checkWordMute } from '@/misc/check-word-mute'; -import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; -import { Packed } from '@/misc/schema'; - -export default class extends Channel { - public readonly chName = 'homeTimeline'; - public static shouldShare = true; - public static requireCredential = true; - - @autobind - public async init(params: any) { - // Subscribe events - this.subscriber.on('notesStream', this.onNote); - } - - @autobind - private async onNote(note: Packed<'Note'>) { - if (note.channelId) { - if (!this.followingChannels.has(note.channelId)) return; - } else { - // その投稿のユーザーをフォローしていなかったら弾く - if ((this.user!.id !== note.userId) && !this.following.has(note.userId)) return; - } - - if (['followers', 'specified'].includes(note.visibility)) { - note = await Notes.pack(note.id, this.user!, { - detail: true - }); - - if (note.isHidden) { - return; - } - } else { - // リプライなら再pack - if (note.replyId != null) { - note.reply = await Notes.pack(note.replyId, this.user!, { - detail: true - }); - } - // Renoteなら再pack - if (note.renoteId != null) { - note.renote = await Notes.pack(note.renoteId, this.user!, { - detail: true - }); - } - } - - // 関係ない返信は除外 - if (note.reply) { - const reply = note.reply; - // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 - if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; - } - - // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isMutedUserRelated(note, this.muting)) return; - // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isBlockerUserRelated(note, this.blocking)) return; - - // 流れてきたNoteがミュートすべきNoteだったら無視する - // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) - // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 - // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 - // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる - if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; - - this.connection.cacheNote(note); - - this.send('note', note); - } - - @autobind - public dispose() { - // Unsubscribe events - this.subscriber.off('notesStream', this.onNote); - } -} diff --git a/src/server/api/stream/channels/hybrid-timeline.ts b/src/server/api/stream/channels/hybrid-timeline.ts deleted file mode 100644 index 51f95fc0cd..0000000000 --- a/src/server/api/stream/channels/hybrid-timeline.ts +++ /dev/null @@ -1,89 +0,0 @@ -import autobind from 'autobind-decorator'; -import { isMutedUserRelated } from '@/misc/is-muted-user-related'; -import Channel from '../channel'; -import { fetchMeta } from '@/misc/fetch-meta'; -import { Notes } from '@/models/index'; -import { checkWordMute } from '@/misc/check-word-mute'; -import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; -import { Packed } from '@/misc/schema'; - -export default class extends Channel { - public readonly chName = 'hybridTimeline'; - public static shouldShare = true; - public static requireCredential = true; - - @autobind - public async init(params: any) { - const meta = await fetchMeta(); - if (meta.disableLocalTimeline && !this.user!.isAdmin && !this.user!.isModerator) return; - - // Subscribe events - this.subscriber.on('notesStream', this.onNote); - } - - @autobind - private async onNote(note: Packed<'Note'>) { - // チャンネルの投稿ではなく、自分自身の投稿 または - // チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または - // チャンネルの投稿ではなく、全体公開のローカルの投稿 または - // フォローしているチャンネルの投稿 の場合だけ - if (!( - (note.channelId == null && this.user!.id === note.userId) || - (note.channelId == null && this.following.has(note.userId)) || - (note.channelId == null && (note.user.host == null && note.visibility === 'public')) || - (note.channelId != null && this.followingChannels.has(note.channelId)) - )) return; - - if (['followers', 'specified'].includes(note.visibility)) { - note = await Notes.pack(note.id, this.user!, { - detail: true - }); - - if (note.isHidden) { - return; - } - } else { - // リプライなら再pack - if (note.replyId != null) { - note.reply = await Notes.pack(note.replyId, this.user!, { - detail: true - }); - } - // Renoteなら再pack - if (note.renoteId != null) { - note.renote = await Notes.pack(note.renoteId, this.user!, { - detail: true - }); - } - } - - // 関係ない返信は除外 - if (note.reply) { - const reply = note.reply; - // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 - if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; - } - - // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isMutedUserRelated(note, this.muting)) return; - // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isBlockerUserRelated(note, this.blocking)) return; - - // 流れてきたNoteがミュートすべきNoteだったら無視する - // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) - // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 - // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 - // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる - if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; - - this.connection.cacheNote(note); - - this.send('note', note); - } - - @autobind - public dispose() { - // Unsubscribe events - this.subscriber.off('notesStream', this.onNote); - } -} diff --git a/src/server/api/stream/channels/index.ts b/src/server/api/stream/channels/index.ts deleted file mode 100644 index 1841573043..0000000000 --- a/src/server/api/stream/channels/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import main from './main'; -import homeTimeline from './home-timeline'; -import localTimeline from './local-timeline'; -import hybridTimeline from './hybrid-timeline'; -import globalTimeline from './global-timeline'; -import serverStats from './server-stats'; -import queueStats from './queue-stats'; -import userList from './user-list'; -import antenna from './antenna'; -import messaging from './messaging'; -import messagingIndex from './messaging-index'; -import drive from './drive'; -import hashtag from './hashtag'; -import channel from './channel'; -import admin from './admin'; -import gamesReversi from './games/reversi'; -import gamesReversiGame from './games/reversi-game'; - -export default { - main, - homeTimeline, - localTimeline, - hybridTimeline, - globalTimeline, - serverStats, - queueStats, - userList, - antenna, - messaging, - messagingIndex, - drive, - hashtag, - channel, - admin, - gamesReversi, - gamesReversiGame -}; diff --git a/src/server/api/stream/channels/local-timeline.ts b/src/server/api/stream/channels/local-timeline.ts deleted file mode 100644 index a6166c2be2..0000000000 --- a/src/server/api/stream/channels/local-timeline.ts +++ /dev/null @@ -1,74 +0,0 @@ -import autobind from 'autobind-decorator'; -import { isMutedUserRelated } from '@/misc/is-muted-user-related'; -import Channel from '../channel'; -import { fetchMeta } from '@/misc/fetch-meta'; -import { Notes } from '@/models/index'; -import { checkWordMute } from '@/misc/check-word-mute'; -import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; -import { Packed } from '@/misc/schema'; - -export default class extends Channel { - public readonly chName = 'localTimeline'; - public static shouldShare = true; - public static requireCredential = false; - - @autobind - public async init(params: any) { - const meta = await fetchMeta(); - if (meta.disableLocalTimeline) { - if (this.user == null || (!this.user.isAdmin && !this.user.isModerator)) return; - } - - // Subscribe events - this.subscriber.on('notesStream', this.onNote); - } - - @autobind - private async onNote(note: Packed<'Note'>) { - if (note.user.host !== null) return; - if (note.visibility !== 'public') return; - if (note.channelId != null && !this.followingChannels.has(note.channelId)) return; - - // リプライなら再pack - if (note.replyId != null) { - note.reply = await Notes.pack(note.replyId, this.user, { - detail: true - }); - } - // Renoteなら再pack - if (note.renoteId != null) { - note.renote = await Notes.pack(note.renoteId, this.user, { - detail: true - }); - } - - // 関係ない返信は除外 - if (note.reply) { - const reply = note.reply; - // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 - if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; - } - - // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isMutedUserRelated(note, this.muting)) return; - // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isBlockerUserRelated(note, this.blocking)) return; - - // 流れてきたNoteがミュートすべきNoteだったら無視する - // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) - // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 - // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 - // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる - if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; - - this.connection.cacheNote(note); - - this.send('note', note); - } - - @autobind - public dispose() { - // Unsubscribe events - this.subscriber.off('notesStream', this.onNote); - } -} diff --git a/src/server/api/stream/channels/main.ts b/src/server/api/stream/channels/main.ts deleted file mode 100644 index 131ac30472..0000000000 --- a/src/server/api/stream/channels/main.ts +++ /dev/null @@ -1,43 +0,0 @@ -import autobind from 'autobind-decorator'; -import Channel from '../channel'; -import { Notes } from '@/models/index'; - -export default class extends Channel { - public readonly chName = 'main'; - public static shouldShare = true; - public static requireCredential = true; - - @autobind - public async init(params: any) { - // Subscribe main stream channel - this.subscriber.on(`mainStream:${this.user!.id}`, async data => { - switch (data.type) { - case 'notification': { - if (data.body.userId && this.muting.has(data.body.userId)) return; - - if (data.body.note && data.body.note.isHidden) { - const note = await Notes.pack(data.body.note.id, this.user, { - detail: true - }); - this.connection.cacheNote(note); - data.body.note = note; - } - break; - } - case 'mention': { - if (this.muting.has(data.body.userId)) return; - if (data.body.isHidden) { - const note = await Notes.pack(data.body.id, this.user, { - detail: true - }); - this.connection.cacheNote(note); - data.body = note; - } - break; - } - } - - this.send(data.type, data.body); - }); - } -} diff --git a/src/server/api/stream/channels/messaging-index.ts b/src/server/api/stream/channels/messaging-index.ts deleted file mode 100644 index 0c495398ab..0000000000 --- a/src/server/api/stream/channels/messaging-index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import autobind from 'autobind-decorator'; -import Channel from '../channel'; - -export default class extends Channel { - public readonly chName = 'messagingIndex'; - public static shouldShare = true; - public static requireCredential = true; - - @autobind - public async init(params: any) { - // Subscribe messaging index stream - this.subscriber.on(`messagingIndexStream:${this.user!.id}`, data => { - this.send(data); - }); - } -} diff --git a/src/server/api/stream/channels/messaging.ts b/src/server/api/stream/channels/messaging.ts deleted file mode 100644 index c049e880b9..0000000000 --- a/src/server/api/stream/channels/messaging.ts +++ /dev/null @@ -1,106 +0,0 @@ -import autobind from 'autobind-decorator'; -import { readUserMessagingMessage, readGroupMessagingMessage, deliverReadActivity } from '../../common/read-messaging-message'; -import Channel from '../channel'; -import { UserGroupJoinings, Users, MessagingMessages } from '@/models/index'; -import { User, ILocalUser, IRemoteUser } from '@/models/entities/user'; -import { UserGroup } from '@/models/entities/user-group'; -import { StreamMessages } from '../types'; - -export default class extends Channel { - public readonly chName = 'messaging'; - public static shouldShare = false; - public static requireCredential = true; - - private otherpartyId: string | null; - private otherparty: User | null; - private groupId: string | null; - private subCh: `messagingStream:${User['id']}-${User['id']}` | `messagingStream:${UserGroup['id']}`; - private typers: Record<User['id'], Date> = {}; - private emitTypersIntervalId: ReturnType<typeof setInterval>; - - @autobind - public async init(params: any) { - this.otherpartyId = params.otherparty; - this.otherparty = this.otherpartyId ? await Users.findOneOrFail({ id: this.otherpartyId }) : null; - this.groupId = params.group; - - // Check joining - if (this.groupId) { - const joining = await UserGroupJoinings.findOne({ - userId: this.user!.id, - userGroupId: this.groupId - }); - - if (joining == null) { - return; - } - } - - this.emitTypersIntervalId = setInterval(this.emitTypers, 5000); - - this.subCh = this.otherpartyId - ? `messagingStream:${this.user!.id}-${this.otherpartyId}` - : `messagingStream:${this.groupId}`; - - // Subscribe messaging stream - this.subscriber.on(this.subCh, this.onEvent); - } - - @autobind - private onEvent(data: StreamMessages['messaging']['payload'] | StreamMessages['groupMessaging']['payload']) { - if (data.type === 'typing') { - const id = data.body; - const begin = this.typers[id] == null; - this.typers[id] = new Date(); - if (begin) { - this.emitTypers(); - } - } else { - this.send(data); - } - } - - @autobind - public onMessage(type: string, body: any) { - switch (type) { - case 'read': - if (this.otherpartyId) { - readUserMessagingMessage(this.user!.id, this.otherpartyId, [body.id]); - - // リモートユーザーからのメッセージだったら既読配信 - if (Users.isLocalUser(this.user!) && Users.isRemoteUser(this.otherparty!)) { - MessagingMessages.findOne(body.id).then(message => { - if (message) deliverReadActivity(this.user as ILocalUser, this.otherparty as IRemoteUser, message); - }); - } - } else if (this.groupId) { - readGroupMessagingMessage(this.user!.id, this.groupId, [body.id]); - } - break; - } - } - - @autobind - private async emitTypers() { - const now = new Date(); - - // Remove not typing users - for (const [userId, date] of Object.entries(this.typers)) { - if (now.getTime() - date.getTime() > 5000) delete this.typers[userId]; - } - - const users = await Users.packMany(Object.keys(this.typers), null, { detail: false }); - - this.send({ - type: 'typers', - body: users, - }); - } - - @autobind - public dispose() { - this.subscriber.off(this.subCh, this.onEvent); - - clearInterval(this.emitTypersIntervalId); - } -} diff --git a/src/server/api/stream/channels/queue-stats.ts b/src/server/api/stream/channels/queue-stats.ts deleted file mode 100644 index 0bda0cfcb9..0000000000 --- a/src/server/api/stream/channels/queue-stats.ts +++ /dev/null @@ -1,41 +0,0 @@ -import autobind from 'autobind-decorator'; -import Xev from 'xev'; -import Channel from '../channel'; - -const ev = new Xev(); - -export default class extends Channel { - public readonly chName = 'queueStats'; - public static shouldShare = true; - public static requireCredential = false; - - @autobind - public async init(params: any) { - ev.addListener('queueStats', this.onStats); - } - - @autobind - private onStats(stats: any) { - this.send('stats', stats); - } - - @autobind - public onMessage(type: string, body: any) { - switch (type) { - case 'requestLog': - ev.once(`queueStatsLog:${body.id}`, statsLog => { - this.send('statsLog', statsLog); - }); - ev.emit('requestQueueStatsLog', { - id: body.id, - length: body.length - }); - break; - } - } - - @autobind - public dispose() { - ev.removeListener('queueStats', this.onStats); - } -} diff --git a/src/server/api/stream/channels/server-stats.ts b/src/server/api/stream/channels/server-stats.ts deleted file mode 100644 index d245a7f70c..0000000000 --- a/src/server/api/stream/channels/server-stats.ts +++ /dev/null @@ -1,41 +0,0 @@ -import autobind from 'autobind-decorator'; -import Xev from 'xev'; -import Channel from '../channel'; - -const ev = new Xev(); - -export default class extends Channel { - public readonly chName = 'serverStats'; - public static shouldShare = true; - public static requireCredential = false; - - @autobind - public async init(params: any) { - ev.addListener('serverStats', this.onStats); - } - - @autobind - private onStats(stats: any) { - this.send('stats', stats); - } - - @autobind - public onMessage(type: string, body: any) { - switch (type) { - case 'requestLog': - ev.once(`serverStatsLog:${body.id}`, statsLog => { - this.send('statsLog', statsLog); - }); - ev.emit('requestServerStatsLog', { - id: body.id, - length: body.length - }); - break; - } - } - - @autobind - public dispose() { - ev.removeListener('serverStats', this.onStats); - } -} diff --git a/src/server/api/stream/channels/user-list.ts b/src/server/api/stream/channels/user-list.ts deleted file mode 100644 index 63b254605b..0000000000 --- a/src/server/api/stream/channels/user-list.ts +++ /dev/null @@ -1,92 +0,0 @@ -import autobind from 'autobind-decorator'; -import Channel from '../channel'; -import { Notes, UserListJoinings, UserLists } from '@/models/index'; -import { isMutedUserRelated } from '@/misc/is-muted-user-related'; -import { User } from '@/models/entities/user'; -import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; -import { Packed } from '@/misc/schema'; - -export default class extends Channel { - public readonly chName = 'userList'; - public static shouldShare = false; - public static requireCredential = false; - private listId: string; - public listUsers: User['id'][] = []; - private listUsersClock: NodeJS.Timer; - - @autobind - public async init(params: any) { - this.listId = params.listId as string; - - // Check existence and owner - const list = await UserLists.findOne({ - id: this.listId, - userId: this.user!.id - }); - if (!list) return; - - // Subscribe stream - this.subscriber.on(`userListStream:${this.listId}`, this.send); - - this.subscriber.on('notesStream', this.onNote); - - this.updateListUsers(); - this.listUsersClock = setInterval(this.updateListUsers, 5000); - } - - @autobind - private async updateListUsers() { - const users = await UserListJoinings.find({ - where: { - userListId: this.listId, - }, - select: ['userId'] - }); - - this.listUsers = users.map(x => x.userId); - } - - @autobind - private async onNote(note: Packed<'Note'>) { - if (!this.listUsers.includes(note.userId)) return; - - if (['followers', 'specified'].includes(note.visibility)) { - note = await Notes.pack(note.id, this.user, { - detail: true - }); - - if (note.isHidden) { - return; - } - } else { - // リプライなら再pack - if (note.replyId != null) { - note.reply = await Notes.pack(note.replyId, this.user, { - detail: true - }); - } - // Renoteなら再pack - if (note.renoteId != null) { - note.renote = await Notes.pack(note.renoteId, this.user, { - detail: true - }); - } - } - - // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isMutedUserRelated(note, this.muting)) return; - // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isBlockerUserRelated(note, this.blocking)) return; - - this.send('note', note); - } - - @autobind - public dispose() { - // Unsubscribe events - this.subscriber.off(`userListStream:${this.listId}`, this.send); - this.subscriber.off('notesStream', this.onNote); - - clearInterval(this.listUsersClock); - } -} diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts deleted file mode 100644 index da4ea5ec99..0000000000 --- a/src/server/api/stream/index.ts +++ /dev/null @@ -1,421 +0,0 @@ -import autobind from 'autobind-decorator'; -import * as websocket from 'websocket'; -import { readNotification } from '../common/read-notification'; -import call from '../call'; -import readNote from '@/services/note/read'; -import Channel from './channel'; -import channels from './channels/index'; -import { EventEmitter } from 'events'; -import { User } from '@/models/entities/user'; -import { Channel as ChannelModel } from '@/models/entities/channel'; -import { Users, Followings, Mutings, UserProfiles, ChannelFollowings, Blockings } from '@/models/index'; -import { ApiError } from '../error'; -import { AccessToken } from '@/models/entities/access-token'; -import { UserProfile } from '@/models/entities/user-profile'; -import { publishChannelStream, publishGroupMessagingStream, publishMessagingStream } from '@/services/stream'; -import { UserGroup } from '@/models/entities/user-group'; -import { StreamEventEmitter, StreamMessages } from './types'; -import { Packed } from '@/misc/schema'; - -/** - * Main stream connection - */ -export default class Connection { - public user?: User; - public userProfile?: UserProfile; - public following: Set<User['id']> = new Set(); - public muting: Set<User['id']> = new Set(); - public blocking: Set<User['id']> = new Set(); // "被"blocking - public followingChannels: Set<ChannelModel['id']> = new Set(); - public token?: AccessToken; - private wsConnection: websocket.connection; - public subscriber: StreamEventEmitter; - private channels: Channel[] = []; - private subscribingNotes: any = {}; - private cachedNotes: Packed<'Note'>[] = []; - - constructor( - wsConnection: websocket.connection, - subscriber: EventEmitter, - user: User | null | undefined, - token: AccessToken | null | undefined - ) { - this.wsConnection = wsConnection; - this.subscriber = subscriber; - if (user) this.user = user; - if (token) this.token = token; - - this.wsConnection.on('message', this.onWsConnectionMessage); - - this.subscriber.on('broadcast', data => { - this.onBroadcastMessage(data); - }); - - if (this.user) { - this.updateFollowing(); - this.updateMuting(); - this.updateBlocking(); - this.updateFollowingChannels(); - this.updateUserProfile(); - - this.subscriber.on(`user:${this.user.id}`, this.onUserEvent); - } - } - - @autobind - private onUserEvent(data: StreamMessages['user']['payload']) { // { type, body }と展開するとそれぞれ型が分離してしまう - switch (data.type) { - case 'follow': - this.following.add(data.body.id); - break; - - case 'unfollow': - this.following.delete(data.body.id); - break; - - case 'mute': - this.muting.add(data.body.id); - break; - - case 'unmute': - this.muting.delete(data.body.id); - break; - - // TODO: block events - - case 'followChannel': - this.followingChannels.add(data.body.id); - break; - - case 'unfollowChannel': - this.followingChannels.delete(data.body.id); - break; - - case 'updateUserProfile': - this.userProfile = data.body; - break; - - case 'terminate': - this.wsConnection.close(); - this.dispose(); - break; - - default: - break; - } - } - - /** - * クライアントからメッセージ受信時 - */ - @autobind - private async onWsConnectionMessage(data: websocket.IMessage) { - if (data.utf8Data == null) return; - - let obj: Record<string, any>; - - try { - obj = JSON.parse(data.utf8Data); - } catch (e) { - return; - } - - const { type, body } = obj; - - switch (type) { - case 'api': this.onApiRequest(body); break; - case 'readNotification': this.onReadNotification(body); break; - case 'subNote': this.onSubscribeNote(body); break; - case 's': this.onSubscribeNote(body); break; // alias - case 'sr': this.onSubscribeNote(body); this.readNote(body); break; - case 'unsubNote': this.onUnsubscribeNote(body); break; - case 'un': this.onUnsubscribeNote(body); break; // alias - case 'connect': this.onChannelConnectRequested(body); break; - case 'disconnect': this.onChannelDisconnectRequested(body); break; - case 'channel': this.onChannelMessageRequested(body); break; - case 'ch': this.onChannelMessageRequested(body); break; // alias - - // 個々のチャンネルではなくルートレベルでこれらのメッセージを受け取る理由は、 - // クライアントの事情を考慮したとき、入力フォームはノートチャンネルやメッセージのメインコンポーネントとは別 - // なこともあるため、それらのコンポーネントがそれぞれ各チャンネルに接続するようにするのは面倒なため。 - case 'typingOnChannel': this.typingOnChannel(body.channel); break; - case 'typingOnMessaging': this.typingOnMessaging(body); break; - } - } - - @autobind - private onBroadcastMessage(data: StreamMessages['broadcast']['payload']) { - this.sendMessageToWs(data.type, data.body); - } - - @autobind - public cacheNote(note: Packed<'Note'>) { - const add = (note: Packed<'Note'>) => { - const existIndex = this.cachedNotes.findIndex(n => n.id === note.id); - if (existIndex > -1) { - this.cachedNotes[existIndex] = note; - return; - } - - this.cachedNotes.unshift(note); - if (this.cachedNotes.length > 32) { - this.cachedNotes.splice(32); - } - }; - - add(note); - if (note.reply) add(note.reply); - if (note.renote) add(note.renote); - } - - @autobind - private readNote(body: any) { - const id = body.id; - - const note = this.cachedNotes.find(n => n.id === id); - if (note == null) return; - - if (this.user && (note.userId !== this.user.id)) { - readNote(this.user.id, [note], { - following: this.following, - followingChannels: this.followingChannels, - }); - } - } - - /** - * APIリクエスト要求時 - */ - @autobind - private async onApiRequest(payload: any) { - // 新鮮なデータを利用するためにユーザーをフェッチ - const user = this.user ? await Users.findOne(this.user.id) : null; - - const endpoint = payload.endpoint || payload.ep; // alias - - // 呼び出し - call(endpoint, user, this.token, payload.data).then(res => { - this.sendMessageToWs(`api:${payload.id}`, { res }); - }).catch((e: ApiError) => { - this.sendMessageToWs(`api:${payload.id}`, { - error: { - message: e.message, - code: e.code, - id: e.id, - kind: e.kind, - ...(e.info ? { info: e.info } : {}) - } - }); - }); - } - - @autobind - private onReadNotification(payload: any) { - if (!payload.id) return; - readNotification(this.user!.id, [payload.id]); - } - - /** - * 投稿購読要求時 - */ - @autobind - private onSubscribeNote(payload: any) { - if (!payload.id) return; - - if (this.subscribingNotes[payload.id] == null) { - this.subscribingNotes[payload.id] = 0; - } - - this.subscribingNotes[payload.id]++; - - if (this.subscribingNotes[payload.id] === 1) { - this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage); - } - } - - /** - * 投稿購読解除要求時 - */ - @autobind - private onUnsubscribeNote(payload: any) { - if (!payload.id) return; - - this.subscribingNotes[payload.id]--; - if (this.subscribingNotes[payload.id] <= 0) { - delete this.subscribingNotes[payload.id]; - this.subscriber.off(`noteStream:${payload.id}`, this.onNoteStreamMessage); - } - } - - @autobind - private async onNoteStreamMessage(data: StreamMessages['note']['payload']) { - this.sendMessageToWs('noteUpdated', { - id: data.body.id, - type: data.type, - body: data.body.body, - }); - } - - /** - * チャンネル接続要求時 - */ - @autobind - private onChannelConnectRequested(payload: any) { - const { channel, id, params, pong } = payload; - this.connectChannel(id, params, channel, pong); - } - - /** - * チャンネル切断要求時 - */ - @autobind - private onChannelDisconnectRequested(payload: any) { - const { id } = payload; - this.disconnectChannel(id); - } - - /** - * クライアントにメッセージ送信 - */ - @autobind - public sendMessageToWs(type: string, payload: any) { - this.wsConnection.send(JSON.stringify({ - type: type, - body: payload - })); - } - - /** - * チャンネルに接続 - */ - @autobind - public connectChannel(id: string, params: any, channel: string, pong = false) { - if ((channels as any)[channel].requireCredential && this.user == null) { - return; - } - - // 共有可能チャンネルに接続しようとしていて、かつそのチャンネルに既に接続していたら無意味なので無視 - if ((channels as any)[channel].shouldShare && this.channels.some(c => c.chName === channel)) { - return; - } - - const ch: Channel = new (channels as any)[channel](id, this); - this.channels.push(ch); - ch.init(params); - - if (pong) { - this.sendMessageToWs('connected', { - id: id - }); - } - } - - /** - * チャンネルから切断 - * @param id チャンネルコネクションID - */ - @autobind - public disconnectChannel(id: string) { - const channel = this.channels.find(c => c.id === id); - - if (channel) { - if (channel.dispose) channel.dispose(); - this.channels = this.channels.filter(c => c.id !== id); - } - } - - /** - * チャンネルへメッセージ送信要求時 - * @param data メッセージ - */ - @autobind - private onChannelMessageRequested(data: any) { - const channel = this.channels.find(c => c.id === data.id); - if (channel != null && channel.onMessage != null) { - channel.onMessage(data.type, data.body); - } - } - - @autobind - private typingOnChannel(channel: ChannelModel['id']) { - if (this.user) { - publishChannelStream(channel, 'typing', this.user.id); - } - } - - @autobind - private typingOnMessaging(param: { partner?: User['id']; group?: UserGroup['id']; }) { - if (this.user) { - if (param.partner) { - publishMessagingStream(param.partner, this.user.id, 'typing', this.user.id); - } else if (param.group) { - publishGroupMessagingStream(param.group, 'typing', this.user.id); - } - } - } - - @autobind - private async updateFollowing() { - const followings = await Followings.find({ - where: { - followerId: this.user!.id - }, - select: ['followeeId'] - }); - - this.following = new Set<string>(followings.map(x => x.followeeId)); - } - - @autobind - private async updateMuting() { - const mutings = await Mutings.find({ - where: { - muterId: this.user!.id - }, - select: ['muteeId'] - }); - - this.muting = new Set<string>(mutings.map(x => x.muteeId)); - } - - @autobind - private async updateBlocking() { // ここでいうBlockingは被Blockingの意 - const blockings = await Blockings.find({ - where: { - blockeeId: this.user!.id - }, - select: ['blockerId'] - }); - - this.blocking = new Set<string>(blockings.map(x => x.blockerId)); - } - - @autobind - private async updateFollowingChannels() { - const followings = await ChannelFollowings.find({ - where: { - followerId: this.user!.id - }, - select: ['followeeId'] - }); - - this.followingChannels = new Set<string>(followings.map(x => x.followeeId)); - } - - @autobind - private async updateUserProfile() { - this.userProfile = await UserProfiles.findOne({ - userId: this.user!.id - }); - } - - /** - * ストリームが切れたとき - */ - @autobind - public dispose() { - for (const c of this.channels.filter(c => c.dispose)) { - if (c.dispose) c.dispose(); - } - } -} diff --git a/src/server/api/stream/types.ts b/src/server/api/stream/types.ts deleted file mode 100644 index 70eb5c5ce5..0000000000 --- a/src/server/api/stream/types.ts +++ /dev/null @@ -1,299 +0,0 @@ -import { EventEmitter } from 'events'; -import Emitter from 'strict-event-emitter-types'; -import { Channel } from '@/models/entities/channel'; -import { User } from '@/models/entities/user'; -import { UserProfile } from '@/models/entities/user-profile'; -import { Note } from '@/models/entities/note'; -import { Antenna } from '@/models/entities/antenna'; -import { DriveFile } from '@/models/entities/drive-file'; -import { DriveFolder } from '@/models/entities/drive-folder'; -import { Emoji } from '@/models/entities/emoji'; -import { UserList } from '@/models/entities/user-list'; -import { MessagingMessage } from '@/models/entities/messaging-message'; -import { UserGroup } from '@/models/entities/user-group'; -import { ReversiGame } from '@/models/entities/games/reversi/game'; -import { AbuseUserReport } from '@/models/entities/abuse-user-report'; -import { Signin } from '@/models/entities/signin'; -import { Page } from '@/models/entities/page'; -import { Packed } from '@/misc/schema'; - -//#region Stream type-body definitions -export interface InternalStreamTypes { - antennaCreated: Antenna; - antennaDeleted: Antenna; - antennaUpdated: Antenna; -} - -export interface BroadcastTypes { - emojiAdded: { - emoji: Packed<'Emoji'>; - }; -} - -export interface UserStreamTypes { - terminate: {}; - followChannel: Channel; - unfollowChannel: Channel; - updateUserProfile: UserProfile; - mute: User; - unmute: User; - follow: Packed<'User'>; - unfollow: Packed<'User'>; - userAdded: Packed<'User'>; -} - -export interface MainStreamTypes { - notification: Packed<'Notification'>; - mention: Packed<'Note'>; - reply: Packed<'Note'>; - renote: Packed<'Note'>; - follow: Packed<'User'>; - followed: Packed<'User'>; - unfollow: Packed<'User'>; - meUpdated: Packed<'User'>; - pageEvent: { - pageId: Page['id']; - event: string; - var: any; - userId: User['id']; - user: Packed<'User'>; - }; - urlUploadFinished: { - marker?: string | null; - file: Packed<'DriveFile'>; - }; - readAllNotifications: undefined; - unreadNotification: Packed<'Notification'>; - unreadMention: Note['id']; - readAllUnreadMentions: undefined; - unreadSpecifiedNote: Note['id']; - readAllUnreadSpecifiedNotes: undefined; - readAllMessagingMessages: undefined; - messagingMessage: Packed<'MessagingMessage'>; - unreadMessagingMessage: Packed<'MessagingMessage'>; - readAllAntennas: undefined; - unreadAntenna: Antenna; - readAllAnnouncements: undefined; - readAllChannels: undefined; - unreadChannel: Note['id']; - myTokenRegenerated: undefined; - reversiNoInvites: undefined; - reversiInvited: Packed<'ReversiMatching'>; - signin: Signin; - registryUpdated: { - scope?: string[]; - key: string; - value: any | null; - }; - driveFileCreated: Packed<'DriveFile'>; - readAntenna: Antenna; -} - -export interface DriveStreamTypes { - fileCreated: Packed<'DriveFile'>; - fileDeleted: DriveFile['id']; - fileUpdated: Packed<'DriveFile'>; - folderCreated: Packed<'DriveFolder'>; - folderDeleted: DriveFolder['id']; - folderUpdated: Packed<'DriveFolder'>; -} - -export interface NoteStreamTypes { - pollVoted: { - choice: number; - userId: User['id']; - }; - deleted: { - deletedAt: Date; - }; - reacted: { - reaction: string; - emoji?: Emoji; - userId: User['id']; - }; - unreacted: { - reaction: string; - userId: User['id']; - }; -} -type NoteStreamEventTypes = { - [key in keyof NoteStreamTypes]: { - id: Note['id']; - body: NoteStreamTypes[key]; - }; -}; - -export interface ChannelStreamTypes { - typing: User['id']; -} - -export interface UserListStreamTypes { - userAdded: Packed<'User'>; - userRemoved: Packed<'User'>; -} - -export interface AntennaStreamTypes { - note: Note; -} - -export interface MessagingStreamTypes { - read: MessagingMessage['id'][]; - typing: User['id']; - message: Packed<'MessagingMessage'>; - deleted: MessagingMessage['id']; -} - -export interface GroupMessagingStreamTypes { - read: { - ids: MessagingMessage['id'][]; - userId: User['id']; - }; - typing: User['id']; - message: Packed<'MessagingMessage'>; - deleted: MessagingMessage['id']; -} - -export interface MessagingIndexStreamTypes { - read: MessagingMessage['id'][]; - message: Packed<'MessagingMessage'>; -} - -export interface ReversiStreamTypes { - matched: Packed<'ReversiGame'>; - invited: Packed<'ReversiMatching'>; -} - -export interface ReversiGameStreamTypes { - started: Packed<'ReversiGame'>; - ended: { - winnerId?: User['id'] | null, - game: Packed<'ReversiGame'>; - }; - updateSettings: { - key: string; - value: FIXME; - }; - initForm: { - userId: User['id']; - form: FIXME; - }; - updateForm: { - userId: User['id']; - id: string; - value: FIXME; - }; - message: { - userId: User['id']; - message: FIXME; - }; - changeAccepts: { - user1: boolean; - user2: boolean; - }; - set: { - at: Date; - color: boolean; - pos: number; - next: boolean; - }; - watching: User['id']; -} - -export interface AdminStreamTypes { - newAbuseUserReport: { - id: AbuseUserReport['id']; - targetUserId: User['id'], - reporterId: User['id'], - comment: string; - }; -} -//#endregion - -// 辞書(interface or type)から{ type, body }ユニオンを定義 -// https://stackoverflow.com/questions/49311989/can-i-infer-the-type-of-a-value-using-extends-keyof-type -// VS Codeの展開を防止するためにEvents型を定義 -type Events<T extends object> = { [K in keyof T]: { type: K; body: T[K]; } }; -type EventUnionFromDictionary< - T extends object, - U = Events<T> -> = U[keyof U]; - -// name/messages(spec) pairs dictionary -export type StreamMessages = { - internal: { - name: 'internal'; - payload: EventUnionFromDictionary<InternalStreamTypes>; - }; - broadcast: { - name: 'broadcast'; - payload: EventUnionFromDictionary<BroadcastTypes>; - }; - user: { - name: `user:${User['id']}`; - payload: EventUnionFromDictionary<UserStreamTypes>; - }; - main: { - name: `mainStream:${User['id']}`; - payload: EventUnionFromDictionary<MainStreamTypes>; - }; - drive: { - name: `driveStream:${User['id']}`; - payload: EventUnionFromDictionary<DriveStreamTypes>; - }; - note: { - name: `noteStream:${Note['id']}`; - payload: EventUnionFromDictionary<NoteStreamEventTypes>; - }; - channel: { - name: `channelStream:${Channel['id']}`; - payload: EventUnionFromDictionary<ChannelStreamTypes>; - }; - userList: { - name: `userListStream:${UserList['id']}`; - payload: EventUnionFromDictionary<UserListStreamTypes>; - }; - antenna: { - name: `antennaStream:${Antenna['id']}`; - payload: EventUnionFromDictionary<AntennaStreamTypes>; - }; - messaging: { - name: `messagingStream:${User['id']}-${User['id']}`; - payload: EventUnionFromDictionary<MessagingStreamTypes>; - }; - groupMessaging: { - name: `messagingStream:${UserGroup['id']}`; - payload: EventUnionFromDictionary<GroupMessagingStreamTypes>; - }; - messagingIndex: { - name: `messagingIndexStream:${User['id']}`; - payload: EventUnionFromDictionary<MessagingIndexStreamTypes>; - }; - reversi: { - name: `reversiStream:${User['id']}`; - payload: EventUnionFromDictionary<ReversiStreamTypes>; - }; - reversiGame: { - name: `reversiGameStream:${ReversiGame['id']}`; - payload: EventUnionFromDictionary<ReversiGameStreamTypes>; - }; - admin: { - name: `adminStream:${User['id']}`; - payload: EventUnionFromDictionary<AdminStreamTypes>; - }; - notes: { - name: 'notesStream'; - payload: Packed<'Note'>; - }; -}; - -// API event definitions -// ストリームごとのEmitterの辞書を用意 -type EventEmitterDictionary = { [x in keyof StreamMessages]: Emitter<EventEmitter, { [y in StreamMessages[x]['name']]: (e: StreamMessages[x]['payload']) => void }> }; -// 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection -type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; -// Emitter辞書から共用体型を作り、UnionToIntersectionで交差型にする -export type StreamEventEmitter = UnionToIntersection<EventEmitterDictionary[keyof StreamMessages]>; -// { [y in name]: (e: spec) => void }をまとめてその交差型をEmitterにかけるとts(2590)にひっかかる - -// provide stream channels union -export type StreamChannels = StreamMessages[keyof StreamMessages]['name']; diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts deleted file mode 100644 index 8808bc9860..0000000000 --- a/src/server/api/streaming.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as http from 'http'; -import * as websocket from 'websocket'; - -import MainStreamConnection from './stream/index'; -import { ParsedUrlQuery } from 'querystring'; -import authenticate from './authenticate'; -import { EventEmitter } from 'events'; -import { subsdcriber as redisClient } from '../../db/redis'; -import { Users } from '@/models/index'; - -module.exports = (server: http.Server) => { - // Init websocket server - const ws = new websocket.server({ - httpServer: server - }); - - ws.on('request', async (request) => { - const q = request.resourceURL.query as ParsedUrlQuery; - - // TODO: トークンが間違ってるなどしてauthenticateに失敗したら - // コネクション切断するなりエラーメッセージ返すなりする - // (現状はエラーがキャッチされておらずサーバーのログに流れて邪魔なので) - const [user, app] = await authenticate(q.i as string); - - if (user?.isSuspended) { - request.reject(400); - return; - } - - const connection = request.accept(); - - const ev = new EventEmitter(); - - async function onRedisMessage(_: string, data: string) { - const parsed = JSON.parse(data); - ev.emit(parsed.channel, parsed.message); - } - - redisClient.on('message', onRedisMessage); - - const main = new MainStreamConnection(connection, ev, user, app); - - const intervalId = user ? setInterval(() => { - Users.update(user.id, { - lastActiveDate: new Date(), - }); - }, 1000 * 60 * 5) : null; - if (user) { - Users.update(user.id, { - lastActiveDate: new Date(), - }); - } - - connection.once('close', () => { - ev.removeAllListeners(); - main.dispose(); - redisClient.off('message', onRedisMessage); - if (intervalId) clearInterval(intervalId); - }); - - connection.on('message', async (data) => { - if (data.utf8Data === 'ping') { - connection.send('pong'); - } - }); - }); -}; |