├── .gitignore ├── README.md ├── awesome └── plugins │ ├── arcaea.py │ └── arcaea_crawler.py ├── config.py ├── ds.txt ├── main.py ├── requirements.txt └── run_docker.sh /.gitignore: -------------------------------------------------------------------------------- 1 | coolq 2 | __pycache__ 3 | arc_namecache.txt 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ArcaeaBot 2 | Using interface of redive.estertion.win to query Arcaea player's info in QQ chating. 3 | 4 | ### Deploying 5 | Python version: 3.8.1 6 | 7 | nonebot version: 1.3.1 8 | 9 | Fetching coolq http API at https://github.com/richardchien/coolq-http-api, then connects with this bot. 10 | 11 | Refer: https://github.com/richardchien/nonebot 12 | -------------------------------------------------------------------------------- /awesome/plugins/arcaea.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command, CommandSession, helpers, get_bot 2 | from aiocqhttp.exceptions import Error as CQHttpError 3 | import requests 4 | import demjson 5 | import random 6 | import math 7 | import time as _time 8 | from awesome.plugins.arcaea_crawler import * 9 | 10 | f = open('ds.txt', 'r', encoding='utf-8') 11 | dss = f.readlines() 12 | f.close() 13 | 14 | help_text = '''欢迎使用Arcaea Bot。支持的命令如下: 15 | .ds <曲名/等级> 查询定数 16 | .arc <玩家名/好友码> 查询玩家的ptt、r10/b30和最近游玩的歌曲 17 | .best <玩家名/好友码> 查询玩家ptt前n的歌曲''' 18 | 19 | 20 | @on_command('help', only_to_me=False) 21 | async def help(session: CommandSession): 22 | await session.send(help_text) 23 | 24 | 25 | @on_command('best', only_to_me=False) 26 | async def lookup(session: CommandSession): 27 | await session.send("Looking up %s\nWarning: .best命令具有刷屏风险,请尽量私聊查询~" % session.state['id']) 28 | QueryThread(session.cmd, session.ctx, session.bot, session.state).start() 29 | 30 | 31 | @lookup.args_parser 32 | async def _(session: CommandSession): 33 | arr = session.current_arg_text.strip().split(' ') 34 | session.state['id'] = arr[0] 35 | try: 36 | session.state['num'] = int(arr[1]) 37 | except Exception: 38 | session.state['num'] = 0 39 | 40 | 41 | @on_command('arcaea', aliases=['arc'], only_to_me=False) 42 | async def arcaea(session: CommandSession): 43 | await session.send("Querying %s" % session.state['id']) 44 | QueryThread(session.cmd, session.ctx, session.bot, session.state).start() 45 | 46 | 47 | @arcaea.args_parser 48 | async def _(session: CommandSession): 49 | session.state['id'] = session.current_arg_text.strip() 50 | 51 | 52 | @on_command('ds', only_to_me=False) 53 | async def ds(session: CommandSession): 54 | result_str = "" 55 | num = 0 56 | for line in dss: 57 | if session.state['arg'].lower() in line.lower(): 58 | num += 1 59 | result_str += line.replace('\t', ' ') 60 | await session.send("共找到%d条结果:\n" % num + result_str[:-1]) 61 | 62 | 63 | @ds.args_parser 64 | async def _(session: CommandSession): 65 | session.state['arg'] = session.current_arg_text.strip() 66 | -------------------------------------------------------------------------------- /awesome/plugins/arcaea_crawler.py: -------------------------------------------------------------------------------- 1 | import websocket 2 | import brotli 3 | import json 4 | import threading 5 | from nonebot import get_bot 6 | import asyncio 7 | 8 | clear_list = ['Track Lost', 'Normal Clear', 'Full Recall', 'Pure Memory', 'Easy Clear', 'Hard Clear'] 9 | diff_list = ['PST', 'PRS', 'FTR', 'BYD'] 10 | 11 | f = open('arc_namecache.txt', 'w') 12 | f.close() 13 | 14 | 15 | def load_cache(): 16 | cache = {} 17 | f = open('arc_namecache.txt', 'r') 18 | for line in f.readlines(): 19 | ls = line.replace('\n', '').split(' ') 20 | cache[ls[0]] = ls[1] 21 | f.close() 22 | return cache 23 | 24 | 25 | def put_cache(d: dict): 26 | f = open('arc_namecache.txt', 'w') 27 | for key in d: 28 | f.write('%s %s\n' % (key, d[key])) 29 | 30 | 31 | def cmp(a): 32 | return a['rating'] 33 | 34 | 35 | def calc(ptt, s): 36 | brating = 0 37 | for i in range(0, 30): 38 | try: 39 | brating += s[i]['rating'] 40 | except IndexError: 41 | break 42 | brating /= 30 43 | rrating = 4 * (ptt - brating * 0.75) 44 | return brating, rrating 45 | 46 | 47 | def lookup(nickname: str): 48 | ws = websocket.create_connection("wss://arc.estertion.win:616/") 49 | ws.send("lookup " + nickname) 50 | buffer = "" 51 | while buffer != "bye": 52 | buffer = ws.recv() 53 | if type(buffer) == type(b''): 54 | obj2 = json.loads(str(brotli.decompress(buffer), encoding='utf-8')) 55 | id = obj2['data'][0]['code'] 56 | cache = load_cache() 57 | cache[nickname] = id 58 | put_cache(cache) 59 | return id 60 | 61 | def query(id: str): 62 | s = "" 63 | song_title, userinfo, scores = _query(id) 64 | b, r = calc(userinfo['rating'] / 100, scores) 65 | s += "Player: %s\nPotential: %.2f\nBest 30: %.5f\nRecent Top 10: %.5f\n\n" % (userinfo['name'], userinfo['rating'] / 100, b, r) 66 | score = userinfo['recent_score'][0] 67 | s += "Recent Play: \n%s %s %.1f \n%s\nPure: %d(%d)\nFar: %d\nLost: %d\nScore: %d\nRating: %.2f" % (song_title[score['song_id']]['en'], diff_list[score['difficulty']], score['constant'], clear_list[score['clear_type']], 68 | score["perfect_count"], score["shiny_perfect_count"], score["near_count"], score["miss_count"], score["score"], score["rating"]) 69 | return s 70 | 71 | 72 | def best(id: str, num: int): 73 | if num < 1: 74 | return [] 75 | result = [] 76 | s = "" 77 | song_title, userinfo, scores = _query(id) 78 | s += "%s's Top %d Songs:\n" % (userinfo['name'], num) 79 | for j in range(0, int((num - 1) / 15) + 1): 80 | for i in range(15 * j, 15 * (j + 1)): 81 | if i >= num: 82 | break 83 | try: 84 | score = scores[i] 85 | except IndexError: 86 | break 87 | s += "#%d %s %s %.1f \n\t%s\n\tPure: %d(%d)\n\tFar: %d\n\tLost: %d\n\tScore: %d\n\tRating: %.2f\n" % (i+1, song_title[score['song_id']]['en'], diff_list[score['difficulty']], score['constant'], clear_list[score['clear_type']], 88 | score["perfect_count"], score["shiny_perfect_count"], score["near_count"], score["miss_count"], score["score"], score["rating"]) 89 | result.append(s[:-1]) 90 | s = "" 91 | return result 92 | 93 | def _query(id: str): 94 | cache = load_cache() 95 | # print(cache) 96 | try: 97 | id = cache[id] 98 | except KeyError: 99 | pass 100 | ws = websocket.create_connection("wss://arc.estertion.win:616/") 101 | ws.send(id) 102 | buffer = "" 103 | scores = [] 104 | userinfo = {} 105 | song_title = {} 106 | while buffer != "bye": 107 | try: 108 | buffer = ws.recv() 109 | except websocket._exceptions.WebSocketConnectionClosedException: 110 | ws = websocket.create_connection("wss://arc.estertion.win:616/") 111 | ws.send(lookup(id)) 112 | if type(buffer) == type(b''): 113 | # print("recv") 114 | obj = json.loads(str(brotli.decompress(buffer), encoding='utf-8')) 115 | # al.append(obj) 116 | if obj['cmd'] == 'songtitle': 117 | song_title = obj['data'] 118 | elif obj['cmd'] == 'scores': 119 | scores += obj['data'] 120 | elif obj['cmd'] == 'userinfo': 121 | userinfo = obj['data'] 122 | scores.sort(key=cmp, reverse=True) 123 | return song_title, userinfo, scores 124 | 125 | 126 | class QueryThread(threading.Thread): 127 | def __init__(self, cmd, ctx, bot, state): 128 | threading.Thread.__init__(self) 129 | self.operation = cmd.name[0] 130 | self.ctx = ctx 131 | self.bot = bot 132 | self.state = state 133 | 134 | def run(self): 135 | funcs = [] 136 | if self.operation == 'arcaea': 137 | try: 138 | message = query(self.state['id']) 139 | except Exception as e: 140 | message = "An exception occurred: %s" % repr(e) 141 | funcs.append(self.bot.send(self.ctx, message=message)) 142 | elif self.operation == 'best': 143 | try: 144 | s = best(self.state['id'], self.state['num']) 145 | except Exception as e: 146 | s = ["An exception occurred: %s" % repr(e)] 147 | for elem in s: 148 | funcs.append(self.bot.send(self.ctx, message=elem)) 149 | loop = asyncio.new_event_loop() 150 | loop.run_until_complete(asyncio.wait(funcs)) 151 | loop.close() -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | from nonebot.default_config import * 2 | 3 | SUPERUSERS = {2300756578} 4 | COMMAND_START = {'.', '/', '!', '。'} 5 | -------------------------------------------------------------------------------- /ds.txt: -------------------------------------------------------------------------------- 1 | Grievous Lady 6.5 9.3 11.3 2 | Fracture Ray 6.0 9.5 11.2 3 | SAIKYO STRONGER 5.5 9.4 11.0 4 | #1f1e33 5.5 9.2 10.9 5 | World Vanquisher 2.5 5.5 10.9 6 | Axium Crisis 5.5 8.5 10.9 7 | Dantalion 5.0 8.2 10.8 8 | Ringed Genesis 5.5 8.4 10.8 9 | Cyaegha 5.5 8.4 10.8 10 | ouroboros -twin stroke of the end- 4.5 7.0 10.7 11 | Singularity 4.5 7.5 10.7 12 | Halcyon 5.5 8.2 10.7 13 | Tempestissimo 6.5 9.5 10.6 11.5 14 | corps-sans-organes 4.5 7.5 10.6 15 | GLORY:ROAD 4.5 7.0 10.6 16 | Tiferet 4.5 7.5 10.6 17 | cyanine 4.0 7.5 10.6 18 | Ikazuchi 3.5 7.5 10.5 19 | γuarδina 4.0 7.5 10.5 20 | Valhalla:0 4.5 7.0 10.4 21 | Garakuta Doll Play 4.5 6.5 10.4 22 | Nirv lucE 2.5 7.0 10.3 23 | Ether Strike 5.5 8.3 10.3 24 | IZANA 5.0 8.3 10.2 25 | Sheriruth 5.5 7.5 10.2 26 | Metallic Punisher 3.0 7.0 10.2 27 | αterlβus 4.0 7.0 10.2 28 | Scarlet Lance 4.0 7.0 10.1 29 | PRAGMATISM 4.5 8.6 10.1 30 | conflict 4.5 7.5 10.1 31 | Mirzam 4.0 7.0 10.0 32 | Vicious Heroism 4.0 7.5 10.0 33 | trappola bewitching 3.0 6.0 10.0 34 | Modelista 3.5 7.5 10.0 35 | Alexandrite 4.5 7.0 10.0 36 | Arcahv 4.5 7.5 9.9 37 | Antagonism 4.5 7.5 9.9 38 | Heavensdoor 4.5 7.5 9.9 39 | Corruption 3.0 6.5 9.9 40 | SOUNDWiTCH 3.5 6.5 9.9 41 | Nhelv 3.0 6.5 9.9 42 | Lost Desire 4.0 7.5 9.8 43 | Einherjar Joker 4.0 7.0 9.8 44 | Heavenly caress 3.5 7.5 9.8 45 | Dreadnought 4.0 7.0 9.8 46 | SUPERNOVA 3.0 6.0 9.8 47 | DX Choseinou Full Metal Shojo 3.0 6.0 9.8 48 | Memory Forest 3.5 6.0 9.8 49 | Linear Accelerator 2.5 6.5 9.8 50 | Altale 2.5 5.5 9.7 51 | Black Lotus 3.0 6.5 9.7 52 | BLRINK 3.5 7.5 9.7 53 | BATTLE NO.1 3.5 6.5 9.7 54 | Filament 4.5 7.5 9.7 55 | A Wandering Melody of Love 3.5 7.5 9.7 56 | Black Territory 3.0 7.5 9.7 57 | The Message 3.0 6.5 9.7 58 | Sulfur 4.0 6.0 9.7 59 | Quon 4.0 6.5 9.7 60 | Lethaeus 3.5 6.5 9.7 61 | amygdata 4.0 7.5 9.6 62 | Avant Raze 3.5 6.5 9.6 63 | Monochrome Princess 4.5 7.5 9.6 64 | Vindication 4.0 6.5 9.6 65 | Astral tale 4.5 7.0 9.6 66 | Fallensquare 3.0 7.0 9.6 67 | LunarOrbit -believe in the Espebranch road- 3.5 6.0 9.6 68 | Dreamin' Attraction!! 4.5 7.0 9.6 69 | Illegal Paradise 2.0 7.0 9.6 70 | carmine:scythe 4.0 7.5 9.6 71 | AI[UE]OON 3.5 6.5 9.5 72 | OMAKENO Stroke 3.0 6.5 9.5 73 | STAGER (ALL STAGE CLEAR) 3.0 6.5 9.5 74 | Specta 3.5 6.5 9.5 75 | Party Vinyl 4.0 7.5 9.5 76 | DataErr0r 3.0 7.0 9.5 77 | Cybernecia Catharsis 4.0 7.0 9.5 78 | Equilibrium 3.5 6.5 9.4 79 | VECTOЯ 3.0 7.0 9.4 80 | Yosakura Fubuki 4.5 7.0 9.4 81 | Your voice so... feat. Such 3.5 6.5 9.4 82 | Red and Blue 4.0 7.5 9.4 83 | Impure Bird 2.0 5.5 9.4 84 | CROSSSOUL 4.0 7.0 9.4 85 | Be There 4.0 7.5 9.4 86 | Syro 3.5 6.5 9.3 87 | Oracle 3.0 5.5 9.3 88 | Ignotus 3.5 6.5 9.3 89 | GOODTEK (Arcaea Edit) 4.0 6.5 9.3 90 | Blaster 4.0 7.0 9.3 91 | Auxesia 3.5 6.5 9.3 92 | Libertas 3.5 5.5 9.2 93 | Phantasia 4.0 5.5 9.2 94 | Rugie 3.0 6.0 9.2 95 | Strongholds 2.5 5.0 9.2 96 | Lost Civilization 4.0 7.0 9.2 97 | Anökumene 2.5 6.5 9.2 98 | La'qryma of the Wasteland 3.5 6.5 9.1 99 | qualia -ideaesthesia- 4.5 7.0 9.1 100 | Iconoclast 4.0 7.0 9.1 101 | Essence of Twilight 4.5 7.0 9.1 102 | dropdead 1.5 9.5 9.1 103 | Chronostasis 3.5 7.5 9.1 104 | Give Me a Nightmare 3.5 5.5 9.0 105 | ReviXy 3.0 6.0 9.0 106 | Empire of Winter 3.5 6.5 9.0 107 | Kanagawa Cyber Culvert 1.0 5.5 9.0 108 | Flyburg and Endroll 3.0 6.0 9.0 109 | MERLIN 3.0 5.5 8.9 110 | memoryfactory.lzh 2.5 5.5 8.9 111 | Maze No.9 3.0 3.5 8.9 112 | Evoltex (poppi'n mix) 2.0 7.0 8.9 113 | Vivid Theory 2.0 5.0 8.8 114 | Particle Arts 3.5 6.0 8.8 115 | Antithese 2.0 5.0 8.8 116 | Solitary Dream 4.0 7.0 8.8 117 | Surrender 3.0 6.5 8.8 118 | next to you 4.5 7.0 8.8 119 | Flashback 2.0 5.0 8.8 120 | Senkyou 3.0 5.5 8.7 121 | Call My Name feat. Yukacco 3.5 6.0 8.7 122 | Gekka (Short Version) 4.0 6.0 8.6 123 | FREEF4LL 4.0 7.0 8.6 124 | Grimheart 2.5 5.0 8.6 125 | Silent Rush 2.5 5.0 8.6 126 | Journey 3.0 6.0 8.6 127 | world.execute(me); 3.5 5.5 8.5 128 | Chelsea 3.0 6.0 8.5 129 | cry of viyella 3.5 6.0 8.5 130 | Reinvent 2.5 6.5 8.5 131 | Harutopia ~Utopia of Spring~ 1.0 4.5 8.5 132 | Dandelion 2.5 6.0 8.5 133 | Babaroque 3.0 6.5 8.5 134 | Snow White 2.5 5.0 8.4 135 | REconstruction 2.5 6.0 8.4 136 | Rabbit In The Black Room 2.5 5.5 8.4 137 | Purgatorium 2.5 6.0 8.4 138 | Moonheart 2.5 5.5 8.4 139 | Lumia 2.5 5.5 8.4 140 | Oblivia 3.5 5.0 8.3 141 | Tie me down gently 3.0 5.5 8.3 142 | Dot to Dot feat. shully 3.0 6.0 8.3 143 | Shades of Light in a Transcendent Realm 3.0 6.0 8.3 144 | Bookmaker (2D Version) 4.5 6.5 8.3 145 | 1F 2.5 6.5 8.2 146 | One Last Drive 2.5 5.5 8.2 147 | Lucifer 3.5 5.5 8.2 148 | Hall of Mirrors 3.0 5.5 8.2 149 | Genesis 2.0 5.5 8.2 150 | Diode 2.5 5.5 8.1 151 | Hikari 2.5 6.0 8.1 152 | I've heard it said 3.5 6.0 8.1 153 | Relentless 4.5 6.5 8.0 154 | Suomi 2.0 5.0 7.5 155 | Romance Wars 1.0 4.0 7.5 156 | Rise 2.5 4.0 7.5 157 | Paradise 1.0 4.0 7.5 158 | Moonlight of Sand Castle 1.5 5.0 7.5 159 | inkar-usi 2.0 4.0 7.5 160 | Infinity Heaven 1.5 5.5 7.5 161 | Dream goes on 1.5 5.0 7.5 162 | Dement ~after legend~ 3.5 6.0 7.5 163 | Clotho and the stargazer 2.0 5.0 7.5 164 | Brand new world 2.0 4.0 7.5 165 | Vexaria 2.5 5.0 7.0 166 | Sayonara Hatsukoi 1.5 4.5 7.0 167 | Fairytale 1.0 3.5 7.0 168 | Blossoms 1.0 4.0 7.0 169 | 170 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import nonebot 2 | import config 3 | from os import path 4 | # from awesome.plugins.plugin import roll_expression 5 | 6 | if __name__ == '__main__': 7 | nonebot.init(config) 8 | nonebot.load_builtin_plugins() 9 | nonebot.load_plugins( 10 | path.join(path.dirname(__file__), 'awesome', 'plugins'), 11 | 'awesome.plugins' 12 | ) 13 | nonebot.run(host='0.0.0.0', port=8084) 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiocache==0.11.1 2 | aiocqhttp==1.2.5 3 | aiofiles==0.5.0 4 | blinker==1.4 5 | Brotli==1.0.7 6 | certifi==2020.6.20 7 | chardet==3.0.4 8 | click==7.1.2 9 | demjson==2.2.4 10 | h11==0.9.0 11 | h2==3.2.0 12 | hpack==3.0.0 13 | hstspreload==2020.6.30 14 | httpcore==0.9.1 15 | httpx==0.13.3 16 | Hypercorn==0.10.1 17 | hyperframe==5.2.0 18 | idna==2.10 19 | itsdangerous==1.1.0 20 | Jinja2==2.11.2 21 | MarkupSafe==1.1.1 22 | nonebot==1.6.0 23 | priority==1.3.0 24 | Quart==0.11.5 25 | requests==2.24.0 26 | rfc3986==1.4.0 27 | six==1.15.0 28 | sniffio==1.1.0 29 | toml==0.10.1 30 | typing-extensions==3.7.4.2 31 | urllib3==1.25.9 32 | websocket-client==0.57.0 33 | Werkzeug==1.0.1 34 | wsproto==0.15.0 35 | 36 | -------------------------------------------------------------------------------- /run_docker.sh: -------------------------------------------------------------------------------- 1 | docker run -ti --rm --name cqhttp-arcbot -d -v $(pwd)/coolq:/home/user/coolq -p 9001:9000 -p 5701:5700 -e COOLQ_ACCOUNT=123456 -e CQHTTP_POST_URL=http://example.com:8080 -e CQHTTP_SERVE_DATA_FILES=yes richardchien/cqhttp:latest 2 | --------------------------------------------------------------------------------