31 import socket |
31 import socket |
32 import time |
32 import time |
33 import sys |
33 import sys |
34 import traceback |
34 import traceback |
35 import re |
35 import re |
36 import json |
|
37 import urllib |
36 import urllib |
38 import urllib2 |
37 import urllib2 |
39 import hgapi |
38 import hgapi |
40 import os |
39 import os |
41 import suds |
40 import suds |
42 import math |
41 import math |
|
42 import json |
43 from datetime import datetime |
43 from datetime import datetime |
44 import commandhandler as CommandHandler |
44 import modulecore as ModuleCore |
45 |
45 import configfile |
46 CommandHandler.init_data() |
46 from configfile import Config |
|
47 |
|
48 ModuleCore.init_data() |
47 |
49 |
48 try: |
50 try: |
49 uid = os.geteuid() |
51 uid = os.geteuid() |
50 except: |
52 except: |
51 uid = -1 |
53 uid = -1 |
52 |
54 |
53 if uid == 0 and raw_input ('Do you seriously want to run cobalt as root? [y/N] ') != 'y': |
55 if uid == 0 and raw_input ('Do you seriously want to run cobalt as root? [y/N] ') != 'y': |
54 quit() |
56 quit() |
55 |
57 |
56 print 'Loading configuration...' |
58 configfile.init() |
57 try: |
59 g_admins = Config.get_value ('admins', default=[]) |
58 with open ('cobalt.json', 'r') as fp: |
60 g_mynick = Config.get_value ('nickname', default='cobalt') |
59 g_config = json.loads (fp.read()) |
|
60 except IOError as e: |
|
61 print 'couldn\'t open cobalt.json: %s' % e |
|
62 quit() |
|
63 |
|
64 g_admins = g_config['admins'] |
|
65 g_mynick = g_config['nickname'] |
|
66 |
|
67 g_BotActive = False |
61 g_BotActive = False |
68 g_needCommitsTxtRebuild = True |
62 g_needCommitsTxtRebuild = True |
69 |
63 |
70 # |
64 # |
71 # SOAP stuff |
65 # SOAP stuff |
82 pass |
76 pass |
83 |
77 |
84 btannounce_active = False |
78 btannounce_active = False |
85 btannounce_timeout = 0 |
79 btannounce_timeout = 0 |
86 |
80 |
87 def save_config(): |
|
88 with open ('cobalt.json', 'w') as fp: |
|
89 json.dump (g_config, fp, sort_keys = True, indent = 4) |
|
90 |
|
91 def cfg (key, default): |
|
92 if not hasattr (g_config, key): |
|
93 g_config[key] = default |
|
94 save_config() |
|
95 return default |
|
96 return g_config[key] |
|
97 |
|
98 def bt_updatechecktimeout(): |
81 def bt_updatechecktimeout(): |
99 global btannounce_timeout |
82 global btannounce_timeout |
100 btannounce_timeout = time.time() + (cfg ('btlatest_checkinterval', 5) * 60) |
83 btannounce_timeout = time.time() + (Config.get_node ('bt').get_value ('checkinterval', default=5) * 60) |
|
84 |
|
85 def bt_credentials(): |
|
86 bt = Config.get_node ('bt') |
|
87 user = bt.get_value ('username', '') |
|
88 password = bt.get_value ('password', '') |
|
89 return [user, password] |
101 |
90 |
102 if suds_active: |
91 if suds_active: |
103 try: |
92 sys.stdout.write ('Retrieving latest tracker ticket... ') |
104 sys.stdout.write ('Retrieving latest tracker ticket... ') |
93 user, password = bt_credentials() |
105 btannounce_id = suds_client.service.mc_issue_get_biggest_id (g_config['trackeruser'], g_config['trackerpassword'], 0) |
94 btannounce_id = suds_client.service.mc_issue_get_biggest_id (user, password, 0) |
106 btannounce_active = True |
95 btannounce_active = True |
107 bt_updatechecktimeout() |
96 bt_updatechecktimeout() |
108 print btannounce_id |
97 print btannounce_id |
109 except Exception as e: |
|
110 pass |
|
111 |
98 |
112 def bt_getissue(ticket): |
99 def bt_getissue(ticket): |
113 global suds_client |
100 global suds_client |
114 global g_config |
101 user, password = bt_credentials() |
115 return suds_client.service.mc_issue_get (g_config['trackeruser'], g_config['trackerpassword'], ticket) |
102 return suds_client.service.mc_issue_get (user, password, ticket) |
116 |
103 |
117 def bt_checklatest(): |
104 def bt_checklatest(): |
118 global btannounce_timeout |
105 global btannounce_timeout |
119 global btannounce_id |
106 global btannounce_id |
120 |
107 |
121 if time.time() >= btannounce_timeout: |
108 if time.time() >= btannounce_timeout: |
122 bt_updatechecktimeout() |
109 bt_updatechecktimeout() |
123 newid = btannounce_id |
110 newid = btannounce_id |
124 try: |
111 try: |
125 newid = suds_client.service.mc_issue_get_biggest_id (g_config['trackeruser'], g_config['trackerpassword'], 0) |
112 user, password = bt_credentials() |
|
113 newid = suds_client.service.mc_issue_get_biggest_id (user, password, 0) |
126 except Exception as e: |
114 except Exception as e: |
127 pass |
115 pass |
128 |
116 |
129 while newid > btannounce_id: |
117 while newid > btannounce_id: |
130 try: |
118 try: |
273 check_repo_exists ('zandronum-stable', 'Torr_Samaho') |
261 check_repo_exists ('zandronum-stable', 'Torr_Samaho') |
274 check_repo_exists ('zandronum-sandbox', 'crimsondusk') |
262 check_repo_exists ('zandronum-sandbox', 'crimsondusk') |
275 check_repo_exists ('zandronum-sandbox-stable', 'crimsondusk') |
263 check_repo_exists ('zandronum-sandbox-stable', 'crimsondusk') |
276 check_repo_exists ('zandronum-everything', '') |
264 check_repo_exists ('zandronum-everything', '') |
277 |
265 |
278 repocheck_timeout = {'zandronum':(time.time()) + 15, 'zandronum-stable':(time.time() + 15), 'zandronum-sandbox':(time.time()) + 15, 'zandronum-sandbox-stable':(time.time()) + 15} |
266 repocheck_timeout = {(time.time()) + 15} |
279 |
267 |
280 def get_commit_data (zanrepo, rev, template): |
268 def get_commit_data (zanrepo, rev, template): |
281 return zanrepo.hg_command ('log', '-l', '1', '-r', rev, '--template', template) |
269 return zanrepo.hg_command ('log', '-l', '1', '-r', rev, '--template', template) |
282 #enddef |
270 #enddef |
283 |
271 |
332 ' Retrieves and processes commits for zandronum repositories ' |
320 ' Retrieves and processes commits for zandronum repositories ' |
333 ' Ensure both repositories are OK before using this! ' |
321 ' Ensure both repositories are OK before using this! ' |
334 def process_zan_repo_updates (repo_name): |
322 def process_zan_repo_updates (repo_name): |
335 global repocheck_timeout |
323 global repocheck_timeout |
336 global suds_client |
324 global suds_client |
337 global g_config |
|
338 global g_clients |
325 global g_clients |
|
326 |
|
327 hgns = Config.get_node ('hg') |
|
328 |
|
329 if not hgns.get_value ('track', default=True): |
|
330 return |
339 |
331 |
340 usestable = repo_name == 'zandronum-stable' |
332 usestable = repo_name == 'zandronum-stable' |
341 usesandbox = repo_name == 'zandronum-sandbox' or repo_name == 'zandronum-sandbox-stable' |
333 usesandbox = repo_name == 'zandronum-sandbox' or repo_name == 'zandronum-sandbox-stable' |
342 repo_owner = 'Torr_Samaho' if not usesandbox else 'crimsondusk' |
334 repo_owner = 'Torr_Samaho' if not usesandbox else 'crimsondusk' |
343 repo_url = 'https://bitbucket.org/%s/%s' % (repo_owner, repo_name) |
335 repo_url = 'https://bitbucket.org/%s/%s' % (repo_owner, repo_name) |
344 num_commits = 0 |
336 num_commits = 0 |
345 |
337 btuser, btpassword = bt_credentials() |
346 if time.time() < repocheck_timeout[repo_name]: |
338 |
|
339 if time.time() < repocheck_timeout: |
347 return |
340 return |
348 |
341 |
349 repocheck_timeout[repo_name] = time.time() + (cfg ('hg_checkinterval', 15) * 60) |
342 repocheck_timeout = time.time() + hgns.get_value ('checkinterval', default=15) * 60 |
350 zanrepo = hgapi.Repo (repo_name) |
343 zanrepo = hgapi.Repo (repo_name) |
351 commit_data = [] |
344 commit_data = [] |
352 delimeter = '@@@@@@@@@@' |
345 delimeter = '@@@@@@@@@@' |
353 |
346 |
354 try: |
347 try: |
550 need_update = True |
542 need_update = True |
551 #fi |
543 #fi |
552 |
544 |
553 # Announce on IRC |
545 # Announce on IRC |
554 for irc_client in g_clients: |
546 for irc_client in g_clients: |
555 for channel in irc_client.cfg['channels']: |
547 for channel in irc_client.channels: |
556 if 'btannounce' in channel and channel['btannounce'] == True: |
548 if channel.get_value ('btannounce', default=True): |
557 irc_client.privmsg (channel['name'], |
549 irc_client.privmsg (channel.get_value ('name'), |
558 "%s: commit %s fixes issue %d: %s" |
550 "%s: commit %s fixes issue %d: %s" |
559 % (repo_name, commit_node, ticket_id, commit_message)) |
551 % (repo_name, commit_node, ticket_id, commit_message)) |
560 irc_client.privmsg (channel['name'], |
552 irc_client.privmsg (channel.get_value ('name'), |
561 "Read all about it here: " + irc_client.get_ticket_url (ticket_id)) |
553 "Read all about it here: " + irc_client.get_ticket_url (ticket_id)) |
562 #fi |
554 #fi |
563 #done |
555 #done |
564 #done |
556 #done |
565 |
557 |
566 if need_update: |
558 if need_update: |
567 # We need to remove the note data, otherwise the ticket notes |
559 # We need to remove the note data, otherwise the ticket notes |
568 # will get unnecessary updates. WTF, MantisBT? |
560 # will get unnecessary updates. WTF, MantisBT? |
569 ticket_data.notes = [] |
561 ticket_data.notes = [] |
570 suds_client.service.mc_issue_update (g_config['trackeruser'], g_config['trackerpassword'], ticket_id, ticket_data) |
562 suds_client.service.mc_issue_update (btuser, btpassword, ticket_id, ticket_data) |
571 #fi |
563 #fi |
572 |
564 |
573 suds_client.service.mc_issue_note_add (g_config['trackeruser'], g_config['trackerpassword'], ticket_id, { 'text': message }) |
565 suds_client.service.mc_issue_note_add (btuser, btpassword, ticket_id, { 'text': message }) |
574 num_commits += 1 |
566 num_commits += 1 |
575 except Exception as e: |
567 except Exception as e: |
576 chanlog ('Error while processing %s: %s' % (commit_node, `e`)) |
568 chanlog ('Error while processing %s: %s' % (commit_node, `e`)) |
577 continue |
569 continue |
578 #tried |
570 #tried |
587 # |
579 # |
588 # Main IRC client class |
580 # Main IRC client class |
589 # |
581 # |
590 class irc_client (asyncore.dispatcher): |
582 class irc_client (asyncore.dispatcher): |
591 def __init__ (self, cfg, flags): |
583 def __init__ (self, cfg, flags): |
592 self.name = cfg['name'] |
584 self.name = cfg.get_value ('name') |
593 self.host = cfg['address'] |
585 self.host = cfg.get_value ('address') |
594 self.port = cfg['port'] |
586 self.port = cfg.get_value ('port', default=6667) |
595 self.password = cfg['password'] if 'password' in cfg else '' |
587 self.password = cfg.get_value ('password', default='') |
596 self.channels = cfg['channels'] |
588 self.channels = cfg.get_nodelist ('channels') |
597 self.flags = flags |
589 self.flags = flags |
598 self.send_buffer = list() |
590 self.send_buffer = [] |
599 self.umode = cfg['umode'] if 'umode' in cfg else '' |
591 self.umode = cfg.get_value ('umode', default='') |
600 self.cfg = cfg |
592 self.cfg = cfg |
601 self.mynick = '' |
593 self.desired_name = Config.get_value ('nickname', default='cobalt') |
602 self.verbose = g_config['verbose'] if 'verbose' in g_config else False |
594 self.mynick = self.desired_name |
603 self.commandprefix = g_config['commandprefix'][0] if 'commandprefix' in g_config else '.' |
595 self.verbose = Config.get_value ('verbose', default=False) |
604 |
596 self.commandprefix = Config.get_value ('commandprefix', default='.') |
605 for channel in self.channels: |
597 |
606 if not 'logchannel' in channel: |
|
607 channel['logchannel'] = False |
|
608 channel['namesdone'] = True |
|
609 #channel['haslinkbot'] = False |
|
610 |
|
611 if not 'conflictsuffix' in self.cfg: |
|
612 self.cfg['conflictsuffix'] = '`' |
|
613 |
|
614 self.desired_name = self.cfg['nickname'] if 'nickname' in self.cfg else g_config['nickname'] |
|
615 g_clients.append (self) |
598 g_clients.append (self) |
616 asyncore.dispatcher.__init__ (self) |
599 asyncore.dispatcher.__init__ (self) |
617 self.create_socket (socket.AF_INET, socket.SOCK_STREAM) |
600 self.create_socket (socket.AF_INET, socket.SOCK_STREAM) |
618 self.connect ((self.host, self.port)) |
601 self.connect ((self.host, self.port)) |
619 |
602 |
620 def register_to_irc (self): |
603 def register_to_irc (self): |
621 ident = self.cfg['ident'] if 'ident' in self.cfg else g_config['ident'] |
604 ident = Config.get_value ('ident', default='cobalt') |
622 gecos = self.cfg['gecos'] if 'gecos' in self.cfg else g_config['gecos'] |
605 gecos = Config.get_value ('gecos', default='cobalt') |
623 if 'password' in self.cfg: |
606 self.write ("PASS %s" % self.password) |
624 self.write ("PASS %s" % self.cfg['password']) |
|
625 self.write ("USER %s * * :%s" % (ident, gecos)) |
607 self.write ("USER %s * * :%s" % (ident, gecos)) |
626 self.write ("NICK %s" % self.mynick) |
608 self.write ("NICK %s" % self.mynick) |
627 |
609 |
628 def handle_connect (self): |
610 def handle_connect (self): |
629 self.mynick = self.desired_name |
|
630 print "Connected to [%s] %s:%d" % (self.name, self.host, self.port) |
611 print "Connected to [%s] %s:%d" % (self.name, self.host, self.port) |
631 self.register_to_irc() |
612 self.register_to_irc() |
632 |
613 |
633 def write (self, utfdata): |
614 def write (self, utfdata): |
634 try: |
615 try: |
673 words = line.split(" ") |
654 words = line.split(" ") |
674 if len(words) >= 2: |
655 if len(words) >= 2: |
675 if words[1] == "001": |
656 if words[1] == "001": |
676 self.flags |= CLIF_CONNECTED |
657 self.flags |= CLIF_CONNECTED |
677 |
658 |
678 for channel in self.cfg['channels']: |
659 for channel in self.channels: |
679 self.write ("JOIN %s %s" % (channel['name'], channel['password'] if 'password' in channel else '')) |
660 self.write ("JOIN %s %s" % (channel.get_value ('name'), channel.get_value ('password', default=''))) |
680 |
661 |
681 if 'umode' in self.cfg: |
662 umode = self.cfg.get_value ('umode', '') |
682 self.write ('MODE %s %s' % (self.mynick, self.cfg['umode'])) |
663 |
|
664 if umode != '': |
|
665 self.write ('MODE %s %s' % (self.mynick, self.cfg.get_value ('umode', ''))) |
683 elif words[1] == "PRIVMSG": |
666 elif words[1] == "PRIVMSG": |
684 self.handle_privmsg (line) |
667 self.handle_privmsg (line) |
685 elif words[1] == 'JOIN': |
|
686 rex = re.compile (r'^:([^!]+)!([^@]+)@([^ ]+) JOIN :#(.+)') |
|
687 match = rex.match (line) |
|
688 |
|
689 #if match and match.group(1).toLower() == 'linkbot': |
|
690 #channel_by_name (match.group(4))['haslinkbot'] = True |
|
691 elif words[1] == 'QUIT': |
668 elif words[1] == 'QUIT': |
692 rex = re.compile (r'^:([^!]+)!([^@]+)@([^ ]+) QUIT') |
669 rex = re.compile (r'^:([^!]+)!([^@]+)@([^ ]+) QUIT') |
693 match = rex.match (line) |
670 match = rex.match (line) |
694 |
671 |
695 # Try reclaim our nickname if possible |
672 # Try reclaim our nickname if possible |
696 if match and match.group(1) == self.desired_name: |
673 if match and match.group(1) == self.desired_name: |
697 self.mynick = self.desired_name |
674 self.mynick = self.desired_name |
698 self.write ("NICK %s" % self.mynick) |
675 self.write ("NICK %s" % self.mynick) |
699 |
|
700 #if match and match.group(1).toLower() == 'linkbot': |
|
701 #for channel in self.channels: |
|
702 #channels['haslinkbot'] = False |
|
703 elif words[1] == "433": |
676 elif words[1] == "433": |
704 #:irc.localhost 433 * cobalt :Nickname is already in use. |
677 #:irc.localhost 433 * cobalt :Nickname is already in use. |
705 self.mynick = '%s%s' % (self.mynick, self.cfg['conflictsuffix']) |
678 self.mynick += self.cfg.get_value ('conflictsuffix', default='`') |
706 self.write ("NICK %s" % self.mynick) |
679 self.write ("NICK " + self.mynick) |
707 |
680 |
708 # Check for new issues on the bugtracker |
681 # Check for new issues on the bugtracker |
709 bt_checklatest() |
682 bt_checklatest() |
710 |
683 |
711 # Check for new commits in the repositories |
684 # Check for new commits in the repositories |
712 for n in ['zandronum-stable', 'zandronum', 'zandronum-sandbox', 'zandronum-sandbox-stable']: |
685 for n in ['zandronum-stable', 'zandronum', 'zandronum-sandbox', 'zandronum-sandbox-stable']: |
713 process_zan_repo_updates (n) |
686 process_zan_repo_updates (n) |
714 |
687 |
715 def channel_by_name (self, name): |
688 def channel_by_name (self, name): |
716 for channel in self.channels: |
689 for channel in self.channels: |
717 if channel['name'].upper() == args[0].upper(): |
690 if channel.get_value ('name').upper() == args[0].upper(): |
718 return channel |
691 return channel |
719 else: |
692 else: |
720 raise logical_exception ('unknown channel ' + args[0]) |
693 raise logical_exception ('unknown channel ' + args[0]) |
721 |
694 |
722 # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # |
695 # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # |
733 channel = match.group (4) |
706 channel = match.group (4) |
734 message = match.group (5) |
707 message = match.group (5) |
735 replyto = channel if channel != g_mynick else sender |
708 replyto = channel if channel != g_mynick else sender |
736 |
709 |
737 # Check for tracker url in the message |
710 # Check for tracker url in the message |
738 http_regex = re.compile (r'.*http(s?)://%s/view\.php\?id=([0-9]+).*' % g_config['trackerurl']) |
711 url = Config.get_node ('bt').get_value ('url') |
|
712 http_regex = re.compile (r'.*http(s?)://%s/view\.php\?id=([0-9]+).*' % url) |
739 http_match = http_regex.match (line) |
713 http_match = http_regex.match (line) |
740 |
714 |
741 # Check for command. |
715 # Check for command. |
742 if len(message) >= 2 and message[0] == self.commandprefix and message[1] != self.commandprefix: |
716 if len(message) >= 2 and message[0] == self.commandprefix and message[1] != self.commandprefix: |
743 stuff = message[1:].split(' ') |
717 stuff = message[1:].split(' ') |
757 # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # |
731 # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # |
758 # |
732 # |
759 # Get the URL for a specified ticket |
733 # Get the URL for a specified ticket |
760 # |
734 # |
761 def get_ticket_url (self, ticket): |
735 def get_ticket_url (self, ticket): |
762 return 'https://%s/view.php?id=%s' % (g_config['trackerurl'], ticket) |
736 url = Config.get_node ('bt').get_value ('url') |
|
737 return 'https://%s/view.php?id=%s' % (url, ticket) |
763 |
738 |
764 # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # |
739 # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # |
765 # |
740 # |
766 # Retrieve a ticket from mantisbt |
741 # Retrieve a ticket from mantisbt |
767 # |
742 # |
818 # |
790 # |
819 def handle_command (self, sender, ident, host, replyto, command, args, message): |
791 def handle_command (self, sender, ident, host, replyto, command, args, message): |
820 kvargs = {'sender': sender, 'ident': ident, 'host': host, 'replyto': replyto, 'cmdname': command, 'message': message} |
792 kvargs = {'sender': sender, 'ident': ident, 'host': host, 'replyto': replyto, 'cmdname': command, 'message': message} |
821 |
793 |
822 try: |
794 try: |
823 result = CommandHandler.call_command (self, **kvargs) |
795 result = ModuleCore.call_command (self, **kvargs) |
824 |
796 |
825 if result: |
797 if result: |
826 return |
798 return |
827 else: |
799 except ModuleCore.CommandError as e: |
828 print 'CommandHandler.call_command returned false' |
|
829 except CommandHandler.CommandError as e: |
|
830 lines = str (e).split ('\n') |
800 lines = str (e).split ('\n') |
831 self.privmsg (replyto, 'error: %s' % lines[0]) |
801 self.privmsg (replyto, 'error: %s' % lines[0]) |
832 |
802 |
833 for line in lines[1:]: |
803 for line in lines[1:]: |
834 self.privmsg (replyto, ' ' + line) |
804 self.privmsg (replyto, ' ' + line) |
1028 idstring = "0" + idstring |
998 idstring = "0" + idstring |
1029 |
999 |
1030 isprivate = data['view_state']['name'] == 'private' |
1000 isprivate = data['view_state']['name'] == 'private' |
1031 reporter = data['reporter']['name'] if hasattr (data['reporter'], 'name') else '<nobody>' |
1001 reporter = data['reporter']['name'] if hasattr (data['reporter'], 'name') else '<nobody>' |
1032 |
1002 |
1033 for channel in self.cfg['channels']: |
1003 for channel in self.channels: |
1034 if 'btannounce' in channel and channel['btannounce'] == True: |
1004 if channel.get_value ('btannounce', False): |
1035 if not isprivate or ('btprivate' in channel and channel['btprivate'] == True): |
1005 if not isprivate or (channel.get_value ('btprivate', False)): |
1036 self.write ("PRIVMSG %s :[%s] New issue %s, reported by %s: %s: %s" % \ |
1006 self.write ("PRIVMSG %s :[%s] New issue %s, reported by %s: %s: %s" % \ |
1037 (channel['name'], data['project']['name'], idstring, reporter, |
1007 (channel['name'], data['project']['name'], idstring, reporter, |
1038 data['summary'], self.get_ticket_url (idstring))) |
1008 data['summary'], self.get_ticket_url (idstring))) |
1039 #fi |
1009 #fi |
1040 #fi |
1010 #fi |
1066 |
1036 |
1067 # |
1037 # |
1068 # Main procedure: |
1038 # Main procedure: |
1069 # |
1039 # |
1070 try: |
1040 try: |
1071 for aconn in g_config['autoconnect']: |
1041 autoconnects = Config.get_value ('autoconnect', []) |
1072 for conndata in g_config['connections']: |
1042 |
1073 if conndata['name'] == aconn: |
1043 if len (autoconnects) == 0: |
|
1044 print "Nowhere to connect." |
|
1045 quit() |
|
1046 |
|
1047 for aconn in autoconnects: |
|
1048 for conndata in Config.get_nodelist ('connections'): |
|
1049 if conndata.get_value ('name') == aconn: |
1074 irc_client (conndata, 0) |
1050 irc_client (conndata, 0) |
1075 break |
1051 break |
1076 else: |
1052 else: |
1077 raise logical_exception ("unknown autoconnect entry %s" % (aconn)) |
1053 raise ValueError ("unknown autoconnect entry %s" % (aconn)) |
1078 |
1054 |
1079 g_BotActive = True |
1055 g_BotActive = True |
1080 asyncore.loop() |
1056 asyncore.loop() |
1081 except KeyboardInterrupt: |
1057 except KeyboardInterrupt: |
1082 for client in g_clients: |
1058 for client in g_clients: |
1083 client.keyboardinterrupt() |
1059 client.keyboardinterrupt() |