1 |
/* Definitions of IRC message functions and list of messages. |
2 |
* |
3 |
* IRC Services is copyright (c) 1996-2009 Andrew Church. |
4 |
* E-mail: <achurch@achurch.org> |
5 |
* Parts written by Andrew Kempe and others. |
6 |
* This program is free but copyrighted software; see the file GPL.txt for |
7 |
* details. |
8 |
*/ |
9 |
|
10 |
#include "services.h" |
11 |
#include "messages.h" |
12 |
#include "language.h" |
13 |
#include "modules.h" |
14 |
#include "version.h" |
15 |
#include "modules/operserv/operserv.h" |
16 |
|
17 |
/*************************************************************************/ |
18 |
|
19 |
/* Enable ignore code for PRIVMSGs? */ |
20 |
int allow_ignore = 1; |
21 |
|
22 |
/* Callbacks for various messages */ |
23 |
static int cb_privmsg = -1; |
24 |
static int cb_whois = -1; |
25 |
|
26 |
/*************************************************************************/ |
27 |
/************************ Basic message handling *************************/ |
28 |
/*************************************************************************/ |
29 |
|
30 |
static void m_nickcoll(char *source, int ac, char **av) |
31 |
{ |
32 |
if (ac < 1) |
33 |
return; |
34 |
if (!readonly) |
35 |
introduce_user(av[0]); |
36 |
} |
37 |
|
38 |
/*************************************************************************/ |
39 |
|
40 |
static void m_ping(char *source, int ac, char **av) |
41 |
{ |
42 |
if (ac < 1) |
43 |
return; |
44 |
send_cmd(ServerName, "PONG %s %s", ac>1 ? av[1] : ServerName, av[0]); |
45 |
} |
46 |
|
47 |
/*************************************************************************/ |
48 |
|
49 |
static void m_info(char *source, int ac, char **av) |
50 |
{ |
51 |
int i; |
52 |
struct tm *tm; |
53 |
char timebuf[64]; |
54 |
|
55 |
if (!*source) { |
56 |
log("Source missing from INFO message"); |
57 |
return; |
58 |
} |
59 |
|
60 |
tm = localtime(&start_time); |
61 |
strftime(timebuf, sizeof(timebuf), "%a %b %d %H:%M:%S %Y %Z", tm); |
62 |
|
63 |
for (i = 0; info_text[i]; i++) |
64 |
send_cmd(ServerName, "371 %s :%s", source, info_text[i]); |
65 |
send_cmd(ServerName, "371 %s :Version %s (%s)", source, |
66 |
version_number, version_build); |
67 |
send_cmd(ServerName, "371 %s :On-line since %s", source, timebuf); |
68 |
send_cmd(ServerName, "374 %s :End of /INFO list.", source); |
69 |
} |
70 |
|
71 |
/*************************************************************************/ |
72 |
|
73 |
static void m_join(char *source, int ac, char **av) |
74 |
{ |
75 |
if (!*source) { |
76 |
log("Source missing from JOIN message"); |
77 |
return; |
78 |
} else if (ac < 1) { |
79 |
return; |
80 |
} |
81 |
do_join(source, ac, av); |
82 |
} |
83 |
|
84 |
/*************************************************************************/ |
85 |
|
86 |
static void m_kick(char *source, int ac, char **av) |
87 |
{ |
88 |
if (!*source) { |
89 |
log("Source missing from KICK message"); |
90 |
return; |
91 |
} else if (ac != 3) { |
92 |
return; |
93 |
} |
94 |
do_kick(source, ac, av); |
95 |
} |
96 |
|
97 |
/*************************************************************************/ |
98 |
|
99 |
static void m_kill(char *source, int ac, char **av) |
100 |
{ |
101 |
if (!*source) { |
102 |
log("Source missing from KILL message"); |
103 |
return; |
104 |
} else if (ac != 2) { |
105 |
return; |
106 |
} |
107 |
/* Recover if someone kills us. If introduce_user() returns 0, then |
108 |
* the user in question isn't a pseudoclient, so pass it on to the |
109 |
* user handling code. */ |
110 |
if (!introduce_user(av[0])) |
111 |
do_kill(source, ac, av); |
112 |
} |
113 |
|
114 |
/*************************************************************************/ |
115 |
|
116 |
static void m_mode(char *source, int ac, char **av) |
117 |
{ |
118 |
if (!*source) { |
119 |
log("Source missing from MODE message"); |
120 |
return; |
121 |
} |
122 |
|
123 |
if (*av[0] == '#' || *av[0] == '&') { |
124 |
if (ac < 2) |
125 |
return; |
126 |
do_cmode(source, ac, av); |
127 |
} else { |
128 |
if (ac != 2) { |
129 |
return; |
130 |
} else if (irc_stricmp(source,av[0])!=0 && strchr(source,'.')==NULL) { |
131 |
log("user: MODE %s %s from different nick %s!", av[0], av[1], |
132 |
source); |
133 |
wallops(NULL, "%s attempted to change mode %s for %s", |
134 |
source, av[1], av[0]); |
135 |
return; |
136 |
} |
137 |
do_umode(source, ac, av); |
138 |
} |
139 |
} |
140 |
|
141 |
/*************************************************************************/ |
142 |
|
143 |
static void m_motd(char *source, int ac, char **av) |
144 |
{ |
145 |
FILE *f; |
146 |
char buf[BUFSIZE]; |
147 |
|
148 |
if (!*source) { |
149 |
log("Source missing from MOTD message"); |
150 |
return; |
151 |
} |
152 |
|
153 |
f = fopen(MOTDFilename, "r"); |
154 |
send_cmd(ServerName, "375 %s :- %s Message of the Day", |
155 |
source, ServerName); |
156 |
if (f) { |
157 |
while (fgets(buf, sizeof(buf), f)) { |
158 |
buf[strlen(buf)-1] = 0; |
159 |
send_cmd(ServerName, "372 %s :- %s", source, buf); |
160 |
} |
161 |
fclose(f); |
162 |
} else { |
163 |
send_cmd(ServerName, "372 %s :- MOTD file not found! Please " |
164 |
"contact your IRC administrator.", source); |
165 |
} |
166 |
} |
167 |
|
168 |
/*************************************************************************/ |
169 |
|
170 |
static void m_part(char *source, int ac, char **av) |
171 |
{ |
172 |
if (!*source) { |
173 |
log("Source missing from PART message"); |
174 |
return; |
175 |
} else if (ac < 1 || ac > 2) { |
176 |
return; |
177 |
} |
178 |
do_part(source, ac, av); |
179 |
} |
180 |
|
181 |
/*************************************************************************/ |
182 |
|
183 |
static const char msg_up_inactive[] = |
184 |
"Network buffer size exceeded inactive threshold (%d%%), not processing" |
185 |
" PRIVMSGs"; |
186 |
static const char msg_up_ignore[] = |
187 |
"Network buffer size exceeded ignore threshold (%d%%), ignoring PRIVMSGs"; |
188 |
static const char msg_down_inactive[] = |
189 |
"Network buffer size dropped below ignore threshold (%d%%), not" |
190 |
" processing PRIVMSGs"; |
191 |
static const char msg_down_normal[] = |
192 |
"Network buffer size dropped below inactive threshold (%d%%)," |
193 |
" processing PRIVMSGs normally"; |
194 |
|
195 |
static void m_privmsg(char *source, int ac, char **av) |
196 |
{ |
197 |
/* PRIVMSG handling status based on NetBufferLimit settings */ |
198 |
static enum {NORMAL,INACTIVE,IGNORE} netbuf_status = NORMAL; |
199 |
|
200 |
uint32 start, stop; /* When processing started and finished */ |
201 |
User *u = get_user(source); |
202 |
char *s; |
203 |
|
204 |
|
205 |
if (!*source) { |
206 |
log("Source missing from PRIVMSG message"); |
207 |
return; |
208 |
} else if (ac != 2) { |
209 |
return; |
210 |
} |
211 |
|
212 |
/* If a server is specified (nick@server format), make sure it matches |
213 |
* us, and strip it off. */ |
214 |
s = strchr(av[0], '@'); |
215 |
if (s) { |
216 |
*s++ = 0; |
217 |
if (stricmp(s, ServerName) != 0) |
218 |
return; |
219 |
} |
220 |
|
221 |
/* Check network buffer status. */ |
222 |
if (NetBufferLimitInactive) { |
223 |
int bufstat = sock_bufstat(servsock, NULL, NULL, NULL, NULL); |
224 |
const char *message = NULL; |
225 |
int value = 0; |
226 |
switch (netbuf_status) { |
227 |
case NORMAL: |
228 |
if (NetBufferLimitIgnore && bufstat >= NetBufferLimitIgnore) { |
229 |
message = msg_up_ignore; |
230 |
value = NetBufferLimitIgnore; |
231 |
netbuf_status = IGNORE; |
232 |
} else if (bufstat >= NetBufferLimitInactive) { |
233 |
message = msg_up_inactive; |
234 |
value = NetBufferLimitInactive; |
235 |
netbuf_status = INACTIVE; |
236 |
} |
237 |
break; |
238 |
case INACTIVE: |
239 |
if (NetBufferLimitIgnore && bufstat >= NetBufferLimitIgnore) { |
240 |
message = msg_up_ignore; |
241 |
value = NetBufferLimitIgnore; |
242 |
netbuf_status = IGNORE; |
243 |
} else if (bufstat < NetBufferLimitInactive) { |
244 |
message = msg_down_normal; |
245 |
value = NetBufferLimitInactive; |
246 |
netbuf_status = NORMAL; |
247 |
} |
248 |
break; |
249 |
case IGNORE: |
250 |
if (bufstat < NetBufferLimitInactive) { |
251 |
message = msg_down_normal; |
252 |
value = NetBufferLimitInactive; |
253 |
netbuf_status = NORMAL; |
254 |
} else if (bufstat < NetBufferLimitIgnore) { |
255 |
message = msg_down_inactive; |
256 |
value = NetBufferLimitIgnore; |
257 |
netbuf_status = INACTIVE; |
258 |
} |
259 |
break; |
260 |
} /* switch (netbuf_status) */ |
261 |
if (message) { |
262 |
log(message, value); |
263 |
wallops(NULL, message, value); |
264 |
} |
265 |
} |
266 |
|
267 |
/* Check if we should ignore. Operators always get through. */ |
268 |
if (u) { |
269 |
ignore_update(u, 0); |
270 |
if (!is_oper(u)) { |
271 |
if (netbuf_status != NORMAL) { |
272 |
if (netbuf_status == INACTIVE) { |
273 |
if (u) |
274 |
notice_lang(av[0], u, SERVICES_IS_BUSY); |
275 |
else |
276 |
notice(av[0], source, |
277 |
getstring(NULL, SERVICES_IS_BUSY)); |
278 |
} |
279 |
return; |
280 |
} else if (allow_ignore && IgnoreDecay && IgnoreThreshold) { |
281 |
if (u->ignore >= IgnoreThreshold) { |
282 |
log("Ignored message from %s: \"%s\"", source, inbuf); |
283 |
return; |
284 |
} |
285 |
} |
286 |
} |
287 |
} |
288 |
|
289 |
/* Not ignored; actually execute the command, and update ignore data. */ |
290 |
start = time_msec(); |
291 |
call_callback_3(cb_privmsg, source, av[0], av[1]); |
292 |
stop = time_msec(); |
293 |
if (stop > start && u && !is_oper(u)) |
294 |
ignore_update(u, stop-start); |
295 |
} |
296 |
|
297 |
/*************************************************************************/ |
298 |
|
299 |
static void m_quit(char *source, int ac, char **av) |
300 |
{ |
301 |
if (!*source) { |
302 |
log("Source missing from QUIT message"); |
303 |
return; |
304 |
} else if (ac != 1) { |
305 |
return; |
306 |
} |
307 |
do_quit(source, ac, av); |
308 |
} |
309 |
|
310 |
/*************************************************************************/ |
311 |
|
312 |
static void m_server(char *source, int ac, char **av) |
313 |
{ |
314 |
do_server(source, ac, av); |
315 |
} |
316 |
|
317 |
/*************************************************************************/ |
318 |
|
319 |
static void m_squit(char *source, int ac, char **av) |
320 |
{ |
321 |
do_squit(source, ac, av); |
322 |
} |
323 |
|
324 |
/*************************************************************************/ |
325 |
|
326 |
static void m_stats(char *source, int ac, char **av) |
327 |
{ |
328 |
if (!*source) { |
329 |
log("Source missing from STATS message"); |
330 |
return; |
331 |
} else if (ac < 1) { |
332 |
return; |
333 |
} |
334 |
|
335 |
switch (*av[0]) { |
336 |
case 'u': { |
337 |
int uptime = time(NULL) - start_time; |
338 |
Module *module_operserv; |
339 |
typeof(get_operserv_data) *p_get_operserv_data; |
340 |
int32 maxusercnt; |
341 |
|
342 |
send_cmd(NULL, "242 %s :Services up %d day%s, %02d:%02d:%02d", |
343 |
source, uptime/86400, (uptime/86400 == 1) ? "" : "s", |
344 |
(uptime/3600) % 24, (uptime/60) % 60, uptime % 60); |
345 |
if ((module_operserv = find_module("operserv/main")) != NULL |
346 |
&& (p_get_operserv_data = |
347 |
get_module_symbol(module_operserv, "get_operserv_data")) |
348 |
&& p_get_operserv_data(OSDATA_MAXUSERCNT, &maxusercnt) |
349 |
) { |
350 |
send_cmd(NULL, "250 %s :Current users: %d (%d ops); maximum %d", |
351 |
source, usercnt, opcnt, maxusercnt); |
352 |
} else { |
353 |
send_cmd(NULL, "250 %s :Current users: %d (%d ops)", |
354 |
source, usercnt, opcnt); |
355 |
} |
356 |
send_cmd(NULL, "219 %s u :End of /STATS report.", source); |
357 |
break; |
358 |
} /* case 'u' */ |
359 |
|
360 |
case 'l': { |
361 |
uint64 read, written; |
362 |
sock_rwstat(servsock, &read, &written); |
363 |
send_cmd(NULL, "211 %s Server SendBuf SentBytes SentMsgs RecvBuf " |
364 |
"RecvBytes RecvMsgs ConnTime", source); |
365 |
#if SIZEOF_LONG >= 8 |
366 |
send_cmd(NULL, "211 %s %s %u %lu %d %u %lu %d %ld", |
367 |
source, RemoteServer, |
368 |
read_buffer_len(servsock), (unsigned long)read, -1, |
369 |
write_buffer_len(servsock), (unsigned long)written, -1, |
370 |
(long)start_time); |
371 |
#else // assume long long is available |
372 |
send_cmd(NULL, "211 %s %s %u %llu %d %u %llu %d %ld", |
373 |
source, RemoteServer, |
374 |
read_buffer_len(servsock), (unsigned long long)read, -1, |
375 |
write_buffer_len(servsock), (unsigned long long)written, -1, |
376 |
(long)start_time); |
377 |
#endif |
378 |
send_cmd(NULL, "219 %s l :End of /STATS report.", source); |
379 |
break; |
380 |
} |
381 |
|
382 |
case 'c': |
383 |
case 'h': |
384 |
case 'i': |
385 |
case 'k': |
386 |
case 'm': |
387 |
case 'o': |
388 |
case 'y': |
389 |
send_cmd(NULL, "219 %s %c :/STATS %c not applicable or not supported.", |
390 |
source, *av[0], *av[0]); |
391 |
break; |
392 |
} |
393 |
} |
394 |
|
395 |
/*************************************************************************/ |
396 |
|
397 |
static void m_time(char *source, int ac, char **av) |
398 |
{ |
399 |
time_t t; |
400 |
struct tm *tm; |
401 |
char buf[64]; |
402 |
|
403 |
if (!*source) { |
404 |
log("Source missing from TIME message"); |
405 |
return; |
406 |
} |
407 |
|
408 |
time(&t); |
409 |
tm = localtime(&t); |
410 |
strftime(buf, sizeof(buf), "%a %b %d %H:%M:%S %Y %Z", tm); |
411 |
send_cmd(NULL, "391 %s %s :%s", source, ServerName, buf); |
412 |
} |
413 |
|
414 |
/*************************************************************************/ |
415 |
|
416 |
static void m_topic(char *source, int ac, char **av) |
417 |
{ |
418 |
if (ac != 4) |
419 |
return; |
420 |
do_topic(source, ac, av); |
421 |
} |
422 |
|
423 |
/*************************************************************************/ |
424 |
|
425 |
static void m_version(char *source, int ac, char **av) |
426 |
{ |
427 |
if (!*source) { |
428 |
log("Source missing from VERSION message"); |
429 |
return; |
430 |
} |
431 |
send_cmd(ServerName, "351 %s %s-%s %s :%s", source, |
432 |
program_name, version_number, ServerName, version_build); |
433 |
} |
434 |
|
435 |
/*************************************************************************/ |
436 |
|
437 |
static void m_whois(char *source, int ac, char **av) |
438 |
{ |
439 |
if (!*source) { |
440 |
log("Source missing from WHOIS message"); |
441 |
return; |
442 |
} else if (ac < 1) { |
443 |
return; |
444 |
} |
445 |
|
446 |
if (call_callback_3(cb_whois, source, av[0], |
447 |
ac>1 ? av[1] : NULL) <= 0 |
448 |
) { |
449 |
send_cmd(ServerName, "401 %s %s :No such service.", source, av[0]); |
450 |
} |
451 |
} |
452 |
|
453 |
/*************************************************************************/ |
454 |
|
455 |
/* Basic messages (defined above). Note that NICK and USER are left to the |
456 |
* protocol modules, since their usage varies widely between protocols. */ |
457 |
|
458 |
static Message base_messages[] = { |
459 |
|
460 |
{ "401", NULL }, |
461 |
{ "436", m_nickcoll }, |
462 |
{ "AWAY", NULL }, |
463 |
{ "INFO", m_info }, |
464 |
{ "JOIN", m_join }, |
465 |
{ "KICK", m_kick }, |
466 |
{ "KILL", m_kill }, |
467 |
{ "MODE", m_mode }, |
468 |
{ "MOTD", m_motd }, |
469 |
{ "NOTICE", NULL }, |
470 |
{ "PART", m_part }, |
471 |
{ "PASS", NULL }, |
472 |
{ "PING", m_ping }, |
473 |
{ "PONG", NULL }, |
474 |
{ "PRIVMSG", m_privmsg }, |
475 |
{ "QUIT", m_quit }, |
476 |
{ "SERVER", m_server }, |
477 |
{ "SQUIT", m_squit }, |
478 |
{ "STATS", m_stats }, |
479 |
{ "TIME", m_time }, |
480 |
{ "TOPIC", m_topic }, |
481 |
{ "VERSION", m_version }, |
482 |
{ "WALLOPS", NULL }, |
483 |
{ "WHOIS", m_whois }, |
484 |
|
485 |
{ NULL } |
486 |
|
487 |
}; |
488 |
|
489 |
/*************************************************************************/ |
490 |
/******************** Message registration and lookup ********************/ |
491 |
/*************************************************************************/ |
492 |
|
493 |
/* Structure to link tables together */ |
494 |
typedef struct messagetable_ MessageTable; |
495 |
struct messagetable_ { |
496 |
MessageTable *next, *prev; |
497 |
Message *table; |
498 |
}; |
499 |
static MessageTable *msgtable = NULL; |
500 |
|
501 |
/* List of known messages (for speed-lookup list) */ |
502 |
typedef struct messagenode_ MessageNode; |
503 |
struct messagenode_ { |
504 |
MessageNode *next, *prev; |
505 |
Message *msg; |
506 |
}; |
507 |
static MessageNode *msglist = NULL; |
508 |
|
509 |
/*************************************************************************/ |
510 |
/*************************************************************************/ |
511 |
|
512 |
/* (Re)generate the speed-lookup list. This list is used to reduce the |
513 |
* time spent searching for messages; every time a message is seen, its |
514 |
* entry in this list is moved one place closer to the head of the list, |
515 |
* allowing frequently-seen messages to "percolate" to the top of the list |
516 |
* so that they will be found more quickly by searches. |
517 |
*/ |
518 |
|
519 |
static void init_message_list(void) |
520 |
{ |
521 |
MessageNode *mn, *mn2; |
522 |
MessageTable *mt; |
523 |
Message *m; |
524 |
|
525 |
LIST_FOREACH_SAFE(mn, msglist, mn2) |
526 |
free(mn); |
527 |
msglist = NULL; |
528 |
|
529 |
LIST_FOREACH (mt, msgtable) { |
530 |
for (m = mt->table; m->name; m++) { |
531 |
LIST_SEARCH(msglist, msg->name, m->name, stricmp, mn); |
532 |
if (!mn) { |
533 |
mn = smalloc(sizeof(*mn)); |
534 |
mn->msg = m; |
535 |
LIST_INSERT(mn, msglist); |
536 |
} |
537 |
} |
538 |
} |
539 |
} |
540 |
|
541 |
/*************************************************************************/ |
542 |
|
543 |
/* Register the given table of messages. Returns 1 on success, 0 on |
544 |
* failure (`table' == NULL, `table' already registered, or out of memory). |
545 |
*/ |
546 |
|
547 |
int register_messages(Message *table) |
548 |
{ |
549 |
MessageTable *mt; |
550 |
|
551 |
if (!table) |
552 |
return 0; |
553 |
LIST_SEARCH_SCALAR(msgtable, table, table, mt); |
554 |
if (mt) /* if it's already on the list, abort */ |
555 |
return 0; |
556 |
mt = malloc(sizeof(*mt)); |
557 |
if (!mt) /* out of memory */ |
558 |
return 0; |
559 |
mt->table = table; |
560 |
LIST_INSERT(mt, msgtable); |
561 |
init_message_list(); |
562 |
return 1; |
563 |
} |
564 |
|
565 |
/*************************************************************************/ |
566 |
|
567 |
/* Unregister the given table of messages. Returns 1 on success, 0 on |
568 |
* failure (`table' not registered). |
569 |
*/ |
570 |
|
571 |
int unregister_messages(Message *table) |
572 |
{ |
573 |
MessageTable *mt; |
574 |
|
575 |
LIST_SEARCH_SCALAR(msgtable, table, table, mt); |
576 |
if (!mt) |
577 |
return 0; |
578 |
LIST_REMOVE(mt, msgtable); |
579 |
free(mt); |
580 |
init_message_list(); |
581 |
return 1; |
582 |
} |
583 |
|
584 |
/*************************************************************************/ |
585 |
|
586 |
/* Return the Message structure for the given message name, or NULL if none |
587 |
* exists. If there are multiple tables with entries for the message, |
588 |
* returns the entry in the most recently registered table. |
589 |
*/ |
590 |
|
591 |
Message *find_message(const char *name) |
592 |
{ |
593 |
MessageNode *mn; |
594 |
|
595 |
LIST_SEARCH(msglist, msg->name, name, stricmp, mn); |
596 |
if (mn) { |
597 |
MessageNode *prev = mn->prev; |
598 |
if (prev) { |
599 |
MessageNode *pprev = prev->prev; |
600 |
MessageNode *next = mn->next; |
601 |
/* Current order: pprev -> prev -> mn -> next */ |
602 |
/* New order: pprev -> mn -> prev -> next */ |
603 |
if (pprev) |
604 |
pprev->next = mn; |
605 |
else |
606 |
msglist = mn; |
607 |
mn->prev = pprev; |
608 |
mn->next = prev; |
609 |
prev->prev = mn; |
610 |
prev->next = next; |
611 |
if (next) |
612 |
next->prev = prev; |
613 |
} |
614 |
return mn->msg; |
615 |
} |
616 |
return NULL; |
617 |
} |
618 |
|
619 |
/*************************************************************************/ |
620 |
/************************ Initialization/cleanup *************************/ |
621 |
/*************************************************************************/ |
622 |
|
623 |
int messages_init(int ac, char **av) |
624 |
{ |
625 |
if (!register_messages(base_messages)) { |
626 |
log("messages_init: Unable to register base messages\n"); |
627 |
return 0; |
628 |
} |
629 |
cb_privmsg = register_callback("m_privmsg"); |
630 |
cb_whois = register_callback("m_whois"); |
631 |
if (cb_privmsg < 0 || cb_whois < 0) { |
632 |
log("messages_init: register_callback() failed\n"); |
633 |
return 0; |
634 |
} |
635 |
return 1; |
636 |
} |
637 |
|
638 |
/*************************************************************************/ |
639 |
|
640 |
void messages_cleanup(void) |
641 |
{ |
642 |
unregister_callback(cb_whois); |
643 |
unregister_callback(cb_privmsg); |
644 |
unregister_messages(base_messages); |
645 |
} |
646 |
|
647 |
/*************************************************************************/ |
648 |
|
649 |
/* |
650 |
* Local variables: |
651 |
* c-file-style: "stroustrup" |
652 |
* c-file-offsets: ((case-label . *) (statement-case-intro . *)) |
653 |
* indent-tabs-mode: nil |
654 |
* End: |
655 |
* |
656 |
* vim: expandtab shiftwidth=4: |
657 |
*/ |