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 
11 import dscord.client,
12        dscord.types.all;
13 
14 class MessageEmbed : IModel {
15   mixin Model;
16 
17   string  title;
18   string  type;
19   string  description;
20   string  url;
21 
22   // TODO: thumbnail, provider
23 
24   override void load(ref JSON obj) {
25     obj.keySwitch!(
26       "title", "type", "description", "url"
27     )(
28       { this.title = obj.read!string; },
29       { this.type = obj.read!string; },
30       { this.description = obj.read!string; },
31       { this.url = obj.read!string; },
32     );
33   }
34 }
35 
36 class MessageAttachment : IModel {
37   mixin Model;
38 
39   Snowflake  id;
40   string     filename;
41   uint       size;
42   string     url;
43   string     proxyUrl;
44   uint       height;
45   uint       width;
46 
47   override void load(ref JSON obj) {
48     obj.keySwitch!(
49       "id", "filename", "size", "url", "proxy_url",
50       "height", "width",
51     )(
52       { this.id = readSnowflake(obj); },
53       { this.filename = obj.read!string; },
54       { this.size = obj.read!uint; },
55       { this.url = obj.read!string; },
56       { this.proxyUrl = obj.read!string; },
57       { this.height = obj.read!uint; },
58       { this.width = obj.read!uint; },
59     );
60   }
61 }
62 
63 class Message : IModel {
64   mixin Model;
65 
66   Snowflake  id;
67   Snowflake  channelID;
68   Channel    channel;
69   User       author;
70   string     content;
71   string     timestamp; // TODO: timestamps lol
72   string     editedTimestamp; // TODO: timestamps lol
73   bool       tts;
74   bool       mentionEveryone;
75   string     nonce;
76   bool       pinned;
77 
78   // TODO: GuildMemberMap here
79   UserMap    mentions;
80   RoleMap    roleMentions;
81 
82   // Embeds
83   MessageEmbed[]  embeds;
84 
85   // Attachments
86   MessageAttachment[]  attachments;
87 
88   this(Client client, ref JSON obj) {
89     super(client, obj);
90   }
91 
92   this(Channel channel, ref JSON obj) {
93     this.channel = channel;
94     super(channel.client, obj);
95   }
96 
97   override void init() {
98     this.mentions = new UserMap;
99     this.roleMentions = new RoleMap;
100   }
101 
102   override void load(ref JSON obj) {
103     // TODO: avoid leaking user
104 
105     obj.keySwitch!(
106       "id", "channel_id", "content", "timestamp", "edited_timestamp", "tts",
107       "mention_everyone", "nonce", "author", "pinned", "mentions", "mention_roles",
108       // "embeds", "attachments",
109     )(
110       { this.id = readSnowflake(obj); },
111       { this.channelID = readSnowflake(obj); },
112       { this.content = obj.read!string; },
113       { this.timestamp = obj.read!string; },
114       {
115         if (obj.peek() == DataType..string) {
116           this.editedTimestamp = obj.read!string;
117         } else {
118           obj.skipValue;
119         }
120       },
121       { this.tts = obj.read!bool; },
122       { this.mentionEveryone = obj.read!bool; },
123       { this.nonce = obj.read!string; },
124       { this.author = new User(this.client, obj); },
125       { this.pinned = obj.read!bool; },
126       { loadMany!User(this.client, obj, (u) { this.mentions[u.id] = u; }); },
127       { obj.skipValue; },
128       // { obj.skipValue; },
129       // { obj.skipvalue; },
130     );
131 
132     if (!this.channel && this.client.state.channels.has(this.channelID)) {
133       this.channel = this.client.state.channels.get(this.channelID);
134     }
135   }
136 
137   /*
138     Returns a version of the message contents, with mentions completely removed
139   */
140   string withoutMentions() {
141     return this.replaceMentions((m, u) => "", (m, r) => "");
142   }
143 
144   /*
145     Returns a version of the message contents, replacing all mentions with user/nick names
146   */
147   string withProperMentions(bool nicks=true) {
148     return this.replaceMentions((msg, user) {
149       GuildMember m;
150       if (nicks) {
151         m = msg.guild.members.get(user.id);
152       }
153       return "@" ~ ((m && m.nick != "") ? m.nick : user.username);
154     }, (msg, role) { return "@" ~ role.name; });
155   }
156 
157   /**
158     Returns the message contents, replacing all mentions with the result from the
159     specified delegate.
160   */
161   string replaceMentions(string delegate(Message, User) fu, string delegate(Message, Role) fr) {
162     if (!this.mentions.length) {
163       return this.content;
164     }
165 
166     string result = this.content;
167     foreach (ref User user; this.mentions.values) {
168       result = replaceAll(result, regex(format("<@!?(%s)>", user.id)), fu(this, user));
169     }
170 
171     foreach (ref Role role; this.roleMentions.values) {
172       result = replaceAll(result, regex(format("<@!?(%s)>", role.id)), fr(this, role));
173     }
174 
175     return result;
176   }
177 
178   // Sends a new message to the same channel as this message object
179   Message reply(string content, string nonce=null, bool tts=false) {
180     return this.client.api.sendMessage(this.channel.id, content, nonce, tts);
181   }
182 
183   // Formats and sends a new message to the same channel as this message object
184   Message replyf(T...)(string content, T args) {
185     return this.client.api.sendMessage(this.channel.id, format(content, args), null, false);
186   }
187 
188   // Edits the current messages content
189   Message edit(string content) {
190     // We can only edit messages we sent
191     assert(this.client.me.id == this.author.id);
192     return this.client.api.editMessage(this.channel.id, this.id, content);
193   }
194 
195   // Deletes the current message
196   void del() {
197     // TODO: permissions check
198     return this.client.api.deleteMessage(this.channel.id, this.id);
199   }
200 
201   /*
202     True if this message mentions the current user in any way (everyone, direct mention, role mention)
203   */
204   @property bool mentioned() {
205     this.client.log.tracef("M: %s", this.mentions.keys);
206 
207     return this.mentionEveryone ||
208       this.mentions.has(this.client.state.me.id) ||
209       this.roleMentions.memberHasRoleWithin(
210         this.guild.getMember(this.client.state.me));
211   }
212 
213   @property Guild guild() {
214     if (this.channel && this.channel.guild) return this.channel.guild;
215     return null;
216   }
217 
218   // Returns an array of emoji IDs for all custom emoji used in this message
219   @property Snowflake[] customEmojiByID() {
220     return matchAll(this.content, regex("<:\\w+:(\\d+)>")).map!((m) => m.back.to!Snowflake).array;
221   }
222 }