]> jfr.im git - irc/quakenet/newserv.git/blame - qabot/qabot.c
Merge default.
[irc/quakenet/newserv.git] / qabot / qabot.c
CommitLineData
0253ea49 1#include <stdio.h>
2#include <stdlib.h>
3#include <stdarg.h>
0253ea49 4#include <time.h>
5
6#include "../nick/nick.h"
7#include "../localuser/localuserchannel.h"
8#include "../core/hooks.h"
9#include "../core/schedule.h"
10#include "../lib/array.h"
11#include "../lib/base64.h"
12#include "../lib/irc_string.h"
13#include "../lib/splitline.h"
df3bf970 14#include "../lib/version.h"
0253ea49 15
16#include "qabot.h"
17
ee746395
P
18MODULE_VERSION("");
19
0253ea49 20time_t qab_startime;
21int qabot_nickext;
22int qabot_spam_nickext;
23int qabot_chanext;
24nick* qabot_nick = 0;
25CommandTree* qabot_commands;
26CommandTree* qabot_chancommands;
27qab_bot* qab_bots = 0;
28unsigned long qab_lastq_crc = 0;
29int qab_lastq_count = 0;
30qab_bot* qabot_currentbot = 0;
31channel* qabot_currentchan = 0;
32
33void _init() {
34 qab_startime = time(0);
35
36 registerhook(HOOK_NICK_LOSTNICK, qabot_lostnick);
37 registerhook(HOOK_CHANNEL_PART, qabot_channel_part);
38
39 qabot_commands = newcommandtree();
40 addcommandtotree(qabot_commands, "showcommands", 0, 0, qabot_doshowcommands);
41 addcommandtotree(qabot_commands, "help", QAUFLAG_STAFF, 1, qabot_dohelp);
42 addcommandtotree(qabot_commands, "hello", 0, 0, qabot_dohello);
43 addcommandtotree(qabot_commands, "save", QAUFLAG_ADMIN, 0, qabot_dosave);
44 addcommandtotree(qabot_commands, "listbots", QAUFLAG_STAFF, 0, qabot_dolistbots);
45 addcommandtotree(qabot_commands, "listusers", QAUFLAG_STAFF, 0, qabot_dolistusers);
46 addcommandtotree(qabot_commands, "showbot", QAUFLAG_STAFF, 1, qabot_doshowbot);
47 addcommandtotree(qabot_commands, "addbot", QAUFLAG_STAFF, 6, qabot_doaddbot);
48 addcommandtotree(qabot_commands, "delbot", QAUFLAG_STAFF, 1, qabot_dodelbot);
49 addcommandtotree(qabot_commands, "adduser", QAUFLAG_ADMIN, 2, qabot_doadduser);
50 addcommandtotree(qabot_commands, "changelev", QAUFLAG_ADMIN, 2, qabot_dochangelev);
51 addcommandtotree(qabot_commands, "deluser", QAUFLAG_ADMIN, 1, qabot_dodeluser);
52 addcommandtotree(qabot_commands, "whois", QAUFLAG_STAFF, 1, qabot_dowhois);
53 addcommandtotree(qabot_commands, "status", QAUFLAG_DEVELOPER, 0, qabot_dostatus);
54
55 qabot_chancommands = newcommandtree();
56 addcommandtotree(qabot_chancommands, "answer", QAC_QUESTIONCHAN|QAC_STAFFCHAN, 2, qabot_dochananswer);
57 addcommandtotree(qabot_chancommands, "block", QAC_QUESTIONCHAN|QAC_STAFFCHAN, 2, qabot_dochanblock);
58 addcommandtotree(qabot_chancommands, "clear", QAC_STAFFCHAN, 0, qabot_dochanclear);
59 addcommandtotree(qabot_chancommands, "closechan", QAC_QUESTIONCHAN|QAC_STAFFCHAN, 0, qabot_dochanclosechan);
60 addcommandtotree(qabot_chancommands, "config", QAC_QUESTIONCHAN|QAC_STAFFCHAN, 2, qabot_dochanconfig);
61 addcommandtotree(qabot_chancommands, "help", QAC_QUESTIONCHAN|QAC_STAFFCHAN, 1, qabot_dochanhelp);
62 addcommandtotree(qabot_chancommands, "listblocks", QAC_QUESTIONCHAN|QAC_STAFFCHAN, 0, qabot_dochanlistblocks);
63 addcommandtotree(qabot_chancommands, "mic", QAC_STAFFCHAN, 0, qabot_dochanmic);
64 addcommandtotree(qabot_chancommands, "moo", QAC_QUESTIONCHAN|QAC_STAFFCHAN, 0, qabot_dochanmoo);
65 addcommandtotree(qabot_chancommands, "offtopic", QAC_QUESTIONCHAN|QAC_STAFFCHAN, 30, qabot_dochanofftopic);
66 addcommandtotree(qabot_chancommands, "openchan", QAC_QUESTIONCHAN|QAC_STAFFCHAN, 0, qabot_dochanopenchan);
67 addcommandtotree(qabot_chancommands, "ping", QAC_QUESTIONCHAN|QAC_STAFFCHAN, 0, qabot_dochanping);
68 addcommandtotree(qabot_chancommands, "reset", QAC_QUESTIONCHAN|QAC_STAFFCHAN, 1, qabot_dochanreset);
69 addcommandtotree(qabot_chancommands, "spam", QAC_QUESTIONCHAN|QAC_STAFFCHAN, 30, qabot_dochanspam);
70 addcommandtotree(qabot_chancommands, "status", QAC_QUESTIONCHAN|QAC_STAFFCHAN, 0, qabot_dochanstatus);
71 addcommandtotree(qabot_chancommands, "unblock", QAC_QUESTIONCHAN|QAC_STAFFCHAN, 2, qabot_dochanunblock);
79590bee
P
72 addcommandtotree(qabot_chancommands, "record", QAC_STAFFCHAN, 1, qabot_dochanrecord);
73 addcommandtotree(qabot_chancommands, "play", QAC_STAFFCHAN, 1, qabot_dochanplay);
74 addcommandtotree(qabot_chancommands, "continue", QAC_STAFFCHAN, 0, qabot_dochancontinue);
75 addcommandtotree(qabot_chancommands, "stop", QAC_STAFFCHAN, 0, qabot_dochanstop);
76 addcommandtotree(qabot_chancommands, "delete", QAC_STAFFCHAN, 1, qabot_dochandelete);
77 addcommandtotree(qabot_chancommands, "list", QAC_STAFFCHAN, 0, qabot_dochanlist);
0253ea49 78
79 if ((qabot_nickext = registernickext("QABOT")) == -1) {
80 return;
81 }
82
83 if ((qabot_spam_nickext = registernickext("QABOT_SPAM")) == -1) {
84 return;
85 }
86
87 qabot_loaddb();
88
89 scheduleoneshot(time(NULL) + 1, &qabot_createfakeuser, NULL);
90 scheduleoneshot(time(NULL) + QABOT_SAVEWAIT, &qabot_savetimer, NULL);
91}
92
93void _fini() {
94 deregisterhook(HOOK_NICK_LOSTNICK, qabot_lostnick);
95 deregisterhook(HOOK_CHANNEL_PART, qabot_channel_part);
96
97 deleteschedule(0, qabot_savetimer, NULL);
98
99 qabot_savedb();
100
101 while (qab_bots)
102 qabot_delbot(qab_bots);
103
104 if (qabot_nickext != -1)
105 releasenickext(qabot_nickext);
106
107 if (qabot_spam_nickext != -1)
108 releasenickext(qabot_spam_nickext);
109
110 if (qabot_nick) {
111 deregisterlocaluser(qabot_nick, "Module unloaded.");
112 qabot_nick = NULL;
113 }
114
115 while (qabot_users)
116 qabot_squelchuser(qabot_users);
117
118 deletecommandfromtree(qabot_commands, "showcommands", qabot_doshowcommands);
119 deletecommandfromtree(qabot_commands, "help", qabot_dohelp);
120 deletecommandfromtree(qabot_commands, "hello", qabot_dohello);
121 deletecommandfromtree(qabot_commands, "save", qabot_dosave);
122 deletecommandfromtree(qabot_commands, "listbots", qabot_dolistbots);
123 deletecommandfromtree(qabot_commands, "listusers", qabot_dolistusers);
124 deletecommandfromtree(qabot_commands, "showbot", qabot_doshowbot);
125 deletecommandfromtree(qabot_commands, "addbot", qabot_doaddbot);
126 deletecommandfromtree(qabot_commands, "delbot", qabot_dodelbot);
127 deletecommandfromtree(qabot_commands, "adduser", qabot_doadduser);
128 deletecommandfromtree(qabot_commands, "changelev", qabot_dochangelev);
129 deletecommandfromtree(qabot_commands, "deluser", qabot_dodeluser);
130 deletecommandfromtree(qabot_commands, "whois", qabot_dowhois);
131 deletecommandfromtree(qabot_commands, "status", qabot_dostatus);
132 destroycommandtree(qabot_commands);
133
134 deletecommandfromtree(qabot_chancommands, "answer", qabot_dochananswer);
135 deletecommandfromtree(qabot_chancommands, "block", qabot_dochanblock);
136 deletecommandfromtree(qabot_chancommands, "clear", qabot_dochanclear);
137 deletecommandfromtree(qabot_chancommands, "closechan", qabot_dochanclosechan);
138 deletecommandfromtree(qabot_chancommands, "config", qabot_dochanconfig);
139 deletecommandfromtree(qabot_chancommands, "help", qabot_dochanhelp);
140 deletecommandfromtree(qabot_chancommands, "listblocks", qabot_dochanlistblocks);
141 deletecommandfromtree(qabot_chancommands, "mic", qabot_dochanmic);
142 deletecommandfromtree(qabot_chancommands, "moo", qabot_dochanmoo);
143 deletecommandfromtree(qabot_chancommands, "offtopic", qabot_dochanofftopic);
144 deletecommandfromtree(qabot_chancommands, "openchan", qabot_dochanopenchan);
145 deletecommandfromtree(qabot_chancommands, "ping", qabot_dochanping);
146 deletecommandfromtree(qabot_chancommands, "reset", qabot_dochanreset);
147 deletecommandfromtree(qabot_chancommands, "spam", qabot_dochanspam);
148 deletecommandfromtree(qabot_chancommands, "status", qabot_dochanstatus);
149 deletecommandfromtree(qabot_chancommands, "unblock", qabot_dochanunblock);
79590bee
P
150 deletecommandfromtree(qabot_chancommands, "record", qabot_dochanrecord);
151 deletecommandfromtree(qabot_chancommands, "play", qabot_dochanplay);
152 deletecommandfromtree(qabot_chancommands, "continue", qabot_dochancontinue);
153 deletecommandfromtree(qabot_chancommands, "stop", qabot_dochanstop);
154 deletecommandfromtree(qabot_chancommands, "delete", qabot_dochandelete);
155 deletecommandfromtree(qabot_chancommands, "list", qabot_dochanlist);
0253ea49 156 destroycommandtree(qabot_chancommands);
157}
158
159void qabot_lostnick(int hooknum, void* arg) {
160 nick* np = (nick*)arg;
161 qab_bot* b;
162
163 if (!qab_bots)
164 return;
165
166 if (!IsAccount(np))
167 return;
168
169 for (b = qab_bots; b; b = b->next) {
170 if (b->micnumeric == np->numeric) {
171 b->micnumeric = 0;
172 sendmessagetochannel(b->np, b->staff_chan->channel, "Mic deactivated.");
173 if (!b->lastspam)
174 qabot_spamstored((void*)b);
175 }
79590bee
P
176
177 if (b->recnumeric == np->numeric) {
178 b->recnumeric = 0;
179
180 if (b->recfile) {
181 fclose(b->recfile);
182 b->recfile = NULL;
183 }
184
185 sendmessagetochannel(b->np, b->staff_chan->channel, "Recorder deactivated.");
186 }
0253ea49 187 }
188}
189
190void qabot_channel_part(int hooknum, void* arg) {
191 channel* cp = (channel*)((void**)arg)[0];
192 nick* np = (nick*)((void**)arg)[1];
193 qab_bot* b;
194
195 if (!qab_bots)
196 return;
197
198 if (!IsAccount(np))
199 return;
200
201 for (b = qab_bots; b; b = b->next) {
202 if ((b->micnumeric == np->numeric) && (b->staff_chan->channel == cp)) {
203 b->micnumeric = 0;
204 sendmessagetochannel(b->np, b->staff_chan->channel, "Mic deactivated.");
205 if (!b->lastspam)
206 qabot_spamstored((void*)b);
207 }
79590bee
P
208
209 if (b->recnumeric == np->numeric) {
210 b->recnumeric = 0;
211
212 if (b->recfile) {
213 fclose(b->recfile);
214 b->recfile = NULL;
215 }
216
217 sendmessagetochannel(b->np, b->staff_chan->channel, "Recorder deactivated.");
218 }
0253ea49 219 }
220}
221
222void qabot_createfakeuser(void* arg) {
223 channel* cp;
224
225 if ((qabot_nick = registerlocaluser(QABOT_NICK, QABOT_USER, QABOT_HOST,
226 QABOT_NAME, QABOT_ACCT, QABOT_UMDE, &qabot_handler))) {
227 if ((cp = findchannel(QABOT_HOMECHAN))) {
228 localjoinchannel(qabot_nick, cp);
229 localgetops(qabot_nick, cp);
230 }
231 else
232 localcreatechannel(qabot_nick, QABOT_HOMECHAN);
233 }
234}
235
236void qabot_handler(nick* me, int type, void** args) {
237 nick* sender;
238 Command* cmd;
239 channel* cp;
240 char* cargv[50];
241 int cargc;
242 qab_user* u;
243
244 switch (type) {
245 case LU_PRIVMSG:
246 case LU_SECUREMSG:
247 sender = (nick*)args[0];
248
249 if (!strncmp("\001VERSION", args[1], 8)) {
250 sendnoticetouser(qabot_nick, sender, "\001VERSION %s\001", QABOT_NAME);
251 return;
252 }
253
254 cargc = splitline((char*)args[1], cargv, 50, 0);
255 cmd = findcommandintree(qabot_commands, cargv[0], 1);
256 if (!cmd) {
257 sendnoticetouser(qabot_nick, sender, "Unknown command.");
258 return;
259 }
260
261 if (!IsAccount(sender)) {
262 sendnoticetouser(qabot_nick, sender, "You must be authed to use this command.");
263 return;
264 }
265
266 u = qabot_getuser(sender->authname);
267 if (cmd->level) {
268 if (!u) {
269 sendnoticetouser(qabot_nick, sender, "You need an account to use this command.");
270 return;
271 }
272
273 if ((cmd->level & QAUFLAG_STAFF) && !QAIsStaff(u)) {
274 sendnoticetouser(qabot_nick, sender, "You do not have access to this command.");
275 return;
276 }
277
278 if ((cmd->level & QAUFLAG_ADMIN) && !QAIsAdmin(u)) {
279 sendnoticetouser(qabot_nick, sender, "You do not have access to this command.");
280 return;
281 }
282
283 if ((cmd->level & QAUFLAG_DEVELOPER) && !QAIsDeveloper(u)) {
284 sendnoticetouser(qabot_nick, sender, "You do not have access to this command.");
285 return;
286 }
287 }
288
289 if (cmd->maxparams < (cargc-1)) {
290 rejoinline(cargv[cmd->maxparams], cargc - (cmd->maxparams));
291 cargc=(cmd->maxparams) + 1;
292 }
293
294 (cmd->handler)((void*)sender, cargc - 1, &(cargv[1]));
295
296 break;
297
298 case LU_KILLED:
299 qabot_nick = NULL;
300 scheduleoneshot(time(NULL) + 1, &qabot_createfakeuser, NULL);
301 break;
302
303 case LU_KICKED:
304 cp = (channel*)args[1];
305 if (cp) {
306 localjoinchannel(qabot_nick, cp);
307 localgetops(qabot_nick, cp);
308 }
309 break;
310 }
311}
312
313void qabot_child_handler(nick* me, int type, void** args) {
314 nick* sender;
315 channel* cp;
316 qab_bot* bot = me->exts[qabot_nickext];
317 char* text;
318 Command* cmd;
319 char* cargv[50];
320 int cargc;
321
322 if (!bot) {
323 return;
324 }
325
326 switch (type) {
327 case LU_CHANMSG:
328 sender = (nick*)args[0];
329 cp = (channel*)args[1];
330 text = (char*)args[2];
331
332 if (*text == '!') {
333 if (*(++text) == '\0') {
334 sendnoticetouser(me, sender, "No command specified.");
335 return;
336 }
337 cargc = splitline((char*)text, cargv, 50, 0);
338 cmd = findcommandintree(qabot_chancommands, cargv[0], 1);
339 if (!cmd) {
340 sendnoticetouser(me, sender, "Unknown command.");
341 return;
342 }
343
344 if ((cp->index == bot->staff_chan) && !(cmd->level & QAC_STAFFCHAN)) {
345 sendnoticetouser(me, sender, "This command cannot be used in the staff channel.");
346 return;
347 }
348
349 if ((cp->index == bot->question_chan) && !(cmd->level & QAC_QUESTIONCHAN)) {
350 sendnoticetouser(me, sender, "This command cannot be used in the question channel.");
351 return;
352 }
353
354 if (cmd->maxparams < (cargc-1)) {
355 rejoinline(cargv[cmd->maxparams], cargc - (cmd->maxparams));
356 cargc=(cmd->maxparams) + 1;
357 }
358
359 qabot_currentbot = bot;
360 qabot_currentchan = cp;
361
362 (cmd->handler)((void*)sender, cargc - 1, &(cargv[1]));
363 }
364 else if ((*text != '!') && (bot->micnumeric == sender->numeric) && (cp->index == bot->staff_chan)) {
365 bot->lastmic = time(NULL);
366 if (bot->lastspam) {
367 qab_spam* s;
368
369 s = (qab_spam*)malloc(sizeof(qab_spam));
370 s->message = strdup(text);
371 s->next = 0;
372 bot->lastspam->next = s;
373 bot->lastspam = s;
374 }
375 else {
376 if ((bot->spamtime + bot->spam_interval) < time(0)) {
377 sendmessagetochannel(me, bot->public_chan->channel, "%s", text);
378 bot->spammed++;
379 bot->spamtime = time(0);
380 }
381 else {
382 qab_spam* s;
383
384 s = (qab_spam*)malloc(sizeof(qab_spam));
385 s->message = strdup(text);
386 s->next = 0;
387 bot->nextspam = bot->lastspam = s;
388 scheduleoneshot(bot->spamtime + bot->spam_interval, qabot_spam, (void*)bot);
389 }
390 }
391 }
79590bee
P
392 else if ((*text != '!') && (bot->recnumeric == sender->numeric) && (cp->index == bot->staff_chan)) {
393 if (bot->recfile) {
394 fprintf(bot->recfile, "%s\n", text);
395 }
396 }
0253ea49 397 break;
398
399 case LU_PRIVMSG:
400 case LU_SECUREMSG:
401 sender = (nick*)args[0];
402 text = (char*)args[1];
403 time_t lastmsg;
404
405 lastmsg = (time_t)sender->exts[qabot_spam_nickext];
406
407 if ((lastmsg + bot->ask_wait) > time(0)) {
408 sendnoticetouser(me, sender, "You have already sent a question recently, please wait at least %d seconds between asking questions.", bot->ask_wait);
409 return;
410 }
411 else {
412 char hostbuf[NICKLEN + USERLEN + HOSTLEN + 3];
413 qab_block* b;
414 qab_question* q;
f011b15a 415 unsigned long crc = 0;
0253ea49 416 int len;
417
418 sendnoticetouser(me, sender, "Your question has been relayed to the %s staff.", bot->public_chan->name->content);
419
420 if (!getnumerichandlefromchanhash(bot->public_chan->channel->users, sender->numeric))
421 return;
422
423 if (*text == '<')
424 text++;
425
426 len = strlen(text);
427 if ((len > 0) && (text[len - 1] == '>')) {
428 text[len - 1] = '\0';
429 len--;
430 }
431
432 if ((len < 5) || !strchr(text, ' ') || (*text == 1))
433 return;
434
435 if ((bot->flags & QAB_AUTHEDONLY) && !IsAccount(sender))
436 return;
437
438 if (bot->flags & QAB_CONTROLCHAR) {
439 char* ch;
440
441 for (ch = text; *ch; ch++)
442 if ((*ch == 2) || (*ch == 3) || (*ch == 22) || (*ch == 27) || (*ch == 31))
443 return;
444 }
445 else if (bot->flags & QAB_COLOUR) {
446 char* ch;
447
448 for (ch = text; *ch; ch++)
449 if (*ch == 3)
450 return;
451 }
452
453 if (bot->flags & QAB_FLOODDETECT) {
454 crc = crc32i(text);
455 if (crc == qab_lastq_crc) {
456 qab_lastq_count++;
457 if (qab_lastq_count >= 3) {
458 if ((qab_lastq_count == 3) || ((qab_lastq_count % 10) == 0))
459 sendmessagetochannel(me, bot->question_chan->channel, "WARNING: Possible question flood detected%s", (bot->flags & QAB_FLOODSTOP) ? " - AUTO IGNORED." : ".");
460 if (bot->flags & QAB_FLOODSTOP)
461 return;
462 }
463 }
464 else {
465 qab_lastq_crc = crc;
466 qab_lastq_count = 1;
467 }
468 }
469
470 sprintf(hostbuf,"%s!%s@%s", sender->nick, sender->ident, sender->host->name->content);
471
472 for (b = bot->blocks; b; b = b->next) {
473 switch (b->type) {
474 case QABBLOCK_ACCOUNT:
475 if (IsAccount(sender) && !ircd_strncmp(sender->authname, b->blockstr, ACCOUNTLEN))
476 return;
477 break;
478
479 case QABBLOCK_HOST:
480 if (!match(b->blockstr, hostbuf))
481 return;
482 break;
483
484 case QABBLOCK_TEXT:
485 if (!match(b->blockstr, text))
486 return;
487 break;
488 }
489 }
490
491 sender->exts[qabot_spam_nickext] = (void*)time(0);
492
493 q = (qab_question*)malloc(sizeof(qab_question));
494 q->id = ++bot->lastquestionID;
495 q->question = strdup(text);
496 q->flags = QAQ_NEW;
497 strncpy(q->nick, sender->nick, NICKLEN);
498 q->nick[NICKLEN] = '\0';
499 q->numeric = sender->numeric;
500 q->crc = (bot->flags & QAB_FLOODDETECT) ? crc : 0;
501 q->answer = 0;
502 q->next = bot->questions[q->id % QUESTIONHASHSIZE];
503
504 bot->questions[q->id % QUESTIONHASHSIZE] = q;
505
506 sendmessagetochannel(me, bot->question_chan->channel, "ID: %3d <%s> %s", q->id, visiblehostmask(sender, hostbuf), text);
507
508 if ((bot->flags & QAB_LINEBREAK) && ((bot->lastquestionID % 10) == 0))
509 sendmessagetochannel(me, bot->question_chan->channel, "-----------------------------------");
510 }
511 break;
512
513 case LU_KILLED:
514 bot->np = NULL;
515 scheduleoneshot(time(NULL) + 1, &qabot_createbotfakeuser, (void*)bot);
516 break;
517
518 case LU_KICKED:
519 cp = (channel*)args[1];
520 if (cp) {
521 localjoinchannel(bot->np, cp);
522 localgetops(bot->np, cp);
523 }
524 break;
525
526 default:
527 break;
528 }
529}
530
531char* qabot_getvictim(nick* np, char* target) {
532 if (*target != '#') {
533 nick* victim;
534
535 if (!(victim = getnickbynick(target))) {
536 sendnoticetouser(qabot_nick, np, "No such nickname.");
537 return 0;
538 }
539
540 if (!IsAccount(victim)) {
541 sendnoticetouser(qabot_nick, np, "%s is not authed.", victim->nick);
542 return 0;
543 }
544
545 return victim->authname;
546 }
547
548 return target + 1;
549}
550
551const char* qabot_uflagtostr(flag_t flags) {
552 static char buf[20];
553 int i = 0;
554
555 buf[i++] = '+';
556
557 if (flags & QAUFLAG_ADMIN) { buf[i++] = 'a'; }
558 if (flags & QAUFLAG_DEVELOPER) { buf[i++] = 'd'; }
559 if (flags & QAUFLAG_STAFF) { buf[i++] = 's'; }
560
561 buf[i] = '\0';
562
563 return buf;
564}
565
566const char* qabot_formattime(time_t tme) {
567 static char buf[50];
568 struct tm* tmp;
569
570 tmp = gmtime(&tme);
571 strftime(buf, 15, "%d/%m/%y %H:%M", tmp);
572
573 return buf;
574}
575
576qab_bot* qabot_getcurrentbot() {
577 return qabot_currentbot;
578}
579
580channel* qabot_getcurrentchannel() {
581 return qabot_currentchan;
582}