1 module discord.w.api;
2 
3 import core.time;
4 import std.conv;
5 import std.datetime;
6 import std.string;
7 import std.typecons;
8 import std.uri;
9 
10 import vibe.core.core;
11 import vibe.core.log;
12 import vibe.data.json;
13 import vibe.http.client;
14 import vibe.inet.url;
15 import vibe.stream.operations;
16 
17 import discord.w.types;
18 import discord.w.json;
19 
20 alias TTS = Flag!"tts";
21 
22 enum discordwVersion = "1";
23 
24 enum discordEndpointBase = "https://discord.com/api/v6";
25 
26 struct HTTPRateLimit
27 {
28 	struct Info
29 	{
30 		long limit, remaining;
31 		SysTime reset;
32 	}
33 
34 	Info[string] infos;
35 	bool globalRatelimit;
36 	SysTime globalReset;
37 
38 	void call(string bucket) @safe
39 	{
40 		if (globalRatelimit)
41 		{
42 			auto left = globalReset - Clock.currTime;
43 			(() @trusted => logDebug("Global rate limit waiting %s", left))();
44 			sleep(left);
45 		}
46 		while (true)
47 		{
48 			auto info = bucket in infos;
49 			if (!info)
50 				return;
51 			auto now = Clock.currTime;
52 			if (info.reset == SysTime.init || info.reset <= now)
53 				return;
54 			if (info.remaining == 0)
55 			{
56 				(() @trusted => logDebug("Waiting for %s due to heuristic rate limit", info.reset - now))();
57 				sleep(info.reset - now);
58 				continue;
59 			}
60 			info.remaining--;
61 			(() @trusted => logDebugV("Got %s left in bucket %s", *info, bucket))();
62 			return;
63 		}
64 	}
65 
66 	bool update(string bucket, scope HTTPClientResponse res) @safe
67 	{
68 		const limit = res.headers.get("X-RateLimit-Limit", "");
69 		const remaining = res.headers.get("X-RateLimit-Remaining", "");
70 		const reset = res.headers.get("X-RateLimit-Reset", "");
71 		const global = res.headers.get("X-RateLimit-Global", "");
72 		const retryAfter = res.headers.get("Retry-After", "");
73 		if (global == "true")
74 		{
75 			auto dur = retryAfter.length ? retryAfter.to!int.msecs : 5.seconds;
76 			(() @trusted => logDiagnostic("Got globally rate limited, retrying in %s", dur))();
77 			globalReset = Clock.currTime + dur;
78 			globalRatelimit = true;
79 			sleep(dur);
80 			return false;
81 		}
82 		if (!reset.length || !limit.length || !remaining.length)
83 		{
84 			if (res.statusCode == HTTPStatus.tooManyRequests)
85 			{
86 				(() @trusted => logDiagnostic("TOO MANY REQUESTS, but no RateLimit headers sent?"))();
87 				sleep(1.seconds);
88 				return false;
89 			}
90 			return true;
91 		}
92 		Info info;
93 		info.limit = limit.to!long;
94 		info.remaining = remaining.to!long;
95 		info.reset = SysTime.fromUnixTime(reset.to!long);
96 		(() @trusted => logDebugV("Updating ratelimit bucket %s to %s", bucket, info))();
97 		infos[bucket] = info;
98 		bool gotResponse = res.statusCode != HTTPStatus.tooManyRequests;
99 		if (!gotResponse)
100 		{
101 			auto now = Clock.currTime;
102 			if (info.reset > now)
103 			{
104 				(() @trusted => logDebug("Retrying in %s because of %s rate limit", info.reset - now,
105 						bucket))();
106 				sleep(info.reset - now);
107 			}
108 			else
109 				(() @trusted => logDebug("Retrying immediately because of %s rate limit", bucket))();
110 		}
111 		return gotResponse;
112 	}
113 }
114 
115 HTTPRateLimit httpRateLimit;
116 
117 Json requestDiscordEndpoint(string route, string bucket = "",
118 		void delegate(scope HTTPClientRequest req) @safe requester = null) @safe
119 {
120 	(() @trusted => logDebug("Requesting Discord API Route %s (with rate limit bucket %s)",
121 			route, bucket))();
122 	if (!bucket.length)
123 		bucket = route;
124 	assert(route.length && route[0] == '/');
125 	assert(bucket.length && bucket[0] == '/');
126 	URL url = URL(discordEndpointBase ~ route);
127 	Json ret;
128 	bool haveRet;
129 	httpRateLimit.call(bucket);
130 	int try_ = 0;
131 	while (!haveRet)
132 	{
133 		if (try_ > 5)
134 			throw new Exception("Failed to request endpoint after 5 retries.");
135 		try_++;
136 		(() @trusted => logDebugV("Request try %s for %s", try_, url))();
137 		auto task = Task.getThis();
138 		auto t = (() @trusted => setTimer(12.seconds, { task.interrupt(); }))();
139 		scope (exit)
140 			t.stop();
141 		try
142 		{
143 			requestHTTP(url, (scope req) {
144 				req.headers.addField("User-Agent",
145 					"DiscordBot (https://github.com/WebFreak001/discord-w, " ~ discordwVersion ~ ")");
146 				if (requester)
147 					requester(req);
148 			}, (scope res) {
149 				t.stop();
150 				if (res.statusCode >= 300 && res.statusCode < 400)
151 				{
152 					string loc = res.headers.get("Location", "");
153 					(() @trusted => logDebugV("Getting redirected to %s", loc))();
154 					if (!loc.length)
155 						throw new Exception("Expected 'Location' header for redirect status code");
156 					if (loc.startsWith("http:", "https:"))
157 					{
158 						if (!loc.startsWith(discordEndpointBase))
159 							throw new Exception(
160 								"Global redirect not redirecting to discord endpoint base, aborting request");
161 						url = URL(discordEndpointBase ~ loc[discordEndpointBase.length .. $]);
162 					}
163 					else if (loc[0] == '/')
164 					{
165 						auto apiBase = URL(discordEndpointBase).pathString;
166 						if (!loc.startsWith(apiBase))
167 							throw new Exception("Redirect escaping current API base path");
168 						url = URL(discordEndpointBase ~ loc[apiBase.length .. $]);
169 					}
170 					else
171 					{
172 						url = url.parentURL ~ InetPath(loc);
173 					}
174 				}
175 				else
176 				{
177 					(() @trusted => logTrace("updating bucket for %s", url))();
178 					bool cont = httpRateLimit.update(bucket, res);
179 					(() @trusted => logTrace("updated bucket for %s, result=%s", url, cont))();
180 					if (res.statusCode == HTTPStatus.tooManyRequests)
181 					{
182 						(() @trusted => logDebugV("Got 429 TOO MANY REQUESTS for %s", url))();
183 						return;
184 					}
185 					else if (!(res.statusCode >= 200 && res.statusCode < 300))
186 						throw new Exception(
187 							"Got invalid HTTP status code " ~ res.statusCode.to!string
188 							~ " with data " ~ res.bodyReader.readAllUTF8);
189 					if (res.statusCode != 204)
190 						ret = res.bodyReader.readAllUTF8.parseJsonString;
191 					haveRet = true;
192 				}
193 			});
194 		}
195 		catch (InterruptException)
196 		{
197 			logWarn("Request for %s took too long and was interrupted", url);
198 		}
199 	}
200 	return ret;
201 }
202 
203 Json requestDiscordEndpointNull(HTTPMethod method, string route, string bucket,
204 		scope void delegate(scope HTTPClientRequest req) @safe requester) @safe
205 {
206 	return requestDiscordEndpoint(route, bucket, (scope req) @safe {
207 		if (requester)
208 			requester(req);
209 		req.method = method;
210 		req.writeBody(null, null);
211 	});
212 }
213 
214 Json requestDiscordEndpointJson(T)(HTTPMethod method, T value, string route,
215 		string bucket, scope void delegate(scope HTTPClientRequest req) @safe requester) @safe
216 {
217 	return requestDiscordEndpoint(route, bucket, (scope req) @safe {
218 		if (requester)
219 			requester(req);
220 		req.method = method;
221 		req.writeJsonBody(value);
222 	});
223 }
224 
225 enum simpleRequesters = q{
226 	Json simpleNullRequest(HTTPMethod method, string route = "", string bucket = "") const @safe
227 	{
228 		prependEndpoint(endpoint, route);
229 		if (bucket.length)
230 			prependEndpoint(endpoint, bucket);
231 		return requestDiscordEndpointNull(method, route, bucket, requester);
232 	}
233 
234 	Json simpleJsonRequest(T)(HTTPMethod method, T value, string route = "", string bucket = "") const @safe
235 	{
236 		prependEndpoint(endpoint, route);
237 		if (bucket.length)
238 			prependEndpoint(endpoint, bucket);
239 		return requestDiscordEndpointJson(method, value, route, bucket, requester);
240 	}
241 };
242 
243 void prependEndpoint(string endpoint, ref string value) @safe
244 {
245 	if (!value.length)
246 	{
247 		value = endpoint;
248 		return;
249 	}
250 	else if (value.startsWith(endpoint))
251 		return;
252 	else if (value.startsWith("/"))
253 		value = endpoint ~ value;
254 	else
255 		value = endpoint ~ "/" ~ value;
256 }
257 
258 void delegate(scope HTTPClientRequest req) @safe authBot(string token,
259 		scope void delegate(scope HTTPClientRequest req) @safe then = null) @safe
260 {
261 	return (scope req) @safe {
262 		req.headers.addField("Authorization", "Bot " ~ token);
263 		if (then)
264 			then(req);
265 	};
266 }
267 
268 struct ChannelAPI
269 {
270 	string endpoint;
271 	void delegate(scope HTTPClientRequest req) @safe requester;
272 
273 	this(Snowflake id, void delegate(scope HTTPClientRequest req) @safe requester = null) @safe
274 	{
275 		endpoint = "/channels/" ~ id.toString;
276 		this.requester = requester;
277 	}
278 
279 	struct Update
280 	{
281 		mixin OptionalSerializer!(typeof(this));
282 
283 		@optional Nullable!string name;
284 		@optional int position = -1;
285 		@optional Nullable!string topic;
286 		@optional Nullable!bool nsfw;
287 		@optional int bitrate;
288 		@optional int user_limit = -1;
289 		@optional Overwrite[] permission_overwrites;
290 		@optional Nullable!Snowflake parent_id;
291 	}
292 
293 	mixin(simpleRequesters);
294 
295 	Channel get() const @safe
296 	{
297 		return simpleNullRequest(HTTPMethod.GET).deserializeJson!Channel;
298 	}
299 
300 	@(Permissions.MANAGE_CHANNELS)
301 	void updateChannel(Update update) const @safe
302 	{
303 		simpleJsonRequest(HTTPMethod.PATCH, update);
304 	}
305 
306 	@(Permissions.MANAGE_CHANNELS)
307 	void deleteChannel() const @safe
308 	{
309 		simpleNullRequest(HTTPMethod.DELETE);
310 	}
311 
312 	@(Permissions.READ_MESSAGE_HISTORY)
313 	Message[] getMessages(int limit = 50, Nullable!Snowflake around = Nullable!Snowflake.init,
314 			Nullable!Snowflake before = Nullable!Snowflake.init,
315 			Nullable!Snowflake after = Nullable!Snowflake.init) const @safe
316 	{
317 		if (limit > 100)
318 			throw new Exception("Can only get at most 100 messages");
319 		auto route = "/messages";
320 		string query = "?limit=" ~ limit.to!string;
321 		if (!around.isNull)
322 			query ~= "&around=" ~ around.get.toString;
323 		if (!before.isNull)
324 			query ~= "&before=" ~ before.get.toString;
325 		if (!after.isNull)
326 			query ~= "&after=" ~ after.get.toString;
327 		return simpleNullRequest(HTTPMethod.GET, route ~ query, route).deserializeJson!(Message[]);
328 	}
329 
330 	@(Permissions.READ_MESSAGE_HISTORY)
331 	Message getMessage(Snowflake id) const @safe
332 	{
333 		return simpleNullRequest(HTTPMethod.GET, "/messages/" ~ id.toString, "/messages")
334 			.deserializeJson!Message;
335 	}
336 
337 	@(Permissions.SEND_MESSAGES)
338 	Message sendMessage(string content, Nullable!Snowflake nonce = Nullable!Snowflake.init,
339 			TTS tts = No.tts, Nullable!Embed embed = Nullable!Embed.init) const @safe
340 	{
341 		auto route = endpoint ~ "/messages";
342 		Json json = Json.emptyObject;
343 		json["content"] = Json(content);
344 		if (!nonce.isNull)
345 			json["nonce"] = Json(nonce.get.toString);
346 		if (tts)
347 			json["tts"] = Json(true);
348 		if (!embed.isNull)
349 			json["embed"] = serializeToJson(embed.get);
350 		return requestDiscordEndpoint(route, route, (scope req) {
351 			// waiting for https://github.com/vibe-d/vibe.d/pull/1876
352 			// or https://github.com/vibe-d/vibe.d/pull/1178
353 			// to get merged to support file/image upload
354 			if (requester)
355 				requester(req);
356 			req.method = HTTPMethod.POST;
357 			req.writeJsonBody(json);
358 		}).deserializeJson!Message;
359 	}
360 
361 	@(Permissions.SEND_MESSAGES)
362 	Message updateOwnMessage(Snowflake message, string content = null,
363 			Nullable!Embed embed = Nullable!Embed.init) const @safe
364 	{
365 		Json json = Json.emptyObject;
366 		if (content.length)
367 			json["content"] = Json(content);
368 		if (!embed.isNull)
369 			json["embed"] = serializeToJson(embed.get);
370 		return simpleJsonRequest(HTTPMethod.PATCH, json, "/messages/" ~ message.toString, "/messages")
371 			.deserializeJson!Message;
372 	}
373 
374 	@(Permissions.MANAGE_MESSAGES)
375 	void deleteMessage(Snowflake message) const @safe
376 	{
377 		simpleNullRequest(HTTPMethod.DELETE, "/messages/" ~ message.toString, "/messages");
378 	}
379 
380 	@(Permissions.MANAGE_MESSAGES)
381 	void deleteMessages(Snowflake[] messages) const @safe
382 	{
383 		if (messages.length < 1)
384 			throw new Exception("Need to delete at least 1 message");
385 		if (messages.length == 0)
386 			return deleteMessage(messages[0]);
387 		if (messages.length > 100)
388 			throw new Exception("Can delete at most 100 messages");
389 		simpleJsonRequest(HTTPMethod.POST, ["messages" : messages], "/messages/bulk-delete");
390 	}
391 
392 	@(Permissions.READ_MESSAGE_HISTORY | Permissions.ADD_REACTIONS)
393 	void react(Snowflake message, Emoji emoji) const @trusted
394 	{
395 		string path = "/messages/" ~ message.toString ~ "/reactions/"
396 			~ emoji.toAPIString ~ "/" ~ "@me".encodeComponent;
397 		simpleNullRequest(HTTPMethod.PUT, path, "/messages/reactions");
398 	}
399 
400 	@(Permissions.READ_MESSAGE_HISTORY | Permissions.ADD_REACTIONS)
401 	void unreact(Snowflake message, Emoji emoji) const @trusted
402 	{
403 		string path = "/messages/" ~ message.toString ~ "/reactions/"
404 			~ emoji.toAPIString ~ "/" ~ "@me".encodeComponent;
405 		simpleNullRequest(HTTPMethod.DELETE, path, "/messages/reactions");
406 	}
407 
408 	@(Permissions.READ_MESSAGE_HISTORY | Permissions.MANAGE_MESSAGES)
409 	void deleteReaction(Snowflake message, Emoji emoji, Snowflake author) const @trusted
410 	{
411 		string path = "/messages/" ~ message.toString ~ "/reactions/"
412 			~ emoji.toAPIString ~ "/" ~ author.toString;
413 		simpleNullRequest(HTTPMethod.DELETE, path, "/messages/reactions");
414 	}
415 
416 	@(Permissions.READ_MESSAGE_HISTORY)
417 	User[] getReactionsByEmoji(Snowflake message, Emoji emoji, Nullable!Snowflake before = Nullable!Snowflake.init,
418 			Nullable!Snowflake after = Nullable!Snowflake.init, int limit = 100) const @trusted
419 	{
420 		string query = "?limit=" ~ limit.to!string;
421 		if (!before.isNull)
422 			query ~= "&before=" ~ before.get.toString;
423 		if (!after.isNull)
424 			query ~= "&after=" ~ after.get.toString;
425 		string path = "/messages/" ~ message.toString ~ "/reactions/" ~ emoji.toAPIString ~ query;
426 		return simpleNullRequest(HTTPMethod.GET, path, "/messages/reactions").deserializeJson!(User[]);
427 	}
428 
429 	@(Permissions.MANAGE_MESSAGES)
430 	void clearReactions(Snowflake message) const @trusted
431 	{
432 		simpleNullRequest(HTTPMethod.DELETE,
433 				"/messages/" ~ message.toString ~ "/reactions", "/messages/reactions");
434 	}
435 
436 	@(Permissions.MANAGE_ROLES)
437 	void editChannelPermissions(Snowflake overwrite, uint allow, uint deny, string type) const @trusted
438 	{
439 		Json json = Json.emptyObject;
440 		json["allow"] = Json(allow);
441 		json["deny"] = Json(deny);
442 		json["type"] = Json(type);
443 		simpleJsonRequest(HTTPMethod.PUT, json, "/permissions/" ~ overwrite.toString);
444 	}
445 
446 	@(Permissions.MANAGE_ROLES)
447 	void deleteChannelPermissions(Snowflake overwrite) const @trusted
448 	{
449 		simpleNullRequest(HTTPMethod.DELETE, "/permissions/" ~ overwrite.toString, "/permissions");
450 	}
451 
452 	@(Permissions.MANAGE_CHANNELS)
453 	Invite[] getInvites() const @trusted
454 	{
455 		return simpleNullRequest(HTTPMethod.GET, "/invites").deserializeJson!(Invite[]);
456 	}
457 
458 	@(Permissions.CREATE_INSTANT_INVITE)
459 	Invite createInvite(Duration maxAge = 24.hours, int maxUses = 0,
460 			bool temporary = false, bool unique = false) const @trusted
461 	{
462 		Json json;
463 		if (maxAge != 24.hours)
464 			json["max_age"] = Json(maxAge.total!"seconds");
465 		if (maxUses)
466 			json["max_uses"] = Json(maxUses);
467 		if (temporary)
468 			json["temporary"] = Json(temporary);
469 		if (unique)
470 			json["unique"] = Json(unique);
471 		return simpleJsonRequest(HTTPMethod.POST, json, "/invites").deserializeJson!Invite;
472 	}
473 
474 	void triggerTypingIndicator() const @trusted
475 	{
476 		simpleNullRequest(HTTPMethod.POST, "/typing");
477 	}
478 
479 	Message[] getPinnedMessages() const @trusted
480 	{
481 		return simpleNullRequest(HTTPMethod.GET, "/pins").deserializeJson!(Message[]);
482 	}
483 
484 	@(Permissions.MANAGE_MESSAGES)
485 	void pinMessage(Snowflake id) const @trusted
486 	{
487 		simpleNullRequest(HTTPMethod.PUT, "/pins/" ~ id.toString, "/pins");
488 	}
489 
490 	@(Permissions.MANAGE_MESSAGES)
491 	void unpinMessage(Snowflake id) const @trusted
492 	{
493 		simpleNullRequest(HTTPMethod.DELETE, "/pins/" ~ id.toString, "/pins");
494 	}
495 
496 	void addToGroupDM(Snowflake user) const @trusted
497 	{
498 		simpleNullRequest(HTTPMethod.PUT, "/recipients/" ~ user.toString, "/recipients");
499 	}
500 
501 	void removeFromGroupDM(Snowflake user) const @trusted
502 	{
503 		simpleNullRequest(HTTPMethod.DELETE, "/recipients/" ~ user.toString, "/recipients");
504 	}
505 }
506 
507 struct GuildAPI
508 {
509 	string endpoint;
510 	void delegate(scope HTTPClientRequest req) @safe requester;
511 
512 	this(Snowflake id, void delegate(scope HTTPClientRequest req) @safe requester = null) @safe
513 	{
514 		endpoint = "/guilds/" ~ id.toString;
515 		this.requester = requester;
516 	}
517 
518 	mixin(simpleRequesters);
519 
520 	Guild get() const @safe
521 	{
522 		return simpleNullRequest(HTTPMethod.GET).deserializeJson!Guild;
523 	}
524 
525 	struct Update
526 	{
527 		mixin OptionalSerializer!(typeof(this));
528 
529 		@optional Nullable!string name;
530 		@optional Nullable!string region;
531 		@optional int verification_level = -1;
532 		@optional int default_message_notifications = -1;
533 		@optional int explicit_content_filter = -1;
534 		@optional Nullable!Snowflake afk_channel_id;
535 		@optional int afk_timeout = -1;
536 		@optional Nullable!string icon;
537 		@optional Nullable!Snowflake owner_id;
538 		@optional Nullable!string splash;
539 		@optional Nullable!Snowflake system_channel_id;
540 	}
541 
542 	@(Permissions.MANAGE_GUILD)
543 	Guild updateGuild(Update update) const @safe
544 	{
545 		return simpleJsonRequest(HTTPMethod.PATCH, update).deserializeJson!Guild;
546 	}
547 
548 	@(Permissions.MANAGE_GUILD)
549 	void deleteGuild() const @safe
550 	{
551 		simpleNullRequest(HTTPMethod.DELETE);
552 	}
553 
554 	Channel[] channels() const @safe
555 	{
556 		return simpleNullRequest(HTTPMethod.GET, "/channels").deserializeJson!(Channel[]);
557 	}
558 
559 	struct ChannelArgs
560 	{
561 		mixin OptionalSerializer!(typeof(this));
562 
563 		@optional Nullable!string name;
564 		@optional int type = -1;
565 		@optional int bitrate = -1;
566 		@optional int user_limit = -1;
567 		@optional Overwrite[] permission_overwrites;
568 		@optional Nullable!Snowflake parent_id;
569 		@optional Nullable!bool nsfw;
570 	}
571 
572 	@(Permissions.MANAGE_CHANNELS)
573 	Channel createChannel(ChannelArgs args) const @safe
574 	{
575 		return simpleJsonRequest(HTTPMethod.POST, args, "/channels").deserializeJson!Channel;
576 	}
577 
578 	@(Permissions.MANAGE_CHANNELS)
579 	void moveChannel(Snowflake channel, int position) const @safe
580 	{
581 		simpleJsonRequest(HTTPMethod.PATCH, ["id" : channel.toJson, "position"
582 				: Json(position)], "/channels");
583 	}
584 
585 	GuildMember guildMember(Snowflake userID) const @safe
586 	{
587 		return simpleNullRequest(HTTPMethod.GET, "/members/" ~ userID.toString, "/members")
588 			.deserializeJson!GuildMember;
589 	}
590 
591 	GuildMember[] members(int limit = 1, Snowflake after = Snowflake.init) const @safe
592 	{
593 		string query = "?";
594 		if (limit != 1)
595 			query ~= "limit=" ~ limit.to!string ~ "&";
596 		if (after != Snowflake.init)
597 			query ~= "after=" ~ after.toString ~ "&";
598 		query.length--;
599 		return simpleNullRequest(HTTPMethod.GET, "/members" ~ query, "/members").deserializeJson!(
600 				GuildMember[]);
601 	}
602 
603 	struct AddGuildMemberArgs
604 	{
605 		mixin OptionalSerializer!(typeof(this));
606 
607 		@optional Nullable!string access_token;
608 		@optional Nullable!string nick;
609 		@optional Snowflake[] roles;
610 		@optional Nullable!bool mute;
611 		@optional Nullable!bool deaf;
612 	}
613 
614 	GuildMember addMember(Snowflake userID, AddGuildMemberArgs args) const @safe
615 	{
616 		return simpleJsonRequest(HTTPMethod.PUT, args, "/members/" ~ userID.toString, "/members")
617 			.deserializeJson!GuildMember;
618 	}
619 
620 	struct ChangeGuildMemberArgs
621 	{
622 		mixin OptionalSerializer!(typeof(this));
623 
624 		@optional Nullable!string nick;
625 		@optional Snowflake[] roles;
626 		@optional Nullable!bool mute;
627 		@optional Nullable!bool deaf;
628 		@optional Nullable!Snowflake channel_id;
629 	}
630 
631 	void modifyMember(Snowflake userID, ChangeGuildMemberArgs args) const @safe
632 	{
633 		simpleJsonRequest(HTTPMethod.PATCH, args, "/members/" ~ userID.toString, "/members");
634 	}
635 
636 	@(Permissions.CHANGE_NICKNAME)
637 	string changeNickname(string nickname) const @safe
638 	{
639 		return simpleJsonRequest(HTTPMethod.PATCH, ["nick" : nickname], "/members/@me/nick")
640 			.deserializeJson!string;
641 	}
642 
643 	@(Permissions.MANAGE_ROLES)
644 	void addMemberRole(Snowflake user, Snowflake role) const @safe
645 	{
646 		simpleNullRequest(HTTPMethod.PUT,
647 				"/members/" ~ user.toString ~ "/roles/" ~ role.toString, "/members/roles");
648 	}
649 
650 	@(Permissions.MANAGE_ROLES)
651 	void removeMemberRole(Snowflake user, Snowflake role) const @safe
652 	{
653 		simpleNullRequest(HTTPMethod.DELETE,
654 				"/members/" ~ user.toString ~ "/roles/" ~ role.toString, "/members/roles");
655 	}
656 
657 	@(Permissions.KICK_MEMBERS)
658 	void kickUser(Snowflake user) const @safe
659 	{
660 		simpleNullRequest(HTTPMethod.DELETE, "/members/" ~ user.toString, "/members");
661 	}
662 
663 	@(Permissions.BAN_MEMBERS)
664 	Ban[] bans() const @safe
665 	{
666 		return simpleNullRequest(HTTPMethod.GET, "/bans").deserializeJson!(Ban[]);
667 	}
668 
669 	@(Permissions.BAN_MEMBERS)
670 	void banUser(Snowflake user, string reason = "", int deleteMessageDays = 0) const @safe
671 	{
672 		string query = "";
673 		if (deleteMessageDays != 0)
674 			query ~= "&delete-message-days=" ~ deleteMessageDays.to!string;
675 		if (reason.length)
676 			query ~= "&reason=" ~ (() @trusted => reason.encodeComponent)();
677 		if (query.length)
678 			(() @trusted => (cast(char[]) query)[0] = '?')();
679 		simpleNullRequest(HTTPMethod.PUT, "/bans/" ~ user.toString ~ query, "/bans");
680 	}
681 
682 	@(Permissions.BAN_MEMBERS)
683 	void unbanUser(Snowflake user) const @safe
684 	{
685 		simpleNullRequest(HTTPMethod.DELETE, "/bans/" ~ user.toString, "/bans");
686 	}
687 
688 	Role[] roles() const @safe
689 	{
690 		return simpleNullRequest(HTTPMethod.GET, "/roles").deserializeJson!(Role[]);
691 	}
692 
693 	struct RoleCreateArgs
694 	{
695 		mixin OptionalSerializer!(typeof(this));
696 
697 		string name;
698 		int color;
699 		bool hoist;
700 		uint permissions;
701 		bool mentionable;
702 	}
703 
704 	@(Permissions.MANAGE_ROLES)
705 	Role createRole(RoleCreateArgs role) const @safe
706 	{
707 		return simpleJsonRequest(HTTPMethod.POST, role, "/roles").deserializeJson!Role;
708 	}
709 
710 	@(Permissions.MANAGE_ROLES)
711 	Role[] moveRole(Snowflake role, int position) const @safe
712 	{
713 		return simpleJsonRequest(HTTPMethod.PATCH, ["id" : role.toJson, "position"
714 				: Json(position)], "/roles").deserializeJson!(Role[]);
715 	}
716 
717 	@(Permissions.MANAGE_ROLES)
718 	Role updateRole(Snowflake id, RoleCreateArgs role) const @safe
719 	{
720 		return simpleJsonRequest(HTTPMethod.PATCH, role, "/roles/" ~ id.toString, "/roles")
721 			.deserializeJson!Role;
722 	}
723 
724 	@(Permissions.MANAGE_ROLES)
725 	void removeRole(Snowflake id) const @safe
726 	{
727 		simpleNullRequest(HTTPMethod.DELETE, "/roles/" ~ id.toString, "/roles");
728 	}
729 
730 	@(Permissions.KICK_MEMBERS)
731 	int checkPrune(int days) const @safe
732 	{
733 		return simpleNullRequest(HTTPMethod.GET, "/prune?days=" ~ days.to!string)["pruned"]
734 			.deserializeJson!int;
735 	}
736 
737 	@(Permissions.KICK_MEMBERS)
738 	int pruneMembers(int days) const @safe
739 	{
740 		return simpleNullRequest(HTTPMethod.POST, "/prune?days=" ~ days.to!string)["pruned"]
741 			.deserializeJson!int;
742 	}
743 
744 	VoiceRegion[] voiceRegions() const @safe
745 	{
746 		return simpleNullRequest(HTTPMethod.GET, "/regions").deserializeJson!(VoiceRegion[]);
747 	}
748 
749 	@(Permissions.MANAGE_GUILD)
750 	Invite[] invites() const @safe
751 	{
752 		return simpleNullRequest(HTTPMethod.GET, "/invites").deserializeJson!(Invite[]);
753 	}
754 
755 	@(Permissions.MANAGE_GUILD)
756 	Integration[] integration() const @safe
757 	{
758 		return simpleNullRequest(HTTPMethod.GET, "/integrations").deserializeJson!(Integration[]);
759 	}
760 
761 	@(Permissions.MANAGE_GUILD)
762 	void createIntegration(string type, Snowflake id) const @safe
763 	{
764 		simpleJsonRequest(HTTPMethod.POST, ["type" : Json(type), "id" : id.toJson], "/integrations");
765 	}
766 
767 	@(Permissions.MANAGE_GUILD)
768 	void updateGuildIntegration(Snowflake id, int expireBehavior,
769 			int expireGracePeriod, bool enableEmoticons) const @safe
770 	{
771 		simpleJsonRequest(HTTPMethod.PATCH, ["expire_behavior" : Json(expireBehavior), "expire_grace_period"
772 				: Json(expireGracePeriod), "enable_emoticons" : Json(enableEmoticons)],
773 				"/integrations/" ~ id.toString, "/integrations");
774 	}
775 
776 	@(Permissions.MANAGE_GUILD)
777 	void deleteIntegration(Snowflake id) const @safe
778 	{
779 		simpleNullRequest(HTTPMethod.DELETE, "/integrations/" ~ id.toString, "/integrations");
780 	}
781 
782 	@(Permissions.MANAGE_GUILD)
783 	void syncIntegration(Snowflake id) const @safe
784 	{
785 		simpleNullRequest(HTTPMethod.POST, "/integrations/" ~ id.toString ~ "/sync",
786 				"/integrations/sync");
787 	}
788 
789 	@(Permissions.MANAGE_GUILD)
790 	GuildEmbed embed(Snowflake id) const @safe
791 	{
792 		return simpleNullRequest(HTTPMethod.GET, "/embed").deserializeJson!GuildEmbed;
793 	}
794 
795 	@(Permissions.MANAGE_GUILD)
796 	GuildEmbed updateEmbed(GuildEmbed embed) const @safe
797 	{
798 		return simpleJsonRequest(HTTPMethod.PATCH, embed, "/embed").deserializeJson!GuildEmbed;
799 	}
800 
801 	@(Permissions.MANAGE_GUILD)
802 	string vanityUrl() const @safe
803 	{
804 		return simpleNullRequest(HTTPMethod.GET, "/vanity-url")["code"].opt!string(null);
805 	}
806 }