├── requirements.txt ├── utils.py ├── __init__.py ├── .gitignore ├── README.md ├── whois.py ├── dota2_dicts.py ├── LICENSE └── steam.py /requirements.txt: -------------------------------------------------------------------------------- 1 | pillow 2 | pygtrie==2.4.2 3 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import logging 4 | 5 | def loadjson(jsonfile, default={}): 6 | try: 7 | data = json.load(open(jsonfile, 'r', encoding='utf-8')) 8 | except: 9 | data = default 10 | finally: 11 | return data 12 | 13 | def dumpjson(jsondata, jsonfile): 14 | with open(jsonfile, 'w', encoding='utf-8') as f: 15 | json.dump(jsondata, f, ensure_ascii=False, indent=4) 16 | 17 | def load_config(): 18 | return loadjson(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'config.json')) 19 | 20 | def mkdir_if_not_exists(path): 21 | if not os.path.exists(path): 22 | os.mkdir(path) 23 | 24 | def init_logger(name): 25 | logger = logging.getLogger(name) 26 | logger.propagate = False 27 | logger.setLevel(logging.INFO) 28 | if logger.hasHandlers(): 29 | logger.handlers.clear() 30 | sh = logging.StreamHandler() 31 | formatter = logging.Formatter(fmt='[%(asctime)s] %(message)s') 32 | sh.setFormatter(formatter) 33 | logger.addHandler(sh) 34 | return logger 35 | 36 | def get_logger(name): 37 | return logging.getLogger(name) -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from .utils import * 4 | 5 | __all__ = [ 6 | 'steam', 7 | ] 8 | 9 | default_config = { 10 | 'ADMIN': '', 11 | 'BOT': '', 12 | 'STEAM_APIKEY': '', 13 | 'ONE_LINE_MODE': False, 14 | 'BKB_RECOMMENDED': False, 15 | 'IMAGE_MODE_OPTIONS': ['ORIGINAL_PNG', 'BASE64_IMAGE', 'YOBOT_OUTPUT'], 16 | 'IMAGE_MODE': 'BASE64_IMAGE', 17 | } 18 | 19 | config_path = os.path.join( 20 | os.path.abspath(os.path.dirname(__file__)), 21 | 'config.json' 22 | ) 23 | if not os.path.exists(config_path): 24 | dumpjson(default_config, config_path) 25 | else: 26 | config = loadjson(config_path) 27 | for c in default_config: 28 | if config.get(c) is None: 29 | config[c] = default_config[c] 30 | dumpjson(config, config_path) 31 | 32 | mkdir_if_not_exists(os.path.expanduser('~/.Steam_watcher')) 33 | mkdir_if_not_exists(os.path.expanduser('~/.Steam_watcher/fonts')) 34 | mkdir_if_not_exists(os.path.expanduser('~/.Steam_watcher/images')) 35 | mkdir_if_not_exists(os.path.expanduser('~/.Steam_watcher/DOTA2_matches/')) 36 | 37 | init_logger('Steam_watcher') 38 | 39 | from .steam import Steam -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # config 2 | config.json 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Steam_watcher 2 | 这是 [prcbot/yobot](https://github.com/pcrbot/yobot) 的自定义插件,可自动播报玩家的Steam游戏状态和DOTA2图文战报 3 | 4 | ## 都有些什么功能? 5 | 6 | 本插件可以在用户绑定后自动推送Steam游戏状态的更新和 **Dota2** 图文战报,以及提供一些手动查询功能 7 | 8 | ## 指令列表 9 | 10 | `atbot` 表示需要@BOT 11 | 12 | `atsb` 表示@某人 13 | 14 | `xxx` `yyy` 等表示自定义参数 15 | 16 | `[]` 方括号表示参数可以省略 17 | 18 | ### Steam 19 | 20 | 负责Steam相关的功能 21 | 22 | | 指令 | 说明 | 23 | | :----- | :---- | 24 | | Steam帮助 | 查看帮助 | 25 | | 订阅Steam | 在本群开启Steam内容的推送 | 26 | | 取消订阅Steam| 在本群关闭Steam内容的推送 | 27 | | 绑定Steam `好友代码` | 绑定Steam账号,一人一号
可直接覆盖绑定| 28 | | 解除绑定Steam | 解除绑定Steam账号 | 29 | | `xxx`在干嘛 | 查询`xxx`的Steam游戏状态 | 30 | | 查询`xxx`的天梯段位 | | 31 | | 查询`xxx`的常用英雄 | | 32 | | 查询`xxx`的英雄池 | | 33 | 34 | ### Whois 35 | 36 | 负责区分各个群的各位群友 37 | 38 | | 指令0 | 说明 | 39 | | :----- | :---- | 40 | | `atbot` 我是`xxx` | 为自己增加一个别名`xxx`1 | 41 | | `atbot` 请叫我`xxx` | 为自己增加一个别名`xxx`并设为默认2 | 42 | | `atbot` 我不是`xxx` | 删除自己的别名`xxx` | 43 | | `atbot` `yyy`是`xxx` | 为`yyy`增加一个别名`xxx`3 | 44 | | `xxx`是谁? | 查询`xxx`的别名 | 45 | | `xxx`是不是`yyy`? | 比对`xxx`和`yyy`的默认别名 | 46 | | 查询群友 | 查询群内所有拥有别名的群友的默认别名 | 47 | 48 | 0 简单地说,涉及**修改**的指令需要 `atbot`,而**查询**的指令不需要 49 | 50 | 1, 2 一个人可以拥有多个别名,其中第一个是默认别名 51 | 52 | 3 `yyy`可以是`atsb` 53 | 54 | ## 使用方法 55 | 56 | ### 事前准备 57 | 58 | #### Steam APIKEY 59 | 60 | [获取 Steam APIKEY](https://steamcommunity.com/dev/apikey) 61 | 62 | Steam APIKEY 的权限与其所属账号挂钩,要看到被观察者的游戏状态,需要满足以下两个条件之一 63 | 64 | - 被观察者的 Steam 隐私设置中游戏详情设置为 **公开** ,且好友与聊天状态设置为在线 65 | - APIKEY 账号与被观察者为好友,被观察者的 Steam 隐私设置中游戏详情设置为 **仅限好友** ,且好友与聊天状态设置为在线 66 | 67 | 条件不满足时,从 API 获取到的被观察者的游戏状态为空,即没在玩游戏 68 | 69 | ### Linux 70 | 71 | #### 0. yobot 源码版 72 | 73 | 本插件基于 yobot 运行,所以首先需要 [部署 **yobot 源码版** 和 **go-cqhttp**](https://yobot.win/install/Linux-gocqhttp/),并保持两者同时运行 74 | 75 | #### 1. 下载本项目 76 | 77 | ```sh 78 | # 在 ybplugins 目录下克隆本项目 79 | cd yobot/src/client/ybplugins 80 | git clone https://github.com/SonodaHanami/Steam_watcher 81 | ``` 82 | 83 | #### 2. 安装依赖 84 | ``` 85 | cd Steam_watcher 86 | pip3 install -r requirements.txt --user 87 | # 国内可加上参数 -i https://pypi.tuna.tsinghua.edu.cn/simple 88 | ``` 89 | 90 | #### 3. 导入 91 | 92 | 将 Steam_watcher 导入 yobot ,请参考 [这个例子](https://github.com/SonodaHanami/yobot/commit/80b5857ca722cf6221b40b369ac3375059b8b0b6) 修改 `yobot.py` 93 | 94 | #### 4. 填写配置文件 95 | 96 | 启动 yobot ,第一次启动 Steam_watcher 后会在 `Steam_watcher` 文件夹下自动生成 `config.json`,修改它 97 | ```json 98 | { 99 | "ADMIN": "123456789", // 填写管理员的QQ号 100 | "BOT": "987654321", // 填写BOT的QQ号 101 | "STEAM_APIKEY": "" // 填写 Steam APIKEY 102 | } 103 | ``` 104 | 105 | #### 5. 应该可以了 106 | 107 | 重新启动 yobot ,开始使用 108 | 109 | ### Windows 110 | 111 | #### 0. yobot 源码版 112 | 113 | 本插件基于 yobot 运行,所以首先需要 [部署 **yobot 源码版** 和 **go-cqhttp**](https://yobot.win/install/Windows-yobot/),并保持两者同时运行 114 | 115 | #### 1. 下载本项目 116 | 117 | 推荐使用 [Github Desktop](https://desktop.github.com/) 在 `yobot/src/client/ybplugins` 目录下克隆本项目,后续更新可直接pull 118 | 119 |
120 | 下载源码(不推荐) 121 | 下载 https://github.com/SonodaHanami/Steam_watcher/archive/refs/heads/master.zip ,将整个 Steam_watcher 文件夹解压到 yobot/src/client/ybplugins 目录下 122 |
123 | 124 | 完成本步骤后,项目目录结构应该如下所示(仅列出本文档相关的关键文件/文件夹示意) 125 | ``` 126 | yobot 127 | └─src 128 | └─client 129 | ├─yobot.py 130 | └─ybplugins 131 | └─Steam_watcher 132 | └─steam.py 133 | ``` 134 | #### 2. 安装依赖 135 | 进入 `Steam_watcher` 文件夹,在空白处Shift+右键,点击“在此处打开 PowerShell 窗口”(或者命令提示符) 136 | ```PowerShell 137 | pip3 install -r requirements.txt --user 138 | # 国内可加上参数 -i https://pypi.tuna.tsinghua.edu.cn/simple 139 | ``` 140 | 141 | #### 3. 导入 142 | 143 | 将 Steam_watcher 导入 yobot ,请参考 [这个例子](https://github.com/SonodaHanami/yobot/commit/80b5857ca722cf6221b40b369ac3375059b8b0b6) 修改 `yobot.py` 144 | 145 | #### 4. 填写配置文件 146 | 147 | 启动 yobot ,第一次启动 Steam_watcher 后会在 `Steam_watcher` 文件夹下自动生成 `config.json`,修改它 148 | ```json 149 | { 150 | "ADMIN": "123456789", // 填写管理员的QQ号 151 | "BOT": "987654321", // 填写BOT的QQ号 152 | "STEAM_APIKEY": "" // 填写 Steam APIKEY 153 | } 154 | ``` 155 | 156 | #### 5. 应该可以了 157 | 158 | 重新启动 yobot ,开始使用 159 | 160 | ### 开始使用 161 | 162 | #### 1. 订阅Steam 163 | 164 | 在群内发送“订阅Steam”,开启Steam内容的推送 165 | 166 | #### 2. 成为群友 167 | 168 | 在群内发送“`atbot` 我是`xxx`”,为自己添加一个别名 169 | 170 |
171 | 为什么需要这样做? 172 | 这样做的目的是隔离。因为bot可以加入多个群,同一个人也可以同时在不同的的群里,但是同一个人的推送不一定要发到所有群
173 | bot仅向每个群里发送绑定了Steam的群友的推送。
174 | 举个例子:
175 | 有A和B两个群,两个群里都有枫哥、甲哥、翔哥和bot,枫哥、甲哥和翔哥各自都绑定了Steam
176 | A群的群友有枫哥和甲哥
177 | B群的群友有枫哥和翔哥
178 | 则bot会向A群发送枫哥和甲哥的推送,向B群发送枫哥和翔哥的推送
179 | 或者说,枫哥的推送会被bot发送到A和B两个群,甲哥的推送只会被bot发送到A群,翔哥的推送只会被bot发送到B群 180 |
181 | 182 | #### 3. 绑定Steam 183 | 184 | 在群内发送“绑定Steam `好友代码`”,绑定自己的Steam号 185 | 186 | #### 4. 试一下 187 | 188 | 在群内发送“`xxx`是谁?”,bot将回复`xxx`的别名 189 | 190 | 在群内发送“查询群友”,bot将回复该群的群友列表 191 | 192 | 在群内发送“`xxx`在干嘛”,bot将回复`xxx`的Steam游戏状态 -------------------------------------------------------------------------------- /whois.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import pygtrie 4 | import random 5 | import re 6 | import sys 7 | from datetime import datetime, timedelta 8 | from .utils import * 9 | 10 | logger = get_logger('Steam_watcher') 11 | 12 | CONFIG = load_config() 13 | BOT = CONFIG['BOT'] 14 | ATBOT = f'[CQ:at,qq={BOT}]' 15 | UNKNOWN = None 16 | IDK = '我不知道' 17 | taowa = '我俺你汝是谁爸妈爹娘父母爷奶姥' 18 | qunyou = '群友' 19 | MEMBER = os.path.expanduser('~/.Steam_watcher/member.json') 20 | 21 | DEFAULT_DATA = {} 22 | 23 | class Whois: 24 | def __init__(self, **kwargs): 25 | logger.info('初始化Whois') 26 | 27 | if not os.path.exists(MEMBER): 28 | dumpjson(DEFAULT_DATA, MEMBER) 29 | 30 | self._rosters = {} 31 | self._update() 32 | 33 | async def execute_async(self, message): 34 | msg = message['raw_message'].strip() 35 | group = str(message.get('group_id', '')) 36 | user = str(message.get('user_id', '')) 37 | atbot = False 38 | ''' ↓ ONLY FOR GROUP ↓ ''' 39 | if not group: 40 | return None 41 | if msg.startswith(ATBOT): 42 | msg = msg[len(ATBOT):].strip() 43 | atbot = True 44 | 45 | 46 | if msg == '查询群友': 47 | return self.get_group_member(group) 48 | 49 | if re.search('是不是', msg): 50 | prm = re.search('(.+)是不是(.+)[??]', msg) 51 | if prm and prm[1] and prm[2]: 52 | return self.alias_equals(group, user, prm[1], prm[2]) 53 | 54 | if atbot and (msg == '是谁?' or msg == '是谁?'): 55 | return self.whois(group, user, ATBOT) 56 | if not atbot and re.match('.*是谁[\??]', msg): 57 | try: 58 | obj = re.match('(.+)是谁[\??]', msg)[1] 59 | return self.whois(group, user, obj) 60 | except: 61 | return IDK 62 | 63 | if atbot and msg.startswith('我不是'): 64 | return self.del_alias(group, user, msg[3:]) 65 | if atbot and re.match('.+是.*', msg): 66 | prm = re.match('(.+)是(.*)', msg) 67 | try: 68 | return self.add_alias(group, user, prm[1], prm[2].strip()) 69 | except: 70 | return '嗯? {}'.format(str(sys.exc_info())) 71 | if msg.startswith('请叫我') and atbot: 72 | return self.set_default_alias(group, user, msg[3:]) 73 | 74 | if re.search('我(什么|谁|啥)(也|都)不是', msg) and atbot: 75 | return self.del_all_alias(group, user) 76 | 77 | 78 | def whois(self, group, user, obj): 79 | self._update() 80 | object = self.object_explainer(group, user, obj) 81 | name = object['name'] 82 | uid = object['uid'] 83 | data = loadjson(MEMBER) 84 | if name == '我': 85 | obj = name 86 | uid = str(BOT) 87 | if name == '你': 88 | if random.randint(1, 10) == 1: 89 | return '你就是你' 90 | try: 91 | names = data[group][user] 92 | except: 93 | return f'我不认识{name}' 94 | return f'你是{",".join(names)}!' 95 | if not obj: 96 | return IDK 97 | if uid == UNKNOWN: 98 | return f'{obj}?{IDK}' 99 | if obj == name: 100 | if random.randint(1, 10) == 1 or len(data[group][uid]) == 1: 101 | return f'{name}就是{name}' 102 | else: 103 | return f'{name}是{",".join(data[group][uid][1:])}!' 104 | else: 105 | return f'{obj}是{data[group][uid][0]}' 106 | 107 | 108 | def alias_equals(self, group, user, obj1, obj2): 109 | self._update() 110 | data = loadjson(MEMBER) 111 | if not obj1 or not obj2: 112 | return '啥?' 113 | if obj1 == obj2: 114 | return '这不是一样嘛' 115 | root1 = self.object_explainer(group, user, obj1)['name'] 116 | root2 = self.object_explainer(group, user, obj2)['name'] 117 | if root1 == '我': 118 | root1 = self.object_explainer(group, user, str(BOT))['name'] 119 | elif root1 == '你': 120 | root1 = self.object_explainer(group, user, user)['name'] 121 | if root2 == '我': 122 | root2 = self.object_explainer(group, user, str(BOT))['name'] 123 | elif root2 == '你': 124 | root2 = self.object_explainer(group, user, user)['name'] 125 | if obj1 == '你': 126 | obj1 = '我' 127 | elif obj1 == '我': 128 | obj1 = '你' 129 | if obj2 == '你': 130 | obj2 = '我' 131 | elif obj2 == '我': 132 | obj2 = '你' 133 | if root1 and root2: 134 | if root1 == root2: 135 | return f'{obj1}是{root1},{obj2}也是{root2},所以{obj1}是{obj2}' 136 | else: 137 | return f'{obj1}是{root1},{obj2}是{root2},所以{obj1}不是{obj2}' 138 | else: 139 | return random.choice([ 140 | IDK, 141 | '难说,毕竟兵不厌诈', 142 | '不好说,我只能说懂的都懂', 143 | '不知道,毕竟我只是一只小猫咪', 144 | 'それはどうかな…', 145 | ]) 146 | 147 | 148 | def add_alias(self, group, user, subject, object): 149 | if not object: 150 | return '是啥?' 151 | self._update() 152 | data = loadjson(MEMBER) 153 | sbj = self.object_explainer(group, user, subject) 154 | ''' 如果默认名字存在,则obj是object对应的默认名字 ''' 155 | ''' 如果默认名字不存在,则obj是object ''' 156 | obj = self.object_explainer(group, user, object)['name'] or object 157 | ''' 获取obj的owner ''' 158 | owner = self.get_uid(group, obj) 159 | ''' obj已被占用,即object存在对应的默认名字,现在obj是object对应的默认名字 ''' 160 | if owner != UNKNOWN: 161 | ''' 尝试将object转换成已记录的大小写形式 ''' 162 | # 遍历群友名单中owner的所有名字 163 | for n in data[group][owner]: 164 | # 除了大小写可能不同以外完全一致 165 | if n.lower() == object.lower(): 166 | obj = n 167 | break 168 | # owner是sbj本身 169 | if owner == sbj['uid']: 170 | return f'{sbj["name"]}已经是{obj}了' 171 | # owner是其他人 172 | return f'唔得,{data[group][owner][0]}已经是{obj}了' 173 | ''' obj未被占用,即object不存在对应的默认名字,现在obj是object ''' 174 | if obj in taowa or True in [i in obj for i in taowa]: 175 | return '不准套娃' 176 | if qunyou in obj: 177 | return '唔得,大家都是群友' 178 | ''' sbj准备 ''' 179 | if group not in data: 180 | data[group] = {} 181 | if sbj['uid'] not in data[group]: 182 | if sbj['uid'] == UNKNOWN: 183 | return f'我们群里有{subject}吗?' 184 | data[group][sbj['uid']] = [] 185 | data[group][sbj['uid']].append(obj) 186 | with open(MEMBER, 'w', encoding='utf8') as f: 187 | json.dump(data, f, ensure_ascii=False, indent=4) 188 | self._update() 189 | if not sbj["name"]: 190 | sbj["name"] = subject 191 | return f'好,现在{sbj["name"]}是{object}了' 192 | 193 | 194 | def set_default_alias(self, group, user, name): 195 | self._update() 196 | if self.add_alias(group, user, '我', name) == '不准套娃': 197 | return '不准套娃' 198 | data = loadjson(MEMBER) 199 | data[group][user].remove(name) 200 | data[group][user] = [name,] + data[group][user] 201 | with open(MEMBER, 'w', encoding='utf8') as f: 202 | json.dump(data, f, ensure_ascii=False, indent=4) 203 | self._update() 204 | return f'好的,{name}' 205 | 206 | 207 | def del_alias(self, group, user, name): 208 | self._update() 209 | data = loadjson(MEMBER) 210 | if group not in data: 211 | return None 212 | elif user not in data[group]: 213 | return '你谁啊?' 214 | elif name in data[group][user]: 215 | reply = f'好,你不再是{name}了' 216 | data[group][user].remove(name) 217 | if len(data[group][user]) == 0: 218 | data[group].pop(user) 219 | reply += '\n现在群友名单里没有你了' 220 | with open(MEMBER, 'w', encoding='utf8') as f: 221 | json.dump(data, f, ensure_ascii=False, indent=4) 222 | self._update() 223 | return reply 224 | else: 225 | return f'你本来就不是{name}' 226 | 227 | 228 | def del_all_alias(self, group, user): 229 | self._update() 230 | data = loadjson(MEMBER) 231 | if group not in data: 232 | return None 233 | elif user not in data[group]: 234 | return '你谁啊?' 235 | else: 236 | if len(data[group][user]) == 1: 237 | return f'唔得,你只剩下{data[group][user][0]}了' 238 | to_del = data[group][user][1:] 239 | data[group][user] = data[group][user][0:1] 240 | with open(MEMBER, 'w', encoding='utf8') as f: 241 | json.dump(data, f, ensure_ascii=False, indent=4) 242 | self._update() 243 | return f'好,你不再是{",".join(to_del)}了' 244 | 245 | 246 | def get_uid(self, group, obj): 247 | self._update() 248 | ''' 如果obj是at,返回obj中的uid ''' 249 | try: 250 | uid = re.match('\[CQ:at,qq=(\d+)\]', obj.strip())[1] 251 | return uid 252 | except: 253 | pass 254 | ''' 如果obj本身就是uid,返回obj ''' 255 | data = loadjson(MEMBER) 256 | if group in data and obj in data[group]: 257 | return obj 258 | ''' 如果obj不是at也不是uid,从群友名单中找obj对应的uid ''' 259 | if group in self._rosters: 260 | if obj.lower() in self._rosters[group]: 261 | return self._rosters[group][obj.lower()] 262 | ''' 找不到,返回UNKNOWN ''' 263 | return UNKNOWN 264 | 265 | 266 | def object_explainer(self, group, user, obj) -> dict: 267 | ''' 268 | 输入为用户视角:“我”是用户,“你”是BOT 269 | 输出为BOT视角 :“我”是BOT,“你”是用户 270 | ''' 271 | self._update() 272 | obj = obj.strip() 273 | data = loadjson(MEMBER) 274 | if obj == '我': 275 | uid = user 276 | name = '你' 277 | elif obj == '你': 278 | uid = str(BOT) 279 | name = '我' 280 | else: 281 | # obj对应的uid和默认名字 282 | # 若不存在则为UNKNOWN 283 | uid = self.get_uid(group, obj) 284 | try: 285 | name = data[group][uid][0] 286 | except: 287 | name = UNKNOWN 288 | return {'uid': uid, 'name': name} 289 | 290 | 291 | def get_group_member(self, group): 292 | data = loadjson(MEMBER) 293 | if group in data and len(data[group]): 294 | return '本群群友有{}'.format(",".join([data[group][p][0] for p in data[group].keys()])) 295 | else: 296 | return "没有查询到本群群友" 297 | 298 | 299 | def _update(self): 300 | data = loadjson(MEMBER) 301 | self._rosters = {} 302 | for group in data: 303 | self._rosters[group] = pygtrie.CharTrie() 304 | for user in data[group]: 305 | for name in data[group][user]: 306 | if name.lower() not in self._rosters[group]: 307 | self._rosters[group][name.lower()] = user -------------------------------------------------------------------------------- /dota2_dicts.py: -------------------------------------------------------------------------------- 1 | HEROES = { 2 | 1: 'antimage', 3 | 2: 'axe', 4 | 3: 'bane', 5 | 4: 'bloodseeker', 6 | 5: 'crystal_maiden', 7 | 6: 'drow_ranger', 8 | 7: 'earthshaker', 9 | 8: 'juggernaut', 10 | 9: 'mirana', 11 | 10: 'morphling', 12 | 11: 'nevermore', 13 | 12: 'phantom_lancer', 14 | 13: 'puck', 15 | 14: 'pudge', 16 | 15: 'razor', 17 | 16: 'sand_king', 18 | 17: 'storm_spirit', 19 | 18: 'sven', 20 | 19: 'tiny', 21 | 20: 'vengefulspirit', 22 | 21: 'windrunner', 23 | 22: 'zuus', 24 | 23: 'kunkka', 25 | 25: 'lina', 26 | 26: 'lion', 27 | 27: 'shadow_shaman', 28 | 28: 'slardar', 29 | 29: 'tidehunter', 30 | 30: 'witch_doctor', 31 | 31: 'lich', 32 | 32: 'riki', 33 | 33: 'enigma', 34 | 34: 'tinker', 35 | 35: 'sniper', 36 | 36: 'necrolyte', 37 | 37: 'warlock', 38 | 38: 'beastmaster', 39 | 39: 'queenofpain', 40 | 40: 'venomancer', 41 | 41: 'faceless_void', 42 | 42: 'skeleton_king', 43 | 43: 'death_prophet', 44 | 44: 'phantom_assassin', 45 | 45: 'pugna', 46 | 46: 'templar_assassin', 47 | 47: 'viper', 48 | 48: 'luna', 49 | 49: 'dragon_knight', 50 | 50: 'dazzle', 51 | 51: 'rattletrap', 52 | 52: 'leshrac', 53 | 53: 'furion', 54 | 54: 'life_stealer', 55 | 55: 'dark_seer', 56 | 56: 'clinkz', 57 | 57: 'omniknight', 58 | 58: 'enchantress', 59 | 59: 'huskar', 60 | 60: 'night_stalker', 61 | 61: 'broodmother', 62 | 62: 'bounty_hunter', 63 | 63: 'weaver', 64 | 64: 'jakiro', 65 | 65: 'batrider', 66 | 66: 'chen', 67 | 67: 'spectre', 68 | 68: 'ancient_apparition', 69 | 69: 'doom_bringer', 70 | 70: 'ursa', 71 | 71: 'spirit_breaker', 72 | 72: 'gyrocopter', 73 | 73: 'alchemist', 74 | 74: 'invoker', 75 | 75: 'silencer', 76 | 76: 'obsidian_destroyer', 77 | 77: 'lycan', 78 | 78: 'brewmaster', 79 | 79: 'shadow_demon', 80 | 80: 'lone_druid', 81 | 81: 'chaos_knight', 82 | 82: 'meepo', 83 | 83: 'treant', 84 | 84: 'ogre_magi', 85 | 85: 'undying', 86 | 86: 'rubick', 87 | 87: 'disruptor', 88 | 88: 'nyx_assassin', 89 | 89: 'naga_siren', 90 | 90: 'keeper_of_the_light', 91 | 91: 'wisp', 92 | 92: 'visage', 93 | 93: 'slark', 94 | 94: 'medusa', 95 | 95: 'troll_warlord', 96 | 96: 'centaur', 97 | 97: 'magnataur', 98 | 98: 'shredder', 99 | 99: 'bristleback', 100 | 100: 'tusk', 101 | 101: 'skywrath_mage', 102 | 102: 'abaddon', 103 | 103: 'elder_titan', 104 | 104: 'legion_commander', 105 | 105: 'techies', 106 | 106: 'ember_spirit', 107 | 107: 'earth_spirit', 108 | 108: 'abyssal_underlord', 109 | 109: 'terrorblade', 110 | 110: 'phoenix', 111 | 111: 'oracle', 112 | 112: 'winter_wyvern', 113 | 113: 'arc_warden', 114 | 114: 'monkey_king', 115 | 119: 'dark_willow', 116 | 120: 'pangolier', 117 | 121: 'grimstroke', 118 | 123: 'hoodwink', 119 | 126: 'void_spirit', 120 | 128: 'snapfire', 121 | 129: 'mars', 122 | 135: 'dawnbreaker', 123 | 136: 'marci', 124 | 137: 'primal_beast', 125 | 138: 'muerta', 126 | } 127 | 128 | # 每个英雄的第一个为游戏内默认名字 129 | HEROES_CHINESE = { 130 | 1: ['敌法师', '敌法', 'AM'], 131 | 2: ['斧王'], 132 | 3: ['祸乱之源', '祸乱', '水桶腰'], 133 | 4: ['血魔'], 134 | 5: ['水晶室女', '冰女', 'CM'], 135 | 6: ['卓尔游侠', '小黑'], 136 | 7: ['撼地者', '小牛'], 137 | 8: ['主宰', '剑圣', 'jugg', '奶棒人'], 138 | 9: ['米拉娜', '白虎', 'pom'], 139 | 10: ['变体精灵', '水人'], 140 | 11: ['影魔', '影魔王', 'SF', '影儿魔儿'], 141 | 12: ['幻影长矛手', 'PL'], 142 | 13: ['帕克'], 143 | 14: ['帕吉', '屠夫', '扒鸡', '啪唧'], 144 | 15: ['剃刀', '电魂', '电棍'], 145 | 16: ['沙王', 'SK'], 146 | 17: ['风暴之灵', '蓝猫'], 147 | 18: ['斯温', '流浪剑客', '流浪'], 148 | 19: ['小小'], 149 | 20: ['复仇之魂', '复仇', 'VS'], 150 | 21: ['风行者', '风行', 'WR'], 151 | 22: ['宙斯'], 152 | 23: ['昆卡', '船长'], 153 | 25: ['莉娜', '火女'], 154 | 26: ['莱恩', '恶魔巫师', 'Lion'], 155 | 27: ['暗影萨满', '小Y', '小歪'], 156 | 28: ['斯拉达', '大鱼', '大鱼人'], 157 | 29: ['潮汐猎人', '潮汐', '西瓜皮'], 158 | 30: ['巫医'], 159 | 31: ['巫妖'], 160 | 32: ['力丸', '隐形刺客', '隐刺'], 161 | 33: ['谜团'], 162 | 34: ['修补匠', 'TK', 'Tinker'], 163 | 35: ['狙击手', '矮人火枪手', '火枪', '传说哥'], 164 | 36: ['瘟疫法师', '死灵法', 'NEC'], 165 | 37: ['术士'], 166 | 38: ['兽王'], 167 | 39: ['痛苦女王', '女王', 'QOP'], 168 | 40: ['剧毒术士', '剧毒'], 169 | 41: ['虚空假面', '虚空', 'JB脸'], 170 | 42: ['冥魂大帝', '骷髅王'], 171 | 43: ['死亡先知', 'DP'], 172 | 44: ['幻影刺客', '幻刺', 'PA'], 173 | 45: ['帕格纳', '骨法', '湮灭法师'], 174 | 46: ['圣堂刺客', '圣堂', 'TA'], 175 | 47: ['冥界亚龙', '毒龙', 'Viper'], 176 | 48: ['露娜', '月骑', 'Luna'], 177 | 49: ['龙骑士', '龙骑'], 178 | 50: ['戴泽', '暗影牧师', '暗牧'], 179 | 51: ['发条技师', '发条'], 180 | 52: ['拉席克', '老鹿'], 181 | 53: ['先知'], 182 | 54: ['噬魂鬼', '小狗'], 183 | 55: ['黑暗贤者', '黑贤'], 184 | 56: ['克林克兹', '小骷髅'], 185 | 57: ['全能骑士', '全能'], 186 | 58: ['魅惑魔女', '小鹿'], 187 | 59: ['哈斯卡', '神灵', '神灵武士'], 188 | 60: ['暗夜魔王', '夜魔'], 189 | 61: ['育母蜘蛛', '蜘蛛'], 190 | 62: ['赏金猎人', '赏金'], 191 | 63: ['编织者', '蚂蚁'], 192 | 64: ['杰奇洛', '双头龙'], 193 | 65: ['蝙蝠骑士', '蝙蝠'], 194 | 66: ['陈', '老陈'], 195 | 67: ['幽鬼', 'SPE', 'UG'], 196 | 68: ['远古冰魄', '冰魂'], 197 | 69: ['末日使者', '末日', 'Doom'], 198 | 70: ['熊战士', '拍拍', '拍拍熊'], 199 | 71: ['裂魂人', '白牛'], 200 | 72: ['矮人直升机', '飞机'], 201 | 73: ['炼金术士', '炼金'], 202 | 74: ['祈求者', '卡尔'], 203 | 75: ['沉默术士', '沉默'], 204 | 76: ['殁境神蚀者', '黑鸟'], 205 | 77: ['狼人'], 206 | 78: ['酒仙', '熊猫', '熊猫酒仙'], 207 | 79: ['暗影恶魔', '毒狗'], 208 | 80: ['德鲁伊', '熊德'], 209 | 81: ['混沌骑士', '混沌', 'CK'], 210 | 82: ['米波'], 211 | 83: ['树精卫士', '大树', '树精'], 212 | 84: ['食人魔魔法师', '蓝胖'], 213 | 85: ['不朽尸王', '尸王'], 214 | 86: ['拉比克'], 215 | 87: ['干扰者', '萨尔'], 216 | 88: ['司夜刺客', '小强'], 217 | 89: ['娜迦海妖', '小娜迦'], 218 | 90: ['光之守卫', '光法'], 219 | 91: ['艾欧', '小精灵'], 220 | 92: ['维萨吉', '死灵龙', '死灵飞龙'], 221 | 93: ['斯拉克', '小鱼', '小鱼人'], 222 | 94: ['美杜莎', '一姐', '美杜莎'], 223 | 95: ['巨魔战将', '巨魔', '巨馍蘸酱'], 224 | 96: ['半人马战行者', '人马', '半人马'], 225 | 97: ['马格纳斯', '猛犸'], 226 | 98: ['伐木机'], 227 | 99: ['钢背兽', '钢背'], 228 | 100: ['巨牙海民', '海民'], 229 | 101: ['天怒法师', '天怒'], 230 | 102: ['亚巴顿'], 231 | 103: ['上古巨神', '大牛'], 232 | 104: ['军团指挥官', '军团'], 233 | 105: ['工程师', '炸弹', '炸弹人'], 234 | 106: ['灰烬之灵', '火猫'], 235 | 107: ['大地之灵', '土猫'], 236 | 108: ['孽主', '大屁股'], 237 | 109: ['恐怖利刃', 'TB'], 238 | 110: ['凤凰'], 239 | 111: ['神谕者', '神谕'], 240 | 112: ['寒冬飞龙', '冰龙'], 241 | 113: ['天穹守望者', '电狗'], 242 | 114: ['齐天大圣', '大圣'], 243 | 119: ['邪影芳灵', '小仙女'], 244 | 120: ['石鳞剑士', '滚滚'], 245 | 121: ['天涯墨客', '墨客'], 246 | 123: ['森海飞霞', '松鼠', '小松鼠', '小松许'], 247 | 126: ['虚无之灵', '紫猫'], 248 | 128: ['电炎绝手', '老奶奶'], 249 | 129: ['玛尔斯'], 250 | 135: ['破晓辰星'], 251 | 136: ['玛西'], 252 | 137: ['獸'], 253 | 138: ['琼英碧灵'], 254 | } 255 | 256 | ITEMS = { 257 | 1: 'blink', 258 | 2: 'blades_of_attack', 259 | 3: 'broadsword', 260 | 4: 'chainmail', 261 | 5: 'claymore', 262 | 6: 'helm_of_iron_will', 263 | 7: 'javelin', 264 | 8: 'mithril_hammer', 265 | 9: 'platemail', 266 | 10: 'quarterstaff', 267 | 11: 'quelling_blade', 268 | 12: 'ring_of_protection', 269 | 13: 'gauntlets', 270 | 14: 'slippers', 271 | 15: 'mantle', 272 | 16: 'branches', 273 | 17: 'belt_of_strength', 274 | 18: 'boots_of_elves', 275 | 19: 'robe', 276 | 20: 'circlet', 277 | 21: 'ogre_axe', 278 | 22: 'blade_of_alacrity', 279 | 23: 'staff_of_wizardry', 280 | 24: 'ultimate_orb', 281 | 25: 'gloves', 282 | 26: 'lifesteal', 283 | 27: 'ring_of_regen', 284 | 28: 'sobi_mask', 285 | 29: 'boots', 286 | 30: 'gem', 287 | 31: 'cloak', 288 | 32: 'talisman_of_evasion', 289 | 33: 'cheese', 290 | 34: 'magic_stick', 291 | 35: 'recipe_magic_wand', 292 | 36: 'magic_wand', 293 | 37: 'ghost', 294 | 38: 'clarity', 295 | 39: 'flask', 296 | 40: 'dust', 297 | 41: 'bottle', 298 | 42: 'ward_observer', 299 | 43: 'ward_sentry', 300 | 44: 'tango', 301 | 45: 'courier', 302 | 46: 'tpscroll', 303 | 47: 'recipe_travel_boots', 304 | 48: 'travel_boots', 305 | 49: 'recipe_phase_boots', 306 | 50: 'phase_boots', 307 | 51: 'demon_edge', 308 | 52: 'eagle', 309 | 53: 'reaver', 310 | 54: 'relic', 311 | 55: 'hyperstone', 312 | 56: 'ring_of_health', 313 | 57: 'void_stone', 314 | 58: 'mystic_staff', 315 | 59: 'energy_booster', 316 | 60: 'point_booster', 317 | 61: 'vitality_booster', 318 | 62: 'recipe_power_treads', 319 | 63: 'power_treads', 320 | 64: 'recipe_hand_of_midas', 321 | 65: 'hand_of_midas', 322 | 66: 'recipe_oblivion_staff', 323 | 67: 'oblivion_staff', 324 | 68: 'recipe_pers', 325 | 69: 'pers', 326 | 70: 'recipe_poor_mans_shield', 327 | 71: 'poor_mans_shield', 328 | 72: 'recipe_bracer', 329 | 73: 'bracer', 330 | 74: 'recipe_wraith_band', 331 | 75: 'wraith_band', 332 | 76: 'recipe_null_talisman', 333 | 77: 'null_talisman', 334 | 78: 'recipe_mekansm', 335 | 79: 'mekansm', 336 | 80: 'recipe_vladmir', 337 | 81: 'vladmir', 338 | 85: 'recipe_buckler', 339 | 86: 'buckler', 340 | 87: 'recipe_ring_of_basilius', 341 | 88: 'ring_of_basilius', 342 | 89: 'recipe_pipe', 343 | 90: 'pipe', 344 | 91: 'recipe_urn_of_shadows', 345 | 92: 'urn_of_shadows', 346 | 93: 'recipe_headdress', 347 | 94: 'headdress', 348 | 95: 'recipe_sheepstick', 349 | 96: 'sheepstick', 350 | 97: 'recipe_orchid', 351 | 98: 'orchid', 352 | 99: 'recipe_cyclone', 353 | 100: 'cyclone', 354 | 101: 'recipe_force_staff', 355 | 102: 'force_staff', 356 | 103: 'recipe_dagon', 357 | 104: 'dagon', 358 | 105: 'recipe_necronomicon', 359 | 106: 'necronomicon', 360 | 107: 'recipe_ultimate_scepter', 361 | 108: 'ultimate_scepter', 362 | 109: 'recipe_refresher', 363 | 110: 'refresher', 364 | 111: 'recipe_assault', 365 | 112: 'assault', 366 | 113: 'recipe_heart', 367 | 114: 'heart', 368 | 115: 'recipe_black_king_bar', 369 | 116: 'black_king_bar', 370 | 117: 'aegis', 371 | 118: 'recipe_shivas_guard', 372 | 119: 'shivas_guard', 373 | 120: 'recipe_bloodstone', 374 | 121: 'bloodstone', 375 | 122: 'recipe_sphere', 376 | 123: 'sphere', 377 | 124: 'recipe_vanguard', 378 | 125: 'vanguard', 379 | 126: 'recipe_blade_mail', 380 | 127: 'blade_mail', 381 | 128: 'recipe_soul_booster', 382 | 129: 'soul_booster', 383 | 130: 'recipe_hood_of_defiance', 384 | 131: 'hood_of_defiance', 385 | 132: 'recipe_rapier', 386 | 133: 'rapier', 387 | 134: 'recipe_monkey_king_bar', 388 | 135: 'monkey_king_bar', 389 | 136: 'recipe_radiance', 390 | 137: 'radiance', 391 | 138: 'recipe_butterfly', 392 | 139: 'butterfly', 393 | 140: 'recipe_greater_crit', 394 | 141: 'greater_crit', 395 | 142: 'recipe_basher', 396 | 143: 'basher', 397 | 144: 'recipe_bfury', 398 | 145: 'bfury', 399 | 146: 'recipe_manta', 400 | 147: 'manta', 401 | 148: 'recipe_lesser_crit', 402 | 149: 'lesser_crit', 403 | 150: 'recipe_armlet', 404 | 151: 'armlet', 405 | 152: 'invis_sword', 406 | 153: 'recipe_sange_and_yasha', 407 | 154: 'sange_and_yasha', 408 | 155: 'recipe_satanic', 409 | 156: 'satanic', 410 | 157: 'recipe_mjollnir', 411 | 158: 'mjollnir', 412 | 159: 'recipe_skadi', 413 | 160: 'skadi', 414 | 161: 'recipe_sange', 415 | 162: 'sange', 416 | 163: 'recipe_helm_of_the_dominator', 417 | 164: 'helm_of_the_dominator', 418 | 165: 'recipe_maelstrom', 419 | 166: 'maelstrom', 420 | 167: 'recipe_desolator', 421 | 168: 'desolator', 422 | 169: 'recipe_yasha', 423 | 170: 'yasha', 424 | 171: 'recipe_mask_of_madness', 425 | 172: 'mask_of_madness', 426 | 173: 'recipe_diffusal_blade', 427 | 174: 'diffusal_blade', 428 | 175: 'recipe_ethereal_blade', 429 | 176: 'ethereal_blade', 430 | 177: 'recipe_soul_ring', 431 | 178: 'soul_ring', 432 | 179: 'recipe_arcane_boots', 433 | 180: 'arcane_boots', 434 | 181: 'orb_of_venom', 435 | 182: 'stout_shield', 436 | 183: 'recipe_invis_sword', 437 | 184: 'recipe_ancient_janggo', 438 | 185: 'ancient_janggo', 439 | 186: 'recipe_medallion_of_courage', 440 | 187: 'medallion_of_courage', 441 | 188: 'smoke_of_deceit', 442 | 189: 'recipe_veil_of_discord', 443 | 190: 'veil_of_discord', 444 | 191: 'recipe_necronomicon_2', 445 | 192: 'recipe_necronomicon_3', 446 | 193: 'necronomicon_2', 447 | 194: 'necronomicon_3', 448 | 196: 'diffusal_blade_2', 449 | 197: 'recipe_dagon_2', 450 | 198: 'recipe_dagon_3', 451 | 199: 'recipe_dagon_4', 452 | 200: 'recipe_dagon_5', 453 | 201: 'dagon_2', 454 | 202: 'dagon_3', 455 | 203: 'dagon_4', 456 | 204: 'dagon_5', 457 | 205: 'recipe_rod_of_atos', 458 | 206: 'rod_of_atos', 459 | 207: 'recipe_abyssal_blade', 460 | 208: 'abyssal_blade', 461 | 209: 'recipe_heavens_halberd', 462 | 210: 'heavens_halberd', 463 | 211: 'recipe_ring_of_aquila', 464 | 212: 'ring_of_aquila', 465 | 213: 'recipe_tranquil_boots', 466 | 214: 'tranquil_boots', 467 | 215: 'shadow_amulet', 468 | 216: 'enchanted_mango', 469 | 217: 'recipe_ward_dispenser', 470 | 218: 'ward_dispenser', 471 | 219: 'recipe_travel_boots_2', 472 | 220: 'travel_boots_2', 473 | 221: 'recipe_lotus_orb', 474 | 222: 'recipe_meteor_hammer', 475 | 223: 'meteor_hammer', 476 | 224: 'recipe_nullifier', 477 | 225: 'nullifier', 478 | 226: 'lotus_orb', 479 | 227: 'recipe_solar_crest', 480 | 228: 'recipe_octarine_core', 481 | 229: 'solar_crest', 482 | 230: 'recipe_guardian_greaves', 483 | 231: 'guardian_greaves', 484 | 232: 'aether_lens', 485 | 233: 'recipe_aether_lens', 486 | 234: 'recipe_dragon_lance', 487 | 235: 'octarine_core', 488 | 236: 'dragon_lance', 489 | 237: 'faerie_fire', 490 | 238: 'recipe_iron_talon', 491 | 239: 'iron_talon', 492 | 240: 'blight_stone', 493 | 241: 'tango_single', 494 | 242: 'crimson_guard', 495 | 243: 'recipe_crimson_guard', 496 | 244: 'wind_lace', 497 | 245: 'recipe_bloodthorn', 498 | 246: 'recipe_moon_shard', 499 | 247: 'moon_shard', 500 | 248: 'recipe_silver_edge', 501 | 249: 'silver_edge', 502 | 250: 'bloodthorn', 503 | 251: 'recipe_echo_sabre', 504 | 252: 'echo_sabre', 505 | 253: 'recipe_glimmer_cape', 506 | 254: 'glimmer_cape', 507 | 255: 'recipe_aeon_disk', 508 | 256: 'aeon_disk', 509 | 257: 'tome_of_knowledge', 510 | 258: 'recipe_kaya', 511 | 259: 'kaya', 512 | 260: 'refresher_shard', 513 | 261: 'crown', 514 | 262: 'recipe_hurricane_pike', 515 | 263: 'hurricane_pike', 516 | 265: 'infused_raindrop', 517 | 266: 'recipe_spirit_vessel', 518 | 267: 'spirit_vessel', 519 | 268: 'recipe_holy_locket', 520 | 269: 'holy_locket', 521 | 270: 'recipe_ultimate_scepter_2', 522 | 271: 'ultimate_scepter_2', 523 | 272: 'recipe_kaya_and_sange', 524 | 273: 'kaya_and_sange', 525 | 274: 'recipe_yasha_and_kaya', 526 | 275: 'recipe_trident', 527 | 276: 'combo_breaker', 528 | 277: 'yasha_and_kaya', 529 | 279: 'ring_of_tarrasque', 530 | 286: 'flying_courier', 531 | 287: 'keen_optic', 532 | 288: 'grove_bow', 533 | 289: 'quickening_charm', 534 | 290: 'philosophers_stone', 535 | 291: 'force_boots', 536 | 292: 'desolator_2', 537 | 293: 'phoenix_ash', 538 | 294: 'seer_stone', 539 | 295: 'greater_mango', 540 | 297: 'vampire_fangs', 541 | 298: 'craggy_coat', 542 | 299: 'greater_faerie_fire', 543 | 300: 'timeless_relic', 544 | 301: 'mirror_shield', 545 | 302: 'elixer', 546 | 303: 'recipe_ironwood_tree', 547 | 304: 'ironwood_tree', 548 | 305: 'royal_jelly', 549 | 306: 'pupils_gift', 550 | 307: 'tome_of_aghanim', 551 | 308: 'repair_kit', 552 | 309: 'mind_breaker', 553 | 310: 'third_eye', 554 | 311: 'spell_prism', 555 | 312: 'horizon', 556 | 313: 'fusion_rune', 557 | 317: 'recipe_fallen_sky', 558 | 325: 'princes_knife', 559 | 326: 'spider_legs', 560 | 327: 'helm_of_the_undying', 561 | 328: 'mango_tree', 562 | 329: 'recipe_vambrace', 563 | 330: 'witless_shako', 564 | 331: 'vambrace', 565 | 334: 'imp_claw', 566 | 335: 'flicker', 567 | 336: 'spy_gadget', 568 | 349: 'arcane_ring', 569 | 354: 'ocean_heart', 570 | 355: 'broom_handle', 571 | 356: 'trusty_shovel', 572 | 357: 'nether_shawl', 573 | 358: 'dragon_scale', 574 | 359: 'essence_ring', 575 | 360: 'clumsy_net', 576 | 361: 'enchanted_quiver', 577 | 362: 'ninja_gear', 578 | 363: 'illusionsts_cape', 579 | 364: 'havoc_hammer', 580 | 365: 'panic_button', 581 | 366: 'apex', 582 | 367: 'ballista', 583 | 368: 'woodland_striders', 584 | 369: 'trident', 585 | 370: 'demonicon', 586 | 371: 'fallen_sky', 587 | 372: 'pirate_hat', 588 | 373: 'dimensional_doorway', 589 | 374: 'ex_machina', 590 | 375: 'faded_broach', 591 | 376: 'paladin_sword', 592 | 377: 'minotaur_horn', 593 | 378: 'orb_of_destruction', 594 | 379: 'the_leveller', 595 | 381: 'titan_sliver', 596 | 473: 'voodoo_mask', 597 | 485: 'blitz_knuckles', 598 | 533: 'recipe_witch_blade', 599 | 534: 'witch_blade', 600 | 565: 'chipped_vest', 601 | 566: 'wizard_glass', 602 | 569: 'orb_of_corrosion', 603 | 570: 'gloves_of_travel', 604 | 571: 'trickster_cloak', 605 | 573: 'elven_tunic', 606 | 574: 'cloak_of_flames', 607 | 575: 'venom_gland', 608 | 576: 'gladiator_helm', 609 | 577: 'possessed_mask', 610 | 578: 'ancient_perseverance', 611 | 582: 'oakheart', 612 | 585: 'stormcrafter', 613 | 588: 'overflowing_elixir', 614 | 589: 'mysterious_hat', 615 | 593: 'fluffy_hat', 616 | 596: 'falcon_blade', 617 | 597: 'recipe_mage_slayer', 618 | 598: 'mage_slayer', 619 | 599: 'recipe_falcon_blade', 620 | 600: 'overwhelming_blink', 621 | 603: 'swift_blink', 622 | 604: 'arcane_blink', 623 | 606: 'recipe_arcane_blink', 624 | 607: 'recipe_swift_blink', 625 | 608: 'recipe_overwhelming_blink', 626 | 609: 'aghanims_shard', 627 | 610: 'wind_waker', 628 | 612: 'recipe_wind_waker', 629 | 633: 'recipe_helm_of_the_overlord', 630 | 635: 'helm_of_the_overlord', 631 | 637: 'star_mace', 632 | 638: 'penta_edged_sword', 633 | 640: 'recipe_orb_of_corrosion', 634 | 653: 'recipe_grandmasters_glaive', 635 | 655: 'grandmasters_glaive', 636 | 674: 'warhammer', 637 | 675: 'psychic_headband', 638 | 676: 'ceremonial_robe', 639 | 677: 'book_of_shadows', 640 | 678: 'giants_ring', 641 | 679: 'vengeances_shadow', 642 | 680: 'bullwhip', 643 | 686: 'quicksilver_amulet', 644 | 691: 'recipe_eternal_shroud', 645 | 692: 'eternal_shroud', 646 | 725: 'aghanims_shard_roshan', 647 | 727: 'ultimate_scepter_roshan', 648 | 731: 'satchel', 649 | 824: 'assassins_dagger', 650 | 825: 'ascetic_cap', 651 | 826: 'sample_picker', 652 | 827: 'icarus_wings', 653 | 839: 'fortitude_ring', 654 | 828: 'misericorde', 655 | 829: 'force_field', 656 | 834: 'black_powder_bag', 657 | 835: 'paintball', 658 | 836: 'light_robes', 659 | 837: 'witchbane', 660 | 838: 'unstable_wand', 661 | 840: 'pogo_stick', 662 | 849: 'mechanical_arm', 663 | 907: 'recipe_wraith_pact', 664 | 908: 'wraith_pact', 665 | 910: 'recipe_revenants_brooch', 666 | 911: 'revenants_brooch', 667 | 931: 'boots_of_bearing', 668 | 930: 'recipe_boots_of_bearing', 669 | 938: 'slime_vial', 670 | 939: 'harpoon', 671 | 940: 'wand_of_the_brine', 672 | 945: 'seeds_of_serenity', 673 | 946: 'lance_of_pursuit', 674 | 947: 'occult_bracelet', 675 | 948: 'tome_of_omniscience', 676 | 949: 'ogre_seal_totem', 677 | 950: 'defiant_shell', 678 | 968: 'arcane_scout', 679 | 969: 'barricade', 680 | 990: 'eye_of_the_vizier', 681 | 998: 'manacles_of_power', 682 | 1000: 'bottomless_chalice', 683 | 1017: 'wand_of_sanctitude', 684 | 1021: 'river_painter', 685 | 1022: 'river_painter2', 686 | 1023: 'river_painter3', 687 | 1024: 'river_painter4', 688 | 1025: 'river_painter5', 689 | 1026: 'river_painter6', 690 | 1027: 'river_painter7', 691 | 1028: 'mutation_tombstone', 692 | 1029: 'super_blink', 693 | 1030: 'pocket_tower', 694 | 1032: 'pocket_roshan', 695 | 1076: 'specialists_array', 696 | 1077: 'dagger_of_ristul', 697 | 1090: 'muertas_gun', 698 | 1091: 'samurai_tabi', 699 | 1092: 'recipe_hermes_sandals', 700 | 1093: 'hermes_sandals', 701 | 1094: 'recipe_lunar_crest', 702 | 1095: 'lunar_crest', 703 | 1096: 'recipe_disperser', 704 | 1097: 'disperser', 705 | 1098: 'recipe_samurai_tabi', 706 | 1099: 'recipe_witches_switch', 707 | 1100: 'witches_switch', 708 | 1101: 'recipe_harpoon', 709 | 1106: 'recipe_phylactery', 710 | 1107: 'phylactery', 711 | 1122: 'diadem', 712 | 1123: 'blood_grenade', 713 | 1124: 'spark_of_courage', 714 | 1125: 'cornucopia', 715 | 1127: 'recipe_pavise', 716 | 1128: 'pavise', 717 | 1154: 'royale_with_cheese', 718 | 1466: 'gungir', 719 | 1565: 'recipe_gungir', 720 | 2091: 'tier1_token', 721 | 2092: 'tier2_token', 722 | 2093: 'tier3_token', 723 | 2094: 'tier4_token', 724 | 2095: 'tier5_token', 725 | 2096: 'vindicators_axe', 726 | 2097: 'duelist_gloves', 727 | 2098: 'horizons_equilibrium', 728 | 2099: 'blighted_spirit', 729 | 2190: 'dandelion_amulet', 730 | 2191: 'turtle_shell', 731 | 2192: 'martyrs_plate', 732 | 2193: 'gossamer_cape', 733 | 4204: 'famango', 734 | 4205: 'great_famango', 735 | 4206: 'greater_famango', 736 | 4300: 'ofrenda', 737 | 4301: 'ofrenda_shovel', 738 | 4302: 'ofrenda_pledge', 739 | 9999: 'heavy_blade', 740 | } 741 | 742 | GAME_MODE = { 743 | 0: 'No Game Mode', 744 | 1: '全英雄选择', 745 | 2: '队长模式', 746 | 3: '随机征召', 747 | 4: '小黑屋', 748 | 5: '全部随机', 749 | 7: '万圣节活动', 750 | 8: '反队长模式', 751 | 9: '贪魔活动', 752 | 10: '教程', 753 | 11: '中路模式', 754 | 12: '生疏模式', 755 | 13: '新手模式', 756 | 14: 'Compendium Matchmaking', 757 | 15: '自定义游戏', 758 | 16: '队长征召', 759 | 17: '平衡征召', 760 | 18: '技能征召', 761 | 19: '活动模式', 762 | 20: '全英雄死亡随机', 763 | 21: '中路SOLO', 764 | 22: '全英雄选择', 765 | 23: '加速模式' 766 | } 767 | 768 | LOBBY = { 769 | -1: '非法ID', 770 | 0: '普通匹配', 771 | 1: '练习', 772 | 2: '锦标赛', 773 | 3: '教程', 774 | 4: '合作对抗电脑', 775 | 5: '组排模式', 776 | 6: '单排模式', 777 | 7: '天梯匹配', 778 | 8: '中路SOLO', 779 | 12: '天陨旦' 780 | } 781 | 782 | # 服务器ID列表 783 | AREA_CODE = { 784 | 111: '美国西部', 785 | 112: '美国西部', 786 | 114: '美国西部', 787 | 121: '美国东部', 788 | 122: '美国东部', 789 | 123: '美国东部', 790 | 124: '美国东部', 791 | 131: '欧洲西部', 792 | 132: '欧洲西部', 793 | 133: '欧洲西部', 794 | 134: '欧洲西部', 795 | 135: '欧洲西部', 796 | 136: '欧洲西部', 797 | 142: '南韩', 798 | 143: '南韩', 799 | 151: '东南亚', 800 | 152: '东南亚', 801 | 153: '东南亚', 802 | 161: '中国', 803 | 163: '中国', 804 | 171: '澳大利亚', 805 | 181: '俄罗斯', 806 | 182: '俄罗斯', 807 | 183: '俄罗斯', 808 | 184: '俄罗斯', 809 | 185: '俄罗斯', 810 | 186: '俄罗斯', 811 | 191: '欧洲东部', 812 | 192: '欧洲东部', 813 | 200: '南美洲', 814 | 202: '南美洲', 815 | 203: '南美洲', 816 | 204: '南美洲', 817 | 211: '非洲南部', 818 | 212: '非洲南部', 819 | 213: '非洲南部', 820 | 221: '中国', 821 | 222: '中国', 822 | 223: '中国', 823 | 224: '中国', 824 | 225: '中国', 825 | 231: '中国', 826 | 236: '中国', 827 | 242: '智利', 828 | 251: '秘鲁', 829 | 261: '印度' 830 | } 831 | 832 | REGION = { 833 | 'region_0': '自动', 834 | 'region_1': '美国西部', 835 | 'region_2': '美国东部', 836 | 'region_3': '卢森堡', 837 | 'region_5': '新加坡', 838 | 'region_6': '迪拜', 839 | 'region_7': '澳大利亚', 840 | 'region_8': '斯德哥尔摩', 841 | 'region_9': '奥地利', 842 | 'region_10': '巴西', 843 | 'region_11': '南非', 844 | 'region_12': '电信(上海)', 845 | 'region_13': '联通(一)', 846 | 'region_14': '智利', 847 | 'region_15': '秘鲁', 848 | 'region_16': '印度', 849 | 'region_17': '电信(广东)', 850 | 'region_18': '电信(浙江)', 851 | 'region_19': '日本', 852 | 'region_20': '电信(华中)', 853 | 'region_25': '联通(二)', 854 | } 855 | 856 | ITEM_SLOTS = ['item_0', 'item_1', 'item_2', 'item_3', 'item_4', 'item_5', 'item_neutral'] 857 | 858 | SLOT = ['Radiant', 'Dire'] 859 | SLOT_CHINESE = ['天辉', '夜魇'] 860 | 861 | PLAYER_RANK = { 862 | 0: '未知', 863 | 1: '先锋', 864 | 2: '卫士', 865 | 3: '中军', 866 | 4: '统帅', 867 | 5: '传奇', 868 | 6: '万古流芳', 869 | 7: '超凡入圣', 870 | 8: '冠绝一世', 871 | } 872 | 873 | SKILL_LEVEL = {1: 'Normal', 2: 'High', 3: 'Very High'} 874 | 875 | OTHER_IMAGES = [ 876 | 'hero_None', 877 | 'item_None', 878 | 'logo_dire', 879 | 'logo_radiant', 880 | 'rank_icon_0', 881 | 'rank_icon_1', 882 | 'rank_icon_2', 883 | 'rank_icon_3', 884 | 'rank_icon_4', 885 | 'rank_icon_5', 886 | 'rank_icon_6', 887 | 'rank_icon_7', 888 | 'rank_icon_8', 889 | 'rank_star_1', 890 | 'rank_star_2', 891 | 'rank_star_3', 892 | 'rank_star_4', 893 | 'rank_star_5', 894 | 'scepter_0', 895 | 'scepter_1', 896 | 'shard_0', 897 | 'shard_1', 898 | ] 899 | 900 | WIN_NEGATIVE = [ 901 | '{}侥幸赢得了比赛', 902 | '{}走狗屎运赢得了比赛', 903 | '{}躺赢了比赛', 904 | '{}打团都没来, 队友4V5赢得了比赛', 905 | ] 906 | 907 | WIN_POSITIVE = [ 908 | '{}带领团队走向了胜利', 909 | '{}暴打对面后赢得了胜利', 910 | '{} CARRY全场赢得了胜利', 911 | '{}把对面当猪宰了, 赢得了胜利', 912 | '{}又赢了, 这游戏就是这么枯燥, 且乏味', 913 | '{}直接进行一个比赛的赢', 914 | ] 915 | 916 | LOSE_NEGATIVE = [ 917 | '{}被人按在地上摩擦, 输掉了这场比赛', 918 | '{}悲惨地输掉了比赛', 919 | '{}头都被打歪了, 心态爆炸地输掉了比赛', 920 | '{}捕鱼被鱼吃了, 输掉了比赛', 921 | '{}打的是个几把', 922 | '{}直接进行一个比赛的输', 923 | ] 924 | 925 | LOSE_POSITIVE = [ 926 | '{}无力回天输掉了比赛', 927 | '{}尽力了, 但还是输了比赛', 928 | '{}背靠世界树, 虽败犹荣', 929 | '{}带不动队友, 输了比赛', 930 | '{}又输了, 很难受, 宁愿输的是我', 931 | ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /steam.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import os 4 | import random 5 | import re 6 | import requests 7 | import time 8 | from datetime import datetime, timedelta 9 | from apscheduler.triggers.cron import CronTrigger 10 | from PIL import Image, ImageDraw, ImageFont 11 | from urllib.parse import urljoin 12 | 13 | from . import whois 14 | from .dota2_dicts import * 15 | from .utils import * 16 | 17 | logger = get_logger('Steam_watcher') 18 | 19 | CONFIG = load_config() 20 | APIKEY = CONFIG['STEAM_APIKEY'] 21 | BOT = CONFIG['BOT'] 22 | ATBOT = f'[CQ:at,qq={BOT}]' 23 | IMAGE_MODE = CONFIG.get('IMAGE_MODE') 24 | UNKNOWN = None 25 | IDK = '我不知道' 26 | MAX_ATTEMPTS = 5 27 | 28 | MEMBER = os.path.expanduser('~/.Steam_watcher/member.json') 29 | STEAM = os.path.expanduser('~/.Steam_watcher/steam.json') 30 | IMAGES = os.path.expanduser('~/.Steam_watcher/images/') 31 | DOTA2_MATCHES = os.path.expanduser('~/.Steam_watcher/DOTA2_matches/') 32 | 33 | IMAGE_URL = 'https://cdn.cloudflare.steamstatic.com/apps/dota2/images/dota_react/{}/{}.png' 34 | OTHER_IMAGE_URL = 'https://raw.githubusercontent.com/SonodaHanami/Steam_watcher/web/images/{}.png' 35 | 36 | PLAYER_SUMMARY = 'http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key={}&steamids={}' 37 | LAST_MATCH = 'https://api.steampowered.com/IDOTA2Match_570/GetMatchHistory/v001/?key={}&account_id={}&matches_requested=1' 38 | MATCH_DETAILS = 'https://api.steampowered.com/IDOTA2Match_570/GetMatchDetails/V001/?key={}&match_id={}' 39 | OPENDOTA_REQUEST = 'https://api.opendota.com/api/request/{}' 40 | OPENDOTA_MATCHES = 'https://api.opendota.com/api/matches/{}' 41 | OPENDOTA_PLAYERS = 'https://api.opendota.com/api/players/{}' 42 | 43 | DEFAULT_DATA = { 44 | 'DOTA2_matches_pool': {}, 45 | 'players': {}, 46 | 'subscribe_groups': [], 47 | 'subscribers': {}, 48 | } 49 | 50 | class Steam: 51 | Passive = True 52 | Active = True 53 | Request = False 54 | 55 | def __init__(self, **kwargs): 56 | logger.info('初始化Steam 开始!') 57 | 58 | self.setting = kwargs['glo_setting'] 59 | self.api = kwargs['bot_api'] 60 | self.whois = kwargs.get('whois') or whois.Whois(**kwargs) 61 | self.MINUTE = (datetime.now() + timedelta(minutes=2)).minute 62 | self.nowork = 0 63 | self.nosleep = 0 64 | 65 | self.init_fonts() 66 | self.init_images() 67 | if not os.path.exists(STEAM): 68 | dumpjson(DEFAULT_DATA, STEAM) 69 | 70 | # 图片传输方式 71 | if IMAGE_MODE == 'YOBOT_OUTPUT': 72 | self.YOBOT_OUTPUT = os.path.join(self.setting['dirname'], 'output/DOTA2_matches/') 73 | self.IMAGE_URL = urljoin( 74 | self.setting['public_address'], 75 | '{}{}'.format(self.setting['public_basepath'], 'output/DOTA2_matches/{}.png') 76 | ) 77 | mkdir_if_not_exists(self.YOBOT_OUTPUT) 78 | self.setting.update({ 79 | 'image_mode': IMAGE_MODE, 80 | 'output_path': self.YOBOT_OUTPUT, 81 | 'image_url': self.IMAGE_URL, 82 | }) 83 | elif IMAGE_MODE == 'BASE64_IMAGE': 84 | self.setting.update({ 85 | 'image_mode': IMAGE_MODE, 86 | }) 87 | else: 88 | self.setting.update({ 89 | 'image_mode': 'ORIGINAL_PNG', 90 | }) 91 | logger.info('图片传输方式为{}'.format(self.setting['image_mode'])) 92 | 93 | self.dota2 = Dota2(**kwargs) 94 | 95 | logger.info('初始化Steam 完成!MINUTE={}'.format(self.MINUTE)) 96 | 97 | 98 | async def execute_async(self, func_num, message): 99 | msg = message['raw_message'].strip() 100 | group = str(message.get('group_id', '')) 101 | user = str(message.get('user_id', '')) 102 | if not group: 103 | return None 104 | 105 | whois_reply = await self.whois.execute_async(message) 106 | if whois_reply: 107 | return whois_reply 108 | 109 | if 'steam' in msg.lower() and ('help' in msg.lower() or '帮助' in msg or '说明书' in msg): 110 | return 'https://docs.qq.com/sheet/DWGFiTVpPS0lkZ2Vv' 111 | 112 | if msg.lower() == '订阅steam': 113 | steamdata = loadjson(STEAM) 114 | if group in steamdata['subscribe_groups']: 115 | return '本群已订阅Steam' 116 | else: 117 | steamdata['subscribe_groups'].append(group) 118 | dumpjson(steamdata, STEAM) 119 | return '订阅Steam成功' 120 | 121 | if msg.lower() == '取消订阅steam': 122 | steamdata = loadjson(STEAM) 123 | if group in steamdata['subscribe_groups']: 124 | steamdata['subscribe_groups'].remove(group) 125 | dumpjson(steamdata, STEAM) 126 | return '取消订阅Steam成功' 127 | else: 128 | return '本群未订阅Steam' 129 | 130 | prm = re.match('(怎么)?绑定 *steam(.*)', msg, re.I) 131 | if prm: 132 | usage = '使用方法:\n绑定Steam Steam好友代码(8~10位)' 133 | success = '绑定{}成功' 134 | try: 135 | if prm[1]: 136 | return usage 137 | id3 = int(prm[2]) 138 | await self.api.send_group_msg( 139 | group_id=message['group_id'], 140 | message=f'正在尝试绑定并初始化玩家信息', 141 | ) 142 | if id3 > 76561197960265728: 143 | id3 -= 76561197960265728 144 | id64 = id3 + 76561197960265728 145 | id3 = str(id3) 146 | steamdata = loadjson(STEAM) 147 | # 之前已经绑定过 148 | if steamdata['subscribers'].get(user): 149 | old_id3 = steamdata['subscribers'][user] 150 | if id3 == old_id3: 151 | success = f'你已绑定{old_id3}' 152 | if old_id3 != id3: 153 | steamdata['players'][old_id3]['subscribers'].remove(user) 154 | if not steamdata['players'][old_id3]['subscribers']: 155 | del steamdata['players'][old_id3] 156 | success += f'\n已自动解除绑定{old_id3}' 157 | steamdata['subscribers'][user] = id3 158 | if steamdata['players'].get(id3): 159 | steamdata['players'][id3]['subscribers'].append(user) 160 | steamdata['players'][id3]['subscribers'] = list(set(steamdata['players'][id3]['subscribers'])) 161 | else: 162 | now = int(datetime.now().timestamp()) 163 | gameextrainfo = '' 164 | match_id, start_time, action, rank = 0, 0, 0, 0 165 | try: 166 | try: 167 | j = requests.get(PLAYER_SUMMARY.format(APIKEY, id64), timeout=10).json() 168 | except requests.exceptions.RequestException as e: 169 | logger.warning(f'kale PLAYER_SUMMARY {e}') 170 | success += '\nkale' 171 | raise 172 | p = j['response']['players'][0] 173 | if p.get('gameextrainfo'): 174 | gameextrainfo = p['gameextrainfo'] 175 | s1 = p['personaname'] + '现在正在玩' + p['gameextrainfo'] 176 | else: 177 | gameextrainfo = '' 178 | s1 = p['personaname'] + '现在没在玩游戏' 179 | match_id, start_time = self.dota2.get_last_match(id64) 180 | if match_id and start_time: 181 | s2 = '最近一场Dota 2比赛编号为{},开始于{}'.format( 182 | match_id, 183 | time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(start_time)) 184 | ) 185 | else: 186 | s2 = '没有查询到最近的Dota 2比赛' 187 | name, rank = self.dota2.get_rank_tier(id3) 188 | if rank: 189 | s3 = '现在是{}{}'.format(PLAYER_RANK[rank // 10], rank % 10 or '') 190 | else: 191 | s3 = '没有查询到天梯段位' 192 | action = max(start_time, now if gameextrainfo == 'Dota 2' else 0) 193 | success += '\n{},{},{}'.format(s1, s2, s3) 194 | except Exception as e: 195 | success += '\n初始化玩家信息失败' 196 | logger.warning(f'初始化玩家信息失败 {e}') 197 | steamdata['players'][id3] = { 198 | 'steam_id64': id64, 199 | 'subscribers': [user], 200 | 'gameextrainfo': gameextrainfo, 201 | 'last_change': now, 202 | 'last_DOTA2_action': action, 203 | 'last_DOTA2_match_id': match_id, 204 | 'DOTA2_rank_tier': rank, 205 | } 206 | dumpjson(steamdata, STEAM) 207 | return success.format(id3) 208 | except: 209 | return usage 210 | 211 | if msg.lower() == '解除绑定steam': 212 | steamdata = loadjson(STEAM) 213 | if steamdata['subscribers'].get(user): 214 | id3 = steamdata['subscribers'][user] 215 | steamdata['players'][id3]['subscribers'].remove(user) 216 | if not steamdata['players'][id3]['subscribers']: 217 | del steamdata['players'][id3] 218 | del steamdata['subscribers'][user] 219 | dumpjson(steamdata, STEAM) 220 | return '解除绑定成功' 221 | else: 222 | return '没有找到你的绑定记录' 223 | 224 | prm = re.match('(.+)在(干|做|搞|整)(嘛|啥|哈|什么)', msg) 225 | if prm: 226 | name = prm[1].strip() 227 | steamdata = loadjson(STEAM) 228 | memberdata = loadjson(MEMBER) 229 | if re.search('群友', name): 230 | is_solo = False 231 | players_in_group = [] 232 | for qq, id3 in steamdata['subscribers'].items(): 233 | if qq in memberdata[group]: 234 | players_in_group.append(steamdata['players'][id3]['steam_id64']) 235 | players_in_group = list(set(players_in_group)) 236 | sids = ','.join(str(p) for p in players_in_group) 237 | else: 238 | is_solo = True 239 | obj = self.whois.object_explainer(group, user, name) 240 | name = obj['name'] or name 241 | steam_info = steamdata['players'].get(steamdata['subscribers'].get(obj['uid'])) 242 | if steam_info: 243 | sids = steam_info.get('steam_id64') 244 | if not sids: 245 | return IDK 246 | else: # steam_info is None 247 | if obj['uid'] == UNKNOWN: 248 | return f'我们群里有{name}吗?' 249 | return f'{IDK},因为{name}还没有绑定SteamID' 250 | replys = [] 251 | try: 252 | j = requests.get(PLAYER_SUMMARY.format(APIKEY, sids), timeout=10).json() 253 | except requests.exceptions.RequestException as e: 254 | logger.warning(f'kale PLAYER_SUMMARY {e}') 255 | return 'kale,请稍后再试' 256 | for p in j['response']['players']: 257 | if p.get('gameextrainfo'): 258 | replys.append(p['personaname'] + '现在正在玩' + p['gameextrainfo']) 259 | elif is_solo: 260 | replys.append(p['personaname'] + '现在没在玩游戏') 261 | if replys: 262 | if len(replys) > 2: 263 | replys.append('大家都有光明的未来!') 264 | return '\n'.join(replys) 265 | elif not is_solo: 266 | return '群友都没在玩游戏' 267 | return IDK 268 | 269 | prm = re.match('查询(.+)的天梯段位$', msg) 270 | if prm: 271 | await self.api.send_group_msg( 272 | group_id=message['group_id'], 273 | message=f'正在查询', 274 | ) 275 | name = prm[1].strip() 276 | steamdata = loadjson(STEAM) 277 | memberdata = loadjson(MEMBER) 278 | if re.search('群友', name): 279 | players_in_group = [] 280 | for qq, id3 in steamdata['subscribers'].items(): 281 | if qq in memberdata[group]: 282 | players_in_group.append(id3) 283 | players_in_group = list(set(players_in_group)) 284 | else: 285 | obj = self.whois.object_explainer(group, user, name) 286 | name = obj['name'] or name 287 | id3 = steamdata['subscribers'].get(obj['uid']) 288 | if not id3: 289 | if obj['uid'] == UNKNOWN: 290 | return f'我们群里有{name}吗?' 291 | return f'查不了,{name}可能还没有绑定SteamID' 292 | players_in_group = [id3] 293 | ranks = [] 294 | replys = [] 295 | for id3 in players_in_group: 296 | name, rank = self.dota2.get_rank_tier(id3) 297 | if rank: 298 | ranks.append((name, rank)) 299 | if ranks: 300 | ranks = sorted(ranks, key=lambda i: i[1], reverse=True) 301 | for name, rank in ranks: 302 | replys.append('{}现在是{}{}'.format(name, PLAYER_RANK[rank // 10], rank % 10 or '')) 303 | if len(replys) > 2: 304 | replys.append('大家都有光明的未来!') 305 | return '\n'.join(replys) 306 | else: 307 | return '查不到哟' 308 | 309 | prm = re.match('查询(.+)的(常用英雄|英雄池)$', msg) 310 | if prm: 311 | name = prm[1] 312 | item = prm[2] 313 | if name == '群友': 314 | return '唔得,一个一个查' 315 | steamdata = loadjson(STEAM) 316 | obj = self.whois.object_explainer(group, user, name) 317 | name = obj['name'] or name 318 | id3 = steamdata['subscribers'].get(obj['uid']) 319 | if not id3: 320 | if obj['uid'] == UNKNOWN: 321 | return f'我们群里有{name}吗?' 322 | return f'查不了,{name}可能还没有绑定SteamID' 323 | try: 324 | j = requests.get(OPENDOTA_PLAYERS.format(id3) + '/heroes', timeout=10).json() 325 | except requests.exceptions.RequestException as e: 326 | logger.warning(f'kale OPENDOTA_PLAYERS {e}') 327 | return 'kale,请稍后再试' 328 | if item == '常用英雄': 329 | hero_stat = [] 330 | if j[0]['games'] == 0: 331 | return f'这个{name}是不是什么都没玩过啊' 332 | for i in range(5): 333 | if j[i]['games'] > 0: 334 | hero = HEROES_CHINESE[int(j[i]["hero_id"])][0] 335 | games = j[i]["games"] 336 | win = j[i]["win"] 337 | win_rate = 100 * win / games 338 | hero_stat.append(f'{games}局{hero},赢了{win}局,胜率{win_rate:.2f}%') 339 | else: 340 | break 341 | return f'{name}玩得最多的{len(hero_stat)}个英雄:\n' + '\n'.join(hero_stat) 342 | if item == '英雄池': 343 | hero_num = 0 344 | hero_ge20 = 0 345 | if j[0]['games'] == 0: 346 | return f'这个{name}什么都没玩过啊哪来的英雄池' 347 | for i in range(len(j)): 348 | if j[i]['games'] > 0: 349 | hero_num += 1 350 | if j[i]['games'] >= 20: 351 | hero_ge20 += 1 352 | else: 353 | break 354 | return f'{name}玩过{hero_num}个英雄,其中大于等于20局的有{hero_ge20}个' 355 | 356 | prm = re.match('查询战报(.*)', msg, re.I) 357 | if prm: 358 | usage = '使用方法:\n查询战报 Dota2比赛编号' 359 | try: 360 | match_id = str(int(prm[1])) 361 | steamdata = loadjson(STEAM) 362 | replys = [] 363 | if steamdata['DOTA2_matches_pool'].get(match_id, 0) != 0: 364 | replys.append(f'比赛{match_id}已在比赛缓冲池中') 365 | else: 366 | steamdata['DOTA2_matches_pool'][match_id] = { 367 | 'request_attempts': 0, 368 | 'players': [], 369 | 'is_solo': { 370 | 'group': group, 371 | 'user' : user, 372 | }, 373 | } 374 | dumpjson(steamdata, STEAM) 375 | replys.append(f'已将比赛{match_id}添加至比赛缓冲池') 376 | if group in steamdata['subscribe_groups']: 377 | replys.append('战报将稍后发出') 378 | else: 379 | replys.append('但是因为本群未订阅Steam所以不会发出来') 380 | return ','.join(replys) 381 | except Exception as e: 382 | logger.warning(f'查询战报失败 {e}') 383 | return usage 384 | 385 | prm = re.match('查询(.+)的最近比赛$', msg) 386 | if prm: 387 | name = prm[1].strip() 388 | if name == '群友': 389 | return '唔得,一个一个查' 390 | await self.api.send_group_msg( 391 | group_id=message['group_id'], 392 | message='正在查询', 393 | ) 394 | steamdata = loadjson(STEAM) 395 | obj = self.whois.object_explainer(group, user, name) 396 | name = obj['name'] or name 397 | id3 = steamdata['subscribers'].get(obj['uid']) 398 | if not id3: 399 | if obj['uid'] == UNKNOWN: 400 | return f'我们群里有{name}吗?' 401 | return f'查不了,{name}可能还没有绑定SteamID' 402 | match_id, start_time = self.dota2.get_last_match(id3) 403 | if match_id and start_time: 404 | if match_id > steamdata['players'][id3]['last_DOTA2_match_id']: 405 | if datetime.now().timestamp() - steamdata['players'][id3]['last_DOTA2_action'] < 3600: 406 | return '{}\n{}'.format( 407 | random.choice(['别急好吗', '我知道你很急,但是你先别急']), 408 | '你{}秒前还在玩Dota2,Steam雷达扫描到了比赛就会发' 409 | ) 410 | steamdata['players'][id3]['last_DOTA2_match_id'] = match_id 411 | steamdata['players'][id3]['last_DOTA2_action'] = start_time 412 | replys = [] 413 | match_id = str(match_id) 414 | replys.append('查到了') 415 | # replys.append('{}的最近一场Dota 2比赛编号为{}'.format(name, match_id)) 416 | # replys.append('开始于{}'.format(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(start_time)))) 417 | if steamdata['DOTA2_matches_pool'].get(match_id, 0) != 0: 418 | replys.append('该比赛已在比赛缓冲池中') 419 | else: 420 | steamdata['DOTA2_matches_pool'][match_id] = { 421 | 'request_attempts': 0, 422 | 'players': [], 423 | 'is_solo': { 424 | 'group': group, 425 | 'user' : user, 426 | }, 427 | } 428 | dumpjson(steamdata, STEAM) 429 | replys.append('已将该比赛添加至比赛缓冲池') 430 | if group in steamdata['subscribe_groups']: 431 | replys.append('等着瞧吧(指战报)') 432 | else: 433 | replys.append('但是因为本群未订阅Steam所以不会发出来') 434 | return ','.join(replys) 435 | else: 436 | return '查不到哟' 437 | 438 | if msg.startswith(ATBOT) and '今天' in msg and ('放假' in msg or '休息' in msg or '不上班' in msg): 439 | self.nowork = int(datetime.now().replace(hour=23, minute=59, second=59).timestamp()) 440 | return f'[CQ:at,qq={user}] 好的,今天不上班' 441 | if msg.startswith(ATBOT) and '今晚' in msg and ('通宵' in msg or '不睡觉' in msg): 442 | self.nosleep = int(datetime.now().replace(hour=5, minute=59, second=59).timestamp()) 443 | return f'[CQ:at,qq={user}] 好的,今晚不睡觉' 444 | 445 | 446 | def jobs(self): 447 | trigger = CronTrigger(minute='*') 448 | get_news = (trigger, self.get_news_async) 449 | trigger = CronTrigger(hour='5') 450 | clear_matches = (trigger, self.clear_matches) 451 | return (get_news, clear_matches) 452 | 453 | async def get_news_async(self): 454 | ''' 455 | 返回最新消息 456 | ''' 457 | steamdata = loadjson(STEAM) 458 | memberdata = loadjson(MEMBER) 459 | groups = steamdata.get('subscribe_groups') 460 | if not groups: 461 | return None 462 | 463 | news = [] 464 | news_details = { 465 | 'steam_status': 0, 466 | 'dota2_rank': 0, 467 | 'match_report': 0, 468 | } 469 | players = self.get_players() 470 | sids = ','.join(str(p) for p in players.keys()) 471 | if not sids: 472 | sids = '0' 473 | now = int(datetime.now().timestamp()) 474 | # logger.info('Steam雷达开始扫描') 475 | try: 476 | j = requests.get(PLAYER_SUMMARY.format(APIKEY, sids), timeout=10).json() 477 | except requests.exceptions.RequestException as e: 478 | logger.warning(f'kale PLAYER_SUMMARY {e}') 479 | j = {'response': {'players': []}} 480 | for p in j['response']['players']: 481 | id64 = int(p['steamid']) 482 | id3 = str(id64 - 76561197960265728) 483 | cur_game = p.get('gameextrainfo', '') 484 | pre_game = steamdata['players'][id3]['gameextrainfo'] 485 | pname = p['personaname'] 486 | 487 | # 游戏状态更新 488 | if cur_game == 'Dota 2': 489 | steamdata['players'][id3]['last_DOTA2_action'] = max(now, steamdata['players'][id3]['last_DOTA2_action']) 490 | if cur_game != pre_game: 491 | minutes = (now - steamdata['players'][id3]['last_change']) // 60 492 | if cur_game: 493 | if pre_game: 494 | mt = f'{pname}玩了{minutes}分钟{pre_game}后,玩起了{cur_game}' 495 | else: 496 | mt = f'{pname}启动了{cur_game}' 497 | if datetime.now().hour < 6 and now > self.nosleep: 498 | mt += '\n你他娘的不用睡觉吗?' 499 | if datetime.now().weekday() < 5 and datetime.now().hour in range(8, 18) and now > self.nowork: 500 | mt += '\n见鬼,这群人都不用上班的吗' 501 | news.append({ 502 | 'message': mt, 503 | 'user' : players[id64] 504 | }) 505 | else: 506 | news.append({ 507 | 'message': f'{pname}退出了{pre_game},本次游戏时长{minutes}分钟', 508 | 'user' : players[id64] 509 | }) 510 | news_details['steam_status'] += 1 511 | steamdata['players'][id3]['gameextrainfo'] = cur_game 512 | steamdata['players'][id3]['last_change'] = now 513 | 514 | # DOTA2最近比赛更新 515 | # 每分钟请求时只请求最近3小时内有DOTA2活动的玩家的最近比赛,其余玩家的比赛仅每小时请求一次 516 | if steamdata['players'][id3]['last_DOTA2_action'] >= now - 10800 or datetime.now().minute == self.MINUTE: 517 | # logger.info(f'请求最近比赛更新 {id64}') 518 | match_id, start_time = self.dota2.get_last_match(id64) 519 | else: 520 | match_id, start_time = (0, 0) # 将跳过之后的步骤 521 | new_match = False 522 | if match_id > steamdata['players'][id3]['last_DOTA2_match_id']: 523 | new_match = True 524 | steamdata['players'][id3]['last_DOTA2_action'] = max(start_time, steamdata['players'][id3]['last_DOTA2_action']) 525 | steamdata['players'][id3]['last_DOTA2_match_id'] = match_id 526 | if new_match: 527 | match_id = str(match_id) 528 | player = { 529 | 'personaname': pname, 530 | 'steam_id3' : int(id3), 531 | } 532 | if steamdata['DOTA2_matches_pool'].get(match_id, 0) != 0: 533 | steamdata['DOTA2_matches_pool'][match_id]['players'].append(player) 534 | else: 535 | steamdata['DOTA2_matches_pool'][match_id] = { 536 | 'request_attempts': 0, 537 | 'start_time': start_time, 538 | 'subscribers': [], 539 | 'players': [player] 540 | } 541 | for qq in players[id64]: 542 | if qq not in steamdata['DOTA2_matches_pool'][match_id]['subscribers']: 543 | steamdata['DOTA2_matches_pool'][match_id]['subscribers'].append(qq) 544 | 545 | # 每3小时请求一次天梯段位 546 | if datetime.now().hour % 3 == 0 and datetime.now().minute == self.MINUTE: 547 | pname, cur_rank = self.dota2.get_rank_tier(id3) 548 | pre_rank = steamdata['players'][id3]['DOTA2_rank_tier'] 549 | if cur_rank != pre_rank: 550 | if cur_rank: 551 | if pre_rank: 552 | word = '升' if cur_rank > pre_rank else '掉' 553 | mt = '{}从{}{}{}到了{}{}'.format( 554 | pname, 555 | PLAYER_RANK[pre_rank // 10], pre_rank % 10 or '', 556 | word, 557 | PLAYER_RANK[cur_rank // 10], cur_rank % 10 or '' 558 | ) 559 | else: 560 | mt = '{}达到了{}{}'.format(pname, PLAYER_RANK[cur_rank // 10], cur_rank % 10 or '') 561 | news.append({ 562 | 'message': mt, 563 | 'user' : players[id64] 564 | }) 565 | news_details['dota2_rank'] += 1 566 | steamdata['players'][id3]['DOTA2_rank_tier'] = cur_rank 567 | else: 568 | pass 569 | 570 | dumpjson(steamdata, STEAM) 571 | 572 | match_reports = self.dota2.get_match_reports() 573 | news += match_reports 574 | news_details['match_report'] += len(match_reports) 575 | 576 | for k in list(news_details.keys()): 577 | if news_details[k] == 0: 578 | del news_details[k] 579 | 580 | if len(news) > 0: 581 | logger.info('Steam雷达扫描到了{}个新事件 {}'.format(len(news), str(news_details))) 582 | 583 | to_sends = [] 584 | for msg in news: 585 | if msg.get('target_groups', 0) == 0: 586 | msg['target_groups'] = [] 587 | for u in msg['user']: 588 | for g in memberdata: 589 | if u in memberdata[g] and g not in msg['target_groups']: 590 | msg['target_groups'].append(g) 591 | for g in groups: 592 | if str(g) in msg['target_groups']: 593 | to_sends.append({ 594 | 'message_type': 'group', 595 | 'group_id': g, 596 | 'message': msg['message'] 597 | }) 598 | return to_sends 599 | 600 | def clear_matches(self): 601 | logger.info('清理本地保存的比赛分析数据和战报图片') 602 | try: 603 | size = 0 604 | cnt = len(os.listdir(DOTA2_MATCHES)) 605 | for f in os.listdir(DOTA2_MATCHES): 606 | size += os.path.getsize(os.path.join(DOTA2_MATCHES, f)) 607 | os.remove(os.path.join(DOTA2_MATCHES, f)) 608 | if IMAGE_MODE == 'YOBOT_OUTPUT': 609 | cnt += len(os.listdir(self.YOBOT_OUTPUT)) 610 | for f in os.listdir(self.YOBOT_OUTPUT): 611 | size += os.path.getsize(os.path.join(self.YOBOT_OUTPUT, f)) 612 | os.remove(os.path.join(self.YOBOT_OUTPUT, f)) 613 | logger.info('清理完成,删除了{}个文件,size={:,}'.format(cnt, size)) 614 | except Exception as e: 615 | logger.warning(f'清理失败 {e}') 616 | 617 | def init_fonts(self): 618 | logger.info('初始化字体') 619 | font_path = os.path.expanduser('~/.Steam_watcher/fonts/MSYH.TTC') 620 | font_OK = False 621 | try: 622 | font = ImageFont.truetype(font_path, 12) 623 | font_OK = True 624 | if os.path.getsize(font_path) < 19647736: 625 | logger.warning('字体文件不完整') 626 | font_OK = False 627 | except Exception as e: 628 | logger.warning(f'初始化字体失败 {e}') 629 | if font_OK: 630 | return 631 | try: 632 | with open(font_path, 'wb') as f: 633 | t0 = time.time() 634 | for i in range(1, 193): 635 | n = f'{i:0>3}' 636 | per = i / 192 * 100 637 | t1 = (time.time() - t0) / i * (192 - i) 638 | # logger.info(f'正在重新下载字体({per:.2f}%),预计还需要{t1:.2f}秒') 639 | print(f'正在重新下载字体({per:.2f}%),预计还需要{t1:.2f}秒', end='\r') 640 | f.write(requests.get(f'https://yubo65536.gitee.io/manager/assets/MSYH/x{n}', timeout=10).content) 641 | logger.info('字体下载完成') 642 | except Exception as e: 643 | logger.warning(f'字体下载失败 {e}') 644 | 645 | def init_images(self): 646 | logger.info('初始化图片') 647 | images = [] 648 | for hero in HEROES.values(): 649 | images.append(f'hero_{hero}.png') 650 | for item in ITEMS.values(): 651 | images.append(f'item_{item}.png') 652 | for img in OTHER_IMAGES: 653 | images.append(f'other_{img}.png') 654 | logger.info('从配置文件中读取到{}名英雄、{}个物品和{}张其他图片,尝试读取/下载对应的图片'.format( 655 | len(HEROES), len(ITEMS), len(OTHER_IMAGES) 656 | )) 657 | total = len(images) 658 | downloaded, successful, failed = 0, 0, 0 659 | for img in images: 660 | if img.startswith('item_recipe'): 661 | img_path = os.path.join(IMAGES, 'item_recipe.png') 662 | elif img.startswith('other'): 663 | img_path = os.path.join(IMAGES, img[6:]) 664 | else: 665 | img_path = os.path.join(IMAGES, img) 666 | print('初始化图片({}/{})'.format(downloaded + successful + failed + 1, total), end='\r') 667 | try: 668 | cur_img = Image.open(img_path).verify() 669 | successful += 1 670 | continue 671 | except Exception as e: 672 | pass 673 | try: 674 | with open(img_path, 'wb') as f: 675 | if img.startswith('hero'): 676 | img_url = IMAGE_URL.format('heroes', img[5:-4]) 677 | elif img.startswith('item'): 678 | if img.startswith('item_recipe'): 679 | img_url = IMAGE_URL.format('items', 'recipe') 680 | else: 681 | img_url = IMAGE_URL.format('items', img[5:-4]) 682 | elif img.startswith('other'): 683 | img_url = OTHER_IMAGE_URL.format(img[6:-4]) 684 | f.write(requests.get(img_url, timeout=10).content) 685 | downloaded += 1 686 | except Exception as e: 687 | failed += 1 688 | logger.info(f'从本地读取{successful},重新下载{downloaded},读取/下载失败{failed}') 689 | 690 | def get_players(self): 691 | steamdata = loadjson(STEAM) 692 | players = {} 693 | for p in steamdata['players'].values(): 694 | players[p['steam_id64']] = p['subscribers'] 695 | return players 696 | 697 | 698 | class Dota2: 699 | def __init__(self, **kwargs): 700 | self.setting = kwargs['glo_setting'] 701 | self.IMAGE_MODE = self.setting['image_mode'] 702 | if self.IMAGE_MODE == 'YOBOT_OUTPUT': 703 | self.YOBOT_OUTPUT = self.setting['output_path'] 704 | self.IMAGE_URL = self.setting['image_url'] 705 | 706 | @staticmethod 707 | def get_last_match(id64): 708 | try: 709 | match = requests.get(LAST_MATCH.format(APIKEY, id64), timeout=10).json()['result']['matches'][0] 710 | return match['match_id'], match['start_time'] 711 | except requests.exceptions.RequestException as e: 712 | logger.warning(f'kale LAST_MATCH {e}') 713 | return 0, 0 714 | except Exception as e: 715 | return 0, 0 716 | 717 | @staticmethod 718 | def get_rank_tier(id3): 719 | try: 720 | j = requests.get(OPENDOTA_PLAYERS.format(id3), timeout=10).json() 721 | name = j['profile']['personaname'] 722 | rank = j.get('rank_tier') if j.get('rank_tier') else 0 723 | return name, rank 724 | except requests.exceptions.RequestException as e: 725 | logger.warning(f'kale OPENDOTA_PLAYERS {e}') 726 | return '', 0 727 | except Exception as e: 728 | return '', 0 729 | 730 | # 根据slot判断队伍, 返回0为天辉, 1为夜魇 731 | @staticmethod 732 | def get_team_by_slot(slot): 733 | return slot // 100 734 | 735 | def get_match(self, match_id): 736 | MATCH = os.path.join(DOTA2_MATCHES, f'{match_id}.json') 737 | if os.path.exists(MATCH): 738 | logger.info('比赛编号 {} 读取本地保存的分析结果'.format(match_id)) 739 | return loadjson(MATCH) 740 | steamdata = loadjson(STEAM) 741 | try: 742 | try: 743 | match = requests.get(OPENDOTA_MATCHES.format(match_id), timeout=10).json() 744 | except requests.exceptions.RequestException as e: 745 | logger.warning(f'kale OPENDOTA_MATCHES {e}') 746 | raise 747 | if steamdata['DOTA2_matches_pool'][match_id]['request_attempts'] >= MAX_ATTEMPTS: 748 | logger.warning('比赛编号 {} 重试次数过多,跳过分析'.format(match_id)) 749 | match = {} 750 | if not match.get('players'): 751 | if match.get('error'): 752 | logger.warning('OPENDOTA 返回错误信息 {}'.format(match['error'])) 753 | logger.info('比赛编号 {} 从OPENDOTA获取不到分析结果,使用Valve的API'.format(match_id)) 754 | try: 755 | match = requests.get(MATCH_DETAILS.format(APIKEY, match_id), timeout=10).json()['result'] 756 | match['from_valve'] = True 757 | except requests.exceptions.RequestException as e: 758 | logger.warning(f'kale MATCH_DETAILS {e}') 759 | logger.warning(f'从Valve的API获取比赛结果失败 {e}') 760 | raise 761 | if not match.get('game_mode'): 762 | logger.warning('比赛编号 {} 无效,game_mode {}'.format(match_id, match.get('game_mode'))) 763 | if match.get('game_mode') in (15, 19): 764 | # 活动模式 765 | logger.info('比赛编号 {} 活动模式,跳过分析'.format(match_id)) 766 | match = {'error': '活动模式,跳过分析'} 767 | if match.get('error'): 768 | dumpjson(match, MATCH) 769 | return match 770 | received = match['players'][0].get('damage_inflictor_received', None) 771 | except Exception as e: 772 | steamdata['DOTA2_matches_pool'][match_id]['request_attempts'] += 1 773 | attempts = '(第{}次)'.format(steamdata['DOTA2_matches_pool'][match_id]['request_attempts']) 774 | logger.info('{} {} {}'.format(match_id, attempts, e)) 775 | dumpjson(steamdata, STEAM) 776 | return {} 777 | if received is None and not match.get('from_valve'): 778 | # 比赛分析结果不完整 779 | job_id = steamdata['DOTA2_matches_pool'][match_id].get('job_id') 780 | if job_id: 781 | # 存在之前请求分析的job_id,则查询这个job是否已完成 782 | try: 783 | j = requests.get(OPENDOTA_REQUEST.format(job_id), timeout=10).json() 784 | except requests.exceptions.RequestException as e: 785 | logger.warning(f'kale OPENDOTA_REQUEST {e}') 786 | return {} 787 | if j: 788 | # 查询返回了数据,说明job仍未完成 789 | logger.info('job_id {} 仍在处理中'.format(job_id)) 790 | return {} 791 | else: 792 | # job完成了,可以删掉 793 | del steamdata['DOTA2_matches_pool'][match_id]['job_id'] 794 | dumpjson(steamdata, STEAM) 795 | return {} 796 | else: 797 | # 不存在之前请求分析的job_id,重新请求一次,保存,下次再确认这个job是否已完成 798 | steamdata['DOTA2_matches_pool'][match_id]['request_attempts'] += 1 799 | attempts = '(第{}次)'.format(steamdata['DOTA2_matches_pool'][match_id]['request_attempts']) 800 | try: 801 | j = requests.post(OPENDOTA_REQUEST.format(match_id), timeout=10).json() 802 | except requests.exceptions.RequestException as e: 803 | logger.warning(f'kale OPENDOTA_REQUEST {e}') 804 | return {} 805 | job_id = j['job'].get('jobId', -1) 806 | if job_id == -1: 807 | logger.warning('比赛编号 {} 请求job_id失败'.format(match_id)) 808 | else: 809 | logger.info('比赛编号 {} 请求OPENDOTA分析{},job_id: {}'.format(match_id, attempts, job_id)) 810 | steamdata['DOTA2_matches_pool'][match_id]['job_id'] = job_id 811 | dumpjson(steamdata, STEAM) 812 | return {} 813 | else: 814 | if match.get('from_valve'): 815 | # 比赛结果来自Valve的API 816 | logger.info('比赛编号 {} 从Valve的API获取到分析结果'.format(match_id)) 817 | else: 818 | # 比赛分析结果完整了 819 | logger.info('比赛编号 {} 从OPENDOTA获取到分析结果'.format(match_id)) 820 | for pp in steamdata['DOTA2_matches_pool'][match_id]['players']: 821 | for pm in match['players']: 822 | if pp['steam_id3'] == pm['account_id']: 823 | pm['personaname'] = pp['personaname'] 824 | break 825 | dumpjson(match, MATCH) 826 | return match 827 | 828 | def get_image(self, img_path): 829 | try: 830 | return Image.open(os.path.join(IMAGES, img_path)) 831 | except Exception as e: 832 | logger.warning(e) 833 | return Image.new('RGBA', (30, 30), (255, 160, 160)) 834 | 835 | def init_player(self, player): 836 | if not player.get('net_worth'): 837 | player['net_worth'] = player.get('total_gold') or 0 838 | if not player.get('total_xp'): 839 | player['total_xp'] = 0 840 | if not player.get('hero_damage'): 841 | player['hero_damage'] = 0 842 | if not player.get('damage_inflictor_received'): 843 | player['damage_inflictor_received'] = {} 844 | if not player.get('tower_damage'): 845 | player['tower_damage'] = 0 846 | if not player.get('hero_healing'): 847 | player['hero_healing'] = 0 848 | if not player.get('stuns'): 849 | player['stuns'] = 0 850 | if not player.get('purchase_log'): 851 | player['purchase_log'] = [] 852 | if not player.get('item_usage'): 853 | player['item_usage'] = {} 854 | if not player.get('item_uses'): 855 | player['item_uses'] = {} 856 | if not player.get('lane_role'): 857 | player['lane_role'] = 0 858 | if not player.get('permanent_buffs'): 859 | player['permanent_buffs'] = {} 860 | if not player.get('has_bkb'): 861 | player['has_bkb'] = False 862 | 863 | def draw_title(self, match, draw, font, item, title, color): 864 | idx = item[0] 865 | draw.text( 866 | (match['players'][idx]['title_position'][0], match['players'][idx]['title_position'][1]), 867 | title, font=font, fill=color 868 | ) 869 | title_size = font.getsize(title) 870 | match['players'][idx]['title_position'][0] += title_size[0] + 1 871 | # if match['players'][idx]['title_position'][0] > 195: 872 | # match['players'][idx]['title_position'][0] = 10 873 | # match['players'][idx]['title_position'][1] += 14 874 | 875 | def draw_slogan(self, match, draw, font, idx, title, color): 876 | draw.text((474, 202 + (idx // 5) * 60 + idx * 65), title, font=font, fill=color) 877 | 878 | 879 | def generate_match_message(self, match_id): 880 | match = self.get_match(match_id) 881 | if not match: 882 | return None 883 | for p in match['players']: 884 | self.init_player(p) 885 | steamdata = loadjson(STEAM) 886 | players = steamdata['DOTA2_matches_pool'][match_id]['players'] 887 | start_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(match['start_time'])) 888 | duration = match['duration'] 889 | 890 | # 比赛模式 891 | mode_id = match['game_mode'] 892 | mode = GAME_MODE[mode_id] if mode_id in GAME_MODE else '未知' 893 | 894 | lobby_id = match['lobby_type'] 895 | lobby = LOBBY[lobby_id] if lobby_id in LOBBY else '未知' 896 | # 更新玩家对象的比赛信息 897 | for i in players: 898 | i['ok'] = False 899 | for j in match['players']: 900 | if i['steam_id3'] == j['account_id']: 901 | i['dota2_kill'] = j['kills'] 902 | i['dota2_death'] = j['deaths'] 903 | i['dota2_assist'] = j['assists'] 904 | i['kda'] = ((1. * i['dota2_kill'] + i['dota2_assist']) / i['dota2_death']) \ 905 | if i['dota2_death'] != 0 else (1. * i['dota2_kill'] + i['dota2_assist']) 906 | i['dota2_team'] = self.get_team_by_slot(j['player_slot']) 907 | i['hero'] = j['hero_id'] 908 | i['last_hit'] = j['last_hits'] 909 | i['damage'] = j['hero_damage'] 910 | i['gpm'] = j['gold_per_min'] 911 | i['xpm'] = j['xp_per_min'] 912 | i['ok'] = True 913 | break 914 | if False in [i['ok'] for i in players]: 915 | return '刀雷动,但是摆烂,因为有人的ID不见了,我不说是谁🙄' 916 | personanames = ','.join([players[i]['personaname'] for i in range(-len(players),-1)]) 917 | if personanames: 918 | personanames += '和' 919 | personanames += players[-1]['personaname'] 920 | 921 | # 队伍信息 922 | team = players[0]['dota2_team'] 923 | win = match['radiant_win'] == (team == 0) 924 | 925 | team_damage = 0 926 | team_score = [match['radiant_score'], match['dire_score']][team] 927 | team_deaths = 0 928 | for i in match['players']: 929 | if self.get_team_by_slot(i['player_slot']) == team: 930 | team_damage += i['hero_damage'] 931 | team_deaths += i['deaths'] 932 | 933 | top_kda = 0 934 | for i in players: 935 | if i['kda'] > top_kda: 936 | top_kda = i['kda'] 937 | 938 | if (win and top_kda > 5) or (not win and top_kda > 3): 939 | positive = True 940 | elif (win and top_kda < 2) or (not win and top_kda < 1): 941 | positive = False 942 | else: 943 | if random.randint(0, 1) == 0: 944 | positive = True 945 | else: 946 | positive = False 947 | 948 | # 刀刀雷达动叻! 949 | if CONFIG.get('ONE_LINE_MODE', False): 950 | return '刀雷动!{}直接进行一个比赛的{}'.format(personanames, '赢' if win else '输') 951 | tosend = [] 952 | if win and positive: 953 | tosend.append(random.choice(WIN_POSITIVE).format(personanames)) 954 | elif win and not positive: 955 | tosend.append(random.choice(WIN_NEGATIVE).format(personanames)) 956 | elif not win and positive: 957 | tosend.append(random.choice(LOSE_POSITIVE).format(personanames)) 958 | else: 959 | tosend.append(random.choice(LOSE_NEGATIVE).format(personanames)) 960 | 961 | tosend.append('开始时间: {}'.format(start_time)) 962 | tosend.append('持续时间: {:.0f}分{:.0f}秒'.format(duration / 60, duration % 60)) 963 | tosend.append('游戏模式: [{}/{}]'.format(mode, lobby)) 964 | 965 | for i in players: 966 | personaname = i['personaname'] 967 | hero = random.choice(HEROES_CHINESE[i['hero']]) if i['hero'] in HEROES_CHINESE else '不知道什么鬼' 968 | kda = i['kda'] 969 | last_hits = i['last_hit'] 970 | damage = i['damage'] 971 | kills, deaths, assists = i['dota2_kill'], i['dota2_death'], i['dota2_assist'] 972 | gpm, xpm = i['gpm'], i['xpm'] 973 | 974 | damage_rate = 0 if team_damage == 0 else (100 * damage / team_damage) 975 | participation = 0 if team_score == 0 else (100 * (kills + assists) / team_score) 976 | deaths_rate = 0 if team_deaths == 0 else (100 * deaths / team_deaths) 977 | 978 | tosend.append( 979 | '{}使用{}, KDA: {:.2f}[{}/{}/{}], GPM/XPM: {}/{}, ' \ 980 | '补刀数: {}, 总伤害: {}({:.2f}%), ' \ 981 | '参战率: {:.2f}%, 参葬率: {:.2f}%' \ 982 | .format(personaname, hero, kda, kills, deaths, assists, gpm, xpm, last_hits, 983 | damage, damage_rate, 984 | participation, deaths_rate) 985 | ) 986 | 987 | return '\n'.join(tosend) 988 | 989 | def generate_match_image(self, match_id): 990 | t0 = time.time() 991 | match = self.get_match(match_id) 992 | if not match: 993 | return None 994 | image = Image.new('RGB', (800, 900), (255, 255, 255)) 995 | font = ImageFont.truetype(os.path.expanduser('~/.Steam_watcher/fonts/MSYH.TTC'), 12) 996 | font2 = ImageFont.truetype(os.path.expanduser('~/.Steam_watcher/fonts/MSYH.TTC'), 18) 997 | draw = ImageDraw.Draw(image) 998 | draw.rectangle((0, 0, 800, 70), 'black') 999 | title = '比赛 ' + str(match['match_id']) 1000 | # 手动加粗 1001 | draw.text((30, 15), title, font=font2, fill=(255, 255, 255)) 1002 | draw.text((31, 15), title, font=font2, fill=(255, 255, 255)) 1003 | draw.text((250, 20), '开始时间', font=font, fill=(255, 255, 255)) 1004 | draw.text((251, 20), '开始时间', font=font, fill=(255, 255, 255)) 1005 | draw.text((400, 20), '持续时间', font=font, fill=(255, 255, 255)) 1006 | draw.text((401, 20), '持续时间', font=font, fill=(255, 255, 255)) 1007 | draw.text((480, 20), 'Level', font=font, fill=(255, 255, 255)) 1008 | draw.text((481, 20), 'Level', font=font, fill=(255, 255, 255)) 1009 | draw.text((560, 20), '地区', font=font, fill=(255, 255, 255)) 1010 | draw.text((561, 20), '地区', font=font, fill=(255, 255, 255)) 1011 | draw.text((650, 20), '比赛模式', font=font, fill=(255, 255, 255)) 1012 | draw.text((651, 20), '比赛模式', font=font, fill=(255, 255, 255)) 1013 | start_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(match['start_time'])) 1014 | duration = '{}分{}秒'.format(match['duration'] // 60, match['duration'] % 60) 1015 | skill = SKILL_LEVEL[match['skill']] if match.get('skill') else 'Unknown' 1016 | region_id = 'region_{}'.format(match.get('region')) 1017 | region = REGION[region_id] if region_id in REGION else '未知' 1018 | mode_id = match['game_mode'] 1019 | mode = GAME_MODE[mode_id] if mode_id in GAME_MODE else '未知' 1020 | lobby_id = match['lobby_type'] 1021 | lobby = LOBBY[lobby_id] if lobby_id in LOBBY else '未知' 1022 | draw.text((250, 40), start_time, font=font, fill=(255, 255, 255)) 1023 | draw.text((400, 40), duration, font=font, fill=(255, 255, 255)) 1024 | draw.text((480, 40), skill, font=font, fill=(255, 255, 255)) 1025 | draw.text((560, 40), region, font=font, fill=(255, 255, 255)) 1026 | draw.text((650, 40), f'{mode}/{lobby}', font=font, fill=(255, 255, 255)) 1027 | if match.get('from_valve'): 1028 | draw.text((30, 40), '※分析结果不完整', font=font, fill=(255, 180, 0)) 1029 | else: 1030 | draw.text((30, 40), '※录像分析成功', font=font, fill=(123, 163, 52)) 1031 | RADIANT_GREEN = (60, 144, 40) 1032 | DIRE_RED = (156, 54, 40) 1033 | winner = 1 - int(match['radiant_win']) 1034 | draw.text((364, 81), SLOT_CHINESE[winner] + '胜利', font=font2, fill=[RADIANT_GREEN, DIRE_RED][winner]) 1035 | draw.text((365, 81), SLOT_CHINESE[winner] + '胜利', font=font2, fill=[RADIANT_GREEN, DIRE_RED][winner]) 1036 | radiant_score = str(match['radiant_score']) 1037 | radiant_score_size = font2.getsize(radiant_score) 1038 | draw.text((338 - radiant_score_size[0], 81), radiant_score, font=font2, fill=RADIANT_GREEN) 1039 | draw.text((339 - radiant_score_size[0], 81), radiant_score, font=font2, fill=RADIANT_GREEN) 1040 | draw.text((460, 81), str(match['dire_score']), font=font2, fill=DIRE_RED) 1041 | draw.text((461, 81), str(match['dire_score']), font=font2, fill=DIRE_RED) 1042 | draw.rectangle((0, 120, 800, 122), RADIANT_GREEN) 1043 | draw.rectangle((0, 505, 800, 507), DIRE_RED) 1044 | image.paste(self.get_image('logo_radiant.png').resize((32, 32), Image.ANTIALIAS), (10, 125)) 1045 | image.paste(self.get_image('logo_dire.png').resize((32, 32), Image.ANTIALIAS), (10, 510)) 1046 | draw.text((100, 128 + 385 * winner), '胜利', font=font2, fill=[RADIANT_GREEN, DIRE_RED][winner]) 1047 | # 建筑总血量 基地 4塔 雕像 近战兵营 远程兵营 3塔 2塔 1塔 1048 | total_tower_hp = 4500 + 2600 * 2 + 1000 * 7 + 2200 * 3 + 1300 * 3 + 2500 * 3 + 2500 * 3 + 1800 * 3 1049 | max_net = [0, 0] 1050 | max_xpm = [0, 0] 1051 | max_kills = [0, 0, 0] 1052 | max_deaths = [0, 0, 99999] 1053 | max_assists = [0, 0, 0] 1054 | max_hero_damage = [0, 0] 1055 | max_tower_damage = [0, 0] 1056 | max_stuns = [0, 0] 1057 | max_healing = [0, 0] 1058 | max_hurt = [0, 0] 1059 | min_participation = [0, 999, 999, 999999] 1060 | avg_gold = [0, 0] 1061 | 1062 | for slot in range(0, 2): 1063 | team_damage = 0 1064 | team_damage_received = 0 1065 | team_score = [match['radiant_score'], match['dire_score']][slot] 1066 | team_kills = 0 1067 | team_deaths = 0 1068 | team_gold = 0 1069 | team_exp = 0 1070 | max_mvp_point = [0, 0] 1071 | draw.text((50, 126 + slot * 385), SLOT[slot], font=font, fill=[RADIANT_GREEN, DIRE_RED][slot]) 1072 | draw.text((50, 140 + slot * 385), SLOT_CHINESE[slot], font=font, fill=[RADIANT_GREEN, DIRE_RED][slot]) 1073 | for i in range(5): 1074 | idx = slot * 5 + i 1075 | p = match['players'][idx] 1076 | self.init_player(p) 1077 | p['hurt'] = sum(p['damage_inflictor_received'].values()) 1078 | p['participation'] = 0 if team_score == 0 else 100 * (p['kills'] + p['assists']) / team_score 1079 | team_damage += p['hero_damage'] 1080 | team_damage_received += p['hurt'] 1081 | team_kills += p['kills'] 1082 | team_deaths += p['deaths'] 1083 | team_gold += p['net_worth'] 1084 | team_exp += p['total_xp'] 1085 | hero_img = self.get_image('hero_{}.png'.format(HEROES.get(p['hero_id']))) 1086 | hero_img = hero_img.resize((64, 36), Image.ANTIALIAS) 1087 | image.paste(hero_img, (10, 170 + slot * 60 + idx * 65)) 1088 | draw.rectangle((54, 191 + slot * 60 + idx * 65, 73, 205 + slot * 60 + idx * 65), fill=(50, 50, 50)) 1089 | level = str(p['level']) 1090 | level_size = font.getsize(level) 1091 | draw.text((71 - level_size[0], 190 + slot * 60 + idx * 65), level, font=font, fill=(255, 255, 255)) 1092 | rank = p.get('rank_tier') if p.get('rank_tier') else 0 1093 | rank, star = rank // 10, rank % 10 1094 | rank_img = self.get_image(f'rank_icon_{rank}.png') 1095 | if star: 1096 | rank_star = self.get_image(f'rank_star_{star}.png') 1097 | rank_img = Image.alpha_composite(rank_img, rank_star) 1098 | rank_img = Image.alpha_composite(Image.new('RGBA', rank_img.size, (255, 255, 255)), rank_img) 1099 | rank_img = rank_img.convert('RGB') 1100 | rank_img = rank_img.resize((45, 45), Image.ANTIALIAS) 1101 | image.paste(rank_img, (75, 164 + slot * 60 + idx * 65)) 1102 | rank = '[{}{}] '.format(PLAYER_RANK[rank], star if star else '') 1103 | rank_size = font.getsize(rank) 1104 | draw.text((122, 167 + slot * 60 + idx * 65), rank, font=font, fill=(128, 128, 128)) 1105 | pname = p.get('personaname') if p.get('personaname') else '匿名玩家 {}'.format(p.get('account_id') if p.get('account_id') else '') 1106 | pname_size = font.getsize(pname) 1107 | while rank_size[0] + pname_size[0] > 240: 1108 | pname = pname[:-2] + '…' 1109 | pname_size = font.getsize(pname) 1110 | draw.text((122 + rank_size[0], 167 + slot * 60 + idx * 65), pname, font=font, fill=[RADIANT_GREEN, DIRE_RED][slot]) 1111 | pick = '第?手' 1112 | if match.get('picks_bans'): 1113 | for bp in match.get('picks_bans'): 1114 | if bp['hero_id'] == p['hero_id']: 1115 | pick = '第{}手'.format(bp['order'] + 1) 1116 | break 1117 | if p.get('randomed'): 1118 | pick = '随机' 1119 | lane = '未知分路' 1120 | if p.get('lane_role'): 1121 | lane = ['优势路', '中路', '劣势路', '打野'][p['lane_role'] - 1] 1122 | draw.text((122, 181 + slot * 60 + idx * 65), '{} {}'.format(pick, lane), font=font, fill=(0, 0, 0)) 1123 | net = '{:,}'.format(p['net_worth']) 1124 | net_size = font.getsize(net) 1125 | damage_to_net = '({:.2f})'.format(p['hero_damage'] / p['net_worth'] if p['net_worth'] else 0) 1126 | draw.text((123, 196 + slot * 60 + idx * 65), net, font=font, fill=(0, 0, 0)) 1127 | draw.text((122, 195 + slot * 60 + idx * 65), net, font=font, fill=(255, 255, 0)) 1128 | draw.text((126 + net_size[0], 195 + slot * 60 + idx * 65), damage_to_net, font=font, fill=(0, 0, 0)) 1129 | tower_damage_rate = 0 if p['tower_damage'] == 0 else (100 * p['tower_damage'] / total_tower_hp) 1130 | draw.text((215, 209 + slot * 60 + idx * 65), '建筑伤害: {:,} ({:.2f}%)'.format(p['tower_damage'], tower_damage_rate), font=font, fill=(0, 0, 0)) 1131 | kda = '{}/{}/{} ({:.2f})'.format( 1132 | p['kills'], p['deaths'], p['assists'], 1133 | (p['kills'] + p['assists']) if p['deaths'] == 0 else (p['kills'] + p['assists']) / p['deaths'] 1134 | ) 1135 | draw.text((375, 167 + slot * 60 + idx * 65), kda, font=font, fill=(0, 0, 0)) 1136 | draw.text((375, 195 + slot * 60 + idx * 65), '控制时间: {:.2f}s'.format(p['stuns']), font=font, fill=(0, 0, 0)) 1137 | draw.text((375, 209 + slot * 60 + idx * 65), '治疗量: {:,}'.format(p['hero_healing']), font=font, fill=(0, 0, 0)) 1138 | 1139 | p['title_position'] = [10, 209 + slot * 60 + idx * 65] 1140 | mvp_point = p['kills'] * 5 + p['assists'] * 3 + p['stuns'] * 0.5 + p['hero_damage'] * 0.001 + p['tower_damage'] * 0.01 + p['hero_healing'] * 0.002 1141 | if mvp_point > max_mvp_point[1]: 1142 | max_mvp_point = [idx, mvp_point] 1143 | if p['net_worth'] > max_net[1]: 1144 | max_net = [idx, p['net_worth']] 1145 | if p['xp_per_min'] > max_xpm[1]: 1146 | max_xpm = [idx, p['xp_per_min']] 1147 | if p['kills'] > max_kills[1] or (p['kills'] == max_kills[1] and p['hero_damage'] > max_kills[2]): 1148 | max_kills = [idx, p['kills'], p['hero_damage']] 1149 | if p['deaths'] > max_deaths[1] or (p['deaths'] == max_deaths[1] and p['net_worth'] < max_deaths[2]): 1150 | max_deaths = [idx, p['deaths'], p['net_worth']] 1151 | if p['assists'] > max_assists[1] or (p['assists'] == max_assists[1] and p['hero_damage'] > max_assists[2]): 1152 | max_assists = [idx, p['assists'], p['hero_damage']] 1153 | if p['hero_damage'] > max_hero_damage[1]: 1154 | max_hero_damage = [idx, p['hero_damage']] 1155 | if p['tower_damage'] > max_tower_damage[1]: 1156 | max_tower_damage = [idx, p['tower_damage']] 1157 | if p['stuns'] > max_stuns[1]: 1158 | max_stuns = [idx, p['stuns']] 1159 | if p['hero_healing'] > max_healing[1]: 1160 | max_healing = [idx, p['hero_healing']] 1161 | if p['hurt'] > max_hurt[1]: 1162 | max_hurt = [idx, p['hurt']] 1163 | if ( 1164 | p['participation'] < min_participation[1] 1165 | ) or ( 1166 | p['participation'] == min_participation[1] and p['kills'] + p['assists'] < min_participation[2] 1167 | ) or ( 1168 | p['participation'] == min_participation[1] and p['kills'] + p['assists'] == min_participation[2] and p['hero_damage'] < min_participation[3] 1169 | ): 1170 | min_participation = [idx, p['participation'], p['kills'] + p['assists'], p['hero_damage']] 1171 | 1172 | scepter = 0 1173 | shard = 0 1174 | image.paste(Image.new('RGB', (252, 32), (192, 192, 192)), (474, 171 + slot * 60 + idx * 65)) 1175 | p['purchase_log'].reverse() 1176 | for pl in p['purchase_log']: 1177 | if pl['key'] == ITEMS.get(116): # BKB 1178 | p['has_bkb'] = True 1179 | break 1180 | for item in ITEM_SLOTS: 1181 | if p[item] == 0: 1182 | item_img = Image.new('RGB', (40, 30), (128, 128, 128)) 1183 | else: 1184 | if ITEMS.get(p[item], '').startswith('recipe'): 1185 | item_img = self.get_image('item_recipe.png') 1186 | else: 1187 | item_img = self.get_image('item_{}.png'.format(ITEMS.get(p[item]))) 1188 | if p[item] == 108: 1189 | scepter = 1 1190 | if item == 'item_neutral': 1191 | ima = item_img.convert('RGBA') 1192 | size = ima.size 1193 | r1 = min(size[0], size[1]) 1194 | if size[0] != size[1]: 1195 | ima = ima.crop(( 1196 | (size[0] - r1) // 2, 1197 | (size[1] - r1) // 2, 1198 | (size[0] + r1) // 2, 1199 | (size[1] + r1) // 2 1200 | )) 1201 | r2 = r1 // 2 1202 | imb = Image.new('RGBA', (r2 * 2, r2 * 2), (255, 255, 255, 0)) 1203 | pima = ima.load() 1204 | pimb = imb.load() 1205 | r = r1 / 2 1206 | for i in range(r1): 1207 | for j in range(r1): 1208 | l = ((i - r) ** 2 + (j - r) ** 2) ** 0.5 1209 | if l < r2: 1210 | pimb[i - (r - r2), j - (r - r2)] = pima[i, j] 1211 | imb = imb.resize((30, 30), Image.ANTIALIAS) 1212 | imb = Image.alpha_composite(Image.new('RGBA', imb.size, (255, 255, 255)), imb) 1213 | item_img = imb.convert('RGB') 1214 | image.paste(item_img, (733, 170 + slot * 60 + idx * 65)) 1215 | else: 1216 | item_img = item_img.resize((40, 30), Image.ANTIALIAS) 1217 | image.paste(item_img, (475 + 42 * ITEM_SLOTS.index(item), 172 + slot * 60 + idx * 65)) 1218 | purchase_time = None 1219 | for pl in p['purchase_log']: 1220 | if p[item] == 0: 1221 | continue 1222 | if pl['key'] == ITEMS.get(p[item]): 1223 | purchase_time = pl['time'] 1224 | pl['key'] += '_' 1225 | break 1226 | if purchase_time: 1227 | draw.rectangle((475 + 42 * ITEM_SLOTS.index(item), 191 + slot * 60 + idx * 65, 514 + 42 * ITEM_SLOTS.index(item), 201 + slot * 60 + idx * 65), fill=(50, 50, 50)) 1228 | draw.text( 1229 | (479 + 42 * ITEM_SLOTS.index(item), 188 + slot * 60 + idx * 65), 1230 | '{:0>2}:{:0>2}'.format(purchase_time // 60, purchase_time % 60) if purchase_time > 0 else '-{}:{:0>2}'.format(-purchase_time // 60, -purchase_time % 60), 1231 | font=font, fill=(192, 192, 192) 1232 | ) 1233 | for buff in p['permanent_buffs']: 1234 | if buff['permanent_buff'] == 2: 1235 | scepter = 1 1236 | if buff['permanent_buff'] == 12: 1237 | shard = 1 1238 | scepter_img = self.get_image(f'scepter_{scepter}.png') 1239 | scepter_img = scepter_img.resize((20, 20), Image.ANTIALIAS) 1240 | image.paste(scepter_img, (770 , 170 + slot * 60 + idx * 65)) 1241 | shard_img = self.get_image(f'shard_{shard}.png') 1242 | shard_img = shard_img.resize((20, 11), Image.ANTIALIAS) 1243 | image.paste(shard_img, (770 , 190 + slot * 60 + idx * 65)) 1244 | 1245 | for i in range(4): 1246 | draw.rectangle((0, 228 + slot * 385 + i * 65, 800, 228 + slot * 385 + i * 65), (225, 225, 225)) 1247 | 1248 | for i in range(5): 1249 | idx = slot * 5 + i 1250 | p = match['players'][idx] 1251 | damage_rate = 0 if team_damage == 0 else 100 * (p['hero_damage'] / team_damage) 1252 | damage_received_rate = 0 if team_damage_received == 0 else 100 * (p['hurt'] / team_damage_received) 1253 | draw.text((215, 181 + slot * 60 + idx * 65), '造成伤害: {:,} ({:.2f}%)'.format(p['hero_damage'], damage_rate), font=font, fill=(0, 0, 0)) 1254 | draw.text((215, 195 + slot * 60 + idx * 65), '承受伤害: {:,} ({:.2f}%)'.format(p['hurt'], damage_received_rate), font=font, fill=(0, 0, 0)) 1255 | draw.text((375, 181 + slot * 60 + idx * 65), '参战率: {:.2f}%'.format(p['participation']), font=font, fill=(0, 0, 0)) 1256 | 1257 | if slot == winner: 1258 | self.draw_title(match, draw, font, max_mvp_point, 'MVP', (255, 127, 39)) 1259 | else: 1260 | self.draw_title(match, draw, font, max_mvp_point, '魂', (0, 162, 232)) 1261 | 1262 | draw.text((475, 128 + slot * 385), '杀敌', font=font, fill=(64, 64, 64)) 1263 | draw.text((552, 128 + slot * 385), '总伤害', font=font, fill=(64, 64, 64)) 1264 | draw.text((636, 128 + slot * 385), '总经济', font=font, fill=(64, 64, 64)) 1265 | draw.text((726, 128 + slot * 385), '总经验', font=font, fill=(64, 64, 64)) 1266 | draw.text((475, 142 + slot * 385), f'{team_kills}', font=font, fill=(128, 128, 128)) 1267 | draw.text((552, 142 + slot * 385), f'{team_damage}', font=font, fill=(128, 128, 128)) 1268 | draw.text((636, 142 + slot * 385), f'{team_gold}', font=font, fill=(128, 128, 128)) 1269 | draw.text((726, 142 + slot * 385), f'{team_exp}', font=font, fill=(128, 128, 128)) 1270 | 1271 | avg_gold[slot] = team_gold / 5 1272 | 1273 | if max_net[1] > 0: 1274 | self.draw_title(match, draw, font, max_net, '富', (255, 192, 30)) 1275 | if max_xpm[1] > 0: 1276 | self.draw_title(match, draw, font, max_xpm, '睿', (30, 30, 255)) 1277 | if max_stuns[1] > 0: 1278 | self.draw_title(match, draw, font, max_stuns, '控', (255, 0, 128)) 1279 | if max_hero_damage[1] > 0: 1280 | self.draw_title(match, draw, font, max_hero_damage, '爆', (192, 0, 255)) 1281 | if max_kills[1] > 0: 1282 | self.draw_title(match, draw, font, max_kills, '破', (224, 36, 36)) 1283 | if max_deaths[1] > 0: 1284 | self.draw_title(match, draw, font, max_deaths, '鬼', (192, 192, 192)) 1285 | if max_assists[1] > 0: 1286 | self.draw_title(match, draw, font, max_assists, '助', (0, 132, 66)) 1287 | if max_tower_damage[1] > 0: 1288 | self.draw_title(match, draw, font, max_tower_damage, '拆', (128, 0, 255)) 1289 | if max_healing[1] > 0: 1290 | self.draw_title(match, draw, font, max_healing, '奶', (0, 228, 120)) 1291 | if max_hurt[1] > 0: 1292 | self.draw_title(match, draw, font, max_hurt, '耐', (112, 146, 190)) 1293 | if min_participation[1] < 999: 1294 | self.draw_title(match, draw, font, min_participation, '摸', (200, 190, 230)) 1295 | if CONFIG.get('BKB_RECOMMENDED', False): 1296 | for slot in range(0, 2): 1297 | safe_has_bkb = False 1298 | mid_has_bkb = False 1299 | safe = (0, None) 1300 | for i in range(5): 1301 | idx = slot * 5 + i 1302 | p = match['players'][idx] 1303 | if p['lane_role'] == 1 and p['net_worth'] > safe[0]: 1304 | safe = (p['net_worth'], idx) 1305 | for i in range(5): 1306 | idx = slot * 5 + i 1307 | p = match['players'][idx] 1308 | # if p['lane_role'] == 1 and p['net_worth'] >= avg_gold[slot]: # 一号位 1309 | if idx == safe[1]: # 一号位 1310 | p['is_safe'] = True 1311 | if p['has_bkb']: 1312 | safe_has_bkb = True 1313 | if p['net_worth'] >= 12000 and not p['has_bkb']: 1314 | if p['deaths'] / team_deaths >= 0.2: 1315 | self.draw_slogan(match, draw, font, idx, '打大哥不出BKB死了{}次你有什么头绪吗?'.format(p['deaths']), (255, 0, 0)) 1316 | else: 1317 | self.draw_slogan(match, draw, font2, idx, '打大哥不出BKB?', (255, 0, 0)) 1318 | if p['lane_role'] == 2: # 二号位 1319 | p['is_mid'] = True 1320 | if p['has_bkb']: 1321 | mid_has_bkb = True 1322 | if p['net_worth'] >= 12000 and not p['has_bkb']: 1323 | if p['deaths'] / team_deaths >= 0.2: 1324 | self.draw_slogan(match, draw, font, idx, '打中单不出BKB死了{}次你有什么头绪吗?'.format(p['deaths']), (255, 0, 0)) 1325 | else: 1326 | self.draw_slogan(match, draw, font2, idx, '打中单不出BKB?', (255, 0, 0)) 1327 | for i in range(5): 1328 | idx = slot * 5 + i 1329 | p = match['players'][idx] 1330 | if not p.get('is_safe') and not p.get('is_mid'): # 三四五 1331 | if p['net_worth'] >= 10000: 1332 | if not safe_has_bkb and not mid_has_bkb and p['has_bkb']: 1333 | self.draw_slogan(match, draw, font2, idx, 'BKB!你出得好哇!', (255, 0, 0)) 1334 | draw.text( 1335 | (10, 880), 1336 | '※录像分析数据来自opendota.com,DOTA2游戏图片素材版权归Valve所有', 1337 | font=font, 1338 | fill=(128, 128, 128) 1339 | ) 1340 | if self.IMAGE_MODE == 'YOBOT_OUTPUT': 1341 | image.save(os.path.join(self.YOBOT_OUTPUT, f'{match_id}.png'), 'png') 1342 | else: 1343 | image.save(os.path.join(DOTA2_MATCHES, f'{match_id}.png'), 'png') 1344 | 1345 | logger.info('比赛编号 {} 生成战报图片,耗时{:.3f}s'.format(match_id, time.time() - t0)) 1346 | 1347 | def get_match_reports(self): 1348 | steamdata = loadjson(STEAM) 1349 | reports = [] 1350 | todelete = [] 1351 | for match_id, match_info in steamdata['DOTA2_matches_pool'].items(): 1352 | if match_info.get('is_solo'): 1353 | match = self.get_match(match_id) 1354 | if match: 1355 | if match.get('error'): 1356 | logger.warning('比赛编号 {} 在分析结果中发现错误 {}'.format(match_id, match['error'])) 1357 | m = '[CQ:at,qq={}] 你点的比赛战报来不了了!'.format(match_info['is_solo']['user']) 1358 | m += '\n在分析结果中发现错误 {}'.format(match['error']) 1359 | else: 1360 | self.generate_match_image(match_id) 1361 | m = '[CQ:at,qq={}] 你点的比赛战报来了!'.format(match_info['is_solo']['user']) 1362 | if self.IMAGE_MODE == 'YOBOT_OUTPUT': 1363 | m += '\n[CQ:image,file={},cache=0]'.format(self.IMAGE_URL.format(match_id)) 1364 | elif self.IMAGE_MODE == 'BASE64_IMAGE': 1365 | decoded = base64.b64encode(open(os.path.join(DOTA2_MATCHES, f'{match_id}.png'), 'rb').read()).decode() 1366 | m += '\n[CQ:image,file=base64://{}]'.format(decoded) 1367 | else: 1368 | m += '\n[CQ:image,file=file:///{}]'.format(os.path.join(DOTA2_MATCHES, f'{match_id}.png')) 1369 | reports.append( 1370 | { 1371 | 'message': m, 1372 | 'target_groups': [match_info['is_solo']['group']], 1373 | 'user': [], 1374 | } 1375 | ) 1376 | todelete.append(match_id) 1377 | else: 1378 | now = int(datetime.now().timestamp()) 1379 | if match_info['start_time'] <= now - 86400 * 7: 1380 | todelete.append(match_id) 1381 | continue 1382 | match = self.get_match(match_id) 1383 | if match: 1384 | if match.get('error'): 1385 | logger.warning('比赛编号 {} 在分析结果中发现错误 {}'.format(match_id, match['error'])) 1386 | else: 1387 | m = self.generate_match_message(match_id) 1388 | if isinstance(m, str): 1389 | self.generate_match_image(match_id) 1390 | if self.IMAGE_MODE == 'YOBOT_OUTPUT': 1391 | m += '\n[CQ:image,file={},cache=0]'.format(self.IMAGE_URL.format(match_id)) 1392 | elif self.IMAGE_MODE == 'BASE64_IMAGE': 1393 | decoded = base64.b64encode(open(os.path.join(DOTA2_MATCHES, f'{match_id}.png'), 'rb').read()).decode() 1394 | m += '\n[CQ:image,file=base64://{}]'.format(decoded) 1395 | else: 1396 | m += '\n[CQ:image,file=file:///{}]'.format(os.path.join(DOTA2_MATCHES, f'{match_id}.png')) 1397 | reports.append( 1398 | { 1399 | 'message': m, 1400 | 'user' : match_info['subscribers'], 1401 | } 1402 | ) 1403 | todelete.append(match_id) 1404 | # 数据在生成比赛报告的过程中会被修改,需要重新读取 1405 | steamdata = loadjson(STEAM) 1406 | for match_id in todelete: 1407 | del steamdata['DOTA2_matches_pool'][match_id] 1408 | dumpjson(steamdata, STEAM) 1409 | return reports --------------------------------------------------------------------------------