class SendEmail(object): def __init__(self): self.mail_host = ReadData().get_sendEmail_info()['mail_host'] self.mail_port = ReadData().get_sendEmail_info()['mail_port'] self.mail_account = ReadData().get_sendEmail_info()['mail_account'] self.mail_pwd = ReadData().get_sendEmail_info()['mail_pwd'] self.mail_sender = ReadData().get_sendEmail_info()['mail_sender'] self.mail_receiver = ReadData().get_sendEmail_info()['mail_receiver'] self.test_report = self.get_filepath() self.logger = Logger().get_log() @staticmethod def get_filepath(): # 获取上一级目录 parent_path = os.path.dirname(os.path.dirname(__file__)) # 定位到目录 interfaceTest\report test_report = parent_path + '/report' return test_report def get_filename(self): lists = os.listdir(self.test_report) # 列出目录的下所有文件和文件夹保存到lists # print(lists) lists.sort(key=lambda fn: os.path.getmtime(self.test_report + "\\" + fn )) # 按时间排序 file_name = os.path.join(self.test_report, lists[-1]) # 获取最新的文件保存到file_new print(file_name) return file_name def send_report(self): # 获取实例化对象 message = MIMEMultipart() # 邮件主题 subject = 'SaaS接口自动化测试报告' message['Subject'] = Header(subject, 'utf-8') message["from"] = self.mail_sender message["to"] = self.mail_receiver # 邮件正文有三个参数:第一个为文本内容,第二个 plain 设置文本格式,第三个 utf-8 设置编码 message.attach(MIMEText('最新测试报告如下:', 'plain', 'utf-8')) # 构造附件 att = MIMEText( open(self.get_filename(), 'rb').read(), 'base64', 'utf-8') att['Content-Type'] = 'application/octet-stream' att['Content-Disposition'] = 'attachment;filename=interface_report.html' message.attach(att) try: mail_service = smtplib.SMTP_SSL(self.mail_host, self.mail_port) mail_service.login(self.mail_account, self.mail_pwd) mail_service.sendmail(self.mail_sender, self.mail_receiver, message.as_string()) mail_service.quit() print('邮件发送成功') except smtplib.SMTPException as e: self.logger.warning('send failed, reason is %s' % e) print('邮件发送失败,reason is %s' % e)
class JobScheduler: def __init__(self): self.logger = Logger("setting_manager") self.jobs = [] self.job_id_index = 0 def inject(self, registry): pass def start(self): pass def check_for_scheduled_jobs(self, timestamp): while self.jobs and self.jobs[0]["time"] <= timestamp: try: job = self.jobs.pop(0) job["callback"](job["time"], *job["args"], **job["kwargs"]) except Exception as e: self.logger.warning("Error processing scheduled job", e) def delayed_job(self, callback, delay, *args, **kwargs): return self.scheduled_job(callback, int(time.time()) + delay, *args, **kwargs) def scheduled_job(self, callback, scheduled_time, *args, **kwargs): job_id = self._get_next_job_id() new_job = { "id": job_id, "callback": callback, "args": args, "kwargs": kwargs, "time": scheduled_time } self._insert_job(new_job) return job_id def cancel_job(self, job_id): for index, job in enumerate(self.jobs): if job["id"] == job_id: return self.jobs.pop(index) return None def _insert_job(self, new_job): for index, job in enumerate(self.jobs): if job["time"] > new_job["time"]: self.jobs.insert(index, new_job) return self.jobs.append(new_job) def _get_next_job_id(self): self.job_id_index += 1 return self.job_id_index
class DB: def __init__(self): self.connection = None self.client = None self.logger = Logger("MongoDB") def connect(self, host, name): self.connection = MongoClient(host) self.client = self.connection[name] # Test db connection on start up, do not remove self.connection.admin.command('ismaster') def insert(self, table, query): try: return self.client[table].insert_one(query) except DuplicateKeyError: self.logger.warning('Duplicate error') return False def update(self, table, target, update): return self.client[table].update_one(target, {"$set": update}) def update_all(self, table, target, update): return self.client[table].update_many(target, {"$set": update}) def find(self, table, query): return self.client[table].find_one(query) def find_and_update(self, table, target, update): return self.client[table].find_one_and_update(target, {"$set": update}) def find_all(self, table, query): return self.client[table].find(query) def delete(self, table, query): return self.client[table].delete_one(query) def delete_all(self, table, query): return self.client[table].delete_many(query)
class Cutout: def __init__(self, survey, position, radius, **kwargs): self.survey = survey self.position = position self.ra = self.position.ra.to_value(u.deg) self.dec = self.position.dec.to_value(u.deg) self.radius = radius self.basesurvey = kwargs.get('basesurvey', 'racsI') self.psf = kwargs.get('psf') self.cmap = kwargs.get('cmap', 'gray_r') self.color = 'k' if self.cmap == 'hot' else 'black' self.band = kwargs.get('band', 'g') level = 'DEBUG' if kwargs.get('verbose') else 'INFO' self.logger = Logger(__name__, kwargs.get('log'), streamlevel=level).logger self.logger.propagate = False self.kwargs = kwargs try: self._get_cutout() except Exception as e: msg = f"{survey} failed: {e}" raise FITSException(msg) finally: if 'racs' not in self.survey and 'vast' not in self.survey: self.plot_sources = False self.plot_neighbours = False def __repr__(self): return f"Cutout({self.survey}, ra={self.ra:.2f}, dec={self.dec:.2f})" def _get_source(self): try: pattern = re.compile(r'\S*(\d{4}[+-]\d{2}[AB])\S*') selpath = SURVEYS.loc[self.survey]['selavy'] sel = glob.glob(f'{selpath}/*components.txt') sel = [s for s in sel if pattern.sub(r'\1', self.filepath) in s] if len(sel) > 1: df = pd.concat([pd.read_fwf(s, skiprows=[ 1, ]) for s in sel]) else: df = pd.read_fwf(sel[0], skiprows=[ 1, ]) coords = SkyCoord(df.ra_deg_cont, df.dec_deg_cont, unit=u.deg) d2d = self.position.separation(coords) df['d2d'] = d2d sources = df.iloc[np.where(d2d.deg < 0.5 * self.radius)[0]] sources = sources.sort_values('d2d', ascending=True) if any(sources.d2d < self.pos_err / 3600): self.source = sources.iloc[0] self.neighbours = sources.iloc[1:] self.plot_sources = True else: self.source = None self.neighbours = sources self.plot_sources = False self.plot_neighbours = self.kwargs.get('neighbours', True) self.logger.debug(f'Source: \n {self.source}') if len(self.neighbours) > 0: nn = self.neighbours.iloc[0] self.logger.debug( f'Nearest neighbour coords: \n {nn.ra_deg_cont, nn.dec_deg_cont}' ) self.logger.debug( f'Nearest 5 Neighbours \n {self.neighbours.head()}') except IndexError: self.plot_sources = False self.plot_neighbours = False self.logger.warning('No nearby sources found.') def _get_cutout(self): if not os.path.exists(cutout_cache + self.survey): msg = f"{cutout_cache}{self.survey} cutout directory does not exist, creating." self.logger.info(msg) os.makedirs(cutout_cache + self.survey) if os.path.isfile(self.survey): self._get_local_cutout() elif 'racs' in self.survey or 'vast' in self.survey or 'vlass' in self.survey: self._get_local_cutout() elif self.survey == 'skymapper': self._get_skymapper_cutout() elif self.survey == 'panstarrs': self._get_panstarrs_cutout() elif self.survey == 'decam': self._get_decam_cutout() else: self._get_skyview_cutout() def _get_local_cutout(self): """Fetch cutout data via local FITS images (e.g. RACS / VLASS).""" fields = self._find_image() assert len( fields ) > 0, f"No fields located at {self.position.ra:.2f}, {self.position.dec:.2f}" closest = fields[fields.dist_field_centre == fields.dist_field_centre.min()].iloc[0] image_path = SURVEYS.loc[self.survey]['images'] if self.survey == 'vlass': filepath = f'{closest.epoch}/{closest.tile}/{closest.image}/{closest.filename}' image_path = vlass_path elif 'racs' in self.survey: pol = self.survey[-1] if on_system == 'ada': filepath = f'RACS_test4_1.05_{closest.field}.fits' else: filepath = f'RACS_{closest.field}.EPOCH00.{pol}.fits' elif 'vast' in self.survey: pattern = re.compile(r'vastp(\dx*)([IV])') epoch = pattern.sub(r'\1', self.survey) pol = pattern.sub(r'\2', self.survey) filepath = f'VAST_{closest.field}.EPOCH0{epoch}.{pol}.fits' else: filepath = f'*{closest.field}*0.restored.fits' try: self.filepath = glob.glob(image_path + filepath)[0] except IndexError: raise FITSException( f'Could not match {self.survey} image filepath: \n{image_path + filepath}' ) with fits.open(self.filepath) as hdul: self.header, data = hdul[0].header, hdul[0].data wcs = WCS(self.header, naxis=2) self.mjd = Time(self.header['DATE']).mjd try: cutout = Cutout2D(data[0, 0, :, :], self.position, self.radius * u.deg, wcs=wcs) except IndexError: cutout = Cutout2D(data, self.position, self.radius * u.deg, wcs=wcs) self.data = cutout.data * 1000 self.wcs = cutout.wcs if 'racs' in self.survey or 'vast' in self.survey: self.pos_err = SURVEYS.loc[self.basesurvey].pos_err self._get_source() else: # Probably using vlass, yet to include aegean catalogs self.plot_sources = False self.plot_neighbours = False def _get_panstarrs_cutout(self): """Fetch cutout data via PanSTARRS DR2 API.""" path = cutout_cache + 'panstarrs/{}_{}arcmin_{}_{}.fits'.format( self.band, '{:.3f}', '{:.3f}', '{:.3f}', ) imgpath = path.format(self.radius * 60, self.ra, self.dec) if not os.path.exists(imgpath): pixelrad = int(self.radius * 120 * 120) service = "https://ps1images.stsci.edu/cgi-bin/ps1filenames.py" url = ( f"{service}?ra={self.ra}&dec={self.dec}&size={pixelrad}&format=fits" f"&filters=grizy") table = Table.read(url, format='ascii') msg = f"No PS1 image at {self.position.ra:.2f}, {self.position.dec:.2f}" assert len(table) > 0, msg urlbase = ( f"https://ps1images.stsci.edu/cgi-bin/fitscut.cgi?" f"ra={self.ra}&dec={self.dec}&size={pixelrad}&format=fits&red=" ) flist = ["yzirg".find(x) for x in table['filter']] table = table[np.argsort(flist)] for row in table: self.mjd = row['mjd'] filt = row['filter'] url = urlbase + row['filename'] path = cutout_cache + 'panstarrs/{}_{}arcmin_{}_{}.fits'.format( filt, '{:.3f}', '{:.3f}', '{:.3f}', ) path = path.format(self.radius * 60, self.ra, self.dec) img = requests.get(url, allow_redirects=True) if not os.path.exists(path): with open(path, 'wb') as f: f.write(img.content) with fits.open(imgpath) as hdul: self.header, self.data = hdul[0].header, hdul[0].data self.wcs = WCS(self.header, naxis=2) def _get_skymapper_cutout(self): """Fetch cutout data via Skymapper API.""" path = cutout_cache + self.survey + '/dr2_jd{:.3f}_{:.3f}arcmin_{:.3f}_{:.3f}' linka = 'http://api.skymapper.nci.org.au/aus/siap/dr2/' linkb = 'query?POS={:.5f},{:.5f}&SIZE={:.3f}&BAND=all&RESPONSEFORMAT=CSV' linkc = '&VERB=3&INTERSECT=covers' sm_query = linka + linkb + linkc link = linka + 'get_image?IMAGE={}&SIZE={}&POS={},{}&FORMAT=fits' table = requests.get(sm_query.format(self.ra, self.dec, self.radius)) df = pd.read_csv(io.StringIO(table.text)) assert len( df ) > 0, f'No Skymapper image at {self.position.ra:.2f}, {self.position.dec:.2f}' df = df[df.band == 'z'] self.mjd = df.iloc[0]['mjd_obs'] link = df.iloc[0].get_image img = requests.get(link) path = path.format(self.mjd, self.radius * 60, self.ra, self.dec) if not os.path.exists(path): with open(path, 'wb') as f: f.write(img.content) with fits.open(path) as hdul: self.header, self.data = hdul[0].header, hdul[0].data self.wcs = WCS(self.header, naxis=2) def _get_decam_cutout(self): """Fetch cutout data via DECam LS API.""" size = int(self.radius * 3600 / 0.262) if size > 512: size = 512 maxradius = size * 0.262 / 3600 self.logger.warning( f"Using maximum DECam LS cutout radius of {maxradius:.3f} deg") link = f"http://legacysurvey.org/viewer/fits-cutout?ra={self.ra}&dec={self.dec}" link += f"&size={size}&layer=dr8&pixscale=0.262&bands={self.band}" img = requests.get(link) path = cutout_cache + self.survey + '/dr8_jd{:.3f}_{:.3f}arcmin_{:.3f}_{:.3f}_{}band' path = path.format(self.mjd, self.radius * 60, self.ra, self.dec, self.band) if not os.path.exists(path): with open(path, 'wb') as f: f.write(img.content) with fits.open(path) as hdul: self.header, self.data = hdul[0].header, hdul[0].data self.wcs = WCS(self.header, naxis=2) msg = f"No DECam LS image at {self.position.ra:.2f}, {self.position.dec:.2f}" assert self.data is not None, msg def _get_skyview_cutout(self): """Fetch cutout data via SkyView API.""" sv = SkyView() path = cutout_cache + self.survey + '/{:.3f}arcmin_{:.3f}_{:.3f}.fits' path = path.format(self.radius * 60, self.ra, self.dec) progress = self.kwargs.get('progress', False) if not os.path.exists(path): skyview_key = SURVEYS.loc[self.survey].sv try: hdul = sv.get_images(position=self.position, survey=[skyview_key], radius=self.radius * u.deg, show_progress=progress)[0][0] except IndexError: raise FITSException('Skyview image list returned empty.') except ValueError: raise FITSException( f'{self.survey} is not a valid SkyView survey.') except HTTPError: raise FITSException('No response from Skyview server.') with open(path, 'wb') as f: hdul.writeto(f) with fits.open(path) as hdul: self.header, self.data = hdul[0].header, hdul[0].data self.wcs = WCS(self.header, naxis=2) try: self.mjd = Time(self.header['DATE']).mjd except KeyError: try: self.epoch = self.kwargs.get('epoch') msg = "Could not detect epoch, PM correction disabled." assert self.epoch is not None, msg self.mjd = self.epoch if self.epoch > 3000 else Time( self.epoch, format='decimalyear').mjd except AssertionError as e: if self.kwargs.get('pm'): self.logger.warning(e) self.mjd = None self.data *= 1000 def _find_image(self): """Return DataFrame of survey fields containing coord.""" survey = self.survey.replace('I', '').replace('V', '') try: image_df = pd.read_csv(aux_path + f'{survey}_fields.csv') except FileNotFoundError: raise FITSException(f"Missing field metadata csv for {survey}.") beam_centre = SkyCoord(ra=image_df['cr_ra_pix'], dec=image_df['cr_dec_pix'], unit=u.deg) image_df['dist_field_centre'] = beam_centre.separation( self.position).deg pbeamsize = 1 * u.degree if self.survey == 'vlass' else 5 * u.degree return image_df[image_df.dist_field_centre < pbeamsize].reset_index( drop=True) def _obfuscate(self): """Remove all coordinates and identifying information.""" lon = self.ax.coords[0] lat = self.ax.coords[1] lon.set_ticks_visible(False) lon.set_ticklabel_visible(False) lat.set_ticks_visible(False) lat.set_ticklabel_visible(False) lon.set_axislabel('') lat.set_axislabel('') def _plot_setup(self, fig, ax): """Create figure and determine normalisation parameters.""" if ax: self.fig = fig self.ax = ax else: self.fig = plt.figure() self.ax = self.fig.add_subplot(111, projection=self.wcs) if self.kwargs.get('grid', True): self.ax.coords.grid(color='white', alpha=0.5) self.ax.set_xlabel('RA (J2000)') self.ax.set_ylabel('Dec (J2000)') if self.kwargs.get('title', True): self.ax.set_title(SURVEYS.loc[self.survey]['name'], fontdict={ 'fontsize': 20, 'fontweight': 10 }) if self.kwargs.get('obfuscate', False): self._obfuscate() if self.kwargs.get('annotation'): color = 'white' if self.cmap == 'hot' else 'k' self.ax.text(0.05, 0.85, self.kwargs.get('annotation'), color=color, weight='bold', transform=self.ax.transAxes) def _add_cornermarker(self, ra, dec, span, offset): color = 'white' if self.cmap != 'gray_r' else 'r' cosdec = np.cos(np.radians(dec)) raline = Line2D( xdata=[ra + offset / cosdec, ra + span / cosdec], ydata=[dec, dec], color=color, linewidth=2, path_effects=[pe.Stroke(linewidth=3, foreground='k'), pe.Normal()], transform=self.ax.get_transform('world')) decline = Line2D( xdata=[ra, ra], ydata=[dec + offset, dec + span], color=color, linewidth=2, path_effects=[pe.Stroke(linewidth=3, foreground='k'), pe.Normal()], transform=self.ax.get_transform('world')) self.ax.add_artist(raline) self.ax.add_artist(decline) def plot(self, fig=None, ax=None): """Plot survey data and position overlay.""" self.sign = self.kwargs.get('sign', 1) self._plot_setup(fig, ax) self.data *= self.sign absmax = max(self.data.max(), self.data.min(), key=abs) self.logger.debug(f"Max flux in cutout: {absmax:.2f} mJy.") rms = np.sqrt(np.mean(np.square(self.data))) self.logger.debug(f"RMS flux in cutout: {rms:.2f} mJy.") assert (sum((~np.isnan(self.data).flatten())) > 0 and sum(self.data.flatten()) != 0), \ f"No data in {self.survey}" if self.kwargs.get('maxnorm'): self.norm = ImageNormalize(self.data, interval=ZScaleInterval(), vmax=self.data.max(), clip=True) else: self.norm = ImageNormalize(self.data, interval=ZScaleInterval(contrast=0.2), clip=True) self.im = self.ax.imshow(self.data, cmap=self.cmap, norm=self.norm) if self.kwargs.get('bar', True): try: self.fig.colorbar(self.im, label=r'Flux Density (mJy beam$^{-1}$)', ax=self.ax) except UnboundLocalError: self.logger.error( "Colorbar failed. Upgrade to recent version of astropy ") if self.psf: try: self.bmaj = self.header['BMAJ'] * 3600 self.bmin = self.header['BMIN'] * 3600 self.bpa = self.header['BPA'] except KeyError: self.logger.warning('Header did not contain PSF information.') try: self.bmaj = self.psf[0] self.bmin = self.psf[1] self.bpa = 0 self.logger.warning( 'Using supplied BMAJ/BMin. Assuming BPA=0') except ValueError: self.logger.error('No PSF information supplied.') rhs = self.wcs.wcs_pix2world(self.data.shape[0], 0, 1) lhs = self.wcs.wcs_pix2world(0, 0, 1) # Offset PSF marker by the major axis in pixel coordinates try: cdelt = self.header['CDELT1'] except KeyError: cdelt = self.header['CD1_1'] beamavg = (self.bmaj + self.bmin) / 2 beamsize_pix = beamavg / abs(cdelt) / 3600 ax_len_pix = abs(lhs[0] - rhs[0]) / abs(cdelt) / 3600 beam = self.wcs.wcs_pix2world(beamsize_pix, beamsize_pix, 1) self.beamx = beam[0] self.beamy = beam[1] self.beam = Ellipse((self.beamx, self.beamy), self.bmin / 3600, self.bmaj / 3600, -self.bpa, facecolor='white', edgecolor='k', transform=self.ax.get_transform('world'), zorder=10) self.ax.add_patch(self.beam) # Optionally plot square around the PSF # Set size to greater of 110% PSF size or 10% ax length if self.kwargs.get('beamsquare', False): boxsize = max(beamsize_pix * 1.15, ax_len_pix * .1) offset = beamsize_pix - boxsize / 2 self.square = Rectangle( (offset, offset), boxsize, boxsize, facecolor='white', edgecolor='k', # transform=self.ax.get_transform('world'), zorder=5) self.ax.add_patch(self.square) if self.plot_sources: if self.kwargs.get('corner'): self._add_cornermarker( self.source.ra_deg_cont, self.source.dec_deg_cont, self.kwargs.get('corner_span', 20 / 3600), self.kwargs.get('corner_offset', 10 / 3600)) else: self.sourcepos = Ellipse( (self.source.ra_deg_cont, self.source.dec_deg_cont), self.source.min_axis / 3600, self.source.maj_axis / 3600, -self.source.pos_ang, facecolor='none', edgecolor='r', ls=':', lw=2, transform=self.ax.get_transform('world')) self.ax.add_patch(self.sourcepos) else: if self.kwargs.get('corner'): self._add_cornermarker( self.ra, self.dec, self.kwargs.get('corner_span', 20 / 3600), self.kwargs.get('corner_offset', 10 / 3600)) else: self.bmin = 15 self.bmaj = 15 self.bpa = 0 overlay = SphericalCircle( (self.ra * u.deg, self.dec * u.deg), self.bmaj * u.arcsec, edgecolor='r', linewidth=2, facecolor='none', transform=self.ax.get_transform('world')) self.ax.add_artist(overlay) if self.plot_neighbours: for idx, neighbour in self.neighbours.iterrows(): n = Ellipse((neighbour.ra_deg_cont, neighbour.dec_deg_cont), neighbour.min_axis / 3600, neighbour.maj_axis / 3600, -neighbour.pos_ang, facecolor='none', edgecolor='c', ls=':', lw=2, transform=self.ax.get_transform('world')) self.ax.add_patch(n) def save(self, path, fmt='png'): """Save figure with tight bounding box.""" self.fig.savefig(path, format=fmt, bbox_inches='tight') def savefits(self, path): """Export FITS cutout to path""" header = self.wcs.to_header() hdu = fits.PrimaryHDU(data=self.data, header=header) hdu.writeto(path)
class CommandManager: PRIVATE_CHANNEL = "priv" ORG_CHANNEL = "org" PRIVATE_MESSAGE = "msg" def __init__(self): self.handlers = collections.defaultdict(list) self.logger = Logger("command_manager") self.channels = {} self.ignore_regexes = [ re.compile(" is AFK \(Away from keyboard\) since ", re.IGNORECASE), re.compile("I am away from my keyboard right now", re.IGNORECASE), re.compile("Unknown command or access denied!", re.IGNORECASE), re.compile("I am responding", re.IGNORECASE), re.compile("I only listen", re.IGNORECASE), re.compile("Error!", re.IGNORECASE), re.compile("Unknown command input", re.IGNORECASE), re.compile("You have been auto invited", re.IGNORECASE), ] def inject(self, registry): self.db = registry.get_instance("db") self.util: Util = registry.get_instance("util") self.access_manager: AccessManager = registry.get_instance("access_manager") self.bot: Mangopie = registry.get_instance("mangopie") self.character_manager: CharacterManager = registry.get_instance("character_manager") self.setting_manager: SettingManager = registry.get_instance("setting_manager") self.command_alias_manager = registry.get_instance("command_alias_manager") def pre_start(self): self.bot.add_packet_handler(server_packets.PrivateMessage.id, self.handle_private_message) self.bot.add_packet_handler(server_packets.PrivateChannelMessage.id, self.handle_private_channel_message) self.register_command_channel("Private Message", self.PRIVATE_MESSAGE) self.register_command_channel("Org Channel", self.ORG_CHANNEL) self.register_command_channel("Private Channel", self.PRIVATE_CHANNEL) def start(self): # process decorators for _, inst in Registry.get_all_instances().items(): for name, method in get_attrs(inst).items(): if hasattr(method, "command"): cmd_name, params, access_level, description, help_file, sub_command = getattr(method, "command") handler = getattr(inst, name) module = self.util.get_module_name(handler) help_text = self.get_help_file(module, help_file) self.register(handler, cmd_name, params, access_level, description, module, help_text, sub_command) def register(self, handler, command, params, access_level, description, module, help_text=None, sub_command=None): command = command.lower() if sub_command: sub_command = sub_command.lower() else: sub_command = "" access_level = access_level.lower() module = module.lower() command_key = self.get_command_key(command, sub_command) if help_text is None: help_text = self.generate_help(command, description, params) if not self.access_manager.get_access_level_by_label(access_level): self.logger.error("Could not add command '%s': could not find access level '%s'" % (command, access_level)) return for channel, label in self.channels.items(): row = self.db.find('command_config', {"command": command, "sub_command": sub_command, 'channel': channel}) if row is None: # add new command commands self.db.insert('command_config', {'command': command, 'sub_command': sub_command, 'access_level': access_level, 'channel': channel, 'module': module, 'verified': 1, 'enabled': 1}) elif 'verified' in row and row['verified']: if row['module'] != module: self.logger.warning("module different for different forms of command '%s' and sub_command '%s'" % ( command, sub_command)) else: # mark command as verified self.db.update('command_config', {'module': module, 'command': command, 'channel': channel, 'access_level': access_level, 'sub_command': sub_command}, {'verified': 1}) # save reference to command handler r = re.compile(self.get_regex_from_params(params), re.IGNORECASE) self.handlers[command_key].append( {"regex": r, "callback": handler, "help": help_text, "description": description, "params": params}) def handle_private_message(self, packet: server_packets.PrivateMessage): # since the command symbol is not required for private messages, # the command_str must have length of at least 1 in order to be valid, # otherwise it is ignored if len(packet.message) < 1 or not self.bot.is_ready(): return for regex in self.ignore_regexes: if regex.search(packet.message): return if packet.message[:1] == '!': command_str = packet.message[1:] else: command_str = packet.message self.process_command( command_str, "msg", packet.char_id, lambda msg: self.bot.send_private_message(packet.char_id, msg)) def handle_private_channel_message(self, packet: server_packets.PrivateChannelMessage): # since the command symbol is required in the private channel, # the command_str must have length of at least 2 in order to be valid, # otherwise it is ignored if len(packet.message) < 2: return symbol = packet.message[:1] command_str = packet.message[1:] if symbol == self.setting_manager.get( "symbol").get_value() and packet.private_channel_id == self.bot.char_id: self.process_command( command_str, "priv", packet.char_id, lambda msg: self.bot.send_private_channel_message(msg)) def process_command(self, message: str, channel: str, char_id, reply): try: command_str, command_args = self.get_command_parts(message) # check for command alias command_alias = self.command_alias_manager.check_for_alias(command_str) if command_alias: command_str, command_args = self.get_command_parts( command_alias + " " + command_args if command_args else command_alias) cmd_configs = self.get_command_configs(command_str, channel, 1) cmd_configs = list(cmd_configs) if cmd_configs: # given a list of cmd_configs that are enabled, see if one has regex that matches incoming command_str cmd_config, matches, handler = self.get_matches(cmd_configs, command_args) if matches: if self.access_manager.check_access(char_id, cmd_config['access_level']): sender = MapObject( {"name": self.character_manager.resolve_char_to_name(char_id), "char_id": char_id}) handler["callback"](channel, sender, reply, self.process_matches(matches, handler["params"])) else: self.access_denied_response(char_id, cmd_config, reply) else: # handlers were found, but no handler regex matched help_text = self.get_help_text(char_id, command_str, channel) if help_text: reply(self.format_help_text(command_str, help_text)) else: reply("Error! Invalid syntax.") else: reply("Error! Unknown command.") except Exception as e: self.logger.error("error processing command: %s" % message, e) reply("There was an error processing your request.") def get_help_text(self, char, command_str, channel): data = self.db.find_all('command_config', {'command': command_str, 'channel': channel, 'enabled': 1}) # filter out commands that character does not have access level for data = filter(lambda row: self.access_manager.check_access(char, row['access_level']), data) def read_help_text(row): command_key = self.get_command_key(row['command'], row['sub_command']) return filter(lambda x: x is not None, map(lambda handler: handler["help"], self.handlers[command_key])) content = "\n\n".join(flatmap(read_help_text, data)) return content if content else None def get_help_file(self, module, help_file): if help_file: try: help_file = "./" + module.replace(".", "/") + "/" + help_file with open(help_file) as f: return f.read().strip() except FileNotFoundError as e: self.logger.error("Error reading help file", e) return None def format_help_text(self, topic, help_text): return ChatBlob("Help (" + topic + ")", help_text) def generate_help(self, command, description, params): return description + ":\n" + "<tab><symbol>" + command + " " + " ".join(map(lambda x: x.get_name(), params)) def access_denied_response(self, char_id, cmd_config, reply): reply("Error! Access denied.") def get_command_key(self, command, sub_command): if sub_command: return command + ":" + sub_command else: return command def get_command_key_parts(self, command_str): parts = command_str.split(":", 1) if len(parts) == 2: return parts[0], parts[1] else: return parts[0], "" def get_matches(self, cmd_configs, command_args): if command_args: command_args = " " + command_args for row in cmd_configs: command_key = self.get_command_key(row['command'], row['sub_command']) handlers = self.handlers[command_key] for handler in handlers: matches = handler["regex"].match(command_args) if matches: return row, matches, handler return None, None, None def process_matches(self, matches, params): groups = list(matches.groups()) processed = [] for param in params: processed.append(param.process_matches(groups)) return processed def format_matches(self, command_args, matches): # convert matches to list m = list(matches.groups()) m.insert(0, command_args) # strip leading spaces for each group, if they group exists return list(map(lambda x: x[1:] if x else x, m)) def get_command_parts(self, message): parts = message.split(" ", 1) if len(parts) == 2: return parts[0].lower(), parts[1] else: return parts[0].lower(), "" def get_command_configs(self, command, channel=None, enabled=1, sub_command=None): query = {"command": command} if channel: query['channel'] = channel if enabled: query['enabled'] = enabled if sub_command: query['sub_command'] = sub_command return self.db.find_all('command_config', query) def get_handlers(self, command_key): return self.handlers.get(command_key, None) def register_command_channel(self, label, value): if value in self.channels: self.logger.error("Could not register command channel '%s': command channel already registered" % value) return self.logger.debug("Registering command channel '%s'" % value) self.channels[value] = label def is_command_channel(self, channel): return channel in self.channels def get_regex_from_params(self, params): # params must be wrapped with line-beginning and line-ending anchors in order to match # when no params are specified (eg. "^$") return "^" + "".join(map(lambda x: x.get_regex(), params)) + "$"
class EventManager: def __init__(self): self.handlers = {} self.logger = Logger("event_manager") self.event_types = [] self.last_timer_event = 0 def inject(self, registry): self.db = registry.get_instance("db") self.util = registry.get_instance("util") def pre_start(self): self.register_event_type("timer") def start(self): # process decorators for _, inst in Registry.get_all_instances().items(): for name, method in get_attrs(inst).items(): if hasattr(method, "event"): event_type, description = getattr(method, "event") handler = getattr(inst, name) module = self.util.get_module_name(handler) self.register(handler, event_type, description, module) def register_event_type(self, event_type): event_type = event_type.lower() if event_type in self.event_types: self.logger.error( "Could not register event type '%s': event type already registered" % event_type) return self.logger.debug("Registering event type '%s'" % event_type) self.event_types.append(event_type) def is_event_type(self, event_base_type): return event_base_type in self.event_types def register(self, handler, event_type, description, module): event_base_type, event_sub_type = self.get_event_type_parts(event_type) module = module.lower() handler_name = self.util.get_handler_name(handler).lower() if event_base_type not in self.event_types: self.logger.error( "Could not register handler '%s' for event type '%s': event type does not exist" % (handler_name, event_type)) return if not description: self.logger.warning( "No description for event_type '%s' and handler '%s'" % (event_type, handler_name)) row = self.db.find('event_config', { 'event_type': event_base_type, 'handler': handler_name }) if row is None: # add new event commands self.db.insert( 'event_config', { 'event_type': event_base_type, 'event_sub_type': event_sub_type, 'handler': handler_name, 'description': description, 'module': module, 'verified': 1, 'enabled': 1, 'next_run': int(time.time()) }) else: # mark command as verified self.db.update('event_config', { 'event_type': event_base_type, 'handler': handler_name }, { 'verified': 1, 'module': module, 'description': description, 'event_sub_type': event_sub_type, }) # load command handler self.handlers[handler_name] = handler def fire_event(self, event_type, event_data=None): event_base_type, event_sub_type = self.get_event_type_parts(event_type) if event_base_type not in self.event_types: self.logger.error( "Could not fire event type '%s': event type does not exist" % event_type) return data = self.db.find_all( 'event_config', { 'event_type': event_base_type, 'event_sub_type': event_sub_type, 'enabled': 1 }) for row in data: handler = self.handlers.get(row['handler'], None) if not handler: self.logger.error( "Could not find handler callback for event type '%s' and handler '%s'" % (event_type, row.handler)) return try: handler(event_type, event_data) except Exception as e: self.logger.error("error processing event '%s'" % event_type, e) def get_event_type_parts(self, event_type): parts = event_type.lower().split(":", 1) if len(parts) == 2: return parts[0], parts[1] else: return parts[0], "" def get_event_type_key(self, event_base_type, event_sub_type): return event_base_type + ":" + event_sub_type def check_for_timer_events(self, timestamp): data = self.db.find('event_config', { 'enabled': 1, 'event_type': 'timer', 'next_run': { '$gte': timestamp } }) if data is not None: for row in data: event_type_key = self.get_event_type_key( row['event_type'], row['event_sub_type']) # timer event run times should be consistent, so we base the next run time off the last run time, # instead of the current timestamp next_run = row['next_run'] + int(row['event_sub_type']) # prevents timer events from getting too far behind, or having a large "catch-up" after # the bot has been offline for a time if next_run < timestamp: next_run = timestamp + int(row['event_sub_type']) self.db.update('event_config', { 'event_type': 'timer', 'handler': row['handler'] }, {'next_run': next_run}) self.fire_event(event_type_key)
class BuddyManager: BUDDY_LOGON_EVENT = "buddy_logon" BUDDY_LOGOFF_EVENT = "buddy_logoff" def __init__(self): self.buddy_list = {} self.buddy_list_size = 1000 self.logger = Logger("Mangopie") def inject(self, registry): self.character_manager: CharacterManager = registry.get_instance( "character_manager") self.bot = registry.get_instance("mangopie") self.event_manager = registry.get_instance("event_manager") def pre_start(self): self.bot.add_packet_handler(server_packets.BuddyAdded.id, self.handle_add) self.bot.add_packet_handler(server_packets.BuddyRemoved.id, self.handle_remove) self.bot.add_packet_handler(server_packets.LoginOK.id, self.handle_login_ok) self.event_manager.register_event_type(self.BUDDY_LOGON_EVENT) self.event_manager.register_event_type(self.BUDDY_LOGOFF_EVENT) def handle_add(self, packet): buddy = self.buddy_list.get(packet.char_id, {"types": []}) buddy["online"] = packet.online self.buddy_list[packet.char_id] = buddy if packet.online == 1: self.event_manager.fire_event(self.BUDDY_LOGON_EVENT, packet) else: self.event_manager.fire_event(self.BUDDY_LOGOFF_EVENT, packet) def handle_remove(self, packet): if packet.char_id in self.buddy_list: if len(self.buddy_list[packet.char_id]["types"]) > 0: self.logger.warning( "Removing buddy %d that still has types %s" % (packet.char_id, self.buddy_list[packet.char_id]["types"])) del self.buddy_list[packet.char_id] def handle_login_ok(self, packet): self.buddy_list_size += 1000 def add_buddy(self, char, _type): char_id = self.character_manager.resolve_char_to_id(char) if char_id and char_id != self.bot.char_id: if char_id not in self.buddy_list: self.bot.send_packet(client_packets.BuddyAdd(char_id, "\1")) self.buddy_list[char_id] = {"online": None, "types": [_type]} else: self.buddy_list[char_id]["types"].append(_type) return True else: return False def remove_buddy(self, char, _type): char_id = self.character_manager.resolve_char_to_id(char) if char_id: if char_id not in self.buddy_list: return False else: if _type in self.buddy_list[char_id]["types"]: self.buddy_list[char_id]["types"].remove(_type) if len(self.buddy_list[char_id]["types"]) == 0: self.bot.send_packet(client_packets.BuddyRemove(char_id)) return True else: return False def get_buddy(self, char): char_id = self.character_manager.resolve_char_to_id(char) return self.buddy_list.get(char_id, None) def is_online(self, char): char_id = self.character_manager.resolve_char_to_id(char) buddy = self.get_buddy(char_id) if buddy is None: return None else: return buddy.get("online", None)
class PorkManager: def __init__(self): self.logger = Logger("pork_manager") def inject(self, registry): self.bot = registry.get_instance("mangopie") self.db = registry.get_instance("db") self.character_manager = registry.get_instance("character_manager") def pre_start(self): self.bot.add_packet_handler(server_packets.CharacterLookup.id, self.update) self.bot.add_packet_handler(server_packets.CharacterName.id, self.update) def start(self): pass def get_character_info(self, char): # if we have entry in database and it is less than a day old, use that char_info = self.get_from_database(char) if char_info and char_info['source'] == "chat_server": char_info = None elif char_info and char_info['last_updated'] > (int(time.time()) - 86400): char_info['source'] += " (cache)" return char_info char_name = self.character_manager.resolve_char_to_name(char) url = "http://people.anarchy-online.com/character/bio/d/%d/name/%s/bio.xml?data_type=json" % ( self.bot.dimension, char_name) r = requests.get(url) try: json = r.json() except ValueError as e: self.logger.warning("Error marshalling value as json: %s" % r.text, e) json = None if json: char_info_json = json[0] org_info_json = json[1] if json[1] else {} char_info = MapObject({ "name": char_info_json["NAME"], "char_id": char_info_json["CHAR_INSTANCE"], "first_name": char_info_json["FIRSTNAME"], "last_name": char_info_json["LASTNAME"], "level": char_info_json["LEVELX"], "breed": char_info_json["BREED"], "dimension": char_info_json["CHAR_DIMENSION"], "gender": char_info_json["SEX"], "faction": char_info_json["SIDE"], "profession": char_info_json["PROF"], "profession_title": char_info_json["PROFNAME"], "ai_rank": char_info_json["RANK_name"], "ai_level": char_info_json["ALIENLEVEL"], "pvp_rating": char_info_json["PVPRATING"], "pvp_title": none_to_empty_string(char_info_json["PVPTITLE"]), "head_id": char_info_json["HEADID"], "org_id": org_info_json.get("ORG_INSTANCE", 0), "org_name": org_info_json.get("NAME", ""), "org_rank_name": org_info_json.get("RANK_TITLE", ""), "org_rank_id": org_info_json.get("RANK", 0), "source": "people.anarchy-online.com" }) self.save_character_info(char_info) return char_info else: # return cached info from database, even tho it's old return char_info def get_character_history(self, char): pass def get_org_info(self, org_id): pass def load_character_info(self, char_id): char_info = self.get_character_info(char_id) if not char_info: char_info = MapObject({ "name": "Unknown:" + str(char_id), "char_id": char_id, "first_name": "", "last_name": "", "level": 0, "breed": "", "dimension": 5, "gender": "", "faction": "", "profession": "", "profession_title": "", "ai_rank": "", "ai_level": 0, "pvp_rating": 0, "pvp_title": "", "head_id": 0, "org_id": 0, "org_name": "", "org_rank_name": "", "org_rank_id": 6, "source": "stub" }) self.save_character_info(char_info) def save_character_info(self, char_info): # Remove old data self.db.delete('player', {'char_id': char_info.char_id}) self.db.insert( 'player', { 'char_id': char_info.char_id, 'name': char_info.name, 'first_name': char_info.first_name, 'last_name': char_info.last_name, 'level': char_info.level, 'breed': char_info.breed, 'gender': char_info.gender, 'faction': char_info.faction, 'profession': char_info.profession, 'profession_title ': char_info.profession_title, 'ai_rank': char_info.ai_rank, 'ai_level': char_info.ai_level, 'org_id': char_info.org_id, 'org_name': char_info.org_name, 'org_rank_name': char_info.org_rank_name, 'org_rank_id': char_info.org_rank_id, 'dimension': char_info.dimension, 'head_id': char_info.head_id, 'pvp_rating': char_info.pvp_rating, 'pvp_title': char_info.pvp_title, 'source': char_info.source, 'last_updated': int(time.time()) }) def get_from_database(self, char): char_id = self.character_manager.resolve_char_to_id(char) return self.db.find('player', {'char_id': char_id}) def update(self, packet): character = self.get_from_database(packet.char_id) if character: if character['name'] != packet.name: self.db.update('player', {'char_id': packet.char_id}, {'name': packet.name}) else: self.db.insert( 'player', { 'char_id': packet.char_id, 'name': packet.name, 'source': 'chat_server', 'last_updated': int(time.time()) })
class SettingManager: def __init__(self): self.logger = Logger("setting_manager") self.settings = {} def inject(self, registry): self.db = registry.get_instance("db") self.util = registry.get_instance("util") def start(self): # process decorators for _, inst in Registry.get_all_instances().items(): for name, method in get_attrs(inst).items(): if hasattr(method, "setting"): setting_name, value, description, obj = getattr(method, "setting") handler = getattr(inst, name) module = self.util.get_module_name(handler) self.register(setting_name, value, description, obj, module) def register(self, name, value, description, setting: SettingType, module): name = name.lower() module = module.lower() setting.set_name(name) setting.set_description(description) if not description: self.logger.warning("No description specified for setting '%s'" % name) row = self.db.find('settings', {"name": name}) if row is None: self.logger.debug("Adding setting '%s'" % name) self.db.insert("settings", { "name": name, "value": value, "description": description, "module": module, "verified": 1 }) # verify default value is a valid value, and is formatted appropriately setting.set_value(value) else: self.logger.debug("Updating setting '%s'" % name) self.db.update('settings', {"name": name}, {"description": description, "verified": 1, "module": module}) self.settings[name] = setting def get_value(self, name): row = self.db.find('settings', {"name": name}) return row['value'] if row else None def set_value(self, name, value): self.db.update('settings', {"name": name}, {"value": value}) def get(self, name): name = name.lower() setting = self.settings.get(name, None) if setting: return setting else: return None