1 module dcord.types.message;
2 
3 import std.stdio,
4        std.variant,
5        std.conv,
6        std.format,
7        std.regex,
8        std.array,
9        std.algorithm.iteration,
10        std.uri,
11        std.algorithm.setops : nWayUnion;
12 
13 import dcord.types,
14        dcord.client;
15 
16 /*:*
17   An interface implementting something that can be sent as a message.
18 */
19 interface Sendable {
20   /// Returns the embed (or null if none) for this sendable
21   immutable(MessageEmbed) getEmbed();
22 
23   /// Returns the contents for this sendable, no more than 2000 characters long
24   immutable(string) getContents();
25 
26   /// Returns the nonce for this sendable
27   immutable(string) getNonce();
28 
29   /// Returns the tts setting for this sendable
30   immutable(bool) getTTS();
31 
32   /// Returns the attachments for this sendable (if any)
33   //immutable(Attachment[]) getAttachments();
34 }
35 
36 /// BaseSendable implements the Sendable interface, and is the base implementation for all sendables
37 class BaseSendable: Sendable {
38   /// Get embed from sendable
39   immutable(MessageEmbed) getEmbed() {
40     return null;
41   }
42 
43   /// Get contents from sendable
44  immutable(string) getContents() {
45     return "";
46   }
47 
48   /// Get nonce of sendable
49   immutable(string) getNonce() {
50     return "";
51   }
52   
53   /// Find whether the sendable has TTS (text-to-speech) enabled
54   immutable(bool) getTTS() {
55     return false;
56   }
57 }
58 
59 
60 /// Enumeration of all types a message can be.
61 enum MessageType {
62   DEFAULT = 0,
63   RECIPIENT_ADD = 1,
64   RECIPIENT_REMOVE = 2,
65   CALL = 3,
66   CHANNEL_NAME_CHANGE = 4,
67   CHANNEL_ICON_CHANGE = 5,
68   PINS_ADD = 6,
69   GUILD_MEMBER_JOIN = 7,
70 }
71 
72 // TODO
73 class MessageReaction: IModel {
74   mixin Model;
75 }
76 
77 class MessageEmbedFooter: IModel {
78   mixin Model;
79 
80   string text;
81 
82   @JSONSource("icon_url")
83   string iconURL;
84 
85   @JSONSource("proxy_icon_url")
86   string proxyIconURL;
87 }
88 
89 class MessageEmbedImage: IModel {
90   mixin Model;
91 
92   string url;
93 
94   @JSONSource("proxy_url")
95   string proxyURL;
96 
97   uint width;
98   uint height;
99 }
100 
101 class MessageEmbedThumbnail: IModel {
102   mixin Model;
103 
104   string url;
105 
106   @JSONSource("proxy_url")
107   string proxyURL;
108 
109   uint width;
110   uint height;
111 }
112 
113 class MessageEmbedVideo: IModel {
114   mixin Model;
115 
116   string url;
117   uint height;
118   uint width;
119 }
120 
121 class MessageEmbedAuthor: IModel {
122   mixin Model;
123 
124   string name;
125   string url;
126 
127   @JSONSource("icon_url")
128   string iconURL;
129 
130   @JSONSource("proxy_icon_url")
131   string proxyIconURL;
132 }
133 
134 class MessageEmbedField: IModel {
135   mixin Model;
136 
137   string name;
138   string value;
139   bool inline;
140 }
141 
142 class MessageEmbed : IModel, Sendable {
143   mixin Model;
144 
145   string title;
146   string type;
147   string description;
148   string url;
149   string timestamp;
150   uint color;
151 
152   MessageEmbedFooter footer;
153   MessageEmbedImage image;
154   MessageEmbedThumbnail thumbnail;
155   MessageEmbedVideo video;
156   MessageEmbedAuthor author;
157   MessageEmbedField[] fields;
158 
159   immutable(MessageEmbed) getEmbed() { return cast(immutable(MessageEmbed))this; }
160   immutable(string) getContents() { return ""; }
161   immutable(string) getNonce() { return ""; }
162   immutable(bool) getTTS() { return false; }
163 }
164 
165 class MessageAttachment: IModel {
166   mixin Model;
167 
168   Snowflake id;
169   string filename;
170   uint size;
171   string url;
172   string proxyUrl;
173   uint height;
174   uint width;
175 }
176 
177 class Message: IModel {
178   mixin Model;
179 
180   Snowflake id;
181   Snowflake channelID;
182   User author;
183   string content;
184   bool tts;
185   bool mentionEveryone;
186   bool pinned;
187 
188   // Nonce is very unpredictable and user-provided, so we don't unpack it into
189   //  a concrete type.
190   VibeJSON nonce;
191 
192   @JSONTimestamp
193   SysTime timestamp;
194 
195   @JSONTimestamp
196   SysTime editedTimestamp;
197 
198   GuildMember member;
199 
200   // TODO: GuildMemberMap here
201   @JSONListToMap("id")
202   UserMap mentions;
203 
204   @JSONSource("mention_roles")
205   Snowflake[] roleMentions;
206 
207   // Embeds
208   MessageEmbed[] embeds;
209 
210   // Attachments
211   MessageAttachment[] attachments;
212 
213   /// Get the guild that the message is from
214   @property Guild guild() {
215     return this.channel.guild;
216   }
217 
218   /// Get the channel that the message is from
219   @property Channel channel() {
220     return this.client.state.channels.get(this.channelID);
221   }
222 
223   override string toString() { // stfu
224     return format("<Message %s>", this.id);
225   }
226 
227   /*
228     Returns a version of the message contents, with mentions completely removed
229   */
230   string withoutMentions() {
231     return this.replaceMentions((m, u) => "", (m, r) => "");
232   }
233 
234   /*
235     Returns a version of the message contents, replacing all mentions with user/nick names
236   */
237   string withProperMentions(bool nicks=true) {
238     return this.replaceMentions((msg, user) {
239       GuildMember m;
240       if (nicks) {
241         m = msg.guild.members.get(user.id);
242       }
243       return "@" ~ ((m && m.nick != "") ? m.nick : user.username);
244     }, (msg, role) { return "@" ~ msg.guild.roles.get(role).name; });
245   }
246 
247   /**
248     Returns the message contents, replacing all mentions with the result from the
249     specified delegate.
250   */
251   string replaceMentions(string delegate(Message, User) fu, string delegate(Message, Snowflake) fr) {
252     if(!this.mentions.length && !this.roleMentions.length) 
253       return this.content;
254 
255     string result = this.content;
256     foreach (ref User user; this.mentions.values) {
257       result = replaceAll(result, regex(format("<@!?(%s)>", user.id)), fu(this, user));
258     }
259 
260     foreach (ref Snowflake role; this.roleMentions) {
261       result = replaceAll(result, regex(format("<@!?(%s)>", role)), fr(this, role));
262     }
263 
264     return result;
265   }
266 
267   /**
268     Sends a new message to the same channel as this message.
269 
270     Params:
271       content = the message contents
272       nonce = the message nonce
273       tts = whether this is a TTS message
274   */
275   Message reply(inout(string) content, string nonce=null, bool tts=false) {
276     return this.client.api.channelsMessagesCreate(this.channelID, content, nonce, tts, null);
277   }
278 
279   void react(string emoji) {
280     this.client.api.reactToMessage(this.channelID, this.id, emoji.encode);
281   }
282 
283   /**
284     Sends a Sendable to the same channel as this message.
285   */
286   Message reply(Sendable obj) {
287     return this.client.api.channelsMessagesCreate(
288       this.channelID,
289       obj.getContents(),
290       obj.getNonce(),
291       obj.getTTS(),
292       obj.getEmbed(),
293     );
294   }
295 
296   /**
297     Sends a new formatted message to the same channel as this message.
298   */
299   Message replyf(T...)(inout(string) content, T args) {
300     return this.client.api.channelsMessagesCreate(this.channelID, format(content, args), null, false, null);
301   }
302 
303   /**
304     Edits this message contents.
305   */
306   Message edit(inout(string) content, inout(MessageEmbed) embed=null) {
307     return this.client.api.channelsMessagesModify(this.channelID, this.id, content, embed);
308   }
309 
310   /**
311     Edits this message contents with a Sendable.
312   */
313   Message edit(Sendable obj) {
314     return this.edit(
315       obj.getContents(),
316       obj.getEmbed(),
317     );
318   }
319 
320   /**
321     Deletes this message.
322   */
323   void del() {
324     if (!this.canDelete()) throw new PermissionsError(Permissions.MANAGE_MESSAGES);
325     return this.client.api.channelsMessagesDelete(this.channelID, this.id);
326   }
327 
328   /**
329     True if this message mentions the current user in any way (everyone, direct mention, role mention)
330   */
331   @property bool mentioned() {
332     return (
333       this.mentionEveryone ||
334       this.mentions.has(this.client.state.me.id) ||
335       nWayUnion(
336         [this.roleMentions, this.guild.getMember(this.client.state.me).roles]
337       ).array.length != 0
338     );
339   }
340 
341   /**
342     Returns an array of emoji IDs for all custom emoji used in this message.
343   */
344   @property Snowflake[] customEmojiByID() {
345     return matchAll(this.content, regex("<:\\w+:(\\d+)>")).map!(m => m.back.to!Snowflake).array;
346   }
347 
348   /// Whether the bot can delete this message.
349   bool canDelete() {
350     return (this.author.id == this.client.state.me.id ||
351       this.channel.can(this.client.state.me, Permissions.MANAGE_MESSAGES));
352   }
353 
354   /// Whether the bot can edit this message. 
355   bool canEdit() {
356     return (this.author.id == this.client.state.me.id);
357   }
358 }