v12 (#5712)
Co-authored-by: MeiMei <30769358+mei23@users.noreply.github.com> Co-authored-by: Satsuki Yanagi <17376330+u1-liquid@users.noreply.github.com>
This commit is contained in:
@ -1,8 +1,8 @@
|
||||
import { publishMainStream } from '../../../services/stream';
|
||||
import { User } from '../../../models/entities/user';
|
||||
import { Notification } from '../../../models/entities/notification';
|
||||
import { Mutings, Notifications } from '../../../models';
|
||||
import { In, Not } from 'typeorm';
|
||||
import { Notifications, Users } from '../../../models';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
/**
|
||||
* Mark notifications as read
|
||||
@ -11,11 +11,6 @@ export async function readNotification(
|
||||
userId: User['id'],
|
||||
notificationIds: Notification['id'][]
|
||||
) {
|
||||
const mute = await Mutings.find({
|
||||
muterId: userId
|
||||
});
|
||||
const mutedUserIds = mute.map(m => m.muteeId);
|
||||
|
||||
// Update documents
|
||||
await Notifications.update({
|
||||
id: In(notificationIds),
|
||||
@ -24,14 +19,7 @@ export async function readNotification(
|
||||
isRead: true
|
||||
});
|
||||
|
||||
// Calc count of my unread notifications
|
||||
const count = await Notifications.count({
|
||||
notifieeId: userId,
|
||||
...(mutedUserIds.length > 0 ? { notifierId: Not(In(mutedUserIds)) } : {}),
|
||||
isRead: false
|
||||
});
|
||||
|
||||
if (count === 0) {
|
||||
if (!await Users.getHasUnreadNotification(userId)) {
|
||||
// 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行
|
||||
publishMainStream(userId, 'readAllNotifications');
|
||||
}
|
||||
|
@ -24,7 +24,10 @@ export default function(ctx: Koa.Context, user: ILocalUser, redirect = false) {
|
||||
|
||||
ctx.redirect(config.url);
|
||||
} else {
|
||||
ctx.body = { i: user.token };
|
||||
ctx.body = {
|
||||
id: user.id,
|
||||
i: user.token
|
||||
};
|
||||
ctx.status = 200;
|
||||
}
|
||||
|
||||
|
104
src/server/api/common/signup.ts
Normal file
104
src/server/api/common/signup.ts
Normal file
@ -0,0 +1,104 @@
|
||||
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';
|
||||
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';
|
||||
import { UsedUsername } from '../../../models/entities/used-username';
|
||||
|
||||
export async function signup(username: User['username'], password: UserProfile['password'], host: string | null = null) {
|
||||
// Validate username
|
||||
if (!Users.validateLocalUsername.ok(username)) {
|
||||
throw new Error('INVALID_USERNAME');
|
||||
}
|
||||
|
||||
// Validate password
|
||||
if (!Users.validatePassword.ok(password)) {
|
||||
throw new Error('INVALID_PASSWORD');
|
||||
}
|
||||
|
||||
const usersCount = await Users.count({});
|
||||
|
||||
// Generate hash of password
|
||||
const salt = await bcrypt.genSalt(8);
|
||||
const 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: usersCount === 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,
|
||||
autoWatch: false,
|
||||
password: hash,
|
||||
}));
|
||||
|
||||
await transactionalEntityManager.save(new UsedUsername({
|
||||
createdAt: new Date(),
|
||||
username: username.toLowerCase(),
|
||||
}));
|
||||
});
|
||||
|
||||
usersChart.update(account, true);
|
||||
|
||||
return { account, secret };
|
||||
}
|
33
src/server/api/endpoints/admin/accounts/create.ts
Normal file
33
src/server/api/endpoints/admin/accounts/create.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import define from '../../../define';
|
||||
import { Users } from '../../../../../models';
|
||||
import { signup } from '../../../common/signup';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
params: {
|
||||
username: {
|
||||
validator: Users.validateLocalUsername,
|
||||
},
|
||||
|
||||
password: {
|
||||
validator: Users.validatePassword,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
const noUsers = (await Users.count({})) === 0;
|
||||
if (!noUsers && me == null) throw new Error('access denied');
|
||||
|
||||
const { account, secret } = await signup(ps.username, ps.password);
|
||||
|
||||
const res = await Users.pack(account, account, {
|
||||
detail: true,
|
||||
includeSecrets: true
|
||||
});
|
||||
|
||||
(res as any).token = secret;
|
||||
|
||||
return res;
|
||||
});
|
36
src/server/api/endpoints/admin/announcements/create.ts
Normal file
36
src/server/api/endpoints/admin/announcements/create.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { Announcements } from '../../../../../models';
|
||||
import { genId } from '../../../../../misc/gen-id';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
params: {
|
||||
title: {
|
||||
validator: $.str.min(1)
|
||||
},
|
||||
text: {
|
||||
validator: $.str.min(1)
|
||||
},
|
||||
imageUrl: {
|
||||
validator: $.nullable.str.min(1)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
});
|
34
src/server/api/endpoints/admin/announcements/delete.ts
Normal file
34
src/server/api/endpoints/admin/announcements/delete.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { ID } from '../../../../../misc/cafy-id';
|
||||
import { Announcements } from '../../../../../models';
|
||||
import { ApiError } from '../../../error';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
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);
|
||||
});
|
41
src/server/api/endpoints/admin/announcements/list.ts
Normal file
41
src/server/api/endpoints/admin/announcements/list.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '../../../../../misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import { Announcements, AnnouncementReads } from '../../../../../models';
|
||||
import { makePaginationQuery } from '../../../common/make-pagination-query';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
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(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;
|
||||
});
|
48
src/server/api/endpoints/admin/announcements/update.ts
Normal file
48
src/server/api/endpoints/admin/announcements/update.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { ID } from '../../../../../misc/cafy-id';
|
||||
import { Announcements } from '../../../../../models';
|
||||
import { ApiError } from '../../../error';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
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,
|
||||
});
|
||||
});
|
62
src/server/api/endpoints/admin/emoji/list-remote.ts
Normal file
62
src/server/api/endpoints/admin/emoji/list-remote.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { Emojis } from '../../../../../models';
|
||||
import { toPuny } from '../../../../../misc/convert-host';
|
||||
import { makePaginationQuery } from '../../../common/make-pagination-query';
|
||||
import { ID } from '../../../../../misc/cafy-id';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
'ja-JP': 'カスタム絵文字を取得します。'
|
||||
},
|
||||
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
params: {
|
||||
host: {
|
||||
validator: $.optional.nullable.str,
|
||||
default: null as any
|
||||
},
|
||||
|
||||
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 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) });
|
||||
}
|
||||
|
||||
const emojis = await q
|
||||
.orderBy('emoji.category', 'ASC')
|
||||
.orderBy('emoji.name', 'ASC')
|
||||
.take(ps.limit!)
|
||||
.getMany();
|
||||
|
||||
return emojis.map(e => ({
|
||||
id: e.id,
|
||||
name: e.name,
|
||||
category: e.category,
|
||||
aliases: e.aliases,
|
||||
host: e.host,
|
||||
url: e.url
|
||||
}));
|
||||
});
|
@ -1,7 +1,8 @@
|
||||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { Emojis } from '../../../../../models';
|
||||
import { toPunyNullable } from '../../../../../misc/convert-host';
|
||||
import { makePaginationQuery } from '../../../common/make-pagination-query';
|
||||
import { ID } from '../../../../../misc/cafy-id';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
@ -14,23 +15,28 @@ export const meta = {
|
||||
requireModerator: true,
|
||||
|
||||
params: {
|
||||
host: {
|
||||
validator: $.optional.nullable.str,
|
||||
default: null as any
|
||||
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 emojis = await Emojis.find({
|
||||
where: {
|
||||
host: toPunyNullable(ps.host)
|
||||
},
|
||||
order: {
|
||||
category: 'ASC',
|
||||
name: 'ASC'
|
||||
}
|
||||
});
|
||||
const emojis = await makePaginationQuery(Emojis.createQueryBuilder('emoji'), ps.sinceId, ps.untilId)
|
||||
.andWhere(`emoji.host IS NULL`)
|
||||
.orderBy('emoji.category', 'ASC')
|
||||
.orderBy('emoji.name', 'ASC')
|
||||
.take(ps.limit!)
|
||||
.getMany();
|
||||
|
||||
return emojis.map(e => ({
|
||||
id: e.id,
|
||||
|
31
src/server/api/endpoints/admin/queue/deliver-delayed.ts
Normal file
31
src/server/api/endpoints/admin/queue/deliver-delayed.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import define from '../../../define';
|
||||
import { deliverQueue } from '../../../../../queue';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
params: {
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
});
|
31
src/server/api/endpoints/admin/queue/inbox-delayed.ts
Normal file
31
src/server/api/endpoints/admin/queue/inbox-delayed.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import define from '../../../define';
|
||||
import { inboxQueue } from '../../../../../queue';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
params: {
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
});
|
45
src/server/api/endpoints/admin/server-info.ts
Normal file
45
src/server/api/endpoints/admin/server-info.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import * as os from 'os';
|
||||
import * as si from 'systeminformation';
|
||||
import { getConnection } from 'typeorm';
|
||||
import define from '../../define';
|
||||
import redis from '../../../../db/redis';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: false,
|
||||
|
||||
desc: {
|
||||
},
|
||||
|
||||
tags: ['meta'],
|
||||
|
||||
params: {
|
||||
},
|
||||
};
|
||||
|
||||
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: redis.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
|
||||
}
|
||||
};
|
||||
});
|
@ -13,16 +13,9 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireAdmin: true,
|
||||
|
||||
params: {
|
||||
announcements: {
|
||||
validator: $.optional.nullable.arr($.obj()),
|
||||
desc: {
|
||||
'ja-JP': 'お知らせ'
|
||||
}
|
||||
},
|
||||
|
||||
disableRegistration: {
|
||||
validator: $.optional.nullable.bool,
|
||||
desc: {
|
||||
@ -44,13 +37,6 @@ export const meta = {
|
||||
}
|
||||
},
|
||||
|
||||
enableEmojiReaction: {
|
||||
validator: $.optional.nullable.bool,
|
||||
desc: {
|
||||
'ja-JP': '絵文字リアクションを有効にするか否か'
|
||||
}
|
||||
},
|
||||
|
||||
useStarForReactionFallback: {
|
||||
validator: $.optional.nullable.bool,
|
||||
desc: {
|
||||
@ -347,7 +333,7 @@ export const meta = {
|
||||
}
|
||||
},
|
||||
|
||||
ToSUrl: {
|
||||
tosUrl: {
|
||||
validator: $.optional.nullable.str,
|
||||
desc: {
|
||||
'ja-JP': '利用規約のURL'
|
||||
@ -413,10 +399,6 @@ export const meta = {
|
||||
export default define(meta, async (ps, me) => {
|
||||
const set = {} as Partial<Meta>;
|
||||
|
||||
if (ps.announcements) {
|
||||
set.announcements = ps.announcements;
|
||||
}
|
||||
|
||||
if (typeof ps.disableRegistration === 'boolean') {
|
||||
set.disableRegistration = ps.disableRegistration;
|
||||
}
|
||||
@ -429,10 +411,6 @@ export default define(meta, async (ps, me) => {
|
||||
set.disableGlobalTimeline = ps.disableGlobalTimeline;
|
||||
}
|
||||
|
||||
if (typeof ps.enableEmojiReaction === 'boolean') {
|
||||
set.enableEmojiReaction = ps.enableEmojiReaction;
|
||||
}
|
||||
|
||||
if (typeof ps.useStarForReactionFallback === 'boolean') {
|
||||
set.useStarForReactionFallback = ps.useStarForReactionFallback;
|
||||
}
|
||||
@ -601,8 +579,8 @@ export default define(meta, async (ps, me) => {
|
||||
set.swPrivateKey = ps.swPrivateKey;
|
||||
}
|
||||
|
||||
if (ps.ToSUrl !== undefined) {
|
||||
set.ToSUrl = ps.ToSUrl;
|
||||
if (ps.tosUrl !== undefined) {
|
||||
set.ToSUrl = ps.tosUrl;
|
||||
}
|
||||
|
||||
if (ps.repositoryUrl !== undefined) {
|
||||
|
42
src/server/api/endpoints/announcements.ts
Normal file
42
src/server/api/endpoints/announcements.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '../../../misc/cafy-id';
|
||||
import define from '../define';
|
||||
import { Announcements, AnnouncementReads } from '../../../models';
|
||||
import { makePaginationQuery } from '../common/make-pagination-query';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: false,
|
||||
|
||||
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(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 announcements;
|
||||
});
|
92
src/server/api/endpoints/antennas/create.ts
Normal file
92
src/server/api/endpoints/antennas/create.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import $ from 'cafy';
|
||||
import define from '../../define';
|
||||
import { genId } from '../../../../misc/gen-id';
|
||||
import { Antennas, UserLists } from '../../../../models';
|
||||
import { ID } from '../../../../misc/cafy-id';
|
||||
import { ApiError } from '../../error';
|
||||
|
||||
export const meta = {
|
||||
tags: ['antennas'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
params: {
|
||||
name: {
|
||||
validator: $.str.range(1, 100)
|
||||
},
|
||||
|
||||
src: {
|
||||
validator: $.str.or(['home', 'all', 'users', 'list'])
|
||||
},
|
||||
|
||||
userListId: {
|
||||
validator: $.nullable.optional.type(ID),
|
||||
},
|
||||
|
||||
keywords: {
|
||||
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'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
let userList;
|
||||
|
||||
if (ps.src === 'list') {
|
||||
userList = await UserLists.findOne({
|
||||
id: ps.userListId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (userList == null) {
|
||||
throw new ApiError(meta.errors.noSuchUserList);
|
||||
}
|
||||
}
|
||||
|
||||
const antenna = await Antennas.save({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
name: ps.name,
|
||||
src: ps.src,
|
||||
userListId: userList ? userList.id : null,
|
||||
keywords: ps.keywords,
|
||||
users: ps.users,
|
||||
caseSensitive: ps.caseSensitive,
|
||||
withReplies: ps.withReplies,
|
||||
withFile: ps.withFile,
|
||||
notify: ps.notify,
|
||||
});
|
||||
|
||||
return await Antennas.pack(antenna);
|
||||
});
|
40
src/server/api/endpoints/antennas/delete.ts
Normal file
40
src/server/api/endpoints/antennas/delete.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '../../../../misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { ApiError } from '../../error';
|
||||
import { Antennas } from '../../../../models';
|
||||
|
||||
export const meta = {
|
||||
tags: ['antennas'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
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);
|
||||
});
|
18
src/server/api/endpoints/antennas/list.ts
Normal file
18
src/server/api/endpoints/antennas/list.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import define from '../../define';
|
||||
import { Antennas } from '../../../../models';
|
||||
|
||||
export const meta = {
|
||||
tags: ['antennas', 'account'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:account',
|
||||
};
|
||||
|
||||
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)));
|
||||
});
|
72
src/server/api/endpoints/antennas/notes.ts
Normal file
72
src/server/api/endpoints/antennas/notes.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '../../../../misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { Antennas, Notes, AntennaNotes } from '../../../../models';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
|
||||
import { generateMuteQuery } from '../../common/generate-mute-query';
|
||||
import { ApiError } from '../../error';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account', 'notes', 'antennas'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
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'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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() })`)
|
||||
.leftJoinAndSelect('note.user', 'user')
|
||||
.setParameters(antennaQuery.getParameters());
|
||||
|
||||
generateVisibilityQuery(query, user);
|
||||
generateMuteQuery(query, user);
|
||||
|
||||
const notes = await query
|
||||
.take(ps.limit!)
|
||||
.getMany();
|
||||
|
||||
return await Notes.packMany(notes, user);
|
||||
});
|
41
src/server/api/endpoints/antennas/show.ts
Normal file
41
src/server/api/endpoints/antennas/show.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '../../../../misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { ApiError } from '../../error';
|
||||
import { Antennas } from '../../../../models';
|
||||
|
||||
export const meta = {
|
||||
tags: ['antennas', 'account'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:account',
|
||||
|
||||
params: {
|
||||
antennaId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchAntenna: {
|
||||
message: 'No such antenna.',
|
||||
code: 'NO_SUCH_ANTENNA',
|
||||
id: 'c06569fb-b025-4f23-b22d-1fcd20d2816b'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
108
src/server/api/endpoints/antennas/update.ts
Normal file
108
src/server/api/endpoints/antennas/update.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '../../../../misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { ApiError } from '../../error';
|
||||
import { Antennas, UserLists } from '../../../../models';
|
||||
|
||||
export const meta = {
|
||||
tags: ['antennas'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
params: {
|
||||
antennaId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
name: {
|
||||
validator: $.str.range(1, 100)
|
||||
},
|
||||
|
||||
src: {
|
||||
validator: $.str.or(['home', 'all', 'users', 'list'])
|
||||
},
|
||||
|
||||
userListId: {
|
||||
validator: $.nullable.optional.type(ID),
|
||||
},
|
||||
|
||||
keywords: {
|
||||
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'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
if (ps.src === 'list') {
|
||||
userList = await UserLists.findOne({
|
||||
id: ps.userListId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (userList == null) {
|
||||
throw new ApiError(meta.errors.noSuchUserList);
|
||||
}
|
||||
}
|
||||
|
||||
await Antennas.update(antenna.id, {
|
||||
name: ps.name,
|
||||
src: ps.src,
|
||||
userListId: userList ? userList.id : null,
|
||||
keywords: ps.keywords,
|
||||
users: ps.users,
|
||||
caseSensitive: ps.caseSensitive,
|
||||
withReplies: ps.withReplies,
|
||||
withFile: ps.withFile,
|
||||
notify: ps.notify,
|
||||
});
|
||||
|
||||
return await Antennas.pack(antenna.id);
|
||||
});
|
29
src/server/api/endpoints/clips/create.ts
Normal file
29
src/server/api/endpoints/clips/create.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import $ from 'cafy';
|
||||
import define from '../../define';
|
||||
import { genId } from '../../../../misc/gen-id';
|
||||
import { Clips } from '../../../../models';
|
||||
|
||||
export const meta = {
|
||||
tags: ['clips'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
params: {
|
||||
name: {
|
||||
validator: $.str.range(1, 100)
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const clip = await Clips.save({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
name: ps.name,
|
||||
});
|
||||
|
||||
return await Clips.pack(clip);
|
||||
});
|
40
src/server/api/endpoints/clips/delete.ts
Normal file
40
src/server/api/endpoints/clips/delete.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '../../../../misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { ApiError } from '../../error';
|
||||
import { Clips } from '../../../../models';
|
||||
|
||||
export const meta = {
|
||||
tags: ['clips'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
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);
|
||||
});
|
18
src/server/api/endpoints/clips/list.ts
Normal file
18
src/server/api/endpoints/clips/list.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import define from '../../define';
|
||||
import { Clips } from '../../../../models';
|
||||
|
||||
export const meta = {
|
||||
tags: ['clips', 'account'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:account',
|
||||
};
|
||||
|
||||
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)));
|
||||
});
|
67
src/server/api/endpoints/clips/notes.ts
Normal file
67
src/server/api/endpoints/clips/notes.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '../../../../misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { Clips, Notes } from '../../../../models';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
|
||||
import { generateMuteQuery } from '../../common/generate-mute-query';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account', 'notes', 'clips'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:account',
|
||||
|
||||
params: {
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchClip: {
|
||||
message: 'No such list.',
|
||||
code: 'NO_SUCH_CLIP',
|
||||
id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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 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() })`)
|
||||
.leftJoinAndSelect('note.user', 'user')
|
||||
.setParameters(clipQuery.getParameters());
|
||||
|
||||
generateVisibilityQuery(query, user);
|
||||
generateMuteQuery(query, user);
|
||||
|
||||
const notes = await query
|
||||
.take(ps.limit!)
|
||||
.getMany();
|
||||
|
||||
return await Notes.packMany(notes, user);
|
||||
});
|
41
src/server/api/endpoints/clips/show.ts
Normal file
41
src/server/api/endpoints/clips/show.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '../../../../misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { ApiError } from '../../error';
|
||||
import { Clips } from '../../../../models';
|
||||
|
||||
export const meta = {
|
||||
tags: ['clips', 'account'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:account',
|
||||
|
||||
params: {
|
||||
clipId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchClip: {
|
||||
message: 'No such clip.',
|
||||
code: 'NO_SUCH_CLIP',
|
||||
id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
// Fetch the clip
|
||||
const clip = await Clips.findOne({
|
||||
id: ps.clipId,
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
if (clip == null) {
|
||||
throw new ApiError(meta.errors.noSuchClip);
|
||||
}
|
||||
|
||||
return await Clips.pack(clip);
|
||||
});
|
49
src/server/api/endpoints/clips/update.ts
Normal file
49
src/server/api/endpoints/clips/update.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '../../../../misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { ApiError } from '../../error';
|
||||
import { Clips } from '../../../../models';
|
||||
|
||||
export const meta = {
|
||||
tags: ['clips'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
params: {
|
||||
clipId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
name: {
|
||||
validator: $.str.range(1, 100),
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchClip: {
|
||||
message: 'No such clip.',
|
||||
code: 'NO_SUCH_CLIP',
|
||||
id: 'b4d92d70-b216-46fa-9a3f-a8c811699257'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
return await Clips.pack(clip.id);
|
||||
});
|
51
src/server/api/endpoints/federation/followers.ts
Normal file
51
src/server/api/endpoints/federation/followers.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '../../../../misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { Followings } from '../../../../models';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
|
||||
requireCredential: false,
|
||||
|
||||
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 });
|
||||
});
|
51
src/server/api/endpoints/federation/following.ts
Normal file
51
src/server/api/endpoints/federation/following.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '../../../../misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { Followings } from '../../../../models';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
|
||||
requireCredential: false,
|
||||
|
||||
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 });
|
||||
});
|
@ -9,6 +9,10 @@ export const meta = {
|
||||
requireCredential: false,
|
||||
|
||||
params: {
|
||||
host: {
|
||||
validator: $.optional.nullable.str,
|
||||
},
|
||||
|
||||
blocked: {
|
||||
validator: $.optional.nullable.bool,
|
||||
},
|
||||
@ -17,7 +21,19 @@ export const meta = {
|
||||
validator: $.optional.nullable.bool,
|
||||
},
|
||||
|
||||
markedAsClosed: {
|
||||
suspended: {
|
||||
validator: $.optional.nullable.bool,
|
||||
},
|
||||
|
||||
federating: {
|
||||
validator: $.optional.nullable.bool,
|
||||
},
|
||||
|
||||
subscribing: {
|
||||
validator: $.optional.nullable.bool,
|
||||
},
|
||||
|
||||
publishing: {
|
||||
validator: $.optional.nullable.bool,
|
||||
},
|
||||
|
||||
@ -41,6 +57,8 @@ 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;
|
||||
@ -78,14 +96,42 @@ export default define(meta, async (ps, me) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof ps.markedAsClosed === 'boolean') {
|
||||
if (ps.markedAsClosed) {
|
||||
query.andWhere('instance.isMarkedAsClosed = TRUE');
|
||||
if (typeof ps.suspended === 'boolean') {
|
||||
if (ps.suspended) {
|
||||
query.andWhere('instance.isSuspended = TRUE');
|
||||
} else {
|
||||
query.andWhere('instance.isMarkedAsClosed = FALSE');
|
||||
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;
|
||||
|
51
src/server/api/endpoints/federation/users.ts
Normal file
51
src/server/api/endpoints/federation/users.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '../../../../misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { Users } from '../../../../models';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
|
||||
requireCredential: false,
|
||||
|
||||
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 });
|
||||
});
|
@ -42,12 +42,12 @@ export const meta = {
|
||||
},
|
||||
|
||||
includeTypes: {
|
||||
validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'])),
|
||||
validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted'])),
|
||||
default: [] as string[]
|
||||
},
|
||||
|
||||
excludeTypes: {
|
||||
validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'])),
|
||||
validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted'])),
|
||||
default: [] as string[]
|
||||
}
|
||||
},
|
||||
|
60
src/server/api/endpoints/i/read-announcement.ts
Normal file
60
src/server/api/endpoints/i/read-announcement.ts
Normal file
@ -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';
|
||||
import { publishMainStream } from '../../../../services/stream';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
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.save({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
announcementId: ps.announcementId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (!await Users.getHasUnreadAnnouncement(user.id)) {
|
||||
publishMainStream(user.id, 'readAllAnnouncements');
|
||||
}
|
||||
});
|
@ -1,11 +1,8 @@
|
||||
import $ from 'cafy';
|
||||
import * as os from 'os';
|
||||
import config from '../../../config';
|
||||
import define from '../define';
|
||||
import { fetchMeta } from '../../../misc/fetch-meta';
|
||||
import { Emojis } from '../../../models';
|
||||
import { getConnection } from 'typeorm';
|
||||
import redis from '../../../db/redis';
|
||||
import { Emojis, Users } from '../../../models';
|
||||
import { DB_MAX_NOTE_TEXT_LENGTH } from '../../../misc/hard-limits';
|
||||
|
||||
export const meta = {
|
||||
@ -83,11 +80,6 @@ export const meta = {
|
||||
optional: false as const, nullable: false as const,
|
||||
description: 'Whether disabled GTL.',
|
||||
},
|
||||
enableEmojiReaction: {
|
||||
type: 'boolean' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
description: 'Whether enabled emoji reaction.',
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -119,27 +111,15 @@ export default define(meta, async (ps, me) => {
|
||||
uri: config.url,
|
||||
description: instance.description,
|
||||
langs: instance.langs,
|
||||
ToSUrl: instance.ToSUrl,
|
||||
tosUrl: instance.ToSUrl,
|
||||
repositoryUrl: instance.repositoryUrl,
|
||||
feedbackUrl: instance.feedbackUrl,
|
||||
|
||||
secure: config.https != null,
|
||||
machine: os.hostname(),
|
||||
os: os.platform(),
|
||||
node: process.version,
|
||||
psql: await getConnection().query('SHOW server_version').then(x => x[0].server_version),
|
||||
redis: redis.server_info.redis_version,
|
||||
|
||||
cpu: {
|
||||
model: os.cpus()[0].model,
|
||||
cores: os.cpus().length
|
||||
},
|
||||
|
||||
announcements: instance.announcements || [],
|
||||
disableRegistration: instance.disableRegistration,
|
||||
disableLocalTimeline: instance.disableLocalTimeline,
|
||||
disableGlobalTimeline: instance.disableGlobalTimeline,
|
||||
enableEmojiReaction: instance.enableEmojiReaction,
|
||||
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
|
||||
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
|
||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||
@ -159,6 +139,7 @@ export default define(meta, async (ps, me) => {
|
||||
category: e.category,
|
||||
url: e.url,
|
||||
})),
|
||||
requireSetup: (await Users.count({})) === 0,
|
||||
enableEmail: instance.enableEmail,
|
||||
|
||||
enableTwitterIntegration: instance.enableTwitterIntegration,
|
||||
@ -183,7 +164,7 @@ export default define(meta, async (ps, me) => {
|
||||
};
|
||||
}
|
||||
|
||||
if (me && (me.isAdmin || me.isModerator)) {
|
||||
if (me && me.isAdmin) {
|
||||
response.useStarForReactionFallback = instance.useStarForReactionFallback;
|
||||
response.pinnedUsers = instance.pinnedUsers;
|
||||
response.hiddenTags = instance.hiddenTags;
|
||||
|
@ -113,23 +113,6 @@ export const meta = {
|
||||
}
|
||||
},
|
||||
|
||||
geo: {
|
||||
validator: $.optional.nullable.obj({
|
||||
coordinates: $.arr().length(2)
|
||||
.item(0, $.num.range(-180, 180))
|
||||
.item(1, $.num.range(-90, 90)),
|
||||
altitude: $.nullable.num,
|
||||
accuracy: $.nullable.num,
|
||||
altitudeAccuracy: $.nullable.num,
|
||||
heading: $.nullable.num.range(0, 360),
|
||||
speed: $.nullable.num
|
||||
}).strict(),
|
||||
desc: {
|
||||
'ja-JP': '位置情報'
|
||||
},
|
||||
ref: 'geo'
|
||||
},
|
||||
|
||||
fileIds: {
|
||||
validator: $.optional.arr($.type(ID)).unique().range(1, 4),
|
||||
desc: {
|
||||
@ -308,7 +291,6 @@ export default define(meta, async (ps, user, app) => {
|
||||
apMentions: ps.noExtractMentions ? [] : undefined,
|
||||
apHashtags: ps.noExtractHashtags ? [] : undefined,
|
||||
apEmojis: ps.noExtractEmojis ? [] : undefined,
|
||||
geo: ps.geo
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -15,12 +15,17 @@ export const meta = {
|
||||
|
||||
params: {
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 30),
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10,
|
||||
desc: {
|
||||
'ja-JP': '最大数'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
offset: {
|
||||
validator: $.optional.num.min(0),
|
||||
default: 0
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
@ -35,6 +40,7 @@ export const meta = {
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const max = 30;
|
||||
const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで
|
||||
|
||||
const query = Notes.createQueryBuilder('note')
|
||||
@ -46,7 +52,14 @@ export default define(meta, async (ps, user) => {
|
||||
|
||||
if (user) generateMuteQuery(query, user);
|
||||
|
||||
const notes = await query.orderBy('note.score', 'DESC').take(ps.limit!).getMany();
|
||||
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);
|
||||
});
|
||||
|
@ -1,11 +1,13 @@
|
||||
import $ from 'cafy';
|
||||
import es from '../../../../db/elasticsearch';
|
||||
import define from '../../define';
|
||||
import { ApiError } from '../../error';
|
||||
import { Notes } from '../../../../models';
|
||||
import { In } from 'typeorm';
|
||||
import { ID } from '../../../../misc/cafy-id';
|
||||
import config from '../../../../config';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
|
||||
import { generateMuteQuery } from '../../common/generate-mute-query';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
@ -22,16 +24,19 @@ export const meta = {
|
||||
validator: $.str
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10
|
||||
},
|
||||
|
||||
offset: {
|
||||
validator: $.optional.num.min(0),
|
||||
default: 0
|
||||
},
|
||||
|
||||
host: {
|
||||
validator: $.optional.nullable.str,
|
||||
default: undefined
|
||||
@ -54,74 +59,80 @@ export const meta = {
|
||||
},
|
||||
|
||||
errors: {
|
||||
searchingNotAvailable: {
|
||||
message: 'Searching not available.',
|
||||
code: 'SEARCHING_NOT_AVAILABLE',
|
||||
id: '7ee9c119-16a1-479f-a6fd-6fab00ed946f'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
if (es == null) throw new ApiError(meta.errors.searchingNotAvailable);
|
||||
if (es == null) {
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.andWhere('note.text ILIKE :q', { q: `%${ps.query}%` })
|
||||
.leftJoinAndSelect('note.user', 'user');
|
||||
|
||||
const userQuery = ps.userId != null ? [{
|
||||
term: {
|
||||
userId: ps.userId
|
||||
}
|
||||
}] : [];
|
||||
generateVisibilityQuery(query, me);
|
||||
if (me) generateMuteQuery(query, me);
|
||||
|
||||
const hostQuery = ps.userId == null ?
|
||||
ps.host === null ? [{
|
||||
bool: {
|
||||
must_not: {
|
||||
exists: {
|
||||
field: 'userHost'
|
||||
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]
|
||||
}] : 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)
|
||||
},
|
||||
sort: [{
|
||||
_doc: 'desc'
|
||||
}]
|
||||
}
|
||||
});
|
||||
order: {
|
||||
id: -1
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
return await Notes.packMany(notes, me);
|
||||
}
|
||||
});
|
||||
|
101
src/server/api/endpoints/users/search-by-username-and-host.ts
Normal file
101
src/server/api/endpoints/users/search-by-username-and-host.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import $ from 'cafy';
|
||||
import define from '../../define';
|
||||
import { Users } from '../../../../models';
|
||||
import { User } from '../../../../models/entities/user';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
'ja-JP': 'ユーザーを検索します。'
|
||||
},
|
||||
|
||||
tags: ['users'],
|
||||
|
||||
requireCredential: false,
|
||||
|
||||
params: {
|
||||
username: {
|
||||
validator: $.optional.nullable.str,
|
||||
desc: {
|
||||
'ja-JP': 'クエリ'
|
||||
}
|
||||
},
|
||||
|
||||
host: {
|
||||
validator: $.optional.nullable.str,
|
||||
desc: {
|
||||
'ja-JP': 'クエリ'
|
||||
}
|
||||
},
|
||||
|
||||
offset: {
|
||||
validator: $.optional.num.min(0),
|
||||
default: 0,
|
||||
desc: {
|
||||
'ja-JP': 'オフセット'
|
||||
}
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10,
|
||||
desc: {
|
||||
'ja-JP': '取得する数'
|
||||
}
|
||||
},
|
||||
|
||||
detail: {
|
||||
validator: $.optional.bool,
|
||||
default: true,
|
||||
desc: {
|
||||
'ja-JP': '詳細なユーザー情報を含めるか否か'
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
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) => {
|
||||
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() + '%' })
|
||||
}
|
||||
|
||||
const users = await q.take(ps.limit!).skip(ps.offset).getMany();
|
||||
|
||||
return await Users.packMany(users, me, { detail: ps.detail });
|
||||
} else {
|
||||
let users = await Users.createQueryBuilder('user')
|
||||
.where('user.host IS NULL')
|
||||
.andWhere('user.isSuspended = FALSE')
|
||||
.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' })
|
||||
.take(ps.limit!)
|
||||
.skip(ps.offset)
|
||||
.getMany();
|
||||
|
||||
if (users.length < ps.limit!) {
|
||||
const otherUsers = await Users.createQueryBuilder('user')
|
||||
.where('user.host IS NOT NULL')
|
||||
.andWhere('user.isSuspended = FALSE')
|
||||
.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' })
|
||||
.take(ps.limit! - users.length)
|
||||
.getMany();
|
||||
|
||||
users = users.concat(otherUsers);
|
||||
}
|
||||
|
||||
return await Users.packMany(users, me, { detail: ps.detail });
|
||||
}
|
||||
});
|
@ -1,19 +1,8 @@
|
||||
import * as Koa from 'koa';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { generateKeyPair } from 'crypto';
|
||||
import generateUserToken from '../common/generate-native-user-token';
|
||||
import config from '../../../config';
|
||||
import { fetchMeta } from '../../../misc/fetch-meta';
|
||||
import * as recaptcha from 'recaptcha-promise';
|
||||
import { Users, Signins, RegistrationTickets, UsedUsernames } from '../../../models';
|
||||
import { genId } from '../../../misc/gen-id';
|
||||
import { usersChart } from '../../../services/chart';
|
||||
import { User } from '../../../models/entities/user';
|
||||
import { UserKeypair } from '../../../models/entities/user-keypair';
|
||||
import { toPunyNullable } from '../../../misc/convert-host';
|
||||
import { UserProfile } from '../../../models/entities/user-profile';
|
||||
import { getConnection } from 'typeorm';
|
||||
import { UsedUsername } from '../../../models/entities/used-username';
|
||||
import { Users, RegistrationTickets } from '../../../models';
|
||||
import { signup } from '../common/signup';
|
||||
|
||||
export default async (ctx: Koa.Context) => {
|
||||
const body = ctx.request.body;
|
||||
@ -31,7 +20,6 @@ export default async (ctx: Koa.Context) => {
|
||||
|
||||
if (!success) {
|
||||
ctx.throw(400, 'recaptcha-failed');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,114 +46,18 @@ export default async (ctx: Koa.Context) => {
|
||||
RegistrationTickets.delete(ticket.id);
|
||||
}
|
||||
|
||||
// Validate username
|
||||
if (!Users.validateLocalUsername.ok(username)) {
|
||||
ctx.status = 400;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { account, secret } = await signup(username, password, host);
|
||||
|
||||
// Validate password
|
||||
if (!Users.validatePassword.ok(password)) {
|
||||
ctx.status = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
const usersCount = await Users.count({});
|
||||
|
||||
// Generate hash of password
|
||||
const salt = await bcrypt.genSalt(8);
|
||||
const hash = await bcrypt.hash(password, salt);
|
||||
|
||||
// Generate secret
|
||||
const secret = generateUserToken();
|
||||
|
||||
// Check username duplication
|
||||
if (await Users.findOne({ usernameLower: username.toLowerCase(), host: null })) {
|
||||
ctx.status = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check deleted username duplication
|
||||
if (await UsedUsernames.findOne({ username: username.toLowerCase() })) {
|
||||
ctx.status = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
const res = await Users.pack(account, account, {
|
||||
detail: true,
|
||||
includeSecrets: true
|
||||
});
|
||||
|
||||
if (exist) throw new Error(' the username is already used');
|
||||
(res as any).token = secret;
|
||||
|
||||
account = await transactionalEntityManager.save(new User({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
username: username,
|
||||
usernameLower: username.toLowerCase(),
|
||||
host: toPunyNullable(host),
|
||||
token: secret,
|
||||
isAdmin: config.autoAdmin && usersCount === 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,
|
||||
autoWatch: false,
|
||||
password: hash,
|
||||
}));
|
||||
|
||||
await transactionalEntityManager.save(new UsedUsername({
|
||||
createdAt: new Date(),
|
||||
username: username.toLowerCase(),
|
||||
}));
|
||||
});
|
||||
|
||||
usersChart.update(account, true);
|
||||
|
||||
// Append signin history
|
||||
await Signins.save({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
userId: account.id,
|
||||
ip: ctx.ip,
|
||||
headers: ctx.headers,
|
||||
success: true
|
||||
});
|
||||
|
||||
const res = await Users.pack(account, account, {
|
||||
detail: true,
|
||||
includeSecrets: true
|
||||
});
|
||||
|
||||
(res as any).token = secret;
|
||||
|
||||
ctx.body = res;
|
||||
ctx.body = res;
|
||||
} catch (e) {
|
||||
ctx.throw(400, e);
|
||||
}
|
||||
};
|
||||
|
41
src/server/api/stream/channels/antenna.ts
Normal file
41
src/server/api/stream/channels/antenna.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import autobind from 'autobind-decorator';
|
||||
import Channel from '../channel';
|
||||
import { Notes } from '../../../../models';
|
||||
import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
|
||||
|
||||
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: any) {
|
||||
const { type, body } = data;
|
||||
|
||||
if (type === 'note') {
|
||||
const note = await Notes.pack(body.id, this.user, { detail: true });
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (shouldMuteThisNote(note, this.muting)) return;
|
||||
|
||||
this.send('note', note);
|
||||
} else {
|
||||
this.send(type, body);
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
public dispose() {
|
||||
// Unsubscribe events
|
||||
this.subscriber.off(`antennaStream:${this.antennaId}`, this.onEvent);
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
import autobind from 'autobind-decorator';
|
||||
import Channel from '../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'apLog';
|
||||
public static shouldShare = true;
|
||||
public static requireCredential = false;
|
||||
|
||||
@autobind
|
||||
public async init(params: any) {
|
||||
// Subscribe events
|
||||
this.subscriber.on('apLog', this.onLog);
|
||||
}
|
||||
|
||||
@autobind
|
||||
private async onLog(log: any) {
|
||||
this.send('log', log);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public dispose() {
|
||||
// Unsubscribe events
|
||||
this.subscriber.off('apLog', this.onLog);
|
||||
}
|
||||
}
|
@ -50,7 +50,7 @@ export default class extends Channel {
|
||||
detail: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (shouldMuteThisNote(note, this.muting)) return;
|
||||
|
@ -3,15 +3,14 @@ import homeTimeline from './home-timeline';
|
||||
import localTimeline from './local-timeline';
|
||||
import hybridTimeline from './hybrid-timeline';
|
||||
import globalTimeline from './global-timeline';
|
||||
import notesStats from './notes-stats';
|
||||
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 apLog from './ap-log';
|
||||
import admin from './admin';
|
||||
import gamesReversi from './games/reversi';
|
||||
import gamesReversiGame from './games/reversi-game';
|
||||
@ -22,15 +21,14 @@ export default {
|
||||
localTimeline,
|
||||
hybridTimeline,
|
||||
globalTimeline,
|
||||
notesStats,
|
||||
serverStats,
|
||||
queueStats,
|
||||
userList,
|
||||
antenna,
|
||||
messaging,
|
||||
messagingIndex,
|
||||
drive,
|
||||
hashtag,
|
||||
apLog,
|
||||
admin,
|
||||
gamesReversi,
|
||||
gamesReversiGame
|
||||
|
@ -1,38 +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 = 'notesStats';
|
||||
public static shouldShare = true;
|
||||
public static requireCredential = false;
|
||||
|
||||
@autobind
|
||||
public async init(params: any) {
|
||||
ev.addListener('notesStats', this.onStats);
|
||||
}
|
||||
|
||||
@autobind
|
||||
private onStats(stats: any) {
|
||||
this.send('stats', stats);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public onMessage(type: string, body: any) {
|
||||
switch (type) {
|
||||
case 'requestLog':
|
||||
ev.once(`notesStatsLog:${body.id}`, statsLog => {
|
||||
this.send('statsLog', statsLog);
|
||||
});
|
||||
ev.emit('requestNotesStatsLog', body.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
public dispose() {
|
||||
ev.removeListener('notesStats', this.onStats);
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ import { EventEmitter } from 'events';
|
||||
import { User } from '../../../models/entities/user';
|
||||
import { App } from '../../../models/entities/app';
|
||||
import { Users, Followings, Mutings } from '../../../models';
|
||||
import { ApiError } from '../error';
|
||||
|
||||
/**
|
||||
* Main stream connection
|
||||
@ -83,8 +84,16 @@ export default class Connection {
|
||||
// 呼び出し
|
||||
call(endpoint, user, this.app, payload.data).then(res => {
|
||||
this.sendMessageToWs(`api:${payload.id}`, { res });
|
||||
}).catch(e => {
|
||||
this.sendMessageToWs(`api:${payload.id}`, { e });
|
||||
}).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 } : {})
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -111,7 +120,7 @@ export default class Connection {
|
||||
this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage);
|
||||
}
|
||||
|
||||
if (payload.read && this.user) {
|
||||
if (this.user) {
|
||||
readNote(this.user.id, payload.id);
|
||||
}
|
||||
}
|
||||
|
@ -59,10 +59,9 @@ const nodeinfo2 = async () => {
|
||||
email: meta.maintainerEmail
|
||||
},
|
||||
langs: meta.langs,
|
||||
ToSUrl: meta.ToSUrl,
|
||||
tosUrl: meta.ToSUrl,
|
||||
repositoryUrl: meta.repositoryUrl,
|
||||
feedbackUrl: meta.feedbackUrl,
|
||||
announcements: meta.announcements,
|
||||
disableRegistration: meta.disableRegistration,
|
||||
disableLocalTimeline: meta.disableLocalTimeline,
|
||||
disableGlobalTimeline: meta.disableGlobalTimeline,
|
||||
|
@ -12,7 +12,6 @@ import * as send from 'koa-send';
|
||||
import * as glob from 'glob';
|
||||
import config from '../../config';
|
||||
import { licenseHtml } from '../../misc/license';
|
||||
import { copyright } from '../../const.json';
|
||||
import * as locales from '../../../locales';
|
||||
import * as nestedProperty from 'nested-property';
|
||||
|
||||
@ -48,7 +47,7 @@ async function genVars(lang: string): Promise<{ [key: string]: any }> {
|
||||
|
||||
vars['config'] = config;
|
||||
|
||||
vars['copyright'] = copyright;
|
||||
vars['copyright'] = '(c) Misskey';
|
||||
|
||||
vars['license'] = licenseHtml;
|
||||
|
||||
|
@ -31,6 +31,7 @@ const app = new Koa();
|
||||
app.use(views(__dirname + '/views', {
|
||||
extension: 'pug',
|
||||
options: {
|
||||
version: config.version,
|
||||
config
|
||||
}
|
||||
}));
|
||||
|
@ -10,7 +10,7 @@ html
|
||||
meta(charset='utf-8')
|
||||
meta(name='application-name' content='Misskey')
|
||||
meta(name='referrer' content='origin')
|
||||
meta(name='theme-color' content='#105779')
|
||||
meta(name='theme-color' content='#86b300')
|
||||
meta(property='og:site_name' content= instanceName || 'Misskey')
|
||||
meta(name='viewport' content='width=device-width, initial-scale=1')
|
||||
link(rel='icon' href= icon || '/favicon.ico')
|
||||
@ -30,12 +30,23 @@ html
|
||||
meta(property='og:image' content=img)
|
||||
|
||||
style
|
||||
include ./../../../../built/client/assets/init.css
|
||||
script
|
||||
include ./../../../../built/client/assets/boot.js
|
||||
|
||||
script
|
||||
include ./../../../../built/client/assets/safe.js
|
||||
include ./../../../../built/client/assets/style.css
|
||||
script(src=`/assets/app.${version}.js` async defer)
|
||||
script.
|
||||
const theme = localStorage.getItem('theme');
|
||||
if (theme) {
|
||||
for (const [k, v] of Object.entries(JSON.parse(theme))) {
|
||||
document.documentElement.style.setProperty(`--${k}`, v.toString());
|
||||
if (k === 'accent') {
|
||||
for (const tag of document.head.children) {
|
||||
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
|
||||
tag.setAttribute('content', v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body
|
||||
noscript: p
|
||||
|
Reference in New Issue
Block a user