From 16fec3a89894383dbfe511fca29f01a52b85f129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=91=E5=B1=B1?= <2596473306@qq.com> Date: Sun, 1 Jan 2023 18:15:05 +0800 Subject: [PATCH] NGCBot v1.3 --- Api_Server/Api_Server_Main.py | 534 +++++++++++++++++++ BotServer/MainServer.py | 251 +++++++++ BotServer/SendServer.py | 144 +++++ Cache/Cache_Server.py | 54 ++ Config/config.yaml | 221 ++++++++ Db_Server/Db_Point_Server.py | 169 ++++++ Db_Server/Db_User_Server.py | 193 +++++++ Monitor_Server/Monitor_Server_Main.py | 533 ++++++++++++++++++ Output/output.py | 18 + Push_Server/Push_Main_Server.py | 76 +++ README.MD | 298 +++++++++++ README.assets/image-20221212162417977.png | Bin 0 -> 135011 bytes README.assets/image-20221212181409831.png | Bin 0 -> 31740 bytes README.assets/image-20221212182205271.png | Bin 0 -> 43792 bytes README.assets/image-20221212182246271.png | Bin 0 -> 59792 bytes README.assets/image-20221212182337205.png | Bin 0 -> 11956 bytes README.assets/image-20221212182859195.png | Bin 0 -> 70998 bytes README.assets/image-20221212183023378.png | Bin 0 -> 11581 bytes README.assets/image-20221212184032220.png | Bin 0 -> 52229 bytes README.assets/image-20221212184133728.png | Bin 0 -> 30684 bytes README.assets/image-20221212184307342.png | Bin 0 -> 28515 bytes README.assets/image-20221212184317466.png | Bin 0 -> 28687 bytes README.assets/image-20221212185343056.png | Bin 0 -> 19334 bytes README.assets/image-20221212185422047.png | Bin 0 -> 41876 bytes README.assets/image-20221212185519094.png | Bin 0 -> 32652 bytes README.assets/image-20221212190112759.png | Bin 0 -> 67135 bytes README.assets/image-20221212190436791.png | Bin 0 -> 12079 bytes README.assets/image-20221212190457890.png | Bin 0 -> 15901 bytes README.assets/image-20221212190611674.png | Bin 0 -> 68566 bytes README.assets/image-20221212190838294.png | Bin 0 -> 28842 bytes "README.assets/\345\205\263\346\263\250.gif" | Bin 0 -> 320713 bytes Recv_Msg_Dispose/FriendMsg_dispose.py | 81 +++ Recv_Msg_Dispose/RoomMsg_dispose.py | 375 +++++++++++++ main.py | 15 + requirements.txt | 9 + 35 files changed, 2971 insertions(+) create mode 100644 Api_Server/Api_Server_Main.py create mode 100644 BotServer/MainServer.py create mode 100644 BotServer/SendServer.py create mode 100644 Cache/Cache_Server.py create mode 100644 Config/config.yaml create mode 100644 Db_Server/Db_Point_Server.py create mode 100644 Db_Server/Db_User_Server.py create mode 100644 Monitor_Server/Monitor_Server_Main.py create mode 100644 Output/output.py create mode 100644 Push_Server/Push_Main_Server.py create mode 100644 README.MD create mode 100644 README.assets/image-20221212162417977.png create mode 100644 README.assets/image-20221212181409831.png create mode 100644 README.assets/image-20221212182205271.png create mode 100644 README.assets/image-20221212182246271.png create mode 100644 README.assets/image-20221212182337205.png create mode 100644 README.assets/image-20221212182859195.png create mode 100644 README.assets/image-20221212183023378.png create mode 100644 README.assets/image-20221212184032220.png create mode 100644 README.assets/image-20221212184133728.png create mode 100644 README.assets/image-20221212184307342.png create mode 100644 README.assets/image-20221212184317466.png create mode 100644 README.assets/image-20221212185343056.png create mode 100644 README.assets/image-20221212185422047.png create mode 100644 README.assets/image-20221212185519094.png create mode 100644 README.assets/image-20221212190112759.png create mode 100644 README.assets/image-20221212190436791.png create mode 100644 README.assets/image-20221212190457890.png create mode 100644 README.assets/image-20221212190611674.png create mode 100644 README.assets/image-20221212190838294.png create mode 100644 "README.assets/\345\205\263\346\263\250.gif" create mode 100644 Recv_Msg_Dispose/FriendMsg_dispose.py create mode 100644 Recv_Msg_Dispose/RoomMsg_dispose.py create mode 100644 main.py create mode 100644 requirements.txt diff --git a/Api_Server/Api_Server_Main.py b/Api_Server/Api_Server_Main.py new file mode 100644 index 0000000..1f29a61 --- /dev/null +++ b/Api_Server/Api_Server_Main.py @@ -0,0 +1,534 @@ +from Output.output import output +import feedparser +import requests +import urllib3 +import random +import time +import yaml +import os +import re + + +class Api_Server_Main: + def __init__(self): + # 全局header头 + self.headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", + "Accept-Language": "zh-CN,zh;q=0.9", + "Accept-Encoding": "gzip, deflate, br", + # 'Connection':'keep-alive' ,#默认时链接一次,多次爬取之后不能产生新的链接就会产生报错Max retries exceeded with url + "Upgrade-Insecure-Requests": "1", + "Pragma": "no-cache", + "Cache-Control": "no-cache", + "Connection": "close", # 解决Max retries exceeded with url报错 + } + # 忽略HTTPS告警 + urllib3.disable_warnings() + # 获取当前文件路径 + current_path = os.path.dirname(__file__) + + # 配置缓存文件夹路径 + current_list_path = current_path.split('\\') + current_list_path.pop() + self.Cache_path = '/'.join(current_list_path) + '/Cache' + # 初始化读取配置文件 + config = yaml.load(open(current_path + '/../Config/config.yaml', encoding='UTF-8'), yaml.Loader) + self.system_copyright = config['System_Config']['System_Copyright'] + + # 配置文章变量 + self.news_list = '' + + # 读取配置文件 + config = yaml.load(open(current_path + '/../Config/config.yaml', encoding='UTF-8'), yaml.Loader) + self.appid = config['Api_Server']['Api_Config']['Appid'] + self.appsecret = config['Api_Server']['Api_Config']['Appsecret'] + self.key = config['Api_Server']['Api_Config']['Key'] + self.threatbook_key = config['Api_Server']['Api_Config']['ThreatBook_Key'] + + self.pic_apis = config['Api_Server']['Pic_Api'] + self.video_apis = config['Api_Server']['Video_Api'] + self.icp_api = config['Api_Server']['Icp_Api'] + self.extensions_api = config['Api_Server']['Extensions_Api'] + self.attribution_api = config['Api_Server']['Attribution_Api'] + self.whois_api = config['Api_Server']['Whois_Api'] + self.fish_api = config['Api_Server']['Fish_Api'] + self.wether_api = config['Api_Server']['Wether_Api'] + self.dog_api = config['Api_Server']['Dog_Api'] + self.constellation_api = config['Api_Server']['Constellation_Api'] + self.morning_api = config['Api_Server']['Morning_Api'] + self.threatbook_url = config['Api_Server']['ThreatBook_Api'] + + # AI对话接口 + def get_ai(self, keyword): + url = 'https://v.api.aa1.cn/api/api-xiaoai/talk.php?msg={keyword}&type=text'.format(keyword=keyword) + try: + msg = requests.get(url=url, headers=self.headers).text.strip() + except Exception as e: + msg = f'[ERROR]:AI对话接口错误,错误信息:{e}' + return msg + + # 美女图片接口 + def get_pic(self): + output('[-]:正在调用美女图片API接口... ...') + url = random.choice(self.pic_apis) + try: + pic_data = requests.get(url=url, headers=self.headers, timeout=30).content + save_path = self.Cache_path + '/Pic_Cache/' + str(int(time.time() * 1000)) + '.jpg' + with open(file=save_path, mode='wb') as pd: + pd.write(pic_data) + except Exception as e: + msg = f'[ERROR]:美女图片API接口出现错误,错误信息:{e}' + output(msg) + return msg + return save_path + + # 美女视频接口 + def get_video(self): + output('[-]:正在调用美女视频API接口... ...') + url = random.choice(self.video_apis) + try: + try: + src = requests.get(url=url, headers=self.headers).json()['mp4'] + except requests.exceptions.JSONDecodeError: + src = re.findall('src="(.*?)"', requests.get(url=url, headers=self.headers, timeout=20).text)[0] + mp4_url = 'http:' + src + video_data = requests.get(url=mp4_url, headers=self.headers).content + save_path = self.Cache_path + '/Video_Cache/' + str(int(time.time() * 1000)) + '.mp4' + with open(file=save_path, mode='wb') as vd: + vd.write(video_data) + except Exception as e: + msg = f'[ERROR]:美女视频API接口出现错误,错误信息:{e}' + output(msg) + return msg + return save_path + + # 备案查询接口 + def get_icp(self, keyword): + try: + domain = re.findall(r' (\w+.\w+)', keyword)[0] + except Exception as e: + msg = '语法格式:\nICP查询 qq.com' + output(f'[ERROR]:备案查询接口出现错误,错误信息:{e}') + return msg + url = self.icp_api.format(domain) + try: + data = requests.get(url=url, headers=self.headers, timeout=10).json() + except Exception as e: + msg = f'[ERROR]:备案查询接口超时,错误信息:{e}' + output(msg) + return msg + if data['icp'] == '未备案': + return '该域名未备案!' + msg = f'======== 查询信息 ========\nICP备案号:{data["icp"]}\n备案主体:{data["name"]}\n备案类型:{data["tyle"]}\n{"By: #" + self.system_copyright if self.system_copyright else ""}\n========================' + return msg.strip() + + # 后缀名查询接口 + def get_suffix(self, keyword): + try: + word = re.findall(r' (\w+)', keyword)[0] + except Exception as e: + msg = '语法格式:\n后缀名查询 EXE' + output(f'\n[ERROR]:后缀名查询接口出现错误,错误信息:{e}') + return msg + url = self.extensions_api.format(self.key, word) + try: + data = requests.get(url=url, headers=self.headers).json() + except TimeoutError as e: + msg = f'\n[ERROR]:后缀名查询接口超时,错误信息:{e}' + output(msg) + return msg + if data['code'] != 200: + msg = '查询结果为空!' + else: + msg = f'\n======== 查询后缀:{word} ========\n查询结果:{data["result"]["notes"]}\n{"By: #" + self.system_copyright if self.system_copyright else ""}\n============================' + return msg + + # 归属地查询 + def get_attribution(self, keyword): + try: + phone = re.findall(r' (\d+)', keyword)[0] + except Exception as e: + msg = '语法格式:\n归属查询 110' + output(f'\n[ERROR]:归属查询接口出现错误,错误信息:{e}') + return msg + url = self.attribution_api.format(phone) + try: + data = requests.get(url=url, headers=self.headers).json() + except TimeoutError as e: + msg = f'\n[ERROR]:归属查询接口超时,错误信息:{e}' + output(msg) + return msg + if not data['data']['province']: + msg = '查询结果为空!' + else: + msg = f'\n===== 查询信息 =====\n手机号码:{phone}\n省份:{data["data"]["province"]}\n城市:{data["data"]["city"]}\n运营商:{data["data"]["sp"]}\n{"By: #" + self.system_copyright if self.system_copyright else ""}\n=================' + return msg + + # Whois查询接口 + def get_whois(self, keyword): + try: + domain = re.findall(r' (\w+.\w+)', keyword)[0] + except Exception as e: + msg = '语法格式:\nWHOIS查询 qq.com' + output(f'[ERROR]:WHOIS查询接口出现错误,错误信息:{e}') + return msg + url = self.whois_api.format(domain) + try: + source_data = requests.get(url=url, headers=self.headers).text + except TimeoutError as e: + msg = f'\n[ERROR]:WHOIS查询接口超时,错误信息:{e}' + output(msg) + return msg + msg = '\n' + source_data.strip().split('For more information')[0].strip('
').strip() + f"\n{'By: #' + self.system_copyright if self.system_copyright else ''}" + return msg + + # 微步ip查询接口 + def get_threatbook_ip(self, keyword): + try: + keyword = keyword.split(' ')[-1] + except Exception as e: + output(f'[ERROR]:微步ip查询接口出现错误,错误信息:{e}') + reg = r"((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}" + ip_result = re.match(reg, keyword.replace(' ', '').strip()) + if ip_result is None: + msg = "语法格式: \nIP查询 xx.xx.xx.xx" + return msg + elif len(keyword) > 0 and ip_result.group(): + search_ip = ip_result.group() + ips = str(search_ip).split('.') + continuous_bool = True if [i for i in ips if ips[0] != i] else False + if ips[0] in ['127', '192', '0', '224', '240', '255'] or \ + search_ip in ['1.1.1.1', '2.2.2.2', '3.3.3.3', '4.4.4.4', '5.5.5.5', '6.6.6.6', '7.7.7.7', + '8.8.8.8', '9.9.9.9', '10.10.10.10'] or \ + '.'.join(ips[0:2]) in ['169.254', '100.64', '198.51', '198.18', '172.16'] or \ + '.'.join(ips[0:3]) in ['203.0.113'] or \ + ips[-1] in ['255', '254']: + msg = "[微笑]暂不支持查询该地址!" + return msg + if not continuous_bool: + msg = "[微笑]暂不支持查询该地址!" + return msg + try: + + data = { + "apikey": self.threatbook_key, + "resource": search_ip, + } + + resp = requests.post( + self.threatbook_url, + data=data, + timeout=10, + verify=False, + ) + if resp.status_code == 200 and resp.json()["response_code"] == 0: + # 查风险等级 + sec_level = resp.json()["data"]["{}".format(search_ip)]["severity"] + # 查是否恶意IP + is_malicious = resp.json()["data"]["{}".format(search_ip)]["is_malicious"] + # 查可信度 + confidence_level = resp.json()["data"]["{}".format(search_ip)]["confidence_level"] + # 查IP归属国家 + country = resp.json()["data"]["{}".format(search_ip)]["basic"]["location"][ + "country" + ] + # 查IP归属省份 + province = resp.json()["data"]["{}".format(search_ip)]["basic"]["location"][ + "province" + ] + # 查IP归属城市 + city = resp.json()["data"]["{}".format(search_ip)]["basic"]["location"]["city"] + # 将IP归属的国家、省份、城市合并成一个字符串 + location = country + "-" + province + "-" + city + # 查威胁类型 + judgments = "" + for j in resp.json()["data"]["{}".format(search_ip)]["judgments"]: + judgments += j + " " + if is_malicious: + is_malicious_msg = "是" + else: + is_malicious_msg = "否" + msg = f"\n===================\n[+]ip:{search_ip}\n[+]风险等级:{sec_level}\n[+]是否为恶意ip:{is_malicious_msg}\n[+]可信度:{confidence_level}\n[+]威胁类型:{str(judgments)}\n[+]ip归属地:{location}\n更新时间:{resp.json()['data']['{}'.format(search_ip)]['update_time']}\n{'By: #' + self.system_copyright if self.system_copyright else ''}\n===================" + else: + msg = f"[ERROR]:查询失败,返回信息:{resp.json()['verbose_msg']}" + output(msg) + except Exception as e: + output(f"[ERROR]:微步IP查询出错,错误信息:{e}") + msg = f"[ERROR]:查询出错请稍后重试,错误信息:{e}" + return msg + + # 摸鱼日记接口 + def get_fish(self): + output('[-]:正在调用摸鱼日记API接口... ...') + try: + pic_data = requests.get(url=self.fish_api, headers=self.headers, timeout=10).content + save_path = self.Cache_path + '/Fish_Cache/' + str(int(time.time() * 1000)) + '.jpg' + with open(file=save_path, mode='wb') as pd: + pd.write(pic_data) + except Exception as e: + msg = f'[ERROR]:摸鱼日记API接口出现错误,错误信息:{e}' + output(msg) + return msg + return save_path + + # 天气查询接口 + def get_wether(self, keyword): + try: + city = re.findall(r' (\w+)', keyword)[0] + except Exception as e: + msg = '语法格式:\n天气查询 北京' + output(f'\n[ERROR]:天气查询接口出现错误,错误信息:{e}') + return msg + url = self.wether_api.format(self.appid, self.appsecret, city) + try: + data = requests.get(url=url, headers=self.headers).json() + except TimeoutError as e: + msg = f'\n[ERROR]:天气查询接口超时,错误信息:{e}' + output(msg) + return msg + try: + if city != data['city']: + msg = f'城市中不存在:{data["city"]}' + return msg + else: + msg = f'\n今日{data["city"]}天气:{data["wea"]}\n日期:{data["date"]}\n当前温度:{data["tem"]}\n最低温度:{data["tem_day"]}\n风向:{data["win"] + data["win_speed"]}\n风速:{data["win_meter"]}\n湿度:{data["humidity"]}\n{"By: #" + self.system_copyright if self.system_copyright else ""}' + return msg + except Exception as e: + output(f'[ERROR]:天气查询接口出现错误出现错误,错误信息:{e}') + msg = f'城市中不存在:{city}' + return msg + + # 舔狗日记 + def get_dog(self): + url = self.dog_api.format(self.key) + try: + data = requests.get(url=url, headers=self.headers).json() + except TimeoutError as e: + msg = f'\n[ERROR]:舔狗日记接口超时,错误信息:{e}' + output(msg) + return msg + try: + msg = data['newslist'][0]['content'].strip() + except Exception as e: + msg = f'[ERROR]:舔狗日记接口出现错误出现错误,错误信息:{e}' + output(msg) + return msg + + # 星座查询接口 + def get_constellation(self, keyword): + msg = '' + try: + constellation = re.findall(r' (\w+)', keyword)[0] + if '座' not in constellation: + constellation += '座' + except Exception as e: + msg = '语法格式:\n星座查询 白羊座' + output(f'\n[ERROR]:星座查询接口出现错误,错误信息:{e}') + return msg + url = self.constellation_api.format(self.key, constellation) + try: + data = requests.get(url=url, headers=self.headers).json() + except TimeoutError as e: + msg = f'\n[ERROR]:星座查询接口超时,错误信息:{e}' + output(msg) + return msg + for news in data['newslist']: + msg += news['type'] + ':' + news['content'] + '\n' + msg = f'\n星座:{constellation}\n' + msg.strip() + f"\n{'By: #' + self.system_copyright if self.system_copyright else ''}" + return msg + + # 早安寄语 + def get_morning(self): + url = self.morning_api.format(self.key) + try: + data = requests.get(url=url, headers=self.headers).json() + except TimeoutError as e: + msg = f'\n[ERROR]:早安寄语接口超时,错误信息:{e}' + output(msg) + return msg + msg = f'{data["result"]["content"]}' + return msg + + # 早报推送 + def get_freebuf_news(self, ): + str_list = "#FreeBuf早报\n" + try: + rs1 = feedparser.parse('https://www.freebuf.com/feed') + length = len(rs1.entries) + for buf in range(length): + try: + if ( + f'tm_year={time.strftime("%Y")}' + in str(rs1.entries[buf]["published_parsed"]) + and f'tm_mon={time.strftime("%m")}' + in str(rs1.entries[buf]["published_parsed"]) + and f'tm_mday={str(int(time.strftime("%d")) - 1)}' + in str(rs1.entries[buf]["published_parsed"]) + ): + url_f = rs1.entries[buf]["link"] + title_f = ( + rs1.entries[buf]["title_detail"]["value"] + .replace("FreeBuf早报 |", "") + .replace(" ", "") + ) + link4 = "\n" + title_f + "\n" + url_f + "\n" + str_list += link4 + else: + pass + except Exception as e: + output("[ERROR]:获取FreeBuf早报出错,错误信息:{}".format(e)) + break + if len(str_list) == 0: + link6 = "\n今日暂无文章" + str_list += link6 + else: + pass + except Exception as e: + link6 = "\n今日暂无文章" + str_list += link6 + output("ERROR:freebuf {}".format(e)) + str_list += f"\n{self.system_copyright + '整理分享,更多内容请戳:#' + self.system_copyright if self.system_copyright else ''}\n{time.strftime('%Y-%m-%d %X')}" + return str_list + + # 获取先知社区文章 + def get_xz_news(self, ): + str_list = "" + str_list += "#先知社区" + try: + rs1 = feedparser.parse('https://xz.aliyun.com/feed') + length = len(rs1.entries) + for buf in range(length): + try: + if str(time.strftime("%Y-%m-%d")) in str(rs1.entries[buf]["published"]): + url_f = rs1.entries[buf]["link"] + title_f = rs1.entries[buf]["title_detail"]["value"] + link4 = "\n" + title_f + "\n" + url_f + "\n" + str_list += link4 + else: + pass + except Exception as e: + output("[ERROR]:获取先知社区文章出现错误,错误信息:{}".format(e)) + break + if len(str_list) > 10: + self.news_list += str_list + else: + link6 = "#先知社区\n今日暂无文章" + self.news_list += link6 + except Exception as e: + link6 = "#先知社区\n今日暂无文章" + self.news_list += link6 + output("ERROR:先知社区 {}".format(e)) + return f'[-]:爬取先知社区文章出错,错误信息:{e}' + + # 获取奇安信攻防社区文章 + def get_qax_news(self, ): + str_list = "" + str_list += "\n#奇安信攻防社区" + try: + rs1 = feedparser.parse('https://forum.butian.net/Rss') + length = len(rs1.entries) + for buf in range(length): + try: + if str(time.strftime("%Y-%m-%d")) in str(rs1.entries[buf]["published"]): + url_f = rs1.entries[buf]["link"] + title_f = rs1.entries[buf]["title_detail"]["value"] + link4 = "\n" + title_f + "\n" + url_f + "\n" + str_list += link4 + else: + pass + except Exception as e: + output("[ERROR]:爬取奇安信攻防社区文章出错,错误信息:{}".format(e)) + break + if len(str_list) > 10: + self.news_list += str_list + else: + link6 = "\n#奇安信攻防社区\n今日暂无文章" + self.news_list += link6 + except Exception as e: + link6 = "\n#奇安信攻防社区\n今日暂无文章" + self.news_list += link6 + output("[ERROR]:奇安信攻防社区 {}".format(e)) + return f"[-]:爬取奇安信攻防社区文章出错,错误信息:{e}" + + # 获取安全客文章 + def get_anquanke_news(self, ): + str_list = "" + str_list += "\n#安全客" + try: + rs1 = requests.get('https://www.anquanke.com/knowledge', timeout=5, verify=False) + rs1.encoding = "utf-8" + resp_text = ( + rs1.text.replace("\xa9", "") + .replace("\n", "") + .replace(">", "") + .replace(" ", "") + .replace(" ", "") + .replace(" ", "") + ) + newlist = re.findall( + '(.*?) ', + resp_text, + re.S, + ) + timelist = re.findall( + ' (.*?)', + resp_text, + re.S, + ) + for a in range(len(timelist)): + try: + if time.strftime("%Y-%m-%d") in timelist[a]: + link1 = str(newlist[a][1]) + link2 = "https://www.anquanke.com" + str(newlist[a][0]) + link3 = "\n" + str(link1) + "\n" + str(link2) + "\n" + str_list += link3 + else: + pass + except Exception as e: + output("爬取安全客文章出错,错误信息:{}".format(e)) + break + if len(str_list) > 6: + self.news_list += str_list + else: + link6 = "\n#安全客\n今日暂无文章" + self.news_list += link6 + except Exception as e: + link6 = "\n#安全客\n今日暂无文章" + self.news_list += link6 + output("[ERROR]:爬取安全客文章出错,错误信息:{}".format(e)) + return f"[-]:爬取安全客文章出错,错误信息:{e}" + + # 获取各平台安全文章 + def get_safety_news(self, ): + output("[+]:正在爬取安全新闻... ...") + self.get_xz_news() + self.get_qax_news() + self.get_anquanke_news() + output("[+]:获取成功") + self.news_list += f"\n{self.system_copyright + '整理分享,更多内容请戳:#' + self.system_copyright if self.system_copyright else ''}\n{time.strftime('%Y-%m-%d %X')}" + return self.news_list + + # 测试专用 + def demo(self): + # url = 'https://tucdn.wpon.cn/api-girl/' + # data = requests.get(url=url, headers=self.headers).json() + # print(data) + domain = 'qq.com' + text = 'https://v.api.aa1.cn/api/icp/index.php?url={domain}'.format(domain=domain) + print(text) + + +if __name__ == '__main__': + Asm = Api_Server_Main() + # Asm.get_pic() + # Asm.demo() + # Asm.get_video() + # Asm.icp_query(keyword='ICP查询 qq.com') + # Asm.get_suffix(keyword='icp查询 apk') + # Asm.get_attribution(keyword='归属查询 17371963534') + # Asm.get_whois(keyword='whois查询 qq.com') + # Asm.get_wether(keyword='天气查询 123') + # Asm.get_dog() + # Asm.get_constellation('星座查询 白羊座') + Asm.get_morning() diff --git a/BotServer/MainServer.py b/BotServer/MainServer.py new file mode 100644 index 0000000..79a3492 --- /dev/null +++ b/BotServer/MainServer.py @@ -0,0 +1,251 @@ +from Recv_Msg_Dispose.FriendMsg_dispose import FriendMsg_dispose +from Recv_Msg_Dispose.RoomMsg_dispose import RoomMsg_disposes +from Push_Server.Push_Main_Server import Push_Main_Server +from Db_Server.Db_User_Server import Db_User_Server +from concurrent.futures import ThreadPoolExecutor +from BotServer.SendServer import SendServer +from bs4 import BeautifulSoup +from Output.output import * +import websocket +import yaml +import json +import os + + +class MainServers: + + def __init__(self): + # 初始化读取配置文件 + current_path = os.path.dirname(__file__) + config = yaml.load(open(current_path + '/../Config/config.yaml', encoding='UTF-8'), yaml.Loader) + self.ip = config['BotServer']['IP'] + self.port = config['BotServer']['PORT'] + self.system_copyright = config['System_Config']['System_Copyright'] + + # 配置HOOK信息类型 + self.SERVER = f"ws://{self.ip}:{self.port}" + self.HEART_BEAT = 5005 + self.RECV_TXT_MSG = 1 + self.RECV_TXT_CITE_MSG = 49 + self.RECV_PIC_MSG = 3 + self.USER_LIST = 5000 + self.GET_USER_LIST_SUCCSESS = 5001 + self.GET_USER_LIST_FAIL = 5002 + self.TXT_MSG = 555 + self.PIC_MSG = 500 + self.AT_MSG = 550 + self.CHATROOM_MEMBER = 5010 + self.CHATROOM_MEMBER_NICK = 5020 + self.PERSONAL_INFO = 6500 + self.DEBUG_SWITCH = 6000 + self.PERSONAL_DETAIL = 6550 + self.DESTROY_ALL = 9999 + self.JOIN_ROOM = 10000 + self.ATTATCH_FILE = 5003 + + # 启动机器人 + self.ws = websocket.WebSocketApp( + self.SERVER, on_open=self.on_open, on_message=self.on_message, on_error=self.on_error, + on_close=self.on_close + ) + + # 实例化消息服务 + self.Ss = SendServer() + + # 实例化群消息处理类 + self.Rmd = RoomMsg_disposes() + + # 实例化好友消息处理 + self.Fmd = FriendMsg_dispose() + + # 实例化用户数据服务类 + self.Dus = Db_User_Server() + + # Robot初始化执行 + def on_open(self, ws): + # 实例化实时监控类 + self.Pms = Push_Main_Server(ws=self.ws) + pool = ThreadPoolExecutor(5) + pool.submit(self.Pms.run) + + self.get_personal_info() + + # Robot 启动函数 + def Bot_start(self, ): + self.ws.run_forever() + + # Robot 关闭执行 + def on_close(self, ws): + output("The Robot is Closed...") + + # Robot 错误输出 + def on_error(self, ws, error): + output(f"[ERROR]:出现错误,错误信息:{error}") + + # 启动完成输出 + def handle_wxuser_list(self): + output("Bot is Start!") + + # Robot 心跳输出 + def heartbeat(self, msgJson): + output(f'[*]:{msgJson["content"]}') + + # DEBUG选择HOOK信息类型 + def debug_switch(self, ): + qs = { + "id": self.Ss.get_id(), + "type": self.DEBUG_SWITCH, + "content": "off", + "wxid": "ROOT", + } + return json.dumps(qs) + + # 处理缺口 + def handle_nick(self, j): + data = j.content + i = 0 + for d in data: + output(f"nickname:{d.nickname}") + i += 1 + + # 处理所有Roomid + def hanle_memberlist(self, j): + data = j.content + i = 0 + for d in data: + output(f"roomid:{d.roomid}") + i += 1 + + # 销毁全部接口 + def destroy_all(self, ): + qs = { + "id": self.Ss.get_id(), + "type": self.DESTROY_ALL, + "content": "none", + "wxid": "node", + } + return json.dumps(qs) + + # 处理带引用的文字消息 + def handleMsg_cite(self, msgJson): + msgXml = ( + msgJson["content"]["content"] + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + ) + soup = BeautifulSoup(msgXml, "xml") + msgJson = { + "content": soup.select_one("title").text, + "id": msgJson["id"], + "id1": msgJson["content"]["id2"], + "id2": "wxid_fys2fico9put22", + "id3": "", + "srvid": msgJson["srvid"], + "time": msgJson["time"], + "type": msgJson["type"], + "wxid": msgJson["content"]["id1"], + } + self.handle_recv_msg(msgJson) + + # 选择消息类型 + def on_message(self, ws, message): + j = json.loads(message) + resp_type = j["type"] + # switch结构 + action = { + self.CHATROOM_MEMBER_NICK: self.handle_nick, + self.PERSONAL_DETAIL: self.handle_recv_msg, + self.AT_MSG: self.handle_recv_msg, + self.DEBUG_SWITCH: self.handle_recv_msg, + self.PERSONAL_INFO: self.handle_recv_msg, + self.TXT_MSG: self.handle_recv_msg, + self.PIC_MSG: self.handle_recv_msg, + self.CHATROOM_MEMBER: self.hanle_memberlist, + self.RECV_PIC_MSG: self.handle_recv_msg, + self.RECV_TXT_MSG: self.handle_recv_msg, + self.RECV_TXT_CITE_MSG: self.handleMsg_cite, + self.HEART_BEAT: self.heartbeat, + self.USER_LIST: self.handle_wxuser_list, + self.GET_USER_LIST_SUCCSESS: self.handle_wxuser_list, + self.GET_USER_LIST_FAIL: self.handle_wxuser_list, + self.JOIN_ROOM: self.welcome_join, + } + action.get(resp_type, print)(j) + + # 获取获取微信通讯录用户名字和wxid,好友列表 + def get_wx_user_list(self, ): + qs = { + "id": self.Ss.get_id(), + "type": self.USER_LIST, + "content": "user list", + "wxid": "null", + } + # Output(qs) + return json.dumps(qs) + + def get_personal_info(self, ): + # 获取本机器人的信息 + uri = "/api/get_personal_info" + data = { + "id": self.Ss.get_id(), + "type": self.PERSONAL_INFO, + "content": "op:personal info", + "wxid": "null", + } + respJson = self.Ss.send(uri, data) + wechatBotInfo = f""" + + NGCBot登录信息 + + 微信昵称:{json.loads(respJson["content"])['wx_name']} + 微信号:{json.loads(respJson["content"])['wx_code']} + 微信id:{json.loads(respJson["content"])['wx_id']} + 启动时间:{respJson['time']} + {'By: ' + self.system_copyright if self.system_copyright else ''} + """ + output(wechatBotInfo.strip()) + + # 入群欢迎函数 + def welcome_join(self, msgJson): + output(f"收到消息:{msgJson}") + if "邀请" in msgJson["content"]["content"]: + roomid = msgJson["content"]["id1"] + nickname = msgJson["content"]["content"].split('"')[-2] + msg = '\n欢迎新进群的小可爱[烟花]' + if roomid in self.Dus.show_white_room(): + self.ws.send(self.Ss.send_msg(msg=msg, roomid=roomid, wxid='null', + nickname=nickname)) + + # 消息接收函数 + def handle_recv_msg(self, msgJson): + if "wxid" not in msgJson and msgJson["status"] == "SUCCSESSED": + output(f"[*]:消息发送成功!") + return + output(f"收到消息:{msgJson}") + msg = "" + # 判断群聊消息还是私人消息 + if "@chatroom" in msgJson["wxid"]: + # 获取群ID + roomid = msgJson["wxid"] + # 获取发送人ID + senderid = msgJson["id1"] + else: + roomid = None + nickname = "null" + # 获取发送人ID + senderid = msgJson["wxid"] + + # 获取发送者的名字 + nickname = self.Ss.get_member_nick(roomid, senderid) + if roomid: + # 处理微信群消息 + self.Rmd.get_information(msgJson=msgJson, roomid=roomid, senderid=senderid, nickname=nickname, ws=self.ws) + else: + # 处理通讯录好友发送的消息 + self.Fmd.get_information(msgJson=msgJson, senderid=senderid, ws=self.ws) + + +if __name__ == '__main__': + Ms = MainServers() + Ms.Bot_start() diff --git a/BotServer/SendServer.py b/BotServer/SendServer.py new file mode 100644 index 0000000..9ec6c27 --- /dev/null +++ b/BotServer/SendServer.py @@ -0,0 +1,144 @@ +from Output.output import output +import requests +import time +import json +import yaml +import os + + +class SendServer: + + def __init__(self): + # 初始化读取配置文件 + current_path = os.path.dirname(__file__) + config = yaml.load(open(current_path + '/../config/config.yaml', encoding='UTF-8'), yaml.Loader) + self.ip = config['BotServer']['IP'] + self.port = config['BotServer']['PORT'] + + # 配置HOOK信息类型 + self.SERVER = f"ws://{self.ip}:{self.port}" + self.HEART_BEAT = 5005 + self.RECV_TXT_MSG = 1 + self.RECV_TXT_CITE_MSG = 49 + self.RECV_PIC_MSG = 3 + self.USER_LIST = 5000 + self.GET_USER_LIST_SUCCSESS = 5001 + self.GET_USER_LIST_FAIL = 5002 + self.TXT_MSG = 555 + self.PIC_MSG = 500 + self.AT_MSG = 550 + self.CHATROOM_MEMBER = 5010 + self.CHATROOM_MEMBER_NICK = 5020 + self.PERSONAL_INFO = 6500 + self.DEBUG_SWITCH = 6000 + self.PERSONAL_DETAIL = 6550 + self.DESTROY_ALL = 9999 + self.JOIN_ROOM = 10000 + self.ATTATCH_FILE = 5003 + + # 通用消息发送函数 + def send(self, uri, data): + base_data = { + "id": self.get_id(), + "type": "null", + "roomid": "null", + "wxid": "null", + "content": "null", + "nickname": "null", + "ext": "null", + } + base_data.update(data) + url = f'http://{self.ip}:{self.port}/{uri}' + res = requests.post(url, json={"para": base_data}, timeout=5) + return res.json() + + # 定义信息ID + def get_id(self): + return time.strftime("%Y-%m-%d %H:%M:%S") + + # 发送消息函数 + def send_msg(self, msg, wxid="null", roomid='null', nickname="null"): + if roomid != 'null': + msg_type = self.AT_MSG + else: + msg_type = self.TXT_MSG + qs = { + "id": self.get_id(), + "type": msg_type, + "roomid": roomid, + "wxid": wxid, + "content": msg, + "nickname": nickname, + "ext": "null", + } + output(f"[*]:发送消息: {qs}") + return json.dumps(qs) + + # 通用文件发送函数 + def send_file_room(self, file, roomid): + output("[+]:文件发送中... ...") + data = { + "id": self.get_id(), + "type": self.ATTATCH_FILE, + "roomid": "null", + "content": file, + "wxid": roomid, + "nickname": "null", + "ext": "null", + } + url = f"http://{self.ip}:{self.port}/api/sendattatch" + res = requests.post(url, json={"para": data}, timeout=5) + if res.status_code == 200 and res.json()["status"] == "SUCCSESSED": + output("[*]:文件发送成功") + else: + output(f"[ERROR]:出现错误,错误信息:{res.text}") + + # 图片发送函数 + def send_img_room(self, msg, roomid): + output("[+]:图片发送中... ...") + data = { + "id": self.get_id(), + "type": self.PIC_MSG, + "roomid": "null", + "content": msg, + "wxid": roomid, + "nickname": "null", + "ext": "null", + } + url = f"http://{self.ip}:{self.port}/api/sendpic" + res = requests.post(url, json={"para": data}, timeout=5) + if res.status_code == 200 and res.json()["status"] == "SUCCSESSED": + output("[*]:图片发送成功!") + else: + output(f"[ERROR]:出现错误,错误信息:{res.text}") + + # 获取所有群的wxid + def get_memberid(self, ): + uri = 'api/getmemberid' + data = { + 'type': self.CHATROOM_MEMBER, + 'content': 'op:list member' + } + output(self.send(uri, data)) + + # 获取@昵称 或 微信好友的昵称 + def get_member_nick(self, roomid, wxid): + uri = "api/getmembernick" + data = {"type": self.CHATROOM_MEMBER_NICK, "wxid": wxid, "roomid": roomid or "null"} + respJson = self.send(uri, data) + return json.loads(respJson["content"])["nick"] + + # 获取机器人微信ID和微信名字 + def get_bot_info(self, ): + uri = "/api/get_personal_info" + data = { + "id": self.get_id(), + "type": self.PERSONAL_INFO, + "content": "op:personal info", + "wxid": "null", + } + respJson = self.send(uri, data) + bot_wxid = json.loads(respJson["content"])['wx_id'] + return bot_wxid + + diff --git a/Cache/Cache_Server.py b/Cache/Cache_Server.py new file mode 100644 index 0000000..8753a72 --- /dev/null +++ b/Cache/Cache_Server.py @@ -0,0 +1,54 @@ +from Output.output import output +import os + + +class Cache_Server: + + def __init__(self): + # 配置缓存文件存放路径 + current_path = os.path.dirname(__file__) + self.video_cache = current_path + '/Video_Cache' + self.fish_cache = current_path + '/Fish_Cache' + self.pic_cache = current_path + '/Pic_Cache' + self.create_folder() + + def delete_file(self): + output('[+]:缓存清除功能工作中... ...') + if os.path.exists(self.video_cache): + try: + file_lists = list() + file_lists += [self.video_cache + '/' + file for file in os.listdir(self.video_cache)] + file_lists += [self.fish_cache + '/' + file for file in os.listdir(self.fish_cache)] + file_lists += [self.pic_cache + '/' + file for file in os.listdir(self.pic_cache)] + for rm_file in file_lists: + os.remove(rm_file) + except Exception as e: + msg = "[ERROR]:清除缓存时出错,错误信息:{}".format(e) + output(msg) + return msg + msg = "缓存文件已清除!" + return msg + else: + msg = "[-]:缓存文件夹未创建,正在创建缓存文件夹... ..." + output(msg) + self.create_folder() + + def create_folder(self): + if not os.path.exists(self.video_cache): + try: + os.mkdir(self.video_cache) + os.mkdir(self.pic_cache) + os.mkdir(self.fish_cache) + except Exception as e: + msg = '[ERROR]:创建文件夹出错,错误信息:{}'.format(e) + output(msg) + else: + msg = '[+]:缓存文件夹已创建!' + output(msg) + + +if __name__ == '__main__': + Fs = Cache_Server() + # # Fs.create_folder() + Fs.delete_file() + diff --git a/Config/config.yaml b/Config/config.yaml new file mode 100644 index 0000000..64fc6fa --- /dev/null +++ b/Config/config.yaml @@ -0,0 +1,221 @@ +## 机器人服务配置 +BotServer: + IP: 127.0.0.1 + PORT: 5555 + +## 超级管理员配置 +Administrators: + - 'wxid_7bizfilssbwi22' + +## 关键词配置 +Key_Word: + # 触发美女图片关键词 + Pic_Word: + - '图片' + - '美女图片' + # 触发美女视频关键词 + Video_Word: + - '视频' + - '美女视频' + # 触发备案查询关键词 + Icp_Word: + - '备案查询' + - 'ICP查询' + - 'icp查询' + # 触发后缀名查询关键词 + Suffix_Word: + - '后缀名查询' + - '后缀查询' + # 触发归属查询关键词 + Attribution_Word: + - '归属查询' + - '归属地查询' + # 触发WHOIS查询关键词 + Whois_Word: + - 'whois查询' + - 'WHOIS查询' + # 触发摸鱼日记关键词 + Fish_Word: + - '摸鱼日记' + - '摸鱼日历' + # 触发天气查询关键词 + Weather_Word: + - '天气查询' + - '查询天气' + # 触发舔狗日记关键词 + Dog_Word: + - '舔狗日记' + - '舔我' + # 触发星座查询关键词 + Constellation_Word: + - '星座查询' + - '查询星座' + - '运势查询' + - '查询运势' + # 触发早安寄语关键词 + Morning_Word: + - '早安' + # 触发微步IP查询关键词 + ThreatBook_Word: + - 'ip查询' + - 'IP查询' + - '查询ip' + - '查询IP' + - '微步查询' + # 新增管理员关键词 + Add_Admin_Word: + - '添加管理员' + - '添加管理' + - '新增管理员' + # 删除管理员关键词 + Del_Admin_Word: + - '删除管理员' + - '删除管理' + - '移除管理员' + # 新增黑名单群聊关键词 + Add_BlackRoom_Word: + - '拉黑群聊' + - '添加黑名单' + # 移出黑名单群聊关键词 + Del_BlackRoom_Word: + - '解除拉黑' + - '移出黑名单' + # 新增白名单群聊关键词 + Add_WhiteRoom_Word: + - '拉白' + - '添加白名单' + - '开启推送服务' + - '开启推送功能' + # 移出白名单群聊关键词 + Del_WhiteRoom_Word: + - '关闭推送服务' + - '关闭推送功能' + - '移出白名单' + # 触发早报关键词 + Morning_Page: + - '早报' + - '早间咨询' + # 触发晚报关键词 + Evening_Page: + - '晚报' + - '晚间咨询' + +## API接口服务配置 +Api_Server: + Api_Config: + Appid: '45279436' + Appsecret: 'lohAjD4R' + Key: 'e8409cd6401b93d170297440ace30f27' + ThreatBook_Key: '6518ffbfade84bc1a01718f595cefedb23fa2924b51842978b7e0b5a13b02827' + # 扩展名查询API + Extensions_Api: 'https://apis.tianapi.com/targa/index?key={}&word={}' + # 天气查询API + Wether_Api: 'https://www.tianqiapi.com/free/day?appid={}&appsecret={}&city={}' + # 舔狗日记API + Dog_Api: 'http://api.tianapi.com/tiangou/index?key={}' + # 星座查询API + Constellation_Api: 'http://api.tianapi.com/star/index?key={}&astro={}' + # 早安寄语API + Morning_Api: 'https://apis.tianapi.com/zaoan/index?key={}' + # 微步查询API + ThreatBook_Api: 'https://api.threatbook.cn/v3/scene/ip_reputation' + + # 摸鱼日记API + Fish_Api: 'https://api.vvhan.com/api/moyu' + # Whois查询API + Whois_Api: 'https://v.api.aa1.cn/api/whois/index.php?domain={}' + # 归属地查询API + Attribution_Api: 'https://v.api.aa1.cn/api/phone/guishu-api.php?phone={}' + # 备案查询API配置 + Icp_Api: 'https://v.api.aa1.cn/api/icp/index.php?url={}' + # 图片API配置 + Pic_Api: + - 'https://api.vvhan.com/api/girl' + - 'https://v.api.aa1.cn/api/pc-girl_bz/index.php?wpon=ro38d57y8rhuwur3788y3rd' + - 'https://api.vvhan.com/api/mobil.girl' + # 视频API配置 + Video_Api: + - 'https://tucdn.wpon.cn/api-girl/' + - 'https://v.api.aa1.cn/api/api-dy-girl/index.php?aa1=json' + - 'https://v.api.aa1.cn/api/api-girl-11-02/index.php?type=json' + +## 积分功能配置 +Point_Function: + # 签到口令 + Sign_Keyword: '签到:NGC660安全实验室祝大家天天得0day!' + # 签到积分配置 + Sign_Point: 10 + # 积分功能配置 + Function: + ThreatBook_Point: 8 + # 增加积分关键词 + Add_Point_Word: + - '加' + - '+' + # 扣除积分关键词 + Del_Point_Word: + - '减' + - '-' + # 赠送积分关键词 + Give_Point_Word: + - '送' + # 查看积分关键词 + Query_Point: + - '查看积分' + - '积分查询' + +## 定时推送配置 +Timed_Push: + # 早报推送时间 + Morning_Page_Time: '08:00' + # 晚报推送时间 + Evening_Page_Time: '17:00' + # 摸鱼日记推送时间 + Fish_Time: '10:00' + +## 实时监控配置 +Monitor_Server: + # Github令牌 + Github_Token: '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' + # 工具监控 + tools_list: + - 'https://api.github.com/repos/BeichenDream/Godzilla' + - 'https://api.github.com/repos/rebeyond/Behinder' + - 'https://api.github.com/repos/AntSwordProject/antSword' + - 'https://api.github.com/repos/j1anFen/shiro_attack' + - 'https://api.github.com/repos/yhy0/github-cve-monitor' + - 'https://api.github.com/repos/gentilkiwi/mimikatz' + - 'https://api.github.com/repos/ehang-io/nps' + - 'https://api.github.com/repos/chaitin/xray' + - 'https://api.github.com/repos/FunnyWolf/pystinger' + - 'https://api.github.com/repos/L-codes/Neo-reGeorg' + - 'https://api.github.com/repos/shadow1ng/fscan' + - 'https://api.github.com/repos/SafeGroceryStore/MDUT' + - 'https://api.github.com/repos/EdgeSecurityTeam/Vulnerability' + # 关键词监控 + keyword_list: + - 'Sql注入' + - 'cnvd' + - '未授权' + # 用户监控 + user_list: + - 'yhy0' + - 'su18' + - 'BeichenDream' + - 'phith0n' + - 'zhzyker' + - 'lijiejie' + - 'projectdiscovery' + - 'HavocFramework' + +## 系统相关配置 +System_Config: + # 缓存清除关键词配置 + Cache_Config_Word: + - '清除缓存' + - '清空缓存' + # 版权信息配置 + System_Copyright: 'NGC660安全实验室' + # 帮助菜单关键词配置 + Help_Menu: + - '帮助菜单' \ No newline at end of file diff --git a/Db_Server/Db_Point_Server.py b/Db_Server/Db_Point_Server.py new file mode 100644 index 0000000..550c7ed --- /dev/null +++ b/Db_Server/Db_Point_Server.py @@ -0,0 +1,169 @@ +from Output.output import output +import sqlite3 +import yaml +import os + + +class Db_Point_Server: + def __init__(self): + current_path = os.path.dirname(__file__) + # 数据库存放地址 + self.db_file = current_path + '/../Config/Point_db.db' + self.judge_init() + config = yaml.load(open(current_path + '/../Config/config.yaml', encoding='UTF-8'), yaml.Loader) + + # 读取积分配置 + self.sign_point = config['Point_Function']['Sign_Point'] + + # 打开数据库 + def open_db(self): + conn = sqlite3.connect(database=self.db_file, ) + cursor = conn.cursor() + return conn, cursor + + # 关闭数据库 + def close_db(self, conn, cursor): + cursor.close() + conn.close() + + # 判断数据库是否初始化 + def judge_init(self, ): + conn, cursor = self.open_db() + judge_table_sql = '''SELECT name FROM sqlite_master;''' + cursor.execute(judge_table_sql) + data = cursor.fetchall() + if not data: + msg = '[+]:检测到积分数据库未初始化,正在初始化数据库' + self.init_db() + output(msg) + self.close_db(conn, cursor) + + + # 初始化数据库 + def init_db(self): + conn, cursor = self.open_db() + create_point_table_sql = '''CREATE TABLE IF NOT EXISTS points + (wx_id varchar(255), + wx_name varchar(255), + point int(20));''' + create_sign_table_sql = '''CREATE TABLE IF NOT EXISTS sign (wx_id varchar(255), wx_name varchar(255));''' + cursor.execute(create_point_table_sql) + cursor.execute(create_sign_table_sql) + self.close_db(conn, cursor) + output('[*]:积分服务初始化成功!') + + # 初始化新用户 + def init_user(self, wx_id, wx_name): + conn, cursor = self.open_db() + add_user_sql = f'''INSERT INTO points VALUES ('{wx_id}', '{wx_name}', 0);''' + cursor.execute(add_user_sql) + conn.commit() + self.close_db(conn, cursor) + + # 判断用户是否存在 + def judge_user(self, wx_id, sign_bool=False): + conn, cursor = self.open_db() + judge_user_sql = f'''SELECT wx_id FROM points WHERE wx_id='{wx_id}';''' + if sign_bool: + judge_user_sql = f'''SELECT wx_id FROM sign WHERE wx_id='{wx_id}';''' + cursor.execute(judge_user_sql) + data = cursor.fetchall() + if data: + return True + else: + return False + + # 增加积分 + def add_point(self, wx_id, point): + conn, cursor = self.open_db() + add_point_sql = f'''UPDATE points SET point=point+{point} WHERE wx_id='{wx_id}';''' + cursor.execute(add_point_sql) + conn.commit() + self.close_db(conn, cursor) + msg = f'\n基于你的优越表现,+{point}分\n当前未使用积分:{self.query_point(wx_id=wx_id, )}分' + return msg + + # 扣除积分 + def del_point(self, wx_id, point): + conn, cursor = self.open_db() + add_point_sql = f'''UPDATE points SET point=point-{point} WHERE wx_id='{wx_id}';''' + cursor.execute(add_point_sql) + conn.commit() + self.close_db(conn, cursor) + msg = f'\n介于你的近期表现,-{point}分\n当前未使用积分:{self.query_point(wx_id=wx_id, )}分' + return msg + + # 查询积分 + def query_point(self, wx_id): + conn, cursor = self.open_db() + query_point_sql = f'''SELECT point FROM points WHERE wx_id='{wx_id}';''' + cursor.execute(query_point_sql) + data = cursor.fetchone()[0] + return data + + # 签到功能 + def sign(self, wx_id, wx_name): + conn, cursor = self.open_db() + sign_sql = f'''INSERT INTO sign VALUES ('{wx_id}', '{wx_name}');''' + self.add_point(wx_id=wx_id, point=self.sign_point) + cursor.execute(sign_sql) + conn.commit() + self.close_db(conn, cursor) + msg = f'签到成功 + {self.sign_point} 分\n当前可用积分:{self.query_point(wx_id=wx_id)}' + return msg + + # 清空签到表 + def clear_sign(self): + conn, cursor = self.open_db() + clear_sign_sql = 'DELETE FROM sign' + cursor.execute(clear_sign_sql) + conn.commit() + self.close_db(conn, cursor) + + # 积分赠送 + def give_point(self, wx_id, wx_name, at_wx_id, at_wx_name, point): + if self.query_point(wx_id=wx_id) >= self.query_point(wx_id=at_wx_id): + # 赠送人扣除积分 + self.judge_main(wx_id=wx_id, wx_name=wx_name, point=point, del_bool=True) + # 被赠送人增加积分 + self.judge_main(wx_id=at_wx_id, wx_name=at_wx_name, point=point, add_bool=True) + msg = f'\n您已给予 {at_wx_name} {point}分 \n当前可用积分 {self.query_point(wx_id=wx_id)}分' + else: + msg = f'\n您当前的余额不足\n当前可用积分:{self.query_point(wx_id=wx_id)} 分' + give_bool = True + return msg, give_bool + + # 主判断 + def judge_main(self, wx_id, wx_name, point=None, add_bool=False, del_bool=False, sign_bool=False): + msg = '' + if sign_bool: + if not self.judge_user(wx_id=wx_id, sign_bool=True): + if self.judge_user(wx_id=wx_id, ): + msg = self.sign(wx_id=wx_id, wx_name=wx_name, ) + else: + output('[+]:当前用户不存在,正在初始化该用户... ...') + self.init_user(wx_id=wx_id, wx_name=wx_name) + msg = self.sign(wx_id=wx_id, wx_name=wx_name) + elif add_bool: + if self.judge_user(wx_id=wx_id): + msg = self.add_point(wx_id=wx_id, point=point) + else: + output('[+]:当前用户不存在,正在初始化该用户... ...') + self.init_user(wx_id=wx_id, wx_name=wx_name) + msg = self.add_point(wx_id=wx_id, point=point) + elif del_bool: + if self.judge_user(wx_id=wx_id): + msg = self.del_point(wx_id=wx_id, point=point) + else: + output('[+]:当前用户不存在,正在初始化该用户... ...') + self.init_user(wx_id=wx_id, wx_name=wx_name) + msg = self.del_point(wx_id=wx_id, point=point) + return msg + + +if __name__ == '__main__': + Dps = Db_Point_Server() + # Dps.init_db() + msg = Dps.judge_main(wx_id='123123123', wx_name='123', sign_bool=True, point=230) + print(msg) + # Dps.query_point(wx_id='123') diff --git a/Db_Server/Db_User_Server.py b/Db_Server/Db_User_Server.py new file mode 100644 index 0000000..caa01c6 --- /dev/null +++ b/Db_Server/Db_User_Server.py @@ -0,0 +1,193 @@ +from Output.output import output +import sqlite3 +import os + + +class Db_User_Server: + def __init__(self): + current_path = os.path.dirname(__file__) + # 数据库存放地址 + self.db_file = current_path + '/../Config/User_db.db' + self.judge_init() + + # 打开数据库 + def open_db(self): + conn = sqlite3.connect(database=self.db_file, ) + cursor = conn.cursor() + return conn, cursor + + # 关闭数据库 + def close_db(self, conn, cursor): + cursor.close() + conn.close() + + # 判断是否初始化 + def judge_init(self, ): + conn, cursor = self.open_db() + judge_table_sql = '''SELECT name FROM sqlite_master;''' + cursor.execute(judge_table_sql) + data = cursor.fetchall() + if not data: + msg = '[-]:检测到用户数据库未初始化,正在初始化数据库' + output(msg) + self.init_db() + self.close_db(conn, cursor) + + # 初始化数据库 + def init_db(self): + conn, cursor = self.open_db() + create_admin_table_sql = '''CREATE TABLE IF NOT EXISTS admins + (wx_id varchar(255), + wx_name varchar(255), + wx_roomid varchar(255), + wx_room_name varchar(255));''' + create_white_rooms = '''CREATE TABLE IF NOT EXISTS white_rooms + (wx_roomid varchar(255), + wx_room_name varchar(255));''' + create_black_rooms = '''CREATE TABLE IF NOT EXISTS black_rooms + (wx_roomid varchar(255), + wx_room_name varchar(255));''' + cursor.execute(create_admin_table_sql) + cursor.execute(create_white_rooms) + cursor.execute(create_black_rooms) + self.close_db(conn=conn, cursor=cursor) + output('[*]:用户数据库服务初始化成功!') + + # 添加管理员 + def add_admin(self, wx_id, wx_roomid, wx_name, wx_room_name): + if not self.judge_data(wx_id=wx_id, wx_roomid=wx_roomid): + conn, cursor = self.open_db() + add_admin_sql = f'''INSERT INTO admins VALUES ( + '{wx_id}', '{wx_name}', '{wx_roomid}', '{wx_room_name}');''' + cursor.execute(add_admin_sql) + conn.commit() + self.close_db(conn=conn, cursor=cursor) + msg = f'添加管理员 {wx_name} 成功!' + else: + msg = f'管理员 {wx_name} 已存在!' + return msg + + # 删除管理员 + def del_admin(self, wx_id, wx_name, wx_roomid): + if self.judge_data(wx_id=wx_id, wx_roomid=wx_roomid): + conn, cursor = self.open_db() + del_admin_sql = f'''DELETE FROM admins WHERE wx_id='{wx_id}' and wx_roomid='{wx_roomid}';''' + cursor.execute(del_admin_sql) + conn.commit() + self.close_db(conn, cursor) + msg = f'移除管理员 {wx_name} 成功!' + else: + msg = f'管理员 {wx_name} 已移出!' + return msg + + # 查看所有管理员 + def show_admin(self): + conn, cursor = self.open_db() + show_admin_sql = '''SELECT wx_id, wx_roomid FROM admins;''' + cursor.execute(show_admin_sql) + data = cursor.fetchall() + self.close_db(conn, cursor) + msg = [] + for d in data: + msg.append({'wx_id': d[0], 'wx_roomid': d[1]}) + return msg + + # 添加黑名单群聊 + def add_black_room(self, wx_roomid, wx_room_name): + if not self.judge_data(black_bool=True, wx_roomid=wx_roomid): + conn, cursor = self.open_db() + add_black_room_sql = f'''INSERT INTO black_rooms VALUES ('{wx_roomid}', '{wx_room_name}');''' + cursor.execute(add_black_room_sql) + conn.commit() + self.close_db(conn, cursor) + msg = f'{wx_room_name} 群聊已拉黑! ' + else: + msg = '当前群聊已添加至黑名单' + return msg + + # 删除黑名单群聊 + def del_black_room(self, wx_roomid, wx_room_name): + if self.judge_data(wx_roomid=wx_roomid, black_bool=True): + conn, cursor = self.open_db() + del_black_room_sql = f'''DELETE FROM black_rooms WHERE wx_roomid='{wx_roomid}';''' + cursor.execute(del_black_room_sql) + conn.commit() + self.close_db(conn, cursor) + msg = f'移除黑名单群聊 {wx_room_name} 成功!' + else: + msg = '该群聊未被拉黑!' + return msg + + # 查看黑名单群聊 + def show_black_room(self): + conn, cursor = self.open_db() + show_black_room_sql = '''SELECT wx_roomid FROM black_rooms;''' + cursor.execute(show_black_room_sql) + data = cursor.fetchall() + self.close_db(conn, cursor) + msg = list() + for d in data: + msg.append({'wx_roomid': d[0]}) + return msg + + # 添加白名单群聊 + def add_white_room(self, wx_roomid, wx_room_name): + if not self.judge_data(wx_roomid=wx_roomid): + conn, cursor = self.open_db() + add_white_room_sql = f'''INSERT INTO white_rooms VALUES ('{wx_roomid}', '{wx_room_name}');''' + cursor.execute(add_white_room_sql) + conn.commit() + self.close_db(conn, cursor) + msg = f'{wx_room_name} 群聊已开启推送服务!' + else: + msg = '该群聊已开启推送服务!' + return msg + + # 删除白名单群聊 + def del_white_room(self, wx_roomid, wx_room_name): + if self.judge_data(wx_roomid=wx_roomid, ): + conn, cursor = self.open_db() + del_white_room_sql = f'''DELETE FROM white_rooms WHERE wx_roomid='{wx_roomid}';''' + cursor.execute(del_white_room_sql) + conn.commit() + self.close_db(conn, cursor) + msg = f'{wx_room_name} 群聊已关闭推送服务!' + else: + msg = '该群聊未开启推送服务!' + return msg + + # 查看白名单群聊 + def show_white_room(self): + conn, cursor = self.open_db() + show_white_room_sql = '''SELECT wx_roomid FROM white_rooms;''' + cursor.execute(show_white_room_sql) + data = cursor.fetchall() + self.close_db(conn, cursor) + white_rooms = list() + for d in data: + white_rooms.append(d[0]) + return white_rooms + + # 判断表中数据是否存在 True False + def judge_data(self, wx_id=None, wx_roomid=None, black_bool=False): + conn, cursor = self.open_db() + if wx_id: + sql = f'''SELECT wx_id FROM admins WHERE wx_id='{wx_id}' and wx_roomid='{wx_roomid}';''' + elif black_bool: + sql = f'''SELECT wx_roomid FROM black_rooms where wx_roomid='{wx_roomid}';''' + else: + sql = f'''SELECT wx_roomid FROM white_rooms where wx_roomid='{wx_roomid}';''' + cursor.execute(sql) + data = cursor.fetchall() + if data: + return True + else: + return False + + +if __name__ == '__main__': + Dus = Db_User_Server() + # Dus.init_db() + # Dus.add_admin(wx_id='yunyun', wx_name='云云', wx_roomid='123123', wx_room_name='测试') + # Dus.del_admin(wx_id='yunyun', wx_name='云云', wx_roomid='123123') + Dus.show_admin() diff --git a/Monitor_Server/Monitor_Server_Main.py b/Monitor_Server/Monitor_Server_Main.py new file mode 100644 index 0000000..057c7bb --- /dev/null +++ b/Monitor_Server/Monitor_Server_Main.py @@ -0,0 +1,533 @@ +from Db_Server.Db_User_Server import Db_User_Server +from BotServer.SendServer import SendServer +from collections import OrderedDict +from Output.output import output +from lxml import etree +import requests +import datetime +import hashlib +import sqlite3 +import json +import yaml +import time +import re +import os + + +class Monitor_Server_Main: + def __init__(self, ws): + + self.ws = ws + self.Ss = SendServer() + self.Dus = Db_User_Server() + + current_path = os.path.dirname(__file__) + config = yaml.load(open(current_path + '/../Config/config.yaml', encoding='UTF-8'), yaml.Loader) + self.db_file = current_path + '/../Config/Monitor_db.db' + + self.github_headers = { + 'Authorization': f"token {config['Monitor_Server']['Github_Token']}" + } + + self.tools_list = config['Monitor_Server']['tools_list'] + self.keyword_list = config['Monitor_Server']['keyword_list'] + self.user_list = config['Monitor_Server']['user_list'] + + self.judge_init() + + # 打开数据库 + def open_db(self): + conn = sqlite3.connect(self.db_file) + cur = conn.cursor() + return conn, cur + + # 关闭数据库 + def close_db(self, conn, cur): + cur.close() + conn.close() + + # 判断数据库是否初始化 + def judge_init(self, ): + conn, cursor = self.open_db() + judge_table_sql = '''SELECT name FROM sqlite_master;''' + cursor.execute(judge_table_sql) + data = cursor.fetchall() + if not data: + msg = '[+]:检测到积分数据库未初始化,正在初始化数据库' + self.create_database() + output(msg) + self.close_db(conn, cursor) + + # 初始化创建数据库 + def create_database(self, ): + conn, cur = self.open_db() + try: + cur.execute('''CREATE TABLE IF NOT EXISTS cve_monitor + (cve_name varchar(255), + pushed_at varchar(255), + cve_url varchar(255));''') + output("[+]:成功创建CVE监控表") + cur.execute('''CREATE TABLE IF NOT EXISTS keyword_monitor + (keyword_name varchar(255), + pushed_at varchar(255), + keyword_url varchar(255));''') + output("[+]:成功创建关键字监控表") + cur.execute('''CREATE TABLE IF NOT EXISTS redteam_tools_monitor + (tools_name varchar(255), + pushed_at varchar(255), + tag_name varchar(255));''') + output("[+]:成功创建红队工具监控表") + cur.execute('''CREATE TABLE IF NOT EXISTS user_monitor + (repo_name varchar(255));''') + output("[+]:成功创建大佬仓库监控表") + except Exception as e: + output("[ERROR]:创建监控表错误,错误信息:{}".format(e)) + conn.commit() # 数据库存储在硬盘上需要commit 存储在内存中的数据库不需要 + self.close_db(conn, cur) + self.wx_send(text='Github实时推送连接成功!', body='') + + # 根据排序获取本年前20条CVE + def getNews(self, ): + today_cve_info_tmp = [] + try: + # 抓取本年的 + year = datetime.datetime.now().year + api = "https://api.github.com/search/repositories?q=CVE-{}&sort=updated".format(year) + json_str = requests.get(api, headers=self.github_headers, timeout=10).json() + today_date = datetime.date.today() + n = len(json_str['items']) + if n > 20: + n = 20 + for i in range(0, n): + cve_url = json_str['items'][i]['html_url'] + try: + cve_name_tmp = json_str['items'][i]['name'].upper() + cve_name = re.findall('(CVE\-\d+\-\d+)', cve_name_tmp)[0].upper() + pushed_at_tmp = json_str['items'][i]['created_at'] + pushed_at = re.findall('\d{4}-\d{2}-\d{2}', pushed_at_tmp)[0] + if pushed_at == str(today_date): + today_cve_info_tmp.append( + {"cve_name": cve_name, "cve_url": cve_url, "pushed_at": pushed_at}) + except Exception as e: + pass + today_cve_info = OrderedDict() + for item in today_cve_info_tmp: + today_cve_info.setdefault(item['cve_name'], {**item, }) + today_cve_info = list(today_cve_info.values()) + return today_cve_info + except Exception as e: + output(f'[ERROR]:连接Github出错,错误信息:{e}') + return '', '', '' + + def getKeywordNews(self, keyword): + today_keyword_info_tmp = [] + try: + # 抓取本年的 + api = "https://api.github.com/search/repositories?q={}&sort=updated".format(keyword) + json_str = requests.get(api, headers=self.github_headers, timeout=10).json() + today_date = datetime.date.today() + n = len(json_str['items']) + if n > 20: + n = 20 + for i in range(0, n): + keyword_url = json_str['items'][i]['html_url'] + try: + keyword_name = json_str['items'][i]['name'] + pushed_at_tmp = json_str['items'][i]['created_at'] + pushed_at = re.findall('\d{4}-\d{2}-\d{2}', pushed_at_tmp)[0] + if pushed_at == str(today_date): + today_keyword_info_tmp.append( + {"keyword_name": keyword_name, "keyword_url": keyword_url, "pushed_at": pushed_at}) + except Exception as e: + output(f'[ERROR]:出现错误,错误信息:{e}') + today_keyword_info = OrderedDict() + for item in today_keyword_info_tmp: + today_keyword_info.setdefault(item['keyword_name'], {**item, }) + today_keyword_info = list(today_keyword_info.values()) + return today_keyword_info + + except Exception as e: + output(f'[ERROR]:连接Github出错,错误信息:{e}') + return today_keyword_info_tmp + + # 获取到的关键字仓库信息插入到数据库 + def keyword_insert_into_sqlite3(self, data): + conn, cur = self.open_db() + for i in range(len(data)): + try: + keyword_name = data[i]['keyword_name'] + cur.execute( + "INSERT INTO keyword_monitor (keyword_name,pushed_at,keyword_url) VALUES ('{}', '{}', '{}')".format( + keyword_name, data[i]['pushed_at'], data[i]['keyword_url'])) + except Exception as e: + output(f'[ERROR]:关键字仓库信息插入到数据库出错,错误信息:{e}') + self.close_db(conn, cur) + + # 查询数据库里是否存在该关键字仓库的方法 + def query_keyword_info_database(self, keyword_name): + conn, cur = self.open_db() + sql_grammar = "SELECT keyword_name FROM keyword_monitor WHERE keyword_name = '{}';".format(keyword_name) + cursor = cur.execute(sql_grammar) + self.close_db(conn, cur) + return len(list(cursor)) + + # 获取不存在数据库里的关键字信息 + def get_today_keyword_info(self, today_keyword_info_data): + today_all_keyword_info = [] + for i in range(len(today_keyword_info_data)): + try: + today_keyword_name = today_keyword_info_data[i]['keyword_name'] + today_cve_name = re.findall(r'(CVE\-\d+\-\d+)', today_keyword_info_data[i]['keyword_name'].upper()) + if len(today_cve_name) > 0 and self.query_cve_info_database(today_cve_name.upper()) == 1: + pass + Verify = self.query_keyword_info_database(today_keyword_name) + if Verify == 0: + today_all_keyword_info.append(today_keyword_info_data[i]) + except Exception as e: + output(f'[ERROR]:获取不存在数据库里的关键字信息出错,错误信息:{e}') + return today_all_keyword_info + + # 获取到的CVE信息插入到数据库 + def cve_insert_into_sqlite3(self, data): + conn, cur = self.open_db() + for i in range(len(data)): + try: + cve_name = re.findall('(CVE\-\d+\-\d+)', data[i]['cve_name'])[0].upper() + cur.execute( + "INSERT INTO cve_monitor (cve_name,pushed_at,cve_url) VALUES ('{}', '{}', '{}')".format(cve_name, + data[i][ + 'pushed_at'], + data[i][ + 'cve_url'])) + except Exception as e: + output(f'[ERROR]:CVE信息插入到数据库出错,错误信息:{e}') + self.close_db(conn, cur) + + # 查询数据库里是否存在该CVE的方法 + def query_cve_info_database(self, cve_name): + conn, cur = self.open_db() + sql_grammar = "SELECT cve_name FROM cve_monitor WHERE cve_name = '{}';".format(cve_name) + cursor = cur.execute(sql_grammar) + self.close_db(conn, cur) + return len(list(cursor)) + + # 查询数据库里是否存在该tools工具名字的方法 + def query_tools_info_database(self, tools_name): + conn, cur = self.open_db() + sql_grammar = "SELECT tools_name FROM redteam_tools_monitor WHERE tools_name = '{}';".format(tools_name) + cursor = cur.execute(sql_grammar) + return len(list(cursor)) + + # 获取不存在数据库里的CVE信息 + def get_today_cve_info(self, today_cve_info_data): + today_all_cve_info = [] + for i in range(len(today_cve_info_data)): + try: + today_cve_name = re.findall('(CVE\-\d+\-\d+)', today_cve_info_data[i]['cve_name'])[0].upper() + if self.exist_cve(today_cve_name) == 1: + Verify = self.query_cve_info_database(today_cve_name.upper()) + if Verify == 0: + today_all_cve_info.append(today_cve_info_data[i]) + except Exception as e: + output(f'[ERROR]:获取不存在数据库里的CVE信息出错,错误信息:{e}') + return today_all_cve_info + + # 获取红队工具信息插入到数据库 + def tools_insert_into_sqlite3(self, data): + conn, cur = self.open_db() + for i in range(len(data)): + Verify = self.query_tools_info_database(data[i]['tools_name']) + if Verify == 0: + cur.execute( + "INSERT INTO redteam_tools_monitor (tools_name,pushed_at,tag_name) VALUES ('{}', '{}','{}')".format( + data[i]['tools_name'], data[i]['pushed_at'], data[i]['tag_name'])) + conn.commit() + self.close_db(conn, cur) + + # 获取红队工具的名称,更新时间,版本名称信息 + def get_pushed_at_time(self, tools_list): + tools_info_list = [] + for url in tools_list: + try: + tools_json = requests.get(url, headers=self.github_headers, timeout=10).json() + pushed_at_tmp = tools_json['pushed_at'] + pushed_at = re.findall('\d{4}-\d{2}-\d{2}', pushed_at_tmp)[0] # 获取的是API上的时间 + tools_name = tools_json['name'] + api_url = tools_json['url'] + try: + releases_json = requests.get(url + "/releases", headers=self.github_headers, timeout=10).json() + tag_name = releases_json[0]['tag_name'] + except Exception as e: + tag_name = "no releases" + tools_info_list.append( + {"tools_name": tools_name, "pushed_at": pushed_at, "api_url": api_url, "tag_name": tag_name}) + except Exception as e: + output(f'[ERROR]: 获取红队工具的名称,更新时间,版本名称信息出错,错误信息:{e}') + return tools_info_list + + # 根据红队名名称查询数据库红队工具的更新时间以及版本名称并返回 + def tools_query_sqlite3(self, tools_name): + conn, cur = self.open_db() + result_list = [] + sql_grammar = "SELECT pushed_at,tag_name FROM redteam_tools_monitor WHERE tools_name = '{}';".format(tools_name) + cursor = cur.execute(sql_grammar) + for result in cursor: + result_list.append({"pushed_at": result[0], "tag_name": result[1]}) + self.close_db(conn, cur) + return result_list + + # 获取更新了的红队工具在数据库里面的时间和版本 + def get_tools_update_list(self, data): + tools_update_list = [] + for dist in data: + query_result = self.tools_query_sqlite3(dist['tools_name']) + if len(query_result) > 0: + today_tools_pushed_at = query_result[0]['pushed_at'] + if dist['pushed_at'] != today_tools_pushed_at: + # 返回数据库里面的时间和版本 + tools_update_list.append({"api_url": dist['api_url'], "pushed_at": today_tools_pushed_at, + "tag_name": query_result[0]['tag_name']}) + return tools_update_list + + # 监控用户是否新增仓库,不是 fork 的 + def getUserRepos(self, user): + try: + api = "https://api.github.com/users/{}/repos".format(user) + json_str = requests.get(api, headers=self.github_headers, timeout=10).json() + today_date = datetime.date.today() + + for i in range(0, len(json_str)): + created_at = re.findall('\d{4}-\d{2}-\d{2}', json_str[i]['created_at'])[0] + if not json_str[i]['fork'] and created_at == str(today_date): + Verify = self.user_insert_into_sqlite3(json_str[i]['full_name']) + if Verify == 0: + name = json_str[i]['name'] + try: + description = json_str[i]['description'] + except Exception as e: + description = "作者未写描述" + download_url = json_str[i]['html_url'] + text = r'大佬' + r'** ' + user + r' ** ' + r'又分享了一款工具! ' + body = "工具名称: " + name + " \r\n" + "工具地址: " + download_url + " \r\n" + "工具描述: " + "" + description + self.wx_send(text=text, body=body) + except Exception as e: + output(f'[ERROR]:连接Github出错,错误信息:{e}') + + # 获取用户或者组织信息插入到数据库 + def user_insert_into_sqlite3(self, repo_name): + conn, cur = self.open_db() + sql_grammar = "SELECT repo_name FROM user_monitor WHERE repo_name = '{}';".format(repo_name) + Verify = len(list(cur.execute(sql_grammar))) + if Verify == 0: + cur.execute("INSERT INTO user_monitor (repo_name) VALUES ('{}')".format(repo_name)) + self.close_db(conn, cur) + return Verify + + # 获取更新信息并发送到对应社交软件 + def send_body(self, url, query_pushed_at, query_tag_name): + conn, cur = self.open_db() + json_str = requests.get(url + '/releases', headers=self.github_headers, timeout=10).json() + new_pushed_at = \ + re.findall('\d{4}-\d{2}-\d{2}', + requests.get(url, headers=self.github_headers, timeout=10).json()['pushed_at'])[ + 0] + if len(json_str) != 0: + tag_name = json_str[0]['tag_name'] + if query_pushed_at < new_pushed_at: + if tag_name != query_tag_name: + try: + update_log = json_str[0]['body'] + except Exception as e: + update_log = "作者未写更新内容" + download_url = json_str[0]['html_url'] + tools_name = url.split('/')[-1] + text = r'** ' + tools_name + r' ** 工具,版本更新啦!' + body = "工具名称:" + tools_name + "\r\n" + "工具地址:" + download_url + "\r\n" + "工具更新日志:" + "\r\n" + update_log + self.wx_send(text=text, body=body) + sql_grammar = "UPDATE redteam_tools_monitor SET tag_name = '{}' WHERE tools_name='{}'".format( + tag_name, + tools_name) + sql_grammar1 = "UPDATE redteam_tools_monitor SET pushed_at = '{}' WHERE tools_name='{}'".format( + new_pushed_at, tools_name) + cur.execute(sql_grammar) + cur.execute(sql_grammar1) + elif tag_name == query_tag_name: + commits_url = url + "/commits" + commits_url_response_json = requests.get(commits_url).text + commits_json = json.loads(commits_url_response_json) + tools_name = url.split('/')[-1] + download_url = commits_json[0]['html_url'] + try: + update_log = commits_json[0]['commit']['message'] + except Exception as e: + update_log = "作者未写更新内容,具体点击更新详情地址的URL进行查看" + text = r'** ' + tools_name + r' ** 工具小更新了一波!' + body = "工具名称:" + tools_name + "\r\n" + "更新详情地址:" + download_url + "\r\n" + "commit更新日志:" + "\r\n" + update_log + self.wx_send(text=text, body=body) + sql_grammar = "UPDATE redteam_tools_monitor SET pushed_at = '{}' WHERE tools_name='{}'".format( + new_pushed_at, tools_name) + cur.execute(sql_grammar) + else: + if query_pushed_at != new_pushed_at: + json_str = requests.get(url + '/commits', headers=self.github_headers, timeout=10).json() + update_log = json_str[0]['commit']['message'] + download_url = json_str[0]['html_url'] + tools_name = url.split('/')[-1] + text = r'** ' + tools_name + r' ** 工具更新啦!' + body = "工具名称:" + tools_name + "\r\n" + "工具地址:" + download_url + "\r\n" + "commit更新日志:" + "\r\n" + update_log + self.wx_send(text=text, body=body) + sql_grammar = "UPDATE redteam_tools_monitor SET pushed_at = '{}' WHERE tools_name='{}'".format( + new_pushed_at, tools_name) + cur.execute(sql_grammar) + # return update_log, download_url + self.close_db(conn, cur) + + # 创建md5对象 + def nmd5(self, str): + m = hashlib.md5() + b = str.encode(encoding='utf-8') + m.update(b) + str_md5 = m.hexdigest() + return str_md5 + + # 有道翻译 + def translate(self, word): + headerstr = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36' + bv = self.nmd5(headerstr) + lts = str(round(time.time() * 1000)) + salt = lts + '90' + strexample = 'fanyideskweb' + word + salt + 'Y2FYu%TNSbMCxc3t2u^XT' + sign = self.nmd5(strexample) + data = { + 'i': word, + 'from': 'AUTO', + 'to': 'AUTO', + 'smartresult': 'dict', + 'client': 'fanyideskweb', + 'salt': salt, + 'sign': sign, + 'lts': lts, + 'bv': bv, + 'doctype': 'json', + 'version': '2.1', + 'keyfrom': 'fanyi.web', + 'action': 'FY_BY_CLICKBUTTION', + } + url = 'http://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule' + header = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36', + 'Referer': 'http://fanyi.youdao.com/', + 'Origin': 'http://fanyi.youdao.com', + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'X-Requested-With': 'XMLHttpRequest', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Encoding': 'gzip, deflate', + 'Accept-Language': 'zh-CN,zh;q=0.9', + 'Connection': 'keep-alive', + 'Host': 'fanyi.youdao.com', + 'cookie': '_ntes_nnid=937f1c788f1e087cf91d616319dc536a,1564395185984; OUTFOX_SEARCH_USER_ID_NCOO=; OUTFOX_SEARCH_USER_ID=-10218418@11.136.67.24; JSESSIONID=; ___rl__test__cookies=1' + } + res = requests.post(url=url, data=data, headers=header) + result_dict = res.json() + result = "" + for json_str in result_dict['translateResult'][0]: + tgt = json_str['tgt'] + result += tgt + return result + + # 判断是否存在该CVE + def exist_cve(self, cve): + try: + query_cve_url = "https://cve.mitre.org/cgi-bin/cvename.cgi?name=" + cve + response = requests.get(query_cve_url, timeout=10) + html = etree.HTML(response.text) + return 1 + except Exception as e: + return 0 + + # 根据cve 名字,获取描述,并翻译 + def get_cve_des_zh(self, cve): + time.sleep(3) + try: + query_cve_url = "https://cve.mitre.org/cgi-bin/cvename.cgi?name=" + cve + response = requests.get(query_cve_url, timeout=10) + html = etree.HTML(response.text) + des = html.xpath('//*[@id="GeneratedTable"]/table//tr[4]/td/text()')[0].strip() + cve_time = html.xpath('//*[@id="GeneratedTable"]/table//tr[11]/td[1]/b/text()')[0].strip() + return des, cve_time + except Exception as e: + pass + + # 发送CVE信息到社交工具 + def sendNews(self, data): + try: + text = '有新的CVE送达! \r\n** 请自行分辨是否为红队钓鱼!!! **' + # 获取 cve 名字 ,根据cve 名字,获取描述,并翻译 + for i in range(len(data)): + try: + cve_name = re.findall(r'(CVE\-\d+\-\d+)', data[i]['cve_name'])[0].upper() + cve_zh, cve_time = self.get_cve_des_zh(cve_name) + body = "CVE编号: " + cve_name + " --- " + cve_time + " \r\n" + "Github地址: " + str( + data[i]['cve_url']) + "\r\n" + "CVE描述: " + "\r\n" + cve_zh + self.wx_send(text=text, body=body) + except IndexError: + pass + except Exception as e: + output(f'[ERROR]:发送CVE信息到社交工具出现错误,错误信息:{e}') + + # 发送信息到社交工具 + def sendKeywordNews(self, keyword, data): + try: + text = '有新的关键字监控 - {} - 送达! \r\n** 请自行分辨是否为红队钓鱼!!! **'.format(keyword) + # 获取 cve 名字 ,根据cve 名字,获取描述,并翻译 + for i in range(len(data)): + try: + keyword_name = data[i]['keyword_name'] + body = "项目名称: " + keyword_name + "\r\n" + "Github地址: " + str(data[i]['keyword_url']) + "\r\n" + self.wx_send(text, body) + except IndexError: + pass + except Exception as e: + output(f'[ERROR]:发送信息到社交工具出现错误,错误信息:{e}') + + # 发送微信消息 + def wx_send(self, text, body): + roomid_list = self.Dus.show_white_room() + msg = text + '\n\n' + body + for roomid in roomid_list: + self.ws.send(self.Ss.send_msg(msg=msg.strip(), wxid=roomid)) + + # main函数 + def main(self, ): + while True: + output("[*]:Cve 、Github 工具 和 大佬仓库 监控中... ...") + tools_data = self.get_pushed_at_time(self.tools_list) + self.tools_insert_into_sqlite3(tools_data) # 获取文件中的工具列表,并从 github 获取相关信息,存储下来 + + for user in self.user_list: + self.getUserRepos(user) + # CVE部分 + cve_data = self.getNews() + if len(cve_data) > 0: + today_cve_data = self.get_today_cve_info(cve_data) + self.sendNews(today_cve_data) + self.cve_insert_into_sqlite3(today_cve_data) + # 关键字监控 , 最好不要太多关键字,防止 github 次要速率限制 https://docs.github.com/en/rest/overview/resources-in-the-rest-api#secondary-rate-limits= + for keyword in self.keyword_list: + time.sleep(1) # 每个关键字停 1s ,防止关键字过多导致速率限制 + keyword_data = self.getKeywordNews(keyword) + + if len(keyword_data) > 0: + today_keyword_data = self.get_today_keyword_info(keyword_data) + if len(today_keyword_data) > 0: + self.sendKeywordNews(keyword, today_keyword_data) + self.keyword_insert_into_sqlite3(today_keyword_data) + time.sleep(1) + data2 = self.get_pushed_at_time(self.tools_list) # 再次从文件中获取工具列表,并从 github 获取相关信息, + data3 = self.get_tools_update_list(data2) # 与 3 分钟前数据进行对比,如果在三分钟内有新增工具清单或者工具有更新则通知一下用户 + for i in range(len(data3)): + try: + self.send_body(data3[i]['api_url'], data3[i]['pushed_at'], data3[i]['tag_name']) + except Exception as e: + output(f"[ERROR]:main函数 try循环 遇到错误-->{e}") + output('OK') diff --git a/Output/output.py b/Output/output.py new file mode 100644 index 0000000..abb8be9 --- /dev/null +++ b/Output/output.py @@ -0,0 +1,18 @@ +from termcolor import cprint +import time + + +def output(msg): + if "error" in msg or "ERROR" in msg: + color = "red" + elif '[*]' in msg: + color = "cyan" + elif '[+]' in msg: + color = 'yellow' + else: + color = "magenta" + time_now = time.strftime("%Y-%m-%d %X") + cprint(f"[{time_now}]:{msg}", color) + + +output('') diff --git a/Push_Server/Push_Main_Server.py b/Push_Server/Push_Main_Server.py new file mode 100644 index 0000000..d3886d4 --- /dev/null +++ b/Push_Server/Push_Main_Server.py @@ -0,0 +1,76 @@ +from Monitor_Server.Monitor_Server_Main import Monitor_Server_Main +from Api_Server.Api_Server_Main import Api_Server_Main +from Db_Server.Db_Point_Server import Db_Point_Server +from Db_Server.Db_User_Server import Db_User_Server +from BotServer.SendServer import SendServer +from Output.output import output +import schedule +import yaml +import os + + +class Push_Main_Server: + def __init__(self, ws): + current_path = os.path.dirname(__file__) + config = yaml.load(open(current_path + '/../Config/config.yaml', encoding='UTF-8'), yaml.Loader) + self.db_file = current_path + '/../Config/Point_db.db' + + # 实例化用户类 + self.Dus = Db_User_Server() + + # 实例化积分类 + self.Dps = Db_Point_Server() + + # 实例化发送消息服务 + self.Ss = SendServer() + + # 实例化API类 + self.Asm = Api_Server_Main() + + # 实例化ws + self.ws = ws + + self.MSm = Monitor_Server_Main(ws=self.ws) + self.morning_page_time = config['Timed_Push']['Morning_Page_Time'] + self.evening_page_time = config['Timed_Push']['Evening_Page_Time'] + self.fish_time = config['Timed_Push']['Fish_Time'] + + # 早报推送 + def push_morning_page(self, ): + output('[+]:定时早报推送') + roomid_list = self.Dus.show_white_room() + msg = self.Asm.get_freebuf_news() + for roomid in roomid_list: + self.ws.send(self.Ss.send_msg(msg=msg, wxid=roomid)) + + # 晚报推送 + def push_evening_page(self, ): + output('[+]:定时晚间新闻推送') + roomid_list = self.Dus.show_white_room() + msg = self.Asm.get_safety_news() + for roomid in roomid_list: + self.ws.send(self.Ss.send_msg(msg=msg, wxid=roomid)) + + # 摸鱼日记推送 + def push_fish(self, ): + output('[+]:定时摸鱼日记推送') + roomid_list = self.Dus.show_white_room() + msg = self.Asm.get_fish() + for roomid in roomid_list: + self.Ss.send_msg(msg=msg, wxid=roomid) + + def push_clear_sign(self): + output('[+]:定时签到表清空') + self.Dps.clear_sign() + + def run(self): + schedule.every().day.at(self.morning_page_time).do(self.push_morning_page) + schedule.every().day.at(self.evening_page_time).do(self.push_evening_page) + schedule.every().day.at(self.fish_time).do(self.push_fish) + schedule.every().day.at('00:00').do(self.push_clear_sign) + # schedule.every(1).seconds.do(self.MSm.main) + schedule.every(30).minutes.do(self.MSm.main) + output('[*]:已开启定时推送服务!') + while True: + # output('[*]:已开启定时推送服务!') + schedule.run_pending() diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..6913228 --- /dev/null +++ b/README.MD @@ -0,0 +1,298 @@ + +NGCBot +
+ +![image-20221212162417977](./README.assets/image-20221212162417977.png) + ++一个基于✨HOOK机制的微信机器人,支持🌱安全新闻定时推送【FreeBuf,先知,安全客,奇安信攻防社区】,👯后缀名查询,⚡备案查询,⚡手机号归属地查询,⚡WHOIS信息查询,🎉星座查询,⚡天气查询,🌱摸鱼日历⚡微步威胁情报查询, +🐛美女视频,⚡美女图片,👯帮助菜单。📫 支持积分功能,😄自定义程度丰富,小白也可轻松上手! +
+ +## 目录 + +- [介绍](#介绍) +- [项目结构](#项目结构) +- [如何使用](#如何使用) +- [功能介绍](###功能介绍) +- [配置文件说明](#配置文件说明) +- [后续优化计划](#后续优化计划) +- [后续开发计划](#后续开发计划) +- [更新日志](#更新日志) +- [特别鸣谢](#特别鸣谢) + +### 介绍 + + NGCBot是一个基于HOOK拦截机制的微信机器人,用户高强度自定义,支持多种功能,代码逻辑清晰,因为其HOOK机制,目前仅支持Windows版本。目前支持多种功能功能调用,小白也能轻松搭建!👯 + +### 项目结构 + +```shell + _ _ ____ ____ ____ _ + | \ | |/ ___|/ ___| __ ) ___ | |_ + | \| | | _| | | _ \ / _ \| __| + | |\ | |_| | |___| |_) | (_) | |_ + |_| \_|\____|\____|____/ \___/ \__| + +│ main.py ---主服务 +│ README.MD ---README文件 +│ requirements.txt ---依赖库 +│ test.py ---测试专用文件 +│ +├─BotServer --- 机器人服务 +│ │ MainServer.py --- 机器人主服务 +│ │ SendServer.py --- 发送消息服务 +│ │ __init__.py +│ │ +│ └─__pycache__ +│ MainServer.cpython-38.pyc +│ SendServer.cpython-38.pyc +│ __init__.cpython-38.pyc +│ +├─config --- 配置文件夹 +│ config.yaml +│ points.db --- 积分数据库 +│ privilege.db --- 管理数据库 +│ +├─DailyPush --- 定时推送文件夹 +│ │ Daily_push_server.py --- 定时推送服务 +│ │ Push_main_server.py --- 定时推送主服务 +│ │ __init__.py +│ │ +│ └─__pycache__ +│ Daily_push_server.cpython-38.pyc +│ Push_main_server.cpython-38.pyc +│ __init__.cpython-38.pyc +│ +├─Db_server --- 数据库服务文件夹 +│ │ Db_points_server.py --- 积分数据库服务 +│ │ Db_user_server.py --- 管理数据库服务 +│ │ +│ └─__pycache__ +│ Db_points_server.cpython-38.pyc +│ Db_user_server.cpython-38.pyc +│ +├─File +│ │ File_server.py --- 文件操作服务 +│ │ +│ ├─girl_pic --- 美女图片存放目录 +│ ├─girl_video --- 美女视频存放目录 +│ ├─touch_fish --- 摸鱼日历存放目录 +│ └─__pycache__ +│ File_server.cpython-38.pyc +│ +├─Get_api +| │ Api_github_cve_monitor.py --- CVE-Github工具实时监控 +│ │ Api_news_server.py --- 新闻获取服务 +│ │ Api_server.py --- 其它API调用服务 +│ │ __init__.py +│ │ +│ └─__pycache__ +│ Api_news_server.cpython-38.pyc +│ Api_server.cpython-38.pyc +│ __init__.cpython-38.pyc +│ +├─Output --- 信息输出文件夹 +│ │ output.py +│ │ __init__.py +│ │ +│ └─__pycache__ +│ output.cpython-38.pyc +│ __init__.cpython-38.pyc +│ +├─README.assets +│ image-20221212162417977.png +│ +├─recv_msg_dispose --- 接收消息处理服务 +│ │ FriendMsg_dispose.py --- 通讯录好友消息服务 +│ │ RoomMsg_dispose.py --- 群消息服务 +│ │ __init__.py +│ │ +│ └─__pycache__ +│ FriendMsg_dispose.cpython-38.pyc +│ RoomMsg_dispose.cpython-38.pyc +│ __init__.cpython-38.pyc +│ +└─注入器 --- 注入器 + 3.2.1.121-0.0.0.015_稳定版.dll + 3.2.1.121-0.0.0.018.dll + 3.3.0.115-0.0.0.001.dll + 3.6.0.18-0.0.0.004.dll + readme.md + version2.8.0.121-3.5.7.66.dll + version2.9.0.123-4.5.7.73.dll + version3.1.0.66-0.0.0.13.dll + 微信DLL注入器V1.0.3.exe +``` + +### 如何使用 + + 首先你需要一台Windows主机,并且安装`Python3.8`或者以上版本,接下来请安装依赖库 + +```python +pip install -r requirements.txt +``` + +安装完毕之后之后,需要下载`3.2.1.121`版本的微信,在[Releases](https://github.com/ngc660sec/NGCBot/releases/tag/v1.0)里面有提供的版本。登陆微信之后,在项目文件中打开`注入器`文件夹,进行注入 + +![image-20221212181409831](./README.assets/image-20221212181409831.png) + +注入完毕之后,可以启动`main.py`文件 + +```python +python main.py +``` + +若无报错,恭喜🎉,你已经搭建完成! + +### 功能介绍 + +```shell +1. 超级管理员功能 + - 所有功能 +2. 普通管理员功能 + - 添加特权群聊 + - 删除特权群聊 + - 添加黑名单群聊 + - 删除黑名单群聊 + - 添加积分 + - 扣除积分 +3. 普通群聊功能 + - 娱乐功能 + - 积分功能 + - 自定义回复功能 +4. 特权群聊功能 + - 定时推送功能 + - 新人入群提醒 + - 普通群聊功能 + - CVE-Github工具实时监控 +5. 黑名单群聊功能 + - 积分功能 + - 自定义回复功能 +-------------------------------------------------------------------------------------------------------- +积分功能: + - 手机号归属地查询 + - WHOIS查询 + - 备案查询 + - 后缀名查询 + - 微步情报查询 + - 签到 + - 积分查询 +娱乐功能: + - AI对话 + - 美女视频 + - 美女图片 + - 舔狗日记 + - 摸鱼日历 + - 早安寄语 + - 星座运势查询 + - 天气查询 +``` + +### 配置文件说明 + +配置文件在`config`文件夹中,`config.yaml`就是配置文件,打开配置文件,我们需要做以下操作,来保证服务能够正常使用 + +1. 配置超级管理员【可配置多个超管用户】 + + - 运行Bot之后,随便给机器人发一条消息,来获取您的`wxid` + + ![image-20221212182205271](./README.assets/image-20221212182205271.png) + + - 随后在配置文件中添加即可 + + ![image-20221212182246271](./README.assets/image-20221212182246271.png) + + - 若要添加多个超管,请这样操作 + + ![image-20221212182337205](./README.assets/image-20221212182337205.png) + +2. 配置您的API服务 + + - 配置天行API服务,请在[天行数据](https://www.tianapi.com/)中获取自己的相关配置 + + ![image-20221212182859195](./README.assets/image-20221212182859195.png) + + - 配置微步KEY,请在[微步社区](https://x.threatbook.com/)获取您自己的KEY + + ![image-20221212183023378](./README.assets/image-20221212183023378.png) + +3. 管理员配置说明 + + 管理员一共分为两种,一种是超级管理员,一种是普通管理员,超级管理员拥有添加管理,删除管理功能,管理员能够不用积分去使用积分功能,并且能够添加特权群聊,拉黑群聊的功能【超级管理员也有】 + + 添加管理员需要使用关键词@要添加的群友,例如`添加管理@云山`,当然,关键词可以自己配置 + + - ![image-20221212184032220](./README.assets/image-20221212184032220.png) + + - ![image-20221212184133728](./README.assets/image-20221212184133728.png) + +其它功能也是一样的,只不过不需要`@群友即可` + +- ![image-20221212184307342](./README.assets/image-20221212184307342.png) + +- ![image-20221212184317466](./README.assets/image-20221212184317466.png) + +4. 关键词回复配置 + - 如只有一个关键词配置,说明此功能只需要一个关键词即可触发 + - ![image-20221212185343056](./README.assets/image-20221212185343056.png) + - ![image-20221212185422047](./README.assets/image-20221212185422047.png) + - 如没有内容,说明今日没有文章 + - 如有两个关键词配置,说明此功能需要两个关键词触发 + - ![image-20221212185519094](./README.assets/image-20221212185519094.png) + - ![image-20221212190112759](./README.assets/image-20221212190112759.png) + +5. 自定义回复 + - 第一处地方,请写触发的关键词,第二处请写触发关键词后回复的内容 + - ![image-20221212190436791](./README.assets/image-20221212190436791.png) + - ![image-20221212190457890](./README.assets/image-20221212190457890.png) +6. 积分关键词配置 + - 可自定义签到口令,签到积分,功能积分,关键词配置 + - ![image-20221212190611674](./README.assets/image-20221212190611674.png) +7. 系统消息配置 + - 可自定义入群欢迎消息,版权信息,下班通知【\n即为换行】 + - ![image-20221212190838294](./README.assets/image-20221212190838294.png) + + 8. 由于时间关系,不方便详细暂时,可自行摸索,若在使用中有任何问题请随时联系 + +### 后续优化计划 + +```shell +1. 优化群消息处理【已优化】 +2. 优化相关配置信息【已优化】 +3. 优化积分模块【已优化,可@多人加积分】 +4. 有其它想法可提交iessus +5. ... ... +``` + +### 后续开发计划 + +``` +- Github工具 + CVE 实时推送【已对接】 +- MD5解密【暂时没钱】 +- ... ... +``` + +### 更新日志 + +``` +- 【2023.1.1】 推送NGCBot 1.3版本,重写部分代码,优化代码逻辑,优化积分功能,优化定时推送功能 +``` + +#### 最后,若在使用过程中有任何问题,也提交Iessus,或者关注微信公众号,后台回复消息 + +![关注](./README.assets/关注.gif) + +#### 特别鸣谢 + +- https://github.com/zhizhuoshuma/WechatBot +- https://github.com/tom-snow/wechat-windows-versions +- https://github.com/yhy0/github-cve-monitor diff --git a/README.assets/image-20221212162417977.png b/README.assets/image-20221212162417977.png new file mode 100644 index 0000000000000000000000000000000000000000..eef3614042306d718554e33d50b014ce98bad308 GIT binary patch literal 135011 zcmeFZ`9D|d`#tWQ6qQh#O-ZFlgk*LQDn*6NBtkM~o=YStLdYBvndf;36*7fno=Rk% z=jpq)^L~Fm=TG?l@bP#(&N;90+|Rx5`?}V(*1E2Hd)|+ + + + + + ++;T4Uu`ELIYx5jl9+;R*jTq+k?ijkz772bqZ~ViXq~fPUoly5(X&eO z3HZtlU4F9T#I8Mm6r%Z$G_laf-nu#R?Opsk@{`xUhB1ljb*8m0`pmBil{)3zH{~s? zw=3QJGcWX@d$c4qdts(wUR}fH1o4&s@8|!m!2er;|F;7Fzh8lWZhx+<3}RJejf=s( zdNcFJZ6hIhbe5L$&OeE_lOGBYh)P1TVGwfBwC96-+^0{UD&+79A3A<6a@QSuj)rY~ z-1?0r9l_aJZlx6U<$A$Mp8HKF)~DUv+$tFy&XT*%kG2M}DV>We$E8U~`l!yHwdg6d zncUo%+`OHvdSRdEyLazi& M3LNE5Z zY_4+@&)u4GIdwhcb@N~U{*K25z1lZ@y|J;e?u;g->kDDtl*IQu5=}aK!O*C;M3}EO zO2VTf$2hdP@h=jR%0ofT_}53@^_d!u-!(z0@^P|Hf1N*n{@&Nu?P=N-GrwzMq-7l} ziEEIoX)C5`u8*dciWL1V@H!-9rQ< z3sYMHCvfXT;o#jIkbTJMX-< zl%SY6Xj$~{r}y$a_+j+>Yd~e42qT}_u?rWhT2nM8OIA(lLM~Vc7@{b}EQJwwyK%iH zkX_4h`txzg=XBF!xZfVTp>X%zhf-9t4e`qlek8SL-EYR9Q_}I7h5RDLUdfaSk&rk` zpDUUwzZoNSl7+>zBkTTnN4DttT(ghphH{E}zQbhkMY=TO&K$kE5U2I|R(@I*-^(XN zL`2S=J9pv2g}7~42ojPix6FEh#D~AXo;h=7IQQVSD_4RqnrgOL>wJEGAm3uBzKq=S z((#=;cV6r=tB_`Nnj4Ob>@lyTudc4 ZVKc-{5{fB zyedH@%yZ}CBkl$v?tPwJ&a30Yq2{7fAGh(-GynUstFp418M;@fNf*0qv}7x1wDbq6 zd>hqgv4$yy69w51THnk4ld4geV>#T!Pk)N|&_4$y9n95jx~;^hEyp`9=>J+al}}d9 z?sZ;eCwI-t%Hr%UGVLvyl9; Q9LEfLfn_WxoGdBSt@h)m|LpRi$ZqzWMgfn+7k;PNm&eGZyy=3Q zwYLIxvnTsetpPUl=^omV?;re|AFnNre2c1x5O(msy& y2Lv! Nm)U3ih)btu9i iS6sihl7ugA$o2=U8B2>D)qC+@yW z;weQir*`@Ei`{dpsm{X0M>L)0TmH~=rfEy`m%Gd9mD8H!^^fQFp2O3)AJXJ=T$&nN z-}CnF=jRAT?@PxUpAXg=w59b=bQeB3)|PLfPffb}&^e0UEtGsz`!!N@({=l9E=&JE zow<`IgVcdArI&naBZLKAHrDv*$0j8Ks; 5Nb5n6^EZo|myvDA_ zlr#0N8yb#SwQ2-um`fe^U6~*C!K!68h>Ug@+90DdEXSw%D`Gf4_7pp?$VE6-pA5Pz zv4yL_R)7rmBVUjM_?g0*h61z0nAp1G |E#%?SE2Sjmn+XaZj3}~!J6bO5R@9_Q>gJMx=U#Yu(OxuBpX+|Mkk5esXwSHp zd0w1Y?mp4mSbnY(M`F0>kXct{$yN)gX=y{FS}ij5>Iy7}AKuMuNl+a95Er4EWzhI| z8(H~~konI~0R6FNJcy2;zkUG{Cjhb~02`G8v#M82+76>r7nIZPh~=7eBYG6m?v#x^ zA2&l#0I1_koVUI>%djz8@)bXJ+Mf8*S7BjYNS3+v*E)CaW^{BPe}X(Zt5Gob%SUwL zM{?)0z05~>jcl8=H(U_)w{G3auvaX!9>+Q@F8%Vc8E*O=prpCewGa1FlkzH>YanFy zcTH<1WWu$Qtrg}^LUIFL<@NS`qY_;+! DrPl4rl`E;m z3#!=c 4zsjciZ&Rla_6x5;B|qe5fH()CCpd zv)3Wf;GMsI{=_rB5ww}0Ad3n9gYZENtmy{w9eF}VRnq^IHp8g{1 e6-_`#5 zOH=*f0#?YqnN}^AZY)xjwlSNm55O|uoW_bmtz*TWi|<_g)>jur4 o1!#_w z6TQ1&2kdRtu+fkuTN1n#e X_I;#cmEQDnjja6f z_^8(*4sVg#05+u_iw0r*OSbneEpgqY3O41mzEIPW;HdF01J(YCiiv^EQR|u~SXdUv z+ARQ!M;^{0uuvpMJ}95pDsj|unpe#G3Y_`kO|OOko7 @#eAizwd3(4i*0gXu2Q0^^>D?7Jr4$O+ zzK@T$m9EXyY^<*m*GN*y9Md>=>eOI*CBtrujMatlP_BDl*>k&YrQiMh=TTc*+ZF#) zit#s?99o5qQNA;ED!q}t!VU|hVrlvf5vvo0si-ms*lwRyPJ3%ZMcmCs^Xt<$e- 2{_`O%A|J*=gcR_QSW@EgKn#`;6*8|+ z`)dE=x$ord6m<{{8$b>6F LUBvI X6un1T7Yu|lzOqf70QH#z&YY3 z`V_8Sy}BY{W5pK&p2wDwcbuP}UrjGjt8}C637On|t--v!yeSEMK+9n~8;R(q_3PI! z`CGTH2C!Pqw Ge0-dp<{G74)}|{rt0IMo zuY0uVXg4#UQeb(%*nU3aUJi;;!<+Lls738uo#u)kZk_D#i$);;s;!xpu3Jy_l^OI7 zAW72{KHM7S@bvUNgJ&AvIXR%1pl~0X#ZSLgvp??qfW!jg`pbzN@w{~R?p-YC%us_% zdFIKJCu2=39YauICr1)fHG#};oO-~A_=m#$wXfZ})dXJSUt%te%Hh)RGC(HiTzE4R zakC_w+cVC~sm5Ln2xM2&v~Itr^X0`(*E^j##>DL*Bs5*t=SNx+rH}g#3=9}#dR;!@ zhcrd*+-n_)`~L2Vf4Sf;*S>{LQ&Go{;1NBxzh2@m4{&HY{z0{TO2PkFG3)czH*Lh| z>ik^*1}nNYB{4PmBT1zh+{x@?@8-rL0Ul%0>KVG#MCnJI4eMih+12yRR+yJ(hd`^W zy_IMPDDY>IJEv8$)>rlfm#IL2>K>kIaM|D{ceR@xOh$?f)rVV+{t#Yp1JKl GG4##gDk5k1k$IOG}6dqp-9~p8K!?QzcHT II63wr)n0Xh#Zh2 zREOxd1<$QJWklWkY*Mm32o2Jbq9Ir~cvdmtyEnaHQ&W@cv9?l|P02$VuQ$e!LeZWb z=XJi&IWKQNq@IQ}3+C1b**EVgvO_{Y_#S`L`m^<2)D-W)_gK{h!O5abm(7i1 QcPE&`D@0&OXjPP5@x~#SUB`Zc=du*Jso*6Bif1diAU4 zb`@adcxNt|SQ_s6{q@j@wi8>?rb10a(~Jy|^g76{lJV|AYfAftactn?$8A(@Co4`~ zeSMhMNVzR78ge7japnq7@6l6+C|X0M>*J=wHm6=(I!?nP`+`ypPy&9E$(AD@hqwf+ z0^Cb}K4*JM#kf(vn xxplf_;a7pRgLX=Ex_^S6A> zVtZW3D}@XE%6@+2-bTsRjmoN{aDbUc0F!~SM!{*|Si=tjD+V5JtS#f#st;+}_M`6X z>O9Gzo=5EII#3Zb$WpEpU?sFX;NPfuFzWmF@6*33Bkcx*+xKZIV81iXrHN%XZcB?o z()G6@(sgoGO91#6O}gG_SA0Pk-dt{Q5$QOYTCy@i#^?xELQn%xEy0OAW(bW+Z&uta zu`_=fW58;Ukd{HFEe`n05u(Z?w`T|O2vyFN*YE0~K6h*vGpnSx(dE;8ci9&=V~V@L z3TQvH?k#a*bet-4+estk!_XZg?L*<-RQXEOWdjtH!J!sxJR^N}{)m3q`G*5lwU)a< zN|q}{H}>Ydl+~grL2)T|STyO#8qVo75&S9iFGc}CKxl=KoWFy#B9^BNzsINJd7)JF z`UAI?cEjxX{xcb7{pBF9q7~C987Lw7AV!D_6Y#M%j-PE-pQJ57MvJj5hzau2z%%mi z6%%hCnQ6tv*KiN!cXQ41;3Z&J?&%F6tERxf2<(BNCLu@8`z(V7-D0t^MLMu>pW zkFe8wDojN~Lleq#e+C*<%HqebN}q;Xws 00A^!2tCg8^@>Pr8Wq+Y)u)7&)(T$9~eXrdXMZluB@RMghw| z%sxUhfPZ)feE@tEjag(NcxfRL>)xp`itK8Z$(7O6-eLzEAhHkdTWDI)hkWGsp>yho zum7`&PXhw$dM?X;G0yD<#ARPTaqIoHU a2 zj 9?H8Pwdr6?81bS5;0V-z>77w&``cn6z2EGLk4) z!TTgg%Xt~p<|vPWS;8TKl&=A7a$$VaAYsGuwQRSOE|+d _Ww(>hZJ*92Zo)HTKio3g$xZ^&B``rarZ^Vt;Gk(85Bkx@tst&>+ z!%@tG+*ire3y-#iA>{x;>wX{)6m|8d7YDov{~Z3&vCM{&!H@?){W(CPTR{D2Yicjz z2ssE7j={S+fUQ+dbbUW>2Dxir 1GG*t6$;0WO%;lAL 4e|+-#?Xj6t@PAk(6y!54m{>r^5 tv<1YBS_aqHE#<(dEb y> z=jT6F@U>u)qAP!5u&gY#LJPyqLkdX&cEhW-`_J4OSe-2G&AKm-D{aojiCzFfgQL~| z2zN8tl|Ng}5}Bl&{u6MC%+!KPn449%sJqZ;X^2`{Ap^{!VQ5FahJ_##z#5=E&pCRO zk avw3~4yAkRSc_Wa%Yskf6K@Gqi z2e*E`<5+rSfz4z@$;*eQ6mjFg%JlSfb8~YRM$K#2YCFMCKhen1IVS)@pvt^+Gzi?} zc}&Q4K;ATHv3JjRSRP*c&p;lF02V3eW4ZH?{vH%Z{Gy_ytv`V3Qj==0Kg19AcXfXB zkV*Oy2tQrnp-S*mZjfa_6QL;JF!h@}Zl3^!-Pl;2^g6(1&3cNAh*8-n_=?VO-Wrqs zAP6=#7EAR-ZmL1F9%NU&R61Dg&%#~UL*I(a7NJ`7^^W(Jy5Ne)2NZI6K=tXJir}z_ zRCTKsMF6}L>KHPx6-5E766R>+ds+9}TQO;{P^<&RO-Y$JhlR1_eh)_CYZ23wuE)ks zuH&|}u%D66E5ihDZO_yfo&PTH(Vzm%L{-_bRRG_KYC^@(bY4CG1Zd{&vRKLJ;)r5i z6T}gTtwRoiw-FYscDb8Q(!Pykd=tOzR3ErC`|^4PAm~gOBk|Zv%x7-Bt6G}=1=m8r zVzBlYc}l@Z!WtMcE;XCX0Mj@N%nU*v6@)-&;73j1S=}c)_LN^THUNufMBRXiw0{Rt zTVz9chVypAp0lkF x+C$=0 zr2HWC2uLj|eYwCNRGn_i=Ib6>${=z56&@U5Sz4vejZ#N|TAM4arLubRB`BJR ;zHC-d=DfDm>H`^fh9a=Xf1DHMR44&f$R?DzI00J>RskK#0rqeddzh{ z_hIAb&;RG~-}oHB6)<`aMIxg>9cu~`y%PlgoO0T+Yufr&Pb*HII<+yXwFyxIjX*}l zK;e1^PfC>$@Q^31dWTx67VZD5?gL^fF!6T-|wHWCt3Rt7^&Mc~H6t=J*P ziEK Rt5-J=gWyJ&B1fJdU}L?Vba+dGMk`LStVwMl?B>p^foJVt z^1;op!M7li;`AFKJS}S9oFAsWhvGT(l-7i=`aUQ!w^3UfV*2-%i277niArb=rv)@- zPmQw#VP2r~0GrXfk^O|x>zfEM5c$~m@1;_mR)*vJ{r%;*>fgM1gJ{sOY?MSg-sc-f zUcnp*34S2=UZ%S4Vs8 48fAjNe(8mA%9WIF}7 zDxq#ExLE1@4>eTdsuYIG;F}PnHRlS)3Pw{LU|&5swuGGnGlOq)dw#rgQTII@fQB?5 zt;c_FN1KOeAOOnT&$J@&oC&`RbO_m}088*!y$T4yXiDKZc6Rn6n6>?l^1+kV;bOp3 zXsl&aCuy3pR!s+E+mY9S6f7rB6xAi}u>IdzfZ2M%{^xKu#!zSU1a2@^yu8?x2Zogv zJ{P$$7Y8qx$^ZUbZK|MI-$y`YgzYmTdH$g^gnbIYEI+slTgqRT80j#PFGk%3(-uoA z01uB)9_*{s|C%aklm!rfsMvF_m?Xeu=PIZ~lhkrkK-pA63_m{lON{yjgYZg>kH`u{ zFIX3~=cSp!I=CjS5oQ-GAzwjJi4Z+T4iTMe&=?JP)yrGdVTpmGk{R+BK@a8S#!(!O}cFBF0}HPbn$g&I#k| 7g>XM{OJgn@LoOx- z!|w_IW2OU^$p!>eebYk}dIBR+v2FwTVZ&_x=5VRDuiAW%Zjkgf*Tp}LI#|BSyeW{U z2vHa3wyTAxwM1cR)GQ)+20QyWR%5a;rqYX}gzxUBXa0_oIyySdm0_=6TY}>1Z=`SG zg^E8A2f41-`o&(w4@!Zc*%xBp`1uhm9I}1&*&)=#IkW^|8b|`Jxx-l1^QJ%@!(+2i zEUe(7KFzM~cd3~B4RmrnhW3t1pq|&I<8Z9CwSGbB03oI2+pre|LjzaVgfotg55&`Q z+bp3s;VOPzD?pnKPwF$MIogtld@4-Z;X(nn)<1g%tuol>s9Qeg^N`YF)J4b{W@+y= za1w%UY#4#K1MWc*o#E-Es$`xr3fo6ohRCy=QS3x$79b0uov<-?b#$a(9-aR}YZ|7@ zwvX8LK4ol{#k;*_J5%`pBge>l(5^!%MA^+YY(Z?ECU?Eq>p1)D5O~3m+aY7*z>6c? zSg3id{?qHRK(Y?h3{b}$!xmPF)!~>k&PU@N}??rvlV*wODFW|0A4`ghRs z1YTUfa)tlPiS4`gD`S^oCWnIt(G&( XZC|! z#JpKOON8z53bjVuKd1`TVa`4Xs8RU3SmuK}IKjp6fENwEy@i^`^PjyLeJQ8cG?0Rh zk8?#D9p)abjhP^W02_1&Jw2~nyC#G!X0#jdood+s h{m1eMe2O$8jbF#N|bFa(O53ta2V^S{; z`@VYh4Rr=QJokR{DH$fKv9@$v5!JBxPqbGw8SaN>u%tpi&v|`i`|g9+-f;GxU`qn; zYtrwOD@Ccf)LZ)vIByM^DOuB0k+AIF>qun#BhfeT6GT?WGPjXY3Izw2xZKblJkP`w zg>6+~-NA9|-Q_G}$sHcK2kNDWh5-~y4wjUobd3v;3Q|Mq!+4(XDOa9QiBtt~Xw6*) zCaPr{2E2Mj7*KNrU4g+h-gs)Kbl1UDX&(mkL}Z?lT2hn3O&-U?X6>{>c4dz0Wxm6o zpd-_Q=36^L2_e2P*}G}L%#lCfhhpRAltQy_AL!9RlRIz3utO1$2>=wy Y2ZXR5{PIB_PZZ?8iyswvYq}(_zCU$k=@V|_}h9wNt)W{D~7!#>j%)> zoS6+vLnoT8LevebVboo4p$ep0UG{m~H42)dH+SW})AWq~|s5q0@WDVE-p7%zKmv z_Q4H!*fB@i2yZVWm4e2*G9WCBZv2uj6Vob6&mr_}9JWzPz;vI&QnjI2qLXw^EoTgx zcCGIT)l*7(dvrq>rxSo7+r@~a;WSo-5XVZCAff^Eb(|Ks13lG$h$<}6;ROG0*yipv z2nbADsG=gHLjcP%Y(PW58q!Z5N^43Mc;*7noZeU$0naUOq7DHLTM&!{&I|#mC|$%h z=tbPZHegjdVC_J7!F)0xM70MMRb~nbFU1g~9%4=dWzYJa4&j6X^mS_#SoGiyNUsT> zdII2zEZ7vqT9G{J^&-Lzjd@%es_ZI{T>(N$hiCtW+%2HB%lfDm9NbNKl8xABv>m~| z|Gg*33Tb(_%6QL-#u(|9k@~rve~yuZ3s|E}GurqieT8QO(FFAc4}g(WtgWjL?2ZH7 z-My8)4{O-VHnXjqM?S=rPlGlH^jlZ7Br5g7ZC@#5;MTh~XM{hEa9SY5+56boJ@f)| zQ?zoxkk!BSp~ h@s##qyKO?jw$XpSb0qEjlclOW4N< zy{WEmK=uZq+1fDg37%oxE^-6AxiwUPd!X`xQNL)@WiE}d+p-gG@v$;7^`nW;Z`SwE z8w0Pw4lc*b*RMN)dt{`uKZJIv^kHNikEcz^0F}bXz^XwIAc?smatXVQhu3e;JyGvx z7F3E3Bk*DkMN*7<4bE%hK}N~v`}1KifJagRyD-1O;A{tuSNY-yjq#lC5n=!dCfX@7 zXyS7arO0UCB>fbhae$-~41Uyy;NY(H<)O$@2!utf6Yf(5Yf9AZPugHLK&`s8o4SK1 zPn^I_{qmJ&$j7g-$cE*iRRT>^JXOBC;^`g-ShA+Y^FPiC5Bb{wx99~|=@=q0P&WL6 z7_xXocLNu#ppn?>bh5=s)X2ie5h)tuVLgAIXvj2ra6;1~&533wE!rsXh4B9fx>;dk z1r5>$D)@$})$xUW18#{Z2Wr+HGFPt(01b&|E=234AuaIYeqjm3ZVL=(M5Kp%(@pHo z5llV+!I3Yr#~#7^C%mNh? G)71KcmE_nwsY z!9swacK5`2`+rzYo(zY(3}0&=R19} (f1au)?6i T0lvXl>G_2b(T?XMDRnhDMS%&7dJT)mCNWRDQ zR9IZ1r3I24Ee+hqTwT&$W+^u>;Iz^RZ*~xj5RD&G+qg)V4LfM94ErZ6XV7NB+IU$jQ1yDC z8b?gI;YQ+NmF|^0aQa3cl(2e^kqUB-7@OKiT?5k%8|)a0hhBDuXpycFF-0Zm2qKy} zk2cL*HphABVUDT1o5VR1qg*tkWja2>KM G+Q%Bz?O;`@n}6Ob=e~W6XrkfH;aDt96cUM=- b~p1RnR@hDN(e_%r?*@ 2}d 1m+pyhcHm%u7?t{kc1qJWB$5hxRcUyvPXESbk}cuk)+oZs6)ax)UA32 zaP}Ct7X{CrDhFhPj*p=wK8v349J-yCY?x6PkqEox7vM1^Ll)YaA^Hd#t&Ixy7m%SL zTyW8?uqy&J6Vdl_YlIR4gqSh)=1YQ`MDUcXSt&fNPhP(!gg!)fMfg5o@Zxyqp}HO5 z2}Wy6(|`nl{%~qT=>9qoe%w^_k%({80T!EBYO2$TN)e= Ggw|xb3BOv^uzZEo|ecxlU=uQ29^2qck zC%nnA_8*6>Z7tehH_)ZOo1;p=@COtto t$ofY zYQ)$BU?2>)izO@nZj7h~$DA)CV{}U_g6txOQnC#d@x`$FAYXUyKmDqjjOg?c5sqag z90bt*iq}8)rY7Ht{R-aU8_bU)a5(324_pd7JRu8^X5meL_Y)Ht4mZYNS7FhSu#}l2 zzLy73s0bSw#(X+L1kUx+Gj0WV kKn(sUN~M9Cwj%X(iSH&9mSb4f*jaMD7~Es9E#rjC$YnE46g)|Z{T?86}Z9PMx< z8A#xl&!15-DNxB!+@#mUep(W4D6oV;SSVe9dHA5ocOuTGHc`gD_S{8DNyOYEVy ZC}WP6oQx&$!fqj0x5bH(~*P~NX&n9(4Q>wg(JNVRoQQZXSjCxG8~XE==Kop z*0Xii>L~u&S01i7e}V&vTnXpY7PrhSBf2qXM^jJ&QP91W4|L+L36pG!fij99>{GnQ zSeRwHZ=z`k57S{O#pg&El1IfUmSC$O()c7+=A9^ #r4jKX8m?6-gP81Vp0Y`9o!rjA;n%R6p zHMryuaOt=&VlmgKolCWu9_CDzr3)|uWjDs^_f7OFd`?-zXC>yT{;tdBCK^&$!ocb> zECx|QxK*5q7{8XILuq5v1-BhvO)2UCQOt1uuXPNj?&r`H^7kbSu#YY=*e^0MP9zN) zA^NKWu&s!m9_C_^U5Hmjw2onyP8w+V$YscU1Swj;3@jdvlm89oeTg2s&KmmoM7;p% z=`pL=v*##}?Nk|A#craJhFD--D}tJV6GLzOlmMb0GX7#rPwN298t)Fk+#GA?hS7>x za_@G_RKlFsZFFr(%P{C#(J@OfM8q3>6$iMC7$haeRL@?AU47X4qIU-x;TZoZ-y#tj z;1e_2$f6Yt-3czdf#Sg;DZ*&Ws5vJHegPjg1dI}kgBdj)b)w90lq?;0Ob7ZSt(~%_ z;Q+0}) UvSZ@?x(!=tTDm^ pzq5 zn+}~14D0$>^U|s{0}nQu (#`wah0l;t9KHcz3#m$%TV@iNWzxO!?B!E#jHC%y>ro=iLO^=qlA_1Sg*X6| z5CBtIoFEq|GVJf!IQJ75Av#lpv4EYF5f#Crpm)fK!U1vpCNR(%8i&zg1CBOI;TYl* zEJuu59nB0D3!mUdbh+miGw>X+l=6{jEK{~ #5mlbus!7YMa79LDH(L@w z#@??H^B;HNY=}``a?pYmhw}lyF>br=Lj#scjL<2i_JFT>ua1htApo64MPEUc *i+I!!da(ju$VlQAoco*glAuv2(z#%HNvA0wkE_L|~AILMeSFvKTRp-vM- z1}-AePHCPT_<^KO8k~84kX- VP?Tf^$j1Z>fxG YJ$X#yv&EWQAbIKHMeI@rlW95~D^rTmIBDl9*Cp2!9>tW2BXi2-{m?4Vz7M zl;IT+XhRyfMW01;u=5^PN!k-usFS%}4`nc |L*RfS2}reAqbUlZRZ)fnFGWT4 zX;jNc3BiyyxhUk72E<#t+MNh7L^|4UR`B$&@NO+%k?q3$?Hh~^HVlX4S2wBFYE`c{ zmvD3h>}R@q(l|p`#`pEf{=kzbS7o2A4j#;?Ir`k~a+gZx$>+CaLQl%58uZ4EE>_O3 zNAx 0 0d#QdCDzxds|KjHj<2B<1M#In}Vq&R^FwY4d;u*3n$ z l6#)T(k Bdjd-v|d#f!P#vRl*91P-Exl 8vgUIw!-FOKMXW##29 zVU7cpeJU4Y@o1WtwN!)8=3|;zB{VUS&71dBXJlq(=I?=lCG`w+jgim7wo@cbCuTaL zIa^#<1CBB TZrprP?EDk@r>?6na% 36qYx`uel&av9k~MP6UIAS${^oZ65LYeVUQ z^2ES^#JPEhRO8zpKY#ulDP(s`RrTn;eSSefN}8G}c-W KVFI{`oL-M@9@MD!0*54k1b)6 @5mK%D_3PJ! z4 UIGc$(5&%C{5wY8aS{#t>M5%N()17Ui7)n?7`=1t$w(9pgH z4UCDh513wk0GqJV>;7c$u9s4s={jU8=Tkp?SjO?2{rmS{Dx~<&+FYvs@nZpCj>40V zG(!rz(Y+LpGOL`~N2+aVIyo}JTKfjtE4k+DKp$qQ+S}V9? 3g4TXX$~FAO-t+Z7h988U=0|OehUOy_4=T|8ZMigoqe*i*uC-&C&ivU zk{6DOQSaXs3t2qSiWsk{tE;Q727l@|kvj+iZEoIg5`HSr`Ol9ZKbD5eTRr;K?*8vA z0HGMxpHl;(EX>UMBC0-pdY6<$vB?85UdSV+Mt$($L0dKP34MkWc+N1RC)rt9-& cqNE|>&! ztRIk*mv3%ppfb%yc_Rkx;G%WmFVTy&wzdZADX^bsm2)q+{&&u{)s+=k`I#0n2i&r= ztz=|mBqS fX6yhS4jo4sjKgBc?`q=%qzRAE%=7 zK^;-NeS241SWhfYLn =6{y3TuCDjp%m%`1b9_?c0NWeRoVuObiVZqNAJE zWwy>{{JWh09HXS91XDgfnvB{vf%3M<5rZ9m{P^+yUG8yzi=eIF)O3rRn|tC37Z;b9 znAnF8ALuAO&K{Ht=8CQJ1+b#kjpKVM@HYzz1S0;PLkd@|((^wYKpR;Lq`SX@Y|TVL zN$DalFHQ*f7+FsM?=>{K(aYw!Z~=Vu__f(H?&3^rgZuXF0~@WYsVQ_`7nmqIc#3Zo zj1@6oUtdoY6vrjb&^qV&kptr79;eWp$ZBSW$9Fd!7yu+VOz{+j=H|`4!EYK56crZ_ z)rB6SrR|K7rYcJnKx$x26D>Un26HnrBr~={#QZ0=Z-*E)9(Ko$9ZM}(a$9@*=hnZ& zInkuYDqu7q+u1WI2O}HBs5uA-$9;GHlfg^uP)HaBB90p3oLEXyQWw@7jzv;(^7f)I z@u+94d3TU(j3SPA(v@*A7~gd2)T#ZuBxPkMAfyrO#wOO%vA3KY9aU9TQB|nLw{PDL zKzDqP!vMcL8$Y65@MzW%%iM0n>tBu5!X FX+1|dFG~H>{dd3+K z%D!}im6bKQ!W;Hv^Y`zs0|TE5ev^=p5G?*4Sw21s){+;e_7R#x96)ej|9+e!mwA7` zrn(wTVqj?KSA (?kP-xvMDUT+Se_Qgc$eC^1o`829cf*ttVFX|lIw2te;EFk;KJf%E zFPTv07w|4qQcTcEK@9-wR6~P6js+tQ3@H^~zI?%T50aUT^_E7i(p|Yyjc>!sWR+lF zP*lnN8*8ttstO1Rg|W2dmXwI_^S3uPrd#M>c$Uo6_F0U0;8~RnI5{w(`lhE>^Yt4T z?Ch3#IncyAouB z2Xk=iGioX-?UC 2*MWdt&U!$Osm-H&dTVu6Gey(`{!NHJ!US`bxrfale26 zE-NeRDjc_Z_T-5K>Y$|Lc1qeaz{-@Aa~vE5d2x0YF&XW_PTAV>**sRee*NXzkM{Qb zw{P!&&pWNmVTz!(s_K=${{vxP4-c|Ms`&KuX>=k>wdv)rU*A({eeuGD{*Qs+s9MGG zz0aOL{el$)ophcT|Gzo*(M2HRewGAOgXC={u)~(`-vtE9v7Ebi?Wz(xr&$yq6_t)d zp5L$DyLV4PVZY6#Y+qkrY>vl!HP{%SlW-97sBWF!AMrIC3_rd}(%BHbhXG+JY3beE z`=6BEz{tbRoBZ~3r^O90xT2@0my?rY#Or{(C6B^%z-*W(G+$L!6+o`Nvollq>@IhV z1uMEypVO9>lFEtr{O#Km7E)Jc9&?k|u3w*~J>B=iUFh`ws}78!VXbFhz_NL$?;iB} zH8Dv6akpQQcD0l|wC*;FMQbY~=bc9{UQmcp!yJ($H%2Q$iQ~U=nCoX-Nm%n9fJPz)FXuWG>boS0c&X-aZA4)J(j(cJmt=ZVv0IE-R z2^0b*#Z62mV7b8hSqHKf6$$ye?RBuX? s_Orzx&hsMQ5F==* zc>3VrqnG0ls(2j@fB79wZ#sU6>q!jASq#a#-SFt>>7+S)*uc>6tHrlZp9bcJo5j72 z9z5`(9Wl?RdthQx0+wsqmeCz+zc6M8SW;4IMUQiRX_^_yfZHRFLiLfw`vd@V9UTQU z4X^%h1dvdpN~gmyMqU3YqB4dE!d=nRBO+t7vUtv)pGIFT>B9#Vlvfb;sYiKt+nSp8 zQBzMf8@>EN$W3bMG@>hUh{KP}VeStT8yj2I@9+nM JbUp19HnxIJNTNDlhfKS z0Cpj44@}ayYZ1rtfZTWP+)0DegxG;{NNXR1cTOJsCMqt@!{+J +HjJIsZ2wywT1W@4~fmbE+zF;JgTaurrCgpn>!QY2%&|4VmF|5 z&;s>l6zwT1iz=4hjnam5QU{%nfC4}et;~Td>|~k)VTPi0d&NorZP%_{1I{n7$+iP% ziIJCfEZq2@@K8e51SJt-cGY)>fAzqG91Es-viV)(ahUQira6cd4DVPd5$C0agoO(& z@n( J)(l~5+=FA6>8c@JpP3-lhieY7U zrtT-)%WOv-hv~j&&z`(}D>xC2lziph2!4Wc@u{jRULyM@{u%-vcmcHPd+q7NDEgzd zmD!CTp-F3g%4Y*{-98K7c!d#6jhsDuw!^Wdv(o_#a$p7}*4DKu9fJ`>V~!?sx45+Q z5L)E%2Pi$bK`76K?EgQ{PC^nDCSYX<5gC2-Wn9RW*jp796>2ugU#qI-#@hWG_>d>W z`9Na&nUxhDbGa^8VN@Z&cO2()=qTfol4`rVo$%-sJ7N-9#*bsAV2jbbHx9cR8xkB` z7cO{_hbOC;6!Y1`AV*=aG%&l8cUV mc_h_i%XuNjZK>+8K87-jG-15{WLv5?SEEu=6| zD|t3=rz<(q&dZBtjd%OIxVS}Fq_dX7%x<8$`wtv&r`| *4 zT;*@yX2APk8pLoDXo!(NTt?p*6GjDhG>kDpX9^=1_oktt0X$2NiD@>L_X TMFFuM*k;Tk?iFI<(Dj-zRy8u BggsHmv4G-}a@ zH{|6VruzPBklTb0NX}HmeD~5N(uOpiqXaKGe||3x=4h3QaB=x<8g9Rc*(FR0fo~Pr z&+pn)A-S!r{7kC=th6*md+X%Izl-r`Ydq!o^IhtFF*i;-CiolihEmM^*@4^yB0w`D zAXsEmK^v^i%s#5Et*eo@J-8Ni<19OSc6K)V*|X>fsg*XM3L<%fgM+1{q}Du+qFVtr z4^99 !M3CpM^4aDvM@8pz_D19kUVkX#5s6_08j9) zS7hd{7Nh= unI8ps(d}u?QB($WLxA!TwQozx@bmgU! zr~HQ}CtaX8A?N$TsEu%9WU0{`ix}@$1_^7%I0uuJIO=)yD8?=+Q4VqJcGl%TB!cAQ zq%5}kjVdQ7bVdg8I5 G6;Cx}x9{y%lmp`4T*0^#^yzh7F20nCX*2-XOYCI%Px`D`4>m+jPEN`>cP1)vOuW$m z1S%>jZV(h3Q;9DNpSOPdmWKAykA5oGik4hit)389*K%KG>8b-LbZLTWX@$Kg22 SoaWs~N)hL^yn+II_) 4e08?1QPAER%R4i$TmBa;fyq&duS?D-|st zvGI5TgW{_?ih$Rz%PT6X1vI1uCBP_!g@mfgbN}`F@K*vnRdRR}1C!g9RD(@@pucLM za3ypuu^=x`_uf6HwWaewe%J}ewE{TRq`3|g2O_~2hnr5i-GJsq6b^9-Lz|nq7sD+? zc43qcI}E i_(dqQ;{ByvF$J_wThe zHNVXGklT@Xbq|`8ME6fY;dOX8(fx+-FLMyW_zBW=`WM-j(?_q-goYk`K4@2@J4!O9 z-36W1ME5c _-0sGgAGYnFbtDd9*i9zeW%zCL}E2z$6-!rD`#` zVEkymz+p9~+a>-74chQT13^B%zT#CZ$8B}>7Z2(HIhgo`zf$>_HZP+HM4!^bAJj)f zBiz@wj3&tluar<#H8e5l89a+jJH@DkfQx(g?p5q~VBo>E?cNqq&BLHk;dxuH()yo8 z*ogTvI;xdt#>B!x=0Vl>m0tW05V)YA08CTY6Q@e={q22(kumbstGB7CjE4`u2@fY; z^FzsgA1^|{gX85Dko!{Ps3NJqazFdLdD8~bZ{~mINf|VF^wXzL<9BF5eIUq!J3rpx z1p5RDJ(e&nm<(hrO`z;yKv>AhHK5q}pE-hvW!B*lxP(fNKj^BfA08gYNWx+G-?)^! zyQG~RzqrBAPoC&)&n+(Ma|NRt%gVtq)b8l_`L4U0TUP$w<1eL}TUvfQ?_g$QgVk9N zBllrbS{kSNKapFmfHI58e@Nt`-tAAy{#+VODJdb${CuCb_I50_`JqWDs?+T3n8q@g z&}fDE=p6Fbb!t+t&tPu1Zru3Ti(P8{{(Zj-`<>&DVOPWYuCkl{VtM?#z39cg*8iZX za+qlj`-=%B@G!WWsp;vBU%y^e__Zlr9?TBOijwsk#-w1!2V-MnCzx )IL1Z2Hhb*MnJWuo@B`tufOx<#MuUP*)VWB)=0C~(aT4Oe z9=$#K;;-G^>tIcM))I<}2ZHY6nhc`O*A7npNYS9BqkF;Zm+!PR^|&_I*dJB^=jxt) zo3u>VcN{(D28;5damNV`js~Zr0aq23ls1PAx*0zuy?-C*@Bf4v2I`0S_>b-mjV94c zk^1oU`uqDy#bC2w@csrW?5jlRK5S@17(dL-C#v_2jMNLQ+1t~SP)9Jf08e6u0t(7a z#SVB!d-P~VMh2SwYOr!%zrM7=fZNiBe1>{2Rr|&Nf0CAj (~2lVy08`5cTbaZrNL;yYSuD&A& z4-TS& _v-tTpvC*;YKZ8k&q9boeiuL1}OVP#`8ml%aQgZqaQxohj| z)!ln{y53MyLQ&^JhsJnZy{S>Z&T@^-!qIs67Bp z gx|J$i8nhKymysf6oZyRo5jR#y9R4N&)Pfm $+{r64gq|bxim=NDU1V)>z7_(Ho{p}{ROReJ6+vP?7c`BO z>IXU8dvY|R(EHcLYJemBG~;u< _E!ngAG5audu3#5ptW)Cwz|Il9H<<4 zmenKE2f$Frk(ub|s3`M|HM@wzcQba{gNV6)@9ks;mR;_-+!~B3iqWi3pFekZcY{ef z_g`ItxdPH~K->W3id4+a-5tM+z9Eb>6R&^LGc)1hjNkwnKBie@`&aS5?|s~Q?zc4n zHXM75t_vbsepy+LPELZ~t7~+*P?N#S9w#>Nom+-ZM=K3uVw~LE%;e&5`O3@7TUr8G z-a$zHgnI+djuQ~YPOCY`W;5E2q@|_Hf-YK(D5%>eCnS__Z^wmsSM|^Ae-10*93|e( zB?_1M*s({BEgwG~gEuH>{iIHB6DOU>nJ7J;#9fw=5rlvNb1|db>3KC4O9r)t;s*0( zI~m#Tl8ySCii+8}xfq*x9T&%PBvJ#SCo~ji$Xk(+eSLkh?{rWF(TAAq$gb(^EMyC0 ze~T^wEY}kB24IKejtH Lvd7_^Dy}9vD}~k!SjD~ zA#cDcva+*xx*q1zy^LN0xLJo#Ch85STtGkoW_$M)?Nq&T<;<~T$Djs8MI*ad((?0< z1l>bx6a*y}D~ECeoj0YPn4F9OWBdE}@4Ftew6e-y^^*KtUHu^?#k+7jaY$)Ae5}8J zFD`uea30|ReZv{g>Y#Xq1hhCuds;yE&8Rf-3OKu2(7}5V&2VAn5%L%(a{C4_B4-k) zTL!^>K+krTjSU?dcWO*^9T202&j{PV+|qKO-jG@yO*!$?f`WxKw6f1X!N!~DDfa3e z$3-Tj|1PBqmkkOE($8$i4rAJ&0L@Df4|u#7`gum(fj5+hQ4@;P&u@V23effJwdP?< zZ2{utr2qS~v|T|#0rstynp!B1)`W+@v=M(Wdkq-?vI)BmlX#c$5E7Y4X1EMt{EtH4 z<`Wp9niW5 OJ-fWPhP0=q{= zK5zyh^R=c1*k4VF*DsV3wk;^*z->3I3xW(IoEd3pMCow8@t<(|SnBEPdlUYy3d{P! zxSFl)5_}F^tuDOcElet~_o1QI{PK8^d0AOXx$Yw_>Dm=Ld~H@|pmKnqI8sz)M!^8M z>uTIXu|o$G5-(1{4oO;D^G+Q4f~<*-i6KVZ2QT495N5z!NXrL?hW67xw=W*GmDPj` z&5#)%4+CBjPk6lc@4$e7a3Bn3=<(vfp!oQBG=0rVk7CXC?qym#3y{Vu3Xo$6hJDUi zyf;tpKj`&&NO4UP-UBQs6a#p(h0*&{Qc^@v23?P#(F{ZG?2 P%ce=~Zwb0RdP=kZ3zs;bLE-jR`7=)pPAC!ueE-+cM}xjk$|*uMh4rL~Np zpE#6-;N2^ZCmUGaDS)v8C5g>$ilaPLt*wi3adr+45ANT$>gIxW!ecWC+bbw5^PweN z#(`#BH*N}P3NP!0k9w(d_}UUIprF+O?TV*(N5Rh_9<+QXCMKeb{!^Yv3Ow*;NZ@ca zYa2K+N;=1k=t5kh0bF!GH4^>P{%8^O#{L%HKwy|(U&b*g6fi9C;`Fo%+Jl(XsIr)w zpRbl}fvv)!S+oRk47~d-3TDJ*vziP&gGqr-wI`mQJ2#H=^5$4|YuN%Qh{+Tf^T7DR z%wF{qZ?5U#MP$%mAQ^Z;!vWW0fFp9jPlC_I|39kU1Fq-3egDU0gpg37QkjWl6_rY| zQ B ze!b3d9LIT_VV(yMmMXWQ6zdhgI}|L^uPY)d6s06JQb$@=)-&r${=fbk7R;MRhk6@x zhLIz?NuCEE`a}WsV@|od8mQ~OeO 3I8l3X`bj#B_o8+`oSAseIReLeEv#KIhKw z=dmbkF<_sVF5IPH$#vw&Y!ND~o|BKOfEE!-`s)I<)dt^(Dx=$P{`04zyc}#syz4~x zR?t&tn{cfmM@G%Nw4ETK7t*FLCS2IyV40a>91&OZXC8sXShv>S55RbVGpTPF5wN DcWMVn8RK3oQuLJt)iljzQ?FvfBt}e z7og kZ4(8&VK#ZE9nDljA8 z&YD$9hYCb?VMsnDKw{!vIT2(d)WK^;_u=3AcSnHRVtxG@LM5;I)vH%>BKziCgv~tQ z i3$YtrojGBTz{MoGbcGG-YW`Omp{FSiE05dzGT{S?OB6@^uu4(R@RzkQ^O zwqxj1V58QgQ#-ov54&~^hDxZfe#o9H3LSMQwIo1iQFsmoSo#27AWT-Z;Tz;Z&EbB; z^zQp7!V42JxIat-+(cW`uyW|v{VpIhAVa105CRFHxaP}B{&V2l?;&Y%Y${)*_2<#{ zGyuY;uV1l^KIZSQdqosV)D&6MXoF)mQ&tCERv$FzBz(({%IsMqsyAkVI%Ard+%Da% z+X3*S7inp)s+=}#*dQYltZe!^p 5)$%BrPL7TrdLTx`~HX= zN)_AyIKeZ;0Ge=5kqiwV(#8MODZ5V+nd_K&A)uoW6jy$=0*F@f$G(YYGTu#`;5sye zusM0!w3zsKcfzjjZR!7uV9d{`o(>8s?REATbS9%6zNIHGUbv8k)oIDT74~qw#9XI_ zqM60-lMh1u{XDhbmhK CJ23T=jr zj7<7)CT;{pg5jOZ*)|t^)6N@R;~Wt#lFDKWFa7f~aI%WZX>AX82o pH4butJr%iL+v17Y}k%Z*={<>srI?h8!kG5mEgz)I{ G{L%0~3{8nEWn3|FGH*t4U|LX0*xz!*IuZ!k95n!udr* z2M;dZ45UqYxD2?b?GBYbk)e0|*OwNP&d`=69o7lY;iRa#H9P25E;8;Y-ku~F>gYV7 zC{*7$h!*(72`mqaD4vi}#8g|7j%8(KyZCK`#JzbF&^Ce8ggoxy#TRnfHdAQgx!-DP zYV}-SdM5Ca%Q;oHAzL Eh}n-9@sYW*oLO2qo-W}Z*!s`L$j16TiEBx@a2IeWf@~eB zRaQEx;^uEv#;7DwMITJw@%!6Gre+QL_IG; a%o&24u$V3rOFi&_R}aoy-`jYI zgdRTub`^GEqm9k9bx%d91O5Eg+S?x$0~vw6ulF7F*A(&Z^V?&Wx>lvDs7d7%gt2rK z9KF5=XT5vj&{=)-=(#j1$7cHto;*rkzVkB|IZwnYcv@@AOHx*@x3wLgb|$9bNYfn+ z)U3v+1fZNo4SrNqTx?-w IeK9 r9{wa19)J z8J_}0C8O6;28x7nR>vqR0^c-_@Fhv~ZdMxQ_Td0>7KUB8YhGg4DW-4U)O4;{Ir?k~ z*xq6L9eh$<<-fz&{~oE9E*$l^-j3AqT>eigcj&ke9qRq_sm%wfV2_>SgAIl;$pI22 z*q2ZX%QsU*kQ3^?P7dfQNIM)8X*4N26%32dFCN1M=Q1KQ=CDlPS|Yq41W(SK!8POi z^zQv< Z?R66aHw6-PS7Q=~ G)P z*WL&aeD(W!W;CfR=|^bb?CP&DTQ(8em7`k_zdMcx>5&$@>m1s$xqb6^d22S;@o8x< z;ctT00Uo;8?9b%U0_-_AD7PQHc(IhOG%bUpaP#HV!=?V}x?^C`hfdniapCq R12vd%sDa{9p9_`vYpfQr2+HT#jBx z+6^qhuY#F}Fo7VMK%c5~{V@##(GNb-6i6D-5pK0_UzRZ7#q}!bKyM9G>6%_3OML4X zF~jhj*|MDkvux6SgS4PtYDgC)u2gx|Hpk=wukPB_+dZDWQTMf6i+{xk%K7v*60E z4wV)chY-jqSiH|;f#SdgC`=gu$a7nEtj->AJ-;(a9<`0d=FLh)Z$?NQ`d*$nG@?e! z`uV_pa?F2F@_hgP9m(V8@^TcyQ+Nx0e*e+TK0JyH7rKP-iWE}!>MX?!-PLr8x>pg5 zCMH?{W|3qBWYX ($n*~iSwnLsl=b0r2 z(NDF!$MJu7q0@}@8B+NH3&R#W+_;If_xSPtKl|HJRfGL2m_MDM7q+-zXwd1?F-}!R zszJA|U4wVe-4YcWyTit&k~aXR;2JT?-K5z}O0;{se(gI9H;!Ihj}HO@O(M->MhA(x zrMDDgscQ`qh9b1QISv{!IxMUh?Z&3vXQQK|L2TsyVf^sH)vMGCwiInZ?d99ve||Jh zVxg(&V>%>+AALncu3o>c(EAl^5v6P*mlufCjK>BAwqk{(l+=gDq1`%;nu6upwJM-r zvuCIF95AZk+A%)wsSM-W*RO+oHGThHn;)9tl19kwuPY3E6&j5>L5tY@>sMo8-@X9C zlpfDE+ilsRiF8tKMbDRL_R;!KVsII8-uS%ZWa8ROSFYH7xVzUN9Q4@N!K}@F^;r+n zFyRcCqoTSxZ^pteM(}6k!B~%WcT|R4Hga0qDo}LhLj#hF-*EMEBf4q3SALtk$L{6W zs;(_p*He_|_edp)rlxk2o_TB+BDNW@O#o}ytJ+H+4LE(;25I`mi<5a0oX)<(wDMC^ zFC*uDGDDj=mg4equLO#p(g7Drs;Z!O+6jl~MuENPG~r7oC0<;_0kr+Gr=!E!6I$&w zo0Tm-6?w>~c sC^GeEs_MmLVheQQ_g?Cr`eml!h7XcDNtPkeZb8?JdQs7PJnu zbsm3y^pQUi5Rmgw?T2}fge9;doZaRgAezo!mys3u_*`{{$s69^y4QPa`)DySF?tb& z;lp#@1`Lss!YgLm!9|E~B_t(>&Dc3*+&HvS*GM^#DBqofg%6_rVT-ECllJF4S)okg zmzl+$$-h2OY0Mb09iRWM`}uo>o_HvZJ$e1k9VCNUp> E-ibI*kEk^ZSqYK @Pkr=9{{=Aw|~EV`f}d4>i71H1~*p&y8@P!RaB5^4r?ZY6VZ9{d7r#$@Q|70 z?kVRjj2?z)o@xK(q;3jg-@F5UsUdvGA55MWFBUtLI!Q$ZV}B*Bl_n+>t+{aIZa#+& zy#TlN3|(zzCKPEzozaJ0+Dcpmw27M@)uDJT(v}Fy)N=fcn~3lS_$ eGjDqFCJd5a*bfQ%^C+_@%OP3{K0Hl`EM=`uzFLW&E71?p)rxccuWm z^?d#I?StF5>qv7nJ4qP| PZ z5na8ZLlN3ceii8j(#&!EIqJY9wAPCa59!1CDp# z &T$Qm!{{IN5&il(xj7lc;@`??OVnzlWxK%QP(`Gc&;m8z@SU4<@b^^f{L< z>5b@SBN*ku(Gbb)?ssqHA@f|8WOf9qhC=0O(@w`VN1%gxS0RwKAVrXd)YQ slw_l*T=MmT40o`#I1(kKkMz+uGMiDrr7Ijx}Z0w$Ol$~_~ygh zxN+z%;19 ehirM$ah-mOjudc0}yucJi;iDb GZH@ss 7M>FfU{ zce(v?%%8djJyw;R4A+^$l3BsQcK{aqKp7Xc?OtkZjL(KQ#i6HXicm7OpEWo)&4QLC zsbYp^RzXbblq3J?`$eqqCWNxeKN$#q%G7A-9VSdRZQi^v PSX@zO+2EV~#Di8cd@pe2EwKlke@MU*Q82t+j)#`Kj7RGV0v91G-oC zwS;puk(vp^f2dZ_)^pPu5B`GfAOs4gE9m#&rFQ)IP}7TAgR|pv zv8N2ex3zbH4q&jq2ALe8di3NmbTIHTGZ_5A{V2Jxuuy=?yipHR;#G2baMy%udAi}> zT>fC&jSJZ}`AT8gHuNe0JD;`Yl9A?IBmyCZcWAGtuwWo+;v}hu+c+#d^^H?juURAg zvwauH*XL+vblMB*{%-a8dw}*7dc&Q@R<8&mOKiQoa=`+U)Lg3Sm5kPdK0>0XPdSd} zL8vEfzjYUt+VjH1jRjf3yf^ZF%<|I#0o#x(UcEZT!^1;9PL6ZOvu|o@qA-3I{J5yB zY}xA7zGA*UKK<9bHSC(CrdCmRwQLDcGm;Ryw8B^K-zT`#%jyc?xI)HMJ6mEQT{9|B zU0q9opzu4?b>^5cWAY#GihdluFS%H6_Ux+4N|iGi%(Hkd#@!Dp9r+1lT?2&^shQjD zbA9K_$C3&@=~kE)gsc&C(LB;F|NK*1QxoH{LBoQHK!{E<9-wN{ejL~1Vq=A^S 5WHqtsJh6?M4y0|L*0D4~?xU#$nr4}vXomLjx_#7$Ytr4bxjkTYj3 z_hhoJpdcjGvVT{kV;gL3GuNtg>Cy%G<5j!ez^BCy6m^#Y(ppRfMR8#f2T4;?leQGt ziD%fDNI%hAyIXS~2tZ)cpt$#gJRZIb)xHxR{@(Tl @?INjhZy5F(Um0H!Lqe~M$qh*G^| zCdkWuR2<`Sg8YMyizkY81?QA^n*MBs?nA6Up@o4K8e7Xhe?I0>fp#W(eddrBkCy~O zy-70vD>9#W#9`{!sHk($%gcAu1@wkJ ~m1H-?2$^QM`Z3iPGo#@|!-d4a8EIGNBpce!p>?bf~zt1sZ`}Y0&Qdxb+ zgUOt>;hreA`s)JCi=|tN>?64nO>CQP!+!}}!PIPzp}h_$C@E>!yplsDB)A9V-X9IJ zrl`1)DnDW8#`amp#!UpHeC brl-A|I zCKThnrau6hb#@lKCD6fd1<(I0NIxaa!e#`hs0YhRNx5 P8s9{J9Y%hsI~5 zq4%LfsMRbdo#Dk4L~a}qlt~*-CCC5xrLv2V_sZklxLd$?t`E^~MP&VkSh^LY;2~^2 z;1v_=6PBruKe>SB+}X(qF;u^P{XF={$SS}ir%U&B*PbqIuz2xe7v=dgXNFN`Xa<@2 z6QUbu`2l^D92A3xA-8>sO}hPbtxA;LC*M$ch!S+97=M-VJ-LtN$~(aEi{F&Oh%n<< z^l9!tUAm~xm=KWe DVLm4;n7?oZqNaJ}a3=&4V z462zW81?-7M{Wp#0=RJ(XsnZy)4X|U+ xR_|9a3Vg!_bs71Eor4XuLG`z8Z4~@SH?L6OTv=ItE-t Qc!s8) pS@V`Dbv`Fi=qa`&{ogZXG(KzM8GU}_8qY84TU7tRFCsbExpOCn z4w(ZAORMocJ;LtXF$aqf;I;}!XJ>%~;q3BgxU;M0zKU3RlMcZD@|e0WldI?`IRp~R z =z#Sm?P_mpW#*AWDo=lVrO_7EJTe1t4QDbxUl>ae38PgZ zOxm&divN)qr+K7+I$876H~uC?dQv}oci-@lXW)szIhd*@8RXY{kiWn@J>6!JY+#;8 zSS>2d$T%F=Nk&R)OomOz<0^{Y#!rToY?!_m>SRqUaLn eL4?4Lsq-`tD+CL(~G5S`3oHwI?sw;Gvw2mdBa_5jiL7T_J)Ovqu zJ%<{_%uLt<^-#O%?3{}bq)fE+l7GXHTj4=?Z$v7;fA2&GdhQ&6efsYCLEqPmJvp{$ z!v}^LfEgJkjybPQ-S4@WFinewFlLg#xdKo!tBx2!L!)si(`h$}x>N%LdkM<`XxKS1 zo?^MVY;4gJIdJfxFkuST2{QSq#pyq_r_=ah!-f&_+A07nS|Z!Z>IIfiS^D@y3OWkd zcQb+j%&y(Km1_NrE|^}b^bOTz6-E(SQ=ij$6Bh+Egzg^TTLV%|N2gP)qGb?m2Iqf0 zwqw%LH0i&Z)GpkI#3pFJKazEe56fqNyuDh&;Oii (E;pt-^eyUR#RCb!e#J z!`A__FvntRcl=pS&;)NzYq7+4f@5@`&iK{&n-PKo>4S=#KfkA9J>V7tBeUC7&U1Ug z$$;K~f%)46o&ma7 q#5?Wh$Wd9=raMa0Jb;_=Z= z{$>hx>p;VU&tK_}YC_1z?S9rzmg*5CT!mwlIJe^zc1oD48pQeaINr5%$l$@kB$$#? zHSrF {> ze8j9%+O!u)pNPQ Qao* aRY1j!=IA z8%;wvUCIHR&Y%c^0IUU&^#<_L)vK=L8^^1BGLe-oqZE~tIM@RNTey%7b j*SMt} z*}5aL+JEJ0L7)$YML!L?KU0iYMeSJ}KXuoxKevO>#W(VLTopoE0*a=qP5wVE06j9+ z1x+dMvAJUSJc}cZx}BI2Q<+sUd^|fQnI6|%yI=tr_m=mc{_cJH_YpO#38t<@B-oq} zTYfOk(O*~L+QKhYRZt|`4o-Xj?j3~4Y0)#mIDGQiwyqgBQ;f|rPr6O(bzooKcz{F+ zb~#B&O#>|?);La+cD%|#aIsqk#cC4ge9PC@H!RGedd0INLl26HTeO;ba{igj6KIO+ z>H{~6EvZFoUw;303R)yyGekkraL-g|JOBVju6VF uN ziu(2GllrH#@DVG-g?3qeS9s{AwC6&2`1&ZFa;osb<`uNGAGbI#ovEZXUQO*ie_+8| zh8}pwl&qL8Q)ckH#I0BN{{xY?B@i%+CK(I(xD8rpCWS9%gR$)x;)_SrF?3zam(P*t z17yz^WFq5qsGz~U3#da8j(~|u*R5x;fxT(Bp}1@V(r^T}Zc>^4kwB{M5>8?zE7d;U z`8ln>=VC}k7dzQzW8hrcH_D9sk &{mqyrP#_v+m{Ke`a)%juV*A|lj*lKR?QF}en$NL^o> zPlD^>TJVtWyU6Xw4^iw#BO=5@Tc~RJU5|GEk!U}Ic~8!}ccTuKI;qc?GQ|l!OxKy< zA$U|j`}FDYw(9>x^Y*>y9%AB#lXYwB>R3EAVr|^zqel&CkhslTFJ~krDKOH3sF*QR zEChv}o&9wun8(AqEnB?U!O>9|WeU0a=#d+-m+vR{;?*tQ3n6WPxN{URZQX}s27H 3q2D&kpeq7s1!^!kkWix!JT!=D&KX9u+nQs z?-o#fh6KQ@T9+I4UCn$~%wnMXhnA_m>Tb2K9QTefH{w~w?s$l63yWfEX9_En&aff6 z<-=W|{md8#(Pe$IJ+HZWQ64JE(1rAY0IjExmJxHC8`trv& jr8)aC*kT_k6`f-*p!OTeI2H2p@-c6| z7bX6)rq9yFi zyntMuAgo{L vAUcYgz(DlE)vMDilDgKJcIJsEaAzbXN2isM5}?mCZJPndf>-{B z#BH{pV*UICt^5Yib|4%^I2Kx1grWbO+hfVnrMPL8DK8PyIS!05E#X&ww10@piWwDg z*ZxVsoL-M5uPx}YL}&J5y^YRGZr
f{!Lt@qE#hpvc2pAk#s93m-_1c+T!b9=5G*hX~;_7?L_;!*Wxs^Ietp? z)09Sx1MRi9J9X~dVKFRwHpbn~W#CO!)tLrjjj{_cBmE0iG@`SHrl#T6bzPBQam4$l zLf-Zd8firNx*4%2q{!!Iks|WK97x=oPbNFTI&T#WIw6qF)cjU`l*Z_!l*p8o^5Upn zZ{4{g83J?(WEFJDUh5)78|RZkZ{k7A`?(YnER151G*xK_Wxuw~pZ`=q4Vh@aC~p+H z_3qgdg}?aYsjEz;48hNB2M6Gm?h|@lx=13c_*@3kOXg4UvOoetA*m^A&ts +YT!A@sq3fe- zi0Exa3D{U$KjtFI9fQc+%z4sWcjixSINT`n=mpXQFo=u`i;kh@yWJ?A>5Q~o8<3Fe z?2A&-`?FCr8JtK=mnnhfVOjy{12+W61P7b$-rY(rr6~*W_eUGou;^h<$EDfhp#uMi zm+x%8Pm3|om(aNUSrI8r57#A`3^}4owFuPD2^O@Hfl_Q@M#KvT&jjV3gIlQ@WNCqc z0GS|~y=Co~cxohhPvE_ER#uVB8&LeEr07*09CVO|lYh-hCbbQ#-Z%O^o)tTI{(0y| z2wg=eXS8l a5?81qB}(vt#J>Hqo8dzc1+s43&cFMd|F32doBSb(H{~|pkvG47 zeK}xjLz=LO7e#-=j~`KY?~?qmg$RfkRFAtOal)kAp)Pxi&~)F8jt*RJC7O(Y6iI44 zrJvh-J<(*wT;OFvI4?=BnnE#1=ZMB8U41W5?ak)eNAvH|nzX!zHL=Z`Vr(cUmj7sn zamJ#l32NwMe0+VARP-rPNstsF)Lpcj7qVyruOeIkjUYZA)xJoIVc4X&kNht;cURY# z(}k|6&6u4$;ixR-g`(IT$sxmi#Ou**B^~d%lg&XtznAEHIf@&I7SP*8MYs`pi6K%x zQ8_^4`Ni3v-+-Az=*dUir%R)<@A+ l7^}W*?nSHi3jzwT7%jwsYOL6MZk>f_Xsnwrcb|BGbIyvO1}A|1MSKYz zIo^v`&A*#a|NX9Q!GF1(v)Ue7Rl}*PN~%=tXd^C}?*mZ$4WmljxgzHdMt;e4cszQg z;!lwY0S~flA$)eay3U(CcgT?|JYxbgfsA`&81rJfmj%@X)0O^p%Fby?NtOT>D9dg+ z_c|FEIC`~aDT7#zl8AI9aU)1+LWVI{q`1#BLH* tnN2VKbx)qMxNNZ&NUVrB$}y)@XAVTL!g8mVCVM@yi1geSjvCg9gcaLLhi)Z(O%- zGD0nK>Xq>D7s{&{>SmhIHk4y?jr7!bEV;OA*RB~sYV}m~z%hE^y#ZQ)CIxeQ>Ao9) zE4cBaA{;S495Q4za^mI7Rk54f=sU1YcK@|&8)3B^6pl`K2Du+_zTJyh?QrTp4k~S} z6pVOF!E(19n{%-?_x{0eoeY@!0=XYC!ZW#;%3>~NB3G^e*&W+8=ylK0TlpCM`}c=o zc;k7Ce^PpeIYtl{Nx&D^JBtr6cuI+W^yjN%83-f!*@z7e|NI8{Cg2W88u)uCNfA<* zj5j)EHn-0v3JKb(36C<)6qS|^mtIK=(fg;q3p4mXRZ~ ^??I?2kq^qm^J`S}l0Ew9idCjgeeg+Y=^QL!7L|3SutPbeTs?pO zowvbvu3p8idasqoG>~WV9YAMF9P^3AG<_miC{07TjJ)m3j5=YDj@bM~8zDh2n_F(N zwH4fD(2zGbH`C7~CFz=&m{e^}R;l5@0#u5()gQpxv?EfS2Mm}&zl}%=OnYI#SBKbq z$|5MjRp<6Ilt@N#aA+X2qC1?MvO|#H>PJr)@YHJ3{PYm3)+6o`haM1*(Qu4i_l`M< zvHPbFIpX}Sa!GNQ_$&$O+D+&>2{|V=yH6BdcW)*18e-siydVV2^J4GG2$o>p-YXI0 zVITt=18O6t3l4g+YIa;oPR@Pcr-WoTt#*zz04O#Lw$UEmr(HD*Ff{ZLyeMRmQsICA zPT17%_rQ)9R5VN|Qo%-aMT7|uQ`0n`iZtBJ$nD%_kuXaCLp$uK4K7~1cwp#)N3M;! zzXu+*B*Syo^xDN <-09$fERwmn;>i{lzA7VxS-mzgie$8a$}q|K3@HIjPh;l9u!*%>k7tiL36* zG)@*GWs)0zT<1jsR-pYBr5QLE^E@-NPoXbjmgC36E?s)_Wsl#(cg~k?Jah3)T HmZCe*9cu~##9Hr3Q@(bh(bFkAZC#f#g(DK;)2=5i7j^3bej)mT?j1f6KU(H(U# zE5$J^N}8KII*-gRCZ=PT`M+=1si-~KB{JDXJ10B4*fADSNW>sNz8jf$)N7@^Iu7_h zQB7|ZpR#UUDSb0}vOf1dekwO>yKFKyFTDNZBs{?gFBkYmfMvTskB~|L+_P_Ye-9Jr zNB{NO>Rp!nViW-Y@}t-ONvIn(pqg2JGVKkk#Ub7lgU;6!O)#dg=WWlcU#qM^3K$SF zd*;kU+B|4p717CRYV2+icRvl!$UuBh(9o&N7|1}m_l V4p&`*?*iHq; uQcY=31#Ub(dFKgh}IDx_MWS?$e@7jeL4N`Ym&$w^5g{`fw= zb;}k!fbxS3(_fd8p?~`tN>h$NiA7%75djw5HW6tsz&3*x!uC+xXBZD~EMa_V+k{=j zC4qWN1v@!GCiM!86}GazrY8T#T{_(r%a^kg8{F{liBB1>nr7I*d_QwdNA7&gF#7v1 z#Kdc%*Yx|pc+0mP;rf2bmcQ<&um&{F10^H?I;E%Sv^dkhh=c)3m6ex^4;ZJc973hy z;L>+h5Kuf7IoS6^(cZm#zj*$fKKA&QtNh&Io}6CcT;NoU_ykno`08>0y9Q4@WCPOa z%(04LggAA;Wdb7soqsBKdzJ*V&bk*IX>VV-;>_U$q 4K}s?epge^BSIu5%M8| zJbQLELmDf`p`U^%Ow_P3#95vONy>|EcHyw~RP8)Bw}2 E8H%C^O}tecSu!(LmR$JX+eb!R=>Wl;KmKn4G--(xi@C)XD4K%9|+7c(-7q zy+>QqM2J@`Aa=~2eZZ9#UofF0gAABqGV CCUImCagzA8A~( zo4bR9!^)KiGkS&e{>d_La21g-h&S%3^EEFN{e=&=Zzgb~<_!o8e5O*v5GkROB14!k z`7&bjRBZ9&? 2YmUvy2pocV$Y%Qr z>+y1Oo$QkqmQ7KdqhHH4CG0LOFx$9t)hcBBNZ4gGU5}l9A#_`PR4K?Ayv$a9+%$WO zRGQ_zlpUpX&ej_?3`j-d!Kh&S@YP%X^ZmjRL;IvTK{#N(GbYmp%8qh5KYtA{1Nf>) z7y({9fr%cjg}BF*AxF@7@Plw5#RFpaxoSi+%cr#)(GK3V_$OPTlVK~au0E+RrTacZ z_VcWCy1Ke=T2e|jYEn?FB1r_6sGRMh%Obl%AVOSO`|o!irJ<4c^r_d0Ro>~$jKfL= z>7Sa^{O4HOc{qA5; sph=x~sSlgOK!xMb3O=Vr_V|603N@T|O1L1W6t z12d cav>zWc*g3Ow>RFO@J0>u!4XIM|Okqs~xkuuGratvXaP z4UaXLnAa6ZaWam;#q<0%Y>$H#>eU# XpiPiXZdQjsN8=$ Bc7apDYSy;rtNjnqvvB{($)MS@&LGuR-3 zS$ky3L^L1(1W?Rnj9VYf6QeNFa}B?K9lPt$s3U6tAn!*+G`qD?5K>MeM7dlje%BPL zT##to2-rfzZP?vc@x?Nq-cwaojimQhefdJr_czjPy!+|Ta?lZe#L}hTz};Xgc`IbJ zy*jThC(eIQ9fiqtS)ahuSWP0BBSJMEgfXB;741H2Ul{F+m@W`pC6nu5hxmquW*t3x zl<_(#FD8uPjIhzqGgJ+%{9Bvi5k z72Cv7joN@^4qx6V7T IQMvg!&jP9v(1V=tqgK|WiIR3nrb?QSFa@`H=t^i>)C;oy|KDShCIUnK zB`23s=v){ep&qPUzH<)W6 x*B>Yn8Rm GS!VD#Whv_WTi~6`&j6)(KS#118$q?FepN2kaMUk;|87;AB=C ze*jP)riWWcKZSh(L$N|i1v)`!|E;lfuEIt(-LfI`@ab-_PmItzKfGdsd=OB}f3SOR zA~d-(Y01EogPnAByC{CTks8e)q2p*cxnZ)h^gs5Bfe~L_A5qO8Ja7PMa^$sZJ}JFs zk5N)$OTzK 2^m)|Lg}M#8ua$GF=)EzRJ(9sjE}2?$Ysy=9Smi z9~CQU65u85@#U@cF^B;OC>9ljkUSv!2cD-A*9x}G1e4}jXrT#4Ji?NGAe01uLc2E8 zRNy>pm|FxYaP9{G0D_l6@&}BboIG4b;z*b*t pLC)I{3vjyu5H zxAOy;cquB9GnmimH#9W#02P7V<11m9Ft^qiHI+}lHtaQdqUS1~7(9jxk9Ti<$RP(u z`&2uO2tjIpVR^5iU>tQp^FAB5 EWhGb#cgn{& z3uLIl7(FgKz>q_KlwD_Gv6U;%`5@gnYI}Sc3uycIA^H3iY?VaXRGB_P7xoz{h`h)U z9N*+7b$)5-exbRjr6g;dPBG)VvU2=j3Ko=$eGd-DN*iXpF)%dXGItPAajuB4UY%}W zpm~R;wrH0N2b^w?>;XK8Ox%XjUtDxwz2>{RCL*X0hvD$yfp&Ki*Q(Ui*3v|HKe1yP zj_}QE6#7#YePiQooO24OQ(KHq$Xn)6pNKCR^?hhRR?d1pU!|m|_z9tNYvITb{;_xn zRLxLJv9Pw@POS{E=5N;7^W&pBSf`rEVbT8nui=oEn3|3k6TxMQl@U31V(hut$jHUT zY<6AE&mFbP()aLTJrPLc-V>g)L`TZ&h}1+~IWdY)F_&_Re(|}HC2o2s^X&1h&TW3} z;N%3q-ZwSKqa)_{2N&plOuMNTFNYe$@j=F^pQw3%2Ss=j ya>JG9cj2Bk3MW0a9gU>e?l?vzm{7=R z{LcT6w#97%rKNX`Bc)>J&Ydm$Zbv->!_`we*=iEL8n6N=)s)TS^PMa10kKj Ve2TCz3~H9< zN1`bo<%*mq%B8*9OuP-9?r4QNg6-(*?d|VezWjDmtU^G#DAuO&QCX#pY+wr~PAkL; zYFZGrw4ihPHk&sm0HMywwVvzk)tye`eNN6r$9NJn-^A#hIpXMwin-Rg!Dt>=u}|H> z+%W=p!%ud`>WJ&u1N`RIfX6LAbTL65*#vET@doq P@mSwgpsDJcAf6#>O`%A~C2-gRBUa)GnpVsEkE00=KB4ohEttDix;SA%t_Xm^2~ zyEOOIh5l9B%*;X_Kb~>&zO2gHPWN0wQ}}oBde*hhExCX9?lh0KM-FO(6gAeAh7*j_ z@or4;Je_HT;|@AIGb{?ls8zR|{N+B2DCqXB^0 H6M zyQ;*`m{J7BnP=ED#gxc68F}O7OOU~Bf3d4fU_vyUKrL^xv*kBBju?J<@Bu5r82Mmk znH9EjoU%*f;+T5dW%4Kg9f^zqH>y;Es*zYV|^Vp@%NfsV`pQD zLVmKCE|46V2eM@7)J;pnL;JB4P=JhFPx~2$a}+Jj{L-x)W7mud_{IJ;k%U%q;YU=% z*EV$Y7Y!3&8q~>F=`T+P1hDc@te_#1TOLvOE05Op`t`92)xZw4zP>n=d8Y j~u@v^eiXU`ZuxpMK{L_QiO6i 7ihX80V6&LLkP7WM>fh?A{hPW_*XPlD-j(`LqRVX zAHo%;9+k9PrGk>P=k2 PX8 _X07A+a6ZK1c|fw4Pn}@f%7F z@f#{Bcipw?MdfR fr9Z$4oJ2y zC}rlLaFg+(z6aNB-psK>YNy^Rx`#2YT&f*Bi+Xrj5T2>_UuI<`&=>2-;i6FY?E;=K zct65vpI&@(!tXB>ZYWopYiayg?d&5=N!)<)2k(KIao%bAzf-iwL!PVus3%}x6E2 Y z9!e;Znx91nf*%%+m6U}Oam9SR6#f3iY=K#W9Ym9(S DuK{pH#K2^fnaf zd`738mk^E6v?I<+w3;Nieq#zKnoNKI8Kt*U5gm$40e`| >$kLj(BIlv>S}P@CHtXvuv@n(K9qRa-;F~?Nj3??PNdhXEQ&8 z@OFys*Wt&+S>N;R>sJLA^$SRZAiHPoGdi&C3vY)W+;~+~4TTT;5OsbBe&s; &?7c8%pm63T&0Oeq-PMN}h@{1==bntIzpFE7x7S`OSG@;$>KT@I*LDRM>gv38s zt;FPrLav`%SLMKkyFZpxR(=?}l`*o9(e|NuamGFSgLlI(H~3WF>VcqmUeQW3ML64d z68EW>)$!$%LVq~-Y@36JHUq7F@xVEVt`1ZA6M{6Z&oHfX&m*kKMMqBjD5L JIzE0bLb1BXoLtXGOhL5 zgc5>M*#7;=l#P_{6Gf|`H<&S)_9D8YZfbk*VT&`M$fgN<$a$fuxqB8aKsC~=d;kpE z=(Yws1E_nUvrHbjPL!<@Rv#kt<>AVFU(WUhb+D33iRf_>H{PDLb_59 reJj4$m8D&Co)1hRw+L%gwmxVMGM5w64^lnyJmT8Si`ks%(fN`l zlP)Ca%MAz4 @wspvl_jrUu1TCL0-G2^;$Psov*nOkv0w@<>KR;o#!Do6syfGlBdhHDK z(!gNjeoG;rqaP?Pp2oHmhiO)Wd@NQ{{a|1~jA4<}JF+}rqW`pJ1fiEQq(^SUdDF4p zC>#gmal3QT%r1SWe~)UEaIx&{A3#Er2(2OVGDn}~Av(uHj1T))=LQB3F07!HgY~z~ zR2dRBv~U0Ut-wDIr sMT=&O2sPqS`;^faQs)x$=(#^^iF(t`Qp!tk_;_#b z@x8GT`Q?b%!4rcrIbl*>TVwMXJ{FJb2LZG)=AxQ4N_sSFv)+yS`1Z?oqO?0Ax)auO z_4F1OTJE!Cbb<=vWV!N^*t@|DPPh9EJeWLDMP=>%Tvziyn2H-1LPSmn)dPAtb7pYk z{F6*h=*ZzEofsW);^uUAkByBVFW=} J>l2r%X4-^5!e2K)8Q4j=6FdW!3 zN!3{VZ`5&*hZur7p6l`|e~0!XBv7&wPV1chTR8nPM-9G(r^d0_6N&qDIuvv=mm>?u zI@3E6JjP9!Fxu{TipCoj)8gWBH2 IX43rSMjr z`4i;66DAmz |P7Z(zIctQE{kBgaA{#UD9O#0mI1 z2;+#k|Cz5l!8K2c&V$9v%E}@F3iCUh0H4FxqN1EXryXY>%gyg;!F>73F>hIGi?ocP z-2vOikj{&{rvyjsmB2e^+carpB(y!$6W~vvR)UA*zA)H&EEC6X9cg;u#*KwFGv=-8 z1Ox->J2$iIRw_vFWdCIg^G_jz(a_Ks`=U*jr_P4Hh0fKGReT?%oUfvyq9K|)_KEET zz3ivmBG=|a6H3@v9{y`uu)zvrW8rY21tpHN`zat`K$}}$NVe&Myeg-M>`N^8HP^s^ zN#rUrVo7L0FI;U{A?Kd=%dL%$013 xZmzFHE^w z)Az82R8VHTYomg{vc&`?C8k|ePFj0tV;s^>9h6>YLJri|I7R7235#It(z4#=2KS3d z%$~0p?!-q51}}!7-GA7c!9#}@#%xVSenL0%gTZ_)kxB=)?K)rssN;V>dK-aPHgt zo*aG#8T0d7_zmFD8@)P^uI3*^=fgep(Z~IM_n>7MamJpt7DQ=4353U>db5XxG7HGe z2~q{mV_om{-=Fw_)J38p^!{P`Zn!6eXP)y$D<$p|79&6?Bifn(pLy{NS?;EO$M`|v zkp`eZWm_jBd>Ni4+@G*HDZQTGV--2qIu129qRqbzHihodh_eZ^pbbt=+uyfWFaPa3 zfKn~p>-WyN$O8uyW5jE){8IY)B})=3nuJNQl5wB_n>SZSW&LSxzG)h_9H$CbR_%@E zI-yT74er8P*}HcuzpUyCI{=)@Y}*mOGHoCDfWPaQjsm%U-@SVi3nZtG9s9-P{XPpR z=Cz3;#Et;ifDz_&^1&fe5)xj+Fbl|Lmv2vH!sYkM?BOLB0A`6I!h#5bIh!sjZvr&% z*=cbJ7zm_`^BVaExTakByQ1uS^@`VOj$6tAY5ZsVieuytOz2;AY4sTmm)kmW$0BM- zd`ng}{Y+<)kX~Lq|1oH-PvM}sT|g6%cx3EHesK
1}v9d;lmJseCAD2Q O3qCQv$gR@y$Uf>z$ zbbl@`n88A)tT1l{@b(Vv@6xNjUY{On+)A;bLrvJ7#nWNAoYS=jz1xQUwW7moX|8yC zSZ-ck@94~#!Z%o;;s>fh{HBFHamO`KRkx$;Yc4=Ybf#qLt>D# e|J3#dE9`wQ$`jiD(R^qt|IPS`O38<>Lo}d|an%p`k&E zk4iv5LVw=B>P)T|f23s9*d1Ln-1$V^F;k~vDSdfi1z}Uro(oHKwX}HZu)w#irj8jq z76L@i^$O7r;en|P!*;B#EF-(V@hIvj2pd|B*h%adTlmnoy%gb@mP?&<+vG#3;^Dux zP{%UIF}zKD9{H>Im< kV<()7=rEq!{ogq zJx0xxa&zVa*TPO`kp0Rld_taMG #fbZyND>>dg^4U!yHZO2&bh5 zoIb%YjuEEk3%-aPOrB1!yu!YoUx^c6`Yk WnsN fielr+R-yoV2Izu{C0)*HQgrTohHkJiv_GO%0amxA56`K?$Heeuri z$x@==b&N5$U>j3`^uuQiawuL%|07zb^~*WyELtAsF!p9t)DLn2Fz*Su@gtAZ<$?aK zu%A>Fe4YxfkEkfIpP3rUn-uLO=;6uCMrNO|4dwkQ6bR0d1Gbr}1{ `3wd6U$4dJF5r*$fvI8F}euT9mYm z49i1O9$juucpbUAt$ Un8lf@8mI zd@K|ge;PvJ;^H9@`2L=+0JJ8GVkm3>bF!%H(FzuTlr|#q1;E+4r{fWK@@4k}{w!4E zMvpd_#!ZtZX7+4r!o1Xb%J|{KOAiine`0*e1L9dOy~Lm=ayH}XQ(=YBt0``VBD_>H zyfaf%{bxw02#Yz9P%SogFriegF?xXduX*`p|IJjv)L?vRphm7aR|8R=if-DoXFZbd z0){?&`ZVf~?O`q!f^tL|9&98AEQ&iFNPRt^Vlv`;HjeA8Qrhvq55C7EM0UnbtReq; zp{3Yc$qyF{Cf`qH{F{AJA{917ILrh%(h3HW;e$m|GtE|wdq=Xqq_3Ik1aGPQpzNeS zo*P|FeN1Ap# S`v4l6Umguy;RF4QIBbu2N zK)|A7_7Ma4&7tH(tVJaX-&}z2IE%vVF3KVUa+~40em%zV;#+ uGIVa=Og!H$N6mEtKC)R$rxBs_-wFQN4$|Z!PTyZ`fEkBFV z|L$00uwe7anHtllqhXQKo-HRvgq@Z0YzsXi_$`pk4_-MT3pbvz2Fu`x+IU=)gglKY zJHA?N9NjS%+EAZd=7=A7iun-z=L&)oys#*TTC=I4;a|6G*9`^i?;?|OFw}#=na?7i z4hc~*G DHF^ohnV@|B>wqhVv*PfiTf86}`AJ(`T^ffr?z(m_(TAW8~tcZ^r8 z2vxH;V+xRD*H|iySy|p$+HnNrlp#z7s#W_X{ssMYQHh{9AY;3FlbaOWGKdkk*TgEk zw1Lp2=%A$LMXl3NFNemXP!EW*y9dhy$~}AE`TIBEl?|-B3&R|W$@w%z{in-S5$`xw z=@0X4$=Lke#i}x*$+$DIx4C%`hF^=GE5Tb4FN}Nx_W-UDgY#bASk_iR04J*R8XigZ zY1}QsLFJ{E7)Cw!2H!tmTn--Azn*P*rw}Xhx0s8K8cA*3qkH#JBSye!ls@A>KL7C_ z@UdksHkbmaNB#Pj4`5FtP||!=VHt y~+Iyz;3fx?X*rj;^b{L7sd z=?wV4OHcQSm~SBkFFe8hx_j-%siOiEGs{RZ?hLxnE@6D^U804g73pGILvlaxs;7m! zYUBNo2Ow9kt?TiDu9C*^!R`W3Kj@>BS V`^01!PYl~J}q-ZV2(-*s~st9Wx3>BmU*ft$X(zhH{=vaN@K@9&&e_7D(V?- zPz_qt!qCf>4|nA$pGJ?~Kt C55 zEk+nlNquJ*F={x{Kj6j_5o?Rnj&^zN(!0Mg>ZjE4dsbkExpA56a>P|ii&U?lKYw@A z+p(JUAvntW7*}4W8Sa^Nn|VyGHc5-ck*uT=&rXg1RNDiB0P*2rms6}61kxp0^7IT0 z4e#6(yln=CW7!_b0RCDje)k;fVb1@%1baNj)6t uX*PQd zjPkt Ef=epTOZg7j2zRcn$!GYZ znRamEW`-UDnspIe=jHuBrp`Q&%XRDfYO6HM+$4l3g$5}q38_RHNF_r^r9=uP8kDIC zm6Q^Z+NGjFNrsRlDUB#XlO{t0m3TjQ`+aiuALlvG*?Y(D{$2OF*7{CMkB(`~7(oa_ zhopQje%6z-Hrp+_x$h4@awM#vfWm;_ I# u6VO;0PT}l9JmQn4iIc7#j(h< zd*#BH@e3jh&>Ng{=Z=sXN$1E*1sf$62TsKZ0kP~t+;dCM=_ek5jS2euFF*bs5)lyx zzE14t1>Bar*TME9NG7g&aYjg?08JA&L|lwQm|q;8;_1`A1IC(u4MEkFe_q%T>7D*k zTR}mj Q0|xA1x)uG)>E_Mg3t63iPfocOGkQT@b33C)Sq*v|EQNp}H#=lXJmZKui}K9qFVn9Q}5QK7ff z!A zqcJU_Df2x_IZB)L_W zlwx3UKCjU646?O)p?|q`WEj01f0|;TyuJ_liaQvn0Z1=bI4%fec?L*Q*DhVY)jbT# zvA+v}p*Y{xmd hXp8u8H8q9J zl2a%@`Yoy+Pp?}|BYfNhWc+!mUJc%^H=`@|+!>{*DePk$_c)ZOMb@v<51>jrf8ubW z? D^c*)vnF z3%|?to @j0tA>P$X@Yhd6&!Sx>_7(RJm%;yS1&J%(0un|=ca+Zu5udST| zIWTf$Z$&TEIK6Ur_8qd6bvWd;6D`2oMb1`LK>KEZaogdU^8`VZ?schov=rCIdFQd_ z?(A8!J1d!fX3xHCn-~|DLJC^-xzP{^rQPoTky{I3(3mhjYw)P9v9rJl51rWvj|#Fc zZpgb{^YyYq6sM8i3^b8QeF4*ZRVdl&D-osKO;l7Nkdx1N$hAl$kHM4M9Y^DM5$^$> zej=Iz#nU}s_lPPw$`%=dJiZPM3Jin*dO3xnj@aMCD5|SWnDZZB{R8F|${Fu!u>A9~ zGGA_8z%Kf8K`2NNY~_b*$S8neea^figM?dg=9F+NC=<0o&JkimgaPl-*J{-M{zN-B zB&uA%OVDHHft>^rGf6RK-QFCH>V90|bzGucX-R?sz^i=6Ke<|r796`j>+La`qV!U{ zXA|*&qYo)7e+N)rP0e0TMnYn)wRKRyBO}rKPVD)nF`@(nB_|6_5OtoYf1}ST9Z$hP zmLx$&6E?q}Xt$k?YSw6G)vNvcrQ+vpg^JC}nj$|Xgl|$e@IJ4h(Gbo_NUX2^Qoh0m zXAb99i*J&SAe%n*>Q%Y$@_=azxhYugzAID~<@28e@iQR$V=J${{ cuH@vp60|)@chgi)x4NMJ;o>$>X4~9v0--`gT(R^q8)tG#y;{z*9BU_lh zo4cNZTG3{;ZPIltJN$+1kdMd#_d+7IC%qUHpGk}L16)%qqZM4mVv3GmQtE{r5~gNa z6(qHG?s5?=J6+h^gNNDC`?yl0>N&|u1OjL}_9zVqZ9szDty?#nsN}df-s7?r+6*O9 z>kQ=grY$d#*G~Hud5iiZZph#_N=?nw-29f_7bNGB=M5os;Ec#E#W|;NVgLhJew|~< zYftC-Tk`NAYN3cBIO^}`+VTiEDl>C(<@Zi~5AUdT(K$k`T @A4MR$@2rRLo0ft67ZwFw#cVWo1Qncp9A2u&3WELN ziDERdbdgE+G5~1%ubS##!liJ7Jz!lDBg_$?(ZA=Zt}k7FpRbq4nL0|3tuqe=Dw!Xe zKZw0eyYsFTeIk>TOGf1mI}q} EKzzXsoVbad%gDU1vhHraI-M}ypwg1{lBsTH*#zeIfV zK~}fXGFKEoVob9-yO0Z4&EvN+fCmzc6Z?G=#Tcee*vr7Lb1{`Z686jcG}!ZM4rZ$s zu)@A?HwAmyliBiE?-AE)ruV`-!mSm3P*GJi62bg00-dUxSVo7=llnw9TU&T3cQ}c{ z?Y%z;Bt4GUUcK&?l*A&fU3#sd>%Ud^>CXEZ0xnWhs?s~u+grZBtE(eEVHoKjkg}gv zZb24K5A*5MCt|+wlZA=*=-xd%G7^t{%hG_O$3COBOo*YSIQo6%zR1X2O(VC@)^>IQ zpP%C#xjt&aC}Gb{p0|7C4wxlLZ&=LjS?R4`VbRl?&8h-fAU7ATk zh>a|3z|+l(kbG}HTO{6XZ^04mWhOp8EdY|hND$xuSqukDOREe@+VJ6t5UC^e=;4D! z?iAekku8K3FicG|G|XHe!g}-+rY |nGk4vvmZeLpS# zix(l>@%C z%0#rKC>9nJXX<`yhSVKNxeYa z4PWA$lq6iDx-) }`EDKlRxszA-TW$E-=g9hB^@V7 zcdG%xZ(Hm4n@)nmhXkQW?OFPq6gfczFs$dLj?=5vD_2Hy=^%W5NSL+tbkkT-l*NJW z*N8P*Q1g?WLRQ Rx-apKD#|1JeiSX$0LYV(-KPijB| zYCal1In9Ha5j$UEMbz5nYM3bakh&34ut~-yCNph>Kws|z?h)+o{Z^mVZZfxdyZP+d z(pg Gl@M_?JQAYCddqB+iOmLrx|@b$ag Rq%hF^~F2N0s20!Zq&jdy{J15-ZY)|J2Ra7CIvX*d2`vqZe2iDuZ}|1*UyMAS$@ zoH`Lw=AQ$g4ZqRav-sGgfr?%zJy02PMN#W 6;OZBgU-PYjydGMvfRkQ~18@;#-d3 zjQ&do0M94?e1*^1)aOZ+){t(0KchnAi4)7vH{nLW@_cN&6IA5Nl^Zsqul}f}w4#|+ ziYX-i;83WErlX|So-rg5?CI#l_5b2MrP~E2+V3LF_3GBQHzXkZ=WU#OZ~i6lBY}%b zZECQ=q`SUOa!0@*P}9Ib(GWYMc#mrh6$@nqAS6A4-Sp8i_f?ofWZKU75(2chAkya& z^#I#=*fqtKNn9Pw>P&=4ENDRhe`%JoYa5ZvqYDgKW_ryk3WWi;M31bKob*Rg9j91f z9qb@6Y;3LMn2N>090AIc!5`emneS5|D% SrR$RUGh*JD0 zrQ@ALJAG-Q@l0*@MRmo^Ct0@V@l?CD#{<&;PhB{?w`*TjNgBiQ6xk_7%O_-tEiJ zw^5oZs*(LQV!!xHMMQ5ZIU1<1yUz1mdU}nm6>|Ze-I=!gAd*V|f%Q1KW`l1sk-ej@ zU?eP?@}6&)piXC0EKir=aiS%Nn`3Hqel@xc=c0q?^f{X~M+N#AM<~smajNI@yq7oL zFz_SnU$AgtusAzETCI0=oQ*BRaoAY=zIy#ST4sMNB