]> gitweb.pimeys.fr Git - bots/deconnaisseur.git/blob - deconnaisseur.py
Les déconnaissances ton maintenant dans un seul fichier,
[bots/deconnaisseur.git] / deconnaisseur.py
1 #!/usr/bin/python
2 # -*- coding:utf8 -*-
3
4 # Codé par 20-100 le 23/04/12
5
6 # Un bot IRC qui sort des déconnaissances
7
8 import irclib
9 import ircbot
10 import threading
11 import random
12 import time
13 import pickle
14 import re
15
16 config_password="PatrickSébastien"
17 config_pseudo="deconnaisseur"
18 config_chanlist=["#bot","#flood"]
19 config_play_channels=["#flood"]
20 config_stay_channels=["#flood","#bot"]
21 config_overops=["[20-100]","[20-100]_","PEB"]
22 config_ops=["Nit","Eguel","Harry"]
23
24 config_source_file="deconnaissances.txt"
25 config_played_file_template="played.%s.txt" #il faut rajouter le nom du serveur
26 def get_config_played_file(serveur):
27 serveurs={"acoeur.crans.org":"acoeur","irc.crans.org":"crans"}
28 return config_played_file_template%(serveurs[serveur])
29 ttrig=120 #time trigger (normalement 120, mais diminué pour les tests)
30 Ttrig=600 #between two enigms
31 config_time_incompressible=15 #on peut pas retrigger en dessous de ce temps (60)
32 config_time_incompressible_clue=60 #on peut pas forcer la demande d'indice en dessous
33
34 config_score_file="scores.pickle"
35
36 config_tag_triggers=[u"t(|a)g",u"ta gueule",u"la ferme",u"ferme( |-)la",u"tais-toi",u"chut"]
37 config_tag_actions=[u"se tait",u"ferme sa gueule",u"se la ferme",u"la ferme"]
38 config_tag_answers=[u"J'me tais si j'veux !",
39 u"Je t'entends pas :°",
40 u"Héhé, try again",
41 u"Non, j'ai pas envie",
42 u"Peut-être quand toi tu la fermeras, et encore…"]
43
44 class UnicodeBotError(Exception):
45 pass
46 def bot_unicode(chain):
47 try:
48 unicode(chain,"utf8")
49 except UnicodeDecodeError:
50 raise UnicodeBotError
51
52 def log(serveur,channel="prout",auteur=None,message=None):
53 #f=open(config_logfile,"a")
54 #if auteur==message==None:
55 # chain=channel
56 #else:
57 # chain="%s [%s:%s] %s"%(time.strftime("%T"),channel,auteur,message)
58 #f.write(chain+"\n")
59 #print chain
60 #f.close()
61 a=0 # does nothing
62
63
64 def tolere(regexp):
65 """Renvoie une regexp plus tolérante"""
66 reg=unicode(regexp,"utf8").lower()
67 reg=reg.replace(u"á",u"(á|a)").replace(u"à",u"(à|a)").replace(u"â",u"(â|a)").replace(u"ä",u"(ä|a)")
68 reg=reg.replace(u"é",u"(é|e)").replace(u"è",u"(è|e)").replace(u"ê",u"(ê|e)").replace(u"ë",u"(ë|e)")
69 reg=reg.replace(u"í",u"(í|i)").replace(u"ì",u"(ì|i)").replace(u"î",u"(î|i)").replace(u"ï",u"(ï|i)")
70 reg=reg.replace(u"ó",u"(ó|o)").replace(u"ò",u"(ò|o)").replace(u"ô",u"(ô|o)").replace(u"ö",u"(ö|o)")
71 reg=reg.replace(u"ú",u"(ú|u)").replace(u"ù",u"(ù|u)").replace(u"û",u"(û|u)").replace(u"ü",u"(ü|u)")
72 reg=reg.replace(u"ý",u"(ý|y)").replace(u"ỳ",u"(ỳ|y)").replace(u"ŷ",u"(ŷ|y)").replace(u"ÿ",u"(ÿ|y)")
73 reg=reg.replace(u"œ",u"(œ|oe)").replace(u"æ",u"(æ|ae)")
74 return reg
75
76 def is_something(chain,matches,avant=u".*(?:^| )",apres=u"(?:$|\.| |,|;).*",case_sensitive=False,debug=False):
77 if case_sensitive:
78 chain=unicode(chain,"utf8")
79 else:
80 chain=unicode(chain,"utf8").lower()
81 allmatches="("+"|".join(matches)+")"
82 reg=(avant+allmatches+apres).lower()
83 o=re.match(reg,chain)
84 return o
85
86 def is_tag(chain):
87 return is_something(chain,config_tag_triggers)
88
89 class RefuseError(Exception):
90 pass
91
92 class Deconnaisseur(ircbot.SingleServerIRCBot):
93 def __init__(self,serveur,debug=False):
94 temporary_pseudo=config_pseudo+str(random.randrange(10000,100000))
95 ircbot.SingleServerIRCBot.__init__(self, [(serveur, 6667)],
96 temporary_pseudo,"Un bot irc.[flagellez 20-100, il le mérite]", 10)
97 self.debug=debug
98 self.serveur=serveur
99 self.overops=config_overops
100 self.ops=self.overops+config_ops
101 self.chanlist=config_chanlist
102 self.stay_channels=config_stay_channels
103 self.play_channels=config_play_channels
104 self.play_status={i:[0] for i in self.play_channels}
105 self.quiet_channels=[]
106
107 def give_me_my_pseudo(self,serv):
108 serv.privmsg("NickServ","RECOVER %s %s"%(config_pseudo,config_password))
109 serv.privmsg("NickServ","RELEASE %s %s"%(config_pseudo,config_password))
110 time.sleep(0.3)
111 serv.nick(config_pseudo)
112
113 def on_welcome(self, serv, ev):
114 self.give_me_my_pseudo(serv)
115 serv.privmsg("NickServ","identify %s"%(config_password))
116 log("Connected")
117 if self.debug:
118 self.chanlist=["#bot"]
119 self.play_channels=["#bot"]
120 for c in self.chanlist:
121 log("JOIN %s"%(c))
122 serv.join(c)
123 for c in self.play_channels:
124 token=time.time()-3600
125 self.play_status[c]=[0,token]
126 serv.execute_delayed(random.randrange(ttrig),self.start_enigme,(serv,c,token))
127
128 def start_enigme(self,serv,channel,token=None):
129 if self.play_status[channel][0]==0 and channel in self.play_channels:
130 ok="skip"
131 if token==self.play_status[channel][-1]:
132 ok="do_it"
133 if token==None:
134 if time.time() > self.play_status[channel][-1]+config_time_incompressible:
135 ok="do_it"
136 else:
137 ok="refuse"
138 if ok=="do_it":
139 enigme,indice,answer_reg,answer=self.get_enigme()
140 print "%s; %s; %s; %s"%(enigme, indice, answer_reg, answer)
141 serv.privmsg(channel,enigme)
142 token=time.time()
143 self.play_status[channel]=[1,enigme,indice,answer_reg,answer,token]
144 serv.execute_delayed(random.randrange(ttrig*3,ttrig*5),self.give_indice,(serv,channel,token))
145 elif ok=="refuse":
146 raise RefuseError
147 def give_indice(self,serv,channel,token):
148 if self.play_status[channel][0]==1:
149 if token==None:
150 # c'est donc que l'indice a été demandé
151 if self.play_status[channel][-1]+config_time_incompressible_clue<time.time():
152 token=self.play_status[channel][-1]
153 if self.play_status[channel][-1]==token:
154 indice=self.play_status[channel][2]
155 serv.privmsg(channel,"indice : %s"%(indice))
156 self.play_status[channel][0]=2
157 serv.execute_delayed(random.randrange(ttrig*1,ttrig*3),self.give_answer,(serv,channel,token))
158 def give_answer(self,serv,channel,token):
159 if self.play_status[channel][0]==2 and self.play_status[channel][-1]==token:
160 answer=self.play_status[channel][4]
161 serv.privmsg(channel,"C'était : %s"%(answer))
162 token=time.time()
163 self.play_status[channel]=[0,token]
164 serv.execute_delayed(random.randrange(Ttrig*5,Ttrig*10),self.start_enigme,(serv,channel,token))
165
166 def get_enigme(self):
167 # on récupère les déconnaissances
168 f=open(config_source_file)
169 t=f.read()
170 l=re.findall("%\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)\n",t)
171 dec={int(i[0]):list(i[1:]) for i in l if len(i)==5}
172 # on va chercher combien de fois elles ont été jouées
173 played_file=get_config_played_file(self.serveur)
174 f=open(played_file)
175 t=f.read()
176 l=re.findall("(.*):(.*)",t)
177 played={int(i[0]):int(i[1]) for i in l}
178 # on récupère le nombre d'occurrences le plus faible
179 mini=min(played.values())
180 # on choisit un id dans ceux qui ont ce nombre d'occurences
181 id_choisi=random.choice([k for k,v in played.items() if v==mini])
182 enigme,indice,answer_reg,answer=dec[id_choisi]
183 # on incrémente la choisie
184 played[id_choisi]+=1
185 # on enregistre le played_file
186 f=open(played_file,"w")
187 f.write("\n".join(["%-3s : %s"%(k,v) for k,v in played.items()]))
188 f.close()
189 return enigme,indice,answer_reg,answer
190
191 def pourmoi(self, serv, message):
192 pseudo=serv.get_nickname()
193 size=len(pseudo)
194 if message[:size]==pseudo and len(message)>size and message[size]==":":
195 return (True,message[size+1:].strip(" "))
196 else:
197 return (False,message)
198
199 def on_privmsg(self, serv, ev):
200 message=ev.arguments()[0]
201 auteur = irclib.nm_to_n(ev.source())
202 try:
203 test=bot_unicode(message)
204 except UnicodeBotError:
205 serv.privmsg(auteur,
206 "Euh, tu fais de la merde avec ton encodage là, j'ai failli crasher…")
207 return
208 message=message.split()
209 cmd=message[0].lower()
210 notunderstood=False
211 if cmd=="help":
212 helpmsg_default="""Liste des commandes :
213 HELP Affiche ce message d'aide
214 SCORE Affiche ton score (SCORE TRANSFERT <pseudo> [<n>] pour transférer des points)
215 SCORES Affiche les scores"""
216 helpmsg_ops="""
217 JOIN Faire rejoindre un channel (sans paramètres, donne la liste des chans actuels)
218 LEAVE Faire quitter un channel
219 PLAY Passe un channel en mode "jouer"
220 NOPLAY Passe un channel en mode "ne pas jouer"
221 QUIET Se taire sur un channel
222 NOQUIET Opposé de QUIET"""
223 helpmsg_overops="""
224 SCORES {DEL|ADD|SUB} Tu veux un dessin ?
225 SAY Fais envoyer un message sur un chan ou à une personne
226 STAY Ignorera les prochains LEAVE pour un chan
227 NOSTAY Opposé de STAY
228 STATUS Montre l'état courant
229 DIE Mourir"""
230 helpmsg=helpmsg_default
231 if auteur in self.ops:
232 helpmsg+=helpmsg_ops
233 if auteur in self.overops:
234 helpmsg+=helpmsg_overops
235 for ligne in helpmsg.split("\n"):
236 serv.privmsg(auteur,ligne)
237 elif cmd=="join":
238 if auteur in self.ops:
239 if len(message)>1:
240 if message[1] in self.chanlist:
241 serv.privmsg(auteur,"Je suis déjà sur %s"%(message[1]))
242 else:
243 serv.join(message[1])
244 self.chanlist.append(message[1])
245 serv.privmsg(auteur,"Channels : "+" ".join(self.chanlist))
246 log("priv",auteur," ".join(message))
247 else:
248 serv.privmsg(auteur,"Channels : "+" ".join(self.chanlist))
249 else:
250 notunderstood=True
251 elif cmd=="leave":
252 if auteur in self.ops and len(message)>1:
253 if message[1] in self.chanlist:
254 if not (message[1] in self.stay_channels) or auteur in self.overops:
255 serv.part(message[1])
256 self.chanlist.remove(message[1])
257 log("priv",auteur," ".join(message)+"[successful]")
258 else:
259 serv.privmsg(auteur,"Non, je reste !")
260 log("priv",auteur," ".join(message)+"[failed]")
261 else:
262 serv.privmsg(auteur,"Je ne suis pas sur %s"%(message[1]))
263 else:
264 notunderstood=True
265 elif cmd=="stay":
266 if auteur in self.overops:
267 if len(message)>1:
268 if message[1] in self.stay_channels:
269 serv.privmsg(auteur,"Je stay déjà sur %s."%(message[1]))
270 log("priv",auteur," ".join(message)+"[failed]")
271 else:
272 self.stay_channels.append(message[1])
273 serv.privmsg(auteur,"Stay channels : "+" ".join(self.stay_channels))
274 log("priv",auteur," ".join(message)+"[successful]")
275 else:
276 serv.privmsg(auteur,"Stay channels : "+" ".join(self.stay_channels))
277 else:
278 notunderstood=True
279 elif cmd=="nostay":
280 if auteur in self.overops:
281 if len(message)>1:
282 if message[1] in self.stay_channels:
283 self.stay_channels.remove(message[1])
284 serv.privmsg(auteur,"Stay channels : "+" ".join(self.stay_channels))
285 log("priv",auteur," ".join(message)+"[successful]")
286 else:
287 serv.privmsg(auteur,"Je ne stay pas sur %s."%(message[1]))
288 log("priv",auteur," ".join(message)+"[failed]")
289 else:
290 notunderstood=True
291 elif cmd=="play":
292 if auteur in self.ops:
293 if len(message)>1:
294 if message[1] in self.play_channels:
295 serv.privmsg(auteur,"Je play déjà sur %s."%(message[1]))
296 log("priv",auteur," ".join(message)+"[failed]")
297 else:
298 self.play_channels.append(message[1])
299 self.play_status[message[1]]=[0,time.time()-3600]
300 serv.privmsg(auteur,"Play channels : "+" ".join(self.play_channels))
301 log("priv",auteur," ".join(message)+"[successful]")
302 else:
303 serv.privmsg(auteur,"Play channels : "+" ".join(self.play_channels))
304 else:
305 notunderstood=True
306 elif cmd=="noplay":
307 if auteur in self.ops:
308 if len(message)>1:
309 if message[1] in self.play_channels:
310 self.play_channels.remove(message[1])
311 serv.privmsg(auteur,"Play channels : "+" ".join(self.play_channels))
312 log("priv",auteur," ".join(message)+"[successful]")
313 else:
314 serv.privmsg(auteur,"Je ne play pas sur %s."%(message[1]))
315 log("priv",auteur," ".join(message)+"[failed]")
316 else:
317 notunderstood=True
318 elif cmd=="quiet":
319 if auteur in self.ops:
320 if len(message)>1:
321 if message[1] in self.quiet_channels:
322 serv.privmsg(auteur,"Je me la ferme déjà sur %s"%(message[1]))
323 log(self.serveur,"priv",auteur," ".join(message)+"[failed]")
324 else:
325 self.quiet_channels.append(message[1])
326 serv.privmsg(auteur,"Quiet channels : "+" ".join(self.quiet_channels))
327 log(self.serveur,"priv",auteur," ".join(message)+"[successful]")
328 else:
329 serv.privmsg(auteur,"Quiet channels : "+" ".join(self.quiet_channels))
330 else:
331 notunderstood=True
332 elif cmd=="noquiet":
333 if auteur in self.ops:
334 if len(message)>1:
335 if message[1] in self.quiet_channels:
336 self.quiet_channels.remove(message[1])
337 serv.privmsg(auteur,"Quiet channels : "+" ".join(self.quiet_channels))
338 log(self.serveur,"priv",auteur," ".join(message)+"[successful]")
339 else:
340 serv.privmsg(auteur,"Je ne me la ferme pas sur %s."%(message[1]))
341 log(self.serveur,"priv",auteur," ".join(message)+"[failed]")
342 else:
343 notunderstood=True
344 elif cmd in ["states","status"]:
345 if auteur in self.overops:
346 for k in self.play_status.keys():
347 serv.privmsg(auteur,"%s : %s"%(k,"; ".join([str(i) for i in self.play_status[k]])))
348 elif cmd=="say":
349 if auteur in self.overops and len(message)>2:
350 serv.privmsg(message[1]," ".join(message[2:]))
351 log("priv",auteur," ".join(message))
352 elif len(message)<=2:
353 serv.privmsg(auteur,"Syntaxe : SAY <channel> <message>")
354 else:
355 notunderstood=True
356 elif cmd=="die":
357 if auteur in self.overops:
358 self.die()
359 elif cmd=="score":
360 if len(message)>1:
361 if len(message) in [3,4] and message[1].lower()=="transfert":
362 scores=self.get_scores()
363 de,to=auteur,message[2]
364 value=scores.get(de,0)
365 if len(message)==4:
366 try:
367 asked=int(message[3])
368 except ValueError:
369 serv.privmsg(auteur,"Syntaxe : SCORE TRANSFERT <pseudo> [<n>]")
370 return
371 else:
372 asked=value
373 if value==0:
374 serv.privmsg(auteur,"Vous n'avez pas de points")
375 return
376 elif asked<=0:
377 serv.privmsg(auteur,"Bien tenté…")
378 return
379 elif asked>value:
380 serv.privmsg(auteur,"Vous n'avez que %s points"%(value))
381 return
382 else:
383 self.add_score(de,-asked)
384 self.add_score(to,asked)
385 serv.privmsg(auteur,"Transfert de %s points de %s à %s"%(asked,de,to))
386 else:
387 serv.privmsg(auteur,"Syntaxe : SCORE TRANSFERT <pseudo> [<n>]")
388 else:
389 serv.privmsg(auteur,"Votre score : %s"%(self.get_scores().get(auteur,0)) )
390 elif cmd=="scores":
391 if len(message)==1:
392 scores=self.get_scores().items()
393 # trie par score
394 scores.sort(lambda x,y:cmp(x[1],y[1]))
395 scores.reverse()
396 serv.privmsg(auteur,"Scores by score : "+" ; ".join(["%s %s"%(i[0],i[1]) for i in scores]))
397 # trie par pseudo
398 scores.sort(lambda x,y:cmp(x[0].lower(),y[0].lower()))
399 serv.privmsg(auteur,"Scores by pseudo : "+" ; ".join(["%s %s"%(i[0],i[1]) for i in scores]))
400 elif auteur in self.overops:
401 souscmd=message[1].lower()
402 if souscmd=="del":
403 if len(message)==3:
404 todelete=message[2]
405 scores=self.get_scores()
406 if scores.has_key(todelete):
407 del scores[todelete]
408 self.save_scores(scores)
409 serv.privmsg(auteur,"Score de %s supprimé"%(todelete))
410 else:
411 serv.privmsg(auteur,"Ce score n'existe pas : %s"%(todelete))
412 else:
413 serv.privmsg(auteur,"Syntaxe : SCORES DEL <pseudo>")
414 elif souscmd in ["add","sub"]:
415 if len(message)==4:
416 toadd,val=message[2],message[3]
417 try:
418 val=int(val)
419 except ValueError:
420 serv.privmsg(auteur,"Syntaxe : SCORES {ADD|SUB} <pseudo> <n>")
421 return
422 if souscmd=="sub":
423 val=-val
424 self.add_score(toadd,val)
425 serv.privmsg(auteur,"Done")
426 else:
427 serv.privmsg(auteur,"Syntaxe : SCORES {ADD|SUB} <pseudo> <n>")
428 else:
429 serv.privmsg(auteur,"Syntaxe : SCORES {DEL|ADD|SUB} <pseudo> [<n>]")
430 else:
431 notunderstood=True
432 else:
433 notunderstood=True
434 if notunderstood:
435 serv.privmsg(auteur,"Je n'ai pas compris. Essaye HELP…")
436
437 def on_pubmsg(self, serv, ev):
438 auteur = irclib.nm_to_n(ev.source())
439 canal = ev.target()
440 message = ev.arguments()[0]
441 try:
442 test=bot_unicode(message)
443 except UnicodeBotError:
444 if not canal in self.quiet_channels:
445 serv.privmsg(canal,
446 "%s: Euh, tu fais de la merde avec ton encodage là, j'ai failli crasher…"%(auteur))
447 return
448 tryother=False
449 pour_moi,message=self.pourmoi(serv,message)
450 if pour_moi and message.split()!=[]:
451 cmd=message.split()[0].lower()
452 try:
453 args=" ".join(message.split()[1:])
454 except:
455 args=""
456 if cmd in ["meurs","die","crève"]:
457 if auteur in self.overops:
458 self.die()
459 log(canal,auteur,message+"[successful]")
460 else:
461 serv.privmsg(canal,"%s: crève !"%(auteur))
462 log(canal,auteur,message+"[failed]")
463 if cmd in ["meur", "meurt","meurre","meurres"] and not canal in self.quiet_channels:
464 serv.privmsg(canal,'%s: Mourir, impératif, 2ème personne du singulier : "meurs" (de rien)'%(auteur))
465 if cmd in ["part","leave","dégage"]:
466 if auteur in self.ops and (not (canal in self.stay_channels)
467 or auteur in self.overops):
468 serv.part(canal,message="Éjecté par %s"%(auteur))
469 log(canal,auteur,message+"[successful]")
470 self.chanlist.remove(canal)
471 else:
472 serv.privmsg(canal,"%s: Non, je reste !"%(auteur))
473 log(canal,auteur,message+"[failed]")
474
475 if cmd in ["deviens","pseudo"]:
476 if auteur in self.ops:
477 become=args
478 serv.nick(become)
479 log(canal,auteur,message+"[successful]")
480
481 if cmd in ["coucou"] and not canal in self.quiet_channels:
482 serv.privmsg(canal,"%s: coucou"%(auteur))
483 if cmd in ["ping"] and not canal in self.quiet_channels:
484 serv.privmsg(canal,"%s: pong"%(auteur))
485 if cmd in ["déconnaissance","deconnaissance","énigme","enigme","encore"]:
486 if canal in self.play_channels:
487 if self.play_status.get(canal,[-1])[0]==0:
488 try:
489 self.start_enigme(serv,canal)
490 except RefuseError:
491 serv.privmsg(canal,"%s: Je peux souffler une minute ?"%(auteur))
492 else:
493 serv.privmsg(canal,"%s: Rappel : %s"%(auteur,self.play_status[canal][1]))
494 else:
495 serv.privmsg(canal,"%s: pas ici…"%(auteur))
496 if cmd=="indice" and canal in self.play_channels:
497 self.give_indice(serv,canal,None)
498 if is_tag(message) and not canal in self.quiet_channels:
499 if auteur in self.ops:
500 action=random.choice(config_tag_actions)
501 serv.action(canal,action.encode("utf8"))
502 self.quiet_channels.append(canal)
503 else:
504 answer=random.choice(config_tag_answers)
505 for ligne in answer.split("\n"):
506 serv.privmsg(canal,"%s: %s"%(auteur,ligne.encode("utf8")))
507 else:
508 tryother=True
509 else:
510 tryother=True
511 if tryother:
512 if self.play_status.get(canal,[-1])[0] in [1,2]:
513 answer_regexp=self.play_status[canal][3]
514 if re.match(tolere(answer_regexp),unicode(message,"utf8").lower()):
515 answer=self.play_status[canal][4]
516 serv.privmsg(canal,"%s: bravo ! (C'était %s)"%(auteur,answer))
517 self.add_score(auteur,1)
518 token=time.time()
519 self.play_status[canal]=[0,token]
520 serv.execute_delayed(random.randrange(Ttrig*5,Ttrig*10),self.start_enigme,(serv,canal,token))
521 def get_scores(self):
522 f=open(config_score_file)
523 scores=pickle.load(f)
524 f.close()
525 return scores
526
527 def add_score(self,pseudo,value):
528 scores=self.get_scores()
529 if scores.has_key(pseudo):
530 scores[pseudo]+=value
531 else:
532 scores[pseudo]=value
533 self.save_scores(scores)
534
535 def save_scores(self,scores):
536 f=open(config_score_file,"w")
537 pickle.dump(scores,f)
538 f.close()
539
540 if __name__=="__main__":
541 import sys
542 if len(sys.argv)==1:
543 print "Usage : deconnaisseur.py <serveur> [--debug]"
544 exit(1)
545 serveur=sys.argv[1]
546 if "debug" in sys.argv or "--debug" in sys.argv:
547 debug=True
548 else:
549 debug=False
550 serveurs={"a♡":"acoeur.crans.org","acoeur":"acoeur.crans.org","acoeur.crans.org":"acoeur.crans.org",
551 "irc":"irc.crans.org","crans":"irc.crans.org","irc.crans.org":"irc.crans.org"}
552 try:
553 serveur=serveurs[serveur]
554 except KeyError:
555 print "Server Unknown : %s"%(serveur)
556 exit(404)
557 deco=Deconnaisseur(serveur,debug)
558 deco.start()