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 }