├── README.md ├── gzbot.py ├── images ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png └── banner.png └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | ## GZCTF-bot 2 | 3 | ![](./images/banner.png) 4 | 5 | ~~《以赛促学、以赛促教、学赛结合》~~ 6 | 7 | 适用于GZ::CTF的赛事播报机器人,GZ::CTF非常牛逼,GZ大大的好!,你的代码写的真棒!.jpeg 8 | 9 | ### 特性🛠️ 10 | 11 | - 一血二血三血播报(一血、二血、三血) 12 | - 题目动态播报 13 | - 赛事动态播报 14 | - 队伍作弊播报 15 | 16 | ### 部署🔨 17 | 18 | #### 直接go-cqhttp(推荐) 19 | 20 | > 这样做比较方便一点,开箱即用 21 | 22 | - 到 [https://github.com/Mrs4s/go-cqhttp/releases](https://github.com/Mrs4s/go-cqhttp/releases) 下载对应的版本,在 config.yaml 填入你的QQ号和密码 23 | 24 | ```yaml 25 | account: # 账号相关 26 | uin: 1145141919810 # QQ账号 27 | password: '' # 密码为空时使用扫码登录 28 | encrypt: false # 是否开启密码加密 29 | status: 0 # 在线状态 请参考 https://docs.go-cqhttp.org/guide/config.html#在线状态 30 | relogin: # 重连设置 31 | delay: 3 # 首次重连延迟, 单位秒 32 | interval: 3 # 重连间隔 33 | max-times: 0 # 最大重连次数, 0为无限制 34 | ``` 35 | 36 | - 然后使用 http 进行连接即可 37 | 38 | ![1.png](./images/1.png) 39 | 40 | ```yaml 41 | # 连接服务列表 42 | servers: 43 | # 添加方式,同一连接方式可添加多个,具体配置说明请查看文档 44 | #- http: # http 通信 45 | #- ws: # 正向 Websocket 46 | #- ws-reverse: # 反向 Websocket 47 | #- pprof: #性能分析服务器 48 | 49 | - http: # HTTP 通信设置 50 | address: 0.0.0.0:5700 # HTTP监听地址 51 | version: 11 # OneBot协议版本, 支持 11/12 52 | timeout: 5 # 反向 HTTP 超时时间, 单位秒,<5 时将被忽略 53 | long-polling: # 长轮询拓展 54 | enabled: false # 是否开启 55 | max-queue-size: 2000 # 消息队列大小,0 表示不限制队列大小,谨慎使用 56 | middlewares: 57 | <<: *default # 引用默认中间件 58 | post: # 反向HTTP POST地址列表 59 | ``` 60 | 61 | 然后配置下脚本就行了 62 | 63 | - 参数说明 64 | - --url 比赛平台 e.g: https://ctf.xmutsec.cn 65 | - --notice 赛事播报群 群号 66 | - --id 赛事ID 67 | - --port CQ的端口 68 | - 以下为可选选项 69 | - --events 赛事详情通知群 70 | - --cookie 管理员Cookie,开了赛事详情通知群后配置 71 | 72 | ```bash 73 | python gzbot.py --url="https://ctf.xmutsec.cn" --notice=280853253 --id=1 --port=5700 74 | ``` 75 | 76 | > 第一次使用请先添加题目 77 | 78 | 如果您成功安装了依赖,并正确的配好了参数,他将会这样子运行 :D 79 | 80 | ![](./images/2.png) 81 | 82 | 如何正确获取比赛ID?新建完比赛后,点击比赛 83 | 84 | ![](./images/3.png) 85 | 86 | 然后URL上边的数字就是比赛ID 87 | 88 | ![](./images/4.png) 89 | 90 | 比赛题目播报 91 | 92 | ![](./images/5.png) 93 | 94 | 一二三血播报 95 | 96 | ![](./images/6.png) 97 | 98 | 提示播报 99 | 100 | ![](./images/7.png) 101 | 102 | 比赛公告 103 | 104 | ![](./images/8.png) 105 | 106 | 107 | 108 | #### 使用 nonebot 插件 109 | 110 | ~~(开发ing)~~ 111 | 112 | 开发个√⑧,爱咋用咋用 113 | -------------------------------------------------------------------------------- /gzbot.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import requests 3 | import urllib3 4 | import argparse 5 | import json, time, sys 6 | from datetime import datetime, timezone, timedelta 7 | 8 | urllib3.disable_warnings() 9 | 10 | URL = "" 11 | GROUP_NOTICE_ID = 0 12 | GROUP_EVENTS_ID = 0 13 | MATCH_ID = 0 14 | CQ_PORT = 0 15 | 16 | # Total Announcement Lens 17 | noticeLen = 0 18 | # Match Announcement Lens 19 | normalListLen = 0 20 | # New Challenge Lens 21 | newchallengeListLen = 0 22 | # New Hint Lens 23 | newhintLen = 0 24 | # First Blood Lens 25 | firstbloodLen = 0 26 | # Second Blood Lens 27 | secondbloodLen = 0 28 | # Third Blood Lens 29 | thirdbloodLen = 0 30 | 31 | noticeList = [] 32 | 33 | def processTime(t): 34 | t_truncated = t[:26] + t[26:].split('+')[0] 35 | input_time = datetime.fromisoformat(t_truncated) 36 | input_time_utc = input_time.replace(tzinfo=timezone.utc) 37 | beijing_timezone = timezone(timedelta(hours=8)) 38 | beijing_time = input_time_utc.astimezone(beijing_timezone) 39 | return beijing_time.strftime("%Y-%m-%d %H:%M:%S") 40 | 41 | def getNotice(URL, MATCH_ID): 42 | request = requests.session() 43 | dic = { 44 | 'platform-notice': URL + '/api/game/{0}/notices'.format(MATCH_ID), 45 | 'platform-events': URL + '/api/game/{0}/events?hideContainer=false&count=2&skip=0'.format(MATCH_ID) 46 | } 47 | try: 48 | res = request.get(dic['platform-notice'], verify=False) 49 | if res.text == '[]': 50 | sys.exit() 51 | allList = json.loads(res.text) 52 | return allList 53 | except: 54 | sys.exit("\033[31m[%s] [ERROR]: Please add a new challenge and use it! :(\033[0m" % (str(processTime(time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())))))) 55 | 56 | # Match Announcement Information 57 | def getNormalInfo(URL, MATCH_ID): 58 | global noticeLen 59 | tmpList = getNotice(URL, MATCH_ID) 60 | tmpListLen = len(tmpList) 61 | if (noticeLen < tmpListLen): 62 | interval = tmpListLen - noticeLen 63 | normal_list = [] 64 | for i in range(0, interval): 65 | if (tmpList[i]['type'] == 'Normal'): 66 | notices = { 67 | "id": tmpList[i]['id'], 68 | "time": tmpList[i]['time'], 69 | "content": tmpList[i]['content'] 70 | } 71 | normal_list.append(notices) 72 | sorted(normal_list, key=lambda notices: notices['id']) 73 | return normal_list 74 | 75 | # New Challenge Information 76 | def getNewChallengeInfo(URL, MATCH_ID): 77 | global noticeLen 78 | tmpList = getNotice(URL, MATCH_ID) 79 | tmpListLen = len(tmpList) 80 | if (noticeLen < tmpListLen): 81 | interval = tmpListLen - noticeLen 82 | newchallenge_list = [] 83 | for i in range(0, interval): 84 | if (tmpList[i]['type'] == 'NewChallenge'): 85 | notices = { 86 | "id": tmpList[i]['id'], 87 | "time": tmpList[i]['time'], 88 | "content": tmpList[i]['content'] 89 | } 90 | newchallenge_list.append(notices) 91 | sorted(newchallenge_list, key=lambda notices: notices['id']) 92 | return newchallenge_list 93 | 94 | # New Hint Information 95 | def getNewHintInfo(URL, MATCH_ID): 96 | global noticeLen 97 | tmpList = getNotice(URL, MATCH_ID) 98 | tmpListLen = len(tmpList) 99 | if (noticeLen < tmpListLen): 100 | interval = tmpListLen - noticeLen 101 | newhint_list = [] 102 | for i in range(0, interval): 103 | if (tmpList[i]['type'] == 'NewHint'): 104 | notices = { 105 | "id": tmpList[i]['id'], 106 | "time": tmpList[i]['time'], 107 | "content": tmpList[i]['content'] 108 | } 109 | newhint_list.append(notices) 110 | sorted(newhint_list, key=lambda notices: notices['id']) 111 | return newhint_list 112 | 113 | # First Blood Information 114 | def getFirstBloodInfo(URL, MATCH_ID): 115 | global noticeLen 116 | tmpList = getNotice(URL, MATCH_ID) 117 | tmpListLen = len(tmpList) 118 | if (noticeLen < tmpListLen): 119 | interval = tmpListLen - noticeLen 120 | firstblood_list = [] 121 | for i in range(0, interval): 122 | if (tmpList[i]['type'] == 'FirstBlood'): 123 | notices = { 124 | "id": tmpList[i]['id'], 125 | "time": tmpList[i]['time'], 126 | "content": tmpList[i]['content'] 127 | } 128 | firstblood_list.append(notices) 129 | sorted(firstblood_list, key=lambda notices: notices['id']) 130 | return firstblood_list 131 | 132 | # Second Blood Information 133 | def getSecondBloodInfo(URL, MATCH_ID): 134 | global noticeLen 135 | tmpList = getNotice(URL, MATCH_ID) 136 | tmpListLen = len(tmpList) 137 | if (noticeLen < tmpListLen): 138 | interval = tmpListLen - noticeLen 139 | secondblood_list = [] 140 | for i in range(0, interval): 141 | if (tmpList[i]['type'] == 'SecondBlood'): 142 | notices = { 143 | "id": tmpList[i]['id'], 144 | "time": tmpList[i]['time'], 145 | "content": tmpList[i]['content'] 146 | } 147 | secondblood_list.append(notices) 148 | sorted(secondblood_list, key=lambda notices: notices['id']) 149 | return secondblood_list 150 | 151 | # Third Blood Information 152 | def getThirdBloodInfo(URL, MATCH_ID): 153 | global noticeLen 154 | tmpList = getNotice(URL, MATCH_ID) 155 | tmpListLen = len(tmpList) 156 | if (noticeLen < tmpListLen): 157 | interval = tmpListLen - noticeLen 158 | thirdblood_list = [] 159 | for i in range(0, interval): 160 | if (tmpList[i]['type'] == 'ThirdBlood'): 161 | notices = { 162 | "id": tmpList[i]['id'], 163 | "time": tmpList[i]['time'], 164 | "content": tmpList[i]['content'] 165 | } 166 | thirdblood_list.append(notices) 167 | sorted(thirdblood_list, key=lambda notices: notices['id']) 168 | return thirdblood_list 169 | 170 | # Sending Message 171 | def sendMessage(msg, CQ_PORT, GROUP_NOTICE_ID): 172 | try: 173 | request = requests.Session() 174 | r = request.get("http://127.0.0.1:%s/send_group_msg?group_id=%s&message=%s" 175 | % ( 176 | str(CQ_PORT), 177 | str(GROUP_NOTICE_ID), 178 | str(msg)) 179 | ) 180 | print('\033[32m[%s] [SEND] Sending message to %s\033[0m' % (str(processTime(time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())))), GROUP_NOTICE_ID)) 181 | except requests.exceptions.ConnectionError as e: 182 | sys.exit("\033[31m[%s] [ERROR] Error sending message to %s, Connection error possible port or address error\033[0m" % (str(processTime(time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())))), GROUP_NOTICE_ID)) 183 | 184 | async def sendMessageNotice(URL, MATCH_ID, CQ_PORT, GROUP_NOTICE_ID): 185 | global normalListLen, newchallengeListLen, newhintLen, firstbloodLen, secondbloodLen, thirdbloodLen 186 | try: 187 | while True: 188 | message = "" 189 | # Normal List 190 | tmpNormalList = getNormalInfo(URL, MATCH_ID) 191 | tmpNormalListLen = len(getNormalInfo(URL, MATCH_ID)) 192 | # New Challenge 193 | tmpNewChallengeInfo = getNewChallengeInfo(URL, MATCH_ID) 194 | tmpNewChallengeInfoLen = len(getNewChallengeInfo(URL, MATCH_ID)) 195 | # New Hint 196 | tmpNewHintInfo = getNewHintInfo(URL, MATCH_ID) 197 | tmpNewHintInfoLen = len(getNewHintInfo(URL, MATCH_ID)) 198 | # First Blood 199 | tmpFirstBloodInfo = getFirstBloodInfo(URL, MATCH_ID) 200 | tmpFirstBloodInfoLen = len(getFirstBloodInfo(URL, MATCH_ID)) 201 | # Second Blood 202 | tmpSecondBloodInfo = getSecondBloodInfo(URL, MATCH_ID) 203 | tmpSecondBloodInfoLen = len(getSecondBloodInfo(URL, MATCH_ID)) 204 | # Third Blood 205 | tmpThirdBloodInfo = getThirdBloodInfo(URL, MATCH_ID) 206 | tmpThirdBloodInfoLen = len(getThirdBloodInfo(URL, MATCH_ID)) 207 | 208 | print('\033[33m[%s] [INFO]: Waiting data...\033[0m' % (str(processTime(time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())))))) 209 | if (normalListLen < tmpNormalListLen): 210 | message = "【比赛公告】\n内容:%s\n时间:%s" % (tmpNormalList[0]['content'], processTime(tmpNormalList[0]['time'])) 211 | print("\033[32m[%s] [ANNOUNCEMENTS] Data on receipt of Announcements %s\033[0m" % (str(processTime(time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())))), tmpNormalList[0]['content'])) 212 | sendMessage(msg=message, CQ_PORT=CQ_PORT, GROUP_NOTICE_ID=GROUP_NOTICE_ID) 213 | normalListLen = tmpNormalListLen 214 | else: 215 | normalListLen = tmpNormalListLen 216 | if (newchallengeListLen < tmpNewChallengeInfoLen): 217 | message = "【新增题目】\n%s\n时间:%s" % (tmpNewChallengeInfo[0]['content'], processTime(tmpNewChallengeInfo[0]['time'])) 218 | print("\033[32m[%s] [NEW CHALLENGE] Data on receipt of New Challenge %s\033[0m" % (str(), tmpNewChallengeInfo[0]['content'])) 219 | sendMessage(msg=message, CQ_PORT=CQ_PORT, GROUP_NOTICE_ID=GROUP_NOTICE_ID) 220 | newchallengeListLen = tmpNewChallengeInfoLen 221 | else: 222 | newchallengeListLen = tmpNewChallengeInfoLen 223 | if (newhintLen < tmpNewHintInfoLen): 224 | message = "【题目提示】\n%s\n时间:%s" % (tmpNewHintInfo[0]['content'], processTime(tmpNewHintInfo[0]['time'])) 225 | print("\033[32m[%s] [NEW HINT] Data on receipt of New Hint %s\033[0m" % (str(processTime(time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())))), tmpNewHintInfo[0]['content'])) 226 | sendMessage(msg=message, CQ_PORT=CQ_PORT, GROUP_NOTICE_ID=GROUP_NOTICE_ID) 227 | newhintLen = tmpNewHintInfoLen 228 | else: 229 | newhintLen = tmpNewHintInfoLen 230 | if (firstbloodLen < tmpFirstBloodInfoLen): 231 | message = "【一血播报】\n%s\n时间:%s" % (tmpFirstBloodInfo[0]['content'], processTime(tmpFirstBloodInfo[0]['time'])) 232 | print("\033[32m[%s] [FIRST BLOOD] Data on receipt of First Blood %s\033[0m" % (str(processTime(time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())))), tmpFirstBloodInfo[0]['content'])) 233 | sendMessage(msg=message, CQ_PORT=CQ_PORT, GROUP_NOTICE_ID=GROUP_NOTICE_ID) 234 | firstbloodLen = tmpFirstBloodInfoLen 235 | else: 236 | firstbloodLen = tmpFirstBloodInfoLen 237 | if (secondbloodLen < tmpSecondBloodInfoLen): 238 | message = "【二血播报】\n%s\n时间:%s" % (tmpSecondBloodInfo[0]['content'], processTime(tmpSecondBloodInfo[0]['time'])) 239 | print("\033[32m[%s] [SECOND BLOOD] Data on receipt of First Blood %s\033[0m" % (str(processTime(time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())))), tmpSecondBloodInfo[0]['content'])) 240 | sendMessage(msg=message, CQ_PORT=CQ_PORT, GROUP_NOTICE_ID=GROUP_NOTICE_ID) 241 | secondbloodLen = tmpSecondBloodInfoLen 242 | else: 243 | secondbloodLen = tmpSecondBloodInfoLen 244 | if (thirdbloodLen < tmpThirdBloodInfoLen): 245 | message = "【三血播报】\n%s\n时间:%s" % (tmpThirdBloodInfo[0]['content'], processTime(tmpThirdBloodInfo[0]['time'])) 246 | print("\033[32m[%s] [THIRD BLOOD] Data on receipt of First Blood %s\033[0m" % (str(processTime(time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())))), tmpThirdBloodInfo[0]['content'])) 247 | sendMessage(msg=message, CQ_PORT=CQ_PORT, GROUP_NOTICE_ID=GROUP_NOTICE_ID) 248 | thirdbloodLen = tmpThirdBloodInfoLen 249 | else: 250 | thirdbloodLen = tmpThirdBloodInfoLen 251 | await asyncio.sleep(3) 252 | 253 | except KeyboardInterrupt as e: 254 | print('\033[31m[%s] [ERROR]: Trying to exit !!!\033[0m' % (str(processTime(time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())))))) 255 | 256 | async def runner(): 257 | tasks = [ 258 | asyncio.create_task(sendMessageNotice( 259 | URL=URL, 260 | GROUP_NOTICE_ID=GROUP_NOTICE_ID, 261 | MATCH_ID=MATCH_ID, 262 | CQ_PORT=CQ_PORT 263 | )), 264 | ] 265 | normalListLen = len(getNormalInfo(URL=URL, MATCH_ID=MATCH_ID)) 266 | newchallengeListLen = len(getNewChallengeInfo(URL=URL, MATCH_ID=MATCH_ID)) 267 | newhintLen = len(getNewHintInfo(URL=URL, MATCH_ID=MATCH_ID)) 268 | firstbloodLen = len(getFirstBloodInfo(URL=URL, MATCH_ID=MATCH_ID)) 269 | secondbloodLen = len(getSecondBloodInfo(URL=URL, MATCH_ID=MATCH_ID)) 270 | thirdbloodLen = len(getThirdBloodInfo(URL=URL, MATCH_ID=MATCH_ID)) 271 | 272 | await asyncio.gather(*tasks) 273 | 274 | def main(): 275 | global URL, GROUP_NOTICE_ID, MATCH_ID, CQ_PORT 276 | BANNER = """\033[01;34m\ 277 | 278 | _____ ______ _____ ___________ ______ _ 279 | | __ \|___ /_ _/ __ \_ _| ___| | ___ \ | | 280 | | | \/ / /(_|_) / \/ | | | |_ | |_/ / ___ | |_ 281 | | | __ / / | | | | | _| | ___ \/ _ \| __| 282 | | |_\ \./ /____ _| \__/\ | | | | | |_/ / (_) | |_ \033[0m\033[4;37m%s\033[0m 283 | \____/\_____(_|_)\____/ \_/ \_| \____/ \___/ \__| \033[0m\033[4;37m%s\033[0m\n 284 | """ % ("Author: IceCliffs", "Version: v0.0.2") 285 | print(BANNER) 286 | parser = argparse.ArgumentParser(description="GZ::CTF QQ Bot") 287 | parser.add_argument('--url', 288 | required=True, 289 | help="platform url") 290 | parser.add_argument('--notice', 291 | required=True, 292 | help="qq notice Group") 293 | parser.add_argument('--id', 294 | required=True, 295 | help="race id") 296 | parser.add_argument('--port', 297 | required=True, 298 | help="cq port") 299 | parser.add_argument('--events', 300 | help="qq detail group (optional)") 301 | parser.add_argument('--cookie', 302 | help="administrator cookie") 303 | args = parser.parse_args() 304 | 305 | if args.url and args.notice and args.id and args.port: 306 | URL = args.url 307 | GROUP_NOTICE_ID = args.notice 308 | MATCH_ID = args.id 309 | CQ_PORT = args.port 310 | asyncio.run(runner()) 311 | 312 | if __name__ == "__main__": 313 | main() 314 | -------------------------------------------------------------------------------- /images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Archives/GZCTFBOT/c235839068cefb25ad6d9271bf600930bfa65253/images/1.png -------------------------------------------------------------------------------- /images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Archives/GZCTFBOT/c235839068cefb25ad6d9271bf600930bfa65253/images/2.png -------------------------------------------------------------------------------- /images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Archives/GZCTFBOT/c235839068cefb25ad6d9271bf600930bfa65253/images/3.png -------------------------------------------------------------------------------- /images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Archives/GZCTFBOT/c235839068cefb25ad6d9271bf600930bfa65253/images/4.png -------------------------------------------------------------------------------- /images/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Archives/GZCTFBOT/c235839068cefb25ad6d9271bf600930bfa65253/images/5.png -------------------------------------------------------------------------------- /images/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Archives/GZCTFBOT/c235839068cefb25ad6d9271bf600930bfa65253/images/6.png -------------------------------------------------------------------------------- /images/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Archives/GZCTFBOT/c235839068cefb25ad6d9271bf600930bfa65253/images/7.png -------------------------------------------------------------------------------- /images/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Archives/GZCTFBOT/c235839068cefb25ad6d9271bf600930bfa65253/images/8.png -------------------------------------------------------------------------------- /images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Archives/GZCTFBOT/c235839068cefb25ad6d9271bf600930bfa65253/images/banner.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Requests==2.31.0 2 | urllib3==1.26.16 3 | --------------------------------------------------------------------------------