1 /** 2 Base abstractions for dealing with the Discord REST API 3 */ 4 5 module dscord.api.client; 6 7 import std.array, 8 std.variant, 9 std.conv, 10 std.algorithm.iteration, 11 core.time; 12 13 import vibe.http.client, 14 vibe.stream.operations; 15 16 import dscord.types.all, 17 dscord.api.ratelimit, 18 dscord.api.util; 19 20 21 /** 22 APIClient is the base abstraction for interacting with the Discord API. 23 */ 24 class APIClient { 25 string baseURL = "https://discordapp.com/api"; 26 string token; 27 RateLimiter ratelimit; 28 Client client; 29 Logger log; 30 31 this(Client client) { 32 this.client = client; 33 this.log = client.log; 34 this.token = client.token; 35 this.ratelimit = new RateLimiter; 36 } 37 38 /** 39 Makes a HTTP request to the API (with empty body), returning an APIResponse 40 41 Params: 42 method = HTTPMethod to use when requesting 43 url = URL to make the request on 44 */ 45 APIResponse requestJSON(HTTPMethod method, U url) { 46 return requestJSON(method, url, ""); 47 } 48 49 /** 50 Makes a HTTP request to the API (with JSON body), returning an APIResponse 51 52 Params: 53 method = HTTPMethod to use when requesting 54 url = URL to make the request on 55 obj = VibeJSON object for the body 56 */ 57 APIResponse requestJSON(HTTPMethod method, U url, VibeJSON obj) { 58 return requestJSON(method, url, obj.toString); 59 } 60 61 /** 62 Makes a HTTP request to the API (with string body), returning an APIResponse 63 64 Params: 65 method = HTTPMethod to use when requesting 66 url = URL to make the request on 67 data = string contents of the body 68 timeout = HTTP timeout (default is 15 seconds) 69 */ 70 APIResponse requestJSON(HTTPMethod method, U url, string data, 71 Duration timeout=15.seconds) { 72 73 // Grab the rate limit lock 74 if (!this.ratelimit.wait(url.getBucket(), timeout)) { 75 throw new APIError(-1, "Request expired before rate-limit"); 76 } 77 78 this.log.tracef("API Request: [%s] %s: %s", method, this.baseURL ~ url.value, data); 79 auto res = new APIResponse(requestHTTP(this.baseURL ~ url.value, 80 (scope req) { 81 req.method = method; 82 req.headers["Authorization"] = this.token; 83 req.headers["Content-Type"] = "application/json"; 84 req.bodyWriter.write(data); 85 })); 86 87 // If we got a 429, cooldown and recurse 88 if (res.statusCode == 429) { 89 this.ratelimit.cooldown(url.getBucket(), 90 dur!"seconds"(res.header("Retry-After", "1").to!int)); 91 return this.requestJSON(method, url, data, timeout); 92 // If we got a 502, just retry immedietly 93 } else if (res.statusCode == 502) { 94 return this.requestJSON(method, url, data, timeout); 95 } 96 97 return res; 98 } 99 100 /** 101 Return the User object for the currently logged in user. 102 */ 103 User me() { 104 auto json = this.requestJSON(HTTPMethod.GET, U("users")("@me")).ok().fastJSON; 105 return new User(this.client, json); 106 } 107 108 /** 109 Return a User object for a Snowflake ID. 110 */ 111 User user(Snowflake id) { 112 auto json = this.requestJSON(HTTPMethod.GET, U("users")(id)).ok().fastJSON; 113 return new User(this.client, json); 114 } 115 116 /** 117 Modifies the current users settings. Returns a User object. 118 */ 119 User meSettings(string username, string avatar) { 120 auto json = this.requestJSON(HTTPMethod.PATCH, U("users")("@me")).ok().fastJSON; 121 return new User(this.client, json); 122 } 123 124 /** 125 Returns a list of Guild objects for the current user. 126 */ 127 Guild[] meGuilds() { 128 auto json = this.requestJSON(HTTPMethod.GET, U("users")("@me")("guilds")).ok().fastJSON; 129 return loadManyArray!Guild(this.client, json); 130 } 131 132 /** 133 Leaves a guild. 134 */ 135 void meGuildLeave(Snowflake id) { 136 this.requestJSON(HTTPMethod.DELETE, U("users")("@me")("guilds")(id)).ok(); 137 } 138 139 /** 140 Returns a list of Channel objects for the current user. 141 */ 142 Channel[] meDMChannels() { 143 auto json = this.requestJSON(HTTPMethod.GET, U("users")("@me")("channels")).ok().fastJSON; 144 return loadManyArray!Channel(this.client, json); 145 } 146 147 /** 148 Creates a new DM for a recipient (user) ID. Returns a Channel object. 149 */ 150 Channel meDMCreate(Snowflake recipientID) { 151 VibeJSON payload = VibeJSON.emptyObject; 152 payload["recipient_id"] = VibeJSON(recipientID); 153 auto json = this.requestJSON(HTTPMethod.POST, 154 U("users")("@me")("channels"), payload).ok().fastJSON; 155 return new Channel(this.client, json); 156 } 157 158 /** 159 Returns a Guild for a Snowflake ID. 160 */ 161 Guild guild(Snowflake id) { 162 auto json = this.requestJSON(HTTPMethod.GET, U("guilds")(id)).ok().fastJSON; 163 return new Guild(this.client, json); 164 } 165 166 /** 167 Deletes a guild. 168 */ 169 void guildDelete(Snowflake id) { 170 this.requestJSON(HTTPMethod.DELETE, U("guilds")(id)).ok(); 171 } 172 173 /** 174 Returns a list of channels for a Guild. 175 */ 176 Channel[] guildChannels(Snowflake id) { 177 auto json = this.requestJSON(HTTPMethod.GET, U("guilds")(id)("channels")).ok().fastJSON; 178 return loadManyArray!Channel(this.client, json); 179 } 180 181 /** 182 Removes (kicks) a user from a Guild. 183 */ 184 void guildRemoveMember(Snowflake id, Snowflake user) { 185 this.requestJSON(HTTPMethod.DELETE, U("guilds")(id)("members")(user)).ok(); 186 } 187 188 /* 189 Channel guildChannelCreate(Snowflake id, string name, string type, int bitrate = -1, int userLimit = -1) { 190 VibeJSON payload = VibeJSON.emptyObject; 191 payload["name"] = VibeJSON(id); 192 payload["type"] = VibeJSON(type); 193 if (bitrate > -1) payload["bitrate"] = VibeJSON(bitrate); 194 if (userLimit > -1) payload["user_limit"] = VibeJSON(userLimit); 195 } 196 */ 197 198 /** 199 Sends a message to a channel. 200 */ 201 Message sendMessage(Snowflake chan, string content, string nonce, bool tts) { 202 VibeJSON payload = VibeJSON.emptyObject; 203 payload["content"] = VibeJSON(content); 204 payload["nonce"] = VibeJSON(nonce); 205 payload["tts"] = VibeJSON(tts); 206 207 // Send payload and return message object 208 auto json = this.requestJSON(HTTPMethod.POST, 209 U("channels")(chan)("messages").bucket("send-message"), payload).ok().fastJSON; 210 return new Message(this.client, json); 211 } 212 213 /** 214 Edits a messages contents. 215 */ 216 Message editMessage(Snowflake chan, Snowflake msg, string content) { 217 VibeJSON payload = VibeJSON.emptyObject; 218 payload["content"] = content; 219 220 auto json = this.requestJSON(HTTPMethod.PATCH, 221 U("channels")(chan)("messages")(msg).bucket("edit-message"), payload).ok().fastJSON; 222 return new Message(this.client, json); 223 } 224 225 /** 226 Deletes a message. 227 */ 228 void deleteMessage(Snowflake chan, Snowflake msg) { 229 this.requestJSON(HTTPMethod.DELETE, 230 U("channels")(chan)("messages")(msg).bucket("del-message")).ok().fastJSON; 231 } 232 233 /** 234 Deletes messages in bulk. 235 */ 236 void bulkDeleteMessages(Snowflake chan, Snowflake[] msgs) { 237 VibeJSON payload = VibeJSON.emptyObject; 238 payload["messages"] = VibeJSON(array(map!((m) => VibeJSON(m))(msgs))); 239 240 this.requestJSON(HTTPMethod.POST, 241 U("channels")(chan)("messages")("bulk_delete").bucket("bulk-del-messages"), 242 payload).ok(); 243 } 244 245 /** 246 Returns a valid Gateway Websocket URL 247 */ 248 string gateway(int gatewayVersion = 5) { 249 return this.requestJSON(HTTPMethod.GET, U("gateway")("v", gatewayVersion.to!string)).ok().vibeJSON["url"].to!string; 250 } 251 }