def execute_plugins(self, network, trigger, *arguments): for plugin in plugin_handler.all_plugins(): try: if plugin.__class__.__dict__.has_key(trigger): # FIXME this is rather ugly, for compatiblity with pynik if plugin.__class__.__dict__[trigger].func_code.co_argcount == len(arguments) + 2: plugin.__class__.__dict__[trigger](plugin, self, *arguments) # Call without network elif plugin.__class__.__dict__[trigger].func_code.co_argcount == len(arguments) + 3: plugin.__class__.__dict__[trigger](plugin, self, *arguments, **{'network': network}) else: raise NotImplementedError("Plugin '%s' argument count missmatch, was %s." % ( plugin, plugin.__class__.__dict__[trigger].func_code.co_argcount)) except: error_handler.output_message("%s %s Plugin '%s' threw exception, exinfo: '%s', traceback: '%s'" % ( datetime.datetime.now().strftime("[%H:%M:%S]"), network, plugin, sys.exc_info(), traceback.extract_tb(sys.exc_info()[2]))) if trigger != "timer_beat": try: self.tell(self.settings.admin_network, self.settings.admin_channel, "%s %s Plugin '%s' threw exception, exinfo: '%s', traceback: '%s'" % ( datetime.datetime.now().strftime("[%H:%M:%S]"), network, plugin, sys.exc_info(), traceback.extract_tb(sys.exc_info()[2])[::-1])) except: error_handler.output_message("%s %s Unable to send exception to admin channel, exinfo: '%s', traceback: '%s'" % ( datetime.datetime.now().strftime("[%H:%M:%S]"), network, sys.exc_info(), traceback.extract_tb(sys.exc_info()[2])))
def trig_mc(self, bot, source, target, trigger, argument): term = argument.strip() if not term: return "usage: .metacritic <game title> or <game> <platform> (slower)" url = "http://apps.metacritic.com/search/process?ty=3&tfs=game_title&ts=" + utility.escape(term) data = utility.read_url(url)["data"] result = self.parse_result(data, term, url) if result: return result error_handler.output_message("<metacritic> title search failed.") url = "http://apps.metacritic.com/search/process?ty=3&ts=" + utility.escape(term) data = utility.read_url(url)["data"] result = self.parse_result(data, term, url) if result: return result return ( "Found nothing. Try it yourself: " + "http://apps.metacritic.com/search/process?ty=3&ts=" + utility.escape(term) )
def on_load(self): try: t = [self.get_nodes_from_scratch, self.get_nodes_from_module, self.get_nodes_from_option] self._table = self.build_tree(None, t, 0) except: import traceback error_handler.output_message(str(sys.exc_info()) + ' ' + str(traceback.extract_tb(sys.exc_info()[2])))
def reload_plugin_modules(): import traceback for module in new_modules: try: reload(module) except: error_handler.output_message('error when reloading module ' + str(module.__name__) + ' ' + str(sys.exc_info()) + ' ' + str(traceback.extract_tb(sys.exc_info()[2])))
def trig_mc(self, bot, source, target, trigger, argument): term = argument.strip() if not term: return "usage: .metacritic <game title> or <game> <platform> (slower)" url = 'http://apps.metacritic.com/search/process?ty=3&tfs=game_title&ts=' + utility.escape( term) data = utility.read_url(url)["data"] result = self.parse_result(data, term, url) if result: return result error_handler.output_message("<metacritic> title search failed.") url = 'http://apps.metacritic.com/search/process?ty=3&ts=' + utility.escape( term) data = utility.read_url(url)["data"] result = self.parse_result(data, term, url) if result: return result return "Found nothing. Try it yourself: " + 'http://apps.metacritic.com/search/process?ty=3&ts=' + utility.escape( term)
def load_data(name, default_value=None): try: with open(os.path.join('data', name + '.txt'), 'r') as handle: return pickle.Unpickler(handle).load() except: error_handler.output_message("Could not load data from file 'data/" + str(name) + ".txt' :(") return default_value
def tw_get_info(): counter = 0 address = "master.teewars.com" master_port = 8300 sock = socket(AF_INET, SOCK_DGRAM) sock.settimeout(5.0) sock.sendto("\x20\x00\x00\x00\x00\x00\xff\xff\xff\xffreqt", (address, master_port)) try: data, addr = sock.recvfrom(1024) sock.close() data = data[14:] num_servers = len(data) / 6 num_players = 0 players_dic = {} for n in range(0, num_servers): ip = ".".join(map(str, map(ord, data[n*6:n*6+4]))) port = ord(data[n*6+5]) * 256 + ord(data[n*6+4]) #print ip, port with list_lock: id = thread.start_new_thread(tw_get_num_players_proxy, (ip, port, players_dic)) players_dic[id] = -2 while True: has_items = False with list_lock: for slot in players_dic.keys(): if players_dic[slot] == -2: has_items = True break if has_items: time.sleep(0.5) else: break players_list = [] for slot in players_dic.keys(): if players_dic[slot] != -1: players_list.append(players_dic[slot]) num_servers = len(players_list) num_players = reduce(lambda x, y: x + y, players_list) with open("data/tw_stats.txt", "a") as file: file.write("%s %s %s\n" % (int(time.time()), num_servers, num_players)) utility.read_url("http://serp.starkast.net/berserker/gief_stats.php?timestamp=%s&servers=%s&players=%s" % (int(time.time()), num_servers, num_players)); return (num_servers, num_players) except: error_handler.output_message('exception O.o ' + str(sys.exc_info()) + ' ' + str(traceback.extract_tb(sys.exc_info()[2]))) return None
def on_command(self, bot, source, target, trigger, arguments): if source == "buffi": return meth_name = "trig_" + trigger.lower() pairs = [] for command_class in commands.Command.__subclasses__(): import __builtin__ meth = None try: meth = command_class.instance.__getattribute__(meth_name) pairs.append([command_class.instance, meth]) except: pass for command in commands.get_commands_by_trigger(trigger): pairs.append([command, command.on_trigger]) for pair in pairs: command, method = pair if command.can_trigger(source, trigger): m = re.search("^(.+)!", source) if m: if target == source: target = m.group(1) source = m.group(1) try: return utility.timeout(method, 10, (bot, source, target, trigger, arguments)) except utility.TimeoutException: return "Command '%s' took too long to execute." % trigger except: boll = list(traceback.extract_tb(sys.exc_info()[2])) bolliStr = ", ".join(map(lambda x: str(x), boll)) bot.tell( "#botnik", "%s triggered an error by typing '%s %s': %s. %s" % (source, trigger, arguments, sys.exc_info(), bolliStr), ) error_handler.output_message(str(sys.exc_info())) error_handler.output_message( "Error when executing command '" + trigger + "':" + str(traceback.extract_tb(sys.exc_info()[2])) ) return "Oops. Error logged." else: return "Bwaha. You can't trigger that!" if not len(pairs): if trigger in favorites.FavoriteCommands.instance.favorites.keys(): return favorites.FavoriteCommands.instance.trig_fav( bot, source, target, "fav", trigger + " " + arguments )
def load_data(name, default_value=None): try: data = konfig.configmap('pyirkbot-data').read().get(name, None) if data is None: return default_value return pickle.loads(data) except: error_handler.output_message("Could not load data from pyirkbot-data :(") return default_value
def reload_plugin_modules(): import traceback for module in new_modules: try: reload(module) except: error_handler.output_message( 'error when reloading module ' + str(module.__name__) + ' ' + str(sys.exc_info()) + ' ' + str(traceback.extract_tb(sys.exc_info()[2])))
def get_request(self, client): try: request = client.wait_for_request() self.request_queue_lock.acquire() self.request_queue.append(request) self.request_queue_lock.release() except TimeoutError: client.socket.close() except InvalidRequestException: error_handler.output_message("[http_server] invalid request O.o") client.socket.close()
def on_load(self): try: t = [ self.get_nodes_from_scratch, self.get_nodes_from_module, self.get_nodes_from_option ] self._table = self.build_tree(None, t, 0) except: import traceback error_handler.output_message( str(sys.exc_info()) + ' ' + str(traceback.extract_tb(sys.exc_info()[2])))
def execute_plugins(self, trigger, *arguments): for plugin in plugin_handler.all_plugins(): try: #if arguments: # print trigger # print("plugin.%s(self, %s)" % (trigger,", ".join(arguments))) #else: # print("plugin.%s(self)" % trigger) plugin.__class__.__dict__[trigger](plugin, self, *arguments) except KeyError: pass except: error_handler.output_message("argh " + str(plugin) + " " + str(sys.exc_info()) + " " + str(traceback.extract_tb(sys.exc_info()[2])))
def tick(self): now = datetime.datetime.now() if not self.timer_heap.empty() and not self.client.connected: error_handler.output_message("ATTENTION! We are not connected. Skipping timers!") else: while not self.timer_heap.empty() and self.timer_heap.top().trigger_time <= now: timer = self.timer_heap.pop() timer.trigger() if timer.recurring: timer.reset() self.timer_heap.push(timer) self.client.tick()
def execute_plugins(self, trigger, event = {}): for plugin in plugin_handler.all_plugins(): try: if plugin.__class__.__dict__.has_key(trigger): event['bot'] = self plugin.__class__.__dict__[trigger](plugin, event) # new object based callback except: messages = ['','EXCEPTION:'] messages.append("%s %s" % (plugin, sys.exc_info())) for row in [[tb[0]+':'+str(tb[1])]+[str(z) for z in tb[2:]] for tb in traceback.extract_tb(sys.exc_info()[2])]: messages.append(row) messages.append('') for message in messages: error_handler.output_message(message)
def execute_plugins(self, network, trigger, *arguments): for plugin in plugin_handler.all_plugins(): try: if plugin.__class__.__dict__.has_key(trigger): # FIXME this is rather ugly, for compatiblity with pynik if plugin.__class__.__dict__[ trigger].func_code.co_argcount == len( arguments) + 2: plugin.__class__.__dict__[trigger]( plugin, self, *arguments) # Call without network elif plugin.__class__.__dict__[ trigger].func_code.co_argcount == len( arguments) + 3: plugin.__class__.__dict__[trigger](plugin, self, *arguments, **{ 'network': network }) else: raise NotImplementedError( "Plugin '%s' argument count missmatch, was %s." % (plugin, plugin.__class__.__dict__[trigger]. func_code.co_argcount)) except: error_handler.output_message( "%s %s Plugin '%s' threw exception, exinfo: '%s', traceback: '%s'" % (datetime.datetime.now().strftime("[%H:%M:%S]"), network, plugin, sys.exc_info(), traceback.extract_tb(sys.exc_info()[2]))) if trigger != "timer_beat": try: self.tell( self.settings.admin_network, self.settings.admin_channel, "%s %s Plugin '%s' threw exception, exinfo: '%s', traceback: '%s'" % (datetime.datetime.now().strftime("[%H:%M:%S]"), network, plugin, sys.exc_info(), traceback.extract_tb(sys.exc_info()[2])[::-1])) except: error_handler.output_message( "%s %s Unable to send exception to admin channel, exinfo: '%s', traceback: '%s'" % (datetime.datetime.now().strftime("[%H:%M:%S]"), network, sys.exc_info(), traceback.extract_tb(sys.exc_info()[2])))
def tw_get_info(): counter = 0 address = "master.teewars.com" master_port = 8300 sock = socket(AF_INET, SOCK_DGRAM) sock.settimeout(5.0) sock.sendto("\x20\x00\x00\x00\x00\x00\xff\xff\xff\xffreqt", (address, master_port)) try: data, addr = sock.recvfrom(1024) sock.close() data = data[14:] num_servers = len(data) / 6 num_players = 0 players_dic = {} for n in range(0, num_servers): ip = ".".join(map(str, map(ord, data[n * 6:n * 6 + 4]))) port = ord(data[n * 6 + 5]) * 256 + ord(data[n * 6 + 4]) #print ip, port with list_lock: id = thread.start_new_thread(tw_get_num_players_proxy, (ip, port, players_dic)) players_dic[id] = -2 while True: has_items = False with list_lock: for slot in players_dic.keys(): if players_dic[slot] == -2: has_items = True break if has_items: time.sleep(0.5) else: break players_list = [] for slot in players_dic.keys(): if players_dic[slot] != -1: players_list.append(players_dic[slot]) num_servers = len(players_list) num_players = reduce(lambda x, y: x + y, players_list) with open("data/tw_stats.txt", "a") as file: file.write("%s %s %s\n" % (int(time.time()), num_servers, num_players)) utility.read_url( "http://serp.starkast.net/berserker/gief_stats.php?timestamp=%s&servers=%s&players=%s" % (int(time.time()), num_servers, num_players)) return (num_servers, num_players) except: error_handler.output_message( 'exception O.o ' + str(sys.exc_info()) + ' ' + str(traceback.extract_tb(sys.exc_info()[2]))) return None
def read_url(url): m = re.match("^(.{3,5}):\/\/([^\/]*)(:?\d*)(\/.*?)?$", url) if m: protocol, address, port, file = m.group(1, 2, 3, 4) if protocol in ['https', 'http']: # Use the built-in functions import urllib try: file = urllib.urlopen(url) except IOError: return None result = { "url": file.geturl(), "data": file.read(1024*1024), "info": file.info() } file.close() return result elif protocol != 'http': error_handler.output_message("<httpget> Only http(s) is supported at this moment, cannot get " + url) return None else: # Disabled custom http reader in favor of built-in Python functionality pass if not port: port = 80 if not file: file = '/' #print "Connecting to %s" % address request = http_get_request(file) request.add_header("User-Agent", "Pynik/0.1") request.add_header("Accept", "*/*") request.add_header("Host", address) s = socket(AF_INET, SOCK_STREAM) s.connect((address, port)) request.send(s) protocol, response_num, response_string, headers = read_http_headers(s) if response_num == 301 or response_num == 302: s.close() # Let's do some simple loop detection... if url == headers['Location']: print "Redirect loop discovered at: %s" % headers['Location'] return None else: print "Site moved to: %s" % headers['Location'] return read_url(headers['Location']) elif response_num == 200: #print "Got response 200. Sweet!" length = 1024*1024 # max one megabyte if "Content-Length" in headers: length = min(length, int(headers["Content-Length"])) data = read_http_data(s, length) s.close() return { "url": url, "data": data } else: error_handler.output_message("<httpget> Got unhandled response code: %s" % response_num) return None else: error_handler.output_message("<httpget> NOT AN URL: %s" % url) return None
def lith_course_info(code, programme, year): # Fetch the study handbook page for the course response = utility.read_url(sh_url(code, year)) sh_data = response["data"] # Locate the Swedish course name m = re.search( "\<span class=\"txtbold\"\>\<b\>(.*?), \d{1,2},?\d? p \</b\>", sh_data) if m: name = utility.unescape(m.group(1)) else: return "Hmm, are you sure the course code is " + code + "?" # Locate the English course name m = re.search("\<br\>/(.*?)/\</b\>\</span\>", sh_data) if m: name = name + " (" + utility.unescape(m.group(1)) + ")" else: error_handler.output_message( "I couldn't find the English name of the LiTH course " + code + " O.o") # Locate the number of HP (ECTS) credits m = re.search( "\<span class=\"txtbold\"\>\<b\> (\d{1,2},?\d?) hp\</span\>\</font\>", sh_data) if m: credits = m.group(1).replace(",", ".") else: credits = "???" #print "I couldn't find the number of credits for the LiTH course " + \ # code + " O.o" # Locate the advancement level m = re.search( "\<span class=\"txtkursivlista\"\>Utbildningsnivå \(G1,G2,A\):\<\/span\>\<i\> \<\/i\>\<span class=\"txtlista\"\>(.+?)\<\/span\>", sh_data) if m: level = m.group(1) else: level = "???" # Fetch the schedule page for the course from the study handbook response = utility.read_url(schedule_url(code, programme, year)) sh_data = response["data"] # Match study periods # (Usually ([HV]t[12]) but some odd courses have other formats, e.g. Ht2a) period_m = re.findall( "\<td\>\<span class=\"txtlista\"\>\d([HV]t.*?)\</span\>\</td\>", sh_data) # Match blocks # (Usually ([1-4]) but some courses have other formats, e.g. "-", "" or "1+2+4") block_m = re.findall( "&-Token.ksk=\[Field:'k_kurskod'\]\"\>---\>" + "(.*?)" + \ "\<!---\<\/a\>---\>\<\/span\>\<\/td\>", sh_data) # Build a list of schedule occasions schedules = [] for i in range(len(period_m)): # Assemble an occasion string match = period_m[i] + "." if not block_m[i]: match += "?" else: match += block_m[i].replace(", ", "+") # Add if not already present if match not in schedules: schedules.append(match) # Convert it into a string if schedules: schedule_text = "Scheduled during " + ", ".join(sorted(schedules)) else: schedule_text = "Not scheduled " + year + "." # Combine all the information and return it return code + ": " + name + ", " + credits + " HP on level " + level + \ ". " + schedule_text + " | " + sh_url(code, year)
def read_url(url): m = re.match("^(.{3,5}):\/\/([^\/]*)(:?\d*)(\/.*?)?$", url) if m: protocol, address, port, file = m.group(1, 2, 3, 4) if protocol == 'https': # Use the built-in functions import urllib try: file = urllib.urlopen(url) except IOError: return None result = { "url": file.geturl(), "data": file.read(1024 * 1024), "info": file.info() } file.close() return result elif protocol != 'http': error_handler.output_message( "<httpget> Only http(s) is supported at this moment, cannot get " + url) return None else: if not port: port = 80 if not file: file = '/' #print "Connecting to %s" % address request = http_get_request(file) request.add_header("User-Agent", "Pynik/0.1") request.add_header("Accept", "*/*") request.add_header("Host", address) s = socket(AF_INET, SOCK_STREAM) s.connect((address, port)) request.send(s) protocol, response_num, response_string, headers = read_http_headers( s) if response_num == 301 or response_num == 302: s.close() # Let's do some simple loop detection... if url == headers['Location']: print "Redirect loop discovered at: %s" % headers[ 'Location'] return None else: print "Site moved to: %s" % headers['Location'] return read_url(headers['Location']) elif response_num == 200: #print "Got response 200. Sweet!" length = 1024 * 1024 # max one megabyte if "Content-Length" in headers: length = min(length, int(headers["Content-Length"])) data = read_http_data(s, length) s.close() return {"url": url, "data": data} else: error_handler.output_message( "<httpget> Got unhandled response code: %s" % response_num) return None else: error_handler.output_message("<httpget> NOT AN URL: %s" % url) return None
def menu(location): # Set location-specific settings if location == "[hg]" or location == "hg": # Ryds Herrgård [hg], Linköping url = "http://www.hg.se/?restaurang/kommande" entry_regex = '\<h2\>(.+?dag)en den .+?\<\/h2\>(.+?)(?=(\<h2\>|\<em\>))' entry_day_index = 0 entry_data_index = 1 dish_regex = '\<p\>(.+?)\<br\>(.+?((\d+?) kr))?' dish_name_index = 0 dish_price_index = 3 elif location == "villevalla" or location == "vvp": # VilleValla Pub, Linköping url = "http://www.villevallapub.se/" entry_regex = '\<td valign="top" style="padding-right: 2px;"\>\<strong\>(.+?dag)\<\/strong\>\<\/td\>\s*\<td\>(.+?)\<\/td\>' entry_day_index = 0 entry_data_index = 1 dish_regex = '\A(.+?) ((\d+?) :-)\Z' dish_name_index = 0 dish_price_index = 2 elif location == "karallen" or location == "kara": # Restaurang Kårallen, LiU # Oh well... The Kårallen guys apparently don't know what they are doing. # Until someone implements code that parses out the link to the current # menu page, this hack will have to do: url = "http://www.cgnordic.com/sv/Eurest-Sverige/Restauranger/Restaurang-Karallen-Linkopings-universitet/Lunchmeny-" week_number = int(datetime.now().strftime("%V")) if (week_number % 2) == 1: url += "v-13/" else: url += "v-15/" header_regex = '\<strong\>(.+?dag).+?\<\/strong\>' entry_regex = header_regex + '(\<\/p\>)?\<\/td\>\<\/tr\>(.+?)(?=(' + header_regex + '|\<p\>Pris dagens:))' entry_day_index = 0 entry_data_index = 2 dish_regex = '\<\/td\>\s+\<td\>(\s+\<p( align="[a-z]+")?\>)?([^\<]+?)(\<\/p\>)?\<\/td\>\<\/tr\>()' dish_name_index = 2 dish_price_index = 4 # Dummy index. elif location == "blamesen" or location == "galaxen": # Restaurang Blåmesen, Galaxen, LiU url = "http://davidg.nu/lunch/blamesen.php?price" entry_regex = '([A-Za-zåäö]{3,4}dag)(.+?)(?=([A-Za-zåäö]{3,4}dag|$))' entry_day_index = 0 entry_data_index = 1 dish_regex = ': (.+?) \((\d+) kr\)' dish_name_index = 0 dish_price_index = 1 elif location == "zenit": # Restaurang & Café Zenit, LiU url = "http://hors.se/new_site/restauranter_pdf.php?UID=24" header_regex = '\<b\>(.+?dag) [\d]{2}-[A-Za-z]{3}\<\/b\>' entry_regex = header_regex + '(.+?)(?=(' + header_regex + '|\<td width="\d+px" valign="top"\>Veckans|\<\/html\>))' entry_day_index = 0 entry_data_index = 1 dish_regex = '\<td valign="top"\>([^\<]+)\<\/td\>\<td width="\d+px" valign="top"\>()' dish_name_index = 0 dish_price_index = 1 # Dummy index. else: return [] # Not implemented yet # Settings are correct, now it's time to actually do something. # Fetch the web page response = utility.read_url(url) if not response: return [] data = response["data"] data = utility.unescape(data.replace("\n", "")) data = data.replace(utility.unescape(" "), " ") #print data # Build the menu menu = [] for entry in re.findall(entry_regex, data): #print entry day = entry[entry_day_index] dishes = [] for dish in re.findall(dish_regex, entry[entry_data_index]): #print dish dish_name = dish[dish_name_index].strip() dish_name = re.sub('\s+', ' ', dish_name) if not dish_name: pass # Odd input or bad regex elif dish_name.find(">") != -1: error_handler.output_message("Hmm, 'mat' got an odd dish from " + location + ": " + dish_name) elif dish[dish_price_index]: # Price found, let's add it dishes.append(dish_name + " (" + dish[dish_price_index] + " kr)") else: # No price, exclude it dishes.append(dish_name) menu.append((day, dishes)) # Done! return menu
def on_command(self, bot, source, target, trigger, arguments, network): meth_name = 'trig_' + trigger.lower() pairs = [] for command_class in commands.Command.__subclasses__(): import __builtin__ meth = None try: meth = command_class.instance.__getattribute__(meth_name) pairs.append([command_class.instance, meth]) except: pass for command in commands.get_commands_by_trigger(trigger): pairs.append([command, command.on_trigger]) for pair in pairs: command, method = pair if command.can_trigger(source, trigger): m = re.search('^(.+)!', source) if m: if target == source: target = m.group(1) source = m.group(1) try: # FIXME this is rather ugly, for compatiblity with pynik if method.im_func.func_code.co_argcount == 7: ret = utility.timeout(method, 10, (bot, source, target, trigger, arguments), {'network': network}) elif method.im_func.func_code.co_argcount == 6: ret = utility.timeout(method, 10, (bot, source, target, trigger, arguments)) else: raise NotImplementedError("Trigger '%s' argument count missmatch, was %s." % ( trigger, method.im_func.func_code.co_argcount)) return ret except utility.TimeoutException: return "Command '%s' took too long to execute." % trigger except MemoryError: return "Command '%s' used to much memory." % trigger except: error_handler.output_message("Error triggered by '%s' with command '%s', exinfo: '%s', traceback: '%s'" % ( source, trigger, sys.exc_info(), traceback.extract_tb(sys.exc_info()[2]))) try: bot.tell(bot.settings.admin_network, bot.settings.admin_channel, "%s triggered an error by typing '%s %s': %s, tb: %s." % ( source, trigger, arguments, sys.exc_info(), traceback.extract_tb(sys.exc_info()[2])[::-1])) except: error_handler.output_message("%s %s Unable to send exception to admin channel, exinfo: '%s', traceback: '%s'" % ( datetime.datetime.now().strftime("[%H:%M:%S]"), network, sys.exc_info(), traceback.extract_tb(sys.exc_info()[2]))) return "Oops. Error logged." else: return "Bwaha. You can't trigger that!" if not len(pairs): if trigger in favorites.FavoriteCommands.instance.favorites.keys(): return favorites.FavoriteCommands.instance.trig_fav(bot, source, target, 'fav', trigger + ' ' + arguments)
def lith_course_info(code, programme, year): # Fetch the study handbook page for the course response = utility.read_url(sh_url(code, year)) sh_data = response["data"] # Locate the Swedish course name m = re.search( "\<span class=\"txtbold\"\>\<b\>(.*?), \d{1,2},?\d? p \</b\>", sh_data) if m: name = utility.unescape(m.group(1)) else: return "Hmm, are you sure the course code is " + code + "?" # Locate the English course name m = re.search( "\<br\>/(.*?)/\</b\>\</span\>", sh_data) if m: name = name + " (" + utility.unescape(m.group(1)) + ")" else: error_handler.output_message( "I couldn't find the English name of the LiTH course " + code + " O.o") # Locate the number of HP (ECTS) credits m = re.search( "\<span class=\"txtbold\"\>\<b\> (\d{1,2},?\d?) hp\</span\>\</font\>", sh_data) if m: credits = m.group(1).replace(",", ".") else: credits = "???" #print "I couldn't find the number of credits for the LiTH course " + \ # code + " O.o" # Locate the advancement level m = re.search( "\<span class=\"txtkursivlista\"\>Utbildningsnivå \(G1,G2,A\):\<\/span\>\<i\> \<\/i\>\<span class=\"txtlista\"\>(.+?)\<\/span\>", sh_data) if m: level = m.group(1) else: level = "???" # Fetch the schedule page for the course from the study handbook response = utility.read_url(schedule_url(code, programme, year)) sh_data = response["data"] # Match study periods # (Usually ([HV]t[12]) but some odd courses have other formats, e.g. Ht2a) period_m = re.findall( "\<td\>\<span class=\"txtlista\"\>\d([HV]t.*?)\</span\>\</td\>", sh_data) # Match blocks # (Usually ([1-4]) but some courses have other formats, e.g. "-", "" or "1+2+4") block_m = re.findall( "&-Token.ksk=\[Field:'k_kurskod'\]\"\>---\>" + "(.*?)" + \ "\<!---\<\/a\>---\>\<\/span\>\<\/td\>", sh_data) # Build a list of schedule occasions schedules = [] for i in range(len(period_m)): # Assemble an occasion string match = period_m[i] + "." if not block_m[i]: match += "?" else: match += block_m[i].replace(", ", "+") # Add if not already present if match not in schedules: schedules.append(match) # Convert it into a string if schedules: schedule_text = "Scheduled during " + ", ".join(sorted(schedules)) else: schedule_text = "Not scheduled " + year + "." # Combine all the information and return it return code + ": " + name + ", " + credits + " HP on level " + level + \ ". " + schedule_text + " | " + sh_url(code, year)