├── 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
--------------------------------------------------------------------------------