1 /** 2 Base abstractions for dealing with the Discord REST API 3 */ 4 5 module dscord.api.client; 6 7 import std.conv, 8 std.array, 9 std.format, 10 std.variant, 11 std.algorithm.iteration, 12 core.time; 13 14 import vibe.core.core, 15 vibe.http.client, 16 vibe.stream.operations, 17 vibe.textfilter.urlencode; 18 19 import dscord.api, 20 dscord.info, 21 dscord.types, 22 dscord.api.routes, 23 dscord.api.ratelimit; 24 25 /** 26 How messages are returned with respect to a provided messageID. 27 */ 28 enum MessageFilter : string { 29 AROUND = "around", 30 BEFORE = "before", 31 AFTER = "after" 32 } 33 34 /** 35 APIClient is the base abstraction for interacting with the Discord API. 36 */ 37 class APIClient { 38 string baseURL = "https://discordapp.com/api"; 39 string userAgent; 40 string token; 41 RateLimiter ratelimit; 42 Client client; 43 Logger log; 44 45 this(Client client) { 46 this.client = client; 47 this.log = client.log; 48 this.token = client.token; 49 this.ratelimit = new RateLimiter; 50 51 this.userAgent = format("DiscordBot (%s %s) %s", 52 GITHUB_REPO, VERSION, 53 "vibe.d/" ~ vibeVersionString); 54 } 55 56 /** 57 Makes a HTTP request to the API (with empty body), returning an APIResponse 58 59 Params: 60 route = route to make the request for 61 */ 62 APIResponse requestJSON(CompiledRoute route) { 63 return requestJSON(route, null, ""); 64 } 65 66 /** 67 Makes a HTTP request to the API (with JSON body), returning an APIResponse 68 69 Params: 70 route = route to make the request for 71 params = HTTP parameter hash table to pass to the URL router 72 */ 73 APIResponse requestJSON(CompiledRoute route, string[string] params) { 74 return requestJSON(route, params, ""); 75 } 76 77 /** 78 Makes a HTTP request to the API (with JSON body), returning an APIResponse 79 80 Params: 81 route = route to make the request for 82 obj = VibeJSON object for the JSON body 83 */ 84 APIResponse requestJSON(CompiledRoute route, VibeJSON obj) { 85 return requestJSON(route, null, obj.toString); 86 } 87 88 /** 89 Makes a HTTP request to the API (with string body), returning an APIResponse 90 91 Params: 92 route = route to make the request for 93 content = body content as a string 94 params = HTTP parameter hash table to pass to the URL router 95 */ 96 APIResponse requestJSON(CompiledRoute route, string[string] params, string content) { 97 // High timeout, we should never hit this 98 Duration timeout = 15.seconds; 99 100 // Check the rate limit for the route (this may sleep) 101 if (!this.ratelimit.check(route.bucket, timeout)) { 102 throw new APIError(-1, "Request expired before rate-limit cooldown."); 103 } 104 105 string paramString = ""; //A string containing URL encoded parameters 106 107 //If there are parameters, URL encode them into a string for appending 108 if(params != null){ 109 if(params.length > 0){ 110 paramString = "?"; 111 } 112 foreach(key; params.keys){ 113 paramString ~= urlEncode(key) ~ "=" ~ urlEncode(params[key]) ~ "&"; 114 } 115 paramString = paramString[0..$-1]; 116 } 117 118 auto res = new APIResponse(requestHTTP(this.baseURL ~ route.compiled ~ paramString, 119 (scope req) { 120 req.method = route.method; 121 req.headers["Authorization"] = "Bot " ~ this.token; 122 req.headers["Content-Type"] = "application/json"; 123 req.headers["User-Agent"] = this.userAgent; 124 req.bodyWriter.write(content); 125 })); 126 this.log.tracef("[%s] [%s] %s: \n\t%s", route.method, res.statusCode, this.baseURL ~ route.compiled, content); 127 128 // If we returned ratelimit headers, update our ratelimit states 129 if (res.header("X-RateLimit-Limit", "") != "") { 130 this.ratelimit.update(route.bucket, 131 res.header("X-RateLimit-Remaining"), 132 res.header("X-RateLimit-Reset"), 133 res.header("Retry-After", "")); 134 } 135 136 // We ideally should never hit 429s, but in the case we do just retry the 137 /// request fully. 138 if (res.statusCode == 429) { 139 this.log.error("Request returned 429. This should not happen."); 140 return this.requestJSON(route, params, content); 141 // If we got a 502, just retry after a random backoff 142 } else if (res.statusCode == 502) { 143 sleep(randomBackoff()); 144 return this.requestJSON(route, params, content); 145 } 146 147 return res; 148 } 149 150 /** 151 Return the User object for the currently logged in user. 152 */ 153 User usersMeGet() { 154 auto json = this.requestJSON(Routes.USERS_ME_GET()).ok().vibeJSON; 155 return new User(this.client, json); 156 } 157 158 /** 159 Return a User object for a Snowflake ID. 160 */ 161 User usersGet(Snowflake id) { 162 auto json = this.requestJSON(Routes.USERS_GET(id)).vibeJSON; 163 return new User(this.client, json); 164 } 165 166 /** 167 Modifies the current users settings. Returns a User object. 168 */ 169 User usersMePatch(string username, string avatar) { 170 VibeJSON data = VibeJSON(["username": VibeJSON(username), "avatar": VibeJSON(avatar)]); 171 auto json = this.requestJSON(Routes.USERS_ME_PATCH(), data).vibeJSON; 172 return new User(this.client, json); 173 } 174 175 /** 176 Returns a list of Guild objects for the current user. 177 */ 178 Guild[] usersMeGuildsList() { 179 auto json = this.requestJSON(Routes.USERS_ME_GUILDS_LIST()).ok().vibeJSON; 180 return deserializeFromJSONArray(json, v => new Guild(this.client, v)); 181 } 182 183 /** 184 Leaves a guild. 185 */ 186 void usersMeGuildsLeave(Snowflake id) { 187 this.requestJSON(Routes.USERS_ME_GUILDS_LEAVE(id)).ok(); 188 } 189 190 /** 191 Returns a list of Channel objects for the current user. 192 */ 193 Channel[] usersMeDMSList() { 194 auto json = this.requestJSON(Routes.USERS_ME_DMS_LIST()).ok().vibeJSON; 195 return deserializeFromJSONArray(json, v => new Channel(this.client, v)); 196 } 197 198 /** 199 Creates a new DM for a recipient (user) ID. Returns a Channel object. 200 */ 201 Channel usersMeDMSCreate(Snowflake recipientID) { 202 VibeJSON payload = VibeJSON(["recipient_id": VibeJSON(recipientID)]); 203 auto json = this.requestJSON(Routes.USERS_ME_DMS_CREATE(), payload).ok().vibeJSON; 204 return new Channel(this.client, json); 205 } 206 207 /** 208 Returns a Guild for a Snowflake ID. 209 */ 210 Guild guildsGet(Snowflake id) { 211 auto json = this.requestJSON(Routes.GUILDS_GET(id)).ok().vibeJSON; 212 return new Guild(this.client, json); 213 } 214 215 /** 216 Modifies a guild. 217 */ 218 Guild guildsModify(Snowflake id, VibeJSON obj) { 219 auto json = this.requestJSON(Routes.GUILDS_MODIFY(id), obj).vibeJSON; 220 return new Guild(this.client, json); 221 } 222 223 /** 224 Deletes a guild. 225 */ 226 void guildsDelete(Snowflake id) { 227 this.requestJSON(Routes.GUILDS_DELETE(id)).ok(); 228 } 229 230 /** 231 Returns a list of channels for a Guild. 232 */ 233 Channel[] guildsChannelsList(Snowflake id) { 234 auto json = this.requestJSON(Routes.GUILDS_CHANNELS_LIST(id)).ok().vibeJSON; 235 return deserializeFromJSONArray(json, v => new Channel(this.client, v)); 236 } 237 238 /** 239 Removes (kicks) a user from a Guild. 240 */ 241 void guildsMembersKick(Snowflake id, Snowflake user) { 242 this.requestJSON(Routes.GUILDS_MEMBERS_KICK(id, user)).ok(); 243 } 244 245 /** 246 Sends a message to a channel. 247 */ 248 Message channelsMessagesCreate(Snowflake chan, inout(string) content, inout(string) nonce, inout(bool) tts, inout(MessageEmbed) embed) { 249 VibeJSON payload = VibeJSON([ 250 "content": VibeJSON(content), 251 "nonce": VibeJSON(nonce), 252 "tts": VibeJSON(tts), 253 ]); 254 255 if (embed) { 256 payload["embed"] = embed.serializeToJSON(); 257 } 258 259 // Send payload and return message object 260 auto json = this.requestJSON(Routes.CHANNELS_MESSAGES_CREATE(chan), payload).ok().vibeJSON; 261 return new Message(this.client, json); 262 } 263 264 /** 265 Edits a messages contents. 266 */ 267 Message channelsMessagesModify(Snowflake chan, Snowflake msg, inout(string) content, inout(MessageEmbed) embed) { 268 VibeJSON payload = VibeJSON(["content": VibeJSON(content)]); 269 270 if (embed) { 271 payload["embed"] = embed.serializeToJSON(); 272 } 273 274 auto json = this.requestJSON(Routes.CHANNELS_MESSAGES_MODIFY(chan, msg), payload).ok().vibeJSON; 275 return new Message(this.client, json); 276 } 277 278 /** 279 Deletes a message. 280 */ 281 void channelsMessagesDelete(Snowflake chan, Snowflake msg) { 282 this.requestJSON(Routes.CHANNELS_MESSAGES_DELETE(chan, msg)).ok(); 283 } 284 285 /** 286 Returns an array of message IDs for a channel up to limit (max 100), 287 filter with respect to supplied messageID. 288 */ 289 Message[] channelsMessagesList(Snowflake chan, uint limit = 50, MessageFilter filter = MessageFilter.BEFORE, Snowflake msg = 0){ 290 enum string errorTooMany = "The maximum number of messages that can be returned at one time is 100."; 291 assert(limit <= 100, errorTooMany); 292 293 if(limit > 100){ 294 throw new Exception(errorTooMany); 295 } 296 297 string[string] params = ["limit":limit.toString]; 298 299 if(msg){ 300 params[filter] = msg.toString; 301 } 302 303 auto json = this.requestJSON(Routes.CHANNELS_MESSAGES_LIST(chan), params).ok().vibeJSON; 304 return deserializeFromJSONArray(json, v => new Message(this.client, v)); 305 } 306 307 /** 308 Deletes messages in bulk. 309 */ 310 void channelsMessagesDeleteBulk(Snowflake chan, Snowflake[] msgIDs) { 311 VibeJSON payload = VibeJSON(["messages": VibeJSON(array(map!((m) => VibeJSON(m))(msgIDs)))]); 312 this.requestJSON(Routes.CHANNELS_MESSAGES_DELETE_BULK(chan), payload).ok(); 313 } 314 315 /** 316 Returns a valid Gateway Websocket URL 317 */ 318 string gatewayGet() { 319 return this.requestJSON(Routes.GATEWAY_GET()).ok().vibeJSON["url"].to!string; 320 } 321 }