1 /**
2   Base abstractions for dealing with the Discord REST API
3 */
4 
5 module dscord.api.client;
6 
7 import std.array,
8        std.variant,
9        std.conv,
10        std.algorithm.iteration,
11        core.time;
12 
13 import vibe.http.client,
14        vibe.stream.operations;
15 
16 import dscord.types.all,
17        dscord.api.ratelimit,
18        dscord.api.util;
19 
20 
21 /**
22   APIClient is the base abstraction for interacting with the Discord API.
23 */
24 class APIClient {
25   string       baseURL = "https://discordapp.com/api";
26   string       token;
27   RateLimiter  ratelimit;
28   Client       client;
29   Logger       log;
30 
31   this(Client client) {
32     this.client = client;
33     this.log = client.log;
34     this.token = client.token;
35     this.ratelimit = new RateLimiter;
36   }
37 
38   /**
39     Makes a HTTP request to the API (with empty body), returning an APIResponse
40 
41     Params:
42       method = HTTPMethod to use when requesting
43       url = URL to make the request on
44   */
45   APIResponse requestJSON(HTTPMethod method, U url) {
46     return requestJSON(method, url, "");
47   }
48 
49   /**
50     Makes a HTTP request to the API (with JSON body), returning an APIResponse
51 
52     Params:
53       method = HTTPMethod to use when requesting
54       url = URL to make the request on
55       obj = VibeJSON object for the body
56   */
57   APIResponse requestJSON(HTTPMethod method, U url, VibeJSON obj) {
58     return requestJSON(method, url, obj.toString);
59   }
60 
61   /**
62     Makes a HTTP request to the API (with string body), returning an APIResponse
63 
64     Params:
65       method = HTTPMethod to use when requesting
66       url = URL to make the request on
67       data = string contents of the body
68       timeout = HTTP timeout (default is 15 seconds)
69   */
70   APIResponse requestJSON(HTTPMethod method, U url, string data,
71       Duration timeout=15.seconds) {
72 
73     // Grab the rate limit lock
74     if (!this.ratelimit.wait(url.getBucket(), timeout)) {
75       throw new APIError(-1, "Request expired before rate-limit");
76     }
77 
78     this.log.tracef("API Request: [%s] %s: %s", method, this.baseURL ~ url.value, data);
79     auto res = new APIResponse(requestHTTP(this.baseURL ~ url.value,
80       (scope req) {
81         req.method = method;
82         req.headers["Authorization"] = this.token;
83         req.headers["Content-Type"] = "application/json";
84         req.bodyWriter.write(data);
85     }));
86 
87     // If we got a 429, cooldown and recurse
88     if (res.statusCode == 429) {
89       this.ratelimit.cooldown(url.getBucket(),
90           dur!"seconds"(res.header("Retry-After", "1").to!int));
91       return this.requestJSON(method, url, data, timeout);
92     // If we got a 502, just retry immedietly
93     } else if (res.statusCode == 502) {
94       return this.requestJSON(method, url, data, timeout);
95     }
96 
97     return res;
98   }
99 
100   /**
101     Return the User object for the currently logged in user.
102   */
103   User me() {
104     auto json = this.requestJSON(HTTPMethod.GET, U("users")("@me")).ok().fastJSON;
105     return new User(this.client, json);
106   }
107 
108   /**
109     Return a User object for a Snowflake ID.
110   */
111   User user(Snowflake id) {
112     auto json = this.requestJSON(HTTPMethod.GET, U("users")(id)).ok().fastJSON;
113     return new User(this.client, json);
114   }
115 
116   /**
117     Modifies the current users settings. Returns a User object.
118   */
119   User meSettings(string username, string avatar) {
120     auto json = this.requestJSON(HTTPMethod.PATCH, U("users")("@me")).ok().fastJSON;
121     return new User(this.client, json);
122   }
123 
124   /**
125     Returns a list of Guild objects for the current user.
126   */
127   Guild[] meGuilds() {
128     auto json = this.requestJSON(HTTPMethod.GET, U("users")("@me")("guilds")).ok().fastJSON;
129     return loadManyArray!Guild(this.client, json);
130   }
131 
132   /**
133     Leaves a guild.
134   */
135   void meGuildLeave(Snowflake id) {
136     this.requestJSON(HTTPMethod.DELETE, U("users")("@me")("guilds")(id)).ok();
137   }
138 
139   /**
140     Returns a list of Channel objects for the current user.
141   */
142   Channel[] meDMChannels() {
143     auto json = this.requestJSON(HTTPMethod.GET, U("users")("@me")("channels")).ok().fastJSON;
144     return loadManyArray!Channel(this.client, json);
145   }
146 
147   /**
148     Creates a new DM for a recipient (user) ID. Returns a Channel object.
149   */
150   Channel meDMCreate(Snowflake recipientID) {
151     VibeJSON payload = VibeJSON.emptyObject;
152     payload["recipient_id"] = VibeJSON(recipientID);
153     auto json = this.requestJSON(HTTPMethod.POST,
154         U("users")("@me")("channels"), payload).ok().fastJSON;
155     return new Channel(this.client, json);
156   }
157 
158   /**
159     Returns a Guild for a Snowflake ID.
160   */
161   Guild guild(Snowflake id) {
162     auto json = this.requestJSON(HTTPMethod.GET, U("guilds")(id)).ok().fastJSON;
163     return new Guild(this.client, json);
164   }
165 
166   /**
167     Deletes a guild.
168   */
169   void guildDelete(Snowflake id) {
170     this.requestJSON(HTTPMethod.DELETE, U("guilds")(id)).ok();
171   }
172 
173   /**
174     Returns a list of channels for a Guild.
175   */
176   Channel[] guildChannels(Snowflake id) {
177     auto json = this.requestJSON(HTTPMethod.GET, U("guilds")(id)("channels")).ok().fastJSON;
178     return loadManyArray!Channel(this.client, json);
179   }
180 
181   /**
182     Removes (kicks) a user from a Guild.
183   */
184   void guildRemoveMember(Snowflake id, Snowflake user) {
185     this.requestJSON(HTTPMethod.DELETE, U("guilds")(id)("members")(user)).ok();
186   }
187 
188   /*
189   Channel guildChannelCreate(Snowflake id, string name, string type, int bitrate = -1, int userLimit = -1) {
190     VibeJSON payload = VibeJSON.emptyObject;
191     payload["name"] = VibeJSON(id);
192     payload["type"] = VibeJSON(type);
193     if (bitrate > -1) payload["bitrate"] = VibeJSON(bitrate);
194     if (userLimit > -1) payload["user_limit"] = VibeJSON(userLimit);
195   }
196   */
197 
198   /**
199     Sends a message to a channel.
200   */
201   Message sendMessage(Snowflake chan, string content, string nonce, bool tts) {
202     VibeJSON payload = VibeJSON.emptyObject;
203     payload["content"] = VibeJSON(content);
204     payload["nonce"] = VibeJSON(nonce);
205     payload["tts"] = VibeJSON(tts);
206 
207     // Send payload and return message object
208     auto json = this.requestJSON(HTTPMethod.POST,
209         U("channels")(chan)("messages").bucket("send-message"), payload).ok().fastJSON;
210     return new Message(this.client, json);
211   }
212 
213   /**
214     Edits a messages contents.
215   */
216   Message editMessage(Snowflake chan, Snowflake msg, string content) {
217     VibeJSON payload = VibeJSON.emptyObject;
218     payload["content"] = content;
219 
220     auto json = this.requestJSON(HTTPMethod.PATCH,
221         U("channels")(chan)("messages")(msg).bucket("edit-message"), payload).ok().fastJSON;
222     return new Message(this.client, json);
223   }
224 
225   /**
226     Deletes a message.
227   */
228   void deleteMessage(Snowflake chan, Snowflake msg) {
229     this.requestJSON(HTTPMethod.DELETE,
230         U("channels")(chan)("messages")(msg).bucket("del-message")).ok().fastJSON;
231   }
232 
233   /**
234     Deletes messages in bulk.
235   */
236   void bulkDeleteMessages(Snowflake chan, Snowflake[] msgs) {
237     VibeJSON payload = VibeJSON.emptyObject;
238     payload["messages"] = VibeJSON(array(map!((m) => VibeJSON(m))(msgs)));
239 
240     this.requestJSON(HTTPMethod.POST,
241         U("channels")(chan)("messages")("bulk_delete").bucket("bulk-del-messages"),
242         payload).ok();
243   }
244 
245   /**
246     Returns a valid Gateway Websocket URL
247   */
248   string gateway(int gatewayVersion = 5) {
249     return this.requestJSON(HTTPMethod.GET, U("gateway")("v", gatewayVersion.to!string)).ok().vibeJSON["url"].to!string;
250   }
251 }