1package state
2
3import (
4	"github.com/fluffle/goirc/logging"
5
6	"sync"
7)
8
9// The state manager interface
10type Tracker interface {
11	// Nick methods
12	NewNick(nick string) *Nick
13	GetNick(nick string) *Nick
14	ReNick(old, neu string) *Nick
15	DelNick(nick string) *Nick
16	NickInfo(nick, ident, host, name string) *Nick
17	NickModes(nick, modestr string) *Nick
18	// Channel methods
19	NewChannel(channel string) *Channel
20	GetChannel(channel string) *Channel
21	DelChannel(channel string) *Channel
22	Topic(channel, topic string) *Channel
23	ChannelModes(channel, modestr string, modeargs ...string) *Channel
24	// Information about ME!
25	Me() *Nick
26	// And the tracking operations
27	IsOn(channel, nick string) (*ChanPrivs, bool)
28	Associate(channel, nick string) *ChanPrivs
29	Dissociate(channel, nick string)
30	Wipe()
31	// The state tracker can output a debugging string
32	String() string
33}
34
35// ... and a struct to implement it ...
36type stateTracker struct {
37	// Map of channels we're on
38	chans map[string]*channel
39	// Map of nicks we know about
40	nicks map[string]*nick
41
42	// We need to keep state on who we are :-)
43	me *nick
44
45	// And we need to protect against data races *cough*.
46	mu sync.Mutex
47}
48
49var _ Tracker = (*stateTracker)(nil)
50
51// ... and a constructor to make it ...
52func NewTracker(mynick string) *stateTracker {
53	st := &stateTracker{
54		chans: make(map[string]*channel),
55		nicks: make(map[string]*nick),
56	}
57	st.me = newNick(mynick)
58	st.nicks[mynick] = st.me
59	return st
60}
61
62// ... and a method to wipe the state clean.
63func (st *stateTracker) Wipe() {
64	st.mu.Lock()
65	defer st.mu.Unlock()
66	// Deleting all the channels implicitly deletes every nick but me.
67	for _, ch := range st.chans {
68		st.delChannel(ch)
69	}
70}
71
72/******************************************************************************\
73 * tracker methods to create/look up nicks/channels
74\******************************************************************************/
75
76// Creates a new nick, initialises it, and stores it so it
77// can be properly tracked for state management purposes.
78func (st *stateTracker) NewNick(n string) *Nick {
79	if n == "" {
80		logging.Warn("Tracker.NewNick(): Not tracking empty nick.")
81		return nil
82	}
83	st.mu.Lock()
84	defer st.mu.Unlock()
85	if _, ok := st.nicks[n]; ok {
86		logging.Warn("Tracker.NewNick(): %s already tracked.", n)
87		return nil
88	}
89	st.nicks[n] = newNick(n)
90	return st.nicks[n].Nick()
91}
92
93// Returns a nick for the nick n, if we're tracking it.
94func (st *stateTracker) GetNick(n string) *Nick {
95	st.mu.Lock()
96	defer st.mu.Unlock()
97	if nk, ok := st.nicks[n]; ok {
98		return nk.Nick()
99	}
100	return nil
101}
102
103// Signals to the tracker that a nick should be tracked
104// under a "neu" nick rather than the old one.
105func (st *stateTracker) ReNick(old, neu string) *Nick {
106	st.mu.Lock()
107	defer st.mu.Unlock()
108	nk, ok := st.nicks[old]
109	if !ok {
110		logging.Warn("Tracker.ReNick(): %s not tracked.", old)
111		return nil
112	}
113	if _, ok := st.nicks[neu]; ok {
114		logging.Warn("Tracker.ReNick(): %s already exists.", neu)
115		return nil
116	}
117
118	nk.nick = neu
119	delete(st.nicks, old)
120	st.nicks[neu] = nk
121	for ch, _ := range nk.chans {
122		// We also need to update the lookup maps of all the channels
123		// the nick is on, to keep things in sync.
124		delete(ch.lookup, old)
125		ch.lookup[neu] = nk
126	}
127	return nk.Nick()
128}
129
130// Removes a nick from being tracked.
131func (st *stateTracker) DelNick(n string) *Nick {
132	st.mu.Lock()
133	defer st.mu.Unlock()
134	if nk, ok := st.nicks[n]; ok {
135		if nk == st.me {
136			logging.Warn("Tracker.DelNick(): won't delete myself.")
137			return nil
138		}
139		st.delNick(nk)
140		return nk.Nick()
141	}
142	logging.Warn("Tracker.DelNick(): %s not tracked.", n)
143	return nil
144}
145
146func (st *stateTracker) delNick(nk *nick) {
147	// st.mu lock held by DelNick, DelChannel or Wipe
148	if nk == st.me {
149		// Shouldn't get here => internal state tracking code is fubar.
150		logging.Error("Tracker.DelNick(): TRYING TO DELETE ME :-(")
151		return
152	}
153	delete(st.nicks, nk.nick)
154	for ch, _ := range nk.chans {
155		nk.delChannel(ch)
156		ch.delNick(nk)
157		if len(ch.nicks) == 0 {
158			// Deleting a nick from tracking shouldn't empty any channels as
159			// *we* should be on the channel with them to be tracking them.
160			logging.Error("Tracker.delNick(): deleting nick %s emptied "+
161				"channel %s, this shouldn't happen!", nk.nick, ch.name)
162		}
163	}
164}
165
166// Sets ident, host and "real" name for the nick.
167func (st *stateTracker) NickInfo(n, ident, host, name string) *Nick {
168	st.mu.Lock()
169	defer st.mu.Unlock()
170	nk, ok := st.nicks[n]
171	if !ok {
172		return nil
173	}
174	nk.ident = ident
175	nk.host = host
176	nk.name = name
177	return nk.Nick()
178}
179
180// Sets user modes for the nick.
181func (st *stateTracker) NickModes(n, modes string) *Nick {
182	st.mu.Lock()
183	defer st.mu.Unlock()
184	nk, ok := st.nicks[n]
185	if !ok {
186		return nil
187	}
188	nk.parseModes(modes)
189	return nk.Nick()
190}
191
192// Creates a new Channel, initialises it, and stores it so it
193// can be properly tracked for state management purposes.
194func (st *stateTracker) NewChannel(c string) *Channel {
195	if c == "" {
196		logging.Warn("Tracker.NewChannel(): Not tracking empty channel.")
197		return nil
198	}
199	st.mu.Lock()
200	defer st.mu.Unlock()
201	if _, ok := st.chans[c]; ok {
202		logging.Warn("Tracker.NewChannel(): %s already tracked.", c)
203		return nil
204	}
205	st.chans[c] = newChannel(c)
206	return st.chans[c].Channel()
207}
208
209// Returns a Channel for the channel c, if we're tracking it.
210func (st *stateTracker) GetChannel(c string) *Channel {
211	st.mu.Lock()
212	defer st.mu.Unlock()
213	if ch, ok := st.chans[c]; ok {
214		return ch.Channel()
215	}
216	return nil
217}
218
219// Removes a Channel from being tracked.
220func (st *stateTracker) DelChannel(c string) *Channel {
221	st.mu.Lock()
222	defer st.mu.Unlock()
223	if ch, ok := st.chans[c]; ok {
224		st.delChannel(ch)
225		return ch.Channel()
226	}
227	logging.Warn("Tracker.DelChannel(): %s not tracked.", c)
228	return nil
229}
230
231func (st *stateTracker) delChannel(ch *channel) {
232	// st.mu lock held by DelChannel or Wipe
233	delete(st.chans, ch.name)
234	for nk, _ := range ch.nicks {
235		ch.delNick(nk)
236		nk.delChannel(ch)
237		if len(nk.chans) == 0 && nk != st.me {
238			// We're no longer in any channels with this nick.
239			st.delNick(nk)
240		}
241	}
242}
243
244// Sets the topic of a channel.
245func (st *stateTracker) Topic(c, topic string) *Channel {
246	st.mu.Lock()
247	defer st.mu.Unlock()
248	ch, ok := st.chans[c]
249	if !ok {
250		return nil
251	}
252	ch.topic = topic
253	return ch.Channel()
254}
255
256// Sets modes for a channel, including privileges like +o.
257func (st *stateTracker) ChannelModes(c, modes string, args ...string) *Channel {
258	st.mu.Lock()
259	defer st.mu.Unlock()
260	ch, ok := st.chans[c]
261	if !ok {
262		return nil
263	}
264	ch.parseModes(modes, args...)
265	return ch.Channel()
266}
267
268// Returns the Nick the state tracker thinks is Me.
269// NOTE: Nick() requires the mutex to be held.
270func (st *stateTracker) Me() *Nick {
271	st.mu.Lock()
272	defer st.mu.Unlock()
273	return st.me.Nick()
274}
275
276// Returns true if both the channel c and the nick n are tracked
277// and the nick is associated with the channel.
278func (st *stateTracker) IsOn(c, n string) (*ChanPrivs, bool) {
279	st.mu.Lock()
280	defer st.mu.Unlock()
281	nk, nok := st.nicks[n]
282	ch, cok := st.chans[c]
283	if nok && cok {
284		return nk.isOn(ch)
285	}
286	return nil, false
287}
288
289// Associates an already known nick with an already known channel.
290func (st *stateTracker) Associate(c, n string) *ChanPrivs {
291	st.mu.Lock()
292	defer st.mu.Unlock()
293	nk, nok := st.nicks[n]
294	ch, cok := st.chans[c]
295
296	if !cok {
297		// As we can implicitly delete both nicks and channels from being
298		// tracked by dissociating one from the other, we should verify that
299		// we're not being passed an old Nick or Channel.
300		logging.Error("Tracker.Associate(): channel %s not found in "+
301			"internal state.", c)
302		return nil
303	} else if !nok {
304		logging.Error("Tracker.Associate(): nick %s not found in "+
305			"internal state.", n)
306		return nil
307	} else if _, ok := nk.isOn(ch); ok {
308		logging.Warn("Tracker.Associate(): %s already on %s.",
309			nk, ch)
310		return nil
311	}
312	cp := new(ChanPrivs)
313	ch.addNick(nk, cp)
314	nk.addChannel(ch, cp)
315	return cp.Copy()
316}
317
318// Dissociates an already known nick from an already known channel.
319// Does some tidying up to stop tracking nicks we're no longer on
320// any common channels with, and channels we're no longer on.
321func (st *stateTracker) Dissociate(c, n string) {
322	st.mu.Lock()
323	defer st.mu.Unlock()
324	nk, nok := st.nicks[n]
325	ch, cok := st.chans[c]
326
327	if !cok {
328		// As we can implicitly delete both nicks and channels from being
329		// tracked by dissociating one from the other, we should verify that
330		// we're not being passed an old Nick or Channel.
331		logging.Error("Tracker.Dissociate(): channel %s not found in "+
332			"internal state.", c)
333	} else if !nok {
334		logging.Error("Tracker.Dissociate(): nick %s not found in "+
335			"internal state.", n)
336	} else if _, ok := nk.isOn(ch); !ok {
337		logging.Warn("Tracker.Dissociate(): %s not on %s.",
338			nk.nick, ch.name)
339	} else if nk == st.me {
340		// I'm leaving the channel for some reason, so it won't be tracked.
341		st.delChannel(ch)
342	} else {
343		// Remove the nick from the channel and the channel from the nick.
344		ch.delNick(nk)
345		nk.delChannel(ch)
346		if len(nk.chans) == 0 {
347			// We're no longer in any channels with this nick.
348			st.delNick(nk)
349		}
350	}
351}
352
353func (st *stateTracker) String() string {
354	st.mu.Lock()
355	defer st.mu.Unlock()
356	str := "GoIRC Channels\n"
357	str += "--------------\n\n"
358	for _, ch := range st.chans {
359		str += ch.String() + "\n"
360	}
361	str += "GoIRC NickNames\n"
362	str += "---------------\n\n"
363	for _, n := range st.nicks {
364		if n != st.me {
365			str += n.String() + "\n"
366		}
367	}
368	return str
369}
370