From c2e053a208609d59188dce9e328c1ab9706aa35c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 25 Apr 2018 19:53:16 +0900
Subject: [PATCH] wip

---
 src/client/app/desktop/script.ts              |   2 +
 .../app/desktop/views/components/index.ts     |   2 +
 .../desktop/views/components/lists-window.vue |   2 +-
 .../app/desktop/views/components/notes.vue    |  53 +++++-
 .../views/components/timeline.core.vue        |  81 ++++----
 ...st-timeline.vue => user-list-timeline.vue} |  53 ++++--
 .../views/pages/{list.vue => user-list.vue}   |  12 +-
 src/server/api/endpoints.ts                   |  29 +++
 .../api/endpoints/notes/user-list-timeline.ts | 179 ++++++++++++++++++
 src/server/api/endpoints/users/lists/show.ts  |  23 +++
 .../api/endpoints/users/search_by_username.ts |   6 +-
 11 files changed, 363 insertions(+), 79 deletions(-)
 rename src/client/app/desktop/views/components/{list-timeline.vue => user-list-timeline.vue} (60%)
 rename src/client/app/desktop/views/pages/{list.vue => user-list.vue} (81%)
 create mode 100644 src/server/api/endpoints/notes/user-list-timeline.ts
 create mode 100644 src/server/api/endpoints/users/lists/show.ts

diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts
index 3b0ed48cd0..2658a86b95 100644
--- a/src/client/app/desktop/script.ts
+++ b/src/client/app/desktop/script.ts
@@ -28,6 +28,7 @@ import MkUser from './views/pages/user/user.vue';
 import MkFavorites from './views/pages/favorites.vue';
 import MkSelectDrive from './views/pages/selectdrive.vue';
 import MkDrive from './views/pages/drive.vue';
+import MkUserList from './views/pages/user-list.vue';
 import MkHomeCustomize from './views/pages/home-customize.vue';
 import MkMessagingRoom from './views/pages/messaging-room.vue';
 import MkNote from './views/pages/note.vue';
