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 }