1 module discord.w.types;
2 
3 import std.algorithm;
4 import std.conv;
5 import std.datetime;
6 import std..string;
7 import std.typecons;
8 import std.utf;
9 
10 import discord.w.minietf;
11 import discord.w.json;
12 
13 import vibe.data.json;
14 import vibe.inet.url;
15 
16 alias SafeTime = Nullable!SysTime;
17 
18 enum DiscordCDN = "https://cdn.discordapp.com/";
19 
20 /// typesafe alias to ulong
21 struct Snowflake
22 {
23 	private ulong id;
24 
25 	this(ulong id) @safe
26 	{
27 		this.id = id;
28 	}
29 
30 	void erlpack(ref ETFBuffer buffer)
31 	{
32 		buffer.putULong(id);
33 	}
34 
35 	static Snowflake erlunpack(ref ETFBuffer buffer)
36 	{
37 		if (buffer.peek(1)[0] == ETFHeader.binaryExt)
38 			return Snowflake((cast(string) buffer.readBinary()).to!ulong);
39 		return Snowflake(buffer.readULong);
40 	}
41 
42 	Json toJson() const @safe
43 	{
44 		return Json(id.to!string);
45 	}
46 
47 	static Snowflake fromJson(Json src) @safe
48 	{
49 		return Snowflake(src.get!string.to!ulong);
50 	}
51 
52 	string toString() const @safe
53 	{
54 		return id.to!string;
55 	}
56 
57 	static Snowflake fromString(string src) @safe
58 	{
59 		return Snowflake(src.to!ulong);
60 	}
61 }
62 
63 /// https://discordapp.com/developers/docs/topics/permissions
64 enum Permissions : uint
65 {
66 	CREATE_INSTANT_INVITE = 0x00000001, /// Allows creation of instant invites	T, V
67 	KICK_MEMBERS = 0x00000002, /// Allows kicking members	
68 	BAN_MEMBERS = 0x00000004, /// Allows banning members	
69 	ADMINISTRATOR = 0x00000008, /// Allows all permissions and bypasses channel permission overwrites	
70 	MANAGE_CHANNELS = 0x00000010, /// Allows management and editing of channels	T, V
71 	MANAGE_GUILD = 0x00000020, /// Allows management and editing of the guild	
72 	ADD_REACTIONS = 0x00000040, /// Allows for the addition of reactions to messages	T
73 	VIEW_AUDIT_LOG = 0x00000080, /// Allows for viewing of audit logs	
74 	VIEW_CHANNEL = 0x00000400, /// Allows guild members to view a channel, which includes reading messages in text channels	T, V
75 	SEND_MESSAGES = 0x00000800, /// Allows for sending messages in a channel	T
76 	SEND_TTS_MESSAGES = 0x00001000, /// Allows for sending of /tts messages	T
77 	MANAGE_MESSAGES = 0x00002000, /// Allows for deletion of other users messages	T
78 	EMBED_LINKS = 0x00004000, /// Links sent by users with this permission will be auto-embedded	T
79 	ATTACH_FILES = 0x00008000, /// Allows for uploading images and files	T
80 	READ_MESSAGE_HISTORY = 0x00010000, /// Allows for reading of message history	T
81 	MENTION_EVERYONE = 0x00020000, /// Allows for using the @everyone tag to notify all users in a channel, and the @here tag to notify all online users in a channel	T
82 	USE_EXTERNAL_EMOJIS = 0x00040000, /// Allows the usage of custom emojis from other servers	T
83 	CONNECT = 0x00100000, /// Allows for joining of a voice channel	V
84 	SPEAK = 0x00200000, /// Allows for speaking in a voice channel	V
85 	MUTE_MEMBERS = 0x00400000, /// Allows for muting members in a voice channel	V
86 	DEAFEN_MEMBERS = 0x00800000, /// Allows for deafening of members in a voice channel	V
87 	MOVE_MEMBERS = 0x01000000, /// Allows for moving of members between voice channels	V
88 	USE_VAD = 0x02000000, /// Allows for using voice-activity-detection in a voice channel	V
89 	CHANGE_NICKNAME = 0x04000000, /// Allows for modification of own nickname	
90 	MANAGE_NICKNAMES = 0x08000000, /// Allows for modification of other users nicknames	
91 	MANAGE_ROLES = 0x10000000, /// Allows management and editing of roles	T, V
92 	MANAGE_WEBHOOKS = 0x20000000, /// Allows management and editing of webhooks	T, V
93 	MANAGE_EMOJIS = 0x40000000, /// Allows management and editing of emojis	
94 }
95 
96 /// https://discordapp.com/developers/docs/resources/user#user-object
97 struct User
98 {
99 	mixin OptionalSerializer!(typeof(this));
100 
101 	Snowflake id;
102 	string username;
103 	string discriminator;
104 	Nullable!string avatar;
105 	@optional bool bot;
106 	@optional bool mfa_enabled;
107 	@optional bool verified;
108 	@optional Nullable!string email;
109 
110 	URL avatarURL(string format = "png") const @safe
111 	{
112 		if (avatar.isNull)
113 			return URL(DiscordCDN ~ "embed/avatars/" ~ (discriminator.to!int % 5).to!string ~ ".png");
114 		else
115 			return URL(DiscordCDN ~ "avatars/" ~ id.toString ~ "/" ~ avatar.get ~ "." ~ format);
116 	}
117 }
118 
119 struct PartialUser
120 {
121 	mixin OptionalSerializer!(typeof(this));
122 
123 	Snowflake id;
124 }
125 
126 /// https://discordapp.com/developers/docs/resources/channel#channel-object
127 struct Channel
128 {
129 	mixin OptionalSerializer!(typeof(this));
130 
131 	enum Type
132 	{
133 		guildText,
134 		dm,
135 		guildVoice,
136 		groupDM,
137 		guildCategory
138 	}
139 
140 	Snowflake id;
141 	Type type;
142 	@optional Snowflake guild_id;
143 	@optional int position;
144 	@optional Overwrite[] permission_overwrites;
145 	@optional Nullable!string name;
146 	@optional Nullable!string topic;
147 	@optional bool nsfw;
148 	@optional Nullable!string last_message_id;
149 	@optional int bitrate;
150 	@optional int user_limit;
151 	@optional User[] recipients;
152 	@optional Nullable!string icon;
153 	@optional Snowflake owner_id;
154 	@optional Snowflake application_id;
155 	@optional Nullable!Snowflake parent_id;
156 	@optional SafeTime last_pin_timestamp;
157 }
158 
159 /// https://discordapp.com/developers/docs/resources/channel#overwrite-object
160 struct Overwrite
161 {
162 	mixin OptionalSerializer!(typeof(this));
163 
164 	enum Type : Atom
165 	{
166 		role = atom("role"),
167 		member = atom("member")
168 	}
169 
170 	Snowflake id;
171 	Type type;
172 	uint allow;
173 	uint deny;
174 }
175 
176 /// https://discordapp.com/developers/docs/resources/guild#unavailable-guild-object
177 struct UnavailableGuild
178 {
179 	mixin OptionalSerializer!(typeof(this));
180 
181 	Snowflake id;
182 	@optional bool unavailable;
183 }
184 
185 /// https://discordapp.com/developers/docs/resources/guild#guild-object-verification-level
186 enum VerificationLevel
187 {
188 	none, ///
189 	low, /// verified email
190 	medium, /// 5 minute registration
191 	high, /// 10 minute server member
192 	veryHigh /// verified phone number
193 }
194 
195 /// https://discordapp.com/developers/docs/resources/guild#guild-object-default-message-notification-level
196 enum MessageNotificationLevel
197 {
198 	allMessages,
199 	onlyMentions
200 }
201 
202 /// https://discordapp.com/developers/docs/resources/guild#guild-object-explicit-content-filter-level
203 enum ExplicitContentFilterLevel
204 {
205 	disabled,
206 	membersWithoutRoles,
207 	allMembers
208 }
209 
210 /// https://discordapp.com/developers/docs/resources/guild#guild-object-mfa-level
211 enum MFALevel
212 {
213 	none,
214 	elevated
215 }
216 
217 /// https://discordapp.com/developers/docs/topics/permissions#role-object
218 struct Role
219 {
220 	mixin OptionalSerializer!(typeof(this));
221 
222 	Snowflake id;
223 	string name;
224 	int color;
225 	bool hoist;
226 	int position;
227 	uint permissions;
228 	bool managed;
229 	bool mentionable;
230 }
231 
232 /// https://discordapp.com/developers/docs/resources/emoji#emoji-object
233 struct Emoji
234 {
235 	mixin OptionalSerializer!(typeof(this));
236 
237 	static Emoji builtin(string emoji) @safe
238 	{
239 		Emoji ret;
240 		ret.name = emoji;
241 		return ret;
242 	}
243 
244 	static Emoji named(Snowflake id, string name, bool animated = false) @safe
245 	{
246 		Emoji ret;
247 		ret.id = id;
248 		ret.name = name;
249 		ret.animated = animated;
250 		return ret;
251 	}
252 
253 	string toAPIString() @safe
254 	{
255 		import std.uri : encodeComponent;
256 
257 		if (id.isNull)
258 			return (() @trusted => encodeComponent(name))();
259 		else
260 		{
261 			auto s = name ~ ":" ~ id.get.toString;
262 			return (() @trusted => encodeComponent(s))();
263 		}
264 	}
265 
266 	Nullable!Snowflake id;
267 	string name;
268 	@optional Snowflake[] roles;
269 	@optional User user;
270 	@optional bool require_colons;
271 	@optional bool managed;
272 	@optional bool animated;
273 
274 	bool opEquals(in Emoji other) const @safe
275 	{
276 		if (!id.isNull && other.id.isNull && id == other.id)
277 			return true;
278 		if (id != other.id)
279 			return false;
280 		return name == other.name;
281 	}
282 
283 	URL imageURL() const @safe
284 	{
285 		if (id.isNull) // TODO: replace with local URL
286 			return URL("https://cdnjs.cloudflare.com/ajax/libs/emojione/2.2.7/assets/png/" ~ name.byDchar.map!(
287 					a => (cast(ushort) a).to!string(16)).join('-') ~ ".png");
288 		if (animated)
289 			return URL(DiscordCDN ~ "emojis/" ~ id.get.toString ~ ".gif");
290 		else
291 			return URL(DiscordCDN ~ "emojis/" ~ id.get.toString ~ ".png");
292 	}
293 }
294 
295 /// https://discordapp.com/developers/docs/resources/voice#voice-state-object
296 struct VoiceState
297 {
298 	mixin OptionalSerializer!(typeof(this));
299 
300 	// must start with 3 snowflakes because of data.d!
301 	@optional Snowflake guild_id;
302 	Snowflake channel_id;
303 	Snowflake user_id;
304 	string session_id;
305 	bool deaf;
306 	bool mute;
307 	bool self_deaf;
308 	bool self_mute;
309 	bool suppress;
310 }
311 
312 /// https://discordapp.com/developers/docs/resources/guild#guild-member-object
313 struct GuildMember
314 {
315 	mixin OptionalSerializer!(typeof(this));
316 
317 	User user;
318 	@optional Nullable!string nick;
319 	Snowflake[] roles;
320 	SafeTime joined_at;
321 	bool deaf;
322 	bool mute;
323 }
324 
325 /// https://discordapp.com/developers/docs/topics/gateway#activity-object
326 struct Activity
327 {
328 	mixin OptionalSerializer!(typeof(this));
329 
330 	struct Timestamps
331 	{
332 		mixin OptionalSerializer!(typeof(this));
333 
334 		long start, end;
335 	}
336 
337 	struct Party
338 	{
339 		mixin OptionalSerializer!(typeof(this));
340 
341 		string id;
342 		int[2] size;
343 	}
344 
345 	struct Assets
346 	{
347 		mixin OptionalSerializer!(typeof(this));
348 
349 		string large_image;
350 		string large_text;
351 		string small_image;
352 		string small_text;
353 	}
354 
355 	enum Type : int
356 	{
357 		game,
358 		streaming,
359 		listening
360 	}
361 
362 	string name;
363 	Type type;
364 	@optional Nullable!string url;
365 	@optional Timestamps timestamps;
366 	@optional Snowflake application_id;
367 	@optional Nullable!string details;
368 	@optional Nullable!string state;
369 	@optional Party party;
370 	@optional Assets assets;
371 }
372 
373 struct UpdateStatus
374 {
375 	mixin OptionalSerializer!(typeof(this));
376 
377 	enum StatusType : Atom
378 	{
379 		online = atom("online"),
380 		dnd = atom("dnd"),
381 		idle = atom("idle"),
382 		invisible = atom("invisible"),
383 		offline = atom("offline")
384 	}
385 
386 	Nullable!long since;
387 	Nullable!Activity game;
388 	StatusType status;
389 	bool afk;
390 }
391 
392 unittest
393 {
394 	UpdateStatus u;
395 	u.status = UpdateStatus.StatusType.online;
396 	u.afk = false;
397 	u.game = Activity.init;
398 	u.game.name = "Bob";
399 	assert(serializeToJson(u) == Json(["since" : Json(null), "status"
400 			: Json("online"), "afk" : Json(false), "game" : Json(["name" : Json("Bob"), "type" : Json(0)])]));
401 }
402 
403 /// https://discordapp.com/developers/docs/topics/gateway#presence-update
404 struct PresenceUpdate
405 {
406 	mixin OptionalSerializer!(typeof(this));
407 
408 	enum Status : Atom
409 	{
410 		idle = atom("idle"),
411 		dnd = atom("dnd"),
412 		online = atom("online"),
413 		offline = atom("offline")
414 	}
415 
416 	PartialUser user;
417 	Snowflake[] roles;
418 	Activity game;
419 	Snowflake guild_id;
420 	Status status;
421 }
422 
423 /// https://discordapp.com/developers/docs/resources/guild#guild-object
424 struct Guild
425 {
426 	mixin OptionalSerializer!(typeof(this));
427 
428 	Snowflake id;
429 	string name;
430 	Nullable!string icon;
431 	Nullable!string splash;
432 	@optional bool owner;
433 	Snowflake owner_id;
434 	@optional uint permissions;
435 	string region;
436 	Snowflake afk_channel_id;
437 	int afk_timeout;
438 	@optional bool embed_enabled;
439 	@optional Snowflake embed_channel_id;
440 	VerificationLevel verification_level;
441 	MessageNotificationLevel default_message_notifications;
442 	ExplicitContentFilterLevel explicit_content_filter;
443 	Role[] roles;
444 	Emoji[] emojis;
445 	string[] features;
446 	MFALevel mfa_level;
447 	Nullable!Snowflake application_id;
448 	@optional bool widget_enabled;
449 	@optional Snowflake widget_channel_id;
450 	@optional Nullable!Snowflake system_channel_id;
451 	@optional SafeTime joined_at;
452 	@optional bool large;
453 	@optional bool unavailable;
454 	@optional int member_count;
455 	@optional VoiceState[] voice_states;
456 	@optional GuildMember[] members;
457 	@optional Channel[] channels;
458 	@optional PresenceUpdate[] presences;
459 
460 	URL iconURL(string format = "png")() const @safe 
461 			if (format == "png" || format == "jpg" || format == "jpeg" || format == "webp")
462 	{
463 		if (icon.isNull)
464 			return URL.init;
465 		return URL(DiscordCDN ~ "icons/" ~ id.toString ~ "/" ~ icon.get ~ "." ~ format);
466 	}
467 
468 	URL splashURL(string format = "png")() const @safe 
469 			if (format == "png" || format == "jpg" || format == "jpeg" || format == "webp")
470 	{
471 		if (splash.isNull)
472 			return URL.init;
473 		return URL(DiscordCDN ~ "splashes/" ~ id.toString ~ "/" ~ splash.get ~ "." ~ format);
474 	}
475 }
476 
477 /// https://discordapp.com/developers/docs/resources/channel#attachment-object
478 struct Attachment
479 {
480 	mixin OptionalSerializer!(typeof(this));
481 
482 	Snowflake id;
483 	string filename;
484 	long size;
485 	string url;
486 	string proxy_url;
487 	Nullable!uint height;
488 	Nullable!uint width;
489 }
490 
491 /// https://discordapp.com/developers/docs/resources/channel#embed-object
492 struct Embed
493 {
494 	mixin OptionalSerializer!(typeof(this));
495 
496 @optional: // not in the docs
497 
498 	struct Thumbnail
499 	{
500 		mixin OptionalSerializer!(typeof(this));
501 
502 	@optional: // not in the docs
503 
504 		string url;
505 		string proxy_url;
506 		int height;
507 		int width;
508 	}
509 
510 	struct Video
511 	{
512 		mixin OptionalSerializer!(typeof(this));
513 
514 	@optional: // not in the docs
515 
516 		string url;
517 		int height;
518 		int width;
519 	}
520 
521 	struct Image
522 	{
523 		mixin OptionalSerializer!(typeof(this));
524 
525 	@optional: // not in the docs
526 
527 		string url;
528 		string proxy_url;
529 		int height;
530 		int width;
531 	}
532 
533 	struct Provider
534 	{
535 		mixin OptionalSerializer!(typeof(this));
536 
537 	@optional: // not in the docs
538 
539 		string name;
540 		string url;
541 	}
542 
543 	struct Author
544 	{
545 		mixin OptionalSerializer!(typeof(this));
546 
547 	@optional: // not in the docs
548 
549 		string name;
550 		string url;
551 		string icon_url;
552 		string proxy_icon_url;
553 	}
554 
555 	struct Footer
556 	{
557 		mixin OptionalSerializer!(typeof(this));
558 
559 	@optional: // not in the docs
560 
561 		string text;
562 		string icon_url;
563 		string proxy_icon_url;
564 	}
565 
566 	struct Field
567 	{
568 		mixin OptionalSerializer!(typeof(this));
569 
570 	@optional: // not in the docs
571 
572 		string name;
573 		string value;
574 		bool inline;
575 	}
576 
577 	string title;
578 	string type;
579 	string description;
580 	string url;
581 	SafeTime timestamp;
582 	Nullable!int color;
583 	Footer footer;
584 	Image image;
585 	Thumbnail thumbnail;
586 	Video video;
587 	Provider provider;
588 	Author author;
589 	Field[] fields;
590 }
591 
592 /// https://discordapp.com/developers/docs/resources/guild#guild-embed-object
593 struct GuildEmbed
594 {
595 	mixin OptionalSerializer!(typeof(this));
596 
597 	bool enabled;
598 	Nullable!Snowflake channel_id;
599 }
600 
601 /// https://discordapp.com/developers/docs/resources/channel#reaction-object
602 struct Reaction
603 {
604 	mixin OptionalSerializer!(typeof(this));
605 
606 	int count;
607 	bool me;
608 	Emoji emoji;
609 
610 	// custom:
611 	@ignore Snowflake[] users;
612 }
613 
614 /// https://discordapp.com/developers/docs/resources/channel#message-object
615 struct Message
616 {
617 	mixin OptionalSerializer!(typeof(this));
618 
619 	enum Type
620 	{
621 		default_,
622 		recipientAdd,
623 		recipientRemove,
624 		call,
625 		channelNameChange,
626 		channelIconChange,
627 		channelPinnedMessage,
628 		guildMemberJoin
629 	}
630 
631 	Snowflake id;
632 	Snowflake channel_id;
633 	User author;
634 	string content;
635 	SafeTime timestamp;
636 	SafeTime edited_timestamp;
637 	bool tts;
638 	bool mention_everyone;
639 	User[] mentions;
640 	Snowflake[] mention_roles;
641 	Attachment[] attachments;
642 	Embed[] embeds;
643 	@optional Reaction[] reactions;
644 	@optional Nullable!Snowflake nonce;
645 	bool pinned;
646 	@optional Snowflake webhook_id;
647 	Type type;
648 	@optional Activity activity;
649 }
650 
651 struct ApplicationInformation
652 {
653 	mixin OptionalSerializer!(typeof(this));
654 
655 	Snowflake id;
656 	string name;
657 	@optional Nullable!string icon;
658 	@optional Nullable!string description;
659 	@optional Nullable!string[] rpc_origins;
660 	bool bot_public;
661 	bool bot_require_code_grant;
662 	User owner;
663 
664 	URL iconURL(string format = "png")() const @safe 
665 			if (format == "png" || format == "jpg" || format == "jpeg" || format == "webp")
666 	{
667 		if (!icon.length)
668 			return URL.init;
669 		return URL(DiscordCDN ~ "app-icons/" ~ id.toString ~ "/" ~ icon ~ "." ~ format);
670 	}
671 }
672 
673 /// https://discordapp.com/developers/docs/resources/invite#invite-object
674 struct Invite
675 {
676 	mixin OptionalSerializer!(typeof(this));
677 
678 	struct Guild
679 	{
680 		mixin OptionalSerializer!(typeof(this));
681 
682 		Snowflake id;
683 		string name;
684 		Nullable!string icon;
685 		Nullable!string splash;
686 	}
687 
688 	struct Channel
689 	{
690 		mixin OptionalSerializer!(typeof(this));
691 
692 		Snowflake id;
693 		.Channel.Type type;
694 		@optional Nullable!string name;
695 	}
696 
697 	struct InviteMetadata
698 	{
699 		mixin OptionalSerializer!(typeof(this));
700 
701 		User inviter;
702 		int uses;
703 		int max_uses;
704 		int max_age;
705 		bool temporary;
706 		SafeTime created_at;
707 		bool revoked;
708 	}
709 
710 	string code;
711 	Guild guild;
712 	Channel channel;
713 	@optional InviteMetadata metadata;
714 }
715 
716 /// https://discordapp.com/developers/docs/resources/guild#ban-object
717 struct Ban
718 {
719 	mixin OptionalSerializer!(typeof(this));
720 
721 	Nullable!string reason;
722 	User user;
723 }
724 
725 /// https://discordapp.com/developers/docs/resources/guild#integration-object
726 struct Integration
727 {
728 	/// https://discordapp.com/developers/docs/resources/guild#integration-account-object
729 	struct Account
730 	{
731 		mixin OptionalSerializer!(typeof(this));
732 
733 		string id;
734 		string name;
735 	}
736 
737 	mixin OptionalSerializer!(typeof(this));
738 
739 	Snowflake id;
740 	string name;
741 	string type;
742 	bool enabled;
743 	bool syncing;
744 	Snowflake role_id;
745 	int expire_behavior;
746 	int expire_grace_period;
747 	User user;
748 	Account account;
749 	SafeTime synced_at;
750 }
751 
752 // https://discordapp.com/developers/docs/resources/voice#voice-region-object
753 struct VoiceRegion
754 {
755 	mixin OptionalSerializer!(typeof(this));
756 
757 	string id;
758 	string name;
759 	bool vip;
760 	bool optimal;
761 	bool deprecated_;
762 	bool custom;
763 }