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 }