1 module dcord.state; 2 3 import std.functional, 4 std.stdio, 5 std.algorithm.iteration, 6 std.experimental.logger; 7 8 import vibe.core.sync : createManualEvent, LocalManualEvent; 9 import std.algorithm.searching : canFind, countUntil; 10 import std.algorithm.mutation : remove; 11 12 import dcord.api, 13 dcord.types, 14 dcord.client, 15 dcord.gateway, 16 dcord.util.emitter; 17 18 /** 19 The State class is used to track and maintain client state. 20 */ 21 class State: Emitter { 22 // Client 23 Client client; 24 APIClient api; 25 GatewayClient gw; 26 27 /// Currently logged in user, recieved from READY payload. 28 User me; 29 30 /// All users that the bot sees 31 UserMap users; 32 33 /// All currently loaded guilds 34 GuildMap guilds; 35 36 /// All currently loaded DMs 37 ChannelMap directMessages; 38 39 /// All currently loaded channels 40 ChannelMap channels; 41 42 /// All voice states 43 VoiceStateMap voiceStates; 44 45 /// Event triggered when all guilds are synced 46 LocalManualEvent ready; 47 48 bool requestOfflineMembers = true; 49 50 private { 51 Snowflake[] awaitingCreate; 52 53 Logger log; 54 EventListenerArray listeners; 55 } 56 57 this(Client client) { 58 this.client = client; 59 this.log = client.log; 60 this.api = client.api; 61 this.gw = client.gw; 62 63 this.users = new UserMap; 64 this.guilds = new GuildMap; 65 this.directMessages = new ChannelMap; 66 this.channels = new ChannelMap; 67 this.voiceStates = new VoiceStateMap; 68 69 this.ready = createManualEvent(); 70 71 // Finally bind all listeners 72 this.bindListeners(); 73 } 74 75 private void listen(Ty...)() { 76 foreach (T; Ty) { 77 this.listeners ~= this.client.events.listen!T(mixin("&this.on" ~ T.stringof)); 78 } 79 } 80 81 private void bindListeners() { 82 // Unbind all listeners 83 this.listeners.each!((l) => l.unbind()); 84 85 // Always listen for ready payload 86 this.listen!( 87 Ready, GuildCreate, GuildUpdate, GuildDelete, GuildMemberAdd, GuildMemberRemove, 88 GuildMemberUpdate, GuildMembersChunk, GuildRoleCreate, GuildRoleUpdate, GuildRoleDelete, 89 GuildEmojisUpdate, ChannelCreate, ChannelUpdate, ChannelDelete, VoiceStateUpdate, MessageCreate, 90 PresenceUpdate 91 ); 92 } 93 94 private void onReady(Ready r) { 95 this.me = r.me; 96 97 foreach (guild; r.guilds) { 98 this.awaitingCreate ~= guild.id; 99 } 100 101 foreach (dm; r.dms) { 102 this.directMessages[dm.id] = dm; 103 } 104 } 105 106 private void onGuildCreate(GuildCreate c) { 107 // If this guild is "coming online" and we're awaiting its creation, clear that state here 108 if (!c.unavailable && this.awaitingCreate.canFind(c.guild.id)) { 109 this.awaitingCreate.remove(this.awaitingCreate.countUntil(c.guild.id)); 110 111 // If no other guilds are awaiting, emit the event 112 if (this.awaitingCreate.length == 0) { 113 this.ready.emit(); 114 } 115 } 116 117 this.guilds[c.guild.id] = c.guild; 118 119 c.guild.channels.each((c) { 120 this.channels[c.id] = c; 121 }); 122 123 c.guild.members.each((m) { 124 this.users[m.user.id] = m.user; 125 }); 126 127 c.guild.voiceStates.each((v) { 128 this.voiceStates[v.sessionID] = v; 129 }); 130 131 if (this.requestOfflineMembers) { 132 c.guild.requestOfflineMembers(); 133 } 134 } 135 136 private void onGuildUpdate(GuildUpdate c) { 137 if (!this.guilds.has(c.guild.id)) return; 138 // TODO: handle updates, iterate over raw data 139 // this.guilds[c.guild.id].fromUpdate(c); 140 } 141 142 private void onGuildDelete(GuildDelete c) { 143 if (!this.guilds.has(c.guildID)) return; 144 this.guilds.remove(c.guildID); 145 } 146 147 private void onGuildMemberAdd(GuildMemberAdd c) { 148 if (this.users.has(c.member.user.id)) { 149 this.users[c.member.user.id] = c.member.user; 150 } 151 152 if (this.guilds.has(c.member.guild.id)) { 153 this.guilds[c.member.guild.id].members[c.member.user.id] = c.member; 154 } 155 } 156 157 private void onGuildMemberRemove(GuildMemberRemove c) { 158 if (!this.guilds.has(c.guildID)) return; 159 if (!this.guilds[c.guildID].members.has(c.user.id)) return; 160 this.guilds[c.guildID].members.remove(c.user.id); 161 } 162 163 private void onGuildMemberUpdate(GuildMemberUpdate c) { 164 if (!this.guilds.has(c.member.guildID)) return; 165 if (!this.guilds[c.member.guildID].members.has(c.member.user.id)) return; 166 } 167 168 private void onGuildRoleCreate(GuildRoleCreate c) { 169 if (!this.guilds.has(c.guildID)) return; 170 this.guilds[c.guildID].roles[c.role.id] = c.role; 171 } 172 173 private void onGuildRoleDelete(GuildRoleDelete c) { 174 if (!this.guilds.has(c.guildID)) return; 175 if (!this.guilds[c.guildID].roles.has(c.role.id)) return; 176 this.guilds[c.guildID].roles.remove(c.role.id); 177 } 178 179 private void onGuildRoleUpdate(GuildRoleUpdate c) { 180 if (!this.guilds.has(c.guildID)) return; 181 if (!this.guilds[c.guildID].roles.has(c.role.id)) return; 182 this.guilds[c.guildID].roles[c.role.id] = c.role; 183 } 184 185 private void onChannelCreate(ChannelCreate c) { 186 this.channels[c.channel.id] = c.channel; 187 } 188 189 private void onChannelUpdate(ChannelUpdate c) { 190 this.channels[c.channel.id] = c.channel; 191 } 192 193 private void onChannelDelete(ChannelDelete c) { 194 if (this.channels.has(c.channel.id)) { 195 this.channels.remove(c.channel.id); 196 } 197 } 198 199 private void onVoiceStateUpdate(VoiceStateUpdate u) { 200 // TODO: shallow tracking, don't require guilds 201 auto guild = this.guilds.get(u.state.guildID); 202 if (!guild) return; 203 204 if (!u.state.channelID) { 205 this.voiceStates.remove(u.state.sessionID); 206 guild.voiceStates.remove(u.state.sessionID); 207 } else { 208 this.voiceStates[u.state.sessionID] = u.state; 209 guild.voiceStates[u.state.sessionID] = u.state; 210 } 211 } 212 213 private void onGuildMembersChunk(GuildMembersChunk c) { 214 // TODO 215 } 216 217 private void onGuildEmojisUpdate(GuildEmojisUpdate c) { 218 // TODO 219 } 220 221 private void onMessageCreate(MessageCreate mc) { 222 // TODO 223 } 224 225 private void onPresenceUpdate(PresenceUpdate p) { 226 // TODO 227 } 228 }