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