├── playerInfo.db ├── config.py ├── player.py ├── go.sh ├── steam.py ├── message_sender.py ├── README.md ├── common.py ├── DBOper.py ├── run.py ├── DOTA2_dicts.py └── DOTA2.py /playerInfo.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Inv0k3r/DOTA2_Bot/HEAD/playerInfo.db -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | API_KEY = "xxxxxx" 2 | BOT_QQ = 10000 3 | BOT_PASSWORD = "xxxx" 4 | QQ_GROUP_ID = 10000 5 | MIRAI_URL = "http://127.0.0.1:8080" 6 | MIRAI_AUTH_KEY = "mVJ4OMqZAvuVty5I" 7 | PLAYER_LIST = [ 8 | ["枫哥", 90045009], 9 | ["甲哥", 113705693], 10 | ["翔哥", 104744847] 11 | ] 12 | 13 | # 是否在战报中附带链接(带链接的消息可能因风控发不出去) 14 | ENABLE_URL = False 15 | 16 | # 是否仅使用英雄默认名字 17 | DEFAULT_NAME_ONLY = False 18 | 19 | # 是否启用Steam游戏状态监视 20 | ENABLE_STEAM_WATCHER = False 21 | -------------------------------------------------------------------------------- /player.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | 4 | 5 | class player: 6 | # 基本属性 7 | short_steamID = 0 8 | long_steamID = 0 9 | nickname = '' 10 | DOTA2_score = '' 11 | last_DOTA2_match_ID = '' 12 | 13 | # 玩家在最新的一场比赛中的数据 14 | # dota2专属 15 | dota2_kill = 0 16 | dota2_death = 0 17 | dota2_assist = 0 18 | # 1为天辉, 2为夜魇 19 | dota2_team = 1 20 | kda = 0 21 | gpm = 0 22 | xpm = 0 23 | hero = '' 24 | last_hit = 0 25 | damage = 0 26 | 27 | def __init__(self, nickname, short_steamID, long_steamID, last_DOTA2_match_ID): 28 | self.nickname = nickname 29 | self.short_steamID = short_steamID 30 | self.long_steamID = long_steamID 31 | self.last_DOTA2_match_ID = last_DOTA2_match_ID 32 | 33 | 34 | PLAYER_LIST = [] 35 | -------------------------------------------------------------------------------- /go.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo apt install screen 4 | pip install json 5 | pip install requests 6 | 7 | qq=$(python -c "import config; print config.BOT_QQ") 8 | pass=$(python -c "import config; print config.BOT_PASSWORD") 9 | 10 | chmod +x ./miraibot/miraiOK_linux_amd64 11 | 12 | cd miraibot 13 | 14 | screen_name=$"bot" 15 | screen -dmS $screen_name 16 | 17 | cmd=$"./miraiOK_linux_amd64"; 18 | screen -x -S $screen_name -p 0 -X stuff "$cmd" 19 | screen -x -S $screen_name -p 0 -X stuff $'\n' 20 | screen -x -S $screen_name -p 0 -X stuff "$cmd" 21 | screen -x -S $screen_name -p 0 -X stuff $'\n' 22 | cmd=$"login ${qq} ${pass}" 23 | screen -x -S $screen_name -p 0 -X stuff "$cmd" 24 | screen -x -S $screen_name -p 0 -X stuff $'\n' 25 | echo "启动MiraiOK成功" 26 | 27 | screen_name=$"watcher" 28 | screen -dmS $screen_name 29 | cmd=$"python3 ../run.py"; 30 | screen -x -S $screen_name -p 0 -X stuff "$cmd" 31 | screen -x -S $screen_name -p 0 -X stuff $'\n' 32 | echo "偷窥机器人启动成功" -------------------------------------------------------------------------------- /steam.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | from datetime import datetime 4 | from config import API_KEY, PLAYER_LIST 5 | from DBOper import get_playing_game, update_playing_game 6 | 7 | def gaming_status_watcher(): 8 | replys = [] 9 | status_changed = False 10 | sids = ','.join(str(p[1] + 76561197960265728) for p in PLAYER_LIST) 11 | r = requests.get(f'http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key={API_KEY}&steamids={sids}') 12 | j = json.loads(r.content) 13 | for p in j['response']['players']: 14 | sid = int(p['steamid']) 15 | pname = p['personaname'] 16 | cur_game = p.get('gameextrainfo', '') 17 | pre_game, last_update = get_playing_game(sid) 18 | 19 | # 游戏状态更新 20 | if cur_game != pre_game: 21 | status_changed = True 22 | now = int(datetime.now().timestamp()) 23 | minutes = (now - last_update) // 60 24 | if cur_game: 25 | if pre_game: 26 | replys.append(f'{pname}玩了{minutes}分钟{pre_game}后,玩起了{cur_game}') 27 | else: 28 | replys.append(f'{pname}启动了{cur_game}') 29 | else: 30 | replys.append(f'{pname}退出了{pre_game},本次游戏时长{minutes}分钟') 31 | update_playing_game(sid, cur_game, now) 32 | 33 | return '\n'.join(replys) if replys else None -------------------------------------------------------------------------------- /message_sender.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | import json 4 | import requests 5 | import config 6 | 7 | url = config.MIRAI_URL 8 | # 群号 9 | target = config.QQ_GROUP_ID 10 | # bot的QQ号 11 | bot_qq = config.BOT_QQ 12 | # mirai http的auth key 13 | authKey = config.MIRAI_AUTH_KEY 14 | 15 | 16 | def message(m: str): 17 | # Authorize 18 | auth_key = {"authKey": authKey} 19 | r = requests.post(url + "/auth", json.dumps(auth_key)) 20 | if json.loads(r.text).get('code') != 0: 21 | print("ERROR@auth") 22 | print(r.text) 23 | exit(1) 24 | # Verify 25 | session_key = json.loads(r.text).get('session') 26 | session = {"sessionKey": session_key, "qq": bot_qq} 27 | r = requests.post(url + "/verify", json.dumps(session)) 28 | if json.loads(r.text).get('code') != 0: 29 | print("ERROR@verify") 30 | print(r.text) 31 | exit(2) 32 | data = { 33 | "sessionKey": session_key, 34 | "target": target, 35 | "messageChain": [ 36 | {"type": "Plain", "text": m} 37 | ] 38 | } 39 | r = requests.post(url + "/sendGroupMessage", json.dumps(data)) 40 | # release 41 | data = { 42 | "sessionKey": session_key, 43 | "qq": bot_qq 44 | } 45 | r = requests.post(url + "/release", json.dumps(data)) 46 | # print(r.text) 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DOTA2的处刑BOT 2 | 3 | ## 介绍 4 | 在群友启动或退出游戏时向群里播报(可选) 5 | 6 | (发行Steam API key的账号可能需要有监视对象的好友,否则获取不到游戏状态) 7 | 8 | 在群友打完一把游戏后, bot会向群里更新这局比赛的数据 9 | 10 | DOTA2的数据来自于V社的官方API, 每日请求数限制100,000次 11 | 12 | YYGQ的文来自于[dota2_watcher](https://github.com/unilink233/dota2_watcher) 13 | 14 | 有任何建议可以发issue, 随缘更新 15 | 16 | **Windows下可以按照安装指南下载Windows版本的MiraiOK** 17 | 18 | **我这两天找了一下没有合适的免费开源微信机器人, 所以可能不会有微信版本** 19 | 20 | **一键脚本目前可能不太好用, 建议按照安装指南使用, 近期会发布一个更易于操作的版本一键脚本** 21 | 22 | ## 一键脚本 23 | 24 | - 修改`config.py`来配置bot 25 | 26 | - `chmod +x go.sh` 27 | 28 | - `bash go.sh` 29 | 30 | ## 安装指南 31 | 32 | - 下载对应版本的[miraiOK](https://github.com/LXY1226/MiraiOK), 有hxd说下不动, 我传了个Linux64版本的[度盘](https://pan.baidu.com/s/1bLYwWWHCcgmnLHoofXTHxQ) 提取码: 5trx 33 | 34 | - 运行一下miraiOK, 然后关闭, 会自动生成一个`plugins`文件夹 35 | 36 | - 把[mirai-http-api](https://github.com/project-mirai/mirai-api-http)里的release的jar扔进plugins文件夹 37 | 38 | - 通过`screen -S bot && ./miraiOK_linux-amd64`启动miraiOK, 登陆你的BOT账号, 这一步可能有一些登陆上的问题, 可以自行`screen -r bot`上去查看 39 | 40 | - 在[这里](http://steamcommunity.com/dev/apikey)申请你的steam API key, 修改`config.py`中的`api_key` 41 | 42 | - 安装requests模块和json模块: `pip install requests,json` 43 | 44 | - 修改config.py来配置bot 45 | 46 | - 通过screen来后台运行: `screen -S dota_bot`, Windows可以直接运行miraiok 47 | 48 | - 运行`run.py`脚本来启动BOT: `python3 run.py` 49 | 50 | ## 后续计划 51 | 52 | - [ ] 丰富YYGQ内容(大家可以直接提交, 我会合并分支) 53 | 54 | - [ ] 发布release 55 | -------------------------------------------------------------------------------- /common.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | import DOTA2 4 | from DBOper import update_DOTA2_match_ID 5 | from player import PLAYER_LIST 6 | from typing import List, Dict 7 | from steam import gaming_status_watcher 8 | from message_sender import message as send 9 | 10 | def steam_id_convert_32_to_64(short_steamID: int) -> int: 11 | return short_steamID + 76561197960265728 12 | 13 | 14 | def steam_id_convert_64_to_32(long_steamID: int) -> int: 15 | return long_steamID - 76561197960265728 16 | 17 | 18 | # 返回一个最新比赛变化过的字典 19 | # 格式: { match_id1: [player1, player2, player3], match_id2: [player1, player2]} 20 | def update_DOTA2() -> Dict: 21 | result = {} 22 | for i in PLAYER_LIST: 23 | try: 24 | match_id = DOTA2.get_last_match_id_by_short_steamID(i.short_steamID) 25 | except DOTA2.DOTA2HTTPError: 26 | continue 27 | if match_id != i.last_DOTA2_match_ID: 28 | 29 | if result.get(match_id, 0) != 0: 30 | result[match_id].append(i) 31 | else: 32 | result.update({match_id: [i]}) 33 | # 更新数据库的last_DOTA2_match_id字段 34 | update_DOTA2_match_ID(i.short_steamID, match_id) 35 | # 更新列表 36 | i.last_DOTA2_match_ID = match_id 37 | 38 | return result 39 | 40 | 41 | def update_and_send_message_DOTA2(): 42 | # 格式: { match_id1: [player1, player2, player3], match_id2: [player1, player2]} 43 | result = update_DOTA2() 44 | for match_id in result: 45 | msg = DOTA2.generate_match_message( 46 | match_id=match_id, 47 | player_list=result[match_id] 48 | ) 49 | if isinstance(msg, str): 50 | send(msg) 51 | 52 | 53 | def update_and_send_gaming_status(): 54 | msg = gaming_status_watcher() 55 | if isinstance(msg, str): 56 | send(msg) -------------------------------------------------------------------------------- /DBOper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | import sqlite3 4 | from player import player, PLAYER_LIST 5 | conn = sqlite3.connect('playerInfo') 6 | c = conn.cursor() 7 | 8 | 9 | def init(): 10 | cursor = c.execute("SELECT * from playerInfo") 11 | for row in cursor: 12 | player_obj = player(short_steamID=row[0], 13 | long_steamID=row[1], 14 | nickname=row[2], 15 | last_DOTA2_match_ID=row[4]) 16 | player_obj.DOTA2_score = row[4] 17 | PLAYER_LIST.append(player_obj) 18 | 19 | 20 | def update_DOTA2_match_ID(short_steamID, last_DOTA2_match_ID): 21 | c.execute("UPDATE playerInfo SET last_DOTA2_match_ID='{}' " 22 | "WHERE short_steamID={}".format(last_DOTA2_match_ID, short_steamID)) 23 | conn.commit() 24 | 25 | 26 | def insert_info(short_steamID, long_steamID, nickname, last_DOTA2_match_ID): 27 | c.execute("INSERT INTO playerInfo (short_steamID, long_steamID, nickname, last_DOTA2_match_ID) " 28 | "VALUES ({}, {}, '{}', '{}')" 29 | .format(short_steamID, long_steamID, nickname, last_DOTA2_match_ID)) 30 | conn.commit() 31 | 32 | 33 | def is_player_stored(short_steamID: int) -> bool: 34 | c.execute("SELECT * FROM playerInfo WHERE short_steamID=={}".format(short_steamID)) 35 | if len(c.fetchall()) == 0: 36 | return False 37 | return True 38 | 39 | def get_playing_game(short_steamID): 40 | ret = c.execute( 41 | "SELECT gamename, last_update FROM playerInfo WHERE short_steamID=?", 42 | (short_steamID,) 43 | ).fetchone() 44 | return (ret[0], ret[1]) if ret else ('', 0) 45 | 46 | def update_playing_game(short_steamID, gamename, timestamp): 47 | c.execute( 48 | "UPDATE playerInfo SET gamename=?, last_update=? WHERE short_steamID=?", 49 | (gamename, timestamp, short_steamID) 50 | ) 51 | conn.commit() -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | import time 4 | import json 5 | import config 6 | from player import PLAYER_LIST, player 7 | from DBOper import is_player_stored, insert_info, update_DOTA2_match_ID 8 | from common import steam_id_convert_32_to_64, update_and_send_message_DOTA2, update_and_send_gaming_status 9 | import DOTA2 10 | import message_sender 11 | 12 | 13 | def init(): 14 | # 读取配置文件 15 | player_list = config.PLAYER_LIST 16 | # 读取玩家信息 17 | for i in player_list: 18 | nickname = i[0] 19 | short_steamID = i[1] 20 | print("{}信息读取完毕, ID:{}".format(nickname, short_steamID)) 21 | long_steamID = steam_id_convert_32_to_64(short_steamID) 22 | 23 | try: 24 | last_DOTA2_match_ID = DOTA2.get_last_match_id_by_short_steamID(short_steamID) 25 | except DOTA2.DOTA2HTTPError: 26 | last_DOTA2_match_ID = "-1" 27 | 28 | # 如果数据库中没有这个人的信息, 则进行数据库插入 29 | if not is_player_stored(short_steamID): 30 | # 插入数据库 31 | insert_info(short_steamID, long_steamID, nickname, last_DOTA2_match_ID) 32 | # 如果有这个人的信息则更新其最新的比赛信息 33 | else: 34 | update_DOTA2_match_ID(short_steamID, last_DOTA2_match_ID) 35 | # 新建一个玩家对象, 放入玩家列表 36 | temp_player = player(short_steamID=short_steamID, 37 | long_steamID=long_steamID, 38 | nickname=nickname, 39 | last_DOTA2_match_ID=last_DOTA2_match_ID) 40 | 41 | PLAYER_LIST.append(temp_player) 42 | 43 | 44 | def update(player_num: int): 45 | if config.ENABLE_STEAM_WATCHER: 46 | update_and_send_gaming_status() 47 | update_and_send_message_DOTA2() 48 | # dota每日请求限制100,000次 49 | # 每个人假设每次更新都需要请求两次 50 | # 所以请求间隔可以设置为 (24 * 60 * 60 / (100000 / (2 * player_num))) 51 | # 10个人的情况下, 会17秒更新一次信息 52 | # 但是其实每分钟更新一次即可保证及时 53 | if player_num >= 30: 54 | time.sleep((24 * 60 * 60) / (100000 / (2 * player_num))) 55 | else: 56 | time.sleep(60) 57 | 58 | 59 | def main(): 60 | if init() != -1: 61 | print("初始化完成, 开始更新比赛信息") 62 | while True: 63 | player_num = len(PLAYER_LIST) 64 | if player_num == 0: 65 | return 66 | update(player_num=player_num) 67 | 68 | 69 | if __name__ == '__main__': 70 | main() 71 | -------------------------------------------------------------------------------- /DOTA2_dicts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | # 可以在这里添加新的阴阳怪气, {}为昵称位置 4 | 5 | # 单排的阴阳怪气 6 | WIN_NEGATIVE_SOLO = [ 7 | '{}侥幸赢得了比赛', 8 | '{}走狗屎运赢得了比赛', 9 | '{}躺赢了比赛', 10 | '{}打团都没来, 队友4V5赢得了比赛' 11 | ] 12 | 13 | WIN_POSTIVE_SOLO = [ 14 | '{}带领团队走向了胜利', 15 | '{}暴打对面后赢得了胜利', 16 | '{} CARRY全场赢得了胜利', 17 | '{}把对面当猪宰了, 赢得了胜利', 18 | '{}又赢了, 这游戏就是这么枯燥, 且乏味', 19 | ] 20 | 21 | LOSE_NEGATIVE_SOLO = [ 22 | '{}被人按在地上摩擦, 输掉了这场比赛', 23 | '{}悲惨地输掉了比赛', 24 | '{}头都被打歪了, 心态爆炸地输掉了比赛', 25 | '{}捕鱼被鱼吃了, 输掉了比赛', 26 | '{}打的是个几把' 27 | ] 28 | 29 | LOSE_POSTIVE_SOLO = [ 30 | '{}无力回天输掉了比赛', 31 | '{}尽力了, 但还是输了比赛', 32 | '{}背靠世界树, 虽败犹荣', 33 | '{}带不动队友, 输了比赛', 34 | '{}又输了, 很难受, 宁愿输的是我', 35 | ] 36 | 37 | # 组排的阴阳怪气 38 | WIN_NEGATIVE_PARTY = [ 39 | '{}侥幸赢得了比赛', 40 | '{}走狗屎运赢得了比赛', 41 | '{}躺赢了比赛', 42 | '{}打团都没来, 队友4V5赢得了比赛' 43 | ] 44 | 45 | WIN_POSTIVE_PARTY = [ 46 | '{}带领团队走向了胜利', 47 | '{}暴打对面后赢得了胜利', 48 | '{} CARRY全场赢得了胜利', 49 | '{}把对面当猪宰了, 赢得了胜利', 50 | '{}又赢了, 这游戏就是这么枯燥, 且乏味', 51 | ] 52 | 53 | LOSE_NEGATIVE_PARTY = [ 54 | '{}被人按在地上摩擦, 输掉了这场比赛', 55 | '{}悲惨地输掉了比赛', 56 | '{}头都被打歪了, 心态爆炸地输掉了比赛', 57 | '{}捕鱼被鱼吃了, 输掉了比赛', 58 | '{}打的是个几把' 59 | ] 60 | 61 | LOSE_POSTIVE_PARTY = [ 62 | '{}无力回天输掉了比赛', 63 | '{}尽力了, 但还是输了比赛', 64 | '{}背靠世界树, 虽败犹荣', 65 | '{}带不动队友, 输了比赛', 66 | '{}又输了, 很难受, 宁愿输的是我', 67 | ] 68 | 69 | GAME_MODE = { 70 | 0: "No Game Mode", 71 | 1: "全英雄选择", 72 | 2: "队长模式", 73 | 3: "随机征召", 74 | 4: "小黑屋", 75 | 5: "全部随机", 76 | 7: "万圣节活动", 77 | 8: "反队长模式", 78 | 9: "贪魔活动", 79 | 10: "教程", 80 | 11: "中路模式", 81 | 12: "生疏模式", 82 | 13: "新手模式", 83 | 14: "Compendium Matchmaking", 84 | 15: "自定义游戏", 85 | 16: "队长征召", 86 | 17: "平衡征召", 87 | 18: "技能征召", 88 | 19: "活动模式", 89 | 20: "全英雄死亡随机", 90 | 21: "中路SOLO", 91 | 22: "全英雄选择", 92 | 23: "加速模式" 93 | } 94 | 95 | 96 | LOBBY = { 97 | -1: "非法ID", 98 | 0: "普通匹配", 99 | 1: "练习", 100 | 2: "锦标赛", 101 | 3: "教程", 102 | 4: "合作对抗电脑", 103 | 5: "组排模式", 104 | 6: "单排模式", 105 | 7: "天梯匹配", 106 | 8: "中路SOLO", 107 | 12: "夜魇暗潮" 108 | } 109 | 110 | 111 | # 服务器ID列表 112 | AREA_CODE = { 113 | 111: "美国西部", 114 | 112: "美国西部", 115 | 114: "美国西部", 116 | 121: "美国东部", 117 | 122: "美国东部", 118 | 123: "美国东部", 119 | 124: "美国东部", 120 | 131: "欧洲西部", 121 | 132: "欧洲西部", 122 | 133: "欧洲西部", 123 | 134: "欧洲西部", 124 | 135: "欧洲西部", 125 | 136: "欧洲西部", 126 | 142: "南韩", 127 | 143: "南韩", 128 | 151: "东南亚", 129 | 152: "东南亚", 130 | 153: "东南亚", 131 | 161: "中国", 132 | 163: "中国", 133 | 171: "澳大利亚", 134 | 181: "俄罗斯", 135 | 182: "俄罗斯", 136 | 183: "俄罗斯", 137 | 184: "俄罗斯", 138 | 185: "俄罗斯", 139 | 186: "俄罗斯", 140 | 191: "欧洲东部", 141 | 192: "欧洲东部", 142 | 200: "南美洲", 143 | 202: "南美洲", 144 | 203: "南美洲", 145 | 204: "南美洲", 146 | 211: "非洲南部", 147 | 212: "非洲南部", 148 | 213: "非洲南部", 149 | 221: "中国", 150 | 222: "中国", 151 | 223: "中国", 152 | 224: "中国", 153 | 225: "中国", 154 | 231: "中国", 155 | 236: "中国", 156 | 242: "智利", 157 | 251: "秘鲁", 158 | 261: "印度" 159 | } 160 | 161 | 162 | # 英雄昵称 163 | # 每个英雄的第一个为游戏内默认名字 164 | HEROES_LIST_CHINESE = { 165 | 1: ['敌法师', '敌法', 'AM'], 166 | 2: ['斧王'], 167 | 3: ['祸乱之源', '祸乱', '水桶腰'], 168 | 4: ['血魔'], 169 | 5: ['水晶室女', '冰女', 'CM'], 170 | 6: ['卓尔游侠', '小黑'], 171 | 7: ['撼地者', '小牛'], 172 | 8: ['主宰', '剑圣', 'jugg', '奶棒人'], 173 | 9: ['米拉娜', '白虎', 'pom'], 174 | 10: ['变体精灵', '水人'], 175 | 11: ['影魔', '影魔王', 'SF', '影儿魔儿', 'PIS'], 176 | 12: ['幻影长矛手', 'PL'], 177 | 13: ['帕克'], 178 | 14: ['帕吉', '屠夫', '扒鸡', '啪唧'], 179 | 15: ['剃刀', '电魂', '电棍'], 180 | 16: ['沙王', 'SK'], 181 | 17: ['风暴之灵', '蓝猫'], 182 | 18: ['斯温', '流浪剑客', '流浪'], 183 | 19: ['小小'], 184 | 20: ['复仇之魂', '复仇', 'VS'], 185 | 21: ['风行者', '风行', 'WR'], 186 | 22: ['宙斯'], 187 | 23: ['昆卡', '船长'], 188 | 25: ['莉娜', '火女', 'Ori', '曾焦阳'], 189 | 26: ['莱恩', '恶魔巫师', 'Lion'], 190 | 27: ['暗影萨满', '小Y', '小歪'], 191 | 28: ['斯拉达', '大鱼', '大鱼人'], 192 | 29: ['潮汐猎人', '潮汐', '西瓜皮'], 193 | 30: ['巫医'], 194 | 31: ['巫妖'], 195 | 32: ['力丸', '隐形刺客', '隐刺'], 196 | 33: ['谜团'], 197 | 34: ['修补匠', 'TK', 'Tinker'], 198 | 35: ['狙击手', '矮人火枪手', '火枪', '传说哥'], 199 | 36: ['瘟疫法师', '死灵法', 'NEC'], 200 | 37: ['术士'], 201 | 38: ['兽王'], 202 | 39: ['痛苦女王', '女王', 'QOP'], 203 | 40: ['剧毒术士', '剧毒'], 204 | 41: ['虚空假面', '虚空', 'JB脸'], 205 | 42: ['冥魂大帝', '骷髅王'], 206 | 43: ['死亡先知', 'DP'], 207 | 44: ['幻影刺客', '幻刺', 'PA'], 208 | 45: ['帕格纳', '骨法', '湮灭法师'], 209 | 46: ['圣堂刺客', '圣堂', 'TA'], 210 | 47: ['冥界亚龙', '毒龙', 'Viper'], 211 | 48: ['露娜', '月骑', 'Luna'], 212 | 49: ['龙骑士', '龙骑'], 213 | 50: ['戴泽', '暗影牧师', '暗牧'], 214 | 51: ['发条技师', '发条'], 215 | 52: ['拉席克', '老鹿'], 216 | 53: ['先知'], 217 | 54: ['噬魂鬼', '小狗'], 218 | 55: ['黑暗贤者', '黑贤'], 219 | 56: ['克林克兹', '小骷髅'], 220 | 57: ['全能骑士', '全能'], 221 | 58: ['魅惑魔女', '小鹿'], 222 | 59: ['哈斯卡', '神灵', '神灵武士'], 223 | 60: ['暗夜魔王', '夜魔'], 224 | 61: ['育母蜘蛛', '蜘蛛'], 225 | 62: ['赏金猎人', '赏金'], 226 | 63: ['编织者', '蚂蚁'], 227 | 64: ['杰奇洛', '双头龙'], 228 | 65: ['蝙蝠骑士', '蝙蝠'], 229 | 66: ['陈', '老陈'], 230 | 67: ['幽鬼', 'SPE'], 231 | 68: ['远古冰魄', '冰魂'], 232 | 69: ['末日使者', '末日', 'Doom'], 233 | 70: ['熊战士', '拍拍', '拍拍熊'], 234 | 71: ['裂魂人', '白牛'], 235 | 72: ['矮人直升机', '飞机'], 236 | 73: ['炼金术士', '炼金'], 237 | 74: ['祈求者', '卡尔'], 238 | 75: ['沉默术士', '沉默'], 239 | 76: ['殁境神蚀者', '黑鸟'], 240 | 77: ['狼人'], 241 | 78: ['酒仙', '熊猫', '熊猫酒仙'], 242 | 79: ['暗影恶魔', '毒狗'], 243 | 80: ['德鲁伊', '熊德'], 244 | 81: ['混沌骑士', '混沌', 'CK'], 245 | 82: ['米波'], 246 | 83: ['树精卫士', '大树', '树精'], 247 | 84: ['食人魔魔法师', '蓝胖'], 248 | 85: ['不朽尸王', '尸王'], 249 | 86: ['拉比克'], 250 | 87: ['干扰者', '萨尔'], 251 | 88: ['司夜刺客', '小强'], 252 | 89: ['娜迦海妖', '小娜迦'], 253 | 90: ['光之守卫', '光法'], 254 | 91: ['艾欧', '小精灵'], 255 | 92: ['维萨吉', '死灵龙', '死灵飞龙'], 256 | 93: ['斯拉克', '小鱼', '小鱼人'], 257 | 94: ['美杜莎', '一姐'], 258 | 95: ['巨魔战将', '巨魔', '巨馍蘸酱'], 259 | 96: ['半人马战行者', '人马'], 260 | 97: ['马格纳斯', '猛犸'], 261 | 98: ['伐木机'], 262 | 99: ['钢背兽', '钢背'], 263 | 100: ['巨牙海民', '海民'], 264 | 101: ['天怒法师', '天怒'], 265 | 102: ['亚巴顿'], 266 | 103: ['上古巨神', '大牛'], 267 | 104: ['军团指挥官', '军团'], 268 | 105: ['工程师', '炸弹', '炸弹人'], 269 | 106: ['灰烬之灵', '火猫'], 270 | 107: ['大地之灵', '土猫'], 271 | 108: ['孽主', '大屁股'], 272 | 109: ['恐怖利刃', 'TB'], 273 | 110: ['凤凰'], 274 | 111: ['神谕者', '神谕'], 275 | 112: ['寒冬飞龙', '冰龙'], 276 | 113: ['天穹守望者', '电狗'], 277 | 114: ['齐天大圣', '大圣'], 278 | 119: ['邪影芳灵', '小仙女'], 279 | 120: ['石鳞剑士', '滚滚'], 280 | 121: ['天涯墨客', '墨客'], 281 | 123: ['森海飞霞', '松鼠', '小松鼠'], 282 | 126: ['虚无之灵', '紫猫'], 283 | 128: ['电炎绝手', '老奶奶'], 284 | 129: ['玛尔斯'], 285 | 135: ['破晓辰星'], 286 | } -------------------------------------------------------------------------------- /DOTA2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | import requests 4 | from DOTA2_dicts import * 5 | from player import player 6 | import random 7 | import time 8 | from typing import Dict 9 | from config import API_KEY, ENABLE_URL, DEFAULT_NAME_ONLY 10 | 11 | 12 | # 异常处理 13 | class DOTA2HTTPError(Exception): 14 | pass 15 | 16 | 17 | # 根据slot判断队伍, 返回1为天辉, 2为夜魇 18 | def get_team_by_slot(slot: int) -> int: 19 | if slot < 100: 20 | return 1 21 | else: 22 | return 2 23 | 24 | 25 | def get_last_match_id_by_short_steamID(short_steamID: int) -> int: 26 | # get match_id 27 | url = 'https://api.steampowered.com/IDOTA2Match_570/GetMatchHistory/v001/?key={}' \ 28 | '&account_id={}&matches_requested=1'.format(API_KEY, short_steamID) 29 | try: 30 | response = requests.get(url) 31 | except requests.RequestException: 32 | raise DOTA2HTTPError("Requests Error") 33 | if response.status_code >= 400: 34 | if response.status_code == 401: 35 | raise DOTA2HTTPError("Unauthorized request 401. Verify API key.") 36 | if response.status_code == 503: 37 | raise DOTA2HTTPError("The server is busy or you exceeded limits. Please wait 30s and try again.") 38 | raise DOTA2HTTPError("Failed to retrieve data: %s. URL: %s" % (response.status_code, url)) 39 | 40 | match = response.json() 41 | try: 42 | match_id = match["result"]["matches"][0]["match_id"] 43 | except KeyError: 44 | raise DOTA2HTTPError("Response Error: Key Error") 45 | except IndexError: 46 | raise DOTA2HTTPError("Response Error: Index Error") 47 | return match_id 48 | 49 | 50 | def get_match_detail_info(match_id: int) -> Dict: 51 | # get match detail 52 | url = 'https://api.steampowered.com/IDOTA2Match_570/GetMatchDetails/V001/' \ 53 | '?key={}&match_id={}'.format(API_KEY, match_id) 54 | try: 55 | response = requests.get(url) 56 | except requests.RequestException: 57 | raise DOTA2HTTPError("Requests Error") 58 | if response.status_code >= 400: 59 | if response.status_code == 401: 60 | raise DOTA2HTTPError("Unauthorized request 401. Verify API key.") 61 | if response.status_code == 503: 62 | raise DOTA2HTTPError("The server is busy or you exceeded limits. Please wait 30s and try again.") 63 | raise DOTA2HTTPError("Failed to retrieve data: %s. URL: %s" % (response.status_code, url)) 64 | 65 | match = response.json() 66 | try: 67 | match_info = match["result"] 68 | except KeyError: 69 | raise DOTA2HTTPError("Response Error: Key Error") 70 | except IndexError: 71 | raise DOTA2HTTPError("Response Error: Index Error") 72 | 73 | return match_info 74 | 75 | 76 | # 接收某局比赛的玩家列表, 生成比赛战报 77 | # 参数为玩家对象列表和比赛ID 78 | def generate_match_message(match_id: int, player_list: [player]): 79 | try: 80 | match = get_match_detail_info(match_id=match_id) 81 | except DOTA2HTTPError: 82 | return "DOTA2比赛战报生成失败" 83 | 84 | start_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(match['start_time'])) 85 | duration = match['duration'] 86 | 87 | # 比赛模式 88 | mode_id = match["game_mode"] 89 | mode = GAME_MODE.get(mode_id, '未知') 90 | 91 | lobby_id = match['lobby_type'] 92 | lobby = LOBBY.get(lobby_id, '未知') 93 | 94 | player_num = len(player_list) 95 | nicknames = ','.join([player_list[i].nickname for i in range(-player_num, -1)]) 96 | if nicknames: 97 | nicknames += '和' 98 | nicknames += player_list[-1].nickname 99 | 100 | # 更新玩家对象的比赛信息 101 | for i in player_list: 102 | for j in match['players']: 103 | if i.short_steamID == j['account_id']: 104 | i.dota2_kill = j['kills'] 105 | i.dota2_death = j['deaths'] 106 | i.dota2_assist = j['assists'] 107 | i.kda = ((1. * i.dota2_kill + i.dota2_assist) / i.dota2_death) \ 108 | if i.dota2_death != 0 else (1. * i.dota2_kill + i.dota2_assist) 109 | 110 | i.dota2_team = get_team_by_slot(j['player_slot']) 111 | i.hero = j['hero_id'] 112 | i.last_hit = j['last_hits'] 113 | i.damage = j['hero_damage'] 114 | i.gpm = j['gold_per_min'] 115 | i.xpm = j['xp_per_min'] 116 | break 117 | 118 | team = player_list[0].dota2_team 119 | win = match['radiant_win'] == (team == 1) 120 | 121 | if mode_id in (15, 19): # 各种活动模式仅简单通报 122 | return '{}玩了一把[{}/{}],开始于{},持续{}分{}秒,看起来好像是{}了。'.format( 123 | nicknames, mode, lobby, start_time, duration // 60, duration % 60, "赢" if win else "输" 124 | ) 125 | # 队伍信息 126 | team_damage = 0 127 | team_kills = 0 128 | team_deaths = 0 129 | for i in match['players']: 130 | if get_team_by_slot(i['player_slot']) == team: 131 | team_damage += i['hero_damage'] 132 | team_kills += i['kills'] 133 | team_deaths += i['deaths'] 134 | 135 | top_kda = 0 136 | for i in player_list: 137 | if i.kda > top_kda: 138 | top_kda = i.kda 139 | 140 | if (win and top_kda > 10) or (not win and top_kda > 6): 141 | postive = True 142 | elif (win and top_kda < 4) or (not win and top_kda < 1): 143 | postive = False 144 | else: 145 | if random.randint(0, 1) == 0: 146 | postive = True 147 | else: 148 | postive = False 149 | 150 | tosend = [] 151 | if win and postive: 152 | tosend.append(random.choice(WIN_POSTIVE_PARTY).format(nicknames)) 153 | elif win and not postive: 154 | tosend.append(random.choice(WIN_NEGATIVE_PARTY).format(nicknames)) 155 | elif not win and postive: 156 | tosend.append(random.choice(LOSE_POSTIVE_PARTY).format(nicknames)) 157 | else: 158 | tosend.append(random.choice(LOSE_NEGATIVE_PARTY).format(nicknames)) 159 | 160 | tosend.append('开始时间: {}'.format(start_time)) 161 | tosend.append('持续时间: {}分{}秒'.format(duration // 60, duration % 60)) 162 | tosend.append('游戏模式: [{}/{}]'.format(mode, lobby)) 163 | 164 | for i in player_list: 165 | nickname = i.nickname 166 | if i.hero in HEROES_LIST_CHINESE: 167 | if DEFAULT_NAME_ONLY: 168 | hero = HEROES_LIST_CHINESE[i.hero][0] 169 | else: 170 | hero = random.choice(HEROES_LIST_CHINESE[i.hero]) 171 | else: 172 | hero = '不知道什么鬼' 173 | kda = i.kda 174 | last_hits = i.last_hit 175 | damage = i.damage 176 | kills, deaths, assists = i.dota2_kill, i.dota2_death, i.dota2_assist 177 | gpm, xpm = i.gpm, i.xpm 178 | 179 | damage_rate = 0 if team_damage == 0 else (100 * (float(damage) / team_damage)) 180 | participation = 0 if team_kills == 0 else (100 * float(kills + assists) / team_kills) 181 | deaths_rate = 0 if team_deaths == 0 else (100 * float(deaths) / team_deaths) 182 | 183 | tosend.append( 184 | '{}使用{}, KDA: {:.2f}[{}/{}/{}], GPM/XPM: {}/{}, ' \ 185 | '补刀数: {}, 总伤害: {}({:.2f}%), 参战率: {:.2f}%, 参葬率: {:.2f}%' \ 186 | .format(nickname, hero, kda, kills, deaths, assists, gpm, xpm, last_hits, 187 | damage, damage_rate, participation, deaths_rate) 188 | ) 189 | 190 | if ENABLE_URL: 191 | tosend.append('战绩详情: https://zh.dotabuff.com/matches/{}'.format(match_id)) 192 | 193 | return '\n'.join(tosend) 194 | --------------------------------------------------------------------------------