parent
037837b551
commit
0e4a111f81
1714 changed files with 20803 additions and 11751 deletions
41
packages/backend/src/server/api/endpoints/i/2fa/done.ts
Normal file
41
packages/backend/src/server/api/endpoints/i/2fa/done.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
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
|
||||
});
|
||||
});
|
||||
150
packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
Normal file
150
packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
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
|
||||
};
|
||||
});
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
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
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
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
|
||||
};
|
||||
});
|
||||
54
packages/backend/src/server/api/endpoints/i/2fa/register.ts
Normal file
54
packages/backend/src/server/api/endpoints/i/2fa/register.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
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
|
||||
};
|
||||
});
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
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 {};
|
||||
});
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
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
|
||||
});
|
||||
});
|
||||
43
packages/backend/src/server/api/endpoints/i/apps.ts
Normal file
43
packages/backend/src/server/api/endpoints/i/apps.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
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,
|
||||
})));
|
||||
});
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
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
|
||||
})));
|
||||
});
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
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
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
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', {});
|
||||
});
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
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);
|
||||
});
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
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);
|
||||
});
|
||||
16
packages/backend/src/server/api/endpoints/i/export-mute.ts
Normal file
16
packages/backend/src/server/api/endpoints/i/export-mute.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
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);
|
||||
});
|
||||
16
packages/backend/src/server/api/endpoints/i/export-notes.ts
Normal file
16
packages/backend/src/server/api/endpoints/i/export-notes.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
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);
|
||||
});
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
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);
|
||||
});
|
||||
50
packages/backend/src/server/api/endpoints/i/favorites.ts
Normal file
50
packages/backend/src/server/api/endpoints/i/favorites.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
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);
|
||||
});
|
||||
57
packages/backend/src/server/api/endpoints/i/gallery/likes.ts
Normal file
57
packages/backend/src/server/api/endpoints/i/gallery/likes.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
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);
|
||||
});
|
||||
49
packages/backend/src/server/api/endpoints/i/gallery/posts.ts
Normal file
49
packages/backend/src/server/api/endpoints/i/gallery/posts.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
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);
|
||||
});
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
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'
|
||||
})
|
||||
};
|
||||
});
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
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);
|
||||
});
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
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);
|
||||
});
|
||||
60
packages/backend/src/server/api/endpoints/i/import-muting.ts
Normal file
60
packages/backend/src/server/api/endpoints/i/import-muting.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
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);
|
||||
});
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
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);
|
||||
});
|
||||
138
packages/backend/src/server/api/endpoints/i/notifications.ts
Normal file
138
packages/backend/src/server/api/endpoints/i/notifications.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
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);
|
||||
});
|
||||
57
packages/backend/src/server/api/endpoints/i/page-likes.ts
Normal file
57
packages/backend/src/server/api/endpoints/i/page-likes.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
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);
|
||||
});
|
||||
49
packages/backend/src/server/api/endpoints/i/pages.ts
Normal file
49
packages/backend/src/server/api/endpoints/i/pages.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
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);
|
||||
});
|
||||
59
packages/backend/src/server/api/endpoints/i/pin.ts
Normal file
59
packages/backend/src/server/api/endpoints/i/pin.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
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
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
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');
|
||||
});
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
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');
|
||||
});
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
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');
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
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);
|
||||
});
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
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;
|
||||
});
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
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,
|
||||
};
|
||||
});
|
||||
45
packages/backend/src/server/api/endpoints/i/registry/get.ts
Normal file
45
packages/backend/src/server/api/endpoints/i/registry/get.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
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;
|
||||
});
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
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;
|
||||
});
|
||||
28
packages/backend/src/server/api/endpoints/i/registry/keys.ts
Normal file
28
packages/backend/src/server/api/endpoints/i/registry/keys.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
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);
|
||||
});
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
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);
|
||||
});
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
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;
|
||||
});
|
||||
61
packages/backend/src/server/api/endpoints/i/registry/set.ts
Normal file
61
packages/backend/src/server/api/endpoints/i/registry/set.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
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
|
||||
});
|
||||
});
|
||||
31
packages/backend/src/server/api/endpoints/i/revoke-token.ts
Normal file
31
packages/backend/src/server/api/endpoints/i/revoke-token.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
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');
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
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)));
|
||||
});
|
||||
45
packages/backend/src/server/api/endpoints/i/unpin.ts
Normal file
45
packages/backend/src/server/api/endpoints/i/unpin.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
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
|
||||
});
|
||||
});
|
||||
94
packages/backend/src/server/api/endpoints/i/update-email.ts
Normal file
94
packages/backend/src/server/api/endpoints/i/update-email.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
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;
|
||||
});
|
||||
294
packages/backend/src/server/api/endpoints/i/update.ts
Normal file
294
packages/backend/src/server/api/endpoints/i/update.ts
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
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;
|
||||
});
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
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);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue