From 931bdc6aace5e7aa71ffdfb470e208ead78a2a53 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 2 Nov 2018 03:32:24 +0900
Subject: [PATCH] Refactoring, Clean up and bug fixes

---
 package-lock.json                             |  10 +-
 package.json                                  |   2 +-
 src/misc/cafy-id.ts                           |  28 ++--
 src/models/user.ts                            |   2 +-
 src/server/activitypub/followers.ts           |   4 +-
 src/server/activitypub/following.ts           |   4 +-
 src/server/activitypub/outbox.ts              |   6 +-
 src/server/api/endpoints.ts                   |  15 +-
 .../api/endpoints/admin/suspend-user.ts       |   8 +-
 .../api/endpoints/admin/unsuspend-user.ts     |   8 +-
 .../api/endpoints/admin/unverify-user.ts      |   8 +-
 src/server/api/endpoints/admin/update-meta.ts |  30 ++--
 src/server/api/endpoints/admin/verify-user.ts |   8 +-
 .../api/endpoints/aggregation/hashtags.ts     |  66 --------
 .../endpoints/aggregation/users/activity.ts   | 110 --------------
 .../api/endpoints/aggregation/users/post.ts   | 104 -------------
 .../endpoints/aggregation/users/reaction.ts   |  74 ---------
 src/server/api/endpoints/ap/show.ts           |   5 +-
 src/server/api/endpoints/app/show.ts          |  24 +--
 src/server/api/endpoints/blocking/create.ts   |   8 +-
 src/server/api/endpoints/blocking/delete.ts   |   8 +-
 src/server/api/endpoints/blocking/list.ts     |  19 ++-
 src/server/api/endpoints/charts/drive.ts      |  10 +-
 src/server/api/endpoints/charts/federation.ts |  10 +-
 src/server/api/endpoints/charts/hashtag.ts    |  15 +-
 src/server/api/endpoints/charts/network.ts    |  10 +-
 src/server/api/endpoints/charts/notes.ts      |  10 +-
 src/server/api/endpoints/charts/user/drive.ts |  18 ++-
 .../api/endpoints/charts/user/following.ts    |  18 ++-
 src/server/api/endpoints/charts/user/notes.ts |  18 ++-
 .../api/endpoints/charts/user/reactions.ts    |  18 ++-
 src/server/api/endpoints/charts/users.ts      |  10 +-
 src/server/api/endpoints/drive/files.ts       |  74 +++++----
 .../endpoints/drive/files/attached_notes.ts   |   8 +-
 .../endpoints/drive/files/check_existence.ts  |   5 +-
 .../api/endpoints/drive/files/create.ts       |  20 ++-
 .../api/endpoints/drive/files/delete.ts       |   8 +-
 src/server/api/endpoints/drive/files/find.ts  |  36 +++--
 src/server/api/endpoints/drive/files/show.ts  |   8 +-
 .../api/endpoints/drive/files/update.ts       |  30 ++--
 .../endpoints/drive/files/upload_from_url.ts  |  33 ++--
 src/server/api/endpoints/drive/folders.ts     |  65 ++++----
 .../api/endpoints/drive/folders/create.ts     |  13 +-
 .../api/endpoints/drive/folders/delete.ts     |   8 +-
 .../api/endpoints/drive/folders/find.ts       |  33 ++--
 .../api/endpoints/drive/folders/show.ts       |   8 +-
 .../api/endpoints/drive/folders/update.ts     |  19 ++-
 src/server/api/endpoints/drive/stream.ts      |  62 ++++----
 src/server/api/endpoints/following/create.ts  |   8 +-
 src/server/api/endpoints/following/delete.ts  |   8 +-
 .../endpoints/following/requests/accept.ts    |  20 ++-
 .../endpoints/following/requests/cancel.ts    |  20 ++-
 .../api/endpoints/following/requests/list.ts  |   2 +-
 .../endpoints/following/requests/reject.ts    |  20 ++-
 src/server/api/endpoints/following/stalk.ts   |  22 ++-
 src/server/api/endpoints/following/unstalk.ts |  22 ++-
 .../api/endpoints/games/reversi/games.ts      |  57 ++++---
 .../api/endpoints/games/reversi/games/show.ts |  19 ++-
 .../games/reversi/games/surrender.ts          |   8 +-
 .../api/endpoints/games/reversi/match.ts      |  23 ++-
 src/server/api/endpoints/hashtags/search.ts   |  15 +-
 src/server/api/endpoints/i/favorites.ts       |  51 ++++---
 src/server/api/endpoints/i/notifications.ts   |  84 ++++++-----
 src/server/api/endpoints/i/pin.ts             |   8 +-
 src/server/api/endpoints/i/signin_history.ts  |  52 ++++---
 src/server/api/endpoints/i/unpin.ts           |   8 +-
 src/server/api/endpoints/i/update.ts          |  70 +++++----
 .../api/endpoints/messaging/messages.ts       |  86 ++++++-----
 .../endpoints/messaging/messages/create.ts    |  46 +++---
 .../api/endpoints/messaging/messages/read.ts  |   8 +-
 src/server/api/endpoints/mute/create.ts       |  24 +--
 src/server/api/endpoints/mute/delete.ts       |  24 +--
 src/server/api/endpoints/mute/list.ts         |  19 ++-
 src/server/api/endpoints/notes.ts             |  47 +++---
 .../api/endpoints/notes/conversation.ts       |  52 ++++---
 src/server/api/endpoints/notes/create.ts      | 111 ++++++++------
 src/server/api/endpoints/notes/delete.ts      |   8 +-
 .../api/endpoints/notes/favorites/create.ts   |   8 +-
 .../api/endpoints/notes/favorites/delete.ts   |   8 +-
 src/server/api/endpoints/notes/featured.ts    |   5 +-
 .../api/endpoints/notes/global-timeline.ts    |  35 +++--
 .../api/endpoints/notes/hybrid-timeline.ts    |  54 ++++---
 .../api/endpoints/notes/local-timeline.ts     |  45 ++++--
 src/server/api/endpoints/notes/mentions.ts    |  29 ++--
 src/server/api/endpoints/notes/polls/vote.ts  |  40 ++---
 src/server/api/endpoints/notes/reactions.ts   |  30 ++--
 .../api/endpoints/notes/reactions/create.ts   |  13 +-
 .../api/endpoints/notes/reactions/delete.ts   |  20 ++-
 src/server/api/endpoints/notes/renotes.ts     |  81 ++++++++++
 src/server/api/endpoints/notes/replies.ts     |  50 +++---
 src/server/api/endpoints/notes/reposts.ts     |  66 --------
 .../api/endpoints/notes/search_by_tag.ts      | 142 ++++++------------
 src/server/api/endpoints/notes/show.ts        |   8 +-
 src/server/api/endpoints/notes/timeline.ts    |  54 ++++---
 .../api/endpoints/notes/user-list-timeline.ts |  60 +++++---
 src/server/api/endpoints/users/followers.ts   |  69 +++++----
 src/server/api/endpoints/users/following.ts   |  69 +++++----
 .../users/get_frequently_replied_users.ts     |  33 ++--
 .../api/endpoints/users/lists/delete.ts       |   8 +-
 src/server/api/endpoints/users/lists/push.ts  |  33 ++--
 src/server/api/endpoints/users/lists/show.ts  |  19 ++-
 .../api/endpoints/users/lists/update.ts       |  14 +-
 src/server/api/endpoints/users/notes.ts       |  82 ++++++----
 src/server/api/endpoints/users/relation.ts    |   8 +-
 src/server/api/endpoints/users/search.ts      |  20 ++-
 src/server/api/endpoints/users/show.ts        |  75 +++++----
 src/server/api/get-params.ts                  |  21 ++-
 src/server/web/docs.ts                        |   2 +-
 108 files changed, 1722 insertions(+), 1539 deletions(-)
 delete mode 100644 src/server/api/endpoints/aggregation/hashtags.ts
 delete mode 100644 src/server/api/endpoints/aggregation/users/activity.ts
 delete mode 100644 src/server/api/endpoints/aggregation/users/post.ts
 delete mode 100644 src/server/api/endpoints/aggregation/users/reaction.ts
 create mode 100644 src/server/api/endpoints/notes/renotes.ts
 delete mode 100644 src/server/api/endpoints/notes/reposts.ts

diff --git a/package-lock.json b/package-lock.json
index 7257e735e5..35a2f9b091 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "10.36.0",
+	"version": "10.37.0",
 	"lockfileVersion": 1,
 	"requires": true,
 	"dependencies": {
@@ -2250,9 +2250,9 @@
 			}
 		},
 		"cafy": {
-			"version": "11.3.0",
-			"resolved": "https://registry.npmjs.org/cafy/-/cafy-11.3.0.tgz",
-			"integrity": "sha512-7kqqF4I6seSNSAWihRfnM78wP/OwaZMrCNIUzu0+TC1pDGfF2uoVfMsAJ1oV1jZsZ2L2qlUSvo9zhSEIouS/xQ=="
+			"version": "12.0.0",
+			"resolved": "https://registry.npmjs.org/cafy/-/cafy-12.0.0.tgz",
+			"integrity": "sha512-HGsunRfyqFyG1/oh+Szw8GtVpj4pwehyqmp8sTO1QwDF3htjDP+vVBWzg7iOU2Y3Cm+h+UiEpf6DJ0p57RNmAg=="
 		},
 		"caller-path": {
 			"version": "0.1.0",
@@ -15830,7 +15830,7 @@
 				},
 				"fast-deep-equal": {
 					"version": "1.1.0",
-					"resolved": "http://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
+					"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
 					"integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ="
 				},
 				"ignore": {
diff --git a/package.json b/package.json
index 4d03b73168..217124a423 100644
--- a/package.json
+++ b/package.json
@@ -91,7 +91,7 @@
 		"bcryptjs": "2.4.3",
 		"bee-queue": "1.2.2",
 		"bootstrap-vue": "2.0.0-rc.11",
-		"cafy": "11.3.0",
+		"cafy": "12.0.0",
 		"chai": "4.2.0",
 		"chai-http": "4.2.0",
 		"chalk": "2.4.1",
diff --git a/src/misc/cafy-id.ts b/src/misc/cafy-id.ts
index 3880f0bd0c..621f7e7948 100644
--- a/src/misc/cafy-id.ts
+++ b/src/misc/cafy-id.ts
@@ -4,23 +4,31 @@ import isObjectId from './is-objectid';
 
 export const isAnId = (x: any) => mongo.ObjectID.isValid(x);
 export const isNotAnId = (x: any) => !isAnId(x);
+export const transform = (x: string | mongo.ObjectID): mongo.ObjectID => {
+	if (x == null) return null;
+
+	if (isAnId(x) && !isObjectId(x)) {
+		return new mongo.ObjectID(x);
+	} else {
+		return x as mongo.ObjectID;
+	}
+};
+export const transformMany = (xs: (string | mongo.ObjectID)[]): mongo.ObjectID[] => {
+	if (xs == null) return null;
+
+	return xs.map(x => transform(x));
+};
+
+export type ObjectId = mongo.ObjectID;
 
 /**
  * ID
  */
-export default class ID extends Context<mongo.ObjectID> {
+export default class ID extends Context<string> {
 	constructor() {
 		super();
 
-		this.transform = v => {
-			if (isAnId(v) && !isObjectId(v)) {
-				return new mongo.ObjectID(v);
-			} else {
-				return v;
-			}
-		};
-
-		this.push(v => {
+		this.push((v: any) => {
 			if (!isObjectId(v) && isNotAnId(v)) {
 				return new Error('must-be-an-id');
 			}
diff --git a/src/models/user.ts b/src/models/user.ts
index f629ddbd20..1e5b6ad74e 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -189,7 +189,7 @@ export async function getRelation(me: mongo.ObjectId, target: mongo.ObjectId) {
 
 	return {
 		isFollowing: following1 !== null,
-		isStalking: following1 && following1.stalk,
+		isStalking: following1 ? following1.stalk : false,
 		hasPendingFollowRequestFromYou: followReq1 !== null,
 		hasPendingFollowRequestToYou: followReq2 !== null,
 		isFollowed: following2 !== null,
diff --git a/src/server/activitypub/followers.ts b/src/server/activitypub/followers.ts
index fcc75fc5b1..5c809424cc 100644
--- a/src/server/activitypub/followers.ts
+++ b/src/server/activitypub/followers.ts
@@ -1,7 +1,7 @@
 import * as mongo from 'mongodb';
 import * as Router from 'koa-router';
 import config from '../../config';
-import $ from 'cafy'; import ID from '../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../misc/cafy-id';
 import User from '../../models/user';
 import Following from '../../models/following';
 import pack from '../../remote/activitypub/renderer';
@@ -49,7 +49,7 @@ export default async (ctx: Router.IRouterContext) => {
 		// カーソルが指定されている場合
 		if (cursor) {
 			query._id = {
-				$lt: cursor
+				$lt: transform(cursor)
 			};
 		}
 
diff --git a/src/server/activitypub/following.ts b/src/server/activitypub/following.ts
index 2c739ff07d..a46bb9c7ff 100644
--- a/src/server/activitypub/following.ts
+++ b/src/server/activitypub/following.ts
@@ -1,7 +1,7 @@
 import * as mongo from 'mongodb';
 import * as Router from 'koa-router';
 import config from '../../config';
-import $ from 'cafy'; import ID from '../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../misc/cafy-id';
 import User from '../../models/user';
 import Following from '../../models/following';
 import pack from '../../remote/activitypub/renderer';
@@ -49,7 +49,7 @@ export default async (ctx: Router.IRouterContext) => {
 		// カーソルが指定されている場合
 		if (cursor) {
 			query._id = {
-				$lt: cursor
+				$lt: transform(cursor)
 			};
 		}
 
diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts
index aeb6f25dd4..24d4e3730e 100644
--- a/src/server/activitypub/outbox.ts
+++ b/src/server/activitypub/outbox.ts
@@ -1,7 +1,7 @@
 import * as mongo from 'mongodb';
 import * as Router from 'koa-router';
 import config from '../../config';
-import $ from 'cafy'; import ID from '../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../misc/cafy-id';
 import User from '../../models/user';
 import pack from '../../remote/activitypub/renderer';
 import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
@@ -61,11 +61,11 @@ export default async (ctx: Router.IRouterContext) => {
 		if (sinceId) {
 			sort._id = 1;
 			query._id = {
-				$gt: sinceId
+				$gt: transform(sinceId)
 			};
 		} else if (untilId) {
 			query._id = {
-				$lt: untilId
+				$lt: transform(untilId)
 			};
 		}
 		//#endregion
diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts
index 6e5ca90c63..e764ac2e95 100644
--- a/src/server/api/endpoints.ts
+++ b/src/server/api/endpoints.ts
@@ -1,12 +1,21 @@
+import { Context } from 'cafy';
 import * as path from 'path';
 import * as glob from 'glob';
 
 export interface IEndpointMeta {
-	stability?: 'deprecated' | 'experimental' | 'stable';
+	stability?: string; //'deprecated' | 'experimental' | 'stable';
 
-	desc?: any;
+	desc?: { [key: string]: string };
 
-	params?: any;
+	params?: {
+		[key: string]: {
+			validator: Context<any>;
+			transform?: any;
+			default?: any;
+			desc?: { [key: string]: string };
+			ref?: string;
+		};
+	};
 
 	res?: any;
 
diff --git a/src/server/api/endpoints/admin/suspend-user.ts b/src/server/api/endpoints/admin/suspend-user.ts
index 32c2416fb5..6d8b28932f 100644
--- a/src/server/api/endpoints/admin/suspend-user.ts
+++ b/src/server/api/endpoints/admin/suspend-user.ts
@@ -1,5 +1,5 @@
 import $ from 'cafy';
-import ID from '../../../../misc/cafy-id';
+import ID, { transform } from '../../../../misc/cafy-id';
 import getParams from '../../get-params';
 import User from '../../../../models/user';
 
@@ -13,12 +13,14 @@ export const meta = {
 	requireAdmin: true,
 
 	params: {
-		userId: $.type(ID).note({
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーID',
 				'en-US': 'The user ID which you want to suspend'
 			}
-		}),
+		},
 	}
 };
 
diff --git a/src/server/api/endpoints/admin/unsuspend-user.ts b/src/server/api/endpoints/admin/unsuspend-user.ts
index 879c23ab14..5c736122bb 100644
--- a/src/server/api/endpoints/admin/unsuspend-user.ts
+++ b/src/server/api/endpoints/admin/unsuspend-user.ts
@@ -1,5 +1,5 @@
 import $ from 'cafy';
-import ID from '../../../../misc/cafy-id';
+import ID, { transform } from '../../../../misc/cafy-id';
 import getParams from '../../get-params';
 import User from '../../../../models/user';
 
@@ -13,12 +13,14 @@ export const meta = {
 	requireAdmin: true,
 
 	params: {
-		userId: $.type(ID).note({
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーID',
 				'en-US': 'The user ID which you want to unsuspend'
 			}
-		}),
+		},
 	}
 };
 
diff --git a/src/server/api/endpoints/admin/unverify-user.ts b/src/server/api/endpoints/admin/unverify-user.ts
index 178049fa1d..fc55bd7476 100644
--- a/src/server/api/endpoints/admin/unverify-user.ts
+++ b/src/server/api/endpoints/admin/unverify-user.ts
@@ -1,5 +1,5 @@
 import $ from 'cafy';
-import ID from '../../../../misc/cafy-id';
+import ID, { transform } from '../../../../misc/cafy-id';
 import getParams from '../../get-params';
 import User from '../../../../models/user';
 
@@ -13,12 +13,14 @@ export const meta = {
 	requireAdmin: true,
 
 	params: {
-		userId: $.type(ID).note({
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーID',
 				'en-US': 'The user ID which you want to unverify'
 			}
-		}),
+		},
 	}
 };
 
diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts
index f45efd7fe5..aed53e12ce 100644
--- a/src/server/api/endpoints/admin/update-meta.ts
+++ b/src/server/api/endpoints/admin/update-meta.ts
@@ -11,41 +11,47 @@ export const meta = {
 	requireAdmin: true,
 
 	params: {
-		broadcasts: $.arr($.obj()).optional.nullable.note({
+		broadcasts: {
+			validator: $.arr($.obj()).optional.nullable,
 			desc: {
 				'ja-JP': 'ブロードキャスト'
 			}
-		}),
+		},
 
-		emojis: $.arr($.obj()).optional.note({
+		emojis: {
+			validator: $.arr($.obj()).optional,
 			desc: {
 				'ja-JP': 'カスタム絵文字定義'
 			}
-		}),
+		},
 
-		disableRegistration: $.bool.optional.nullable.note({
+		disableRegistration: {
+			validator: $.bool.optional.nullable,
 			desc: {
 				'ja-JP': '招待制か否か'
 			}
-		}),
+		},
 
-		disableLocalTimeline: $.bool.optional.nullable.note({
+		disableLocalTimeline: {
+			validator: $.bool.optional.nullable,
 			desc: {
 				'ja-JP': 'ローカルタイムライン(とソーシャルタイムライン)を無効にするか否か'
 			}
-		}),
+		},
 
-		hidedTags: $.arr($.str).optional.nullable.note({
+		hidedTags: {
+			validator: $.arr($.str).optional.nullable,
 			desc: {
 				'ja-JP': '統計などで無視するハッシュタグ'
 			}
-		}),
+		},
 
-		bannerUrl: $.str.optional.nullable.note({
+		bannerUrl: {
+			validator: $.str.optional.nullable,
 			desc: {
 				'ja-JP': 'インスタンスのバナー画像URL'
 			}
-		}),
+		},
 	}
 };
 
diff --git a/src/server/api/endpoints/admin/verify-user.ts b/src/server/api/endpoints/admin/verify-user.ts
index dd07684ded..b8c0bbaa8d 100644
--- a/src/server/api/endpoints/admin/verify-user.ts
+++ b/src/server/api/endpoints/admin/verify-user.ts
@@ -1,5 +1,5 @@
 import $ from 'cafy';
-import ID from '../../../../misc/cafy-id';
+import ID, { transform } from '../../../../misc/cafy-id';
 import getParams from '../../get-params';
 import User from '../../../../models/user';
 
@@ -13,12 +13,14 @@ export const meta = {
 	requireAdmin: true,
 
 	params: {
-		userId: $.type(ID).note({
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーID',
 				'en-US': 'The user ID which you want to verify'
 			}
-		}),
+		},
 	}
 };
 
diff --git a/src/server/api/endpoints/aggregation/hashtags.ts b/src/server/api/endpoints/aggregation/hashtags.ts
deleted file mode 100644
index ffeafb2538..0000000000
--- a/src/server/api/endpoints/aggregation/hashtags.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import Note from '../../../../models/note';
-import Meta from '../../../../models/meta';
-
-export default () => new Promise(async (res, rej) => {
-	const meta = await Meta.findOne({});
-	const hidedTags = meta ? (meta.hidedTags || []).map(t => t.toLowerCase()) : [];
-
-	const span = 1000 * 60 * 60 * 24 * 7; // 1週間
-
-	//#region 1. 指定期間の内に投稿されたハッシュタグ(とユーザーのペア)を集計
-	const data = await Note.aggregate([{
-		$match: {
-			createdAt: {
-				$gt: new Date(Date.now() - span)
-			},
-			tagsLower: {
-				$exists: true,
-				$ne: []
-			}
-		}
-	}, {
-		$unwind: '$tagsLower'
-	}, {
-		$group: {
-			_id: { tag: '$tagsLower', userId: '$userId' }
-		}
-	}]) as Array<{
-		_id: {
-			tag: string;
-			userId: any;
-		}
-	}>;
-	//#endregion
-
-	if (data.length == 0) {
-		return res([]);
-	}
-
-	let tags: Array<{
-		name: string;
-		count: number;
-	}> = [];
-
-	// カウント
-	data.map(x => x._id).forEach(x => {
-		// ブラックリストに登録されているタグなら弾く
-		if (hidedTags.includes(x.tag)) return;
-
-		const i = tags.findIndex(tag => tag.name == x.tag);
-		if (i != -1) {
-			tags[i].count++;
-		} else {
-			tags.push({
-				name: x.tag,
-				count: 1
-			});
-		}
-	});
-
-	// タグを人気順に並べ替え
-	tags = tags.sort((a, b) => b.count - a.count);
-
-	tags = tags.slice(0, 30);
-
-	res(tags);
-});
diff --git a/src/server/api/endpoints/aggregation/users/activity.ts b/src/server/api/endpoints/aggregation/users/activity.ts
deleted file mode 100644
index 0ec3f0db76..0000000000
--- a/src/server/api/endpoints/aggregation/users/activity.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
-import User from '../../../../../models/user';
-import Note from '../../../../../models/note';
-
-// TODO: likeやfollowも集計
-
-/**
- * Aggregate activity of a user
- */
-export default (params: any) => new Promise(async (res, rej) => {
-	// Get 'limit' parameter
-	const [limit = 365, limitErr] = $.num.optional.range(1, 365).get(params.limit);
-	if (limitErr) return rej('invalid limit param');
-
-	// Get 'userId' parameter
-	const [userId, userIdErr] = $.type(ID).get(params.userId);
-	if (userIdErr) return rej('invalid userId param');
-
-	// Lookup user
-	const user = await User.findOne({
-		_id: userId
-	}, {
-		fields: {
-			_id: true
-		}
-	});
-
-	if (user === null) {
-		return rej('user not found');
-	}
-
-	const datas = await Note
-		.aggregate([
-			{ $match: { userId: user._id } },
-			{ $project: {
-				renoteId: '$renoteId',
-				replyId: '$replyId',
-				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
-			}},
-			{ $project: {
-				date: {
-					year: { $year: '$createdAt' },
-					month: { $month: '$createdAt' },
-					day: { $dayOfMonth: '$createdAt' }
-				},
-				type: {
-					$cond: {
-						if: { $ne: ['$renoteId', null] },
-						then: 'renote',
-						else: {
-							$cond: {
-								if: { $ne: ['$replyId', null] },
-								then: 'reply',
-								else: 'note'
-							}
-						}
-					}
-				}}
-			},
-			{ $group: { _id: {
-				date: '$date',
-				type: '$type'
-			}, count: { $sum: 1 } } },
-			{ $group: {
-				_id: '$_id.date',
-				data: { $addToSet: {
-					type: '$_id.type',
-					count: '$count'
-				}}
-			} }
-		]);
-
-	datas.forEach((data: any) => {
-		data.date = data._id;
-		delete data._id;
-
-		data.notes = (data.data.filter((x: any) => x.type == 'note')[0] || { count: 0 }).count;
-		data.renotes = (data.data.filter((x: any) => x.type == 'renote')[0] || { count: 0 }).count;
-		data.replies = (data.data.filter((x: any) => x.type == 'reply')[0] || { count: 0 }).count;
-
-		delete data.data;
-	});
-
-	const graph = [];
-
-	for (let i = 0; i < limit; i++) {
-		const day = new Date(new Date().setDate(new Date().getDate() - i));
-
-		const data = datas.filter((d: any) =>
-			d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate()
-		)[0];
-
-		if (data) {
-			graph.push(data);
-		} else {
-			graph.push({
-				date: {
-					year: day.getFullYear(),
-					month: day.getMonth() + 1, // In JavaScript, month is zero-based.
-					day: day.getDate()
-				},
-				notes: 0,
-				renotes: 0,
-				replies: 0
-			});
-		}
-	}
-
-	res(graph);
-});
diff --git a/src/server/api/endpoints/aggregation/users/post.ts b/src/server/api/endpoints/aggregation/users/post.ts
deleted file mode 100644
index 090f6d2f09..0000000000
--- a/src/server/api/endpoints/aggregation/users/post.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
-import User from '../../../../../models/user';
-import Note from '../../../../../models/note';
-
-/**
- * Aggregate note of a user
- */
-export default (params: any) => new Promise(async (res, rej) => {
-	// Get 'userId' parameter
-	const [userId, userIdErr] = $.type(ID).get(params.userId);
-	if (userIdErr) return rej('invalid userId param');
-
-	// Lookup user
-	const user = await User.findOne({
-		_id: userId
-	}, {
-		fields: {
-			_id: true
-		}
-	});
-
-	if (user === null) {
-		return rej('user not found');
-	}
-
-	const datas = await Note
-		.aggregate([
-			{ $match: { userId: user._id } },
-			{ $project: {
-				renoteId: '$renoteId',
-				replyId: '$replyId',
-				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
-			}},
-			{ $project: {
-				date: {
-					year: { $year: '$createdAt' },
-					month: { $month: '$createdAt' },
-					day: { $dayOfMonth: '$createdAt' }
-				},
-				type: {
-					$cond: {
-						if: { $ne: ['$renoteId', null] },
-						then: 'renote',
-						else: {
-							$cond: {
-								if: { $ne: ['$replyId', null] },
-								then: 'reply',
-								else: 'note'
-							}
-						}
-					}
-				}}
-			},
-			{ $group: { _id: {
-				date: '$date',
-				type: '$type'
-			}, count: { $sum: 1 } } },
-			{ $group: {
-				_id: '$_id.date',
-				data: { $addToSet: {
-					type: '$_id.type',
-					count: '$count'
-				}}
-			} }
-		]);
-
-	datas.forEach((data: any) => {
-		data.date = data._id;
-		delete data._id;
-
-		data.notes = (data.data.filter((x: any) => x.type == 'note')[0] || { count: 0 }).count;
-		data.renotes = (data.data.filter((x: any) => x.type == 'renote')[0] || { count: 0 }).count;
-		data.replies = (data.data.filter((x: any) => x.type == 'reply')[0] || { count: 0 }).count;
-
-		delete data.data;
-	});
-
-	const graph = [];
-
-	for (let i = 0; i < 30; i++) {
-		const day = new Date(new Date().setDate(new Date().getDate() - i));
-
-		const data = datas.filter((d: any) =>
-			d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate()
-		)[0];
-
-		if (data) {
-			graph.push(data);
-		} else {
-			graph.push({
-				date: {
-					year: day.getFullYear(),
-					month: day.getMonth() + 1, // In JavaScript, month is zero-based.
-					day: day.getDate()
-				},
-				notes: 0,
-				renotes: 0,
-				replies: 0
-			});
-		}
-	}
-
-	res(graph);
-});
diff --git a/src/server/api/endpoints/aggregation/users/reaction.ts b/src/server/api/endpoints/aggregation/users/reaction.ts
deleted file mode 100644
index ce9e150966..0000000000
--- a/src/server/api/endpoints/aggregation/users/reaction.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
-import User from '../../../../../models/user';
-import Reaction from '../../../../../models/note-reaction';
-
-/**
- * Aggregate reaction of a user
- */
-export default (params: any) => new Promise(async (res, rej) => {
-	// Get 'userId' parameter
-	const [userId, userIdErr] = $.type(ID).get(params.userId);
-	if (userIdErr) return rej('invalid userId param');
-
-	// Lookup user
-	const user = await User.findOne({
-		_id: userId
-	}, {
-		fields: {
-			_id: true
-		}
-	});
-
-	if (user === null) {
-		return rej('user not found');
-	}
-
-	const datas = await Reaction
-		.aggregate([
-			{ $match: { userId: user._id } },
-			{ $project: {
-				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
-			}},
-			{ $project: {
-				date: {
-					year: { $year: '$createdAt' },
-					month: { $month: '$createdAt' },
-					day: { $dayOfMonth: '$createdAt' }
-				}
-			}},
-			{ $group: {
-				_id: '$date',
-				count: { $sum: 1 }
-			}}
-		]);
-
-	datas.forEach((data: any) => {
-		data.date = data._id;
-		delete data._id;
-	});
-
-	const graph = [];
-
-	for (let i = 0; i < 30; i++) {
-		const day = new Date(new Date().setDate(new Date().getDate() - i));
-
-		const data = datas.filter((d: any) =>
-			d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate()
-		)[0];
-
-		if (data) {
-			graph.push(data);
-		} else {
-			graph.push({
-				date: {
-					year: day.getFullYear(),
-					month: day.getMonth() + 1, // In JavaScript, month is zero-based.
-					day: day.getDate()
-				},
-				count: 0
-			});
-		}
-	}
-
-	res(graph);
-});
diff --git a/src/server/api/endpoints/ap/show.ts b/src/server/api/endpoints/ap/show.ts
index 490b2f1ad5..c5286583d0 100644
--- a/src/server/api/endpoints/ap/show.ts
+++ b/src/server/api/endpoints/ap/show.ts
@@ -16,11 +16,12 @@ export const meta = {
 	requireCredential: false,
 
 	params: {
-		uri: $.str.note({
+		uri: {
+			validator: $.str,
 			desc: {
 				'ja-JP': 'ActivityPubオブジェクトのURI'
 			}
-		}),
+		},
 	},
 };
 
diff --git a/src/server/api/endpoints/app/show.ts b/src/server/api/endpoints/app/show.ts
index 8cc2abdc28..0d73985b48 100644
--- a/src/server/api/endpoints/app/show.ts
+++ b/src/server/api/endpoints/app/show.ts
@@ -1,19 +1,25 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import App, { pack, IApp } from '../../../../models/app';
 import { ILocalUser } from '../../../../models/user';
+import getParams from '../../get-params';
+
+export const meta = {
+	params: {
+		appId: {
+			validator: $.type(ID),
+			transform: transform
+		},
+	}
+};
 
-/**
- * Show an app
- */
 export default (params: any, user: ILocalUser, app: IApp) => new Promise(async (res, rej) => {
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
+
 	const isSecure = user != null && app == null;
 
-	// Get 'appId' parameter
-	const [appId, appIdErr] = $.type(ID).get(params.appId);
-	if (appIdErr) return rej('invalid appId param');
-
 	// Lookup app
-	const ap = await App.findOne({ _id: appId });
+	const ap = await App.findOne({ _id: ps.appId });
 
 	if (ap === null) {
 		return rej('app not found');
diff --git a/src/server/api/endpoints/blocking/create.ts b/src/server/api/endpoints/blocking/create.ts
index c3a4d24ae0..7ec01b4237 100644
--- a/src/server/api/endpoints/blocking/create.ts
+++ b/src/server/api/endpoints/blocking/create.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 const ms = require('ms');
 import User, { pack, ILocalUser } from '../../../../models/user';
 import Blocking from '../../../../models/blocking';
@@ -23,12 +23,14 @@ export const meta = {
 	kind: 'following-write',
 
 	params: {
-		userId: $.type(ID).note({
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/blocking/delete.ts b/src/server/api/endpoints/blocking/delete.ts
index e712d54d30..adf8d8c500 100644
--- a/src/server/api/endpoints/blocking/delete.ts
+++ b/src/server/api/endpoints/blocking/delete.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 const ms = require('ms');
 import User, { pack, ILocalUser } from '../../../../models/user';
 import Blocking from '../../../../models/blocking';
@@ -23,12 +23,14 @@ export const meta = {
 	kind: 'following-write',
 
 	params: {
-		userId: $.type(ID).note({
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/blocking/list.ts b/src/server/api/endpoints/blocking/list.ts
index a0bef38b58..52f55805d0 100644
--- a/src/server/api/endpoints/blocking/list.ts
+++ b/src/server/api/endpoints/blocking/list.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import Blocking, { packMany } from '../../../../models/blocking';
 import { ILocalUser } from '../../../../models/user';
 import getParams from '../../get-params';
@@ -14,15 +14,20 @@ export const meta = {
 	kind: 'following-read',
 
 	params: {
-		limit: $.num.optional.range(1, 100).note({
+		limit: {
+			validator: $.num.optional.range(1, 100),
 			default: 30
-		}),
+		},
 
-		sinceId: $.type(ID).optional.note({
-		}),
+		sinceId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
 
-		untilId: $.type(ID).optional.note({
-		}),
+		untilId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
 	}
 };
 
diff --git a/src/server/api/endpoints/charts/drive.ts b/src/server/api/endpoints/charts/drive.ts
index dc18331847..2200081330 100644
--- a/src/server/api/endpoints/charts/drive.ts
+++ b/src/server/api/endpoints/charts/drive.ts
@@ -8,18 +8,20 @@ export const meta = {
 	},
 
 	params: {
-		span: $.str.or(['day', 'hour']).note({
+		span: {
+			validator: $.str.or(['day', 'hour']),
 			desc: {
 				'ja-JP': '集計のスパン (day または hour)'
 			}
-		}),
+		},
 
-		limit: $.num.optional.range(1, 100).note({
+		limit: {
+			validator: $.num.optional.range(1, 100),
 			default: 30,
 			desc: {
 				'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
 			}
-		}),
+		},
 	}
 };
 
diff --git a/src/server/api/endpoints/charts/federation.ts b/src/server/api/endpoints/charts/federation.ts
index 5b24783c69..ba19aae04c 100644
--- a/src/server/api/endpoints/charts/federation.ts
+++ b/src/server/api/endpoints/charts/federation.ts
@@ -8,18 +8,20 @@ export const meta = {
 	},
 
 	params: {
-		span: $.str.or(['day', 'hour']).note({
+		span: {
+			validator: $.str.or(['day', 'hour']),
 			desc: {
 				'ja-JP': '集計のスパン (day または hour)'
 			}
-		}),
+		},
 
-		limit: $.num.optional.range(1, 100).note({
+		limit: {
+			validator: $.num.optional.range(1, 100),
 			default: 30,
 			desc: {
 				'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
 			}
-		}),
+		},
 	}
 };
 
diff --git a/src/server/api/endpoints/charts/hashtag.ts b/src/server/api/endpoints/charts/hashtag.ts
index bcd48dc485..2ef6a06062 100644
--- a/src/server/api/endpoints/charts/hashtag.ts
+++ b/src/server/api/endpoints/charts/hashtag.ts
@@ -8,24 +8,27 @@ export const meta = {
 	},
 
 	params: {
-		span: $.str.or(['day', 'hour']).note({
+		span: {
+			validator: $.str.or(['day', 'hour']),
 			desc: {
 				'ja-JP': '集計のスパン (day または hour)'
 			}
-		}),
+		},
 
-		limit: $.num.optional.range(1, 100).note({
+		limit: {
+			validator: $.num.optional.range(1, 100),
 			default: 30,
 			desc: {
 				'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
 			}
-		}),
+		},
 
-		tag: $.str.note({
+		tag: {
+			validator: $.str,
 			desc: {
 				'ja-JP': '対象のハッシュタグ'
 			}
-		}),
+		},
 	}
 };
 
diff --git a/src/server/api/endpoints/charts/network.ts b/src/server/api/endpoints/charts/network.ts
index d5b0791994..249504f2e1 100644
--- a/src/server/api/endpoints/charts/network.ts
+++ b/src/server/api/endpoints/charts/network.ts
@@ -8,18 +8,20 @@ export const meta = {
 	},
 
 	params: {
-		span: $.str.or(['day', 'hour']).note({
+		span: {
+			validator: $.str.or(['day', 'hour']),
 			desc: {
 				'ja-JP': '集計のスパン (day または hour)'
 			}
-		}),
+		},
 
-		limit: $.num.optional.range(1, 100).note({
+		limit: {
+			validator: $.num.optional.range(1, 100),
 			default: 30,
 			desc: {
 				'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
 			}
-		}),
+		},
 	}
 };
 
diff --git a/src/server/api/endpoints/charts/notes.ts b/src/server/api/endpoints/charts/notes.ts
index 573b012469..0f04a4bd90 100644
--- a/src/server/api/endpoints/charts/notes.ts
+++ b/src/server/api/endpoints/charts/notes.ts
@@ -8,18 +8,20 @@ export const meta = {
 	},
 
 	params: {
-		span: $.str.or(['day', 'hour']).note({
+		span: {
+			validator: $.str.or(['day', 'hour']),
 			desc: {
 				'ja-JP': '集計のスパン (day または hour)'
 			}
-		}),
+		},
 
-		limit: $.num.optional.range(1, 100).note({
+		limit: {
+			validator: $.num.optional.range(1, 100),
 			default: 30,
 			desc: {
 				'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
 			}
-		}),
+		},
 	}
 };
 
diff --git a/src/server/api/endpoints/charts/user/drive.ts b/src/server/api/endpoints/charts/user/drive.ts
index 2626c36c9d..beb6b5bea3 100644
--- a/src/server/api/endpoints/charts/user/drive.ts
+++ b/src/server/api/endpoints/charts/user/drive.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
 import getParams from '../../../get-params';
 import perUserDriveChart from '../../../../../chart/per-user-drive';
-import ID from '../../../../../misc/cafy-id';
+import ID, { transform } from '../../../../../misc/cafy-id';
 
 export const meta = {
 	desc: {
@@ -9,25 +9,29 @@ export const meta = {
 	},
 
 	params: {
-		span: $.str.or(['day', 'hour']).note({
+		span: {
+			validator: $.str.or(['day', 'hour']),
 			desc: {
 				'ja-JP': '集計のスパン (day または hour)'
 			}
-		}),
+		},
 
-		limit: $.num.optional.range(1, 100).note({
+		limit: {
+			validator: $.num.optional.range(1, 100),
 			default: 30,
 			desc: {
 				'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
 			}
-		}),
+		},
 
-		userId: $.type(ID).note({
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/charts/user/following.ts b/src/server/api/endpoints/charts/user/following.ts
index 57c15cdcfe..e0aebf4255 100644
--- a/src/server/api/endpoints/charts/user/following.ts
+++ b/src/server/api/endpoints/charts/user/following.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
 import getParams from '../../../get-params';
 import perUserFollowingChart from '../../../../../chart/per-user-following';
-import ID from '../../../../../misc/cafy-id';
+import ID, { transform } from '../../../../../misc/cafy-id';
 
 export const meta = {
 	desc: {
@@ -9,25 +9,29 @@ export const meta = {
 	},
 
 	params: {
-		span: $.str.or(['day', 'hour']).note({
+		span: {
+			validator: $.str.or(['day', 'hour']),
 			desc: {
 				'ja-JP': '集計のスパン (day または hour)'
 			}
-		}),
+		},
 
-		limit: $.num.optional.range(1, 100).note({
+		limit: {
+			validator: $.num.optional.range(1, 100),
 			default: 30,
 			desc: {
 				'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
 			}
-		}),
+		},
 
-		userId: $.type(ID).note({
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/charts/user/notes.ts b/src/server/api/endpoints/charts/user/notes.ts
index 66051fa7c6..251de4d399 100644
--- a/src/server/api/endpoints/charts/user/notes.ts
+++ b/src/server/api/endpoints/charts/user/notes.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
 import getParams from '../../../get-params';
 import perUserNotesChart from '../../../../../chart/per-user-notes';
-import ID from '../../../../../misc/cafy-id';
+import ID, { transform } from '../../../../../misc/cafy-id';
 
 export const meta = {
 	desc: {
@@ -9,25 +9,29 @@ export const meta = {
 	},
 
 	params: {
-		span: $.str.or(['day', 'hour']).note({
+		span: {
+			validator: $.str.or(['day', 'hour']),
 			desc: {
 				'ja-JP': '集計のスパン (day または hour)'
 			}
-		}),
+		},
 
-		limit: $.num.optional.range(1, 100).note({
+		limit: {
+			validator: $.num.optional.range(1, 100),
 			default: 30,
 			desc: {
 				'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
 			}
-		}),
+		},
 
-		userId: $.type(ID).note({
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/charts/user/reactions.ts b/src/server/api/endpoints/charts/user/reactions.ts
index 60cdaa70bb..f812400709 100644
--- a/src/server/api/endpoints/charts/user/reactions.ts
+++ b/src/server/api/endpoints/charts/user/reactions.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
 import getParams from '../../../get-params';
 import perUserReactionsChart from '../../../../../chart/per-user-reactions';
-import ID from '../../../../../misc/cafy-id';
+import ID, { transform } from '../../../../../misc/cafy-id';
 
 export const meta = {
 	desc: {
@@ -9,25 +9,29 @@ export const meta = {
 	},
 
 	params: {
-		span: $.str.or(['day', 'hour']).note({
+		span: {
+			validator: $.str.or(['day', 'hour']),
 			desc: {
 				'ja-JP': '集計のスパン (day または hour)'
 			}
-		}),
+		},
 
-		limit: $.num.optional.range(1, 100).note({
+		limit: {
+			validator: $.num.optional.range(1, 100),
 			default: 30,
 			desc: {
 				'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
 			}
-		}),
+		},
 
-		userId: $.type(ID).note({
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/charts/users.ts b/src/server/api/endpoints/charts/users.ts
index 595bb63f0b..9c23235b23 100644
--- a/src/server/api/endpoints/charts/users.ts
+++ b/src/server/api/endpoints/charts/users.ts
@@ -8,18 +8,20 @@ export const meta = {
 	},
 
 	params: {
-		span: $.str.or(['day', 'hour']).note({
+		span: {
+			validator: $.str.or(['day', 'hour']),
 			desc: {
 				'ja-JP': '集計のスパン (day または hour)'
 			}
-		}),
+		},
 
-		limit: $.num.optional.range(1, 100).note({
+		limit: {
+			validator: $.num.optional.range(1, 100),
 			default: 30,
 			desc: {
 				'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
 			}
-		}),
+		},
 	}
 };
 
diff --git a/src/server/api/endpoints/drive/files.ts b/src/server/api/endpoints/drive/files.ts
index de0bde086b..ae4e2249ff 100644
--- a/src/server/api/endpoints/drive/files.ts
+++ b/src/server/api/endpoints/drive/files.ts
@@ -1,6 +1,7 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import DriveFile, { packMany } from '../../../../models/drive-file';
 import { ILocalUser } from '../../../../models/user';
+import getParams from '../../get-params';
 
 export const meta = {
 	desc: {
@@ -10,68 +11,75 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'drive-read'
+	kind: 'drive-read',
+
+	params: {
+		limit: {
+			validator: $.num.optional.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
+
+		untilId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
+
+		folderId: {
+			validator: $.type(ID).optional.nullable,
+			default: null as any,
+			transform: transform,
+		},
+
+		type: {
+			validator: $.str.optional.match(/^[a-zA-Z\/\-\*]+$/)
+		}
+	}
 };
 
 export default async (params: any, user: ILocalUser) => {
-	// Get 'limit' parameter
-	const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
-	if (limitErr) throw 'invalid limit param';
-
-	// Get 'sinceId' parameter
-	const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId);
-	if (sinceIdErr) throw 'invalid sinceId param';
-
-	// Get 'untilId' parameter
-	const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId);
-	if (untilIdErr) throw 'invalid untilId param';
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) throw psErr;
 
 	// Check if both of sinceId and untilId is specified
-	if (sinceId && untilId) {
+	if (ps.sinceId && ps.untilId) {
 		throw 'cannot set sinceId and untilId';
 	}
 
-	// Get 'folderId' parameter
-	const [folderId = null, folderIdErr] = $.type(ID).optional.nullable.get(params.folderId);
-	if (folderIdErr) throw 'invalid folderId param';
-
-	// Get 'type' parameter
-	const [type, typeErr] = $.str.optional.match(/^[a-zA-Z\/\-\*]+$/).get(params.type);
-	if (typeErr) throw 'invalid type param';
-
-	// Construct query
 	const sort = {
 		_id: -1
 	};
 
 	const query = {
 		'metadata.userId': user._id,
-		'metadata.folderId': folderId,
+		'metadata.folderId': ps.folderId,
 		'metadata.deletedAt': { $exists: false }
 	} as any;
 
-	if (sinceId) {
+	if (ps.sinceId) {
 		sort._id = 1;
 		query._id = {
-			$gt: sinceId
+			$gt: ps.sinceId
 		};
-	} else if (untilId) {
+	} else if (ps.untilId) {
 		query._id = {
-			$lt: untilId
+			$lt: ps.untilId
 		};
 	}
 
-	if (type) {
-		query.contentType = new RegExp(`^${type.replace(/\*/g, '.+?')}$`);
+	if (ps.type) {
+		query.contentType = new RegExp(`^${ps.type.replace(/\*/g, '.+?')}$`);
 	}
 
-	// Issue query
 	const files = await DriveFile
 		.find(query, {
-			limit: limit,
+			limit: ps.limit,
 			sort: sort
 		});
 
-	// Serialize
 	return await packMany(files);
 };
diff --git a/src/server/api/endpoints/drive/files/attached_notes.ts b/src/server/api/endpoints/drive/files/attached_notes.ts
index 1187169c64..ad9a2370b1 100644
--- a/src/server/api/endpoints/drive/files/attached_notes.ts
+++ b/src/server/api/endpoints/drive/files/attached_notes.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 import DriveFile from '../../../../../models/drive-file';
 import { ILocalUser } from '../../../../../models/user';
 import getParams from '../../../get-params';
@@ -17,12 +17,14 @@ export const meta = {
 	kind: 'drive-read',
 
 	params: {
-		fileId: $.type(ID).note({
+		fileId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象のファイルID',
 				'en-US': 'Target file ID'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/drive/files/check_existence.ts b/src/server/api/endpoints/drive/files/check_existence.ts
index a024701655..407c7d5530 100644
--- a/src/server/api/endpoints/drive/files/check_existence.ts
+++ b/src/server/api/endpoints/drive/files/check_existence.ts
@@ -13,11 +13,12 @@ export const meta = {
 	kind: 'drive-read',
 
 	params: {
-		md5: $.str.note({
+		md5: {
+			validator: $.str,
 			desc: {
 				'ja-JP': 'ファイルのMD5ハッシュ'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts
index 2fa4c65846..2653eba655 100644
--- a/src/server/api/endpoints/drive/files/create.ts
+++ b/src/server/api/endpoints/drive/files/create.ts
@@ -1,6 +1,6 @@
 import * as fs from 'fs';
 const ms = require('ms');
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 import { validateFileName, pack } from '../../../../../models/drive-file';
 import create from '../../../../../services/drive/add-file';
 import { ILocalUser } from '../../../../../models/user';
@@ -24,27 +24,31 @@ export const meta = {
 	kind: 'drive-write',
 
 	params: {
-		folderId: $.type(ID).optional.nullable.note({
-			default: null,
+		folderId: {
+			validator: $.type(ID).optional.nullable,
+			transform: transform,
+			default: null as any,
 			desc: {
 				'ja-JP': 'フォルダID'
 			}
-		}),
+		},
 
-		isSensitive: $.bool.optional.note({
+		isSensitive: {
+			validator: $.bool.optional,
 			default: false,
 			desc: {
 				'ja-JP': 'このメディアが「閲覧注意」(NSFW)かどうか',
 				'en-US': 'Whether this media is NSFW'
 			}
-		}),
+		},
 
-		force: $.bool.optional.note({
+		force: {
+			validator: $.bool.optional,
 			default: false,
 			desc: {
 				'ja-JP': 'true にすると、同じハッシュを持つファイルが既にアップロードされていても強制的にファイルを作成します。',
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/drive/files/delete.ts b/src/server/api/endpoints/drive/files/delete.ts
index fc6849e57e..af7e192655 100644
--- a/src/server/api/endpoints/drive/files/delete.ts
+++ b/src/server/api/endpoints/drive/files/delete.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 import DriveFile from '../../../../../models/drive-file';
 import del from '../../../../../services/drive/delete-file';
 import { publishDriveStream } from '../../../../../stream';
@@ -18,12 +18,14 @@ export const meta = {
 	kind: 'drive-write',
 
 	params: {
-		fileId: $.type(ID).note({
+		fileId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象のファイルID',
 				'en-US': 'Target file ID'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/drive/files/find.ts b/src/server/api/endpoints/drive/files/find.ts
index aa44ee688e..0ac110c110 100644
--- a/src/server/api/endpoints/drive/files/find.ts
+++ b/src/server/api/endpoints/drive/files/find.ts
@@ -1,31 +1,39 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 import DriveFile, { pack } from '../../../../../models/drive-file';
 import { ILocalUser } from '../../../../../models/user';
+import getParams from '../../../get-params';
 
 export const meta = {
 	requireCredential: true,
 
-	kind: 'drive-read'
+	kind: 'drive-read',
+
+	params: {
+		name: {
+			validator: $.str
+		},
+
+		folderId: {
+			validator: $.type(ID).optional.nullable,
+			transform: transform,
+			default: null as any,
+			desc: {
+				'ja-JP': 'フォルダID'
+			}
+		},
+	}
 };
 
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'name' parameter
-	const [name, nameErr] = $.str.get(params.name);
-	if (nameErr) return rej('invalid name param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
-	// Get 'folderId' parameter
-	const [folderId = null, folderIdErr] = $.type(ID).optional.nullable.get(params.folderId);
-	if (folderIdErr) return rej('invalid folderId param');
-
-	// Issue query
 	const files = await DriveFile
 		.find({
 			filename: name,
 			'metadata.userId': user._id,
-			'metadata.folderId': folderId
+			'metadata.folderId': ps.folderId
 		});
 
-	// Serialize
-	res(await Promise.all(files.map(async file =>
-		await pack(file))));
+	res(await Promise.all(files.map(file => pack(file))));
 });
diff --git a/src/server/api/endpoints/drive/files/show.ts b/src/server/api/endpoints/drive/files/show.ts
index 49d6027add..ce0812c508 100644
--- a/src/server/api/endpoints/drive/files/show.ts
+++ b/src/server/api/endpoints/drive/files/show.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 import DriveFile, { pack } from '../../../../../models/drive-file';
 import { ILocalUser } from '../../../../../models/user';
 import getParams from '../../../get-params';
@@ -16,12 +16,14 @@ export const meta = {
 	kind: 'drive-read',
 
 	params: {
-		fileId: $.type(ID).note({
+		fileId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象のファイルID',
 				'en-US': 'Target file ID'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts
index 915cf4ceb2..7c335e5bc6 100644
--- a/src/server/api/endpoints/drive/files/update.ts
+++ b/src/server/api/endpoints/drive/files/update.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 import DriveFolder from '../../../../../models/drive-folder';
 import DriveFile, { validateFileName, pack } from '../../../../../models/drive-file';
 import { publishDriveStream } from '../../../../../stream';
@@ -17,34 +17,40 @@ export const meta = {
 	kind: 'drive-write',
 
 	params: {
-		fileId: $.type(ID).note({
+		fileId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象のファイルID'
 			}
-		}),
+		},
 
-		folderId: $.type(ID).optional.nullable.note({
-			default: undefined,
+		folderId: {
+			validator: $.type(ID).optional.nullable,
+			transform: transform,
+			default: undefined as any,
 			desc: {
 				'ja-JP': 'フォルダID'
 			}
-		}),
+		},
 
-		name: $.str.optional.pipe(validateFileName).note({
-			default: undefined,
+		name: {
+			validator: $.str.optional.pipe(validateFileName),
+			default: undefined as any,
 			desc: {
 				'ja-JP': 'ファイル名',
 				'en-US': 'Name of the file'
 			}
-		}),
+		},
 
-		isSensitive: $.bool.optional.note({
-			default: undefined,
+		isSensitive: {
+			validator: $.bool.optional,
+			default: undefined as any,
 			desc: {
 				'ja-JP': 'このメディアが「閲覧注意」(NSFW)かどうか',
 				'en-US': 'Whether this media is NSFW'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/drive/files/upload_from_url.ts b/src/server/api/endpoints/drive/files/upload_from_url.ts
index 783646feb3..3d19725275 100644
--- a/src/server/api/endpoints/drive/files/upload_from_url.ts
+++ b/src/server/api/endpoints/drive/files/upload_from_url.ts
@@ -1,8 +1,9 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 const ms = require('ms');
 import { pack } from '../../../../../models/drive-file';
 import uploadFromUrl from '../../../../../services/drive/upload-from-url';
 import { ILocalUser } from '../../../../../models/user';
+import getParams from '../../../get-params';
 
 export const meta = {
 	desc: {
@@ -16,21 +17,25 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'drive-write'
+	kind: 'drive-write',
+
+	params: {
+		url: {
+			// TODO: Validate this url
+			validator: $.str,
+		},
+
+		folderId: {
+			validator: $.type(ID).optional.nullable,
+			default: null as any as any,
+			transform: transform
+		},
+	}
 };
 
-/**
- * Create a file from a URL
- */
 export default async (params: any, user: ILocalUser): Promise<any> => {
-	// Get 'url' parameter
-	// TODO: Validate this url
-	const [url, urlErr] = $.str.get(params.url);
-	if (urlErr) throw 'invalid url param';
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) throw psErr;
 
-	// Get 'folderId' parameter
-	const [folderId = null, folderIdErr] = $.type(ID).optional.nullable.get(params.folderId);
-	if (folderIdErr) throw 'invalid folderId param';
-
-	return pack(await uploadFromUrl(url, user, folderId));
+	return pack(await uploadFromUrl(ps.url, user, ps.folderId));
 };
diff --git a/src/server/api/endpoints/drive/folders.ts b/src/server/api/endpoints/drive/folders.ts
index 19c2ef7aca..95700ee26c 100644
--- a/src/server/api/endpoints/drive/folders.ts
+++ b/src/server/api/endpoints/drive/folders.ts
@@ -1,6 +1,7 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import DriveFolder, { pack } from '../../../../models/drive-folder';
 import { ILocalUser } from '../../../../models/user';
+import getParams from '../../get-params';
 
 export const meta = {
 	desc: {
@@ -10,58 +11,64 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'drive-read'
+	kind: 'drive-read',
+
+	params: {
+		limit: {
+			validator: $.num.optional.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
+
+		untilId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
+
+		folderId: {
+			validator: $.type(ID).optional.nullable,
+			default: null as any,
+			transform: transform,
+		}
+	}
 };
 
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'limit' parameter
-	const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
-	if (limitErr) return rej('invalid limit param');
-
-	// Get 'sinceId' parameter
-	const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId);
-	if (sinceIdErr) return rej('invalid sinceId param');
-
-	// Get 'untilId' parameter
-	const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId);
-	if (untilIdErr) return rej('invalid untilId param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
 	// Check if both of sinceId and untilId is specified
-	if (sinceId && untilId) {
+	if (ps.sinceId && ps.untilId) {
 		return rej('cannot set sinceId and untilId');
 	}
 
-	// Get 'folderId' parameter
-	const [folderId = null, folderIdErr] = $.type(ID).optional.nullable.get(params.folderId);
-	if (folderIdErr) return rej('invalid folderId param');
-
-	// Construct query
 	const sort = {
 		_id: -1
 	};
 	const query = {
 		userId: user._id,
-		parentId: folderId
+		parentId: ps.folderId
 	} as any;
-	if (sinceId) {
+	if (ps.sinceId) {
 		sort._id = 1;
 		query._id = {
-			$gt: sinceId
+			$gt: ps.sinceId
 		};
-	} else if (untilId) {
+	} else if (ps.untilId) {
 		query._id = {
-			$lt: untilId
+			$lt: ps.untilId
 		};
 	}
 
-	// Issue query
 	const folders = await DriveFolder
 		.find(query, {
-			limit: limit,
+			limit: ps.limit,
 			sort: sort
 		});
 
-	// Serialize
-	res(await Promise.all(folders.map(async folder =>
-		await pack(folder))));
+	res(await Promise.all(folders.map(folder => pack(folder))));
 });
diff --git a/src/server/api/endpoints/drive/folders/create.ts b/src/server/api/endpoints/drive/folders/create.ts
index cca25b0596..b51fb0264e 100644
--- a/src/server/api/endpoints/drive/folders/create.ts
+++ b/src/server/api/endpoints/drive/folders/create.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder';
 import { publishDriveStream } from '../../../../../stream';
 import { ILocalUser } from '../../../../../models/user';
@@ -17,20 +17,23 @@ export const meta = {
 	kind: 'drive-write',
 
 	params: {
-		name: $.str.optional.pipe(isValidFolderName).note({
+		name: {
+			validator: $.str.optional.pipe(isValidFolderName),
 			default: 'Untitled',
 			desc: {
 				'ja-JP': 'フォルダ名',
 				'en-US': 'Folder name'
 			}
-		}),
+		},
 
-		parentId: $.type(ID).optional.nullable.note({
+		parentId: {
+			validator: $.type(ID).optional.nullable,
+			transform: transform,
 			desc: {
 				'ja-JP': '親フォルダID',
 				'en-US': 'Parent folder ID'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/drive/folders/delete.ts b/src/server/api/endpoints/drive/folders/delete.ts
index 41f9108788..304666bdc9 100644
--- a/src/server/api/endpoints/drive/folders/delete.ts
+++ b/src/server/api/endpoints/drive/folders/delete.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 import DriveFolder from '../../../../../models/drive-folder';
 import { ILocalUser } from '../../../../../models/user';
 import getParams from '../../../get-params';
@@ -18,12 +18,14 @@ export const meta = {
 	kind: 'drive-write',
 
 	params: {
-		folderId: $.type(ID).note({
+		folderId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象のフォルダID',
 				'en-US': 'Target folder ID'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/drive/folders/find.ts b/src/server/api/endpoints/drive/folders/find.ts
index ec3c1d2e36..3d24d18685 100644
--- a/src/server/api/endpoints/drive/folders/find.ts
+++ b/src/server/api/endpoints/drive/folders/find.ts
@@ -1,30 +1,39 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 import DriveFolder, { pack } from '../../../../../models/drive-folder';
 import { ILocalUser } from '../../../../../models/user';
+import getParams from '../../../get-params';
 
 export const meta = {
 	requireCredential: true,
 
-	kind: 'drive-read'
+	kind: 'drive-read',
+
+	params: {
+		name: {
+			validator: $.str
+		},
+
+		parentId: {
+			validator: $.type(ID).optional.nullable,
+			transform: transform,
+			default: null as any,
+			desc: {
+				'ja-JP': 'フォルダID'
+			}
+		},
+	}
 };
 
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'name' parameter
-	const [name, nameErr] = $.str.get(params.name);
-	if (nameErr) return rej('invalid name param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
-	// Get 'parentId' parameter
-	const [parentId = null, parentIdErr] = $.type(ID).optional.nullable.get(params.parentId);
-	if (parentIdErr) return rej('invalid parentId param');
-
-	// Issue query
 	const folders = await DriveFolder
 		.find({
 			name: name,
 			userId: user._id,
-			parentId: parentId
+			parentId: ps.parentId
 		});
 
-	// Serialize
 	res(await Promise.all(folders.map(folder => pack(folder))));
 });
diff --git a/src/server/api/endpoints/drive/folders/show.ts b/src/server/api/endpoints/drive/folders/show.ts
index f01c75d957..b7d8f0a29b 100644
--- a/src/server/api/endpoints/drive/folders/show.ts
+++ b/src/server/api/endpoints/drive/folders/show.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 import DriveFolder, { pack } from '../../../../../models/drive-folder';
 import { ILocalUser } from '../../../../../models/user';
 import getParams from '../../../get-params';
@@ -16,12 +16,14 @@ export const meta = {
 	kind: 'drive-read',
 
 	params: {
-		folderId: $.type(ID).note({
+		folderId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象のフォルダID',
 				'en-US': 'Target folder ID'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/drive/folders/update.ts b/src/server/api/endpoints/drive/folders/update.ts
index b041a15920..53bb14bfe7 100644
--- a/src/server/api/endpoints/drive/folders/update.ts
+++ b/src/server/api/endpoints/drive/folders/update.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder';
 import { publishDriveStream } from '../../../../../stream';
 import { ILocalUser } from '../../../../../models/user';
@@ -17,26 +17,31 @@ export const meta = {
 	kind: 'drive-write',
 
 	params: {
-		folderId: $.type(ID).note({
+		folderId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象のフォルダID',
 				'en-US': 'Target folder ID'
 			}
-		}),
+		},
 
-		name: $.str.optional.pipe(isValidFolderName).note({
+		name: {
+			validator: $.str.optional.pipe(isValidFolderName),
 			desc: {
 				'ja-JP': 'フォルダ名',
 				'en-US': 'Folder name'
 			}
-		}),
+		},
 
-		parentId: $.type(ID).optional.nullable.note({
+		parentId: {
+			validator: $.type(ID).optional.nullable,
+			transform: transform,
 			desc: {
 				'ja-JP': '親フォルダID',
 				'en-US': 'Parent folder ID'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/drive/stream.ts b/src/server/api/endpoints/drive/stream.ts
index 3ac7dd0234..ecf405fe64 100644
--- a/src/server/api/endpoints/drive/stream.ts
+++ b/src/server/api/endpoints/drive/stream.ts
@@ -1,36 +1,44 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import DriveFile, { packMany } from '../../../../models/drive-file';
 import { ILocalUser } from '../../../../models/user';
+import getParams from '../../get-params';
 
 export const meta = {
 	requireCredential: true,
 
-	kind: 'drive-read'
+	kind: 'drive-read',
+
+	params: {
+		limit: {
+			validator: $.num.optional.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
+
+		untilId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
+
+		type: {
+			validator: $.str.optional.match(/^[a-zA-Z\/\-\*]+$/)
+		}
+	}
 };
 
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'limit' parameter
-	const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
-	if (limitErr) return rej('invalid limit param');
-
-	// Get 'sinceId' parameter
-	const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId);
-	if (sinceIdErr) return rej('invalid sinceId param');
-
-	// Get 'untilId' parameter
-	const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId);
-	if (untilIdErr) return rej('invalid untilId param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
 	// Check if both of sinceId and untilId is specified
-	if (sinceId && untilId) {
+	if (ps.sinceId && ps.untilId) {
 		return rej('cannot set sinceId and untilId');
 	}
 
-	// Get 'type' parameter
-	const [type, typeErr] = $.str.optional.match(/^[a-zA-Z\/\-\*]+$/).get(params.type);
-	if (typeErr) return rej('invalid type param');
-
-	// Construct query
 	const sort = {
 		_id: -1
 	};
@@ -40,28 +48,26 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 		'metadata.deletedAt': { $exists: false }
 	} as any;
 
-	if (sinceId) {
+	if (ps.sinceId) {
 		sort._id = 1;
 		query._id = {
-			$gt: sinceId
+			$gt: ps.sinceId
 		};
-	} else if (untilId) {
+	} else if (ps.untilId) {
 		query._id = {
-			$lt: untilId
+			$lt: ps.untilId
 		};
 	}
 
-	if (type) {
-		query.contentType = new RegExp(`^${type.replace(/\*/g, '.+?')}$`);
+	if (ps.type) {
+		query.contentType = new RegExp(`^${ps.type.replace(/\*/g, '.+?')}$`);
 	}
 
-	// Issue query
 	const files = await DriveFile
 		.find(query, {
-			limit: limit,
+			limit: ps.limit,
 			sort: sort
 		});
 
-	// Serialize
 	res(await packMany(files));
 });
diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts
index 028a2aa826..7795bbcc61 100644
--- a/src/server/api/endpoints/following/create.ts
+++ b/src/server/api/endpoints/following/create.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 const ms = require('ms');
 import User, { pack, ILocalUser } from '../../../../models/user';
 import Following from '../../../../models/following';
@@ -23,12 +23,14 @@ export const meta = {
 	kind: 'following-write',
 
 	params: {
-		userId: $.type(ID).note({
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts
index 0489c1e041..ae0edd34ee 100644
--- a/src/server/api/endpoints/following/delete.ts
+++ b/src/server/api/endpoints/following/delete.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 const ms = require('ms');
 import User, { pack, ILocalUser } from '../../../../models/user';
 import Following from '../../../../models/following';
@@ -23,12 +23,14 @@ export const meta = {
 	kind: 'following-write',
 
 	params: {
-		userId: $.type(ID).note({
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/following/requests/accept.ts b/src/server/api/endpoints/following/requests/accept.ts
index f6a7dcf120..1172f463d6 100644
--- a/src/server/api/endpoints/following/requests/accept.ts
+++ b/src/server/api/endpoints/following/requests/accept.ts
@@ -1,6 +1,7 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 import acceptFollowRequest from '../../../../../services/following/requests/accept';
 import User, { ILocalUser } from '../../../../../models/user';
+import getParams from '../../../get-params';
 
 export const meta = {
 	desc: {
@@ -10,17 +11,23 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'following-write'
+	kind: 'following-write',
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
+		}
+	}
 };
 
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'userId' parameter
-	const [followerId, followerIdErr] = $.type(ID).get(params.userId);
-	if (followerIdErr) return rej('invalid userId param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
 	// Fetch follower
 	const follower = await User.findOne({
-		_id: followerId
+		_id: ps.userId
 	});
 
 	if (follower === null) {
@@ -29,6 +36,5 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 
 	await acceptFollowRequest(user, follower);
 
-	// Send response
 	res();
 });
diff --git a/src/server/api/endpoints/following/requests/cancel.ts b/src/server/api/endpoints/following/requests/cancel.ts
index 3da4f4734f..77bfcfe15e 100644
--- a/src/server/api/endpoints/following/requests/cancel.ts
+++ b/src/server/api/endpoints/following/requests/cancel.ts
@@ -1,6 +1,7 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 import cancelFollowRequest from '../../../../../services/following/requests/cancel';
 import User, { pack, ILocalUser } from '../../../../../models/user';
+import getParams from '../../../get-params';
 
 export const meta = {
 	desc: {
@@ -10,17 +11,23 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'following-write'
+	kind: 'following-write',
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
+		}
+	}
 };
 
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'userId' parameter
-	const [followeeId, followeeIdErr] = $.type(ID).get(params.userId);
-	if (followeeIdErr) return rej('invalid userId param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
 	// Fetch followee
 	const followee = await User.findOne({
-		_id: followeeId
+		_id: ps.userId
 	});
 
 	if (followee === null) {
@@ -33,6 +40,5 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 		return rej(e);
 	}
 
-	// Send response
 	res(await pack(followee._id, user));
 });
diff --git a/src/server/api/endpoints/following/requests/list.ts b/src/server/api/endpoints/following/requests/list.ts
index 11a387cf16..6d42d06db2 100644
--- a/src/server/api/endpoints/following/requests/list.ts
+++ b/src/server/api/endpoints/following/requests/list.ts
@@ -1,4 +1,4 @@
-//import $ from 'cafy'; import ID from '../../../../../cafy-id';
+//import $ from 'cafy'; import ID, { transform } from '../../../../../cafy-id';
 import FollowRequest, { pack } from '../../../../../models/follow-request';
 import { ILocalUser } from '../../../../../models/user';
 
diff --git a/src/server/api/endpoints/following/requests/reject.ts b/src/server/api/endpoints/following/requests/reject.ts
index 98febe9e9f..45ebb7807b 100644
--- a/src/server/api/endpoints/following/requests/reject.ts
+++ b/src/server/api/endpoints/following/requests/reject.ts
@@ -1,6 +1,7 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 import rejectFollowRequest from '../../../../../services/following/requests/reject';
 import User, { ILocalUser } from '../../../../../models/user';
+import getParams from '../../../get-params';
 
 export const meta = {
 	desc: {
@@ -10,17 +11,23 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'following-write'
+	kind: 'following-write',
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
+		}
+	}
 };
 
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'userId' parameter
-	const [followerId, followerIdErr] = $.type(ID).get(params.userId);
-	if (followerIdErr) return rej('invalid userId param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
 	// Fetch follower
 	const follower = await User.findOne({
-		_id: followerId
+		_id: ps.userId
 	});
 
 	if (follower === null) {
@@ -29,6 +36,5 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 
 	await rejectFollowRequest(user, follower);
 
-	// Send response
 	res();
 });
diff --git a/src/server/api/endpoints/following/stalk.ts b/src/server/api/endpoints/following/stalk.ts
index d44cea2cc4..434bc52b59 100644
--- a/src/server/api/endpoints/following/stalk.ts
+++ b/src/server/api/endpoints/following/stalk.ts
@@ -1,6 +1,7 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import Following from '../../../../models/following';
 import { ILocalUser } from '../../../../models/user';
+import getParams from '../../get-params';
 
 export const meta = {
 	desc: {
@@ -10,20 +11,26 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'following-write'
+	kind: 'following-write',
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
+		}
+	}
 };
 
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	const follower = user;
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
-	// Get 'userId' parameter
-	const [userId, userIdErr] = $.type(ID).get(params.userId);
-	if (userIdErr) return rej('invalid userId param');
+	const follower = user;
 
 	// Fetch following
 	const following = await Following.findOne({
 		followerId: follower._id,
-		followeeId: userId
+		followeeId: ps.userId
 	});
 
 	if (following === null) {
@@ -37,7 +44,6 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 		}
 	});
 
-	// Send response
 	res();
 
 	// TODO: イベント
diff --git a/src/server/api/endpoints/following/unstalk.ts b/src/server/api/endpoints/following/unstalk.ts
index 8b66f0727e..00e91357ec 100644
--- a/src/server/api/endpoints/following/unstalk.ts
+++ b/src/server/api/endpoints/following/unstalk.ts
@@ -1,6 +1,7 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import Following from '../../../../models/following';
 import { ILocalUser } from '../../../../models/user';
+import getParams from '../../get-params';
 
 export const meta = {
 	desc: {
@@ -10,20 +11,26 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'following-write'
+	kind: 'following-write',
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
+		}
+	}
 };
 
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	const follower = user;
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
-	// Get 'userId' parameter
-	const [userId, userIdErr] = $.type(ID).get(params.userId);
-	if (userIdErr) return rej('invalid userId param');
+	const follower = user;
 
 	// Fetch following
 	const following = await Following.findOne({
 		followerId: follower._id,
-		followeeId: userId
+		followeeId: ps.userId
 	});
 
 	if (following === null) {
@@ -37,7 +44,6 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 		}
 	});
 
-	// Send response
 	res();
 
 	// TODO: イベント
diff --git a/src/server/api/endpoints/games/reversi/games.ts b/src/server/api/endpoints/games/reversi/games.ts
index 2838940b5c..9c7a7c4d74 100644
--- a/src/server/api/endpoints/games/reversi/games.ts
+++ b/src/server/api/endpoints/games/reversi/games.ts
@@ -1,33 +1,42 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 import ReversiGame, { pack } from '../../../../../models/games/reversi/game';
 import { ILocalUser } from '../../../../../models/user';
+import getParams from '../../../get-params';
 
 export const meta = {
+	params: {
+		limit: {
+			validator: $.num.optional.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
+
+		untilId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
+
+		my: {
+			validator: $.bool.optional,
+			default: false
+		}
+	}
 };
 
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'my' parameter
-	const [my = false, myErr] = $.bool.optional.get(params.my);
-	if (myErr) return rej('invalid my param');
-
-	// Get 'limit' parameter
-	const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
-	if (limitErr) return rej('invalid limit param');
-
-	// Get 'sinceId' parameter
-	const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId);
-	if (sinceIdErr) return rej('invalid sinceId param');
-
-	// Get 'untilId' parameter
-	const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId);
-	if (untilIdErr) return rej('invalid untilId param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
 	// Check if both of sinceId and untilId is specified
-	if (sinceId && untilId) {
+	if (ps.sinceId && ps.untilId) {
 		return rej('cannot set sinceId and untilId');
 	}
 
-	const q: any = my ? {
+	const q: any = ps.my ? {
 		isStarted: true,
 		$or: [{
 			user1Id: user._id
@@ -42,21 +51,21 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 		_id: -1
 	};
 
-	if (sinceId) {
+	if (ps.sinceId) {
 		sort._id = 1;
 		q._id = {
-			$gt: sinceId
+			$gt: ps.sinceId
 		};
-	} else if (untilId) {
+	} else if (ps.untilId) {
 		q._id = {
-			$lt: untilId
+			$lt: ps.untilId
 		};
 	}
 
 	// Fetch games
 	const games = await ReversiGame.find(q, {
-		sort,
-		limit
+		sort: sort,
+		limit: ps.limit
 	});
 
 	// Reponse
diff --git a/src/server/api/endpoints/games/reversi/games/show.ts b/src/server/api/endpoints/games/reversi/games/show.ts
index 8d7cd987a0..a2c84ae82d 100644
--- a/src/server/api/endpoints/games/reversi/games/show.ts
+++ b/src/server/api/endpoints/games/reversi/games/show.ts
@@ -1,14 +1,23 @@
-import $ from 'cafy'; import ID from '../../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../../misc/cafy-id';
 import ReversiGame, { pack } from '../../../../../../models/games/reversi/game';
 import Reversi from '../../../../../../games/reversi/core';
 import { ILocalUser } from '../../../../../../models/user';
+import getParams from '../../../../get-params';
+
+export const meta = {
+	params: {
+		gameId: {
+			validator: $.type(ID),
+			transform: transform,
+		},
+	}
+};
 
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'gameId' parameter
-	const [gameId, gameIdErr] = $.type(ID).get(params.gameId);
-	if (gameIdErr) return rej('invalid gameId param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
-	const game = await ReversiGame.findOne({ _id: gameId });
+	const game = await ReversiGame.findOne({ _id: ps.gameId });
 
 	if (game == null) {
 		return rej('game not found');
diff --git a/src/server/api/endpoints/games/reversi/games/surrender.ts b/src/server/api/endpoints/games/reversi/games/surrender.ts
index 8ca0143674..2860c154fe 100644
--- a/src/server/api/endpoints/games/reversi/games/surrender.ts
+++ b/src/server/api/endpoints/games/reversi/games/surrender.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../../misc/cafy-id';
 import ReversiGame, { pack } from '../../../../../../models/games/reversi/game';
 import { ILocalUser } from '../../../../../../models/user';
 import getParams from '../../../../get-params';
@@ -12,11 +12,13 @@ export const meta = {
 	requireCredential: true,
 
 	params: {
-		gameId: $.type(ID).note({
+		gameId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '投了したい対局'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/games/reversi/match.ts b/src/server/api/endpoints/games/reversi/match.ts
index d7483a0bfd..f43650aeeb 100644
--- a/src/server/api/endpoints/games/reversi/match.ts
+++ b/src/server/api/endpoints/games/reversi/match.ts
@@ -1,27 +1,34 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 import Matching, { pack as packMatching } from '../../../../../models/games/reversi/matching';
 import ReversiGame, { pack as packGame } from '../../../../../models/games/reversi/game';
 import User, { ILocalUser } from '../../../../../models/user';
 import { publishMainStream, publishReversiStream } from '../../../../../stream';
 import { eighteight } from '../../../../../games/reversi/maps';
+import getParams from '../../../get-params';
 
 export const meta = {
-	requireCredential: true
+	requireCredential: true,
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
+		},
+	}
 };
 
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'userId' parameter
-	const [childId, childIdErr] = $.type(ID).get(params.userId);
-	if (childIdErr) return rej('invalid userId param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
 	// Myself
-	if (childId.equals(user._id)) {
+	if (ps.userId.equals(user._id)) {
 		return rej('invalid userId param');
 	}
 
 	// Find session
 	const exist = await Matching.findOne({
-		parentId: childId,
+		parentId: ps.userId,
 		childId: user._id
 	});
 
@@ -63,7 +70,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 	} else {
 		// Fetch child
 		const child = await User.findOne({
-			_id: childId
+			_id: ps.userId
 		}, {
 			fields: {
 				_id: true
diff --git a/src/server/api/endpoints/hashtags/search.ts b/src/server/api/endpoints/hashtags/search.ts
index 69cdc62ab8..35ae2a83b3 100644
--- a/src/server/api/endpoints/hashtags/search.ts
+++ b/src/server/api/endpoints/hashtags/search.ts
@@ -11,25 +11,28 @@ export const meta = {
 	requireCredential: false,
 
 	params: {
-		limit: $.num.optional.range(1, 100).note({
+		limit: {
+			validator: $.num.optional.range(1, 100),
 			default: 10,
 			desc: {
 				'ja-JP': '最大数'
 			}
-		}),
+		},
 
-		query: $.str.note({
+		query: {
+			validator: $.str,
 			desc: {
 				'ja-JP': 'クエリ'
 			}
-		}),
+		},
 
-		offset: $.num.optional.min(0).note({
+		offset: {
+			validator: $.num.optional.min(0),
 			default: 0,
 			desc: {
 				'ja-JP': 'オフセット'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/i/favorites.ts b/src/server/api/endpoints/i/favorites.ts
index e7cf8a71a7..847ac64d9a 100644
--- a/src/server/api/endpoints/i/favorites.ts
+++ b/src/server/api/endpoints/i/favorites.ts
@@ -1,6 +1,7 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import Favorite, { packMany } from '../../../../models/favorite';
 import { ILocalUser } from '../../../../models/user';
+import getParams from '../../get-params';
 
 export const meta = {
 	desc: {
@@ -10,24 +11,32 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'favorites-read'
+	kind: 'favorites-read',
+
+	params: {
+		limit: {
+			validator: $.num.optional.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
+
+		untilId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		}
+	}
 };
 
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'limit' parameter
-	const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
-	if (limitErr) return rej('invalid limit param');
-
-	// Get 'sinceId' parameter
-	const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId);
-	if (sinceIdErr) return rej('invalid sinceId param');
-
-	// Get 'untilId' parameter
-	const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId);
-	if (untilIdErr) return rej('invalid untilId param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
 	// Check if both of sinceId and untilId is specified
-	if (sinceId && untilId) {
+	if (ps.sinceId && ps.untilId) {
 		return rej('cannot set sinceId and untilId');
 	}
 
@@ -39,21 +48,23 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 		_id: -1
 	};
 
-	if (sinceId) {
+	if (ps.sinceId) {
 		sort._id = 1;
 		query._id = {
-			$gt: sinceId
+			$gt: ps.sinceId
 		};
-	} else if (untilId) {
+	} else if (ps.untilId) {
 		query._id = {
-			$lt: untilId
+			$lt: ps.untilId
 		};
 	}
 
 	// Get favorites
 	const favorites = await Favorite
-		.find(query, { limit, sort });
+		.find(query, {
+			limit: ps.limit,
+			sort: sort
+		});
 
-	// Serialize
 	res(await packMany(favorites, user));
 });
diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts
index 5cc836e362..d16ba63bdc 100644
--- a/src/server/api/endpoints/i/notifications.ts
+++ b/src/server/api/endpoints/i/notifications.ts
@@ -1,38 +1,56 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import Notification from '../../../../models/notification';
 import Mute from '../../../../models/mute';
 import { packMany } from '../../../../models/notification';
 import { getFriendIds } from '../../common/get-friends';
 import read from '../../common/read-notification';
 import { ILocalUser } from '../../../../models/user';
+import getParams from '../../get-params';
+
+export const meta = {
+	desc: {
+		'ja-JP': '通知一覧を取得します。',
+		'en-US': 'Get notifications.'
+	},
+
+	requireCredential: true,
+
+	kind: 'account-read',
+
+	params: {
+		limit: {
+			validator: $.num.optional.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
+
+		untilId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
+
+		following: {
+			validator: $.bool.optional,
+			default: false
+		},
+
+		markAsRead: {
+			validator: $.bool.optional,
+			default: true
+		}
+	}
+};
 
-/**
- * Get notifications
- */
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'following' parameter
-	const [following = false, followingError] =
-		$.bool.optional.get(params.following);
-	if (followingError) return rej('invalid following param');
-
-	// Get 'markAsRead' parameter
-	const [markAsRead = true, markAsReadErr] = $.bool.optional.get(params.markAsRead);
-	if (markAsReadErr) return rej('invalid markAsRead param');
-
-	// Get 'limit' parameter
-	const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
-	if (limitErr) return rej('invalid limit param');
-
-	// Get 'sinceId' parameter
-	const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId);
-	if (sinceIdErr) return rej('invalid sinceId param');
-
-	// Get 'untilId' parameter
-	const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId);
-	if (untilIdErr) return rej('invalid untilId param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
 	// Check if both of sinceId and untilId is specified
-	if (sinceId && untilId) {
+	if (ps.sinceId && ps.untilId) {
 		return rej('cannot set sinceId and untilId');
 	}
 
@@ -53,7 +71,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 		_id: -1
 	};
 
-	if (following) {
+	if (ps.following) {
 		// ID list of the user itself and other users who the user follows
 		const followingIds = await getFriendIds(user._id);
 
@@ -64,29 +82,27 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 		});
 	}
 
-	if (sinceId) {
+	if (ps.sinceId) {
 		sort._id = 1;
 		query._id = {
-			$gt: sinceId
+			$gt: ps.sinceId
 		};
-	} else if (untilId) {
+	} else if (ps.untilId) {
 		query._id = {
-			$lt: untilId
+			$lt: ps.untilId
 		};
 	}
 
-	// Issue query
 	const notifications = await Notification
 		.find(query, {
-			limit: limit,
+			limit: ps.limit,
 			sort: sort
 		});
 
-	// Serialize
 	res(await packMany(notifications));
 
 	// Mark all as read
-	if (notifications.length > 0 && markAsRead) {
+	if (notifications.length > 0 && ps.markAsRead) {
 		read(user._id, notifications);
 	}
 });
diff --git a/src/server/api/endpoints/i/pin.ts b/src/server/api/endpoints/i/pin.ts
index 44c7fe77b8..4341906a57 100644
--- a/src/server/api/endpoints/i/pin.ts
+++ b/src/server/api/endpoints/i/pin.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import { ILocalUser } from '../../../../models/user';
 import { pack } from '../../../../models/user';
 import { addPinned } from '../../../../services/i/pin';
@@ -16,12 +16,14 @@ export const meta = {
 	kind: 'account-write',
 
 	params: {
-		noteId: $.type(ID).note({
+		noteId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿のID',
 				'en-US': 'Target note ID'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/i/signin_history.ts b/src/server/api/endpoints/i/signin_history.ts
index 5a3c122f3a..df1cd34c8c 100644
--- a/src/server/api/endpoints/i/signin_history.ts
+++ b/src/server/api/endpoints/i/signin_history.ts
@@ -1,27 +1,37 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import Signin, { pack } from '../../../../models/signin';
 import { ILocalUser } from '../../../../models/user';
+import getParams from '../../get-params';
 
 export const meta = {
 	requireCredential: true,
-	secure: true
+
+	secure: true,
+
+	params: {
+		limit: {
+			validator: $.num.optional.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
+
+		untilId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		}
+	}
 };
 
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'limit' parameter
-	const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
-	if (limitErr) return rej('invalid limit param');
-
-	// Get 'sinceId' parameter
-	const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId);
-	if (sinceIdErr) return rej('invalid sinceId param');
-
-	// Get 'untilId' parameter
-	const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId);
-	if (untilIdErr) return rej('invalid untilId param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
 	// Check if both of sinceId and untilId is specified
-	if (sinceId && untilId) {
+	if (ps.sinceId && ps.untilId) {
 		return rej('cannot set sinceId and untilId');
 	}
 
@@ -33,25 +43,23 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 		_id: -1
 	};
 
-	if (sinceId) {
+	if (ps.sinceId) {
 		sort._id = 1;
 		query._id = {
-			$gt: sinceId
+			$gt: ps.sinceId
 		};
-	} else if (untilId) {
+	} else if (ps.untilId) {
 		query._id = {
-			$lt: untilId
+			$lt: ps.untilId
 		};
 	}
 
-	// Issue query
 	const history = await Signin
 		.find(query, {
-			limit: limit,
+			limit: ps.limit,
 			sort: sort
 		});
 
 	// Serialize
-	res(await Promise.all(history.map(async record =>
-		await pack(record))));
+	res(await Promise.all(history.map(record => pack(record))));
 });
diff --git a/src/server/api/endpoints/i/unpin.ts b/src/server/api/endpoints/i/unpin.ts
index 6c20e2771d..26ff8ccda1 100644
--- a/src/server/api/endpoints/i/unpin.ts
+++ b/src/server/api/endpoints/i/unpin.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import { ILocalUser } from '../../../../models/user';
 import { pack } from '../../../../models/user';
 import { removePinned } from '../../../../services/i/pin';
@@ -16,12 +16,14 @@ export const meta = {
 	kind: 'account-write',
 
 	params: {
-		noteId: $.type(ID).note({
+		noteId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿のID',
 				'en-US': 'Target note ID'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index 04f132c551..93d4448092 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack, ILocalUser } from '../../../../models/user';
 import { publishMainStream } from '../../../../stream';
 import DriveFile from '../../../../models/drive-file';
@@ -19,83 +19,99 @@ export const meta = {
 	kind: 'account-write',
 
 	params: {
-		name: $.str.optional.nullable.pipe(isValidName).note({
+		name: {
+			validator: $.str.optional.nullable.pipe(isValidName),
 			desc: {
 				'ja-JP': '名前(ハンドルネームやニックネーム)'
 			}
-		}),
+		},
 
-		description: $.str.optional.nullable.pipe(isValidDescription).note({
+		description: {
+			validator: $.str.optional.nullable.pipe(isValidDescription),
 			desc: {
 				'ja-JP': 'アカウントの説明や自己紹介'
 			}
-		}),
+		},
 
-		location: $.str.optional.nullable.pipe(isValidLocation).note({
+		location: {
+			validator: $.str.optional.nullable.pipe(isValidLocation),
 			desc: {
 				'ja-JP': '住んでいる地域、所在'
 			}
-		}),
+		},
 
-		birthday: $.str.optional.nullable.pipe(isValidBirthday).note({
+		birthday: {
+			validator: $.str.optional.nullable.pipe(isValidBirthday),
 			desc: {
 				'ja-JP': '誕生日 (YYYY-MM-DD形式)'
 			}
-		}),
+		},
 
-		avatarId: $.type(ID).optional.nullable.note({
+		avatarId: {
+			validator: $.type(ID).optional.nullable,
+			transform: transform,
 			desc: {
 				'ja-JP': 'アイコンに設定する画像のドライブファイルID'
 			}
-		}),
+		},
 
-		bannerId: $.type(ID).optional.nullable.note({
+		bannerId: {
+			validator: $.type(ID).optional.nullable,
+			transform: transform,
 			desc: {
 				'ja-JP': 'バナーに設定する画像のドライブファイルID'
 			}
-		}),
+		},
 
-		wallpaperId: $.type(ID).optional.nullable.note({
+		wallpaperId: {
+			validator: $.type(ID).optional.nullable,
+			transform: transform,
 			desc: {
 				'ja-JP': '壁紙に設定する画像のドライブファイルID'
 			}
-		}),
+		},
 
-		isLocked: $.bool.optional.note({
+		isLocked: {
+			validator: $.bool.optional,
 			desc: {
 				'ja-JP': '鍵アカウントか否か'
 			}
-		}),
+		},
 
-		carefulBot: $.bool.optional.note({
+		carefulBot: {
+			validator: $.bool.optional,
 			desc: {
 				'ja-JP': 'Botからのフォローを承認制にするか'
 			}
-		}),
+		},
 
-		isBot: $.bool.optional.note({
+		isBot: {
+			validator: $.bool.optional,
 			desc: {
 				'ja-JP': 'Botか否か'
 			}
-		}),
+		},
 
-		isCat: $.bool.optional.note({
+		isCat: {
+			validator: $.bool.optional,
 			desc: {
 				'ja-JP': '猫か否か'
 			}
-		}),
+		},
 
-		autoWatch: $.bool.optional.note({
+		autoWatch: {
+			validator: $.bool.optional,
 			desc: {
 				'ja-JP': '投稿の自動ウォッチをするか否か'
 			}
-		}),
+		},
 
-		alwaysMarkNsfw: $.bool.optional.note({
+		alwaysMarkNsfw: {
+			validator: $.bool.optional,
 			desc: {
 				'ja-JP': 'アップロードするメディアをデフォルトで「閲覧注意」として設定するか'
 			}
-		}),
+		},
 	}
 };
 
diff --git a/src/server/api/endpoints/messaging/messages.ts b/src/server/api/endpoints/messaging/messages.ts
index dec0638eed..43d96e2ee4 100644
--- a/src/server/api/endpoints/messaging/messages.ts
+++ b/src/server/api/endpoints/messaging/messages.ts
@@ -1,8 +1,9 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import Message from '../../../../models/messaging-message';
 import User, { ILocalUser } from '../../../../models/user';
 import { pack } from '../../../../models/messaging-message';
 import read from '../../common/read-messaging-message';
+import getParams from '../../get-params';
 
 export const meta = {
 	desc: {
@@ -12,17 +13,48 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'messaging-read'
+	kind: 'messaging-read',
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
+		},
+
+		limit: {
+			validator: $.num.optional.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
+
+		untilId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
+
+		markAsRead: {
+			validator: $.bool.optional,
+			default: true
+		}
+	}
 };
 
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'userId' parameter
-	const [recipientId, recipientIdErr] = $.type(ID).get(params.userId);
-	if (recipientIdErr) return rej('invalid userId param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
+
+	// Check if both of sinceId and untilId is specified
+	if (ps.sinceId && ps.untilId) {
+		return rej('cannot set sinceId and untilId');
+	}
 
 	// Fetch recipient
 	const recipient = await User.findOne({
-		_id: recipientId
+		_id: ps.userId
 	}, {
 			fields: {
 				_id: true
@@ -33,27 +65,6 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 		return rej('user not found');
 	}
 
-	// Get 'markAsRead' parameter
-	const [markAsRead = true, markAsReadErr] = $.bool.optional.get(params.markAsRead);
-	if (markAsReadErr) return rej('invalid markAsRead param');
-
-	// Get 'limit' parameter
-	const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
-	if (limitErr) return rej('invalid limit param');
-
-	// Get 'sinceId' parameter
-	const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId);
-	if (sinceIdErr) return rej('invalid sinceId param');
-
-	// Get 'untilId' parameter
-	const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId);
-	if (untilIdErr) return rej('invalid untilId param');
-
-	// Check if both of sinceId and untilId is specified
-	if (sinceId && untilId) {
-		return rej('cannot set sinceId and untilId');
-	}
-
 	const query = {
 		$or: [{
 			userId: user._id,
@@ -68,36 +79,33 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 		_id: -1
 	};
 
-	if (sinceId) {
+	if (ps.sinceId) {
 		sort._id = 1;
 		query._id = {
-			$gt: sinceId
+			$gt: ps.sinceId
 		};
-	} else if (untilId) {
+	} else if (ps.untilId) {
 		query._id = {
-			$lt: untilId
+			$lt: ps.untilId
 		};
 	}
 
-	// Issue query
 	const messages = await Message
 		.find(query, {
-			limit: limit,
+			limit: ps.limit,
 			sort: sort
 		});
 
-	// Serialize
-	res(await Promise.all(messages.map(async message =>
-		await pack(message, user, {
-			populateRecipient: false
-		}))));
+	res(await Promise.all(messages.map(message => pack(message, user, {
+		populateRecipient: false
+	}))));
 
 	if (messages.length === 0) {
 		return;
 	}
 
 	// Mark all as read
-	if (markAsRead) {
+	if (ps.markAsRead) {
 		read(user._id, recipient._id, messages);
 	}
 });
diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts
index cb115cf987..ff44e192d1 100644
--- a/src/server/api/endpoints/messaging/messages/create.ts
+++ b/src/server/api/endpoints/messaging/messages/create.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 import Message from '../../../../../models/messaging-message';
 import { isValidText } from '../../../../../models/messaging-message';
 import History from '../../../../../models/messaging-history';
@@ -9,6 +9,7 @@ import { pack } from '../../../../../models/messaging-message';
 import { publishMainStream } from '../../../../../stream';
 import { publishMessagingStream, publishMessagingIndexStream } from '../../../../../stream';
 import pushSw from '../../../../../push-sw';
+import getParams from '../../../get-params';
 
 export const meta = {
 	desc: {
@@ -18,22 +19,37 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'messaging-write'
+	kind: 'messaging-write',
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
+		},
+
+		text: {
+			validator: $.str.optional.pipe(isValidText)
+		},
+
+		fileId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		}
+	}
 };
 
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'userId' parameter
-	const [recipientId, recipientIdErr] = $.type(ID).get(params.userId);
-	if (recipientIdErr) return rej('invalid userId param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
 	// Myself
-	if (recipientId.equals(user._id)) {
+	if (ps.userId.equals(user._id)) {
 		return rej('cannot send message to myself');
 	}
 
 	// Fetch recipient
 	const recipient = await User.findOne({
-		_id: recipientId
+		_id: ps.userId
 	}, {
 		fields: {
 			_id: true
@@ -44,18 +60,10 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 		return rej('user not found');
 	}
 
-	// Get 'text' parameter
-	const [text, textErr] = $.str.optional.pipe(isValidText).get(params.text);
-	if (textErr) return rej('invalid text');
-
-	// Get 'fileId' parameter
-	const [fileId, fileIdErr] = $.type(ID).optional.get(params.fileId);
-	if (fileIdErr) return rej('invalid fileId param');
-
 	let file = null;
-	if (fileId !== undefined) {
+	if (ps.fileId != null) {
 		file = await DriveFile.findOne({
-			_id: fileId,
+			_id: ps.fileId,
 			'metadata.userId': user._id
 		});
 
@@ -65,7 +73,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 	}
 
 	// テキストが無いかつ添付ファイルも無かったらエラー
-	if (text === undefined && file === null) {
+	if (ps.text == null && file == null) {
 		return rej('text or file is required');
 	}
 
@@ -74,7 +82,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 		createdAt: new Date(),
 		fileId: file ? file._id : undefined,
 		recipientId: recipient._id,
-		text: text ? text.trim() : undefined,
+		text: ps.text ? ps.text.trim() : undefined,
 		userId: user._id,
 		isRead: false
 	});
diff --git a/src/server/api/endpoints/messaging/messages/read.ts b/src/server/api/endpoints/messaging/messages/read.ts
index 1c0bdf5230..122034fdf0 100644
--- a/src/server/api/endpoints/messaging/messages/read.ts
+++ b/src/server/api/endpoints/messaging/messages/read.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 import Message from '../../../../../models/messaging-message';
 import { ILocalUser } from '../../../../../models/user';
 import read from '../../../common/read-messaging-message';
@@ -15,12 +15,14 @@ export const meta = {
 	kind: 'messaging-write',
 
 	params: {
-		messageId: $.type(ID).note({
+		messageId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '既読にするメッセージのID',
 				'en-US': 'The ID of a message that you want to mark as read'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/mute/create.ts b/src/server/api/endpoints/mute/create.ts
index 5b2e7a8d71..442d3f51d8 100644
--- a/src/server/api/endpoints/mute/create.ts
+++ b/src/server/api/endpoints/mute/create.ts
@@ -1,6 +1,7 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import User, { ILocalUser } from '../../../../models/user';
 import Mute from '../../../../models/mute';
+import getParams from '../../get-params';
 
 export const meta = {
 	desc: {
@@ -10,24 +11,30 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'account/write'
+	kind: 'account/write',
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
+		},
+	}
 };
 
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
+
 	const muter = user;
 
-	// Get 'userId' parameter
-	const [userId, userIdErr] = $.type(ID).get(params.userId);
-	if (userIdErr) return rej('invalid userId param');
-
 	// 自分自身
-	if (user._id.equals(userId)) {
+	if (user._id.equals(ps.userId)) {
 		return rej('mutee is yourself');
 	}
 
 	// Get mutee
 	const mutee = await User.findOne({
-		_id: userId
+		_id: ps.userId
 	}, {
 		fields: {
 			data: false,
@@ -56,6 +63,5 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 		muteeId: mutee._id,
 	});
 
-	// Send response
 	res();
 });
diff --git a/src/server/api/endpoints/mute/delete.ts b/src/server/api/endpoints/mute/delete.ts
index e8ed75a847..c1d4f35425 100644
--- a/src/server/api/endpoints/mute/delete.ts
+++ b/src/server/api/endpoints/mute/delete.ts
@@ -1,6 +1,7 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import User, { ILocalUser } from '../../../../models/user';
 import Mute from '../../../../models/mute';
+import getParams from '../../get-params';
 
 export const meta = {
 	desc: {
@@ -10,24 +11,30 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'account/write'
+	kind: 'account/write',
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
+		},
+	}
 };
 
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
+
 	const muter = user;
 
-	// Get 'userId' parameter
-	const [userId, userIdErr] = $.type(ID).get(params.userId);
-	if (userIdErr) return rej('invalid userId param');
-
 	// Check if the mutee is yourself
-	if (user._id.equals(userId)) {
+	if (user._id.equals(ps.userId)) {
 		return rej('mutee is yourself');
 	}
 
 	// Get mutee
 	const mutee = await User.findOne({
-		_id: userId
+		_id: ps.userId
 	}, {
 		fields: {
 			data: false,
@@ -54,6 +61,5 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 		_id: exist._id
 	});
 
-	// Send response
 	res();
 });
diff --git a/src/server/api/endpoints/mute/list.ts b/src/server/api/endpoints/mute/list.ts
index 4653877621..e33e709773 100644
--- a/src/server/api/endpoints/mute/list.ts
+++ b/src/server/api/endpoints/mute/list.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import Mute, { packMany } from '../../../../models/mute';
 import { ILocalUser } from '../../../../models/user';
 import getParams from '../../get-params';
@@ -14,15 +14,20 @@ export const meta = {
 	kind: 'account/read',
 
 	params: {
-		limit: $.num.optional.range(1, 100).note({
+		limit: {
+			validator: $.num.optional.range(1, 100),
 			default: 30
-		}),
+		},
 
-		sinceId: $.type(ID).optional.note({
-		}),
+		sinceId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
 
-		untilId: $.type(ID).optional.note({
-		}),
+		untilId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
 	}
 };
 
diff --git a/src/server/api/endpoints/notes.ts b/src/server/api/endpoints/notes.ts
index 4f5a211240..83776c59d0 100644
--- a/src/server/api/endpoints/notes.ts
+++ b/src/server/api/endpoints/notes.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../misc/cafy-id';
 import Note, { packMany } from '../../../models/note';
 import getParams from '../get-params';
 
@@ -8,49 +8,62 @@ export const meta = {
 	},
 
 	params: {
-		local: $.bool.optional.note({
+		local: {
+			validator: $.bool.optional,
 			desc: {
 				'ja-JP': 'ローカルの投稿に限定するか否か'
 			}
-		}),
+		},
 
-		reply: $.bool.optional.note({
+		reply: {
+			validator: $.bool.optional,
 			desc: {
 				'ja-JP': '返信に限定するか否か'
 			}
-		}),
+		},
 
-		renote: $.bool.optional.note({
+		renote: {
+			validator: $.bool.optional,
 			desc: {
 				'ja-JP': 'Renoteに限定するか否か'
 			}
-		}),
+		},
 
-		withFiles: $.bool.optional.note({
+		withFiles: {
+			validator: $.bool.optional,
 			desc: {
 				'ja-JP': 'ファイルが添付された投稿に限定するか否か'
 			}
-		}),
+		},
 
-		media: $.bool.optional.note({
+		media: {
+			validator: $.bool.optional,
 			desc: {
 				'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
 			}
-		}),
+		},
 
-		poll: $.bool.optional.note({
+		poll: {
+			validator: $.bool.optional,
 			desc: {
 				'ja-JP': 'アンケートが添付された投稿に限定するか否か'
 			}
-		}),
+		},
 
-		limit: $.num.optional.range(1, 100).note({
+		limit: {
+			validator: $.num.optional.range(1, 100),
 			default: 10
-		}),
+		},
 
-		sinceId: $.type(ID).optional.note({}),
+		sinceId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
 
-		untilId: $.type(ID).optional.note({}),
+		untilId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
 	}
 };
 
diff --git a/src/server/api/endpoints/notes/conversation.ts b/src/server/api/endpoints/notes/conversation.ts
index 0c23f9e5fc..c7be6d6e3f 100644
--- a/src/server/api/endpoints/notes/conversation.ts
+++ b/src/server/api/endpoints/notes/conversation.ts
@@ -1,26 +1,41 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import Note, { packMany, INote } from '../../../../models/note';
 import { ILocalUser } from '../../../../models/user';
+import getParams from '../../get-params';
+
+export const meta = {
+	desc: {
+		'ja-JP': '指定した投稿の文脈を取得します。',
+		'en-US': 'Show conversation of a note.'
+	},
+
+	requireCredential: false,
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+			transform: transform,
+		},
+
+		limit: {
+			validator: $.num.optional.range(1, 100),
+			default: 10
+		},
+
+		offset: {
+			validator: $.num.optional.min(0),
+			default: 0
+		},
+	}
+};
 
-/**
- * Show conversation of a note
- */
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'noteId' parameter
-	const [noteId, noteIdErr] = $.type(ID).get(params.noteId);
-	if (noteIdErr) return rej('invalid noteId param');
-
-	// Get 'limit' parameter
-	const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
-	if (limitErr) return rej('invalid limit param');
-
-	// Get 'offset' parameter
-	const [offset = 0, offsetErr] = $.num.optional.min(0).get(params.offset);
-	if (offsetErr) return rej('invalid offset param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
 	// Lookup note
 	const note = await Note.findOne({
-		_id: noteId
+		_id: ps.noteId
 	});
 
 	if (note === null) {
@@ -34,11 +49,11 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 		i++;
 		const p = await Note.findOne({ _id: id });
 
-		if (i > offset) {
+		if (i > ps.offset) {
 			conversation.push(p);
 		}
 
-		if (conversation.length == limit) {
+		if (conversation.length == ps.limit) {
 			return;
 		}
 
@@ -51,6 +66,5 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 		await get(note.replyId);
 	}
 
-	// Serialize
 	res(await packMany(conversation, user));
 });
diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts
index 432561da38..9eff8c27bf 100644
--- a/src/server/api/endpoints/notes/create.ts
+++ b/src/server/api/endpoints/notes/create.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform, transformMany } from '../../../../misc/cafy-id';
 const ms = require('ms');
 import Note, { INote, isValidText, isValidCw, pack } from '../../../../models/note';
 import User, { ILocalUser, IUser } from '../../../../models/user';
@@ -24,84 +24,106 @@ export const meta = {
 	kind: 'note-write',
 
 	params: {
-		visibility: $.str.optional.or(['public', 'home', 'followers', 'specified', 'private']).note({
+		visibility: {
+			validator: $.str.optional.or(['public', 'home', 'followers', 'specified', 'private']),
 			default: 'public',
 			desc: {
 				'ja-JP': '投稿の公開範囲'
 			}
-		}),
+		},
 
-		visibleUserIds: $.arr($.type(ID)).optional.unique().min(1).note({
+		visibleUserIds: {
+			validator: $.arr($.type(ID)).optional.unique().min(1),
+			transform: transformMany,
 			desc: {
 				'ja-JP': '(投稿の公開範囲が specified の場合)投稿を閲覧できるユーザー'
 			}
-		}),
+		},
 
-		text: $.str.optional.nullable.pipe(isValidText).note({
-			default: null,
+		text: {
+			validator: $.str.optional.nullable.pipe(isValidText),
+			default: null as any,
 			desc: {
 				'ja-JP': '投稿内容'
 			}
-		}),
+		},
 
-		cw: $.str.optional.nullable.pipe(isValidCw).note({
+		cw: {
+			validator: $.str.optional.nullable.pipe(isValidCw),
 			desc: {
 				'ja-JP': 'コンテンツの警告。このパラメータを指定すると設定したテキストで投稿のコンテンツを隠す事が出来ます。'
 			}
-		}),
+		},
 
-		viaMobile: $.bool.optional.note({
+		viaMobile: {
+			validator: $.bool.optional,
 			default: false,
 			desc: {
 				'ja-JP': 'モバイルデバイスからの投稿か否か。'
 			}
-		}),
+		},
 
-		geo: $.obj({
-			coordinates: $.arr().length(2)
-				.item(0, $.num.range(-180, 180))
-				.item(1, $.num.range(-90, 90)),
-			altitude: $.num.nullable,
-			accuracy: $.num.nullable,
-			altitudeAccuracy: $.num.nullable,
-			heading: $.num.nullable.range(0, 360),
-			speed: $.num.nullable
-		}).optional.nullable.strict().note({
+		geo: {
+			validator: $.obj({
+				coordinates: $.arr().length(2)
+					.item(0, $.num.range(-180, 180))
+					.item(1, $.num.range(-90, 90)),
+				altitude: $.num.nullable,
+				accuracy: $.num.nullable,
+				altitudeAccuracy: $.num.nullable,
+				heading: $.num.nullable.range(0, 360),
+				speed: $.num.nullable
+			}).optional.nullable.strict(),
 			desc: {
 				'ja-JP': '位置情報'
 			},
 			ref: 'geo'
-		}),
+		},
 
-		fileIds: $.arr($.type(ID)).optional.unique().range(1, 4).note({
+		fileIds: {
+			validator: $.arr($.type(ID)).optional.unique().range(1, 4),
+			transform: transformMany,
 			desc: {
 				'ja-JP': '添付するファイル'
 			}
-		}),
+		},
 
-		mediaIds: $.arr($.type(ID)).optional.unique().range(1, 4).note({
+		mediaIds: {
+			validator: $.arr($.type(ID)).optional.unique().range(1, 4),
+			transform: transformMany,
 			desc: {
 				'ja-JP': '添付するファイル (このパラメータは廃止予定です。代わりに fileIds を使ってください。)'
 			}
-		}),
+		},
 
-		renoteId: $.type(ID).optional.note({
+		replyId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+			desc: {
+				'ja-JP': '返信対象'
+			}
+		},
+
+		renoteId: {
+			validator: $.type(ID).optional,
+			transform: transform,
 			desc: {
 				'ja-JP': 'Renote対象'
 			}
-		}),
+		},
 
-		poll: $.obj({
-			choices: $.arr($.str)
-				.unique()
-				.range(2, 10)
-				.each(c => c.length > 0 && c.length < 50)
-		}).optional.strict().note({
+		poll: {
+			validator: $.obj({
+				choices: $.arr($.str)
+					.unique()
+					.range(2, 10)
+					.each(c => c.length > 0 && c.length < 50)
+			}).optional.strict(),
 			desc: {
 				'ja-JP': 'アンケート'
 			},
 			ref: 'poll'
-		})
+		}
 	},
 
 	res: {
@@ -117,15 +139,12 @@ export const meta = {
 	}
 };
 
-/**
- * Create a note
- */
 export default (params: any, user: ILocalUser, app: IApp) => new Promise(async (res, rej) => {
 	const [ps, psErr] = getParams(meta, params);
 	if (psErr) return rej(psErr);
 
 	let visibleUsers: IUser[] = [];
-	if (ps.visibleUserIds !== undefined) {
+	if (ps.visibleUserIds) {
 		visibleUsers = await Promise.all(ps.visibleUserIds.map(id => User.findOne({
 			_id: id
 		})));
@@ -145,7 +164,7 @@ export default (params: any, user: ILocalUser, app: IApp) => new Promise(async (
 	}
 
 	let renote: INote = null;
-	if (ps.renoteId !== undefined) {
+	if (ps.renoteId != null) {
 		// Fetch renote to note
 		renote = await Note.findOne({
 			_id: ps.renoteId
@@ -158,15 +177,11 @@ export default (params: any, user: ILocalUser, app: IApp) => new Promise(async (
 		}
 	}
 
-	// Get 'replyId' parameter
-	const [replyId, replyIdErr] = $.type(ID).optional.get(params.replyId);
-	if (replyIdErr) return rej('invalid replyId');
-
 	let reply: INote = null;
-	if (replyId !== undefined) {
+	if (ps.replyId != null) {
 		// Fetch reply
 		reply = await Note.findOne({
-			_id: replyId
+			_id: ps.replyId
 		});
 
 		if (reply === null) {
@@ -188,7 +203,7 @@ export default (params: any, user: ILocalUser, app: IApp) => new Promise(async (
 	}
 
 	// テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー
-	if ((ps.text === undefined || ps.text === null) && files === null && renote === null && ps.poll === undefined) {
+	if ((ps.text == null) && files === null && renote === null && ps.poll == null) {
 		return rej('text, fileIds, renoteId or poll is required');
 	}
 
diff --git a/src/server/api/endpoints/notes/delete.ts b/src/server/api/endpoints/notes/delete.ts
index 160d5c4cf6..580cb344e1 100644
--- a/src/server/api/endpoints/notes/delete.ts
+++ b/src/server/api/endpoints/notes/delete.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import Note from '../../../../models/note';
 import deleteNote from '../../../../services/note/delete';
 import User, { ILocalUser } from '../../../../models/user';
@@ -17,12 +17,14 @@ export const meta = {
 	kind: 'note-write',
 
 	params: {
-		noteId: $.type(ID).note({
+		noteId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿のID',
 				'en-US': 'Target note ID.'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/notes/favorites/create.ts b/src/server/api/endpoints/notes/favorites/create.ts
index 76673e248a..e489c77761 100644
--- a/src/server/api/endpoints/notes/favorites/create.ts
+++ b/src/server/api/endpoints/notes/favorites/create.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 import Favorite from '../../../../../models/favorite';
 import Note from '../../../../../models/note';
 import { ILocalUser } from '../../../../../models/user';
@@ -17,12 +17,14 @@ export const meta = {
 	kind: 'favorite-write',
 
 	params: {
-		noteId: $.type(ID).note({
+		noteId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿のID',
 				'en-US': 'Target note ID.'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/notes/favorites/delete.ts b/src/server/api/endpoints/notes/favorites/delete.ts
index 70e1ca8ccf..ac1b35d194 100644
--- a/src/server/api/endpoints/notes/favorites/delete.ts
+++ b/src/server/api/endpoints/notes/favorites/delete.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 import Favorite from '../../../../../models/favorite';
 import Note from '../../../../../models/note';
 import { ILocalUser } from '../../../../../models/user';
@@ -17,12 +17,14 @@ export const meta = {
 	kind: 'favorite-write',
 
 	params: {
-		noteId: $.type(ID).note({
+		noteId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿のID',
 				'en-US': 'Target note ID.'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/notes/featured.ts b/src/server/api/endpoints/notes/featured.ts
index 363170ead6..c031308824 100644
--- a/src/server/api/endpoints/notes/featured.ts
+++ b/src/server/api/endpoints/notes/featured.ts
@@ -13,12 +13,13 @@ export const meta = {
 	requireCredential: false,
 
 	params: {
-		limit: $.num.optional.range(1, 30).note({
+		limit: {
+			validator: $.num.optional.range(1, 30),
 			default: 10,
 			desc: {
 				'ja-JP': '最大数'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/notes/global-timeline.ts b/src/server/api/endpoints/notes/global-timeline.ts
index 8a6c848943..fa7a76f12e 100644
--- a/src/server/api/endpoints/notes/global-timeline.ts
+++ b/src/server/api/endpoints/notes/global-timeline.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import Note from '../../../../models/note';
 import Mute from '../../../../models/mute';
 import { packMany } from '../../../../models/note';
@@ -12,29 +12,42 @@ export const meta = {
 	},
 
 	params: {
-		withFiles: $.bool.optional.note({
+		withFiles: {
+			validator: $.bool.optional,
 			desc: {
 				'ja-JP': 'ファイルが添付された投稿に限定するか否か'
 			}
-		}),
+		},
 
-		mediaOnly: $.bool.optional.note({
+		mediaOnly: {
+			validator: $.bool.optional,
 			desc: {
 				'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
 			}
-		}),
+		},
 
-		limit: $.num.optional.range(1, 100).note({
+		limit: {
+			validator: $.num.optional.range(1, 100),
 			default: 10
-		}),
+		},
 
-		sinceId: $.type(ID).optional.note({}),
+		sinceId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
 
-		untilId: $.type(ID).optional.note({}),
+		untilId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
 
-		sinceDate: $.num.optional.note({}),
+		sinceDate: {
+			validator: $.num.optional
+		},
 
-		untilDate: $.num.optional.note({}),
+		untilDate: {
+			validator: $.num.optional
+		},
 	}
 };
 
diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/hybrid-timeline.ts
index b2ea9c60ac..0008144823 100644
--- a/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import Note from '../../../../models/note';
 import Mute from '../../../../models/mute';
 import { getFriends } from '../../common/get-friends';
@@ -13,69 +13,81 @@ export const meta = {
 	},
 
 	params: {
-		limit: $.num.optional.range(1, 100).note({
+		limit: {
+			validator: $.num.optional.range(1, 100),
 			default: 10,
 			desc: {
 				'ja-JP': '最大数'
 			}
-		}),
+		},
 
-		sinceId: $.type(ID).optional.note({
+		sinceId: {
+			validator: $.type(ID).optional,
+			transform: transform,
 			desc: {
 				'ja-JP': '指定すると、この投稿を基点としてより新しい投稿を取得します'
 			}
-		}),
+		},
 
-		untilId: $.type(ID).optional.note({
+		untilId: {
+			validator: $.type(ID).optional,
+			transform: transform,
 			desc: {
 				'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します'
 			}
-		}),
+		},
 
-		sinceDate: $.num.optional.note({
+		sinceDate: {
+			validator: $.num.optional,
 			desc: {
 				'ja-JP': '指定した時間を基点としてより新しい投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。'
 			}
-		}),
+		},
 
-		untilDate: $.num.optional.note({
+		untilDate: {
+			validator: $.num.optional,
 			desc: {
 				'ja-JP': '指定した時間を基点としてより古い投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。'
 			}
-		}),
+		},
 
-		includeMyRenotes: $.bool.optional.note({
+		includeMyRenotes: {
+			validator: $.bool.optional,
 			default: true,
 			desc: {
 				'ja-JP': '自分の行ったRenoteを含めるかどうか'
 			}
-		}),
+		},
 
-		includeRenotedMyNotes: $.bool.optional.note({
+		includeRenotedMyNotes: {
+			validator: $.bool.optional,
 			default: true,
 			desc: {
 				'ja-JP': 'Renoteされた自分の投稿を含めるかどうか'
 			}
-		}),
+		},
 
-		includeLocalRenotes: $.bool.optional.note({
+		includeLocalRenotes: {
+			validator: $.bool.optional,
 			default: true,
 			desc: {
 				'ja-JP': 'Renoteされたローカルの投稿を含めるかどうか'
 			}
-		}),
+		},
 
-		withFiles: $.bool.optional.note({
+		withFiles: {
+			validator: $.bool.optional,
 			desc: {
 				'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
 			}
-		}),
+		},
 
-		mediaOnly: $.bool.optional.note({
+		mediaOnly: {
+			validator: $.bool.optional,
 			desc: {
 				'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
 			}
-		}),
+		},
 	}
 };
 
diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts
index 510564129c..78ddf00626 100644
--- a/src/server/api/endpoints/notes/local-timeline.ts
+++ b/src/server/api/endpoints/notes/local-timeline.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import Note from '../../../../models/note';
 import Mute from '../../../../models/mute';
 import { packMany } from '../../../../models/note';
@@ -12,42 +12,57 @@ export const meta = {
 	},
 
 	params: {
-		withFiles: $.bool.optional.note({
+		withFiles: {
+			validator: $.bool.optional,
 			desc: {
 				'ja-JP': 'ファイルが添付された投稿に限定するか否か'
 			}
-		}),
+		},
 
-		mediaOnly: $.bool.optional.note({
+		mediaOnly: {
+			validator: $.bool.optional,
 			desc: {
 				'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
 			}
-		}),
+		},
 
-		fileType: $.arr($.str).optional.note({
+		fileType: {
+			validator: $.arr($.str).optional,
 			desc: {
 				'ja-JP': '指定された種類のファイルが添付された投稿のみを取得します'
 			}
-		}),
+		},
 
-		excludeNsfw: $.bool.optional.note({
+		excludeNsfw: {
+			validator: $.bool.optional,
 			default: false,
 			desc: {
 				'ja-JP': 'true にすると、NSFW指定されたファイルを除外します(fileTypeが指定されている場合のみ有効)'
 			}
-		}),
+		},
 
-		limit: $.num.optional.range(1, 100).note({
+		limit: {
+			validator: $.num.optional.range(1, 100),
 			default: 10
-		}),
+		},
 
-		sinceId: $.type(ID).optional.note({}),
+		sinceId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
 
-		untilId: $.type(ID).optional.note({}),
+		untilId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
 
-		sinceDate: $.num.optional.note({}),
+		sinceDate: {
+			validator: $.num.optional,
+		},
 
-		untilDate: $.num.optional.note({}),
+		untilDate: {
+			validator: $.num.optional,
+		},
 	}
 };
 
diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts
index c66421284c..3fc3977075 100644
--- a/src/server/api/endpoints/notes/mentions.ts
+++ b/src/server/api/endpoints/notes/mentions.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+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';
@@ -15,22 +15,29 @@ export const meta = {
 	requireCredential: true,
 
 	params: {
-		following: $.bool.optional.note({
+		following: {
+			validator: $.bool.optional,
 			default: false
-		}),
+		},
 
-		limit: $.num.optional.range(1, 100).note({
+		limit: {
+			validator: $.num.optional.range(1, 100),
 			default: 10
-		}),
+		},
 
-		sinceId: $.type(ID).optional.note({
-		}),
+		sinceId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
 
-		untilId: $.type(ID).optional.note({
-		}),
+		untilId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
 
-		visibility: $.str.optional.note({
-		}),
+		visibility: {
+			validator: $.str.optional,
+		},
 	}
 };
 
diff --git a/src/server/api/endpoints/notes/polls/vote.ts b/src/server/api/endpoints/notes/polls/vote.ts
index 3b78d62fd3..32dcefec7b 100644
--- a/src/server/api/endpoints/notes/polls/vote.ts
+++ b/src/server/api/endpoints/notes/polls/vote.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+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';
@@ -6,6 +6,7 @@ import watch from '../../../../../services/note/watch';
 import { publishNoteStream } from '../../../../../stream';
 import notify from '../../../../../notify';
 import { ILocalUser } from '../../../../../models/user';
+import getParams from '../../../get-params';
 
 export const meta = {
 	desc: {
@@ -15,17 +16,27 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'vote-write'
+	kind: 'vote-write',
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+			transform: transform,
+		},
+
+		choice: {
+			validator: $.num
+		},
+	}
 };
 
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'noteId' parameter
-	const [noteId, noteIdErr] = $.type(ID).get(params.noteId);
-	if (noteIdErr) return rej('invalid noteId param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
 	// Get votee
 	const note = await Note.findOne({
-		_id: noteId
+		_id: ps.noteId
 	});
 
 	if (note === null) {
@@ -36,12 +47,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 		return rej('poll not found');
 	}
 
-	// Get 'choice' parameter
-	const [choice, choiceError] =
-		$.num
-			.pipe(c => note.poll.choices.some(x => x.id == c))
-			.get(params.choice);
-	if (choiceError) return rej('invalid choice param');
+	if (!note.poll.choices.some(x => x.id == ps.choice)) return rej('invalid choice param');
 
 	// if already voted
 	const exist = await Vote.findOne({
@@ -58,14 +64,14 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 		createdAt: new Date(),
 		noteId: note._id,
 		userId: user._id,
-		choice: choice
+		choice: ps.choice
 	});
 
 	// Send response
 	res();
 
 	const inc: any = {};
-	inc[`poll.choices.${note.poll.choices.findIndex(c => c.id == choice)}.votes`] = 1;
+	inc[`poll.choices.${note.poll.choices.findIndex(c => c.id == ps.choice)}.votes`] = 1;
 
 	// Increment votes count
 	await Note.update({ _id: note._id }, {
@@ -73,14 +79,14 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 	});
 
 	publishNoteStream(note._id, 'pollVoted', {
-		choice: choice,
+		choice: ps.choice,
 		userId: user._id.toHexString()
 	});
 
 	// Notify
 	notify(note.userId, user._id, 'poll_vote', {
 		noteId: note._id,
-		choice: choice
+		choice: ps.choice
 	});
 
 	// Fetch watchers
@@ -99,7 +105,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 			watchers.forEach(watcher => {
 				notify(watcher.userId, user._id, 'poll_vote', {
 					noteId: note._id,
-					choice: choice
+					choice: ps.choice
 				});
 			});
 		});
diff --git a/src/server/api/endpoints/notes/reactions.ts b/src/server/api/endpoints/notes/reactions.ts
index 5f23068cd6..1ab5afaba1 100644
--- a/src/server/api/endpoints/notes/reactions.ts
+++ b/src/server/api/endpoints/notes/reactions.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import Note from '../../../../models/note';
 import Reaction, { pack } from '../../../../models/note-reaction';
 import { ILocalUser } from '../../../../models/user';
@@ -13,26 +13,34 @@ export const meta = {
 	requireCredential: false,
 
 	params: {
-		noteId: $.type(ID).note({
+		noteId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿のID',
 				'en-US': 'The ID of the target note'
 			}
-		}),
+		},
 
-		limit: $.num.optional.range(1, 100).note({
+		limit: {
+			validator: $.num.optional.range(1, 100),
 			default: 10
-		}),
+		},
 
-		offset: $.num.optional.note({
+		offset: {
+			validator: $.num.optional,
 			default: 0
-		}),
+		},
 
-		sinceId: $.type(ID).optional.note({
-		}),
+		sinceId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
 
-		untilId: $.type(ID).optional.note({
-		}),
+		untilId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
 	}
 };
 
diff --git a/src/server/api/endpoints/notes/reactions/create.ts b/src/server/api/endpoints/notes/reactions/create.ts
index aa9ab07384..f2b06473a5 100644
--- a/src/server/api/endpoints/notes/reactions/create.ts
+++ b/src/server/api/endpoints/notes/reactions/create.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 import Note from '../../../../../models/note';
 import create from '../../../../../services/note/reaction/create';
 import { validateReaction } from '../../../../../models/note-reaction';
@@ -18,17 +18,20 @@ export const meta = {
 	kind: 'reaction-write',
 
 	params: {
-		noteId: $.type(ID).note({
+		noteId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿'
 			}
-		}),
+		},
 
-		reaction: $.str.pipe(validateReaction.ok).note({
+		reaction: {
+			validator: $.str.pipe(validateReaction.ok),
 			desc: {
 				'ja-JP': 'リアクションの種類'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/notes/reactions/delete.ts b/src/server/api/endpoints/notes/reactions/delete.ts
index 598eb65364..2a2577dfe6 100644
--- a/src/server/api/endpoints/notes/reactions/delete.ts
+++ b/src/server/api/endpoints/notes/reactions/delete.ts
@@ -1,7 +1,8 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 import Reaction from '../../../../../models/note-reaction';
 import Note from '../../../../../models/note';
 import { ILocalUser } from '../../../../../models/user';
+import getParams from '../../../get-params';
 
 export const meta = {
 	desc: {
@@ -11,17 +12,23 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'reaction-write'
+	kind: 'reaction-write',
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+			transform: transform,
+		},
+	}
 };
 
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'noteId' parameter
-	const [noteId, noteIdErr] = $.type(ID).get(params.noteId);
-	if (noteIdErr) return rej('invalid noteId param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
 	// Fetch unreactee
 	const note = await Note.findOne({
-		_id: noteId
+		_id: ps.noteId
 	});
 
 	if (note === null) {
@@ -48,7 +55,6 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 			}
 		});
 
-	// Send response
 	res();
 
 	const dec: any = {};
diff --git a/src/server/api/endpoints/notes/renotes.ts b/src/server/api/endpoints/notes/renotes.ts
new file mode 100644
index 0000000000..ff926806eb
--- /dev/null
+++ b/src/server/api/endpoints/notes/renotes.ts
@@ -0,0 +1,81 @@
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
+import Note, { packMany } from '../../../../models/note';
+import { ILocalUser } from '../../../../models/user';
+import getParams from '../../get-params';
+
+export const meta = {
+	desc: {
+		'ja-JP': '指定した投稿のRenote一覧を取得します。',
+		'en-US': 'Show a renotes of a note.'
+	},
+
+	requireCredential: false,
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+			transform: transform,
+		},
+
+		limit: {
+			validator: $.num.optional.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		},
+
+		untilId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+		}
+	}
+};
+
+export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
+
+	// Check if both of sinceId and untilId is specified
+	if (ps.sinceId && ps.untilId) {
+		return rej('cannot set sinceId and untilId');
+	}
+
+	// Lookup note
+	const note = await Note.findOne({
+		_id: ps.noteId
+	});
+
+	if (note === null) {
+		return rej('note not found');
+	}
+
+	const sort = {
+		_id: -1
+	};
+
+	const query = {
+		renoteId: note._id
+	} as any;
+
+	if (ps.sinceId) {
+		sort._id = 1;
+		query._id = {
+			$gt: ps.sinceId
+		};
+	} else if (ps.untilId) {
+		query._id = {
+			$lt: ps.untilId
+		};
+	}
+
+	const renotes = await Note
+		.find(query, {
+			limit: ps.limit,
+			sort: sort
+		});
+
+	res(await packMany(renotes, user));
+});
diff --git a/src/server/api/endpoints/notes/replies.ts b/src/server/api/endpoints/notes/replies.ts
index b2f8f94f69..86a75c1cab 100644
--- a/src/server/api/endpoints/notes/replies.ts
+++ b/src/server/api/endpoints/notes/replies.ts
@@ -1,34 +1,48 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import Note, { packMany } from '../../../../models/note';
 import { ILocalUser } from '../../../../models/user';
+import getParams from '../../get-params';
+
+export const meta = {
+	desc: {
+		'ja-JP': '指定した投稿への返信を取得します。',
+		'en-US': 'Get replies of a note.'
+	},
+
+	requireCredential: false,
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+			transform: transform,
+		},
+
+		limit: {
+			validator: $.num.optional.range(1, 100),
+			default: 10
+		},
+
+		offset: {
+			validator: $.num.optional.min(0),
+			default: 0
+		},
+	}
+};
 
-/**
- * Get replies of a note
- */
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'noteId' parameter
-	const [noteId, noteIdErr] = $.type(ID).get(params.noteId);
-	if (noteIdErr) return rej('invalid noteId param');
-
-	// Get 'limit' parameter
-	const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
-	if (limitErr) return rej('invalid limit param');
-
-	// Get 'offset' parameter
-	const [offset = 0, offsetErr] = $.num.optional.min(0).get(params.offset);
-	if (offsetErr) return rej('invalid offset param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
 	// Lookup note
 	const note = await Note.findOne({
-		_id: noteId
+		_id: ps.noteId
 	});
 
 	if (note === null) {
 		return rej('note not found');
 	}
 
-	const ids = (note._replyIds || []).slice(offset, offset + limit);
+	const ids = (note._replyIds || []).slice(ps.offset, ps.offset + ps.limit);
 
-	// Serialize
 	res(await packMany(ids, user));
 });
diff --git a/src/server/api/endpoints/notes/reposts.ts b/src/server/api/endpoints/notes/reposts.ts
deleted file mode 100644
index 2c6e1a499f..0000000000
--- a/src/server/api/endpoints/notes/reposts.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
-import Note, { packMany } from '../../../../models/note';
-import { ILocalUser } from '../../../../models/user';
-
-/**
- * Show a renotes of a note
- */
-export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'noteId' parameter
-	const [noteId, noteIdErr] = $.type(ID).get(params.noteId);
-	if (noteIdErr) return rej('invalid noteId param');
-
-	// Get 'limit' parameter
-	const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
-	if (limitErr) return rej('invalid limit param');
-
-	// Get 'sinceId' parameter
-	const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId);
-	if (sinceIdErr) return rej('invalid sinceId param');
-
-	// Get 'untilId' parameter
-	const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId);
-	if (untilIdErr) return rej('invalid untilId param');
-
-	// Check if both of sinceId and untilId is specified
-	if (sinceId && untilId) {
-		return rej('cannot set sinceId and untilId');
-	}
-
-	// Lookup note
-	const note = await Note.findOne({
-		_id: noteId
-	});
-
-	if (note === null) {
-		return rej('note not found');
-	}
-
-	// Construct query
-	const sort = {
-		_id: -1
-	};
-	const query = {
-		renoteId: note._id
-	} as any;
-	if (sinceId) {
-		sort._id = 1;
-		query._id = {
-			$gt: sinceId
-		};
-	} else if (untilId) {
-		query._id = {
-			$lt: untilId
-		};
-	}
-
-	// Issue query
-	const renotes = await Note
-		.find(query, {
-			limit: limit,
-			sort: sort
-		});
-
-	// Serialize
-	res(await packMany(renotes, user));
-});
diff --git a/src/server/api/endpoints/notes/search_by_tag.ts b/src/server/api/endpoints/notes/search_by_tag.ts
index e7fa15f768..99a6dce834 100644
--- a/src/server/api/endpoints/notes/search_by_tag.ts
+++ b/src/server/api/endpoints/notes/search_by_tag.ts
@@ -1,11 +1,10 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import Note from '../../../../models/note';
-import User, { ILocalUser } from '../../../../models/user';
+import { ILocalUser } from '../../../../models/user';
 import Mute from '../../../../models/mute';
 import { getFriendIds } from '../../common/get-friends';
 import { packMany } from '../../../../models/note';
 import getParams from '../../get-params';
-import { erase } from '../../../../prelude/array';
 
 export const meta = {
 	desc: {
@@ -13,99 +12,94 @@ export const meta = {
 	},
 
 	params: {
-		tag: $.str.optional.note({
+		tag: {
+			validator: $.str.optional,
 			desc: {
 				'ja-JP': 'タグ'
 			}
-		}),
+		},
 
-		query: $.arr($.arr($.str)).optional.note({
+		query: {
+			validator: $.arr($.arr($.str)).optional,
 			desc: {
 				'ja-JP': 'クエリ'
 			}
-		}),
+		},
 
-		includeUserIds: $.arr($.type(ID)).optional.note({
-			default: []
-		}),
+		following: {
+			validator: $.bool.optional.nullable,
+			default: null as any
+		},
 
-		excludeUserIds: $.arr($.type(ID)).optional.note({
-			default: []
-		}),
-
-		includeUserUsernames: $.arr($.str).optional.note({
-			default: []
-		}),
-
-		excludeUserUsernames: $.arr($.str).optional.note({
-			default: []
-		}),
-
-		following: $.bool.optional.nullable.note({
-			default: null
-		}),
-
-		mute: $.str.optional.note({
+		mute: {
+			validator: $.str.optional,
 			default: 'mute_all'
-		}),
-
-		reply: $.bool.optional.nullable.note({
-			default: null,
+		},
 
+		reply: {
+			validator: $.bool.optional.nullable,
+			default: null as any,
 			desc: {
 				'ja-JP': '返信に限定するか否か'
 			}
-		}),
-
-		renote: $.bool.optional.nullable.note({
-			default: null,
+		},
 
+		renote: {
+			validator: $.bool.optional.nullable,
+			default: null as any,
 			desc: {
 				'ja-JP': 'Renoteに限定するか否か'
 			}
-		}),
+		},
 
-		withFiles: $.bool.optional.note({
+		withFiles: {
+			validator: $.bool.optional,
 			desc: {
 				'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
 			}
-		}),
-
-		media: $.bool.optional.nullable.note({
-			default: null,
+		},
 
+		media: {
+			validator: $.bool.optional.nullable,
+			default: null as any,
 			desc: {
 				'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
 			}
-		}),
-
-		poll: $.bool.optional.nullable.note({
-			default: null,
+		},
 
+		poll: {
+			validator: $.bool.optional.nullable,
+			default: null as any,
 			desc: {
 				'ja-JP': 'アンケートが添付された投稿に限定するか否か'
 			}
-		}),
+		},
 
-		untilId: $.type(ID).optional.note({
+		untilId: {
+			validator: $.type(ID).optional,
+			transform: transform,
 			desc: {
 				'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します'
 			}
-		}),
+		},
 
-		sinceDate: $.num.optional.note({
-		}),
+		sinceDate: {
+			validator: $.num.optional,
+		},
 
-		untilDate: $.num.optional.note({
-		}),
+		untilDate: {
+			validator: $.num.optional,
+		},
 
-		offset: $.num.optional.min(0).note({
+		offset: {
+			validator: $.num.optional.min(0),
 			default: 0
-		}),
+		},
 
-		limit: $.num.optional.range(1, 30).note({
+		limit: {
+			validator: $.num.optional.range(1, 30),
 			default: 10
-		}),
+		},
 	}
 };
 
@@ -113,28 +107,6 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
 	const [ps, psErr] = getParams(meta, params);
 	if (psErr) return rej(psErr);
 
-	if (ps.includeUserUsernames != null) {
-		const ids = erase(null, await Promise.all(ps.includeUserUsernames.map(async (username) => {
-			const _user = await User.findOne({
-				usernameLower: username.toLowerCase()
-			});
-			return _user ? _user._id : null;
-		})));
-
-		ids.forEach(id => ps.includeUserIds.push(id));
-	}
-
-	if (ps.excludeUserUsernames != null) {
-		const ids = erase(null, await Promise.all(ps.excludeUserUsernames.map(async (username) => {
-			const _user = await User.findOne({
-				usernameLower: username.toLowerCase()
-			});
-			return _user ? _user._id : null;
-		})));
-
-		ids.forEach(id => ps.excludeUserIds.push(id));
-	}
-
 	const q: any = {
 		$and: [ps.tag ? {
 			tagsLower: ps.tag.toLowerCase()
@@ -150,20 +122,6 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
 
 	const push = (x: any) => q.$and.push(x);
 
-	if (ps.includeUserIds && ps.includeUserIds.length != 0) {
-		push({
-			userId: {
-				$in: ps.includeUserIds
-			}
-		});
-	} else if (ps.excludeUserIds && ps.excludeUserIds.length != 0) {
-		push({
-			userId: {
-				$nin: ps.excludeUserIds
-			}
-		});
-	}
-
 	if (ps.following != null && me != null) {
 		const ids = await getFriendIds(me._id, false);
 		push({
diff --git a/src/server/api/endpoints/notes/show.ts b/src/server/api/endpoints/notes/show.ts
index e84a948c97..8b426c0053 100644
--- a/src/server/api/endpoints/notes/show.ts
+++ b/src/server/api/endpoints/notes/show.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import Note, { pack } from '../../../../models/note';
 import { ILocalUser } from '../../../../models/user';
 import getParams from '../../get-params';
@@ -14,12 +14,14 @@ export const meta = {
 	requireCredential: false,
 
 	params: {
-		noteId: $.type(ID).note({
+		noteId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿のID',
 				'en-US': 'Target note ID.'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts
index 31a4978407..8834645fc8 100644
--- a/src/server/api/endpoints/notes/timeline.ts
+++ b/src/server/api/endpoints/notes/timeline.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import Note from '../../../../models/note';
 import Mute from '../../../../models/mute';
 import { getFriends } from '../../common/get-friends';
@@ -16,69 +16,81 @@ export const meta = {
 	requireCredential: true,
 
 	params: {
-		limit: $.num.optional.range(1, 100).note({
+		limit: {
+			validator: $.num.optional.range(1, 100),
 			default: 10,
 			desc: {
 				'ja-JP': '最大数'
 			}
-		}),
+		},
 
-		sinceId: $.type(ID).optional.note({
+		sinceId: {
+			validator: $.type(ID).optional,
+			transform: transform,
 			desc: {
 				'ja-JP': '指定すると、この投稿を基点としてより新しい投稿を取得します'
 			}
-		}),
+		},
 
-		untilId: $.type(ID).optional.note({
+		untilId: {
+			validator: $.type(ID).optional,
+			transform: transform,
 			desc: {
 				'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します'
 			}
-		}),
+		},
 
-		sinceDate: $.num.optional.note({
+		sinceDate: {
+			validator: $.num.optional,
 			desc: {
 				'ja-JP': '指定した時間を基点としてより新しい投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。'
 			}
-		}),
+		},
 
-		untilDate: $.num.optional.note({
+		untilDate: {
+			validator: $.num.optional,
 			desc: {
 				'ja-JP': '指定した時間を基点としてより古い投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。'
 			}
-		}),
+		},
 
-		includeMyRenotes: $.bool.optional.note({
+		includeMyRenotes: {
+			validator: $.bool.optional,
 			default: true,
 			desc: {
 				'ja-JP': '自分の行ったRenoteを含めるかどうか'
 			}
-		}),
+		},
 
-		includeRenotedMyNotes: $.bool.optional.note({
+		includeRenotedMyNotes: {
+			validator: $.bool.optional,
 			default: true,
 			desc: {
 				'ja-JP': 'Renoteされた自分の投稿を含めるかどうか'
 			}
-		}),
+		},
 
-		includeLocalRenotes: $.bool.optional.note({
+		includeLocalRenotes: {
+			validator: $.bool.optional,
 			default: true,
 			desc: {
 				'ja-JP': 'Renoteされたローカルの投稿を含めるかどうか'
 			}
-		}),
+		},
 
-		withFiles: $.bool.optional.note({
+		withFiles: {
+			validator: $.bool.optional,
 			desc: {
 				'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
 			}
-		}),
+		},
 
-		mediaOnly: $.bool.optional.note({
+		mediaOnly: {
+			validator: $.bool.optional,
 			desc: {
 				'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
 			}
-		}),
+		},
 	}
 };
 
diff --git a/src/server/api/endpoints/notes/user-list-timeline.ts b/src/server/api/endpoints/notes/user-list-timeline.ts
index 7dddc4834e..3bdcc6c121 100644
--- a/src/server/api/endpoints/notes/user-list-timeline.ts
+++ b/src/server/api/endpoints/notes/user-list-timeline.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import Note from '../../../../models/note';
 import Mute from '../../../../models/mute';
 import { packMany } from '../../../../models/note';
@@ -15,75 +15,89 @@ export const meta = {
 	requireCredential: true,
 
 	params: {
-		listId: $.type(ID).note({
+		listId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': 'リストのID'
 			}
-		}),
+		},
 
-		limit: $.num.optional.range(1, 100).note({
+		limit: {
+			validator: $.num.optional.range(1, 100),
 			default: 10,
 			desc: {
 				'ja-JP': '最大数'
 			}
-		}),
+		},
 
-		sinceId: $.type(ID).optional.note({
+		sinceId: {
+			validator: $.type(ID).optional,
+			transform: transform,
 			desc: {
 				'ja-JP': '指定すると、この投稿を基点としてより新しい投稿を取得します'
 			}
-		}),
+		},
 
-		untilId: $.type(ID).optional.note({
+		untilId: {
+			validator: $.type(ID).optional,
+			transform: transform,
 			desc: {
 				'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します'
 			}
-		}),
+		},
 
-		sinceDate: $.num.optional.note({
+		sinceDate: {
+			validator: $.num.optional,
 			desc: {
 				'ja-JP': '指定した時間を基点としてより新しい投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。'
 			}
-		}),
+		},
 
-		untilDate: $.num.optional.note({
+		untilDate: {
+			validator: $.num.optional,
 			desc: {
 				'ja-JP': '指定した時間を基点としてより古い投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。'
 			}
-		}),
+		},
 
-		includeMyRenotes: $.bool.optional.note({
+		includeMyRenotes: {
+			validator: $.bool.optional,
 			default: true,
 			desc: {
 				'ja-JP': '自分の行ったRenoteを含めるかどうか'
 			}
-		}),
+		},
 
-		includeRenotedMyNotes: $.bool.optional.note({
+		includeRenotedMyNotes: {
+			validator: $.bool.optional,
 			default: true,
 			desc: {
 				'ja-JP': 'Renoteされた自分の投稿を含めるかどうか'
 			}
-		}),
+		},
 
-		includeLocalRenotes: $.bool.optional.note({
+		includeLocalRenotes: {
+			validator: $.bool.optional,
 			default: true,
 			desc: {
 				'ja-JP': 'Renoteされたローカルの投稿を含めるかどうか'
 			}
-		}),
+		},
 
-		withFiles: $.bool.optional.note({
+		withFiles: {
+			validator: $.bool.optional,
 			desc: {
 				'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
 			}
-		}),
+		},
 
-		mediaOnly: $.bool.optional.note({
+		mediaOnly: {
+			validator: $.bool.optional,
 			desc: {
 				'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
 			}
-		}),
+		},
 	}
 };
 
diff --git a/src/server/api/endpoints/users/followers.ts b/src/server/api/endpoints/users/followers.ts
index 7fe3ca9943..71081835c7 100644
--- a/src/server/api/endpoints/users/followers.ts
+++ b/src/server/api/endpoints/users/followers.ts
@@ -1,32 +1,49 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import User, { ILocalUser } from '../../../../models/user';
 import Following from '../../../../models/following';
 import { pack } from '../../../../models/user';
 import { getFriendIds } from '../../common/get-friends';
+import getParams from '../../get-params';
+
+export const meta = {
+	desc: {
+		'ja-JP': '指定したユーザーのフォロワー一覧を取得します。',
+		'en-US': 'Get followers of a user.'
+	},
+
+	requireCredential: false,
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
+		},
+
+		limit: {
+			validator: $.num.optional.range(1, 100),
+			default: 10
+		},
+
+		cursor: {
+			validator: $.type(ID).optional,
+			default: null as any,
+			transform: transform,
+		},
+
+		iknow: {
+			validator: $.bool.optional,
+			default: false,
+		}
+	}
+};
 
-/**
- * Get followers of a user
- */
 export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'userId' parameter
-	const [userId, userIdErr] = $.type(ID).get(params.userId);
-	if (userIdErr) return rej('invalid userId param');
-
-	// Get 'iknow' parameter
-	const [iknow = false, iknowErr] = $.bool.optional.get(params.iknow);
-	if (iknowErr) return rej('invalid iknow param');
-
-	// Get 'limit' parameter
-	const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
-	if (limitErr) return rej('invalid limit param');
-
-	// Get 'cursor' parameter
-	const [cursor = null, cursorErr] = $.type(ID).optional.get(params.cursor);
-	if (cursorErr) return rej('invalid cursor param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
 	// Lookup user
 	const user = await User.findOne({
-		_id: userId
+		_id: ps.userId
 	}, {
 		fields: {
 			_id: true
@@ -43,7 +60,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
 	} as any;
 
 	// ログインしていてかつ iknow フラグがあるとき
-	if (me && iknow) {
+	if (me && ps.iknow) {
 		// Get my friends
 		const myFriends = await getFriendIds(me._id);
 
@@ -53,29 +70,27 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
 	}
 
 	// カーソルが指定されている場合
-	if (cursor) {
+	if (ps.cursor) {
 		query._id = {
-			$lt: cursor
+			$lt: ps.cursor
 		};
 	}
 
 	// Get followers
 	const following = await Following
 		.find(query, {
-			limit: limit + 1,
+			limit: ps.limit + 1,
 			sort: { _id: -1 }
 		});
 
 	// 「次のページ」があるかどうか
-	const inStock = following.length === limit + 1;
+	const inStock = following.length === ps.limit + 1;
 	if (inStock) {
 		following.pop();
 	}
 
-	// Serialize
 	const users = await Promise.all(following.map(f => pack(f.followerId, me, { detail: true })));
 
-	// Response
 	res({
 		users: users,
 		next: inStock ? following[following.length - 1]._id : null,
diff --git a/src/server/api/endpoints/users/following.ts b/src/server/api/endpoints/users/following.ts
index 0e564fd1b6..778ef54a27 100644
--- a/src/server/api/endpoints/users/following.ts
+++ b/src/server/api/endpoints/users/following.ts
@@ -1,32 +1,49 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import User, { ILocalUser } from '../../../../models/user';
 import Following from '../../../../models/following';
 import { pack } from '../../../../models/user';
 import { getFriendIds } from '../../common/get-friends';
+import getParams from '../../get-params';
+
+export const meta = {
+	desc: {
+		'ja-JP': '指定したユーザーのフォロー一覧を取得します。',
+		'en-US': 'Get following users of a user.'
+	},
+
+	requireCredential: false,
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
+		},
+
+		limit: {
+			validator: $.num.optional.range(1, 100),
+			default: 10
+		},
+
+		cursor: {
+			validator: $.type(ID).optional,
+			default: null as any,
+			transform: transform,
+		},
+
+		iknow: {
+			validator: $.bool.optional,
+			default: false,
+		}
+	}
+};
 
-/**
- * Get following users of a user
- */
 export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'userId' parameter
-	const [userId, userIdErr] = $.type(ID).get(params.userId);
-	if (userIdErr) return rej('invalid userId param');
-
-	// Get 'iknow' parameter
-	const [iknow = false, iknowErr] = $.bool.optional.get(params.iknow);
-	if (iknowErr) return rej('invalid iknow param');
-
-	// Get 'limit' parameter
-	const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
-	if (limitErr) return rej('invalid limit param');
-
-	// Get 'cursor' parameter
-	const [cursor = null, cursorErr] = $.type(ID).optional.get(params.cursor);
-	if (cursorErr) return rej('invalid cursor param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
 	// Lookup user
 	const user = await User.findOne({
-		_id: userId
+		_id: ps.userId
 	}, {
 		fields: {
 			_id: true
@@ -43,7 +60,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
 	} as any;
 
 	// ログインしていてかつ iknow フラグがあるとき
-	if (me && iknow) {
+	if (me && ps.iknow) {
 		// Get my friends
 		const myFriends = await getFriendIds(me._id);
 
@@ -53,29 +70,27 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
 	}
 
 	// カーソルが指定されている場合
-	if (cursor) {
+	if (ps.cursor) {
 		query._id = {
-			$lt: cursor
+			$lt: ps.cursor
 		};
 	}
 
 	// Get followers
 	const following = await Following
 		.find(query, {
-			limit: limit + 1,
+			limit: ps.limit + 1,
 			sort: { _id: -1 }
 		});
 
 	// 「次のページ」があるかどうか
-	const inStock = following.length === limit + 1;
+	const inStock = following.length === ps.limit + 1;
 	if (inStock) {
 		following.pop();
 	}
 
-	// Serialize
 	const users = await Promise.all(following.map(f => pack(f.followeeId, me, { detail: true })));
 
-	// Response
 	res({
 		users: users,
 		next: inStock ? following[following.length - 1]._id : null,
diff --git a/src/server/api/endpoints/users/get_frequently_replied_users.ts b/src/server/api/endpoints/users/get_frequently_replied_users.ts
index 42b6ce20d6..b0fd259588 100644
--- a/src/server/api/endpoints/users/get_frequently_replied_users.ts
+++ b/src/server/api/endpoints/users/get_frequently_replied_users.ts
@@ -1,19 +1,31 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import Note from '../../../../models/note';
 import User, { pack, ILocalUser } from '../../../../models/user';
+import getParams from '../../get-params';
+
+export const meta = {
+	requireCredential: false,
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
+		},
+
+		limit: {
+			validator: $.num.optional.range(1, 100),
+			default: 10
+		},
+	}
+};
 
 export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'userId' parameter
-	const [userId, userIdErr] = $.type(ID).get(params.userId);
-	if (userIdErr) return rej('invalid userId param');
-
-	// Get 'limit' parameter
-	const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
-	if (limitErr) return rej('invalid limit param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
 	// Lookup user
 	const user = await User.findOne({
-		_id: userId
+		_id: ps.userId
 	}, {
 		fields: {
 			_id: true
@@ -83,7 +95,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
 	const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]);
 
 	// Extract top replied users
-	const topRepliedUsers = repliedUsersSorted.slice(0, limit);
+	const topRepliedUsers = repliedUsersSorted.slice(0, ps.limit);
 
 	// Make replies object (includes weights)
 	const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({
@@ -91,6 +103,5 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
 		weight: repliedUsers[user] / peak
 	})));
 
-	// Response
 	res(repliesObj);
 });
diff --git a/src/server/api/endpoints/users/lists/delete.ts b/src/server/api/endpoints/users/lists/delete.ts
index c56963aab6..1d4513a822 100644
--- a/src/server/api/endpoints/users/lists/delete.ts
+++ b/src/server/api/endpoints/users/lists/delete.ts
@@ -1,5 +1,5 @@
 import $ from 'cafy';
-import ID from '../../../../../misc/cafy-id';
+import ID, { transform } from '../../../../../misc/cafy-id';
 import UserList from '../../../../../models/user-list';
 import { ILocalUser } from '../../../../../models/user';
 import getParams from '../../../get-params';
@@ -15,12 +15,14 @@ export const meta = {
 	kind: 'account-write',
 
 	params: {
-		listId: $.type(ID).note({
+		listId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象となるユーザーリストのID',
 				'en-US': 'ID of target user list'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/users/lists/push.ts b/src/server/api/endpoints/users/lists/push.ts
index 2d68ec7458..8208e627ed 100644
--- a/src/server/api/endpoints/users/lists/push.ts
+++ b/src/server/api/endpoints/users/lists/push.ts
@@ -1,10 +1,11 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 import UserList from '../../../../../models/user-list';
 import User, { pack as packUser, isRemoteUser, getGhost, ILocalUser } from '../../../../../models/user';
 import { publishUserListStream } from '../../../../../stream';
 import ap from '../../../../../remote/activitypub/renderer';
 import renderFollow from '../../../../../remote/activitypub/renderer/follow';
 import { deliver } from '../../../../../queue';
+import getParams from '../../../get-params';
 
 export const meta = {
 	desc: {
@@ -14,20 +15,28 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'account-write'
+	kind: 'account-write',
+
+	params: {
+		listId: {
+			validator: $.type(ID),
+			transform: transform,
+		},
+
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
+		},
+	}
 };
 
-/**
- * Add a user to a user list
- */
 export default async (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'listId' parameter
-	const [listId, listIdErr] = $.type(ID).get(params.listId);
-	if (listIdErr) return rej('invalid listId param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
 	// Fetch the list
 	const userList = await UserList.findOne({
-		_id: listId,
+		_id: ps.listId,
 		userId: me._id,
 	});
 
@@ -35,13 +44,9 @@ export default async (params: any, me: ILocalUser) => new Promise(async (res, re
 		return rej('list not found');
 	}
 
-	// Get 'userId' parameter
-	const [userId, userIdErr] = $.type(ID).get(params.userId);
-	if (userIdErr) return rej('invalid userId param');
-
 	// Fetch the user
 	const user = await User.findOne({
-		_id: userId
+		_id: ps.userId
 	});
 
 	if (user == null) {
diff --git a/src/server/api/endpoints/users/lists/show.ts b/src/server/api/endpoints/users/lists/show.ts
index a2dd00c6e1..585833a2fe 100644
--- a/src/server/api/endpoints/users/lists/show.ts
+++ b/src/server/api/endpoints/users/lists/show.ts
@@ -1,6 +1,7 @@
-import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
 import UserList, { pack } from '../../../../../models/user-list';
 import { ILocalUser } from '../../../../../models/user';
+import getParams from '../../../get-params';
 
 export const meta = {
 	desc: {
@@ -10,17 +11,23 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'account-read'
+	kind: 'account-read',
+
+	params: {
+		listId: {
+			validator: $.type(ID),
+			transform: transform,
+		},
+	}
 };
 
 export default async (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'listId' parameter
-	const [listId, listIdErr] = $.type(ID).get(params.listId);
-	if (listIdErr) return rej('invalid listId param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
 	// Fetch the list
 	const userList = await UserList.findOne({
-		_id: listId,
+		_id: ps.listId,
 		userId: me._id,
 	});
 
diff --git a/src/server/api/endpoints/users/lists/update.ts b/src/server/api/endpoints/users/lists/update.ts
index e39f66fb5b..fb1a37b2f1 100644
--- a/src/server/api/endpoints/users/lists/update.ts
+++ b/src/server/api/endpoints/users/lists/update.ts
@@ -1,5 +1,5 @@
 import $ from 'cafy';
-import ID from '../../../../../misc/cafy-id';
+import ID, { transform } from '../../../../../misc/cafy-id';
 import UserList, { pack } from '../../../../../models/user-list';
 import { ILocalUser } from '../../../../../models/user';
 import getParams from '../../../get-params';
@@ -15,18 +15,22 @@ export const meta = {
 	kind: 'account-write',
 
 	params: {
-		listId: $.type(ID).note({
+		listId: {
+			validator: $.type(ID),
+			transform: transform,
 			desc: {
 				'ja-JP': '対象となるユーザーリストのID',
 				'en-US': 'ID of target user list'
 			}
-		}),
-		title: $.str.range(1, 100).note({
+		},
+
+		title: {
+			validator: $.str.range(1, 100),
 			desc: {
 				'ja-JP': 'このユーザーリストの名前',
 				'en-US': 'name of this user list'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/users/notes.ts b/src/server/api/endpoints/users/notes.ts
index 6934f7c4be..ac6601f64f 100644
--- a/src/server/api/endpoints/users/notes.ts
+++ b/src/server/api/endpoints/users/notes.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 import getHostLower from '../../common/get-host-lower';
 import Note, { packMany } from '../../../../models/note';
 import User, { ILocalUser } from '../../../../models/user';
@@ -11,100 +11,118 @@ export const meta = {
 	},
 
 	params: {
-		userId: $.type(ID).optional.note({
+		userId: {
+			validator: $.type(ID).optional,
+			transform: transform,
 			desc: {
 				'ja-JP': 'ユーザーID'
 			}
-		}),
+		},
 
-		username: $.str.optional.note({
+		username: {
+			validator: $.str.optional,
 			desc: {
 				'ja-JP': 'ユーザー名'
 			}
-		}),
+		},
 
-		host: $.str.optional.note({
-		}),
+		host: {
+			validator: $.str.optional,
+		},
 
-		includeReplies: $.bool.optional.note({
+		includeReplies: {
+			validator: $.bool.optional,
 			default: true,
 
 			desc: {
 				'ja-JP': 'リプライを含めるか否か'
 			}
-		}),
+		},
 
-		limit: $.num.optional.range(1, 100).note({
+		limit: {
+			validator: $.num.optional.range(1, 100),
 			default: 10,
 			desc: {
 				'ja-JP': '最大数'
 			}
-		}),
+		},
 
-		sinceId: $.type(ID).optional.note({
+		sinceId: {
+			validator: $.type(ID).optional,
+			transform: transform,
 			desc: {
 				'ja-JP': '指定すると、この投稿を基点としてより新しい投稿を取得します'
 			}
-		}),
+		},
 
-		untilId: $.type(ID).optional.note({
+		untilId: {
+			validator: $.type(ID).optional,
+			transform: transform,
 			desc: {
 				'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します'
 			}
-		}),
+		},
 
-		sinceDate: $.num.optional.note({
+		sinceDate: {
+			validator: $.num.optional,
 			desc: {
 				'ja-JP': '指定した時間を基点としてより新しい投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。'
 			}
-		}),
+		},
 
-		untilDate: $.num.optional.note({
+		untilDate: {
+			validator: $.num.optional,
 			desc: {
 				'ja-JP': '指定した時間を基点としてより古い投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。'
 			}
-		}),
+		},
 
-		includeMyRenotes: $.bool.optional.note({
+		includeMyRenotes: {
+			validator: $.bool.optional,
 			default: true,
 			desc: {
 				'ja-JP': '自分の行ったRenoteを含めるかどうか'
 			}
-		}),
+		},
 
-		includeRenotedMyNotes: $.bool.optional.note({
+		includeRenotedMyNotes: {
+			validator: $.bool.optional,
 			default: true,
 			desc: {
 				'ja-JP': 'Renoteされた自分の投稿を含めるかどうか'
 			}
-		}),
+		},
 
-		includeLocalRenotes: $.bool.optional.note({
+		includeLocalRenotes: {
+			validator: $.bool.optional,
 			default: true,
 			desc: {
 				'ja-JP': 'Renoteされたローカルの投稿を含めるかどうか'
 			}
-		}),
+		},
 
-		withFiles: $.bool.optional.note({
+		withFiles: {
+			validator: $.bool.optional,
 			default: false,
 			desc: {
 				'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
 			}
-		}),
+		},
 
-		mediaOnly: $.bool.optional.note({
+		mediaOnly: {
+			validator: $.bool.optional,
 			default: false,
 			desc: {
 				'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
 			}
-		}),
+		},
 
-		fileType: $.arr($.str).optional.note({
+		fileType: {
+			validator: $.arr($.str).optional,
 			desc: {
 				'ja-JP': '指定された種類のファイルが添付された投稿のみを取得します'
 			}
-		}),
+		},
 	}
 };
 
@@ -121,7 +139,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
 		throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified';
 	}
 
-	const q = ps.userId !== undefined
+	const q = ps.userId != null
 		? { _id: ps.userId }
 		: { usernameLower: ps.username.toLowerCase(), host: getHostLower(ps.host) } ;
 
diff --git a/src/server/api/endpoints/users/relation.ts b/src/server/api/endpoints/users/relation.ts
index 5c7da48349..19643ceed0 100644
--- a/src/server/api/endpoints/users/relation.ts
+++ b/src/server/api/endpoints/users/relation.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform, ObjectId } from '../../../../misc/cafy-id';
 import { ILocalUser, getRelation } from '../../../../models/user';
 import getParams from '../../get-params';
 
@@ -10,11 +10,13 @@ export const meta = {
 	requireCredential: true,
 
 	params: {
-		userId: $.or($.type(ID), $.arr($.type(ID)).unique()).note({
+		userId: {
+			validator: $.or($.type(ID), $.arr($.type(ID)).unique()),
+			transform: (v: any): ObjectId | ObjectId[] => Array.isArray(v) ? v.map(x => transform(x)) : transform(v),
 			desc: {
 				'ja-JP': 'ユーザーID (配列でも可)'
 			}
-		})
+		}
 	}
 };
 
diff --git a/src/server/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts
index 307a8f6894..a2077b589d 100644
--- a/src/server/api/endpoints/users/search.ts
+++ b/src/server/api/endpoints/users/search.ts
@@ -11,32 +11,36 @@ export const meta = {
 	requireCredential: false,
 
 	params: {
-		query: $.str.note({
+		query: {
+			validator: $.str,
 			desc: {
 				'ja-JP': 'クエリ'
 			}
-		}),
+		},
 
-		offset: $.num.optional.min(0).note({
+		offset: {
+			validator: $.num.optional.min(0),
 			default: 0,
 			desc: {
 				'ja-JP': 'オフセット'
 			}
-		}),
+		},
 
-		limit: $.num.optional.range(1, 100).note({
+		limit: {
+			validator: $.num.optional.range(1, 100),
 			default: 10,
 			desc: {
 				'ja-JP': '取得する数'
 			}
-		}),
+		},
 
-		localOnly: $.bool.optional.note({
+		localOnly: {
+			validator: $.bool.optional,
 			default: false,
 			desc: {
 				'ja-JP': 'ローカルユーザーのみ検索対象にするか否か'
 			}
-		}),
+		},
 	},
 };
 
diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts
index dd09bd5b91..c6f01593d4 100644
--- a/src/server/api/endpoints/users/show.ts
+++ b/src/server/api/endpoints/users/show.ts
@@ -1,35 +1,54 @@
-import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import $ from 'cafy'; import ID, { transform, transformMany } from '../../../../misc/cafy-id';
 import User, { pack, ILocalUser, isRemoteUser } from '../../../../models/user';
 import resolveRemoteUser from '../../../../remote/resolve-user';
+import getParams from '../../get-params';
 
 const cursorOption = { fields: { data: false } };
 
-/**
- * Show user(s)
- */
+export const meta = {
+	desc: {
+		'ja-JP': '指定したユーザーの情報を取得します。'
+	},
+
+	requireCredential: false,
+
+	params: {
+		userId: {
+			validator: $.type(ID).optional,
+			transform: transform,
+			desc: {
+				'ja-JP': 'ユーザーID'
+			}
+		},
+
+		userIds: {
+			validator: $.arr($.type(ID)).optional.unique(),
+			transform: transformMany,
+			desc: {
+				'ja-JP': 'ユーザーID (配列)'
+			}
+		},
+
+		username: {
+			validator: $.str.optional
+		},
+
+		host: {
+			validator: $.str.optional.nullable
+		}
+	}
+};
+
 export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
+
 	let user;
 
-	// Get 'userId' parameter
-	const [userId, userIdErr] = $.type(ID).optional.get(params.userId);
-	if (userIdErr) return rej('invalid userId param');
-
-	// Get 'userIds' parameter
-	const [userIds, userIdsErr] = $.arr($.type(ID)).optional.get(params.userIds);
-	if (userIdsErr) return rej('invalid userIds param');
-
-	// Get 'username' parameter
-	const [username, usernameErr] = $.str.optional.get(params.username);
-	if (usernameErr) return rej('invalid username param');
-
-	// Get 'host' parameter
-	const [host, hostErr] = $.str.optional.nullable.get(params.host);
-	if (hostErr) return rej('invalid host param');
-
-	if (userIds) {
+	if (ps.userIds) {
 		const users = await User.find({
 			_id: {
-				$in: userIds
+				$in: ps.userIds
 			}
 		});
 
@@ -38,17 +57,17 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
 		}))));
 	} else {
 		// Lookup user
-		if (typeof host === 'string') {
+		if (typeof ps.host === 'string') {
 			try {
-				user = await resolveRemoteUser(username, host, cursorOption);
+				user = await resolveRemoteUser(ps.username, ps.host, cursorOption);
 			} catch (e) {
 				console.warn(`failed to resolve remote user: ${e}`);
 				return rej('failed to resolve remote user');
 			}
 		} else {
-			const q: any = userId !== undefined
-				? { _id: userId }
-				: { usernameLower: username.toLowerCase(), host: null };
+			const q: any = ps.userId != null
+				? { _id: ps.userId }
+				: { usernameLower: ps.username.toLowerCase(), host: null };
 
 			user = await User.findOne(q, cursorOption);
 
@@ -64,7 +83,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
 
 		if (isRemoteUser(user)) {
 			if (user.updatedAt == null || Date.now() - user.updatedAt.getTime() > 1000 * 60 * 60 * 24) {
-				resolveRemoteUser(username, host, { }, true);
+				resolveRemoteUser(ps.username, ps.host, { }, true);
 			}
 		}
 	}
diff --git a/src/server/api/get-params.ts b/src/server/api/get-params.ts
index 878f223cc3..60d038990e 100644
--- a/src/server/api/get-params.ts
+++ b/src/server/api/get-params.ts
@@ -1,27 +1,26 @@
-import { Context } from 'cafy';
+import { IEndpointMeta } from './endpoints';
 
-type Defs = {
-	params: { [key: string]: Context<any> }
-};
-
-export default function <T extends Defs>(defs: T, params: any): [{
-	[P in keyof T['params']]: ReturnType<T['params'][P]['get']>[0];
+export default function <T extends IEndpointMeta>(defs: T, params: any): [{
+	[P in keyof T['params']]: T['params'][P]['transform'] extends Function
+		? ReturnType<T['params'][P]['transform']>
+		: ReturnType<T['params'][P]['validator']['get']>[0];
 }, Error] {
 	const x: any = {};
 	let err: Error = null;
-	Object.keys(defs.params).some(k => {
-		const [v, e] = defs.params[k].get(params[k]);
+	Object.entries(defs.params).some(([k, def]) => {
+		const [v, e] = def.validator.get(params[k]);
 		if (e) {
 			err = new Error(e.message);
 			err.name = 'INVALID_PARAM';
 			(err as any).param = k;
 			return true;
 		} else {
-			if (v === undefined && defs.params[k].data.default) {
-				x[k] = defs.params[k].data.default;
+			if (v === undefined && def.default) {
+				x[k] = def.default;
 			} else {
 				x[k] = v;
 			}
+			if (def.transform) x[k] = def.transform(x[k]);
 			return false;
 		}
 	});
diff --git a/src/server/web/docs.ts b/src/server/web/docs.ts
index 14fb39e9b6..003022bcfc 100644
--- a/src/server/web/docs.ts
+++ b/src/server/web/docs.ts
@@ -181,7 +181,7 @@ router.get('/*/api/endpoints/*', async ctx => {
 		},
 		// @ts-ignore
 		params: ep.meta.params ? sortParams(Object.entries(ep.meta.params).map(([k, v]) => parseParamDefinition(k, v))) : null,
-		paramDefs: ep.meta.params ? extractParamDefRef(Object.values(ep.meta.params)) : null,
+		paramDefs: ep.meta.params ? extractParamDefRef(Object.values(ep.meta.params).map(x => x.validator)) : null,
 		res: ep.meta.res,
 		resProps: ep.meta.res && ep.meta.res.props ? sortParams(Object.entries(ep.meta.res.props).map(([k, v]) => parsePropDefinition(k, v))) : null,
 		resDefs: null as any, //extractPropDefRef(Object.entries(ep.res.props).map(([k, v]) => parsePropDefinition(k, v)))