1 /** 2 Base abstractions for dealing with the Discord REST API 3 */ 4 5 module dcord.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 dcord.api, 20 dcord.info, 21 dcord.types, 22 dcord.api.routes, 23 dcord.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://discord.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 = "Discord bot with Dscord (" ~ vibeVersionString ~ "), using Vibe.d " ~ vibeVersionString; 52 } 53 54 /** 55 Makes a HTTP request to the API (with empty body), returning an APIResponse 56 57 Params: 58 route = route to make the request for 59 */ 60 APIResponse requestJSON(CompiledRoute route) { 61 return requestJSON(route, null, ""); 62 } 63 64 /** 65 Makes a HTTP request to the API (with JSON body), returning an APIResponse 66 67 Params: 68 route = route to make the request for 69 params = HTTP parameter hash table to pass to the URL router 70 */ 71 APIResponse requestJSON(CompiledRoute route, string[string] params) { 72 return requestJSON(route, params, ""); 73 } 74 75 /** 76 Makes a HTTP request to the API (with JSON body), returning an APIResponse 77 78 Params: 79 route = route to make the request for 80 obj = VibeJSON object for the JSON body 81 */ 82 APIResponse requestJSON(CompiledRoute route, VibeJSON obj) { 83 return requestJSON(route, null, obj.toString); 84 } 85 86 /** 87 Makes a HTTP request to the API (with string body), returning an APIResponse 88 89 Params: 90 route = route to make the request for 91 content = string of body content 92 params = HTTP parameter hash table to pass to the URL router 93 */ 94 APIResponse requestJSON(CompiledRoute route, string[string] params, string content) { 95 // High timeout, we should never hit this 96 Duration timeout = 15.seconds; 97 98 // Check the rate limit for the route (this may sleep) 99 if(!this.ratelimit.check(route.bucket, timeout)) { 100 throw new APIError(-1, "Request expired before rate-limit cooldown."); 101 } 102 103 string paramString = ""; //A string containing URL encoded parameters 104 105 //If there are parameters, URL encode them into a string for appending 106 if(params != null) { 107 if(params.length > 0) { 108 paramString = "?"; 109 } 110 foreach(key; params.keys) { 111 paramString ~= urlEncode(key) ~ "=" ~ urlEncode(params[key]) ~ "&"; 112 } 113 paramString = paramString[0..$-1]; 114 } 115 116 auto res = new APIResponse(requestHTTP(this.baseURL ~ route.compiled ~ paramString, 117 (scope req) { 118 req.method = route.method; 119 req.headers["Authorization"] = "Bot " ~ this.token; 120 if(content != "") req.headers["Content-Type"] = "application/json"; 121 req.headers["User-Agent"] = this.userAgent; 122 req.bodyWriter.write(content); 123 })); 124 this.log.tracef("[%s] [%s] %s: \n\t%s", route.method, res.statusCode, this.baseURL ~ route.compiled, content); 125 126 // If we returned ratelimit headers, update our ratelimit states 127 if (res.header("X-RateLimit-Limit", "") != "") { 128 this.ratelimit.update(route.bucket, 129 res.header("X-RateLimit-Remaining"), 130 res.header("X-RateLimit-Reset"), 131 res.header("Retry-After", "")); 132 } 133 134 // We ideally should never hit 429s, but in the case we do just retry the 135 // request fully. 136 if (res.statusCode == 429) { 137 this.log.error("Request returned 429. This should not happen."); 138 return this.requestJSON(route, params, content); 139 } else if (res.statusCode == 502) { // If a 502 is recieved, back off for a while and then retry. 140 sleep(randomBackoff()); 141 return this.requestJSON(route, params, content); 142 } 143 return res; 144 } 145 146 /** 147 Return the User object for the currently logged in user. 148 */ 149 User usersMeGet() { 150 auto json = this.requestJSON(Routes.USERS_ME_GET()).ok().vibeJSON; 151 return new User(this.client, json); 152 } 153 154 /** 155 Return a User object for a Snowflake ID. 156 */ 157 User usersGet(Snowflake id) { 158 auto json = this.requestJSON(Routes.USERS_GET(id)).vibeJSON; 159 return new User(this.client, json); 160 } 161 162 /** 163 Modifies the current users settings. Returns a User object. 164 */ 165 User usersMePatch(string username, string avatar) { 166 VibeJSON data = VibeJSON(["username": VibeJSON(username), "avatar": VibeJSON(avatar)]); 167 auto json = this.requestJSON(Routes.USERS_ME_PATCH(), data).vibeJSON; 168 return new User(this.client, json); 169 } 170 171 /** 172 Returns a list of Guild objects for the current user. 173 */ 174 Guild[] usersMeGuildsList() { 175 auto json = this.requestJSON(Routes.USERS_ME_GUILDS_LIST()).ok().vibeJSON; 176 return deserializeFromJSONArray(json, v => new Guild(this.client, v)); 177 } 178 179 /** 180 Leaves a guild. 181 */ 182 void usersMeGuildsLeave(Snowflake id) { 183 this.requestJSON(Routes.USERS_ME_GUILDS_LEAVE(id)).ok(); 184 } 185 186 /** 187 Returns a list of Channel objects for the current user. 188 */ 189 Channel[] usersMeDMSList() { 190 auto json = this.requestJSON(Routes.USERS_ME_DMS_LIST()).ok().vibeJSON; 191 return deserializeFromJSONArray(json, v => new Channel(this.client, v)); 192 } 193 194 /** 195 Creates a new DM for a recipient (user) ID. Returns a Channel object. 196 */ 197 Channel usersMeDMSCreate(Snowflake recipientID) { 198 VibeJSON payload = VibeJSON(["recipient_id": VibeJSON(recipientID)]); 199 auto json = this.requestJSON(Routes.USERS_ME_DMS_CREATE(), payload).ok().vibeJSON; 200 return new Channel(this.client, json); 201 } 202 203 /** 204 Returns a Guild for a Snowflake ID. 205 */ 206 Guild guildsGet(Snowflake id) { 207 auto json = this.requestJSON(Routes.GUILDS_GET(id)).ok().vibeJSON; 208 return new Guild(this.client, json); 209 } 210 211 /** 212 Modifies a guild. 213 */ 214 Guild guildsModify(Snowflake id, VibeJSON obj) { 215 auto json = this.requestJSON(Routes.GUILDS_MODIFY(id), obj).vibeJSON; 216 return new Guild(this.client, json); 217 } 218 219 /** 220 Deletes a guild. 221 */ 222 void guildsDelete(Snowflake id) { 223 this.requestJSON(Routes.GUILDS_DELETE(id)).ok(); 224 } 225 226 /** 227 Returns a list of channels for a Guild. 228 */ 229 Channel[] guildsChannelsList(Snowflake id) { 230 auto json = this.requestJSON(Routes.GUILDS_CHANNELS_LIST(id)).ok().vibeJSON; 231 return deserializeFromJSONArray(json, v => new Channel(this.client, v)); 232 } 233 234 /** 235 Removes (kicks) a user from a Guild. 236 */ 237 void guildsMembersKick(Snowflake id, Snowflake user) { 238 this.requestJSON(Routes.GUILDS_MEMBERS_KICK(id, user)).ok(); 239 } 240 241 /** 242 Bans a user from a Guild. 243 */ 244 void guildMembersBan(Snowflake id, Snowflake user) { 245 this.requestJSON(Routes.GUILDS_BANS_CREATE(id, user)).ok(); 246 } 247 248 /** 249 Unbans a user from a Guild. 250 */ 251 void guildMembersRemoveBan(Snowflake id, Snowflake user) { 252 this.requestJSON(Routes.GUILDS_BANS_DELETE(id, user)).ok(); 253 } 254 255 /** 256 Sends a message to a channel. 257 */ 258 Message channelsMessagesCreate(Snowflake chan, inout(string) content, inout(string) nonce, inout(bool) tts, inout(MessageEmbed) embed) { 259 VibeJSON payload = VibeJSON([ 260 "content": VibeJSON(content), 261 "nonce": VibeJSON(nonce), 262 "tts": VibeJSON(tts), 263 ]); 264 265 if(embed) payload["embed"] = embed.serializeToJSON(); 266 267 // Send payload and return message object 268 auto json = this.requestJSON(Routes.CHANNELS_MESSAGES_CREATE(chan), payload).ok().vibeJSON; 269 return new Message(this.client, json); 270 } 271 272 /** 273 Edits a message's contents. 274 */ 275 Message channelsMessagesModify(Snowflake chan, Snowflake msg, inout(string) content, inout(MessageEmbed) embed) { 276 VibeJSON payload = VibeJSON(["content": VibeJSON(content)]); 277 278 if(embed) payload["embed"] = embed.serializeToJSON(); 279 280 auto json = this.requestJSON(Routes.CHANNELS_MESSAGES_MODIFY(chan, msg), payload).ok().vibeJSON; 281 return new Message(this.client, json); 282 } 283 284 /** 285 Deletes a message. 286 */ 287 void channelsMessagesDelete(Snowflake chan, Snowflake msg) { 288 this.requestJSON(Routes.CHANNELS_MESSAGES_DELETE(chan, msg)).ok(); 289 } 290 291 /** 292 Returns an array of message IDs for a channel up to limit (max 100), 293 filter with respect to supplied messageID. 294 */ 295 Message[] channelsMessagesList(Snowflake chan, uint limit = 50, MessageFilter filter = MessageFilter.BEFORE, Snowflake msg = 0){ 296 enum string errorTooMany = "Only 100 messages can be returned with one call - please make mutliple calls if you need more than this."; 297 assert(limit <= 100, errorTooMany); 298 299 if(limit > 100) throw new Exception(errorTooMany); 300 301 string[string] params = ["limit": limit.toString]; 302 303 if(msg) params[filter] = msg.toString; 304 305 auto json = this.requestJSON(Routes.CHANNELS_MESSAGES_LIST(chan), params).ok().vibeJSON; 306 return deserializeFromJSONArray(json, v => new Message(this.client, v)); 307 } 308 309 /** 310 Deletes messages in bulk. 311 */ 312 void channelsMessagesDeleteBulk(Snowflake chan, Snowflake[] msgIDs) { 313 VibeJSON payload = VibeJSON(["messages": VibeJSON(array(map!(m => VibeJSON(m))(msgIDs)))]); 314 this.requestJSON(Routes.CHANNELS_MESSAGES_DELETE_BULK(chan), payload).ok(); 315 } 316 317 /** 318 Returns a valid Gateway Websocket URL 319 */ 320 string gatewayGet() { 321 return this.requestJSON(Routes.GATEWAY_GET()).ok().vibeJSON["url"].to!string; 322 } 323 /// Create a webhook. 324 void channelsCreateWebhook(Snowflake chan, string name, string avatar) { 325 VibeJSON payload = VibeJSON.emptyObject(); 326 payload["name"] = name; 327 payload["avatar"] = avatar; 328 this.requestJSON(Routes.WEBHOOKS_CREATE(chan), payload).ok(); 329 } 330 331 /// Delete a webhook. 332 void deleteWebhook(Snowflake id) { 333 this.requestJSON(Routes.WEBHOOKS_DELETE(id)).ok(); 334 } 335 336 void sendWebhookMessage(Snowflake id, string token, inout(string) content, inout(string) nonce, inout(bool) tts, inout(MessageEmbed) embed) { 337 VibeJSON payload = VibeJSON([ 338 "nonce": VibeJSON(nonce), 339 "tts": VibeJSON(tts), 340 ]); 341 342 if(content !is null) { 343 payload["content"] = VibeJSON(content); 344 } else { 345 payload["content"] = VibeJSON(null); 346 } 347 348 if(embed) { 349 payload["embeds"] = VibeJSON.emptyArray(); 350 payload["embeds"] ~= embed.serializeToJSON(); 351 } 352 353 // Send payload and return message object 354 this.requestJSON(Routes.WEBHOOKS_EXECUTE(id, token), payload).ok(); 355 } 356 357 void sendWebhookMessage(Snowflake id, string token, inout(string) content, inout(string) nonce, inout(bool) tts, inout(MessageEmbed[]) embeds) { 358 VibeJSON payload = VibeJSON([ 359 "nonce": VibeJSON(nonce), 360 "tts": VibeJSON(tts), 361 ]); 362 363 if(content !is null) { 364 payload["content"] = VibeJSON(content); 365 } else { 366 payload["content"] = VibeJSON(null); 367 } 368 369 if(embeds) { 370 payload["embeds"] = VibeJSON.emptyArray(); 371 foreach(embed; embeds) payload["embeds"] ~= embed.serializeToJSON(); 372 } 373 374 // Send payload and return message object 375 this.requestJSON(Routes.WEBHOOKS_EXECUTE(id, token), payload).ok(); 376 } 377 378 VibeJSON getWebhook(Snowflake id) { 379 return this.requestJSON(Routes.WEBHOOKS_GET(id)).ok().vibeJSON; 380 } 381 382 void reactToMessage(Snowflake chan, Snowflake msg, string emoji) { 383 this.requestJSON(Routes.REACTIONS_CREATE(chan, msg, emoji)).ok(); 384 } 385 }