1 module ircbod.client;
2 
3 import ircbod.socket, ircbod.message;
4 import std.regex, std.container, std.datetime, std.conv;
5 
6 alias void delegate(IRCMessage message)                 MessageHandler;
7 alias void delegate(IRCMessage message, string[] args)  MessageHandlerWithArgs;
8 
9 class IRCClient
10 {
11 private:
12     struct PatternMessageHandler {
13         MessageHandler          callback;
14         MessageHandlerWithArgs  callbackWithArgs;
15         Regex!char              pattern;
16     }
17 
18     alias DList!PatternMessageHandler HandlerList;
19 
20     IRCSocket                       sock;
21     string                          nickname;
22     string                          password;
23     string[]                        channels;
24     HandlerList[IRCMessage.Type]    handlers;
25     bool                            running;
26 
27     static Regex!char MATCHALL = regex(".*");
28 
29 public:
30 
31     this(string server, ushort port, string nickname, string password = null, string[] channels = [])
32     {
33         this.sock     = new IRCSocket(server.dup, port);
34         this.nickname = nickname;
35         this.password = password;
36         this.channels = channels;
37         this.running  = true;
38     }
39 
40     string name() {
41         return this.nickname;
42     }
43 
44     void connect()
45     {
46         this.sock.connect();
47 
48         if (!this.sock.connected()) {
49             throw new Exception("Could not connect to irc server!");
50         }
51 
52         if (this.password) {
53             this.sock.pass(this.password);
54         }
55 
56         this.sock.nick(this.nickname);
57         this.sock.user(this.nickname, 0, "*", "ircbod");
58 
59         foreach(c; this.channels) {
60             this.sock.join(c);
61         }
62     }
63 
64     bool connected()
65     {
66         return this.sock.connected();
67     }
68 
69     void disconnect()
70     {
71         this.sock.disconnect();
72     }
73 
74     void reconnect()
75     {
76         disconnect();
77         connect();
78     }
79 
80     void on(IRCMessage.Type type, MessageHandler callback)
81     {
82         on(type, MATCHALL, callback);
83     }
84 
85     void on(IRCMessage.Type type, MessageHandlerWithArgs callback)
86     {
87         on(type, MATCHALL, callback);
88     }
89 
90     void on(IRCMessage.Type type, string pattern, MessageHandler callback)
91     {
92         on(type, regex(pattern), callback);
93     }
94 
95     void on(IRCMessage.Type type, string pattern, MessageHandlerWithArgs callback)
96     {
97         on(type, regex(pattern), callback);
98     }
99 
100     void on(IRCMessage.Type type, Regex!char regex, MessageHandler callback)
101     {
102         if(type == IRCMessage.Type.MESSAGE) {
103             on(IRCMessage.Type.CHAN_MESSAGE, regex, callback);
104             on(IRCMessage.Type.PRIV_MESSAGE, regex, callback);
105             return;
106         }
107 
108         PatternMessageHandler handler = { callback, null, regex };
109         if(type !in this.handlers) {
110             this.handlers[type] = HandlerList([handler]);
111         } else {
112             this.handlers[type].insertBack(handler);
113         }
114     }
115 
116     void on(IRCMessage.Type type, Regex!char regex, MessageHandlerWithArgs callback)
117     {
118         if(type == IRCMessage.Type.MESSAGE) {
119             on(IRCMessage.Type.CHAN_MESSAGE, regex, callback);
120             on(IRCMessage.Type.PRIV_MESSAGE, regex, callback);
121             return;
122         }
123 
124         PatternMessageHandler handler = { null, callback, regex };
125         if(type !in this.handlers) {
126             this.handlers[type] = HandlerList([handler]);
127         } else {
128             this.handlers[type].insertBack(handler);
129         }
130     }
131 
132 
133     void run()
134     {
135         if(!connected())
136             connect();
137 
138         scope(exit) disconnect();
139 
140         string line;
141         while (this.running && (line = this.sock.read()).length > 0) {
142             std.stdio.writeln(line);
143             processLine(line);
144         }
145     }
146 
147     bool isRunning()
148     {
149         return this.running;
150     }
151 
152     void quit()
153     {
154         this.running = false;
155     }
156 
157     void sendMessageToChannel(string message, string channel)
158     {
159         this.sock.privmsg(channel, message);
160     }
161 
162     void sendMessageToUser(string message, string nickname)
163     {
164         this.sock.privmsg(nickname, message);
165     }
166 
167     void broadcast(string message)
168     {
169         foreach(c; this.channels) {
170             sendMessageToChannel(message, c);
171         }
172     }
173 
174 private:
175 
176     IRCMessage.Type typeForString(string typeStr)
177     {
178         IRCMessage.Type type;
179         switch(typeStr) {
180             case "JOIN":
181                 return IRCMessage.Type.JOIN;
182             case "PART":
183                 return IRCMessage.Type.PART;
184             case "QUIT":
185                 return IRCMessage.Type.QUIT;
186             default:
187                 return IRCMessage.Type.CHAN_MESSAGE;
188         }
189     }
190 
191     void processLine(string message)
192     {
193         if (auto matcher = match(message, r"^:(\S+)\!\S+ (JOIN|PART|QUIT) :?(\S+).*")) {
194             auto user    = matcher.captures[1];
195             auto typeStr = matcher.captures[2];
196             auto channel = matcher.captures[3];
197             auto time    = to!DateTime(Clock.currTime());
198             auto type    = typeForString(typeStr);
199             IRCMessage ircMessage = {
200                 type,
201                 typeStr,
202                 user,
203                 channel,
204                 time,
205                 this
206             };
207 
208             return handleMessage(ircMessage);
209         }
210 
211         if (auto matcher = match(message, r"^:(\S+)\!\S+ PRIVMSG (\S+) :(.*)$")) {
212             auto user    = matcher.captures[1];
213             auto channel = matcher.captures[2];
214             auto text    = matcher.captures[3];
215             auto time    = to!DateTime(Clock.currTime());
216             auto type    = channel[0] == '#' ? IRCMessage.Type.CHAN_MESSAGE : IRCMessage.Type.PRIV_MESSAGE;
217             IRCMessage ircMessage = {
218                 type,
219                 text,
220                 user,
221                 channel,
222                 time,
223                 this
224             };
225 
226             return handleMessage(ircMessage);
227         }
228 
229         if (auto matcher = match(message, r"^PING (.+)$")) {
230             auto server = matcher.captures[1];
231             this.sock.pong(server);
232         }
233     }
234 
235     void handleMessage(IRCMessage message)
236     {
237         if(message.type in this.handlers) {
238             foreach(PatternMessageHandler h; this.handlers[message.type]) {
239                 if(auto matcher = match(message.text, h.pattern)) {
240                     string[] args = [];
241                     foreach(string m; matcher.captures) {
242                         args ~= m;
243                     }
244                     if(h.callback)
245                         h.callback(message);
246                     if(h.callbackWithArgs)
247                         h.callbackWithArgs(message, args[1..$]);
248                 }
249             }
250         }
251     }
252 }