1 module dscord.api.client; 2 3 import std.conv, 4 std.array, 5 std.format, 6 std.variant, 7 std.algorithm.iteration, 8 core.time; 9 import vibe.core.core, 10 vibe.http.client, 11 vibe.stream.operations, 12 vibe.textfilter.urlencode; 13 import discord.api, 14 discord.info, 15 discord.structures, 16 discord.api.routes, 17 discord.api.ratelimit; 18 19 enum MessageFilter : string { 20 AROUND = "around", 21 BEFORE = "before", 22 AFTER = "after" 23 } 24 25 class APIClient { 26 string baseURL = "https://discordapp.com/api"; 27 string userAgent; 28 string token; 29 RateLimiter ratelimit; 30 Client client; 31 Logger log; 32 33 this(Client client) { 34 this.client = client; 35 this.log = client.log; 36 this.token = client.token; 37 this.ratelimit = new RateLimiter; 38 this.userAgent = format("DiscordBot (%s %s) %s", GITHUB_REPO, VERSION, "vibe.d/" ~ vibeVersionString); 39 } 40 41 APIResponse request(CompiledRoute route) { 42 return request(route, null, ""); 43 } 44 45 APIResponse request(CompiledRoute route, string[string] params) { 46 return request(route, params, ""); 47 } 48 49 APIResponse request(CompiledRoute route, VibeJSON obj) { 50 return request(route, null, obj.toString); 51 } 52 53 APIResponse request(CompiledRoute route, string[string] params, string content) { 54 Duration timeout = 15.seconds; 55 56 if (!this.ratelimit.check(route.bucket, timeout)) { 57 throw new APIError(-1, "Request expired before rate-limit cooldown."); 58 } 59 60 string paramString = ""; 61 if (params != null) { 62 if (params.length > 0) { 63 paramString = "?"; 64 } 65 foreach(key; params.keys) { 66 paramString ~= urlEncode(key) ~ "=" ~ urlEncode(params[key]) ~ "&"; 67 } 68 paramString = paramString[0..$-1]; 69 } 70 71 auto res = new APIResponse(requestHTTP(this.baseURL ~ route.compiled ~ paramString, 72 (scope req) { 73 req.method = route.method; 74 req.headers["Authorization"] = "Bot " ~ this.token; 75 req.headers["Content-Type"] = "application/json"; 76 req.headers["User-Agent"] = this.userAgent; 77 req.bodyWriter.write(content); 78 })); 79 this.log.tracef("[%s] [%s] %s: \n\t%s", route.method, res.statusCode, this.baseURL ~ route.compiled, content); 80 81 if (res.header("X-RateLimit-Limit", "") != "") { 82 this.ratelimit.update(route.bucket, 83 res.header("X-RateLimit-Remaining"), 84 res.header("X-RateLimit-Reset"), 85 res.header("Retry-After", "")); 86 } 87 88 if (res.statusCode == 429) { 89 this.log.error("Request returned 429."); 90 return this.request(route, params, content); 91 } else if (res.statusCode == 502) { 92 sleep(randomBackoff()); 93 return this.request(route, params, content); 94 } 95 return res; 96 } 97 98 /** 99 The client as a Discord user. 100 */ 101 User usersMeGet() { 102 auto json = this.request(Routes.USERS_ME_GET()).ok().vibeJSON; 103 return new User(this.client, json); 104 } 105 106 /** 107 Obtains a Discord user from ID. (Returns User object) 108 */ 109 User usersGet(Snowflake id) { 110 auto json = this.request(Routes.USERS_GET(id)).vibeJSON; 111 return new User(this.client, json); 112 } 113 114 /** 115 Updates the client user settings. 116 */ 117 User usersMePatch(string username, string avatar) { 118 VibeJSON data = VibeJSON(["username": VibeJSON(username), "avatar": VibeJSON(avatar)]); 119 auto json = this.request(Routes.USERS_ME_PATCH(), data).vibeJSON; 120 return new User(this.client, json); 121 } 122 123 /** 124 All the channels the client is currrently handling. 125 */ 126 Channel[] usersMeDMSList() { 127 auto json = this.request(Routes.USERS_ME_DMS_LIST()).ok().vibeJSON; 128 return deserializeFromJSONArray(json, v => new Channel(this.client, v)); 129 } 130 131 /** 132 Creates a new DM for a recipient (user) ID. Returns a Channel object. 133 */ 134 Channel usersMeDMSCreate(Snowflake recipientID) { 135 VibeJSON payload = VibeJSON(["recipient_id": VibeJSON(recipientID)]); 136 auto json = this.request(Routes.USERS_ME_DMS_CREATE(), payload).ok().vibeJSON; 137 return new Channel(this.client, json); 138 } 139 140 /** 141 All the guilds the client is currently handling. 142 */ 143 Guild[] usersMeGuildsList() { 144 auto json = this.request(Routes.USERS_ME_GUILDS_LIST()).ok().vibeJSON; 145 return deserializeFromJSONArray(json, v => new Guild(this.client, v)); 146 } 147 148 /** 149 Leaves a guild. 150 */ 151 void usersMeGuildsLeave(Snowflake id) { 152 this.request(Routes.USERS_ME_GUILDS_LEAVE(id)).ok(); 153 } 154 155 /** 156 Obtains a Guild from a guild ID. 157 */ 158 Guild guildsGet(Snowflake id) { 159 auto json = this.request(Routes.GUILDS_GET(id)).ok().vibeJSON; 160 return new Guild(this.client, json); 161 } 162 163 /** 164 Updates a guild with new data. 165 */ 166 Guild guildsModify(Snowflake id, VibeJSON obj) { 167 auto json = this.request(Routes.GUILDS_MODIFY(id), obj).vibeJSON; 168 return new Guild(this.client, json); 169 } 170 171 /** 172 Deletes a guild. 173 */ 174 void guildsDelete(Snowflake id) { 175 this.request(Routes.GUILDS_DELETE(id)).ok(); 176 } 177 178 /** 179 Kicks a member from a guild. 180 */ 181 void guildsMembersKick(Snowflake id, Snowflake user) { 182 this.request(Routes.GUILDS_MEMBERS_KICK(id, user)).ok(); 183 } 184 185 186 /** 187 Collection of channels that are in a guild. 188 */ 189 Channel[] guildsChannelsList(Snowflake id) { 190 auto json = this.request(Routes.GUILDS_CHANNELS_LIST(id)).ok().vibeJSON; 191 return deserializeFromJSONArray(json, v => new Channel(this.client, v)); 192 } 193 194 /** 195 Sends a message to a channel. 196 */ 197 Message channelsMessagesCreate(Snowflake channel, inout(string) content, inout(string) nonce, inout(bool) tts, inout(MessageEmbed) embed) { 198 VibeJSON payload = VibeJSON([ 199 "content": VibeJSON(content), 200 "nonce": VibeJSON(nonce), 201 "tts": VibeJSON(tts), 202 ]); 203 204 if (embed) { 205 payload["embed"] = embed.serializeToJSON(); 206 } 207 auto json = this.request(Routes.CHANNELS_MESSAGES_CREATE(channel), payload).ok().vibeJSON; 208 return new Message(this.client, json); 209 } 210 211 /** 212 Edits a message's content. 213 */ 214 Message channelsMessagesModify(Snowflake channel, Snowflake message, inout(string) content, inout(MessageEmbed) embed) { 215 VibeJSON payload = VibeJSON(["content": VibeJSON(content)]); 216 217 if (embed) { 218 payload["embed"] = embed.serializeToJSON(); 219 } 220 auto json = this.request(Routes.CHANNELS_MESSAGES_MODIFY(channel, message), payload).ok().vibeJSON; 221 return new Message(this.client, json); 222 } 223 224 /** 225 Deletes a message. 226 */ 227 void channelsMessagesDelete(Snowflake channel, Snowflake message) { 228 this.request(Routes.CHANNELS_MESSAGES_DELETE(channel, message)).ok(); 229 } 230 231 /** 232 Gets the messages sent in a channel. (Limit: 100) (Mapped by their IDs) 233 */ 234 Message[] channelsMessagesList(Snowflake channel, uint limit = 50, MessageFilter filter = MessageFilter.BEFORE, Snowflake msg = 0){ 235 enum string errorTooMany = "The maximum number of messages that can be returned at one time is 100."; 236 assert(limit <= 100, errorTooMany); 237 238 if(limit > 100){ 239 throw new Exception(errorTooMany); 240 } 241 242 string[string] params = ["limit":limit.toString]; 243 244 if (msg) { 245 params[filter] = msg.toString; 246 } 247 auto json = this.request(Routes.CHANNELS_MESSAGES_LIST(channel), params).ok().vibeJSON; 248 return deserializeFromJSONArray(json, v => new Message(this.client, v)); 249 } 250 251 /** 252 Bulk deletes given messages. 253 */ 254 void channelsMessagesDeleteBulk(Snowflake channel, Snowflake[] messages) { 255 VibeJSON payload = VibeJSON(["messages": VibeJSON(array(map!((m) => VibeJSON(m))(messages)))]); 256 this.request(Routes.CHANNELS_MESSAGES_DELETE_BULK(channel), payload).ok(); 257 } 258 259 /** 260 Returns a valid Gateway Websocket URL. 261 */ 262 string getGateway() { 263 return this.request(Routes.GATEWAY_GET()).ok().vibeJSON["url"].to!string; 264 } 265 }