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 }