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