@@ -55,6 +56,7 @@ init(async (launch) => {
 			{ path: '/i/messaging/:user', component: MkMessagingRoom },
 			{ path: '/i/drive', component: MkDrive },
 			{ path: '/i/drive/folder/:folder', component: MkDrive },
+			{ path: '/i/lists/:list', component: MkUserList },
 			{ path: '/selectdrive', component: MkSelectDrive },
 			{ path: '/search', component: MkSearch },
 			{ path: '/othello', component: MkOthello },
diff --git a/src/client/app/desktop/views/components/index.ts b/src/client/app/desktop/views/components/index.ts
index 4f61f43692..f58d0706df 100644
--- a/src/client/app/desktop/views/components/index.ts
+++ b/src/client/app/desktop/views/components/index.ts
@@ -28,6 +28,7 @@ import friendsMaker from './friends-maker.vue';
 import followers from './followers.vue';
 import following from './following.vue';
 import usersList from './users-list.vue';
+import userListTimeline from './user-list-timeline.vue';
 import widgetContainer from './widget-container.vue';
 
 Vue.component('mk-ui', ui);
@@ -58,4 +59,5 @@ Vue.component('mk-friends-maker', friendsMaker);
 Vue.component('mk-followers', followers);
 Vue.component('mk-following', following);
 Vue.component('mk-users-list', usersList);
+Vue.component('mk-user-list-timeline', userListTimeline);
 Vue.component('mk-widget-container', widgetContainer);
diff --git a/src/client/app/desktop/views/components/lists-window.vue b/src/client/app/desktop/views/components/lists-window.vue
index 7097e5ed4b..30b1794a29 100644
--- a/src/client/app/desktop/views/components/lists-window.vue
+++ b/src/client/app/desktop/views/components/lists-window.vue
@@ -1,6 +1,6 @@
 <template>
 <mk-window ref="window" is-modal width="500px" height="550px" @closed="$destroy">
-	<span slot="header" :class="$style.header">%fa:list% リスト</span>
+	<span slot="header">%fa:list% リスト</span>
 
 	<button class="ui" @click="add">リストを作成</button>
 	<router-link v-for="list in lists" :key="list.id" :to="`/i/lists/${list.id}`">{{ list.title }}</router-link>
diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue
index 6965a18eda..ae36c899d5 100644
--- a/src/client/app/desktop/views/components/notes.vue
+++ b/src/client/app/desktop/views/components/notes.vue
@@ -1,5 +1,14 @@
 <template>
 <div class="mk-notes">
+	<div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div>
+
+	<slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot>
+
+	<div v-if="!fetching && requestInitPromise != null">
+		<p>読み込みに失敗しました。</p>
+		<button @click="resolveInitPromise">リトライ</button>
+	</div>
+
 	<transition-group name="mk-notes" class="transition">
 		<template v-for="(note, i) in _notes">
 			<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
@@ -9,7 +18,8 @@
 			</p>
 		</template>
 	</transition-group>
-	<footer v-if="loadMore">
+
+	<footer v-if="more">
 		<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
 			<template v-if="!moreFetching">%i18n:@load-more%</template>
 			<template v-if="moreFetching">%fa:spinner .pulse .fw%</template>
@@ -40,9 +50,10 @@ export default Vue.extend({
 
 	data() {
 		return {
+			requestInitPromise: null as () => Promise<any[]>,
 			notes: [],
 			queue: [],
-			fetching: false,
+			fetching: true,
 			moreFetching: false
 		};
 	},
@@ -80,9 +91,25 @@ export default Vue.extend({
 			Vue.set((this as any).notes, i, note);
 		},
 
-		init(notes) {
+		init(promiseGenerator: () => Promise<any[]>) {
+			this.requestInitPromise = promiseGenerator;
+			this.resolveInitPromise();
+		},
+
+		resolveInitPromise() {
 			this.queue = [];
-			this.notes = notes;
+			this.notes = [];
+			this.fetching = true;
+
+			const promise = this.requestInitPromise();
+
+			promise.then(notes => {
+				this.notes = notes;
+				this.requestInitPromise = null;
+				this.fetching = false;
+			}, e => {
+				this.fetching = false;
+			});
 		},
 
 		prepend(note, silent = false) {
@@ -137,6 +164,9 @@ export default Vue.extend({
 		},
 
 		async loadMore() {
+			if (this.more == null) return;
+			if (this.moreFetching) return;
+
 			this.moreFetching = true;
 			await this.more();
 			this.moreFetching = false;
@@ -157,6 +187,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 root(isDark)
 	.transition
 		.mk-notes-enter
@@ -183,6 +215,13 @@ root(isDark)
 			[data-fa]
 				margin-right 8px
 
+	> .newer-indicator
+		position -webkit-sticky
+		position sticky
+		z-index 100
+		height 3px
+		background $theme-color
+
 	> footer
 		> *
 			display block
@@ -191,16 +230,16 @@ root(isDark)
 			width 100%
 			text-align center
 			color #ccc
-			border-top solid 1px #eaeaea
+			border-top solid 1px isDark ? #1c2023 : #eaeaea
 			border-bottom-left-radius 4px
 			border-bottom-right-radius 4px
 
 		> button
 			&:hover
-				background #f5f5f5
+				background isDark ? #2e3440 : #f5f5f5
 
 			&:active
-				background #eee
+				background isDark ? #21242b : #eee
 
 .mk-notes[data-darkmode]
 	root(true)
diff --git a/src/client/app/desktop/views/components/timeline.core.vue b/src/client/app/desktop/views/components/timeline.core.vue
index 93cc59b556..f5e0ee118e 100644
--- a/src/client/app/desktop/views/components/timeline.core.vue
+++ b/src/client/app/desktop/views/components/timeline.core.vue
@@ -1,14 +1,15 @@
 <template>
 <div class="mk-timeline-core">
-	<div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div>
 	<mk-friends-maker v-if="src == 'home' && alone"/>
 	<div class="fetching" v-if="fetching">
 		<mk-ellipsis-icon/>
 	</div>
-	<p class="empty" v-if="notes.length == 0 && !fetching">
-		%fa:R comments%%i18n:@empty%
-	</p>
-	<mk-notes :notes="notes" ref="timeline" :more="canFetchMore ? more : null"/>
+
+	<mk-notes ref="timeline" :more="canFetchMore ? more : null">
+		<p :class="$style.empty" slot="empty">
+			%fa:R comments%%i18n:@empty%
+		</p>
+	</mk-notes>
 </div>
 </template>
 
@@ -89,28 +90,26 @@ export default Vue.extend({
 	},
 
 	methods: {
-		isScrollTop() {
-			return window.scrollY <= 8;
-		},
-
 		fetch(cb?) {
 			this.fetching = true;
 
-			(this as any).api(this.endpoint, {
-				limit: fetchLimit + 1,
-				untilDate: this.date ? this.date.getTime() : undefined,
-				includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
-				includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
-			}).then(notes => {
-				if (notes.length == fetchLimit + 1) {
-					notes.pop();
-					this.existMore = true;
-				}
-				(this.$refs.timeline as any).init(notes);
-				this.fetching = false;
-				this.$emit('loaded');
-				if (cb) cb();
-			});
+			(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
+				(this as any).api(this.endpoint, {
+					limit: fetchLimit + 1,
+					untilDate: this.date ? this.date.getTime() : undefined,
+					includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
+					includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
+				}).then(notes => {
+					if (notes.length == fetchLimit + 1) {
+						notes.pop();
+						this.existMore = true;
+					}
+					res(notes);
+					this.fetching = false;
+					this.$emit('loaded');
+					if (cb) cb();
+				}, rej);
+			}));
 		},
 
 		more() {
@@ -167,31 +166,27 @@ export default Vue.extend({
 @import '~const.styl'
 
 .mk-timeline-core
-	> .newer-indicator
-		position -webkit-sticky
-		position sticky
-		z-index 100
-		height 3px
-		background $theme-color
-
 	> .mk-friends-maker
 		border-bottom solid 1px #eee
 
 	> .fetching
 		padding 64px 0
 
-	> .empty
-		display block
-		margin 0 auto
-		padding 32px
-		max-width 400px
-		text-align center
-		color #999
+</style>
 
-		> [data-fa]
-			display block
-			margin-bottom 16px
-			font-size 3em
-			color #ccc
+<style lang="stylus" module>
+.empty
+	display block
+	margin 0 auto
+	padding 32px
+	max-width 400px
+	text-align center
+	color #999
+
+	> [data-fa]
+		display block
+		margin-bottom 16px
+		font-size 3em
+		color #ccc
 
 </style>
diff --git a/src/client/app/desktop/views/components/list-timeline.vue b/src/client/app/desktop/views/components/user-list-timeline.vue
similarity index 60%
rename from src/client/app/desktop/views/components/list-timeline.vue
rename to src/client/app/desktop/views/components/user-list-timeline.vue
index e946453f40..f71972ab78 100644
--- a/src/client/app/desktop/views/components/list-timeline.vue
+++ b/src/client/app/desktop/views/components/user-list-timeline.vue
@@ -1,5 +1,5 @@
 <template>
-	<mk-notes ref="timeline" :more="more"/>
+	<mk-notes ref="timeline" :more="existMore ? more : null"/>
 </template>
 
 <script lang="ts">
@@ -19,42 +19,49 @@ export default Vue.extend({
 		};
 	},
 	watch: {
-		$route: 'fetch'
+		$route: 'init'
 	},
 	mounted() {
-		this.fetch();
+		this.init();
 	},
 	beforeDestroy() {
 		this.connection.close();
 	},
 	methods: {
-		fetch() {
+		init() {
 			if (this.connection) this.connection.close();
 			this.connection = new UserListStream((this as any).os, (this as any).os.i, this.list.id);
 			this.connection.on('note', this.onNote);
 			this.connection.on('userAdded', this.onUserAdded);
 			this.connection.on('userRemoved', this.onUserRemoved);
 
+			this.fetch();
+		},
+		fetch() {
 			this.fetching = true;
 
-			(this as any).api('notes/list-timeline', {
-				limit: fetchLimit + 1,
-				includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
-				includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
-			}).then(notes => {
-				if (notes.length == fetchLimit + 1) {
-					notes.pop();
-					this.existMore = true;
-				}
-				(this.$refs.timeline as any).init(notes);
-				this.fetching = false;
-				this.$emit('loaded');
-			});
+			(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
+				(this as any).api('notes/user-list-timeline', {
+					listId: this.list.id,
+					limit: fetchLimit + 1,
+					includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
+					includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
+				}).then(notes => {
+					if (notes.length == fetchLimit + 1) {
+						notes.pop();
+						this.existMore = true;
+					}
+					res(notes);
+					this.fetching = false;
+					this.$emit('loaded');
+				}, rej);
+			}));
 		},
 		more() {
 			this.moreFetching = true;
 
 			(this as any).api('notes/list-timeline', {
+				listId: this.list.id,
 				limit: fetchLimit + 1,
 				untilId: (this.$refs.timeline as any).tail().id,
 				includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
@@ -68,7 +75,17 @@ export default Vue.extend({
 				notes.forEach(n => (this.$refs.timeline as any).append(n));
 				this.moreFetching = false;
 			});
-		}
+		},
+		onNote(note) {
+			// Prepend a note
+			(this.$refs.timeline as any).prepend(note);
+		},
+		onUserAdded() {
+			this.fetch();
+		},
+		onUserRemoved() {
+			this.fetch();
+		},
 	}
 });
 </script>
diff --git a/src/client/app/desktop/views/pages/list.vue b/src/client/app/desktop/views/pages/user-list.vue
similarity index 81%
rename from src/client/app/desktop/views/pages/list.vue
rename to src/client/app/desktop/views/pages/user-list.vue
index 70130eae68..1889f7dbe4 100644
--- a/src/client/app/desktop/views/pages/list.vue
+++ b/src/client/app/desktop/views/pages/user-list.vue
@@ -1,9 +1,11 @@
 <template>
 <mk-ui>
-	<header :class="$style.header">
-		<h1>{{ list.title }}</h1>
-	</header>
-	<mk-list-timeline :list="list"/>
+	<template v-if="!fetching">
+		<header :class="$style.header">
+			<h1>{{ list.title }}</h1>
+		</header>
+		<mk-user-list-timeline :list="list"/>
+	</template>
 </mk-ui>
 </template>
 
@@ -28,7 +30,7 @@ export default Vue.extend({
 			this.fetching = true;
 
 			(this as any).api('users/lists/show', {
-				id: this.$route.params.list
+				listId: this.$route.params.list
 			}).then(list => {
 				this.list = list;
 				this.fetching = false;
diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts
index 3686918147..734b8273f1 100644
--- a/src/server/api/endpoints.ts
+++ b/src/server/api/endpoints.ts
@@ -414,6 +414,27 @@ const endpoints: Endpoint[] = [
 		name: 'users/get_frequently_replied_users'
 	},
 
+	{
+		name: 'users/lists/show',
+		withCredential: true,
+		kind: 'account-read'
+	},
+	{
+		name: 'users/lists/create',
+		withCredential: true,
+		kind: 'account-write'
+	},
+	{
+		name: 'users/lists/push',
+		withCredential: true,
+		kind: 'account-write'
+	},
+	{
+		name: 'users/lists/list',
+		withCredential: true,
+		kind: 'account-read'
+	},
+
 	{
 		name: 'following/create',
 		withCredential: true,
@@ -503,6 +524,14 @@ const endpoints: Endpoint[] = [
 			max: 100
 		}
 	},
+	{
+		name: 'notes/user-list-timeline',
+		withCredential: true,
+		limit: {
+			duration: ms('10minutes'),
+			max: 100
+		}
+	},
 	{
 		name: 'notes/mentions',
 		withCredential: true,
diff --git a/src/server/api/endpoints/notes/user-list-timeline.ts b/src/server/api/endpoints/notes/user-list-timeline.ts
new file mode 100644
index 0000000000..bb94fa0ab9
--- /dev/null
+++ b/src/server/api/endpoints/notes/user-list-timeline.ts
@@ -0,0 +1,179 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy'; import ID from '../../../../cafy-id';
+import Note from '../../../../models/note';
+import Mute from '../../../../models/mute';
+import { pack } from '../../../../models/note';
+import UserList from '../../../../models/user-list';
+
+/**
+ * Get timeline of a user list
+ */
+module.exports = async (params, user, app) => {
+	// Get 'limit' parameter
+	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
+	if (limitErr) throw 'invalid limit param';
+
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$;
+	if (sinceIdErr) throw 'invalid sinceId param';
+
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$;
+	if (untilIdErr) throw 'invalid untilId param';
+
+	// Get 'sinceDate' parameter
+	const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$;
+	if (sinceDateErr) throw 'invalid sinceDate param';
+
+	// Get 'untilDate' parameter
+	const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$;
+	if (untilDateErr) throw 'invalid untilDate param';
+
+	// Check if only one of sinceId, untilId, sinceDate, untilDate specified
+	if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) {
+		throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified';
+	}
+
+	// Get 'includeMyRenotes' parameter
+	const [includeMyRenotes = true, includeMyRenotesErr] = $(params.includeMyRenotes).optional.boolean().$;
+	if (includeMyRenotesErr) throw 'invalid includeMyRenotes param';
+
+	// Get 'includeRenotedMyNotes' parameter
+	const [includeRenotedMyNotes = true, includeRenotedMyNotesErr] = $(params.includeRenotedMyNotes).optional.boolean().$;
+	if (includeRenotedMyNotesErr) throw 'invalid includeRenotedMyNotes param';
+
+	// Get 'listId' parameter
+	const [listId, listIdErr] = $(params.listId).type(ID).$;
+	if (listIdErr) throw 'invalid listId param';
+
+	const [list, mutedUserIds] = await Promise.all([
+		// リストを取得
+		// Fetch the list
+		UserList.findOne({
+			_id: listId,
+			userId: user._id
+		}),
+
+		// ミュートしているユーザーを取得
+		Mute.find({
+			muterId: user._id
+		}).then(ms => ms.map(m => m.muteeId))
+	]);
+
+	if (list.userIds.length == 0) {
+		return [];
+	}
+
+	//#region Construct query
+	const sort = {
+		_id: -1
+	};
+
+	const listQuery = list.userIds.map(u => ({
+		userId: u,
+
+		// リプライは含めない(ただし投稿者自身の投稿へのリプライ、自分の投稿へのリプライ、自分のリプライは含める)
+		$or: [{
+			// リプライでない
+			replyId: null
+		}, { // または
+			// リプライだが返信先が投稿者自身の投稿
+			$expr: {
+				$eq: ['$_reply.userId', '$userId']
+			}
+		}, { // または
+			// リプライだが返信先が自分(フォロワー)の投稿
+			'_reply.userId': user._id
+		}, { // または
+			// 自分(フォロワー)が送信したリプライ
+			userId: user._id
+		}]
+	}));
+
+	const query = {
+		$and: [{
+			// リストに入っている人のタイムラインへの投稿
+			$or: listQuery,
+
+			// mute
+			userId: {
+				$nin: mutedUserIds
+			},
+			'_reply.userId': {
+				$nin: mutedUserIds
+			},
+			'_renote.userId': {
+				$nin: mutedUserIds
+			},
+		}]
+	} as any;
+
+	// MongoDBではトップレベルで否定ができないため、De Morganの法則を利用してクエリします。
+	// つまり、「『自分の投稿かつRenote』ではない」を「『自分の投稿ではない』または『Renoteではない』」と表現します。
+	// for details: https://en.wikipedia.org/wiki/De_Morgan%27s_laws
+
+	if (includeMyRenotes === false) {
+		query.$and.push({
+			$or: [{
+				userId: { $ne: user._id }
+			}, {
+				renoteId: null
+			}, {
+				text: { $ne: null }
+			}, {
+				mediaIds: { $ne: [] }
+			}, {
+				poll: { $ne: null }
+			}]
+		});
+	}
+
+	if (includeRenotedMyNotes === false) {
+		query.$and.push({
+			$or: [{
+				'_renote.userId': { $ne: user._id }
+			}, {
+				renoteId: null
+			}, {
+				text: { $ne: null }
+			}, {
+				mediaIds: { $ne: [] }
+			}, {
+				poll: { $ne: null }
+			}]
+		});
+	}
+
+	if (sinceId) {
+		sort._id = 1;
+		query._id = {
+			$gt: sinceId
+		};
+	} else if (untilId) {
+		query._id = {
+			$lt: untilId
+		};
+	} else if (sinceDate) {
+		sort._id = 1;
+		query.createdAt = {
+			$gt: new Date(sinceDate)
+		};
+	} else if (untilDate) {
+		query.createdAt = {
+			$lt: new Date(untilDate)
+		};
+	}
+	//#endregion
+
+	// Issue query
+	const timeline = await Note
+		.find(query, {
+			limit: limit,
+			sort: sort
+		});
+
+	// Serialize
+	return await Promise.all(timeline.map(note => pack(note, user)));
+};
diff --git a/src/server/api/endpoints/users/lists/show.ts b/src/server/api/endpoints/users/lists/show.ts
new file mode 100644
index 0000000000..61e0f0463f
--- /dev/null
+++ b/src/server/api/endpoints/users/lists/show.ts
@@ -0,0 +1,23 @@
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
+import UserList, { pack } from '../../../../../models/user-list';
+
+/**
+ * Show a user list
+ */
+module.exports = async (params, me) => new Promise(async (res, rej) => {
+	// Get 'listId' parameter
+	const [listId, listIdErr] = $(params.listId).type(ID).$;
+	if (listIdErr) return rej('invalid listId param');
+
+	// Fetch the list
+	const userList = await UserList.findOne({
+		_id: listId,
+		userId: me._id,
+	});
+
+	if (userList == null) {
+		return rej('list not found');
+	}
+
+	res(await pack(userList));
+});
diff --git a/src/server/api/endpoints/users/search_by_username.ts b/src/server/api/endpoints/users/search_by_username.ts
index 41a12d5332..91d9ad1f3a 100644
--- a/src/server/api/endpoints/users/search_by_username.ts
+++ b/src/server/api/endpoints/users/search_by_username.ts
@@ -1,15 +1,11 @@
 /**
  * Module dependencies
  */
-import $ from 'cafy'; import ID from '../../../../cafy-id';
+import $ from 'cafy';
 import User, { pack } from '../../../../models/user';
 
 /**
  * Search a user by username
- *
- * @param {any} params
- * @param {any} me
- * @return {Promise<any>}
  */
 module.exports = (params, me) => new Promise(async (res, rej) => {
 	// Get 'query' parameter