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 }