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:
syuilo
2020-01-30 04:37:25 +09:00
committed by GitHub
parent a5955c1123
commit f6154dc0af
871 changed files with 26140 additions and 71950 deletions

View File

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

View File

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

View 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 };
}

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

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

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

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

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

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

View File

@ -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,

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

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

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

View File

@ -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) {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

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

View File

@ -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[]
}
},

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

View File

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

View File

@ -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 {

View File

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

View File

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

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

View File

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

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

View File

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

View File

@ -50,7 +50,7 @@ export default class extends Channel {
detail: true
});
}
}
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (shouldMuteThisNote(note, this.muting)) return;

View File

@ -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

View File

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

View File

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

View File

@ -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,

View File

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

View File

@ -31,6 +31,7 @@ const app = new Koa();
app.use(views(__dirname + '/views', {
extension: 'pug',
options: {
version: config.version,
config
}
}));

View File

@ -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