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