Use PostgreSQL instead of MongoDB (#4572)

* wip

* Update note.ts

* Update timeline.ts

* Update core.ts

* wip

* Update generate-visibility-query.ts

* wip

* wip

* wip

* wip

* wip

* Update global-timeline.ts

* wip

* wip

* wip

* Update vote.ts

* wip

* wip

* Update create.ts

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update files.ts

* wip

* wip

* Update CONTRIBUTING.md

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update read-notification.ts

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update cancel.ts

* wip

* wip

* wip

* Update show.ts

* wip

* wip

* Update gen-id.ts

* Update create.ts

* Update id.ts

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Docker: Update files about Docker (#4599)

* Docker: Use cache if files used by `yarn install` was not updated

This patch reduces the number of times to installing node_modules.
For example, `yarn install` step will be skipped when only ".config/default.yml" is updated.

* Docker: Migrate MongoDB to Postgresql

Misskey uses Postgresql as a database instead of Mongodb since version 11.

* Docker: Uncomment about data persistence

This patch will save a lot of databases.

* wip

* wip

* wip

* Update activitypub.ts

* wip

* wip

* wip

* Update logs.ts

* wip

* Update drive-file.ts

* Update register.ts

* wip

* wip

* Update mentions.ts

* wip

* wip

* wip

* Update recommendation.ts

* wip

* Update index.ts

* wip

* Update recommendation.ts

* Doc: Update docker.ja.md and docker.en.md (#1) (#4608)

Update how to set up misskey.

* wip

* ✌️

* wip

* Update note.ts

* Update postgre.ts

* wip

* wip

* wip

* wip

* Update add-file.ts

* wip

* wip

* wip

* Clean up

* Update logs.ts

* wip

* 🍕

* wip

* Ad notes

* wip

* Update api-visibility.ts

* Update note.ts

* Update add-file.ts

* tests

* tests

* Update postgre.ts

* Update utils.ts

* wip

* wip

* Refactor

* wip

* Refactor

* wip

* wip

* Update show-users.ts

* Update update-instance.ts

* wip

* Update feed.ts

* Update outbox.ts

* Update outbox.ts

* Update user.ts

* wip

* Update list.ts

* Update update-hashtag.ts

* wip

* Update update-hashtag.ts

* Refactor

* Update update.ts

* wip

* wip

* ✌️

* clean up

* docs

* Update push.ts

* wip

* Update api.ts

* wip

* ✌️

* Update make-pagination-query.ts

* ✌️

* Delete hashtags.ts

* Update instances.ts

* Update instances.ts

* Update create.ts

* Update search.ts

* Update reversi-game.ts

* Update signup.ts

* Update user.ts

* id

* Update example.yml

* 🎨

* objectid

* fix

* reversi

* reversi

* Fix bug of chart engine

* Add test of chart engine

* Improve test

* Better testing

* Improve chart engine

* Refactor

* Add test of chart engine

* Refactor

* Add chart test

* Fix bug

* コミットし忘れ

* Refactoring

* ✌️

* Add tests

* Add test

* Extarct note tests

* Refactor

* 存在しないユーザーにメンションできなくなっていた問題を修正

* Fix bug

* Update update-meta.ts

* Fix bug

* Update mention.vue

* Fix bug

* Update meta.ts

* Update CONTRIBUTING.md

* Fix bug

* Fix bug

* Fix bug

* Clean up

* Clean up

* Update notification.ts

* Clean up

* Add mute tests

* Add test

* Refactor

* Add test

* Fix test

* Refactor

* Refactor

* Add tests

* Update utils.ts

* Update utils.ts

* Fix test

* Update package.json

* Update update.ts

* Update manifest.ts

* Fix bug

* Fix bug

* Add test

* 🎨

* Update endpoint permissions

* Updaye permisison

* Update person.ts

#4299

* データベースと同期しないように

* Fix bug

* Fix bug

* Update reversi-game.ts

* Use a feature of Node v11.7.0 to extract a public key (#4644)

* wip

* wip

* ✌️

* Refactoring

#1540

* test

* test

* test

* test

* test

* test

* test

* Fix bug

* Fix test

* 🍣

* wip

* #4471

* Add test for #4335

* Refactor

* Fix test

* Add tests

* 🕓

* Fix bug

* Add test

* Add test

* rename

* Fix bug
This commit is contained in:
syuilo
2019-04-07 21:50:36 +09:00
committed by GitHub
parent 13caf37991
commit f0a29721c9
592 changed files with 13463 additions and 14147 deletions

View File

@@ -1,9 +1,11 @@
import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id';
import Note, { packMany } from '../../../../models/note';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { getFriends } from '../../common/get-friends';
import { getHideUserIds } from '../../common/get-hide-users';
import { makePaginationQuery } from '../../common/make-pagination-query';
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
import { generateMuteQuery } from '../../common/generate-mute-query';
import { Brackets } from 'typeorm';
import { Notes } from '../../../../models';
export const meta = {
desc: {
@@ -18,7 +20,6 @@ export const meta = {
params: {
noteId: {
validator: $.type(ID),
transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID'
@@ -32,12 +33,10 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
transform: transform,
},
untilId: {
validator: $.optional.type(ID),
transform: transform,
},
},
@@ -50,83 +49,24 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
const [followings, hideUserIds] = await Promise.all([
// フォローを取得
// Fetch following
user ? getFriends(user._id) : [],
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere(new Brackets(qb => { qb
.where(`note.replyId = :noteId`, { noteId: ps.noteId })
.orWhere(new Brackets(qb => { qb
.where(`note.renoteId = :noteId`, { noteId: ps.noteId })
.andWhere(new Brackets(qb => { qb
.where(`note.text IS NOT NULL`)
.orWhere(`note.fileIds != '{}'`)
.orWhere(`note.hasPoll = TRUE`);
}));
}));
}))
.leftJoinAndSelect('note.user', 'user');
// 隠すユーザーを取得
getHideUserIds(user)
]);
if (user) generateVisibilityQuery(query, user);
if (user) generateMuteQuery(query, user);
const visibleQuery = user == null ? [{
visibility: { $in: [ 'public', 'home' ] }
}] : [{
visibility: { $in: [ 'public', 'home' ] }
}, {
// myself (for followers/specified/private)
userId: user._id
}, {
// to me (for specified)
visibleUserIds: { $in: [ user._id ] }
}, {
visibility: 'followers',
$or: [{
// フォロワーの投稿
userId: { $in: followings.map(f => f.id) },
}, {
// 自分の投稿へのリプライ
'_reply.userId': user._id,
}, {
// 自分へのメンションが含まれている
mentions: { $in: [ user._id ] }
}]
}];
const notes = await query.take(ps.limit).getMany();
const q = {
$and: [{
$or: [{
replyId: ps.noteId,
}, {
renoteId: ps.noteId,
$or: [{
text: { $ne: null }
}, {
fileIds: { $ne: [] }
}, {
poll: { $ne: null }
}]
}]
}, {
$or: visibleQuery
}]
} as any;
if (hideUserIds && hideUserIds.length > 0) {
q['userId'] = {
$nin: hideUserIds
};
}
const sort = {
_id: -1
};
if (ps.sinceId) {
sort._id = 1;
q._id = {
$gt: ps.sinceId
};
} else if (ps.untilId) {
q._id = {
$lt: ps.untilId
};
}
const notes = await Note.find(q, {
limit: ps.limit,
sort: sort
});
return await packMany(notes, user);
return await Notes.packMany(notes, user);
});

View File

@@ -1,9 +1,10 @@
import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id';
import Note, { packMany, INote } from '../../../../models/note';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { ApiError } from '../../error';
import { getNote } from '../../common/getters';
import { Note } from '../../../../models/entities/note';
import { Notes } from '../../../../models';
export const meta = {
desc: {
@@ -18,7 +19,6 @@ export const meta = {
params: {
noteId: {
validator: $.type(ID),
transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID'
@@ -58,12 +58,12 @@ export default define(meta, async (ps, user) => {
throw e;
});
const conversation: INote[] = [];
const conversation: Note[] = [];
let i = 0;
async function get(id: any) {
i++;
const p = await Note.findOne({ _id: id });
const p = await Notes.findOne(id);
if (i > ps.offset) {
conversation.push(p);
@@ -82,5 +82,5 @@ export default define(meta, async (ps, user) => {
await get(note.replyId);
}
return await packMany(conversation, user);
return await Notes.packMany(conversation, user);
});

View File

@@ -1,14 +1,15 @@
import $ from 'cafy';
import ID, { transform, transformMany } from '../../../../misc/cafy-id';
import * as ms from 'ms';
import { length } from 'stringz';
import Note, { INote, isValidCw, pack } from '../../../../models/note';
import User, { IUser } from '../../../../models/user';
import DriveFile, { IDriveFile } from '../../../../models/drive-file';
import create from '../../../../services/note/create';
import define from '../../define';
import fetchMeta from '../../../../misc/fetch-meta';
import { ApiError } from '../../error';
import { ID } from '../../../../misc/cafy-id';
import { User } from '../../../../models/entities/user';
import { Users, DriveFiles, Notes } from '../../../../models';
import { DriveFile } from '../../../../models/entities/drive-file';
import { Note } from '../../../../models/entities/note';
let maxNoteTextLength = 1000;
@@ -34,7 +35,7 @@ export const meta = {
max: 300
},
kind: 'note-write',
kind: 'write:notes',
params: {
visibility: {
@@ -47,7 +48,6 @@ export const meta = {
visibleUserIds: {
validator: $.optional.arr($.type(ID)).unique().min(0),
transform: transformMany,
desc: {
'ja-JP': '(投稿の公開範囲が specified の場合)投稿を閲覧できるユーザー'
}
@@ -64,7 +64,7 @@ export const meta = {
},
cw: {
validator: $.optional.nullable.str.pipe(isValidCw),
validator: $.optional.nullable.str.pipe(Notes.validateCw),
desc: {
'ja-JP': 'コンテンツの警告。このパラメータを指定すると設定したテキストで投稿のコンテンツを隠す事が出来ます。'
}
@@ -129,7 +129,6 @@ export const meta = {
fileIds: {
validator: $.optional.arr($.type(ID)).unique().range(1, 4),
transform: transformMany,
desc: {
'ja-JP': '添付するファイル'
}
@@ -137,7 +136,6 @@ export const meta = {
mediaIds: {
validator: $.optional.arr($.type(ID)).unique().range(1, 4),
transform: transformMany,
deprecated: true,
desc: {
'ja-JP': '添付するファイル (このパラメータは廃止予定です。代わりに fileIds を使ってください。)'
@@ -146,7 +144,6 @@ export const meta = {
replyId: {
validator: $.optional.type(ID),
transform: transform,
desc: {
'ja-JP': '返信対象'
}
@@ -154,7 +151,6 @@ export const meta = {
renoteId: {
validator: $.optional.type(ID),
transform: transform,
desc: {
'ja-JP': 'Renote対象'
}
@@ -227,32 +223,28 @@ export const meta = {
};
export default define(meta, async (ps, user, app) => {
let visibleUsers: IUser[] = [];
let visibleUsers: User[] = [];
if (ps.visibleUserIds) {
visibleUsers = await Promise.all(ps.visibleUserIds.map(id => User.findOne({
_id: id
})));
visibleUsers = await Promise.all(ps.visibleUserIds.map(id => Users.findOne(id)));
}
let files: IDriveFile[] = [];
let files: DriveFile[] = [];
const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null;
if (fileIds != null) {
files = await Promise.all(fileIds.map(fileId => {
return DriveFile.findOne({
_id: fileId,
'metadata.userId': user._id
return DriveFiles.findOne({
id: fileId,
userId: user.id
});
}));
files = files.filter(file => file != null);
}
let renote: INote = null;
let renote: Note = null;
if (ps.renoteId != null) {
// Fetch renote to note
renote = await Note.findOne({
_id: ps.renoteId
});
renote = await Notes.findOne(ps.renoteId);
if (renote == null) {
throw new ApiError(meta.errors.noSuchRenoteTarget);
@@ -261,14 +253,12 @@ export default define(meta, async (ps, user, app) => {
}
}
let reply: INote = null;
let reply: Note = null;
if (ps.replyId != null) {
// Fetch reply
reply = await Note.findOne({
_id: ps.replyId
});
reply = await Notes.findOne(ps.replyId);
if (reply === null) {
if (reply == null) {
throw new ApiError(meta.errors.noSuchReplyTarget);
}
@@ -279,12 +269,6 @@ export default define(meta, async (ps, user, app) => {
}
if (ps.poll) {
(ps.poll as any).choices = (ps.poll as any).choices.map((choice: string, i: number) => ({
id: i, // IDを付与
text: choice.trim(),
votes: 0
}));
if (typeof ps.poll.expiresAt === 'number') {
if (ps.poll.expiresAt < Date.now())
throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll);
@@ -298,11 +282,6 @@ export default define(meta, async (ps, user, app) => {
throw new ApiError(meta.errors.contentRequired);
}
// 後方互換性のため
if (ps.visibility == 'private') {
ps.visibility = 'specified';
}
// 投稿を作成
const note = await create(user, {
createdAt: new Date(),
@@ -311,7 +290,7 @@ export default define(meta, async (ps, user, app) => {
choices: ps.poll.choices,
multiple: ps.poll.multiple || false,
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null
} : undefined,
} : null,
text: ps.text,
reply,
renote,
@@ -321,13 +300,13 @@ export default define(meta, async (ps, user, app) => {
localOnly: ps.localOnly,
visibility: ps.visibility,
visibleUsers,
apMentions: ps.noExtractMentions ? [] : undefined,
apHashtags: ps.noExtractHashtags ? [] : undefined,
apEmojis: ps.noExtractEmojis ? [] : undefined,
apMentions: ps.noExtractMentions ? [] : null,
apHashtags: ps.noExtractHashtags ? [] : null,
apEmojis: ps.noExtractEmojis ? [] : null,
geo: ps.geo
});
return {
createdNote: await pack(note, user)
createdNote: await Notes.pack(note, user)
};
});

View File

@@ -1,11 +1,11 @@
import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id';
import { ID } from '../../../../misc/cafy-id';
import deleteNote from '../../../../services/note/delete';
import User from '../../../../models/user';
import define from '../../define';
import * as ms from 'ms';
import { getNote } from '../../common/getters';
import { ApiError } from '../../error';
import { Users } from '../../../../models';
export const meta = {
stability: 'stable',
@@ -19,7 +19,7 @@ export const meta = {
requireCredential: true,
kind: 'note-write',
kind: 'write:notes',
limit: {
duration: ms('1hour'),
@@ -30,7 +30,6 @@ export const meta = {
params: {
noteId: {
validator: $.type(ID),
transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID.'
@@ -59,9 +58,10 @@ export default define(meta, async (ps, user) => {
throw e;
});
if (!user.isAdmin && !user.isModerator && !note.userId.equals(user._id)) {
if (!user.isAdmin && !user.isModerator && (note.userId !== user.id)) {
throw new ApiError(meta.errors.accessDenied);
}
await deleteNote(await User.findOne({ _id: note.userId }), note);
// この操作を行うのが投稿者とは限らない(例えばモデレーター)ため
await deleteNote(await Users.findOne(note.userId), note);
});

View File

@@ -1,9 +1,10 @@
import $ from 'cafy';
import ID, { transform } from '../../../../../misc/cafy-id';
import Favorite from '../../../../../models/favorite';
import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
import { ApiError } from '../../../error';
import { getNote } from '../../../common/getters';
import { NoteFavorites } from '../../../../../models';
import { genId } from '../../../../../misc/gen-id';
export const meta = {
stability: 'stable',
@@ -22,7 +23,6 @@ export const meta = {
params: {
noteId: {
validator: $.type(ID),
transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID.'
@@ -53,21 +53,20 @@ export default define(meta, async (ps, user) => {
});
// if already favorited
const exist = await Favorite.findOne({
noteId: note._id,
userId: user._id
const exist = await NoteFavorites.findOne({
noteId: note.id,
userId: user.id
});
if (exist !== null) {
if (exist != null) {
throw new ApiError(meta.errors.alreadyFavorited);
}
// Create favorite
await Favorite.insert({
await NoteFavorites.save({
id: genId(),
createdAt: new Date(),
noteId: note._id,
userId: user._id
noteId: note.id,
userId: user.id
});
return;
});

View File

@@ -1,9 +1,9 @@
import $ from 'cafy';
import ID, { transform } from '../../../../../misc/cafy-id';
import Favorite from '../../../../../models/favorite';
import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
import { ApiError } from '../../../error';
import { getNote } from '../../../common/getters';
import { NoteFavorites } from '../../../../../models';
export const meta = {
stability: 'stable',
@@ -22,7 +22,6 @@ export const meta = {
params: {
noteId: {
validator: $.type(ID),
transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID.'
@@ -53,19 +52,15 @@ export default define(meta, async (ps, user) => {
});
// if already favorited
const exist = await Favorite.findOne({
noteId: note._id,
userId: user._id
const exist = await NoteFavorites.findOne({
noteId: note.id,
userId: user.id
});
if (exist === null) {
if (exist == null) {
throw new ApiError(meta.errors.notFavorited);
}
// Delete favorite
await Favorite.remove({
_id: exist._id
});
return;
await NoteFavorites.delete(exist.id);
});

View File

@@ -1,8 +1,7 @@
import $ from 'cafy';
import Note from '../../../../models/note';
import { packMany } from '../../../../models/note';
import define from '../../define';
import { getHideUserIds } from '../../common/get-hide-users';
import { generateMuteQuery } from '../../common/generate-mute-query';
import { Notes } from '../../../../models';
export const meta = {
desc: {
@@ -35,25 +34,14 @@ export const meta = {
export default define(meta, async (ps, user) => {
const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで
const hideUserIds = await getHideUserIds(user);
const query = Notes.createQueryBuilder('note')
.andWhere(`note.createdAt > :date`, { date: new Date(Date.now() - day) })
.andWhere(`note.visibility = 'public'`)
.leftJoinAndSelect('note.user', 'user');
const notes = await Note.find({
createdAt: {
$gt: new Date(Date.now() - day)
},
deletedAt: null,
visibility: 'public',
'_user.host': null,
...(hideUserIds && hideUserIds.length > 0 ? { userId: { $nin: hideUserIds } } : {})
}, {
limit: ps.limit,
sort: {
score: -1
},
hint: {
score: -1
}
});
if (user) generateMuteQuery(query, user);
return await packMany(notes, user);
const notes = await query.orderBy('note.score', 'DESC').take(ps.limit).getMany();
return await Notes.packMany(notes, user);
});

View File

@@ -1,11 +1,12 @@
import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id';
import Note from '../../../../models/note';
import { packMany } from '../../../../models/note';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import fetchMeta from '../../../../misc/fetch-meta';
import { getHideUserIds } from '../../common/get-hide-users';
import { ApiError } from '../../error';
import { makePaginationQuery } from '../../common/make-pagination-query';
import { Notes } from '../../../../models';
import { generateMuteQuery } from '../../common/generate-mute-query';
import { activeUsersChart } from '../../../../services/chart';
export const meta = {
desc: {
@@ -22,14 +23,6 @@ export const meta = {
}
},
mediaOnly: {
validator: $.optional.bool,
deprecated: true,
desc: {
'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
}
},
limit: {
validator: $.optional.num.range(1, 100),
default: 10
@@ -37,12 +30,10 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
transform: transform,
},
untilId: {
validator: $.optional.type(ID),
transform: transform,
},
sinceDate: {
@@ -71,6 +62,7 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
// TODO どっかにキャッシュ
const m = await fetchMeta();
if (m.disableGlobalTimeline) {
if (user == null || (!user.isAdmin && !user.isModerator)) {
@@ -78,68 +70,25 @@ export default define(meta, async (ps, user) => {
}
}
// 隠すユーザーを取得
const hideUserIds = await getHideUserIds(user);
//#region Construct query
const sort = {
_id: -1
};
const query = makePaginationQuery(Notes.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.visibility = \'public\'')
.andWhere('note.replyId IS NULL')
.leftJoinAndSelect('note.user', 'user');
const query = {
deletedAt: null,
if (user) generateMuteQuery(query, user);
// public only
visibility: 'public',
replyId: null
} as any;
if (hideUserIds && hideUserIds.length > 0) {
query.userId = {
$nin: hideUserIds
};
query['_reply.userId'] = {
$nin: hideUserIds
};
query['_renote.userId'] = {
$nin: hideUserIds
};
}
const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly;
if (withFiles) {
query.fileIds = { $exists: true, $ne: [] };
}
if (ps.sinceId) {
sort._id = 1;
query._id = {
$gt: ps.sinceId
};
} else if (ps.untilId) {
query._id = {
$lt: ps.untilId
};
} else if (ps.sinceDate) {
sort._id = 1;
query.createdAt = {
$gt: new Date(ps.sinceDate)
};
} else if (ps.untilDate) {
query.createdAt = {
$lt: new Date(ps.untilDate)
};
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
//#endregion
const timeline = await Note.find(query, {
limit: ps.limit,
sort: sort
});
const timeline = await query.take(ps.limit).getMany();
return await packMany(timeline, user);
if (user) {
activeUsersChart.update(user);
}
return await Notes.packMany(timeline, user);
});

View File

@@ -1,12 +1,14 @@
import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id';
import Note from '../../../../models/note';
import { packMany } from '../../../../models/note';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import fetchMeta from '../../../../misc/fetch-meta';
import activeUsersChart from '../../../../services/chart/active-users';
import { getHideUserIds } from '../../common/get-hide-users';
import { ApiError } from '../../error';
import { Notes } from '../../../../models';
import { generateMuteQuery } from '../../common/generate-mute-query';
import { makePaginationQuery } from '../../common/make-pagination-query';
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
import { activeUsersChart } from '../../../../services/chart';
import { Brackets } from 'typeorm';
export const meta = {
desc: {
@@ -23,14 +25,6 @@ export const meta = {
}
},
mediaOnly: {
validator: $.optional.bool,
deprecated: true,
desc: {
'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
}
},
fileType: {
validator: $.optional.arr($.str),
desc: {
@@ -53,12 +47,10 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
transform: transform,
},
untilId: {
validator: $.optional.type(ID),
transform: transform,
},
sinceDate: {
@@ -87,6 +79,7 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
// TODO どっかにキャッシュ
const m = await fetchMeta();
if (m.disableLocalTimeline) {
if (user == null || (!user.isAdmin && !user.isModerator)) {
@@ -94,90 +87,44 @@ export default define(meta, async (ps, user) => {
}
}
// 隠すユーザーを取得
const hideUserIds = await getHideUserIds(user);
//#region Construct query
const sort = {
_id: -1
};
const query = makePaginationQuery(Notes.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)')
.leftJoinAndSelect('note.user', 'user');
const query = {
deletedAt: null,
if (user) generateVisibilityQuery(query, user);
if (user) generateMuteQuery(query, user);
// public only
visibility: 'public',
// リプライでない
//replyId: null,
// local
'_user.host': null
} as any;
if (hideUserIds && hideUserIds.length > 0) {
query.userId = {
$nin: hideUserIds
};
query['_reply.userId'] = {
$nin: hideUserIds
};
query['_renote.userId'] = {
$nin: hideUserIds
};
}
const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly;
if (withFiles) {
query.fileIds = { $exists: true, $ne: [] };
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
if (ps.fileType) {
query.fileIds = { $exists: true, $ne: [] };
query['_files.contentType'] = {
$in: ps.fileType
};
query.andWhere('note.fileIds != \'{}\'');
query.andWhere(new Brackets(qb => {
for (const type of ps.fileType) {
const i = ps.fileType.indexOf(type);
qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type });
}
}));
if (ps.excludeNsfw) {
query['_files.metadata.isSensitive'] = {
// v11 TODO
/*
query['_files.isSensitive'] = {
$ne: true
};
*/
}
}
if (ps.sinceId) {
sort._id = 1;
query._id = {
$gt: ps.sinceId
};
} else if (ps.untilId) {
query._id = {
$lt: ps.untilId
};
} else if (ps.sinceDate) {
sort._id = 1;
query.createdAt = {
$gt: new Date(ps.sinceDate)
};
} else if (ps.untilDate) {
query.createdAt = {
$lt: new Date(ps.untilDate)
};
}
//#endregion
const timeline = await Note.find(query, {
limit: ps.limit,
sort: sort
});
const timeline = await query.take(ps.limit).getMany();
if (user) {
activeUsersChart.update(user);
}
return await packMany(timeline, user);
return await Notes.packMany(timeline, user);
});

View File

@@ -1,11 +1,12 @@
import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id';
import Note from '../../../../models/note';
import { getFriendIds, getFriends } from '../../common/get-friends';
import { packMany } from '../../../../models/note';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import read from '../../../../services/note/read';
import { getHideUserIds } from '../../common/get-hide-users';
import { Notes, Followings } from '../../../../models';
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
import { generateMuteQuery } from '../../common/generate-mute-query';
import { makePaginationQuery } from '../../common/make-pagination-query';
import { Brackets } from 'typeorm';
export const meta = {
desc: {
@@ -30,12 +31,10 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
transform: transform,
},
untilId: {
validator: $.optional.type(ID),
transform: transform,
},
visibility: {
@@ -52,97 +51,34 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
// フォローを取得
const followings = await getFriends(user._id);
const followingQuery = Followings.createQueryBuilder('following')
.select('following.followeeId')
.where('following.followerId = :followerId', { followerId: user.id });
const visibleQuery = [{
visibility: { $in: [ 'public', 'home' ] }
}, {
// myself (for followers/specified/private)
userId: user._id
}, {
// to me (for specified)
visibleUserIds: { $in: [ user._id ] }
}, {
visibility: 'followers',
$or: [{
// フォロワーの投稿
userId: { $in: followings.map(f => f.id) },
}, {
// 自分の投稿へのリプライ
'_reply.userId': user._id,
}, {
// 自分へのメンションが含まれている
mentions: { $in: [ user._id ] }
}]
}];
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere(new Brackets(qb => { qb
.where(`:meId = ANY(note.mentions)`, { meId: user.id })
.orWhere(`:meId = ANY(note.visibleUserIds)`, { meId: user.id });
}))
.leftJoinAndSelect('note.user', 'user');
const query = {
$and: [{
deletedAt: null,
}, {
$or: visibleQuery,
}],
$or: [{
mentions: user._id
}, {
visibleUserIds: user._id
}]
} as any;
// 隠すユーザーを取得
const hideUserIds = await getHideUserIds(user);
if (hideUserIds && hideUserIds.length > 0) {
query.userId = {
$nin: hideUserIds
};
query['_reply.userId'] = {
$nin: hideUserIds
};
query['_renote.userId'] = {
$nin: hideUserIds
};
}
const sort = {
_id: -1
};
generateVisibilityQuery(query, user);
generateMuteQuery(query, user);
if (ps.visibility) {
query.visibility = ps.visibility;
query.andWhere('note.visibility = :visibility', { visibility: ps.visibility });
}
if (ps.following) {
const followingIds = await getFriendIds(user._id);
query.userId = {
$in: followingIds
};
query.andWhere(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: user.id });
query.setParameters(followingQuery.getParameters());
}
if (ps.sinceId) {
sort._id = 1;
query._id = {
$gt: ps.sinceId
};
} else if (ps.untilId) {
query._id = {
$lt: ps.untilId
};
}
const mentions = await Note.find(query, {
limit: ps.limit,
sort: sort
});
const mentions = await query.take(ps.limit).getMany();
for (const note of mentions) {
read(user._id, note._id);
read(user.id, note.id);
}
return await packMany(mentions, user);
return await Notes.packMany(mentions, user);
});

View File

@@ -1,8 +1,7 @@
import $ from 'cafy';
import Vote from '../../../../../models/poll-vote';
import Note, { pack } from '../../../../../models/note';
import define from '../../../define';
import { getHideUserIds } from '../../../common/get-hide-users';
import { Polls, Mutings, Notes, PollVotes } from '../../../../../models';
import { Brackets, In } from 'typeorm';
export const meta = {
desc: {
@@ -28,51 +27,46 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
// Get votes
const votes = await Vote.find({
userId: user._id
}, {
fields: {
_id: false,
noteId: true
}
const query = Polls.createQueryBuilder('poll')
.where('poll.userHost IS NULL')
.andWhere(`poll.userId != :meId`, { meId: user.id })
.andWhere(`poll.noteVisibility = 'public'`)
.andWhere(new Brackets(qb => { qb
.where('poll.expiresAt IS NULL')
.orWhere('poll.expiresAt > :now', { now: new Date() });
}));
//#region exclude arleady voted polls
const votedQuery = PollVotes.createQueryBuilder('vote')
.select('vote.noteId')
.where('vote.userId = :meId', { meId: user.id });
query
.andWhere(`poll.noteId NOT IN (${ votedQuery.getQuery() })`);
query.setParameters(votedQuery.getParameters());
//#endregion
//#region mute
const mutingQuery = Mutings.createQueryBuilder('muting')
.select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: user.id });
query
.andWhere(`poll.userId NOT IN (${ mutingQuery.getQuery() })`);
query.setParameters(mutingQuery.getParameters());
//#endregion
const polls = await query.take(ps.limit).skip(ps.offset).getMany();
if (polls.length === 0) return [];
const notes = await Notes.find({
id: In(polls.map(poll => poll.noteId))
});
const nin = votes && votes.length != 0 ? votes.map(v => v.noteId) : [];
// 隠すユーザーを取得
const hideUserIds = await getHideUserIds(user);
const notes = await Note.find({
'_user.host': null,
_id: {
$nin: nin
},
userId: {
$ne: user._id,
$nin: hideUserIds
},
visibility: 'public',
poll: {
$exists: true,
$ne: null
},
$or: [{
'poll.expiresAt': null
}, {
'poll.expiresAt': {
$gt: new Date()
}
}],
}, {
limit: ps.limit,
skip: ps.offset,
sort: {
_id: -1
}
});
return await Promise.all(notes.map(note => pack(note, user, {
return await Notes.packMany(notes, user, {
detail: true
})));
});
});

View File

@@ -1,19 +1,19 @@
import $ from 'cafy';
import ID, { transform } from '../../../../../misc/cafy-id';
import Vote from '../../../../../models/poll-vote';
import Note from '../../../../../models/note';
import Watching from '../../../../../models/note-watching';
import { ID } from '../../../../../misc/cafy-id';
import watch from '../../../../../services/note/watch';
import { publishNoteStream } from '../../../../../services/stream';
import notify from '../../../../../services/create-notification';
import { createNotification } from '../../../../../services/create-notification';
import define from '../../../define';
import User, { IRemoteUser } from '../../../../../models/user';
import { ApiError } from '../../../error';
import { getNote } from '../../../common/getters';
import { deliver } from '../../../../../queue';
import { renderActivity } from '../../../../../remote/activitypub/renderer';
import renderVote from '../../../../../remote/activitypub/renderer/vote';
import { deliverQuestionUpdate } from '../../../../../services/note/polls/update';
import { PollVotes, NoteWatchings, Users, Polls } from '../../../../../models';
import { Not } from 'typeorm';
import { IRemoteUser } from '../../../../../models/entities/user';
import { genId } from '../../../../../misc/gen-id';
export const meta = {
desc: {
@@ -30,7 +30,6 @@ export const meta = {
params: {
noteId: {
validator: $.type(ID),
transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID'
@@ -84,26 +83,28 @@ export default define(meta, async (ps, user) => {
throw e;
});
if (note.poll == null) {
if (!note.hasPoll) {
throw new ApiError(meta.errors.noPoll);
}
if (note.poll.expiresAt && note.poll.expiresAt < createdAt) {
const poll = await Polls.findOne({ noteId: note.id });
if (poll.expiresAt && poll.expiresAt < createdAt) {
throw new ApiError(meta.errors.alreadyExpired);
}
if (!note.poll.choices.some(x => x.id == ps.choice)) {
if (poll.choices[ps.choice] == null) {
throw new ApiError(meta.errors.invalidChoice);
}
// if already voted
const exist = await Vote.find({
noteId: note._id,
userId: user._id
const exist = await PollVotes.find({
noteId: note.id,
userId: user.id
});
if (exist.length) {
if (note.poll.multiple) {
if (poll.multiple) {
if (exist.some(x => x.choice == ps.choice))
throw new ApiError(meta.errors.alreadyVoted);
} else {
@@ -112,69 +113,54 @@ export default define(meta, async (ps, user) => {
}
// Create vote
const vote = await Vote.insert({
const vote = await PollVotes.save({
id: genId(),
createdAt,
noteId: note._id,
userId: user._id,
noteId: note.id,
userId: user.id,
choice: ps.choice
});
const inc: any = {};
inc[`poll.choices.${note.poll.choices.findIndex(c => c.id == ps.choice)}.votes`] = 1;
// Increment votes count
await Note.update({ _id: note._id }, {
$inc: inc
});
const index = ps.choice + 1; // In SQL, array index is 1 based
await Polls.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE id = '${poll.id}'`);
publishNoteStream(note._id, 'pollVoted', {
publishNoteStream(note.id, 'pollVoted', {
choice: ps.choice,
userId: user._id.toHexString()
userId: user.id
});
// Notify
notify(note.userId, user._id, 'poll_vote', {
noteId: note._id,
createNotification(note.userId, user.id, 'pollVote', {
noteId: note.id,
choice: ps.choice
});
// Fetch watchers
Watching
.find({
noteId: note._id,
userId: { $ne: user._id },
// 削除されたドキュメントは除く
deletedAt: { $exists: false }
}, {
fields: {
userId: true
}
})
.then(watchers => {
for (const watcher of watchers) {
notify(watcher.userId, user._id, 'poll_vote', {
noteId: note._id,
choice: ps.choice
});
}
});
NoteWatchings.find({
noteId: note.id,
userId: Not(user.id),
}).then(watchers => {
for (const watcher of watchers) {
createNotification(watcher.userId, user.id, 'pollVote', {
noteId: note.id,
choice: ps.choice
});
}
});
// この投稿をWatchする
if (user.settings.autoWatch !== false) {
watch(user._id, note);
if (user.autoWatch !== false) {
watch(user.id, note);
}
// リモート投票の場合リプライ送信
if (note._user.host != null) {
const pollOwner: IRemoteUser = await User.findOne({
_id: note.userId
});
if (note.userHost != null) {
const pollOwner: IRemoteUser = await Users.findOne(note.userId);
deliver(user, renderActivity(await renderVote(user, vote, note, pollOwner)), pollOwner.inbox);
deliver(user, renderActivity(await renderVote(user, vote, note, poll, pollOwner)), pollOwner.inbox);
}
// リモートフォロワーにUpdate配信
deliverQuestionUpdate(note._id);
return;
deliverQuestionUpdate(note.id);
});

View File

@@ -1,9 +1,9 @@
import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id';
import NoteReaction, { pack } from '../../../../models/note-reaction';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { getNote } from '../../common/getters';
import { ApiError } from '../../error';
import { NoteReactions } from '../../../../models';
export const meta = {
desc: {
@@ -18,7 +18,6 @@ export const meta = {
params: {
noteId: {
validator: $.type(ID),
transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'The ID of the target note'
@@ -37,12 +36,10 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
transform: transform,
},
untilId: {
validator: $.optional.type(ID),
transform: transform,
},
},
@@ -69,29 +66,17 @@ export default define(meta, async (ps, user) => {
});
const query = {
noteId: note._id
} as any;
const sort = {
_id: -1
noteId: note.id
};
if (ps.sinceId) {
sort._id = 1;
query._id = {
$gt: ps.sinceId
};
} else if (ps.untilId) {
query._id = {
$lt: ps.untilId
};
}
const reactions = await NoteReaction.find(query, {
limit: ps.limit,
const reactions = await NoteReactions.find({
where: query,
take: ps.limit,
skip: ps.offset,
sort: sort
order: {
id: -1
}
});
return await Promise.all(reactions.map(reaction => pack(reaction, user)));
return await Promise.all(reactions.map(reaction => NoteReactions.pack(reaction, user)));
});

View File

@@ -1,5 +1,5 @@
import $ from 'cafy';
import ID, { transform } from '../../../../../misc/cafy-id';
import { ID } from '../../../../../misc/cafy-id';
import createReaction from '../../../../../services/note/reaction/create';
import define from '../../../define';
import { getNote } from '../../../common/getters';
@@ -17,12 +17,11 @@ export const meta = {
requireCredential: true,
kind: 'reaction-write',
kind: 'write:reactions',
params: {
noteId: {
validator: $.type(ID),
transform: transform,
desc: {
'ja-JP': '対象の投稿'
}

View File

@@ -1,5 +1,5 @@
import $ from 'cafy';
import ID, { transform } from '../../../../../misc/cafy-id';
import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
import * as ms from 'ms';
import deleteReaction from '../../../../../services/note/reaction/delete';
@@ -16,7 +16,7 @@ export const meta = {
requireCredential: true,
kind: 'reaction-write',
kind: 'write:reactions',
limit: {
duration: ms('1hour'),
@@ -27,7 +27,6 @@ export const meta = {
params: {
noteId: {
validator: $.type(ID),
transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID'

View File

@@ -1,9 +1,12 @@
import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id';
import Note, { packMany } from '../../../../models/note';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { getNote } from '../../common/getters';
import { ApiError } from '../../error';
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
import { generateMuteQuery } from '../../common/generate-mute-query';
import { makePaginationQuery } from '../../common/make-pagination-query';
import { Notes } from '../../../../models';
export const meta = {
desc: {
@@ -18,7 +21,6 @@ export const meta = {
params: {
noteId: {
validator: $.type(ID),
transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID'
@@ -32,12 +34,10 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
transform: transform,
},
untilId: {
validator: $.optional.type(ID),
transform: transform,
}
},
@@ -63,29 +63,14 @@ export default define(meta, async (ps, user) => {
throw e;
});
const sort = {
_id: -1
};
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere(`note.renoteId = :renoteId`, { renoteId: note.id })
.leftJoinAndSelect('note.user', 'user');
const query = {
renoteId: note._id
} as any;
if (user) generateVisibilityQuery(query, user);
if (user) generateMuteQuery(query, user);
if (ps.sinceId) {
sort._id = 1;
query._id = {
$gt: ps.sinceId
};
} else if (ps.untilId) {
query._id = {
$lt: ps.untilId
};
}
const renotes = await query.take(ps.limit).getMany();
const renotes = await Note.find(query, {
limit: ps.limit,
sort: sort
});
return await packMany(renotes, user);
return await Notes.packMany(renotes, user);
});

View File

@@ -1,9 +1,10 @@
import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id';
import Note, { packMany } from '../../../../models/note';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { getFriends } from '../../common/get-friends';
import { getHideUserIds } from '../../common/get-hide-users';
import { 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 = {
desc: {
@@ -18,22 +19,30 @@ export const meta = {
params: {
noteId: {
validator: $.type(ID),
transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID'
}
},
sinceId: {
validator: $.optional.type(ID),
desc: {
'ja-JP': '指定すると、その投稿を基点としてより新しい投稿を取得します'
}
},
untilId: {
validator: $.optional.type(ID),
desc: {
'ja-JP': '指定すると、その投稿を基点としてより古い投稿を取得します'
}
},
limit: {
validator: $.optional.num.range(1, 100),
default: 10
},
offset: {
validator: $.optional.num.min(0),
default: 0
},
},
res: {
@@ -45,54 +54,14 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
const [followings, hideUserIds] = await Promise.all([
// フォローを取得
// Fetch following
user ? getFriends(user._id) : [],
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere('note.replyId = :replyId', { replyId: ps.noteId })
.leftJoinAndSelect('note.user', 'user');
// 隠すユーザーを取得
getHideUserIds(user)
]);
if (user) generateVisibilityQuery(query, user);
if (user) generateMuteQuery(query, user);
const visibleQuery = user == null ? [{
visibility: { $in: [ 'public', 'home' ] }
}] : [{
visibility: { $in: [ 'public', 'home' ] }
}, {
// myself (for followers/specified/private)
userId: user._id
}, {
// to me (for specified)
visibleUserIds: { $in: [ user._id ] }
}, {
visibility: 'followers',
$or: [{
// フォロワーの投稿
userId: { $in: followings.map(f => f.id) },
}, {
// 自分の投稿へのリプライ
'_reply.userId': user._id,
}, {
// 自分へのメンションが含まれている
mentions: { $in: [ user._id ] }
}]
}];
const timeline = await query.take(ps.limit).getMany();
const q = {
replyId: ps.noteId,
$or: visibleQuery
} as any;
if (hideUserIds && hideUserIds.length > 0) {
q['userId'] = {
$nin: hideUserIds
};
}
const notes = await Note.find(q, {
limit: ps.limit,
skip: ps.offset
});
return await packMany(notes, user);
return await Notes.packMany(timeline, user);
});

View File

@@ -1,10 +1,11 @@
import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id';
import Note from '../../../../models/note';
import { getFriendIds } from '../../common/get-friends';
import { packMany } from '../../../../models/note';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { getHideUserIds } from '../../common/get-hide-users';
import { makePaginationQuery } from '../../common/make-pagination-query';
import { Notes } from '../../../../models';
import { generateMuteQuery } from '../../common/generate-mute-query';
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
import { Brackets } from 'typeorm';
export const meta = {
desc: {
@@ -28,16 +29,6 @@ export const meta = {
}
},
following: {
validator: $.optional.nullable.bool,
default: null as any
},
mute: {
validator: $.optional.str,
default: 'mute_all'
},
reply: {
validator: $.optional.nullable.bool,
default: null as any,
@@ -61,15 +52,6 @@ export const meta = {
}
},
media: {
validator: $.optional.nullable.bool,
default: null as any,
deprecated: true,
desc: {
'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
}
},
poll: {
validator: $.optional.nullable.bool,
default: null as any,
@@ -78,25 +60,18 @@ export const meta = {
}
},
untilId: {
sinceId: {
validator: $.optional.type(ID),
transform: transform,
desc: {
'ja-JP': '指定すると、の投稿を基点としてよりい投稿を取得します'
'ja-JP': '指定すると、の投稿を基点としてより新しい投稿を取得します'
}
},
sinceDate: {
validator: $.optional.num,
},
untilDate: {
validator: $.optional.num,
},
offset: {
validator: $.optional.num.min(0),
default: 0
untilId: {
validator: $.optional.type(ID),
desc: {
'ja-JP': '指定すると、その投稿を基点としてより古い投稿を取得します'
}
},
limit: {
@@ -114,226 +89,58 @@ export const meta = {
};
export default define(meta, async (ps, me) => {
const visibleQuery = me == null ? [{
visibility: { $in: [ 'public', 'home' ] }
}] : [{
visibility: { $in: [ 'public', 'home' ] }
}, {
// myself (for specified/private)
userId: me._id
}, {
// to me (for specified)
visibleUserIds: { $in: [ me._id ] }
}];
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.leftJoinAndSelect('note.user', 'user');
const q: any = {
$and: [ps.tag ? {
tagsLower: ps.tag.toLowerCase()
} : {
$or: ps.query.map(tags => ({
$and: tags.map(t => ({
tagsLower: t.toLowerCase()
}))
}))
}],
deletedAt: { $exists: false },
$or: visibleQuery
};
if (me) generateVisibilityQuery(query, me);
if (me) generateMuteQuery(query, me);
const push = (x: any) => q.$and.push(x);
if (ps.following != null && me != null) {
const ids = await getFriendIds(me._id, false);
push({
userId: ps.following ? {
$in: ids
} : {
$nin: ids.concat(me._id)
if (ps.tag) {
query.andWhere(':tag = ANY(note.tags)', { tag: ps.tag });
} else {
let i = 0;
query.andWhere(new Brackets(qb => {
for (const tags of ps.query) {
qb.orWhere(new Brackets(qb => {
for (const tag of tags) {
qb.andWhere(`:tag${i} = ANY(note.tags)`, { [`tag${i}`]: tag });
i++;
}
}));
}
});
}
if (me != null) {
const hideUserIds = await getHideUserIds(me);
switch (ps.mute) {
case 'mute_all':
push({
userId: {
$nin: hideUserIds
},
'_reply.userId': {
$nin: hideUserIds
},
'_renote.userId': {
$nin: hideUserIds
}
});
break;
case 'mute_related':
push({
'_reply.userId': {
$nin: hideUserIds
},
'_renote.userId': {
$nin: hideUserIds
}
});
break;
case 'mute_direct':
push({
userId: {
$nin: hideUserIds
}
});
break;
case 'direct_only':
push({
userId: {
$in: hideUserIds
}
});
break;
case 'related_only':
push({
$or: [{
'_reply.userId': {
$in: hideUserIds
}
}, {
'_renote.userId': {
$in: hideUserIds
}
}]
});
break;
case 'all_only':
push({
$or: [{
userId: {
$in: hideUserIds
}
}, {
'_reply.userId': {
$in: hideUserIds
}
}, {
'_renote.userId': {
$in: hideUserIds
}
}]
});
break;
}
}));
}
if (ps.reply != null) {
if (ps.reply) {
push({
replyId: {
$exists: true,
$ne: null
}
});
query.andWhere('note.replyId IS NOT NULL');
} else {
push({
$or: [{
replyId: {
$exists: false
}
}, {
replyId: null
}]
});
query.andWhere('note.replyId IS NULL');
}
}
if (ps.renote != null) {
if (ps.renote) {
push({
renoteId: {
$exists: true,
$ne: null
}
});
query.andWhere('note.renoteId IS NOT NULL');
} else {
push({
$or: [{
renoteId: {
$exists: false
}
}, {
renoteId: null
}]
});
query.andWhere('note.renoteId IS NULL');
}
}
const withFiles = ps.withFiles != null ? ps.withFiles : ps.media;
if (withFiles) {
push({
fileIds: { $exists: true, $ne: [] }
});
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
if (ps.poll != null) {
if (ps.poll) {
push({
poll: {
$exists: true,
$ne: null
}
});
query.andWhere('note.hasPoll = TRUE');
} else {
push({
$or: [{
poll: {
$exists: false
}
}, {
poll: null
}]
});
query.andWhere('note.hasPoll = FALSE');
}
}
if (ps.untilId) {
push({
_id: {
$lt: ps.untilId
}
});
}
if (ps.sinceDate) {
push({
createdAt: {
$gt: new Date(ps.sinceDate)
}
});
}
if (ps.untilDate) {
push({
createdAt: {
$lt: new Date(ps.untilDate)
}
});
}
if (q.$and.length == 0) {
delete q.$and;
}
// Search notes
const notes = await Note.find(q, {
sort: {
_id: -1
},
limit: ps.limit,
skip: ps.offset
});
const notes = await query.take(ps.limit).getMany();
return await packMany(notes, me);
return await Notes.packMany(notes, me);
});

View File

@@ -1,10 +1,9 @@
import $ from 'cafy';
import * as mongo from 'mongodb';
import Note from '../../../../models/note';
import { packMany } from '../../../../models/note';
import es from '../../../../db/elasticsearch';
import define from '../../define';
import { ApiError } from '../../error';
import { Notes } from '../../../../models';
import { In } from 'typeorm';
export const meta = {
desc: {
@@ -74,18 +73,19 @@ export default define(meta, async (ps, me) => {
return [];
}
const hits = response.hits.hits.map(hit => new mongo.ObjectID(hit._id));
const hits = response.hits.hits.map((hit: any) => hit.id);
if (hits.length === 0) return [];
// Fetch found notes
const notes = await Note.find({
_id: {
$in: hits
}
}, {
sort: {
_id: -1
const notes = await Notes.find({
where: {
id: In(hits)
},
order: {
id: -1
}
});
return await packMany(notes, me);
return await Notes.packMany(notes, me);
});

View File

@@ -1,9 +1,9 @@
import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id';
import { pack } from '../../../../models/note';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { getNote } from '../../common/getters';
import { ApiError } from '../../error';
import { Notes } from '../../../../models';
export const meta = {
stability: 'stable',
@@ -20,7 +20,6 @@ export const meta = {
params: {
noteId: {
validator: $.type(ID),
transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID.'
@@ -47,7 +46,7 @@ export default define(meta, async (ps, user) => {
throw e;
});
return await pack(note, user, {
return await Notes.pack(note, user, {
detail: true
});
});

View File

@@ -1,17 +1,18 @@
import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id';
import Note from '../../../../models/note';
import { getFriends } from '../../common/get-friends';
import { packMany } from '../../../../models/note';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import fetchMeta from '../../../../misc/fetch-meta';
import activeUsersChart from '../../../../services/chart/active-users';
import { getHideUserIds } from '../../common/get-hide-users';
import { ApiError } from '../../error';
import { makePaginationQuery } from '../../common/make-pagination-query';
import { Followings, Notes } from '../../../../models';
import { Brackets } from 'typeorm';
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
import { generateMuteQuery } from '../../common/generate-mute-query';
import { activeUsersChart } from '../../../../services/chart';
export const meta = {
desc: {
'ja-JP': 'ハイブリッドタイムラインを取得します。'
'ja-JP': 'ソーシャルタイムラインを取得します。'
},
tags: ['notes'],
@@ -27,17 +28,15 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
transform: transform,
desc: {
'ja-JP': '指定すると、の投稿を基点としてより新しい投稿を取得します'
'ja-JP': '指定すると、の投稿を基点としてより新しい投稿を取得します'
}
},
untilId: {
validator: $.optional.type(ID),
transform: transform,
desc: {
'ja-JP': '指定すると、の投稿を基点としてより古い投稿を取得します'
'ja-JP': '指定すると、の投稿を基点としてより古い投稿を取得します'
}
},
@@ -85,14 +84,6 @@ export const meta = {
'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
}
},
mediaOnly: {
validator: $.optional.bool,
deprecated: true,
desc: {
'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
}
},
},
res: {
@@ -112,94 +103,30 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
// TODO どっかにキャッシュ
const m = await fetchMeta();
if (m.disableLocalTimeline && !user.isAdmin && !user.isModerator) {
throw new ApiError(meta.errors.stlDisabled);
}
const [followings, hideUserIds] = await Promise.all([
// フォローを取得
// Fetch following
getFriends(user._id, true, false),
// 隠すユーザーを取得
getHideUserIds(user)
]);
//#region Construct query
const sort = {
_id: -1
};
const followingQuery = Followings.createQueryBuilder('following')
.select('following.followeeId')
.where('following.followerId = :followerId', { followerId: user.id });
const followQuery = followings.map(f => ({
userId: f.id,
const query = makePaginationQuery(Notes.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere(new Brackets(qb => {
qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: user.id })
.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
}))
.leftJoinAndSelect('note.user', 'user')
.setParameters(followingQuery.getParameters());
/*// (稿稿稿)
$or: [{
// リプライでない
replyId: null
}, { // または
// リプライだが返信先が投稿者自身の投稿
$expr: {
$eq: ['$_reply.userId', '$userId']
}
}, { // または
// リプライだが返信先が自分(フォロワー)の投稿
'_reply.userId': user._id
}, { // または
// 自分(フォロワー)が送信したリプライ
userId: user._id
}]*/
}));
const visibleQuery = user == null ? [{
visibility: { $in: ['public', 'home'] }
}] : [{
visibility: { $in: ['public', 'home', 'followers'] }
}, {
// myself (for specified/private)
userId: user._id
}, {
// to me (for specified)
visibleUserIds: { $in: [ user._id ] }
}];
const query = {
$and: [{
deletedAt: null,
$or: [{
$and: [{
// フォローしている人の投稿
$or: followQuery
}, {
// visible for me
$or: visibleQuery
}]
}, {
// public only
visibility: 'public',
// リプライでない
//replyId: null,
// local
'_user.host': null
}],
// hide
userId: {
$nin: hideUserIds
},
'_reply.userId': {
$nin: hideUserIds
},
'_renote.userId': {
$nin: hideUserIds
},
}]
} as any;
generateVisibilityQuery(query, user);
generateMuteQuery(query, user);
/* TODO
// MongoDBではトップレベルで否定ができないため、De Morganの法則を利用してクエリします。
// つまり、「『自分の投稿かつRenote』ではない」を「『自分の投稿ではない』または『Renoteではない』」と表現します。
// for details: https://en.wikipedia.org/wiki/De_Morgan%27s_laws
@@ -207,7 +134,7 @@ export default define(meta, async (ps, user) => {
if (ps.includeMyRenotes === false) {
query.$and.push({
$or: [{
userId: { $ne: user._id }
userId: { $ne: user.id }
}, {
renoteId: null
}, {
@@ -223,7 +150,7 @@ export default define(meta, async (ps, user) => {
if (ps.includeRenotedMyNotes === false) {
query.$and.push({
$or: [{
'_renote.userId': { $ne: user._id }
'_renote.userId': { $ne: user.id }
}, {
renoteId: null
}, {
@@ -251,40 +178,18 @@ export default define(meta, async (ps, user) => {
}]
});
}
*/
if (ps.withFiles || ps.mediaOnly) {
query.$and.push({
fileIds: { $exists: true, $ne: [] }
});
}
if (ps.sinceId) {
sort._id = 1;
query._id = {
$gt: ps.sinceId
};
} else if (ps.untilId) {
query._id = {
$lt: ps.untilId
};
} else if (ps.sinceDate) {
sort._id = 1;
query.createdAt = {
$gt: new Date(ps.sinceDate)
};
} else if (ps.untilDate) {
query.createdAt = {
$lt: new Date(ps.untilDate)
};
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
//#endregion
const timeline = await Note.find(query, {
limit: ps.limit,
sort: sort
});
const timeline = await query.take(ps.limit).getMany();
activeUsersChart.update(user);
if (user) {
activeUsersChart.update(user);
}
return await packMany(timeline, user);
return await Notes.packMany(timeline, user);
});

View File

@@ -1,8 +1,7 @@
import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import Favorite from '../../../../models/favorite';
import NoteWatching from '../../../../models/note-watching';
import { NoteFavorites, NoteWatchings } from '../../../../models';
export const meta = {
stability: 'stable',
@@ -19,7 +18,6 @@ export const meta = {
params: {
noteId: {
validator: $.type(ID),
transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID.'
@@ -30,17 +28,19 @@ export const meta = {
export default define(meta, async (ps, user) => {
const [favorite, watching] = await Promise.all([
Favorite.count({
userId: user._id,
NoteFavorites.count({
where: {
userId: user.id,
noteId: ps.noteId
}, {
limit: 1
},
take: 1
}),
NoteWatching.count({
userId: user._id,
NoteWatchings.count({
where: {
userId: user.id,
noteId: ps.noteId
}, {
limit: 1
},
take: 1
})
]);

View File

@@ -1,11 +1,12 @@
import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id';
import Note from '../../../../models/note';
import { getFriends } from '../../common/get-friends';
import { packMany } from '../../../../models/note';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import activeUsersChart from '../../../../services/chart/active-users';
import { getHideUserIds } from '../../common/get-hide-users';
import { makePaginationQuery } from '../../common/make-pagination-query';
import { Notes, Followings } from '../../../../models';
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
import { generateMuteQuery } from '../../common/generate-mute-query';
import { activeUsersChart } from '../../../../services/chart';
import { Brackets } from 'typeorm';
export const meta = {
desc: {
@@ -28,17 +29,15 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
transform: transform,
desc: {
'ja-JP': '指定すると、の投稿を基点としてより新しい投稿を取得します'
'ja-JP': '指定すると、の投稿を基点としてより新しい投稿を取得します'
}
},
untilId: {
validator: $.optional.type(ID),
transform: transform,
desc: {
'ja-JP': '指定すると、の投稿を基点としてより古い投稿を取得します'
'ja-JP': '指定すると、の投稿を基点としてより古い投稿を取得します'
}
},
@@ -86,14 +85,6 @@ export const meta = {
'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
}
},
mediaOnly: {
validator: $.optional.bool,
deprecated: true,
desc: {
'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
}
},
},
res: {
@@ -105,78 +96,24 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
const [followings, hideUserIds] = await Promise.all([
// フォローを取得
// Fetch following
getFriends(user._id),
// 隠すユーザーを取得
getHideUserIds(user)
]);
//#region Construct query
const sort = {
_id: -1
};
const followingQuery = Followings.createQueryBuilder('following')
.select('following.followeeId')
.where('following.followerId = :followerId', { followerId: user.id });
const followQuery = followings.map(f => ({
userId: f.id,
const query = makePaginationQuery(Notes.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere(new Brackets(qb => { qb
.where(`note.userId IN (${ followingQuery.getQuery() })`)
.orWhere('note.userId = :meId', { meId: user.id });
}))
.leftJoinAndSelect('note.user', 'user')
.setParameters(followingQuery.getParameters());
/*// リプライは含めない(ただし投稿者自身の投稿へのリプライ、自分の投稿へのリプライ、自分のリプライは含める)
$or: [{
// リプライでない
replyId: null
}, { // または
// リプライだが返信先が投稿者自身の投稿
$expr: {
$eq: ['$_reply.userId', '$userId']
}
}, { // または
// リプライだが返信先が自分(フォロワー)の投稿
'_reply.userId': user._id
}, { // または
// 自分(フォロワー)が送信したリプライ
userId: user._id
}]*/
}));
const visibleQuery = user == null ? [{
visibility: { $in: [ 'public', 'home' ] }
}] : [{
visibility: { $in: [ 'public', 'home', 'followers' ] }
}, {
// myself (for specified/private)
userId: user._id
}, {
// to me (for specified)
visibleUserIds: { $in: [ user._id ] }
}];
const query = {
$and: [{
deletedAt: null,
$and: [{
// フォローしている人の投稿
$or: followQuery
}, {
// visible for me
$or: visibleQuery
}],
// mute
userId: {
$nin: hideUserIds
},
'_reply.userId': {
$nin: hideUserIds
},
'_renote.userId': {
$nin: hideUserIds
},
}]
} as any;
generateVisibilityQuery(query, user);
generateMuteQuery(query, user);
/* v11 TODO
// MongoDBではトップレベルで否定ができないため、De Morganの法則を利用してクエリします。
// つまり、「『自分の投稿かつRenote』ではない」を「『自分の投稿ではない』または『Renoteではない』」と表現します。
// for details: https://en.wikipedia.org/wiki/De_Morgan%27s_laws
@@ -184,7 +121,7 @@ export default define(meta, async (ps, user) => {
if (ps.includeMyRenotes === false) {
query.$and.push({
$or: [{
userId: { $ne: user._id }
userId: { $ne: user.id }
}, {
renoteId: null
}, {
@@ -200,7 +137,7 @@ export default define(meta, async (ps, user) => {
if (ps.includeRenotedMyNotes === false) {
query.$and.push({
$or: [{
'_renote.userId': { $ne: user._id }
'_renote.userId': { $ne: user.id }
}, {
renoteId: null
}, {
@@ -227,43 +164,16 @@ export default define(meta, async (ps, user) => {
poll: { $ne: null }
}]
});
}
}*/
const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly;
if (withFiles) {
query.$and.push({
fileIds: { $exists: true, $ne: [] }
});
}
if (ps.sinceId) {
sort._id = 1;
query._id = {
$gt: ps.sinceId
};
} else if (ps.untilId) {
query._id = {
$lt: ps.untilId
};
} else if (ps.sinceDate) {
sort._id = 1;
query.createdAt = {
$gt: new Date(ps.sinceDate)
};
} else if (ps.untilDate) {
query.createdAt = {
$lt: new Date(ps.untilDate)
};
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
//#endregion
const timeline = await Note.find(query, {
limit: ps.limit,
sort: sort
});
const timeline = await query.take(ps.limit).getMany();
activeUsersChart.update(user);
return await packMany(timeline, user);
return await Notes.packMany(timeline, user);
});

View File

@@ -1,12 +1,11 @@
import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id';
import Note from '../../../../models/note';
import { packMany } from '../../../../models/note';
import UserList from '../../../../models/user-list';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { getFriends } from '../../common/get-friends';
import { getHideUserIds } from '../../common/get-hide-users';
import { ApiError } from '../../error';
import { UserLists, UserListJoinings, Notes } from '../../../../models';
import { makePaginationQuery } from '../../common/make-pagination-query';
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
import { activeUsersChart } from '../../../../services/chart';
export const meta = {
desc: {
@@ -21,7 +20,6 @@ export const meta = {
params: {
listId: {
validator: $.type(ID),
transform: transform,
desc: {
'ja-JP': 'リストのID'
}
@@ -37,17 +35,15 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
transform: transform,
desc: {
'ja-JP': '指定すると、の投稿を基点としてより新しい投稿を取得します'
'ja-JP': '指定すると、の投稿を基点としてより新しい投稿を取得します'
}
},
untilId: {
validator: $.optional.type(ID),
transform: transform,
desc: {
'ja-JP': '指定すると、の投稿を基点としてより古い投稿を取得します'
'ja-JP': '指定すると、の投稿を基点としてより古い投稿を取得します'
}
},
@@ -95,14 +91,6 @@ export const meta = {
'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
}
},
mediaOnly: {
validator: $.optional.bool,
deprecated: true,
desc: {
'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
}
},
},
res: {
@@ -122,94 +110,28 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
const [list, followings, hideUserIds] = await Promise.all([
// リストを取得
// Fetch the list
UserList.findOne({
_id: ps.listId,
userId: user._id
}),
// フォローを取得
// Fetch following
getFriends(user._id, true, false),
// 隠すユーザーを取得
getHideUserIds(user)
]);
const list = await UserLists.findOne({
id: ps.listId,
userId: user.id
});
if (list == null) {
throw new ApiError(meta.errors.noSuchList);
}
if (list.userIds.length == 0) {
return [];
}
//#region Construct query
const sort = {
_id: -1
};
const listQuery = UserListJoinings.createQueryBuilder('joining')
.select('joining.userId')
.where('joining.userListId = :userListId', { userListId: list.id });
const listQuery = list.userIds.map(u => ({
userId: u,
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere(`note.userId IN (${ listQuery.getQuery() })`)
.leftJoinAndSelect('note.user', 'user')
.setParameters(listQuery.getParameters());
/*// リプライは含めない(ただし投稿者自身の投稿へのリプライ、自分の投稿へのリプライ、自分のリプライは含める)
$or: [{
// リプライでない
replyId: null
}, { // または
// リプライだが返信先が投稿者自身の投稿
$expr: {
$eq: ['$_reply.userId', '$userId']
}
}, { // または
// リプライだが返信先が自分(フォロワー)の投稿
'_reply.userId': user._id
}, { // または
// 自分(フォロワー)が送信したリプライ
userId: user._id
}]*/
}));
const visibleQuery = [{
visibility: { $in: ['public', 'home'] }
}, {
// myself (for specified/private)
userId: user._id
}, {
// to me (for specified)
visibleUserIds: { $in: [user._id] }
}, {
visibility: 'followers',
userId: { $in: followings.map(f => f.id) }
}];
const query = {
$and: [{
deletedAt: null,
$and: [{
// リストに入っている人のタイムラインへの投稿
$or: listQuery
}, {
// visible for me
$or: visibleQuery
}],
// mute
userId: {
$nin: hideUserIds
},
'_reply.userId': {
$nin: hideUserIds
},
'_renote.userId': {
$nin: hideUserIds
},
}]
} as any;
generateVisibilityQuery(query, user);
/* TODO
// MongoDBではトップレベルで否定ができないため、De Morganの法則を利用してクエリします。
// つまり、「『自分の投稿かつRenote』ではない」を「『自分の投稿ではない』または『Renoteではない』」と表現します。
// for details: https://en.wikipedia.org/wiki/De_Morgan%27s_laws
@@ -217,7 +139,7 @@ export default define(meta, async (ps, user) => {
if (ps.includeMyRenotes === false) {
query.$and.push({
$or: [{
userId: { $ne: user._id }
userId: { $ne: user.id }
}, {
renoteId: null
}, {
@@ -233,7 +155,7 @@ export default define(meta, async (ps, user) => {
if (ps.includeRenotedMyNotes === false) {
query.$and.push({
$or: [{
'_renote.userId': { $ne: user._id }
'_renote.userId': { $ne: user.id }
}, {
renoteId: null
}, {
@@ -260,41 +182,16 @@ export default define(meta, async (ps, user) => {
poll: { $ne: null }
}]
});
}
}*/
const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly;
if (withFiles) {
query.$and.push({
fileIds: { $exists: true, $ne: [] }
});
}
if (ps.sinceId) {
sort._id = 1;
query._id = {
$gt: ps.sinceId
};
} else if (ps.untilId) {
query._id = {
$lt: ps.untilId
};
} else if (ps.sinceDate) {
sort._id = 1;
query.createdAt = {
$gt: new Date(ps.sinceDate)
};
} else if (ps.untilDate) {
query.createdAt = {
$lt: new Date(ps.untilDate)
};
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
//#endregion
const timeline = await Note.find(query, {
limit: ps.limit,
sort: sort
});
const timeline = await query.take(ps.limit).getMany();
return await packMany(timeline, user);
activeUsersChart.update(user);
return await Notes.packMany(timeline, user);
});

View File

@@ -1,5 +1,5 @@
import $ from 'cafy';
import ID, { transform } from '../../../../../misc/cafy-id';
import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
import watch from '../../../../../services/note/watch';
import { getNote } from '../../../common/getters';
@@ -17,12 +17,11 @@ export const meta = {
requireCredential: true,
kind: 'account-write',
kind: 'write:account',
params: {
noteId: {
validator: $.type(ID),
transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID.'
@@ -45,5 +44,5 @@ export default define(meta, async (ps, user) => {
throw e;
});
await watch(user._id, note);
await watch(user.id, note);
});

View File

@@ -1,5 +1,5 @@
import $ from 'cafy';
import ID, { transform } from '../../../../../misc/cafy-id';
import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
import unwatch from '../../../../../services/note/unwatch';
import { getNote } from '../../../common/getters';
@@ -17,12 +17,11 @@ export const meta = {
requireCredential: true,
kind: 'account-write',
kind: 'write:account',
params: {
noteId: {
validator: $.type(ID),
transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID.'
@@ -45,5 +44,5 @@ export default define(meta, async (ps, user) => {
throw e;
});
await unwatch(user._id, note);
await unwatch(user.id, note);
});