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