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 = 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 = string of body content 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 if(content != "") 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 } else if (res.statusCode == 502) { // If a 502 is recieved, back off for a while and then retry. 142 sleep(randomBackoff()); 143 return this.requestJSON(route, params, content); 144 } 145 return res; 146 } 147 148 /** 149 Return the User object for the currently logged in user. 150 */ 151 User usersMeGet() { 152 auto json = this.requestJSON(Routes.USERS_ME_GET()).ok().vibeJSON; 153 return new User(this.client, json); 154 } 155 156 /** 157 Return a User object for a Snowflake ID. 158 */ 159 User usersGet(Snowflake id) { 160 auto json = this.requestJSON(Routes.USERS_GET(id)).vibeJSON; 161 return new User(this.client, json); 162 } 163 164 /** 165 Modifies the current users settings. Returns a User object. 166 */ 167 User usersMePatch(string username, string avatar) { 168 VibeJSON data = VibeJSON(["username": VibeJSON(username), "avatar": VibeJSON(avatar)]); 169 auto json = this.requestJSON(Routes.USERS_ME_PATCH(), data).vibeJSON; 170 return new User(this.client, json); 171 } 172 173 /** 174 Returns a list of Guild objects for the current user. 175 */ 176 Guild[] usersMeGuildsList() { 177 auto json = this.requestJSON(Routes.USERS_ME_GUILDS_LIST()).ok().vibeJSON; 178 return deserializeFromJSONArray(json, v => new Guild(this.client, v)); 179 } 180 181 /** 182 Leaves a guild. 183 */ 184 void usersMeGuildsLeave(Snowflake id) { 185 this.requestJSON(Routes.USERS_ME_GUILDS_LEAVE(id)).ok(); 186 } 187 188 /** 189 Returns a list of Channel objects for the current user. 190 */ 191 Channel[] usersMeDMSList() { 192 auto json = this.requestJSON(Routes.USERS_ME_DMS_LIST()).ok().vibeJSON; 193 return deserializeFromJSONArray(json, v => new Channel(this.client, v)); 194 } 195 196 /** 197 Creates a new DM for a recipient (user) ID. Returns a Channel object. 198 */ 199 Channel usersMeDMSCreate(Snowflake recipientID) { 200 VibeJSON payload = VibeJSON(["recipient_id": VibeJSON(recipientID)]); 201 auto json = this.requestJSON(Routes.USERS_ME_DMS_CREATE(), payload).ok().vibeJSON; 202 return new Channel(this.client, json); 203 } 204 205 /** 206 Returns a Guild for a Snowflake ID. 207 */ 208 Guild guildsGet(Snowflake id) { 209 auto json = this.requestJSON(Routes.GUILDS_GET(id)).ok().vibeJSON; 210 return new Guild(this.client, json); 211 } 212 213 /** 214 Modifies a guild. 215 */ 216 Guild guildsModify(Snowflake id, VibeJSON obj) { 217 auto json = this.requestJSON(Routes.GUILDS_MODIFY(id), obj).vibeJSON; 218 return new Guild(this.client, json); 219 } 220 221 /** 222 Deletes a guild. 223 */ 224 void guildsDelete(Snowflake id) { 225 this.requestJSON(Routes.GUILDS_DELETE(id)).ok(); 226 } 227 228 /** 229 Returns a list of channels for a Guild. 230 */ 231 Channel[] guildsChannelsList(Snowflake id) { 232 auto json = this.requestJSON(Routes.GUILDS_CHANNELS_LIST(id)).ok().vibeJSON; 233 return deserializeFromJSONArray(json, v => new Channel(this.client, v)); 234 } 235 236 /** 237 Removes (kicks) a user from a Guild. 238 */ 239 void guildsMembersKick(Snowflake id, Snowflake user) { 240 this.requestJSON(Routes.GUILDS_MEMBERS_KICK(id, user)).ok(); 241 } 242 243 /** 244 Bans a user from a Guild. 245 */ 246 void guildMembersBan(Snowflake id, Snowflake user) { 247 this.requestJSON(Routes.GUILDS_BANS_CREATE(id, user)).ok(); 248 } 249 250 /** 251 Unbans a user from a Guild. 252 */ 253 void guildMembersRemoveBan(Snowflake id, Snowflake user) { 254 this.requestJSON(Routes.GUILDS_BANS_DELETE(id, user)).ok(); 255 } 256 257 /** 258 Sends a message to a channel. 259 */ 260 Message channelsMessagesCreate(Snowflake chan, inout(string) content, inout(string) nonce, inout(bool) tts, inout(MessageEmbed) embed) { 261 VibeJSON payload = VibeJSON([ 262 "content": VibeJSON(content), 263 "nonce": VibeJSON(nonce), 264 "tts": VibeJSON(tts), 265 ]); 266 267 if(embed) payload["embed"] = embed.serializeToJSON(); 268 269 // Send payload and return message object 270 auto json = this.requestJSON(Routes.CHANNELS_MESSAGES_CREATE(chan), payload).ok().vibeJSON; 271 return new Message(this.client, json); 272 } 273 274 /** 275 Edits a message's contents. 276 */ 277 Message channelsMessagesModify(Snowflake chan, Snowflake msg, inout(string) content, inout(MessageEmbed) embed) { 278 VibeJSON payload = VibeJSON(["content": VibeJSON(content)]); 279 280 if(embed) payload["embed"] = embed.serializeToJSON(); 281 282 auto json = this.requestJSON(Routes.CHANNELS_MESSAGES_MODIFY(chan, msg), payload).ok().vibeJSON; 283 return new Message(this.client, json); 284 } 285 286 /** 287 Deletes a message. 288 */ 289 void channelsMessagesDelete(Snowflake chan, Snowflake msg) { 290 this.requestJSON(Routes.CHANNELS_MESSAGES_DELETE(chan, msg)).ok(); 291 } 292 293 /** 294 Returns an array of message IDs for a channel up to limit (max 100), 295 filter with respect to supplied messageID. 296 */ 297 Message[] channelsMessagesList(Snowflake chan, uint limit = 50, MessageFilter filter = MessageFilter.BEFORE, Snowflake msg = 0){ 298 enum string errorTooMany = "Only 100 messages can be returned with one call - please make mutliple calls if you need more than this."; 299 assert(limit <= 100, errorTooMany); 300 301 if(limit > 100) throw new Exception(errorTooMany); 302 303 string[string] params = ["limit": limit.toString]; 304 305 if(msg) params[filter] = msg.toString; 306 307 auto json = this.requestJSON(Routes.CHANNELS_MESSAGES_LIST(chan), params).ok().vibeJSON; 308 return deserializeFromJSONArray(json, v => new Message(this.client, v)); 309 } 310 311 /** 312 Deletes messages in bulk. 313 */ 314 void channelsMessagesDeleteBulk(Snowflake chan, Snowflake[] msgIDs) { 315 VibeJSON payload = VibeJSON(["messages": VibeJSON(array(map!((m) => VibeJSON(m))(msgIDs)))]); 316 this.requestJSON(Routes.CHANNELS_MESSAGES_DELETE_BULK(chan), payload).ok(); 317 } 318 319 /** 320 Returns a valid Gateway Websocket URL 321 */ 322 string gatewayGet() { 323 return this.requestJSON(Routes.GATEWAY_GET()).ok().vibeJSON["url"].to!string; 324 } 325 }