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 }