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 }