summaryrefslogtreecommitdiff
path: root/src/server/api
diff options
context:
space:
mode:
Diffstat (limited to 'src/server/api')
-rw-r--r--src/server/api/common/read-notification.ts18
-rw-r--r--src/server/api/common/signin.ts5
-rw-r--r--src/server/api/common/signup.ts104
-rw-r--r--src/server/api/endpoints/admin/accounts/create.ts33
-rw-r--r--src/server/api/endpoints/admin/announcements/create.ts36
-rw-r--r--src/server/api/endpoints/admin/announcements/delete.ts34
-rw-r--r--src/server/api/endpoints/admin/announcements/list.ts41
-rw-r--r--src/server/api/endpoints/admin/announcements/update.ts48
-rw-r--r--src/server/api/endpoints/admin/emoji/list-remote.ts62
-rw-r--r--src/server/api/endpoints/admin/emoji/list.ts32
-rw-r--r--src/server/api/endpoints/admin/queue/deliver-delayed.ts31
-rw-r--r--src/server/api/endpoints/admin/queue/inbox-delayed.ts31
-rw-r--r--src/server/api/endpoints/admin/server-info.ts45
-rw-r--r--src/server/api/endpoints/admin/update-meta.ts30
-rw-r--r--src/server/api/endpoints/announcements.ts42
-rw-r--r--src/server/api/endpoints/antennas/create.ts92
-rw-r--r--src/server/api/endpoints/antennas/delete.ts40
-rw-r--r--src/server/api/endpoints/antennas/list.ts18
-rw-r--r--src/server/api/endpoints/antennas/notes.ts72
-rw-r--r--src/server/api/endpoints/antennas/show.ts41
-rw-r--r--src/server/api/endpoints/antennas/update.ts108
-rw-r--r--src/server/api/endpoints/clips/create.ts29
-rw-r--r--src/server/api/endpoints/clips/delete.ts40
-rw-r--r--src/server/api/endpoints/clips/list.ts18
-rw-r--r--src/server/api/endpoints/clips/notes.ts67
-rw-r--r--src/server/api/endpoints/clips/show.ts41
-rw-r--r--src/server/api/endpoints/clips/update.ts49
-rw-r--r--src/server/api/endpoints/federation/followers.ts51
-rw-r--r--src/server/api/endpoints/federation/following.ts51
-rw-r--r--src/server/api/endpoints/federation/instances.ts56
-rw-r--r--src/server/api/endpoints/federation/users.ts51
-rw-r--r--src/server/api/endpoints/i/notifications.ts4
-rw-r--r--src/server/api/endpoints/i/read-announcement.ts60
-rw-r--r--src/server/api/endpoints/meta.ts27
-rw-r--r--src/server/api/endpoints/notes/create.ts18
-rw-r--r--src/server/api/endpoints/notes/featured.ts19
-rw-r--r--src/server/api/endpoints/notes/search.ts133
-rw-r--r--src/server/api/endpoints/users/search-by-username-and-host.ts101
-rw-r--r--src/server/api/private/signup.ts132
-rw-r--r--src/server/api/stream/channels/antenna.ts41
-rw-r--r--src/server/api/stream/channels/ap-log.ts25
-rw-r--r--src/server/api/stream/channels/hybrid-timeline.ts2
-rw-r--r--src/server/api/stream/channels/index.ts6
-rw-r--r--src/server/api/stream/channels/notes-stats.ts38
-rw-r--r--src/server/api/stream/index.ts15
45 files changed, 1679 insertions, 358 deletions
diff --git a/src/server/api/common/read-notification.ts b/src/server/api/common/read-notification.ts
index c8d43ba286..f686446c5c 100644
--- a/src/server/api/common/read-notification.ts
+++ b/src/server/api/common/read-notification.ts
@@ -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');
}
diff --git a/src/server/api/common/signin.ts b/src/server/api/common/signin.ts
index 66f89182d2..aa2786f8fc 100644
--- a/src/server/api/common/signin.ts
+++ b/src/server/api/common/signin.ts
@@ -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;
}
diff --git a/src/server/api/common/signup.ts b/src/server/api/common/signup.ts
new file mode 100644
index 0000000000..f0eb27e5e4
--- /dev/null
+++ b/src/server/api/common/signup.ts
@@ -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 };
+}
diff --git a/src/server/api/endpoints/admin/accounts/create.ts b/src/server/api/endpoints/admin/accounts/create.ts
new file mode 100644
index 0000000000..ac80b579b7
--- /dev/null
+++ b/src/server/api/endpoints/admin/accounts/create.ts
@@ -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;
+});
diff --git a/src/server/api/endpoints/admin/announcements/create.ts b/src/server/api/endpoints/admin/announcements/create.ts
new file mode 100644
index 0000000000..c1d48a7d38
--- /dev/null
+++ b/src/server/api/endpoints/admin/announcements/create.ts
@@ -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;
+});
diff --git a/src/server/api/endpoints/admin/announcements/delete.ts b/src/server/api/endpoints/admin/announcements/delete.ts
new file mode 100644
index 0000000000..284b4bf549
--- /dev/null
+++ b/src/server/api/endpoints/admin/announcements/delete.ts
@@ -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);
+});
diff --git a/src/server/api/endpoints/admin/announcements/list.ts b/src/server/api/endpoints/admin/announcements/list.ts
new file mode 100644
index 0000000000..f4e622144e
--- /dev/null
+++ b/src/server/api/endpoints/admin/announcements/list.ts
@@ -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;
+});
diff --git a/src/server/api/endpoints/admin/announcements/update.ts b/src/server/api/endpoints/admin/announcements/update.ts
new file mode 100644
index 0000000000..b65c3a4f93
--- /dev/null
+++ b/src/server/api/endpoints/admin/announcements/update.ts
@@ -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,
+ });
+});
diff --git a/src/server/api/endpoints/admin/emoji/list-remote.ts b/src/server/api/endpoints/admin/emoji/list-remote.ts
new file mode 100644
index 0000000000..0a3e74c333
--- /dev/null
+++ b/src/server/api/endpoints/admin/emoji/list-remote.ts
@@ -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
+ }));
+});
diff --git a/src/server/api/endpoints/admin/emoji/list.ts b/src/server/api/endpoints/admin/emoji/list.ts
index d2a5e7df0d..d525a659c0 100644
--- a/src/server/api/endpoints/admin/emoji/list.ts
+++ b/src/server/api/endpoints/admin/emoji/list.ts
@@ -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,
diff --git a/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/src/server/api/endpoints/admin/queue/deliver-delayed.ts
new file mode 100644
index 0000000000..d33837c099
--- /dev/null
+++ b/src/server/api/endpoints/admin/queue/deliver-delayed.ts
@@ -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;
+});
diff --git a/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/src/server/api/endpoints/admin/queue/inbox-delayed.ts
new file mode 100644
index 0000000000..643e22f10d
--- /dev/null
+++ b/src/server/api/endpoints/admin/queue/inbox-delayed.ts
@@ -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;
+});
diff --git a/src/server/api/endpoints/admin/server-info.ts b/src/server/api/endpoints/admin/server-info.ts
new file mode 100644
index 0000000000..f51040a2c8
--- /dev/null
+++ b/src/server/api/endpoints/admin/server-info.ts
@@ -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
+ }
+ };
+});
diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts
index bc37228d0a..65650f1295 100644
--- a/src/server/api/endpoints/admin/update-meta.ts
+++ b/src/server/api/endpoints/admin/update-meta.ts
@@ -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) {
diff --git a/src/server/api/endpoints/announcements.ts b/src/server/api/endpoints/announcements.ts
new file mode 100644
index 0000000000..c6050d6092
--- /dev/null
+++ b/src/server/api/endpoints/announcements.ts
@@ -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;
+});
diff --git a/src/server/api/endpoints/antennas/create.ts b/src/server/api/endpoints/antennas/create.ts
new file mode 100644
index 0000000000..0e00eda1a4
--- /dev/null
+++ b/src/server/api/endpoints/antennas/create.ts
@@ -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);
+});
diff --git a/src/server/api/endpoints/antennas/delete.ts b/src/server/api/endpoints/antennas/delete.ts
new file mode 100644
index 0000000000..6bf9165aed
--- /dev/null
+++ b/src/server/api/endpoints/antennas/delete.ts
@@ -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);
+});
diff --git a/src/server/api/endpoints/antennas/list.ts b/src/server/api/endpoints/antennas/list.ts
new file mode 100644
index 0000000000..3f9deff32f
--- /dev/null
+++ b/src/server/api/endpoints/antennas/list.ts
@@ -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)));
+});
diff --git a/src/server/api/endpoints/antennas/notes.ts b/src/server/api/endpoints/antennas/notes.ts
new file mode 100644
index 0000000000..b4c8e7e698
--- /dev/null
+++ b/src/server/api/endpoints/antennas/notes.ts
@@ -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);
+});
diff --git a/src/server/api/endpoints/antennas/show.ts b/src/server/api/endpoints/antennas/show.ts
new file mode 100644
index 0000000000..dd87de1dce
--- /dev/null
+++ b/src/server/api/endpoints/antennas/show.ts
@@ -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);
+});
diff --git a/src/server/api/endpoints/antennas/update.ts b/src/server/api/endpoints/antennas/update.ts
new file mode 100644
index 0000000000..28875d0f08
--- /dev/null
+++ b/src/server/api/endpoints/antennas/update.ts
@@ -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);
+});
diff --git a/src/server/api/endpoints/clips/create.ts b/src/server/api/endpoints/clips/create.ts
new file mode 100644
index 0000000000..a6761c5533
--- /dev/null
+++ b/src/server/api/endpoints/clips/create.ts
@@ -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);
+});
diff --git a/src/server/api/endpoints/clips/delete.ts b/src/server/api/endpoints/clips/delete.ts
new file mode 100644
index 0000000000..7e185e4652
--- /dev/null
+++ b/src/server/api/endpoints/clips/delete.ts
@@ -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);
+});
diff --git a/src/server/api/endpoints/clips/list.ts b/src/server/api/endpoints/clips/list.ts
new file mode 100644
index 0000000000..aa16a18d42
--- /dev/null
+++ b/src/server/api/endpoints/clips/list.ts
@@ -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)));
+});
diff --git a/src/server/api/endpoints/clips/notes.ts b/src/server/api/endpoints/clips/notes.ts
new file mode 100644
index 0000000000..4e76a4d1f3
--- /dev/null
+++ b/src/server/api/endpoints/clips/notes.ts
@@ -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);
+});
diff --git a/src/server/api/endpoints/clips/show.ts b/src/server/api/endpoints/clips/show.ts
new file mode 100644
index 0000000000..0766b3e929
--- /dev/null
+++ b/src/server/api/endpoints/clips/show.ts
@@ -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);
+});
diff --git a/src/server/api/endpoints/clips/update.ts b/src/server/api/endpoints/clips/update.ts
new file mode 100644
index 0000000000..d1c31eb8e6
--- /dev/null
+++ b/src/server/api/endpoints/clips/update.ts
@@ -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);
+});
diff --git a/src/server/api/endpoints/federation/followers.ts b/src/server/api/endpoints/federation/followers.ts
new file mode 100644
index 0000000000..d885daf70e
--- /dev/null
+++ b/src/server/api/endpoints/federation/followers.ts
@@ -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 });
+});
diff --git a/src/server/api/endpoints/federation/following.ts b/src/server/api/endpoints/federation/following.ts
new file mode 100644
index 0000000000..1f79817318
--- /dev/null
+++ b/src/server/api/endpoints/federation/following.ts
@@ -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 });
+});
diff --git a/src/server/api/endpoints/federation/instances.ts b/src/server/api/endpoints/federation/instances.ts
index bc0eb9a1d7..002cfd4335 100644
--- a/src/server/api/endpoints/federation/instances.ts
+++ b/src/server/api/endpoints/federation/instances.ts
@@ -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;
diff --git a/src/server/api/endpoints/federation/users.ts b/src/server/api/endpoints/federation/users.ts
new file mode 100644
index 0000000000..f69bbf949c
--- /dev/null
+++ b/src/server/api/endpoints/federation/users.ts
@@ -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 });
+});
diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts
index cd00501a2e..f624550d49 100644
--- a/src/server/api/endpoints/i/notifications.ts
+++ b/src/server/api/endpoints/i/notifications.ts
@@ -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[]
}
},
diff --git a/src/server/api/endpoints/i/read-announcement.ts b/src/server/api/endpoints/i/read-announcement.ts
new file mode 100644
index 0000000000..c5fbe7d576
--- /dev/null
+++ b/src/server/api/endpoints/i/read-announcement.ts
@@ -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');
+ }
+});
diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
index b71c35946e..2c605a6f0b 100644
--- a/src/server/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -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;
diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts
index 810ad51b67..73db73ed97 100644
--- a/src/server/api/endpoints/notes/create.ts
+++ b/src/server/api/endpoints/notes/create.ts
@@ -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 {
diff --git a/src/server/api/endpoints/notes/featured.ts b/src/server/api/endpoints/notes/featured.ts
index 0a1d8668b0..a499afabf0 100644
--- a/src/server/api/endpoints/notes/featured.ts
+++ b/src/server/api/endpoints/notes/featured.ts
@@ -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);
});
diff --git a/src/server/api/endpoints/notes/search.ts b/src/server/api/endpoints/notes/search.ts
index 5557b469e4..efc08d0d4a 100644
--- a/src/server/api/endpoints/notes/search.ts
+++ b/src/server/api/endpoints/notes/search.ts
@@ -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'
- }
- }
- }
- }] : ps.host !== undefined ? [{
+ const notes = await query.take(ps.limit!).getMany();
+
+ return await Notes.packMany(notes, me);
+ } else {
+ const userQuery = ps.userId != null ? [{
term: {
- userHost: ps.host
+ userId: ps.userId
}
- }] : []
- : [];
+ }] : [];
- const result = await es.search({
- index: config.elasticsearch.index || 'misskey_note',
- body: {
- size: ps.limit!,
- from: ps.offset,
- query: {
+ const hostQuery = ps.userId == null ?
+ ps.host === null ? [{
bool: {
- must: [{
- simple_query_string: {
- fields: ['text'],
- query: ps.query.toLowerCase(),
- default_operator: 'and'
- },
- }, ...hostQuery, ...userQuery]
+ must_not: {
+ exists: {
+ field: 'userHost'
+ }
+ }
}
- },
- sort: [{
- _doc: 'desc'
- }]
- }
- });
+ }] : 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);
+ const hits = result.body.hits.hits.map((hit: any) => hit._id);
- if (hits.length === 0) return [];
+ if (hits.length === 0) return [];
- // Fetch found notes
- const notes = await Notes.find({
- where: {
- id: In(hits)
- },
- order: {
- id: -1
- }
- });
+ // 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);
+ }
});
diff --git a/src/server/api/endpoints/users/search-by-username-and-host.ts b/src/server/api/endpoints/users/search-by-username-and-host.ts
new file mode 100644
index 0000000000..8544731dfd
--- /dev/null
+++ b/src/server/api/endpoints/users/search-by-username-and-host.ts
@@ -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 });
+ }
+});
diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts
index af1aefda84..79ee74389c 100644
--- a/src/server/api/private/signup.ts
+++ b/src/server/api/private/signup.ts
@@ -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;
- }
-
- // 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;
+ try {
+ const { account, secret } = await signup(username, password, host);
- // 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');
-
- 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(),
- }));
- });
+ (res as any).token = secret;
- 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);
+ }
};
diff --git a/src/server/api/stream/channels/antenna.ts b/src/server/api/stream/channels/antenna.ts
new file mode 100644
index 0000000000..714edb502d
--- /dev/null
+++ b/src/server/api/stream/channels/antenna.ts
@@ -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);
+ }
+}
diff --git a/src/server/api/stream/channels/ap-log.ts b/src/server/api/stream/channels/ap-log.ts
deleted file mode 100644
index 867fd3670b..0000000000
--- a/src/server/api/stream/channels/ap-log.ts
+++ /dev/null
@@ -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);
- }
-}
diff --git a/src/server/api/stream/channels/hybrid-timeline.ts b/src/server/api/stream/channels/hybrid-timeline.ts
index b9feb70258..e32f4111c2 100644
--- a/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/src/server/api/stream/channels/hybrid-timeline.ts
@@ -50,7 +50,7 @@ export default class extends Channel {
detail: true
});
}
- }
+ }
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (shouldMuteThisNote(note, this.muting)) return;
diff --git a/src/server/api/stream/channels/index.ts b/src/server/api/stream/channels/index.ts
index 4527fb1e46..6efad078c6 100644
--- a/src/server/api/stream/channels/index.ts
+++ b/src/server/api/stream/channels/index.ts
@@ -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
diff --git a/src/server/api/stream/channels/notes-stats.ts b/src/server/api/stream/channels/notes-stats.ts
deleted file mode 100644
index 0c6b84d6cf..0000000000
--- a/src/server/api/stream/channels/notes-stats.ts
+++ /dev/null
@@ -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);
- }
-}
diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts
index f73f3229d5..6ec644a024 100644
--- a/src/server/api/stream/index.ts
+++ b/src/server/api/stream/index.ts
@@ -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);
}
}