1
+ <?php
2
+ /*
3
+ PHP IRC Library
4
+ irc_client.php
5
+
6
+ Copyright (c) 2015 Alex Ingram
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in
16
+ all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
+ THE SOFTWARE.
25
+ */
26
+
27
+ class IRCClient {
28
+ // This will be set to "false" if a fatal error has been encountered inside
29
+ // of the class.
30
+ protected $ SanityCheckOK = false ;
31
+ private $ ServerSocket ;
32
+
33
+
34
+ // Server connection information:
35
+ protected $ ServerHostname = '' ;
36
+ protected $ ServerPort = 0 ;
37
+ protected $ ServerUseSSL = false ;
38
+
39
+ protected $ ClientNick = '' ;
40
+ protected $ ClientIdent = '' ;
41
+ protected $ ClientGecos = '' ;
42
+
43
+
44
+ // Configurable options:
45
+ public $ ConnectionTimeout = 30 ;
46
+ public $ CallbackTimeout = 60 ;
47
+ public $ VerboseLog = true ;
48
+ public $ LogSockets = false ;
49
+ public $ CTCPVersionReply = "PHP IRC Library (c) 2015 Alex Ingram :: https://github.com/ReimuHakurei/php-irc " ;
50
+
51
+ // Internal database arrays:
52
+ protected $ CurrentChannels = [];
53
+ protected $ QueuedChannels = [];
54
+
55
+ public function __construct ($ URI , $ Nick , $ Ident , $ Gecos ) {
56
+ // In the construct, we will fill in all of the internal server
57
+ // connection information variables.
58
+ //
59
+ // The format of an IRC URI is simple. It can, in general usage,
60
+ // also include a list of channels. If those are found here,
61
+ // they will be added to an array of a to-join channel queue.
62
+ //
63
+ // An IRC URI is of the following format:
64
+ // irc://serverHostname:[+]port[/channel1,channel2]
65
+ //
66
+ // The + in the port section signifies an SSL connection.
67
+ //
68
+ // Although the IRC protocol does allow for channels starting
69
+ // with characters other than # (most commonly things such
70
+ // as +, &, and $), any channels in an IRC URI are assumed
71
+ // to use the standard # prefix, and as such, the prefix
72
+ // should be omitted. Channels should be comma-delimited.
73
+
74
+ $ this ->log ("PHP-IRCClient (c) 2015 Alex Ingram " );
75
+ $ this ->log ("Starting up... " );
76
+
77
+ $ ExplodedURI = explode ("/ " ,$ URI );
78
+ if (count ($ ExplodedURI ) >= 3 ) {
79
+ $ HostnamePortBlock = $ ExplodedURI [2 ];
80
+ $ ChannelList = '' ;
81
+
82
+ if (array_key_exists (3 ,$ ExplodedURI )) {
83
+ $ this ->QueuedChannels = explode (", " ,$ ExplodedURI [3 ]);
84
+
85
+ foreach ($ this ->QueuedChannels as &$ channel ) {
86
+ $ channel = '# ' . $ channel ;
87
+ }
88
+ }
89
+
90
+ $ HostnamePort = explode (": " ,$ HostnamePortBlock );
91
+
92
+ if (count ($ HostnamePort ) == 2 ) {
93
+ $ this ->ServerHostname = $ HostnamePort [0 ];
94
+ $ this ->ServerPort = $ HostnamePort [1 ];
95
+ } else {
96
+ $ this ->error ("Malformed IRC URI in construct. " );
97
+ }
98
+
99
+ if ($ this ->ServerPort [0 ] == "+ " ) {
100
+ $ Port = str_split ($ this ->ServerPort );
101
+ array_shift ($ Port );
102
+
103
+ $ this ->ServerPort = implode ($ Port );
104
+ $ this ->ServerUseSSL = true ;
105
+ }
106
+ } else {
107
+ $ this ->error ("Malformed IRC URI in construct. " );
108
+ }
109
+
110
+ // Just for debugging.
111
+ $ this ->log (" Server : " . $ this ->ServerHostname . ": " . $ this ->ServerPort . ", using SSL: " . var_export ($ this ->ServerUseSSL , true ));
112
+ $ this ->log (" Channels : " . implode (", " , $ this ->QueuedChannels ));
113
+
114
+ $ this ->ClientNick = $ Nick ;
115
+ $ this ->ClientIdent = $ Ident ;
116
+ $ this ->ClientGecos = $ Gecos ;
117
+
118
+ $ this ->SanityCheckOK = true ;
119
+
120
+ $ this ->connect ();
121
+ }
122
+
123
+
124
+
125
+ // Logging functions
126
+
127
+ protected function log ($ LogText ) {
128
+ if ($ this ->VerboseLog ) {
129
+ echo "[ " . date ("Y-m-d H:i:s " ) . "] " . implode (explode ("\r\n" ,$ LogText )) . "\n" ;
130
+ }
131
+ }
132
+
133
+ protected function error ($ ErrorText ) {
134
+ $ this ->log ("ERROR: " . $ ErrorText );
135
+ throw new Exception ($ ErrorText );
136
+ }
137
+
138
+
139
+ // Connection handling functions
140
+
141
+ protected function connect () {
142
+ // Note: This function will block until the bot disconnects.
143
+
144
+
145
+ if (!$ this ->SanityCheckOK ) {
146
+ $ this ->log ("WARNING: The constructor did not complete, expect major issues! " );
147
+ }
148
+
149
+
150
+ $ this ->log ("Connecting... " );
151
+
152
+ if ($ this ->ServerUseSSL ) {
153
+ $ ServerURI = "tls:// " . $ this ->ServerHostname . ": " . $ this ->ServerPort ;
154
+ } else {
155
+ $ ServerURI = "tcp:// " . $ this ->ServerHostname . ": " . $ this ->ServerPort ;
156
+ }
157
+
158
+ // Almost no IRC servers have valid SSL certificates, so we'll just ignore them.
159
+ $ Context = stream_context_create ();
160
+ stream_context_set_option ($ Context , 'ssl ' , 'verify_peer ' , false );
161
+ stream_context_set_option ($ Context , 'ssl ' , 'verify_peer_name ' , false );
162
+
163
+
164
+ @$ this ->ServerSocket = stream_socket_client ($ ServerURI , $ errno , $ errstr , $ this ->ConnectionTimeout , STREAM_CLIENT_CONNECT , $ Context );
165
+
166
+ stream_set_timeout ($ this ->ServerSocket ,$ this ->CallbackTimeout );
167
+
168
+ if (!$ this ->ServerSocket ) {
169
+ $ this ->log ("Server connection failed: $ errstr ( $ errno) " );
170
+ } else {
171
+ $ this ->log ("Connection established. Registering with server... " );
172
+
173
+ $ RegistrationComplete = false ;
174
+
175
+ $ this ->nick ($ this ->ClientNick );
176
+ $ this ->send ("USER " . $ this ->ClientIdent . " 0 * : " . $ this ->ClientGecos );
177
+
178
+ while (!feof ($ this ->ServerSocket )) {
179
+ if ($ Message = fgets ($ this ->ServerSocket , 512 )) {
180
+ if ($ this ->LogSockets ) {
181
+ $ this ->log (">> " . $ Message );
182
+ }
183
+
184
+ $ Message = $ this ->parse_message ($ Message );
185
+
186
+ if (($ Message ->Command == "001 " || $ Message ->Command == "002 " || $ Message ->Command == "003 " || $ Message ->Command == "004 " ) && (!$ RegistrationComplete )) {
187
+ $ RegistrationComplete = true ;
188
+
189
+ $ this ->log ("Registration complete. " );
190
+
191
+ foreach ($ this ->QueuedChannels as $ Channel ) {
192
+ $ this ->join ($ Channel );
193
+ }
194
+ }
195
+
196
+ if ($ RegistrationComplete ) {
197
+ $ this ->recv ($ Message );
198
+ }
199
+ } else {
200
+ $ this ->recv (NULL );
201
+ }
202
+ }
203
+ }
204
+
205
+ $ this ->log ("Socket disconnected. " );
206
+ }
207
+
208
+ private function parse_message ($ Message ) {
209
+ // This function handles the IRC protocol itself (ie: responding to PINGs, CTCP requests, etc), and
210
+ // will return a message object which will be passed along to the user's code.
211
+
212
+ // We want to keep a copy of the raw message, in case the user wants it.
213
+ $ RawMessage = $ Message ;
214
+
215
+
216
+ // RFC 2812 says that any messages lacking a prefix can be assumed to originate from the connection itself.
217
+ // As such, we will set the prefix of any non-prefixed message to the server hostname, for parsing purposes.
218
+ // This also allows us to process all messages in the same block of code.
219
+ if ($ Message [0 ] != ': ' ) {
220
+ $ Message = ": " . $ this ->ServerHostname . " " . $ Message ;
221
+ }
222
+
223
+ // We're now going to remove that ':' we just added above.
224
+ $ MessageArray = str_split ($ Message );
225
+ array_shift ($ MessageArray );
226
+ $ Message = implode ($ MessageArray );
227
+ unset($ MessageArray );
228
+
229
+ // Remove the CR-LF from the end...
230
+ $ Message = substr ($ Message ,0 ,sizeof ($ Message ) - 3 );
231
+
232
+ // RFC 2812 says that the trailing parameter (the one starting with a :), if present, should be treated
233
+ // exactly the same as any other parameter.
234
+ // Pull the trailing parameter, if present...
235
+ $ Trailing = NULL ;
236
+ $ MessageArray = explode (": " ,$ Message );
237
+ if (sizeof ($ MessageArray ) > 1 ) {
238
+ $ Trailing = $ MessageArray [1 ];
239
+ $ Message = $ MessageArray [0 ];
240
+ }
241
+ unset($ MessageArray );
242
+
243
+ // Pull the middle parameters...
244
+ $ MessageArray = explode (" " ,$ Message );
245
+ // ...and slap the trailing parameter back on the end.
246
+ $ MessageArray [] = $ Trailing ;
247
+
248
+ if (sizeof ($ MessageArray ) >= 3 ) {
249
+ // Time to chop up that prefix into its' three segments!
250
+ $ MessagePrefix = split ('[!@] ' ,$ MessageArray [0 ]);
251
+ array_shift ($ MessageArray );
252
+ if (sizeof ($ MessagePrefix ) == 3 ) {
253
+ $ Nick = $ MessagePrefix [0 ];
254
+ $ User = $ MessagePrefix [1 ];
255
+ $ Host = $ MessagePrefix [2 ];
256
+ } else {
257
+ $ Nick = $ MessagePrefix [0 ];
258
+ $ User = NULL ;
259
+ $ Host = NULL ;
260
+ }
261
+
262
+ $ Command = $ MessageArray [0 ];
263
+ array_shift ($ MessageArray );
264
+
265
+ $ Parameters = array_values (array_filter ($ MessageArray ));
266
+
267
+ $ Message = (object ) array ('Nick ' => $ Nick , 'User ' => $ User , 'Host ' => $ Host , 'Command ' => $ Command , 'Parameters ' => $ Parameters , 'RawMessage ' => $ RawMessage );
268
+
269
+ // Respond to PINGs
270
+ if ($ Command == "PING " ) {
271
+ $ this ->send ("PONG : " . $ Parameters [0 ]);
272
+ }
273
+
274
+ // Handle CTCP requests
275
+ if (($ Command == "PRIVMSG " || $ Command == "NOTICE " ) && array_key_exists (1 ,$ Parameters )) {
276
+ if (($ Parameters [1 ][0 ] == "\001" ) && ($ Parameters [1 ][sizeof ($ Parameters [1 ])-1 ] == "\001" )) {
277
+ if ($ Parameters [1 ] == "\001VERSION \001" ) {
278
+ $ this ->send ("NOTICE $ Nick : " . $ this ->CTCPVersionReply );
279
+
280
+ $ this ->log ("Received CTCP VERSION request from $ Nick. " );
281
+ }
282
+
283
+ if ($ Parameters [1 ][1 ] == 'P ' && $ Parameters [1 ][2 ] == 'I ' && $ Parameters [1 ][3 ] == 'N ' && $ Parameters [1 ][4 ] == 'G ' ) {
284
+ $ this ->send ("NOTICE $ Nick : " . $ Parameters [1 ]);
285
+
286
+ $ this ->log ("Received CTCP PING request from $ Nick. " );
287
+ }
288
+ }
289
+ }
290
+
291
+ return $ Message ;
292
+ } else {
293
+ $ this ->log ("Notice: Malformed message detected. " );
294
+ $ this ->log ($ RawMessage );
295
+ }
296
+ }
297
+
298
+
299
+
300
+ // Socket functions
301
+
302
+ protected function send ($ Message ) {
303
+ if (sizeof ($ Message ) > 510 ) {
304
+ $ this ->log ("Notice: An attempt was made to send an excessively long string. The string will be trimmed to 510 characters. " );
305
+ }
306
+
307
+ if ($ this ->LogSockets ) {
308
+ $ this ->log ("<< " . substr ($ Message , 0 , 510 ));
309
+ }
310
+
311
+ fwrite ($ this ->ServerSocket , substr ($ Message , 0 , 510 ) . "\r\n" , 512 );
312
+ }
313
+
314
+
315
+ protected function recv ($ Message ) {
316
+ return 0 ;
317
+ }
318
+
319
+
320
+ // Protocol abstraction functions
321
+
322
+ protected function nick ($ Nick ) {
323
+ $ this ->send ("NICK : " . $ Nick );
324
+ }
325
+
326
+ protected function join ($ Channel ) {
327
+ $ this ->send ("JOIN : " . $ Channel );
328
+ }
329
+
330
+ protected function part ($ Channel , $ Message ) {
331
+ $ this ->send ("PART " . $ Channel . " : " . $ Message );
332
+ }
333
+
334
+ protected function quit ($ Message ) {
335
+ $ this ->send ("QUIT : " . $ Message );
336
+ }
337
+
338
+ protected function kick ($ Channel , $ Person , $ Message ) {
339
+ $ this ->send ("KICK " . $ Channel . " " . $ Person . " : " . $ Message );
340
+ }
341
+ }
342
+ ?>
0 commit comments