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 }