├── playerInfo ├── running_image ├── IMG_0079.PNG └── IMG_0080.png ├── list.json ├── README.md ├── message_sender.py ├── CSGO_dicts.py ├── DBOper.py ├── player.py ├── run.py ├── common.py ├── DOTA2_dicts.py ├── CSGO.py └── DOTA2.py /playerInfo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greenhaha/dota2_csgo_watcher_bot/HEAD/playerInfo -------------------------------------------------------------------------------- /running_image/IMG_0079.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greenhaha/dota2_csgo_watcher_bot/HEAD/running_image/IMG_0079.PNG -------------------------------------------------------------------------------- /running_image/IMG_0080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greenhaha/dota2_csgo_watcher_bot/HEAD/running_image/IMG_0080.png -------------------------------------------------------------------------------- /list.json: -------------------------------------------------------------------------------- 1 | { 2 | "教练": 139136624, 3 | "双哥": 168521852, 4 | "小辣椒": 194034387, 5 | "皮卡": 172524904, 6 | "小猪": 118149534, 7 | "小猪2号": 165973069, 8 | "竹子": 139270719, 9 | "小贝": 370271336, 10 | "D哥": 353690688, 11 | "小楷": 290605070, 12 | "v哥": 221298131, 13 | "崩哥": 165652483, 14 | "清清": 102735759, 15 | "清清2号": 181110287 16 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSGO和DOTA2的处刑BOT 2 | 3 | ## 介绍 4 | 在群友打完一把游戏后, bot会向群里更新这局比赛的数据 5 | 6 | DOTA2的数据来自于V社的官方API, 每日请求数限制100,000次 7 | 8 | CSGO的数据则来自于[完美官方APP](https://pvp.wanmei.com/appdownload-dota2/index.html) 9 | 10 | YYGQ的文来自于[dota2_watcher](https://github.com/unilink233/dota2_watcher) 11 | 12 | 有任何建议可以发issue, 随缘更新 13 | 14 | ## 安装指南 15 | 16 | - 下载对应版本的[miraiOK](https://github.com/LXY1226/MiraiOK), 有hxd说下不动, 我传了个Linux64版本的[度盘](链接: https://pan.baidu.com/s/1bLYwWWHCcgmnLHoofXTHxQ) 提取码: 5trx 17 | 18 | - 运行一下miraiOK, 然后关闭, 会自动生成一个`plugins`文件夹 19 | 20 | - 把[mirai-http-api](https://github.com/project-mirai/mirai-api-http)里的release的jar扔进plugins文件夹 21 | 22 | - 启动miraiOK, 登陆你的BOT账号 23 | 24 | - 在`list.json`中加入你要偷窥的群友账号和昵称 25 | 26 | - 修改`message_sender.py`中的BOT配置 27 | 28 | - 在[这里](http://steamcommunity.com/dev/apikey)申请你的steam API key, 修改`DOTA2.py`中的`api_key` 29 | 30 | - 运行`run.py`脚本来启动BOT 31 | 32 | - 走过路过点个star吧 33 | 34 | ## 后续计划 35 | 36 | - [ ] 搞个一键安装, 方便部署 37 | 38 | - [ ] 丰富YYGQ内容(大家可以直接提交, 我会合并分支) 39 | 40 | - [ ] 更详细的CSGO数据 41 | 42 | ## 运行效果 43 | 44 | ### CSGO: 45 | 46 | ![CSGO](./running_image/IMG_0079.PNG) 47 | 48 | ### DOTA2: 49 | 50 | ![DOTA2](./running_image/IMG_0080.png) 51 | 52 | ## 免责声明 53 | 54 | 由于CSGO的数据来源于完美非公开的API, 所以如果完美方面不允许调用此API的话请发送邮件到`1nv0k3r@protonmail.com`来联系我删除CSGO功能 -------------------------------------------------------------------------------- /message_sender.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | import requests, json 4 | url = "http://127.0.0.1:8080" 5 | # 群号 6 | target = 1111111111 7 | # bot的QQ号 8 | bot_qq = 1111111111 9 | 10 | 11 | def message(m): 12 | # Authorize 13 | auth_key = {"authKey": "xxxxxxxxx"} 14 | r = requests.post(url + "/auth", json.dumps(auth_key)) 15 | if json.loads(r.text).get('code') != 0: 16 | print("ERROR@auth") 17 | print(r.text) 18 | exit(1) 19 | # Verify 20 | session_key = json.loads(r.text).get('session') 21 | session = {"sessionKey": session_key, "qq": bot_qq} 22 | r = requests.post(url + "/verify", json.dumps(session)) 23 | if json.loads(r.text).get('code') != 0: 24 | print("ERROR@verify") 25 | print(r.text) 26 | exit(2) 27 | data = { 28 | "sessionKey": session_key, 29 | "target": target, 30 | "messageChain": [ 31 | {"type": "Plain", "text": m} 32 | ] 33 | } 34 | r = requests.post(url + "/sendGroupMessage", json.dumps(data)) 35 | # release 36 | data = { 37 | "sessionKey": session_key, 38 | "qq": bot_qq 39 | } 40 | r = requests.post(url + "/release", json.dumps(data)) 41 | print(r.text) 42 | -------------------------------------------------------------------------------- /CSGO_dicts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | # 可以在这里添加新的阴阳怪气, {}为昵称位置 4 | 5 | # 单排的阴阳怪气 6 | WIN_NEGATIVE_SOLO = [ 7 | '{}侥幸赢得了比赛', 8 | '{}走狗屎运赢得了比赛', 9 | '{}躺赢了比赛', 10 | ] 11 | 12 | WIN_POSTIVE_SOLO = [ 13 | '{}带领团队走向了胜利', 14 | '{}暴打对面后赢得了胜利', 15 | '{} CARRY全场赢得了胜利', 16 | '{}把对面当猪宰了, 赢得了胜利.', 17 | '{}又赢了, 这游戏就是这么枯燥, 且乏味.', 18 | ] 19 | 20 | LOSE_NEGATIVE_SOLO = [ 21 | '{}被人按在地上摩擦, 输掉了这场比赛', 22 | '{}悲惨地输掉了比赛', 23 | '{}头都被打歪了, 心态爆炸地输掉了比赛.', 24 | '{}捕鱼被鱼吃了, 输掉了比赛.', 25 | '{}打的是个几把.' 26 | ] 27 | 28 | LOSE_POSTIVE_SOLO = [ 29 | '{}无力回天输掉了比赛.', 30 | '{}尽力了, 但还是输了比赛.', 31 | '{}Carry全场, 虽败犹荣.', 32 | '{}带不动队友, 输了比赛', 33 | '{}又输了, 很难受, 宁愿输的是我.', 34 | ] 35 | 36 | # 组排的阴阳怪气 37 | WIN_NEGATIVE_PARTY = [ 38 | '{}侥幸赢得了比赛', 39 | '{}走狗屎运赢得了比赛', 40 | '{}躺赢了比赛', 41 | '{}打团都没来, 队友4V5赢得了比赛.' 42 | ] 43 | 44 | WIN_POSTIVE_PARTY = [ 45 | '{}带领团队走向了胜利', 46 | '{}暴打对面后赢得了胜利', 47 | '{} CARRY全场赢得了胜利', 48 | '{}把对面当猪宰了, 赢得了胜利.', 49 | '{}又赢了, 这游戏就是这么枯燥, 且乏味.', 50 | ] 51 | 52 | LOSE_NEGATIVE_PARTY = [ 53 | '{}被人按在地上摩擦, 输掉了这场比赛', 54 | '{}悲惨地输掉了比赛', 55 | '{}头都被打歪了, 心态爆炸地输掉了比赛.', 56 | '{}捕鱼被鱼吃了, 输掉了比赛.', 57 | '{}打的是个几把.' 58 | ] 59 | 60 | LOSE_POSTIVE_PARTY = [ 61 | '{}无力回天输掉了比赛.', 62 | '{}尽力了, 但还是输了比赛.', 63 | '{}背靠世界树, 虽败犹荣.', 64 | '{}带不动队友, 输了比赛', 65 | '{}又输了, 很难受, 宁愿输的是我.', 66 | ] -------------------------------------------------------------------------------- /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_CSGO_match_ID=row[5], 16 | last_DOTA2_match_ID=row[6]) 17 | player_obj.CSGO_rank = row[3] 18 | player_obj.DOTA2_score = row[4] 19 | PLAYER_LIST.append(player_obj) 20 | 21 | 22 | def update_CSGO_match_ID(short_steamID, last_CSGO_match_ID): 23 | c.execute("UPDATE playerInfo SET last_CSGO_match_ID='{}' " 24 | "WHERE short_steamID={}".format(last_CSGO_match_ID, short_steamID)) 25 | conn.commit() 26 | 27 | 28 | def update_DOTA2_match_ID(short_steamID, last_DOTA2_match_ID): 29 | c.execute("UPDATE playerInfo SET last_DOTA2_match_ID='{}' " 30 | "WHERE short_steamID={}".format(short_steamID, last_DOTA2_match_ID)) 31 | conn.commit() 32 | 33 | 34 | def insert_info(short_steamID, long_steamID, nickname, last_CSGO_match_ID, last_DOTA2_match_ID): 35 | c.execute("INSERT INTO playerInfo (short_steamID, long_steamID, nickname, last_CSGO_match_ID, last_DOTA2_match_ID) " 36 | "VALUES ({}, {}, '{}', '{}', '{}')" 37 | .format(short_steamID, long_steamID, nickname, last_CSGO_match_ID, last_DOTA2_match_ID)) 38 | conn.commit() 39 | 40 | 41 | def is_player_stored(short_steamID): 42 | c.execute("SELECT * FROM playerInfo WHERE short_steamID=={}".format(short_steamID)) 43 | if len(c.fetchall()) == 0: 44 | return False 45 | return True 46 | 47 | -------------------------------------------------------------------------------- /player.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | class player: 4 | # 基本属性 5 | short_steamID = 0 6 | long_steamID = 0 7 | nickname = '' 8 | CSGO_rank = '' 9 | DOTA2_score = '' 10 | last_CSGO_match_ID = 0 11 | last_DOTA2_match_ID = 0 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 | # CSGO专属 28 | csgo_kill = 0 29 | csgo_death = 0 30 | csgo_assist = 0 31 | csgo_rating = 0 32 | # team分为1和2 33 | csgo_team = 1 34 | csgo_win_team = 1 35 | # 比分 36 | csgo_team_1_score = 0 37 | csgo_team_2_score = 0 38 | # 地图和模式 39 | csgo_map = '' 40 | csgo_mode = '' 41 | # 时间 42 | csgo_start_time = '' 43 | 44 | def __init__(self, nickname, short_steamID, long_steamID, last_CSGO_match_ID, last_DOTA2_match_ID): 45 | self.nickname = nickname 46 | self.short_steamID = short_steamID 47 | self.long_steamID = long_steamID 48 | self.last_DOTA2_match_ID = last_DOTA2_match_ID 49 | self.last_CSGO_match_ID = last_CSGO_match_ID 50 | 51 | # csgo的数据记录 52 | def csgo_data_set(self, match_info): 53 | self.csgo_kill = match_info['kill'] 54 | self.csgo_death = match_info['death'] 55 | self.csgo_assist = match_info['assist'] 56 | self.csgo_rating = match_info['rating'] 57 | self.csgo_team = match_info['team'] 58 | self.csgo_win_team = match_info['winTeam'] 59 | self.csgo_team_1_score = match_info['score1'] 60 | self.csgo_team_2_score = match_info['score2'] 61 | self.csgo_map = match_info['mapName'] 62 | self.csgo_mode = match_info['mode'] 63 | self.csgo_start_time = match_info['startTime'] 64 | 65 | 66 | PLAYER_LIST = [] 67 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | import time 4 | from common import update_and_send_message_CSGO, update_and_send_message_DOTA2, steam_id_convert_32_to_64 5 | import json 6 | from player import PLAYER_LIST, player 7 | from DBOper import is_player_stored, insert_info, update_CSGO_match_ID, update_DOTA2_match_ID 8 | import CSGO 9 | import DOTA2 10 | 11 | 12 | def init(): 13 | for nickname, short_steamID in json.load(open("list.json", "r")).items(): 14 | 15 | long_steamID = steam_id_convert_32_to_64(short_steamID) 16 | try: 17 | last_CSGO_match_info = CSGO.get_last_match_by_long_steamID(long_steamID) 18 | last_CSGO_match_ID = last_CSGO_match_info["matchId"] 19 | except CSGO.CSGOHTTPError: # 这人没打过CSGO 20 | last_CSGO_match_ID = "-1" 21 | last_CSGO_match_info = "-1" 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_CSGO_match_ID, last_DOTA2_match_ID) 32 | # 如果有这个人的信息则更新其最新的比赛信息 33 | else: 34 | update_CSGO_match_ID(short_steamID, last_CSGO_match_ID) 35 | update_DOTA2_match_ID(short_steamID, last_DOTA2_match_ID) 36 | # 新建一个玩家对象, 放入玩家列表 37 | temp_player = player(short_steamID=short_steamID, 38 | long_steamID=long_steamID, 39 | nickname=nickname, 40 | last_CSGO_match_ID=last_CSGO_match_ID, 41 | last_DOTA2_match_ID=last_DOTA2_match_ID) 42 | if last_CSGO_match_info != "-1": 43 | temp_player.csgo_data_set(last_CSGO_match_info) 44 | 45 | PLAYER_LIST.append(temp_player) 46 | 47 | 48 | def update(): 49 | update_and_send_message_CSGO() 50 | update_and_send_message_DOTA2() 51 | time.sleep(10 * 60) 52 | 53 | 54 | def main(): 55 | init() 56 | while True: 57 | update() 58 | 59 | 60 | if __name__ == '__main__': 61 | main() 62 | -------------------------------------------------------------------------------- /common.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | import CSGO 4 | import DOTA2 5 | from DBOper import update_CSGO_match_ID, update_DOTA2_match_ID 6 | from player import PLAYER_LIST 7 | 8 | 9 | def steam_id_convert_32_to_64(short_steamID): 10 | return short_steamID + 76561197960265728 11 | 12 | 13 | def steam_id_convert_64_to_32(long_steamID): 14 | return long_steamID - 76561197960265728 15 | 16 | 17 | # 返回一个最新比赛变化过的字典 18 | # 格式: { match_id1: [player1, player2, player3], match_id2: [player1, player2]} 19 | def update_CSGO(): 20 | result = {} 21 | for i in PLAYER_LIST: 22 | try: 23 | match_info = CSGO.get_last_match_by_long_steamID(i.long_steamID) 24 | except CSGO.CSGOHTTPError: 25 | continue 26 | match_id = match_info['matchId'] 27 | if match_id != i.last_CSGO_match_ID: 28 | # 把最新一局的比赛信息进行更新 29 | i.csgo_data_set(match_info) 30 | if result.get(match_id, 0) != 0: 31 | result[match_id].append(i) 32 | else: 33 | result.update({match_id: [i]}) 34 | # 更新数据库的last_CSGO_match_id字段 35 | update_CSGO_match_ID(i.short_steamID, match_id) 36 | # 更新列表 37 | i.last_CSGO_match_ID = match_id 38 | return result 39 | 40 | 41 | def update_and_send_message_CSGO(): 42 | # 格式: { match_id1: [player1, player2, player3], match_id2: [player1, player2]} 43 | result = update_CSGO() 44 | for match_id in result: 45 | if len(result[match_id]) > 1: 46 | CSGO.generate_party_message(player_list=result[match_id]) 47 | elif len(result[match_id]) == 1: 48 | CSGO.generate_solo_message(player_obj=result[match_id][0]) 49 | 50 | 51 | def update_DOTA2(): 52 | result = {} 53 | for i in PLAYER_LIST: 54 | try: 55 | match_id = DOTA2.get_last_match_id_by_short_steamID(i.short_steamID) 56 | except DOTA2.DOTA2HTTPError: 57 | continue 58 | if match_id != i.last_DOTA2_match_ID: 59 | 60 | if result.get(match_id, 0) != 0: 61 | result[match_id].append(i) 62 | else: 63 | result.update({match_id: [i]}) 64 | # 更新数据库的last_DOTA2_match_id字段 65 | update_DOTA2_match_ID(i.short_steamID, match_id) 66 | # 更新列表 67 | i.last_DOTA2_match_ID = match_id 68 | 69 | return result 70 | 71 | 72 | def update_and_send_message_DOTA2(): 73 | # 格式: { match_id1: [player1, player2, player3], match_id2: [player1, player2]} 74 | result = update_DOTA2() 75 | for match_id in result: 76 | if len(result[match_id]) > 1: 77 | DOTA2.generate_party_message(match_id=match_id, player_list=result[match_id]) 78 | elif len(result[match_id]) == 1: 79 | DOTA2.generate_solo_message(match_id=match_id, player_obj=result[match_id][0]) 80 | -------------------------------------------------------------------------------- /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: "OMG", 88 | 19: "活动模式", 89 | 20: "全英雄死亡随机", 90 | 21: "中路SOLO", 91 | 22: "全英雄选择", 92 | 23: "加速模式"} 93 | 94 | 95 | LOBBY = { 96 | -1: "非法ID", 97 | 0: "普通匹配", 98 | 1: "练习", 99 | 2: "锦标赛", 100 | 3: "教程", 101 | 4: "合作对抗电脑", 102 | 5: "组排模式", 103 | 6: "单排模式", 104 | 7: "天梯匹配", 105 | 8: "中路SOLO" 106 | } 107 | 108 | 109 | # 服务器ID列表 110 | AREA_CODE = { 111 | 111: "美国西部", 112 | 112: "美国西部", 113 | 114: "美国西部", 114 | 121: "美国东部", 115 | 122: "美国东部", 116 | 123: "美国东部", 117 | 124: "美国东部", 118 | 131: "欧洲西部", 119 | 132: "欧洲西部", 120 | 133: "欧洲西部", 121 | 134: "欧洲西部", 122 | 135: "欧洲西部", 123 | 136: "欧洲西部", 124 | 142: "南韩", 125 | 143: "南韩", 126 | 151: "东南亚", 127 | 152: "东南亚", 128 | 153: "东南亚", 129 | 161: "中国", 130 | 163: "中国", 131 | 171: "澳大利亚", 132 | 181: "俄罗斯", 133 | 182: "俄罗斯", 134 | 183: "俄罗斯", 135 | 184: "俄罗斯", 136 | 185: "俄罗斯", 137 | 186: "俄罗斯", 138 | 191: "欧洲东部", 139 | 192: "欧洲东部", 140 | 200: "南美洲", 141 | 202: "南美洲", 142 | 203: "南美洲", 143 | 204: "南美洲", 144 | 211: "非洲南部", 145 | 212: "非洲南部", 146 | 213: "非洲南部", 147 | 221: "中国", 148 | 222: "中国", 149 | 223: "中国", 150 | 224: "中国", 151 | 225: "中国", 152 | 231: "中国", 153 | 236: "中国", 154 | 242: "智利", 155 | 251: "秘鲁", 156 | 261: "印度"} 157 | 158 | 159 | # 英雄昵称 160 | HEROES_LIST_CHINESE = { 161 | 1: '敌法师', 162 | 2: '斧王', 163 | 3: '痛苦之源', 164 | 4: '血魔', 165 | 5: '冰女', 166 | 6: '小黑', 167 | 7: '撼地神牛', 168 | 8: '奶棒人', 169 | 9: '白虎', 170 | 10: '水人', 171 | 11: '影魔王', 172 | 12: '幻影长矛手', 173 | 13: '帕克', 174 | 14: '屠夫', 175 | 15: '电魂', 176 | 16: '沙王', 177 | 17: '蓝猫', 178 | 18: '斯温', 179 | 19: '小小', 180 | 20: '复仇之魂', 181 | 21: '风行', 182 | 22: '宙斯', 183 | 23: '船长', 184 | 25: '火女', 185 | 26: '莱恩', 186 | 27: '小歪', 187 | 28: '大鱼', 188 | 29: '潮汐', 189 | 30: '巫医', 190 | 31: '巫妖', 191 | 32: '力丸', 192 | 33: '谜团', 193 | 34: '修补匠', 194 | 35: '火枪', 195 | 36: 'NEC', 196 | 37: '术士', 197 | 38: '兽王', 198 | 39: '女王', 199 | 40: '剧毒', 200 | 41: '虚空', 201 | 42: '骷髅王', 202 | 43: '死亡先知', 203 | 44: '幻影刺客', 204 | 45: '帕格纳', 205 | 46: '圣堂刺客', 206 | 47: '毒龙', 207 | 48: 'Luna', 208 | 49: '龙骑士', 209 | 50: '戴泽', 210 | 51: '发条', 211 | 52: '拉席克', 212 | 53: "先知", 213 | 54: '小狗', 214 | 55: '黑暗贤者', 215 | 56: '克林克兹', 216 | 57: '全能', 217 | 58: '小鹿', 218 | 59: '哈斯卡', 219 | 60: '夜魔', 220 | 61: '蜘蛛', 221 | 62: '赏金', 222 | 63: '蚂蚁', 223 | 64: '双头龙', 224 | 65: '蝙蝠', 225 | 66: '陈', 226 | 67: '幽鬼', 227 | 68: '冰魂', 228 | 69: 'Doom', 229 | 70: '拍拍熊', 230 | 71: '白牛', 231 | 72: '飞机', 232 | 73: '炼金', 233 | 74: '卡尔', 234 | 75: '沉默', 235 | 76: '黑鸟', 236 | 77: '狼人', 237 | 78: '兽王', 238 | 79: '毒狗', 239 | 80: '德鲁伊', 240 | 81: '混沌骑士', 241 | 82: '米波', 242 | 83: '大树', 243 | 84: '蓝胖', 244 | 85: '尸王', 245 | 86: '拉比克', 246 | 87: '萨尔', 247 | 88: '小强', 248 | 89: '小娜迦', 249 | 90: '光法', 250 | 91: '小精灵', 251 | 92: '死灵龙', 252 | 93: '小鱼', 253 | 94: '美杜莎', 254 | 95: '巨馍蘸酱', 255 | 96: '人马', 256 | 97: '猛犸', 257 | 98: '伐木机', 258 | 99: '钢背兽', 259 | 100: '海民', 260 | 101: '天怒', 261 | 102: '亚巴顿', 262 | 103: '大牛', 263 | 104: '军团', 264 | 105: '炸弹人', 265 | 106: '火猫', 266 | 107: '土猫', 267 | 108: '大屁股', 268 | 109: '恐怖利刃', 269 | 110: '凤凰', 270 | 111: '神谕者', 271 | 112: '冰龙', 272 | 113: '电狗', 273 | 114: '大圣', 274 | 119: '小仙女', 275 | 120: '滚滚', 276 | 121: '墨客', 277 | 126: '紫猫', 278 | 128: '老奶奶', 279 | 129: '马尔斯' 280 | } -------------------------------------------------------------------------------- /CSGO.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | import requests 4 | import hashlib 5 | import json 6 | import random 7 | from CSGO_dicts import * 8 | import message_sender 9 | from player import player 10 | 11 | 12 | class CSGOHTTPError(Exception): 13 | pass 14 | 15 | 16 | def get_last_match_by_long_steamID(long_steamID): 17 | url = 'https://api.wmpvp.com/api/v2/home/validUser?sign=' 18 | header = { 19 | "Host": "api.wmpvp.com", 20 | "Connection": "keep-alive", 21 | "Accept": "application/json, text/plain, */*", 22 | "Origin": "https://news.wmpvp.com", 23 | "x-requested-with": "XMLHttpRequest", 24 | "User-Agent": 'Mozilla/5.0 (Linux; Android 5.1.1; SM-G9750 Build/LMY49I; wv) AppleWebKit/537.36 (KHTML, ' 25 | 'like Gecko) Version/4.0 Chrome/52.0.2743.100 Mobile Safari/537.36 EsportsApp Version=1.4.3.43', 26 | "Content-Type": "application/json;charset=UTF-8", 27 | "Referer": "https://news.wmpvp.com/csgo-matchList.html?id=" + str(long_steamID) + "&type=00", 28 | "Accept-Encoding": "gzip, deflate", 29 | "Accept-Language": "zh-CN,en-US;q=0.8", 30 | } 31 | salt = '1f3192e58723aed15b8e8a9dc8e760861f3192e58723aed15b8e8a9dc8e76086' 32 | payload = '{"gameAbbr":"CSGO","steamId":"' + str(long_steamID) + \ 33 | '","accessToken":null,"lastTimeStamp":"","dataSource":0,"pageSize":1} ' 34 | sign = hashlib.md5((payload + salt).encode("utf-8")).hexdigest() 35 | response = requests.post(url + sign, data=payload, headers=header) 36 | if response.status_code >= 400: 37 | if response.status_code == 401: 38 | raise CSGOHTTPError("Unauthorized request 401. Verify API key.") 39 | if response.status_code == 503: 40 | raise CSGOHTTPError("The server is busy or you exceeded limits. Please wait 30s and try again.") 41 | raise CSGOHTTPError("Failed to retrieve data: %s. URL: %s" % (response.status_code, url)) 42 | r = json.loads(response.content.decode("utf-8")) 43 | try: 44 | return r['data'][2]['data'][0] 45 | except KeyError: 46 | raise CSGOHTTPError("Response Error: Key Error") 47 | except IndexError: 48 | raise CSGOHTTPError("Response Error: Index Error") 49 | 50 | 51 | # 接收某局比赛的玩家列表, 生成开黑战报 52 | # 参数为玩家对象列表 53 | def generate_party_message(player_list: [player]): 54 | player_num = len(player_list) 55 | 56 | # 队伍信息 57 | team = player_list[0].csgo_team 58 | win_team = player_list[0].csgo_win_team 59 | if win_team == team: 60 | win = True 61 | else: 62 | win = False 63 | 64 | nicknames = '' 65 | if player_num >= 3: 66 | for i in range(player_num - 1): 67 | nicknames += player_list[i].nickname 68 | nicknames += ', ' 69 | else: 70 | nicknames += player_list[0].nickname 71 | 72 | nicknames += '和' 73 | nicknames += player_list[player_num - 1].nickname 74 | 75 | top_rating = 0 76 | for i in player_list: 77 | if i.rating > top_rating: 78 | top_rating = i.rating 79 | 80 | if (win and top_rating > 1) or (not win and top_rating > 1): 81 | postive = True 82 | elif (win and top_rating < 0.5) or (not win and top_rating < 0.5): 83 | postive = False 84 | else: 85 | if random.randint(0, 1) == 0: 86 | postive = True 87 | else: 88 | postive = False 89 | 90 | print_str = '' 91 | if win and postive: 92 | print_str += random.choice(WIN_POSTIVE_PARTY).format(nicknames) + '\n' 93 | elif win and not postive: 94 | print_str += random.choice(WIN_NEGATIVE_PARTY).format(nicknames) + '\n' 95 | elif not win and postive: 96 | print_str += random.choice(LOSE_POSTIVE_PARTY).format(nicknames) + '\n' 97 | else: 98 | print_str += random.choice(LOSE_NEGATIVE_PARTY).format(nicknames) + '\n' 99 | 100 | start_time = player_list[0].csgo_start_time 101 | score1 = player_list[0].csgo_team_1_score 102 | score2 = player_list[0].csgo_team_2_score 103 | print_str += "开始时间: {}\n".format(start_time) 104 | print_str += "比分: [{}/{}]\n".format(score1, score2 if team == 1 else score2, score1) 105 | 106 | print_str += '游戏模式: [{}/{}]\n'.format(player_list[0].csgo_map, player_list[0].csgo_mode) 107 | 108 | for i in player_list: 109 | nickname = i.nickname 110 | rating = i.csgo_rating 111 | kills, deaths, assists = i.csgo_kill, i.csgo_death, i.csgo_assist 112 | 113 | print_str += "{}KDA: [{}/{}/{}], rating: {}\n" \ 114 | .format(nickname, kills, deaths, assists, rating) 115 | # print(print_str) 116 | message_sender.message(print_str) 117 | 118 | 119 | # 接收某局比赛的玩家信息, 生成单排战报 120 | # 参数为玩家对象 121 | def generate_solo_message(player_obj: player): 122 | # 队伍信息 123 | team = player_obj.csgo_team 124 | win_team = player_obj.csgo_win_team 125 | if win_team == team: 126 | win = True 127 | else: 128 | win = False 129 | 130 | nicknames = player_obj.nickname 131 | 132 | if (win and player_obj.csgo_rating > 1) or (not win and player_obj.csgo_rating > 1): 133 | postive = True 134 | elif (win and player_obj.csgo_rating < 0.5) or (not win and player_obj.csgo_rating < 0.5): 135 | postive = False 136 | else: 137 | if random.randint(0, 1) == 0: 138 | postive = True 139 | else: 140 | postive = False 141 | 142 | print_str = '' 143 | if win and postive: 144 | print_str += random.choice(WIN_POSTIVE_PARTY).format(nicknames) + '\n' 145 | elif win and not postive: 146 | print_str += random.choice(WIN_NEGATIVE_PARTY).format(nicknames) + '\n' 147 | elif not win and postive: 148 | print_str += random.choice(LOSE_POSTIVE_PARTY).format(nicknames) + '\n' 149 | else: 150 | print_str += random.choice(LOSE_NEGATIVE_PARTY).format(nicknames) + '\n' 151 | 152 | start_time = player_obj.csgo_start_time 153 | if team == 1: 154 | score1 = player_obj.csgo_team_1_score 155 | score2 = player_obj.csgo_team_2_score 156 | else: 157 | score1 = player_obj.csgo_team_2_score 158 | score2 = player_obj.csgo_team_1_score 159 | print_str += "开始时间: {}\n".format(start_time) 160 | print_str += "比分: [{}:{}]\n".format(score1, score2) 161 | 162 | print_str += '游戏模式: [{}/{}]\n'.format(player_obj.csgo_map, player_obj.csgo_mode) 163 | 164 | print_str += "{}KDA: [{}/{}/{}], rating: {}\n"\ 165 | .format(player_obj.nickname, player_obj.csgo_kill, 166 | player_obj.csgo_death, player_obj.csgo_assist, player_obj.csgo_rating) 167 | 168 | # print(print_str) 169 | message_sender.message(print_str) -------------------------------------------------------------------------------- /DOTA2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | import requests 4 | import json 5 | from DOTA2_dicts import * 6 | from player import player 7 | import random 8 | import time 9 | import message_sender 10 | 11 | # 这里替换成你自己的API 12 | # http://steamcommunity.com/dev/apikey 13 | api_key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 14 | 15 | 16 | # 异常处理 17 | class DOTA2HTTPError(Exception): 18 | pass 19 | 20 | 21 | # 根据slot判断队伍, 返回1为天辉, 2为夜魇 22 | def get_team_by_slot(slot): 23 | if slot < 100: 24 | return 1 25 | else: 26 | return 2 27 | 28 | 29 | def get_last_match_id_by_short_steamID(short_steamID): 30 | # get match_id 31 | url = 'https://api.steampowered.com/IDOTA2Match_570/GetMatchHistory/v001/?key={}' \ 32 | '&account_id={}&matches_requested=1'.format(api_key, short_steamID) 33 | response = requests.get(url) 34 | if response.status_code >= 400: 35 | if response.status_code == 401: 36 | raise DOTA2HTTPError("Unauthorized request 401. Verify API key.") 37 | if response.status_code == 503: 38 | raise DOTA2HTTPError("The server is busy or you exceeded limits. Please wait 30s and try again.") 39 | raise DOTA2HTTPError("Failed to retrieve data: %s. URL: %s" % (response.status_code, url)) 40 | 41 | match = response.json() 42 | try: 43 | match_id = match["result"]["matches"][0]["match_id"] 44 | except KeyError: 45 | raise DOTA2HTTPError("Response Error: Key Error") 46 | except IndexError: 47 | raise DOTA2HTTPError("Response Error: Index Error") 48 | return match_id 49 | 50 | 51 | def get_match_detail_info(match_id): 52 | # get match detail 53 | url = 'https://api.steampowered.com/IDOTA2Match_570/GetMatchDetails/V001/' \ 54 | '?key={}&match_id={}'.format(api_key, match_id) 55 | response = requests.get(url) 56 | if response.status_code >= 400: 57 | if response.status_code == 401: 58 | raise DOTA2HTTPError("Unauthorized request 401. Verify API key.") 59 | if response.status_code == 503: 60 | raise DOTA2HTTPError("The server is busy or you exceeded limits. Please wait 30s and try again.") 61 | raise DOTA2HTTPError("Failed to retrieve data: %s. URL: %s" % (response.status_code, url)) 62 | 63 | match = response.json() 64 | try: 65 | match_info = match["result"] 66 | except KeyError: 67 | raise DOTA2HTTPError("Response Error: Key Error") 68 | except IndexError: 69 | raise DOTA2HTTPError("Response Error: Index Error") 70 | 71 | return match_info 72 | 73 | 74 | # 接收某局比赛的玩家列表, 生成开黑战报 75 | # 参数为玩家对象列表和比赛ID 76 | def generate_party_message(match_id, player_list: [player]): 77 | try: 78 | match = get_match_detail_info(match_id=match_id) 79 | except DOTA2HTTPError: 80 | message_sender.message("DOTA2开黑战报生成失败") 81 | return 82 | 83 | # 比赛模式 84 | mode_id = match["game_mode"] 85 | if mode_id in (15, 19): # 各种活动模式不通报 86 | return 87 | mode = GAME_MODE[mode_id] if mode_id in GAME_MODE else '未知' 88 | 89 | lobby_id = match['lobby_type'] 90 | lobby = LOBBY[lobby_id] if lobby_id in LOBBY else '未知' 91 | 92 | player_num = len(player_list) 93 | # 更新玩家对象的比赛信息 94 | for i in player_list: 95 | for j in match['players']: 96 | if i.short_steamID == j['account_id']: 97 | i.dota2_kill = j['kills'] 98 | i.dota2_death = j['deaths'] 99 | i.dota2_assist = j['assists'] 100 | i.kda = ((1. * i.dota2_kill + i.dota2_assist) / i.dota2_death) \ 101 | if i.dota2_death != 0 else (1. * i.dota2_kill + i.dota2_assist) 102 | 103 | i.dota2_team = get_team_by_slot(j['player_slot']) 104 | i.hero = j['hero_id'] 105 | i.last_hit = j['last_hits'] 106 | i.damage = j['hero_damage'] 107 | i.gpm = j['gold_per_min'] 108 | i.xpm = j['xp_per_min'] 109 | break 110 | 111 | # 队伍信息 112 | team = player_list[0].dota2_team 113 | team_damage = 0 114 | team_kills = 0 115 | team_deaths = 0 116 | for i in match['players']: 117 | if get_team_by_slot(i['player_slot']) == team: 118 | team_damage += i['hero_damage'] 119 | team_kills += i['kills'] 120 | team_deaths += i['deaths'] 121 | 122 | win = False 123 | if match['radiant_win'] and team == 1: 124 | win = True 125 | elif not match['radiant_win'] and team == 2: 126 | win = True 127 | elif match['radiant_win'] and team == 2: 128 | win = False 129 | elif not match['radiant_win'] and team == 1: 130 | win = False 131 | 132 | nicknames = '' 133 | if player_num >= 3: 134 | for i in range(player_num - 1): 135 | nicknames += player_list[i].nickname 136 | nicknames += ', ' 137 | else: 138 | nicknames += player_list[0].nickname 139 | 140 | nicknames += '和' 141 | nicknames += player_list[player_num - 1].nickname 142 | 143 | top_kda = 0 144 | for i in player_list: 145 | if i.kda > top_kda: 146 | top_kda = i.kda 147 | 148 | if (win and top_kda > 10) or (not win and top_kda > 6): 149 | postive = True 150 | elif (win and top_kda < 4) or (not win and top_kda < 1): 151 | postive = False 152 | else: 153 | if random.randint(0, 1) == 0: 154 | postive = True 155 | else: 156 | postive = False 157 | 158 | print_str = '' 159 | if win and postive: 160 | print_str += random.choice(WIN_POSTIVE_PARTY).format(nicknames) + '\n' 161 | elif win and not postive: 162 | print_str += random.choice(WIN_NEGATIVE_PARTY).format(nicknames) + '\n' 163 | elif not win and postive: 164 | print_str += random.choice(LOSE_POSTIVE_PARTY).format(nicknames) + '\n' 165 | else: 166 | print_str += random.choice(LOSE_NEGATIVE_PARTY).format(nicknames) + '\n' 167 | 168 | start_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(match['start_time'])) 169 | duration = match['duration'] 170 | print_str += "开始时间: {}\n".format(start_time) 171 | print_str += "持续时间: {:.0f}分{:.0f}秒\n".format(duration / 60, duration % 60) 172 | 173 | print_str += '游戏模式: [{}/{}]\n'.format(mode, lobby) 174 | 175 | for i in player_list: 176 | nickname = i.nickname 177 | hero = HEROES_LIST_CHINESE[i.hero] if i.hero in HEROES_LIST_CHINESE else '不知道什么鬼' 178 | kda = i.kda 179 | last_hits = i.last_hit 180 | damage = i.damage 181 | kills, deaths, assists = i.dota2_kill, i.dota2_death, i.dota2_assist 182 | gpm, xpm = i.gpm, i.xpm 183 | 184 | damage_rate = 0 if team_damage == 0 else (100 * (float(damage) / team_damage)) 185 | participation = 0 if team_kills == 0 else (100 * float(kills + assists) / team_kills) 186 | deaths_rate = 0 if team_deaths == 0 else (100 * float(deaths) / team_deaths) 187 | 188 | print_str += "{}使用{}, KDA: {:.2f}[{}/{}/{}], GPM/XPM: {}/{}, " \ 189 | "补刀数: {}, 总伤害: {}({:.2f}%), 参战率: {:.2f}%, 参葬率: {:.2f}%\n" \ 190 | .format(nickname, hero, kda, kills, deaths, assists, gpm, xpm, last_hits, 191 | damage, damage_rate, participation, deaths_rate) 192 | 193 | print_str += "战绩详情: https://cn.dotabuff.com/matches/{}".format(match_id) 194 | 195 | # print(print_str) 196 | message_sender.message(print_str) 197 | 198 | 199 | # 接收某局比赛的玩家信息, 生成单排战报 200 | # 参数为玩家对象 201 | def generate_solo_message(match_id, player_obj: player): 202 | try: 203 | match = get_match_detail_info(match_id=match_id) 204 | except DOTA2HTTPError: 205 | message_sender.message("DOTA2单排战报生成失败") 206 | return 207 | # 比赛模式 208 | mode_id = match["game_mode"] 209 | if mode_id in (15, 19): # 各种活动模式不通报 210 | return 211 | mode = GAME_MODE[mode_id] if mode_id in GAME_MODE else '未知' 212 | 213 | lobby_id = match['lobby_type'] 214 | lobby = LOBBY[lobby_id] if lobby_id in LOBBY else '未知' 215 | 216 | # 更新玩家对象的比赛信息 217 | for j in match['players']: 218 | if player_obj.short_steamID == j['account_id']: 219 | player_obj.dota2_kill = j['kills'] 220 | player_obj.dota2_death = j['deaths'] 221 | player_obj.dota2_assist = j['assists'] 222 | player_obj.kda = ((1. * player_obj.dota2_kill + player_obj.dota2_assist) / player_obj.dota2_death) \ 223 | if player_obj.dota2_death != 0 else (1. * player_obj.dota2_kill + player_obj.dota2_assist) 224 | 225 | player_obj.dota2_team = get_team_by_slot(j['player_slot']) 226 | player_obj.hero = j['hero_id'] 227 | player_obj.last_hit = j['last_hits'] 228 | player_obj.damage = j['hero_damage'] 229 | player_obj.gpm = j['gold_per_min'] 230 | player_obj.xpm = j['xp_per_min'] 231 | break 232 | 233 | # 队伍信息 234 | team = player_obj.dota2_team 235 | team_damage = 0 236 | team_kills = 0 237 | team_deaths = 0 238 | for i in match['players']: 239 | if get_team_by_slot(i['player_slot']) == team: 240 | team_damage += i['hero_damage'] 241 | team_kills += i['kills'] 242 | team_deaths += i['deaths'] 243 | 244 | win = False 245 | if match['radiant_win'] and team == 1: 246 | win = True 247 | elif not match['radiant_win'] and team == 2: 248 | win = True 249 | elif match['radiant_win'] and team == 2: 250 | win = False 251 | elif not match['radiant_win'] and team == 1: 252 | win = False 253 | 254 | if (win and player_obj.kda > 10) or (not win and player_obj.kda > 6): 255 | postive = True 256 | elif (win and player_obj.kda < 4) or (not win and player_obj.kda < 1): 257 | postive = False 258 | else: 259 | if random.randint(0, 1) == 0: 260 | postive = True 261 | else: 262 | postive = False 263 | 264 | print_str = '' 265 | if win and postive: 266 | print_str += random.choice(WIN_POSTIVE_PARTY).format(player_obj.nickname) + '\n' 267 | elif win and not postive: 268 | print_str += random.choice(WIN_NEGATIVE_PARTY).format(player_obj.nickname) + '\n' 269 | elif not win and postive: 270 | print_str += random.choice(LOSE_POSTIVE_PARTY).format(player_obj.nickname) + '\n' 271 | else: 272 | print_str += random.choice(LOSE_NEGATIVE_PARTY).format(player_obj.nickname) + '\n' 273 | 274 | start_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(match['start_time'])) 275 | duration = match['duration'] 276 | print_str += "开始时间: {}\n".format(start_time) 277 | print_str += "持续时间: {:.0f}分{:.0f}秒\n".format(duration // 60, duration % 60) 278 | 279 | print_str += '游戏模式: [{}/{}]\n'.format(mode, lobby) 280 | 281 | nickname = player_obj.nickname 282 | hero = HEROES_LIST_CHINESE[player_obj.hero] if player_obj.hero in HEROES_LIST_CHINESE else '不知道什么鬼' 283 | kda = player_obj.kda 284 | last_hits = player_obj.last_hit 285 | damage = player_obj.damage 286 | kills, deaths, assists = player_obj.dota2_kill, player_obj.dota2_death, player_obj.dota2_assist 287 | gpm, xpm = player_obj.gpm, player_obj.xpm 288 | 289 | damage_rate = 0 if team_damage == 0 else (100 * (float(damage) / team_damage)) 290 | participation = 0 if team_kills == 0 else (100 * float(kills + assists) / team_kills) 291 | deaths_rate = 0 if team_deaths == 0 else (100 * float(deaths) / team_deaths) 292 | 293 | print_str += "{}使用{}, KDA: {:.2f}[{}/{}/{}], GPM/XPM: {}/{}, " \ 294 | "补刀数: {}, 总伤害: {}({:.2f}%), 参战率: {:.2f}%, 参葬率: {:.2f}%\n" \ 295 | .format(nickname, hero, kda, kills, deaths, assists, gpm, xpm, last_hits, 296 | damage, damage_rate, participation, deaths_rate) 297 | 298 | print_str += "战绩详情: https://cn.dotabuff.com/matches/{}".format(match_id) 299 | 300 | # print(print_str) 301 | message_sender.message(print_str) 302 | --------------------------------------------------------------------------------