1 module dscord.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 dscord.types,
13        dscord.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   string     timestamp; // TODO: timestamps lol
180   string     editedTimestamp; // TODO: timestamps lol
181   bool       tts;
182   bool       mentionEveryone;
183   string     nonce;
184   bool       pinned;
185 
186   // TODO: GuildMemberMap here
187   @JSONListToMap("id")
188   UserMap    mentions;
189 
190   @JSONSource("mention_roles")
191   Snowflake[]    roleMentions;
192 
193   // Embeds
194   MessageEmbed[]  embeds;
195 
196   // Attachments
197   MessageAttachment[]  attachments;
198 
199   @property Guild guild() {
200     return this.channel.guild;
201   }
202 
203   @property Channel channel() {
204     return this.client.state.channels.get(this.channelID);
205   }
206 
207   override string toString() {
208     return format("<Message %s>", this.id);
209   }
210 
211   /*
212     Returns a version of the message contents, with mentions completely removed
213   */
214   string withoutMentions() {
215     return this.replaceMentions((m, u) => "", (m, r) => "");
216   }
217 
218   /*
219     Returns a version of the message contents, replacing all mentions with user/nick names
220   */
221   string withProperMentions(bool nicks=true) {
222     return this.replaceMentions((msg, user) {
223       GuildMember m;
224       if (nicks) {
225         m = msg.guild.members.get(user.id);
226       }
227       return "@" ~ ((m && m.nick != "") ? m.nick : user.username);
228     }, (msg, role) { return "@" ~ msg.guild.roles.get(role).name; });
229   }
230 
231   /**
232     Returns the message contents, replacing all mentions with the result from the
233     specified delegate.
234   */
235   string replaceMentions(string delegate(Message, User) fu, string delegate(Message, Snowflake) fr) {
236     if (!this.mentions.length && !this.roleMentions.length) {
237       return this.content;
238     }
239 
240     string result = this.content;
241     foreach (ref User user; this.mentions.values) {
242       result = replaceAll(result, regex(format("<@!?(%s)>", user.id)), fu(this, user));
243     }
244 
245     foreach (ref Snowflake role; this.roleMentions) {
246       result = replaceAll(result, regex(format("<@!?(%s)>", role)), fr(this, role));
247     }
248 
249     return result;
250   }
251 
252   /**
253     Sends a new message to the same channel as this message.
254 
255     Params:
256       content = the message contents
257       nonce = the message nonce
258       tts = whether this is a TTS message
259   */
260   Message reply(inout(string) content, string nonce=null, bool tts=false) {
261     return this.client.api.channelsMessagesCreate(this.channelID, content, nonce, tts, null);
262   }
263 
264   /**
265     Sends a Sendable to the same channel as this message.
266   */
267   Message reply(Sendable obj) {
268     return this.client.api.channelsMessagesCreate(
269       this.channelID,
270       obj.getContents(),
271       obj.getNonce(),
272       obj.getTTS(),
273       obj.getEmbed(),
274     );
275   }
276 
277   /**
278     Sends a new formatted message to the same channel as this message.
279   */
280   Message replyf(T...)(inout(string) content, T args) {
281     return this.client.api.channelsMessagesCreate(this.channelID, format(content, args), null, false, null);
282   }
283 
284   /**
285     Edits this message contents.
286   */
287   Message edit(inout(string) content, inout(MessageEmbed) embed=null) {
288     return this.client.api.channelsMessagesModify(this.channelID, this.id, content, embed);
289   }
290 
291   /**
292     Edits this message contents with a Sendable.
293   */
294   Message edit(Sendable obj) {
295     return this.edit(
296       obj.getContents(),
297       obj.getEmbed(),
298     );
299   }
300 
301   /**
302     Deletes this message.
303   */
304   void del() {
305     if (!this.canDelete()) {
306       throw new PermissionsError(Permissions.MANAGE_MESSAGES);
307     }
308 
309     return this.client.api.channelsMessagesDelete(this.channelID, this.id);
310   }
311 
312   /**
313     True if this message mentions the current user in any way (everyone, direct mention, role mention)
314   */
315   @property bool mentioned() {
316     return (
317       this.mentionEveryone ||
318       this.mentions.has(this.client.state.me.id) ||
319       nWayUnion(
320         [this.roleMentions, this.guild.getMember(this.client.state.me).roles]
321       ).array.length != 0
322     );
323   }
324 
325   /**
326     Returns an array of emoji IDs for all custom emoji used in this message.
327   */
328   @property Snowflake[] customEmojiByID() {
329     return matchAll(this.content, regex("<:\\w+:(\\d+)>")).map!((m) => m.back.to!Snowflake).array;
330   }
331 
332   /// Whether the bot can edit this message
333   bool canDelete() {
334     return (this.author.id == this.client.state.me.id ||
335       this.channel.can(this.client.state.me, Permissions.MANAGE_MESSAGES));
336   }
337 
338   /// Whether the bot can edit this message
339   bool canEdit() {
340     return (this.author.id == this.client.state.me.id);
341   }
342 }