├── .env ├── .gitignore ├── LICENSE ├── README.md ├── bot.py ├── requirements.txt └── src ├── libraries ├── image.py ├── maimai_best_40.py ├── maimai_best_50.py ├── maimaidx_music.py └── tool.py └── plugins ├── maimaidx.py └── public.py /.env: -------------------------------------------------------------------------------- 1 | ENVIRONMENT=prod 2 | COMMAND_START=["", "."] 3 | PORT=10219 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/static 2 | .idea 3 | .code 4 | venv 5 | __pycache__ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Diving-Fish 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mai bot 使用指南 2 | 3 | 此 README 提供了最低程度的 mai bot 教程与支持。 4 | 5 | **建议您至少拥有一定的编程基础之后再尝试使用本工具。** 6 | 7 | ## Step 1. 安装 Python 8 | 9 | 请自行前往 https://www.python.org/ 下载 Python 3 版本(> 3.7)并将其添加到环境变量(在安装过程中勾选 Add Python to system PATH)。对大多数用户来说,您应该下载 Windows installer (64-bit)。 10 | 11 | 在 Linux 系统上,可能需要其他方法安装 Python 3,请自行查找。 12 | 13 | ## Step 2. 运行项目 14 | 15 | 建议使用 git 对此项目进行版本管理。您也可以直接在本界面下载代码的压缩包进行运行。 16 | 17 | 在运行代码之前,您需要从[此链接](https://www.diving-fish.com/maibot/static.zip)下载资源文件并解压到`src`文件夹中。 18 | 19 | > 资源文件仅供学习交流使用,请自觉在下载 24 小时内删除资源文件。 20 | 21 | 在此之后,**您需要打开控制台,并切换到该项目所在的目录。** 22 | 在 Windows 10 系统上,您可以直接在项目的根目录(即 bot.py)文件所在的位置按下 Shift + 右键,点击【在此处打开 PowerShell 窗口】。 23 | 如果您使用的是更旧的操作系统(比如 Windows 7),请自行查找关于`Command Prompt`,`Powershell`以及`cd`命令的教程。 24 | 25 | 之后,在打开的控制台中输入 26 | ``` 27 | python --version 28 | ``` 29 | 控制台应该会打印出 Python 的版本。如果提示找不到 `python` 命令,请检查环境变量或干脆重装 Python,**并务必勾选 Add Python to system PATH**。 30 | 31 | 之后,输入 32 | ``` 33 | pip install -r requirements.txt 34 | ``` 35 | 安装依赖完成后,运行 36 | ``` 37 | python bot.py 38 | ``` 39 | 运行项目。如果输出如下所示的内容,代表运行成功: 40 | ``` 41 | 08-02 11:26:48 [INFO] nonebot | NoneBot is initializing... 42 | 08-02 11:26:48 [INFO] nonebot | Current Env: prod 43 | 08-02 11:26:49 [INFO] nonebot | Succeeded to import "maimaidx" 44 | 08-02 11:26:49 [INFO] nonebot | Succeeded to import "public" 45 | 08-02 11:26:49 [INFO] nonebot | Running NoneBot... 46 | 08-02 11:26:49 [INFO] uvicorn | Started server process [5268] 47 | 08-02 11:26:49 [INFO] uvicorn | Waiting for application startup. 48 | 08-02 11:26:49 [INFO] uvicorn | Application startup complete. 49 | 08-02 11:26:49 [INFO] uvicorn | Uvicorn running on http://127.0.0.1:10219 (Press CTRL+C to quit) 50 | ``` 51 | **运行成功后请勿关闭此窗口,后续需要与 CQ-HTTP 连接。** 52 | 53 | ## Step 3. 连接 CQ-HTTP 54 | 55 | 前往 https://github.com/Mrs4s/go-cqhttp > Releases,下载适合自己操作系统的可执行文件。 56 | go-cqhttp 在初次启动时会询问代理方式,选择反向 websocket 代理即可。 57 | 58 | 之后用任何文本编辑器打开`config.yml`文件,设置反向 ws 地址、上报方式: 59 | ```yml 60 | message: 61 | post-format: array 62 | 63 | servers: 64 | - ws-reverse: 65 | universal: ws://127.0.0.1:10219/onebot/v11/ws 66 | ``` 67 | 然后设置您的 QQ 号和密码。您也可以不设置密码,选择扫码登陆的方式。 68 | 69 | 登陆成功后,后台应该会发送一条类似的信息: 70 | ``` 71 | 08-02 11:50:51 [INFO] nonebot | WebSocket Connection from CQHTTP Bot 114514 Accepted! 72 | ``` 73 | 至此,您可以和对应的 QQ 号聊天并使用 mai bot 的所有功能了。 74 | 75 | ## FAQ 76 | 77 | 不是 Windows 系统该怎么办? 78 | > 请自行查阅其他系统上的 Python 安装方式。cqhttp提供了其他系统的可执行文件,您也可以自行配置 golang module 环境进行编译。 79 | 80 | 配置 nonebot 或 cq-http 过程中出错? 81 | > 请查阅 https://github.com/nonebot/nonebot2 以及 https://github.com/Mrs4s/go-cqhttp 中的文档。 82 | 83 | 部分消息发不出来? 84 | > 被风控了。解决方式:换号或者让这个号保持登陆状态和一定的聊天频率,持续一段时间。 85 | 86 | ## 说明 87 | 88 | 本 bot 提供了如下功能: 89 | 90 | 命令 | 功能 91 | --- | --- 92 | help | 查看帮助文档 93 | 今日舞萌 | 查看今天的舞萌运势 94 | XXXmaimaiXXX什么 | 随机一首歌 95 | 随个[dx/标准][绿黄红紫白]<难度> | 随机一首指定条件的乐曲 96 | 查歌<乐曲标题的一部分> | 查询符合条件的乐曲 97 | [绿黄红紫白]id<歌曲编号> | 查询乐曲信息或谱面信息 98 | 定数查歌 <定数>
定数查歌 <定数下限> <定数上限> | 查询定数对应的乐曲 99 | 分数线 <难度+歌曲id> <分数线> | 展示歌曲的分数线 100 | 101 | ## License 102 | 103 | MIT 104 | 105 | 您可以自由使用本项目的代码用于商业或非商业的用途,但必须附带 MIT 授权协议。 106 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from collections import defaultdict 4 | 5 | import nonebot 6 | from nonebot.adapters.onebot.v11 import Adapter 7 | 8 | 9 | # Custom your logger 10 | # 11 | # from nonebot.log import logger, default_format 12 | # logger.add("error.log", 13 | # rotation="00:00", 14 | # diagnose=False, 15 | # level="ERROR", 16 | # format=default_format) 17 | 18 | # You can pass some keyword args config to init function 19 | nonebot.init() 20 | # nonebot.load_builtin_plugins() 21 | app = nonebot.get_asgi() 22 | 23 | driver = nonebot.get_driver() 24 | driver.register_adapter(Adapter) 25 | driver.config.help_text = {} 26 | 27 | 28 | nonebot.load_plugins("src/plugins") 29 | 30 | # Modify some config / config depends on loaded configs 31 | # 32 | # config = driver.config 33 | # do something... 34 | 35 | 36 | if __name__ == "__main__": 37 | nonebot.run() 38 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Diving-Fish/mai-bot/1c8c0eb2340898db5fd171092857f8918f162d06/requirements.txt -------------------------------------------------------------------------------- /src/libraries/image.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from io import BytesIO 3 | 4 | from PIL import ImageFont, ImageDraw, Image 5 | 6 | 7 | path = 'src/static/high_eq_image.png' 8 | fontpath = "src/static/msyh.ttc" 9 | 10 | 11 | def draw_text(img_pil, text, offset_x): 12 | draw = ImageDraw.Draw(img_pil) 13 | font = ImageFont.truetype(fontpath, 48) 14 | width, height = draw.textsize(text, font) 15 | x = 5 16 | if width > 390: 17 | font = ImageFont.truetype(fontpath, int(390 * 48 / width)) 18 | width, height = draw.textsize(text, font) 19 | else: 20 | x = int((400 - width) / 2) 21 | draw.rectangle((x + offset_x - 2, 360, x + 2 + width + offset_x, 360 + height * 1.2), fill=(0, 0, 0, 255)) 22 | draw.text((x + offset_x, 360), text, font=font, fill=(255, 255, 255, 255)) 23 | 24 | 25 | def text_to_image(text): 26 | font = ImageFont.truetype(fontpath, 24) 27 | padding = 10 28 | margin = 4 29 | text_list = text.split('\n') 30 | max_width = 0 31 | for text in text_list: 32 | w, h = font.getsize(text) 33 | max_width = max(max_width, w) 34 | wa = max_width + padding * 2 35 | ha = h * len(text_list) + margin * (len(text_list) - 1) + padding * 2 36 | i = Image.new('RGB', (wa, ha), color=(255, 255, 255)) 37 | draw = ImageDraw.Draw(i) 38 | for j in range(len(text_list)): 39 | text = text_list[j] 40 | draw.text((padding, padding + j * (margin + h)), text, font=font, fill=(0, 0, 0)) 41 | return i 42 | 43 | 44 | def image_to_base64(img, format='PNG'): 45 | output_buffer = BytesIO() 46 | img.save(output_buffer, format) 47 | byte_data = output_buffer.getvalue() 48 | base64_str = base64.b64encode(byte_data) 49 | return base64_str 50 | -------------------------------------------------------------------------------- /src/libraries/maimai_best_40.py: -------------------------------------------------------------------------------- 1 | # Author: xyb, Diving_Fish 2 | import asyncio 3 | import os 4 | import math 5 | from typing import Optional, Dict, List 6 | 7 | import aiohttp 8 | from PIL import Image, ImageDraw, ImageFont, ImageFilter 9 | from src.libraries.maimaidx_music import get_cover_len5_id, total_list 10 | 11 | 12 | scoreRank = 'D C B BB BBB A AA AAA S S+ SS SS+ SSS SSS+'.split(' ') 13 | combo = ' FC FC+ AP AP+'.split(' ') 14 | diffs = 'Basic Advanced Expert Master Re:Master'.split(' ') 15 | 16 | 17 | class ChartInfo(object): 18 | def __init__(self, idNum:str, diff:int, tp:str, achievement:float, ra:int, comboId:int, scoreId:int, 19 | title:str, ds:float, lv:str): 20 | self.idNum = idNum 21 | self.diff = diff 22 | self.tp = tp 23 | self.achievement = achievement 24 | self.ra = ra 25 | self.comboId = comboId 26 | self.scoreId = scoreId 27 | self.title = title 28 | self.ds = ds 29 | self.lv = lv 30 | 31 | def __str__(self): 32 | return '%-50s' % f'{self.title} [{self.tp}]' + f'{self.ds}\t{diffs[self.diff]}\t{self.ra}' 33 | 34 | def __eq__(self, other): 35 | return self.ra == other.ra 36 | 37 | def __lt__(self, other): 38 | return self.ra < other.ra 39 | 40 | @classmethod 41 | def from_json(cls, data): 42 | rate = ['d', 'c', 'b', 'bb', 'bbb', 'a', 'aa', 'aaa', 's', 'sp', 'ss', 'ssp', 'sss', 'sssp'] 43 | ri = rate.index(data["rate"]) 44 | fc = ['', 'fc', 'fcp', 'ap', 'app'] 45 | fi = fc.index(data["fc"]) 46 | return cls( 47 | idNum=total_list.by_title(data["title"]).id, 48 | title=data["title"], 49 | diff=data["level_index"], 50 | ra=data["ra"], 51 | ds=data["ds"], 52 | comboId=fi, 53 | scoreId=ri, 54 | lv=data["level"], 55 | achievement=data["achievements"], 56 | tp=data["type"] 57 | ) 58 | 59 | 60 | 61 | class BestList(object): 62 | 63 | def __init__(self, size:int): 64 | self.data = [] 65 | self.size = size 66 | 67 | def push(self, elem:ChartInfo): 68 | if len(self.data) >= self.size and elem < self.data[-1]: 69 | return 70 | self.data.append(elem) 71 | self.data.sort() 72 | self.data.reverse() 73 | while(len(self.data) > self.size): 74 | del self.data[-1] 75 | 76 | def pop(self): 77 | del self.data[-1] 78 | 79 | def __str__(self): 80 | return '[\n\t' + ', \n\t'.join([str(ci) for ci in self.data]) + '\n]' 81 | 82 | def __len__(self): 83 | return len(self.data) 84 | 85 | def __getitem__(self, index): 86 | return self.data[index] 87 | 88 | 89 | class DrawBest(object): 90 | 91 | def __init__(self, sdBest:BestList, dxBest:BestList, userName:str, playerRating:int, musicRating:int): 92 | self.sdBest = sdBest 93 | self.dxBest = dxBest 94 | self.userName = self._stringQ2B(userName) 95 | self.playerRating = playerRating 96 | self.musicRating = musicRating 97 | self.rankRating = self.playerRating - self.musicRating 98 | self.pic_dir = 'src/static/mai/pic/' 99 | self.cover_dir = 'src/static/mai/cover/' 100 | self.img = Image.open(self.pic_dir + 'UI_TTR_BG_Base_Plus.png').convert('RGBA') 101 | self.ROWS_IMG = [2] 102 | for i in range(6): 103 | self.ROWS_IMG.append(116 + 96 * i) 104 | self.COLOUMS_IMG = [] 105 | for i in range(6): 106 | self.COLOUMS_IMG.append(2 + 172 * i) 107 | for i in range(4): 108 | self.COLOUMS_IMG.append(888 + 172 * i) 109 | self.draw() 110 | 111 | def _Q2B(self, uchar): 112 | """单个字符 全角转半角""" 113 | inside_code = ord(uchar) 114 | if inside_code == 0x3000: 115 | inside_code = 0x0020 116 | else: 117 | inside_code -= 0xfee0 118 | if inside_code < 0x0020 or inside_code > 0x7e: #转完之后不是半角字符返回原来的字符 119 | return uchar 120 | return chr(inside_code) 121 | 122 | def _stringQ2B(self, ustring): 123 | """把字符串全角转半角""" 124 | return "".join([self._Q2B(uchar) for uchar in ustring]) 125 | 126 | def _getCharWidth(self, o) -> int: 127 | widths = [ 128 | (126, 1), (159, 0), (687, 1), (710, 0), (711, 1), (727, 0), (733, 1), (879, 0), (1154, 1), (1161, 0), 129 | (4347, 1), (4447, 2), (7467, 1), (7521, 0), (8369, 1), (8426, 0), (9000, 1), (9002, 2), (11021, 1), 130 | (12350, 2), (12351, 1), (12438, 2), (12442, 0), (19893, 2), (19967, 1), (55203, 2), (63743, 1), 131 | (64106, 2), (65039, 1), (65059, 0), (65131, 2), (65279, 1), (65376, 2), (65500, 1), (65510, 2), 132 | (120831, 1), (262141, 2), (1114109, 1), 133 | ] 134 | if o == 0xe or o == 0xf: 135 | return 0 136 | for num, wid in widths: 137 | if o <= num: 138 | return wid 139 | return 1 140 | 141 | def _coloumWidth(self, s:str): 142 | res = 0 143 | for ch in s: 144 | res += self._getCharWidth(ord(ch)) 145 | return res 146 | 147 | def _changeColumnWidth(self, s:str, len:int) -> str: 148 | res = 0 149 | sList = [] 150 | for ch in s: 151 | res += self._getCharWidth(ord(ch)) 152 | if res <= len: 153 | sList.append(ch) 154 | return ''.join(sList) 155 | 156 | def _resizePic(self, img:Image.Image, time:float): 157 | return img.resize((int(img.size[0] * time), int(img.size[1] * time))) 158 | 159 | def _findRaPic(self) -> str: 160 | num = '10' 161 | if self.playerRating < 1000: 162 | num = '01' 163 | elif self.playerRating < 2000: 164 | num = '02' 165 | elif self.playerRating < 3000: 166 | num = '03' 167 | elif self.playerRating < 4000: 168 | num = '04' 169 | elif self.playerRating < 5000: 170 | num = '05' 171 | elif self.playerRating < 6000: 172 | num = '06' 173 | elif self.playerRating < 7000: 174 | num = '07' 175 | elif self.playerRating < 8000: 176 | num = '08' 177 | elif self.playerRating < 8500: 178 | num = '09' 179 | return f'UI_CMN_DXRating_S_{num}.png' 180 | 181 | def _drawRating(self, ratingBaseImg:Image.Image): 182 | COLOUMS_RATING = [86, 100, 115, 130, 145] 183 | theRa = self.playerRating 184 | i = 4 185 | while theRa: 186 | digit = theRa % 10 187 | theRa = theRa // 10 188 | digitImg = Image.open(self.pic_dir + f'UI_NUM_Drating_{digit}.png').convert('RGBA') 189 | digitImg = self._resizePic(digitImg, 0.6) 190 | ratingBaseImg.paste(digitImg, (COLOUMS_RATING[i] - 2, 9), mask=digitImg.split()[3]) 191 | i = i - 1 192 | return ratingBaseImg 193 | 194 | def _drawBestList(self, img:Image.Image, sdBest:BestList, dxBest:BestList): 195 | itemW = 164 196 | itemH = 88 197 | Color = [(69, 193, 36), (255, 186, 1), (255, 90, 102), (134, 49, 200), (217, 197, 233)] 198 | levelTriagle = [(itemW, 0), (itemW - 27, 0), (itemW, 27)] 199 | rankPic = 'D C B BB BBB A AA AAA S Sp SS SSp SSS SSSp'.split(' ') 200 | comboPic = ' FC FCp AP APp'.split(' ') 201 | imgDraw = ImageDraw.Draw(img) 202 | titleFontName = 'src/static/adobe_simhei.otf' 203 | for num in range(0, len(sdBest)): 204 | i = num // 5 205 | j = num % 5 206 | chartInfo = sdBest[num] 207 | pngPath = self.cover_dir + f'{get_cover_len5_id(chartInfo.idNum)}.png' 208 | if not os.path.exists(pngPath): 209 | pngPath = self.cover_dir + '01000.png' 210 | temp = Image.open(pngPath).convert('RGB') 211 | temp = self._resizePic(temp, itemW / temp.size[0]) 212 | temp = temp.crop((0, (temp.size[1] - itemH) / 2, itemW, (temp.size[1] + itemH) / 2)) 213 | temp = temp.filter(ImageFilter.GaussianBlur(3)) 214 | temp = temp.point(lambda p: int(p * 0.72)) 215 | 216 | tempDraw = ImageDraw.Draw(temp) 217 | tempDraw.polygon(levelTriagle, Color[chartInfo.diff]) 218 | font = ImageFont.truetype(titleFontName, 16, encoding='utf-8') 219 | title = chartInfo.title 220 | if self._coloumWidth(title) > 15: 221 | title = self._changeColumnWidth(title, 14) + '...' 222 | tempDraw.text((8, 8), title, 'white', font) 223 | font = ImageFont.truetype(titleFontName, 14, encoding='utf-8') 224 | 225 | tempDraw.text((7, 28), f'{"%.4f" % chartInfo.achievement}%', 'white', font) 226 | rankImg = Image.open(self.pic_dir + f'UI_GAM_Rank_{rankPic[chartInfo.scoreId]}.png').convert('RGBA') 227 | rankImg = self._resizePic(rankImg, 0.3) 228 | temp.paste(rankImg, (88, 28), rankImg.split()[3]) 229 | if chartInfo.comboId: 230 | comboImg = Image.open(self.pic_dir + f'UI_MSS_MBase_Icon_{comboPic[chartInfo.comboId]}_S.png').convert('RGBA') 231 | comboImg = self._resizePic(comboImg, 0.45) 232 | temp.paste(comboImg, (119, 27), comboImg.split()[3]) 233 | font = ImageFont.truetype('src/static/adobe_simhei.otf', 12, encoding='utf-8') 234 | tempDraw.text((8, 44), f'Base: {chartInfo.ds} -> {chartInfo.ra}', 'white', font) 235 | font = ImageFont.truetype('src/static/adobe_simhei.otf', 18, encoding='utf-8') 236 | tempDraw.text((8, 60), f'#{num + 1}', 'white', font) 237 | 238 | recBase = Image.new('RGBA', (itemW, itemH), 'black') 239 | recBase = recBase.point(lambda p: int(p * 0.8)) 240 | img.paste(recBase, (self.COLOUMS_IMG[j] + 5, self.ROWS_IMG[i + 1] + 5)) 241 | img.paste(temp, (self.COLOUMS_IMG[j] + 4, self.ROWS_IMG[i + 1] + 4)) 242 | for num in range(len(sdBest), sdBest.size): 243 | i = num // 5 244 | j = num % 5 245 | temp = Image.open(self.cover_dir + f'01000.png').convert('RGB') 246 | temp = self._resizePic(temp, itemW / temp.size[0]) 247 | temp = temp.crop((0, (temp.size[1] - itemH) / 2, itemW, (temp.size[1] + itemH) / 2)) 248 | temp = temp.filter(ImageFilter.GaussianBlur(1)) 249 | img.paste(temp, (self.COLOUMS_IMG[j] + 4, self.ROWS_IMG[i + 1] + 4)) 250 | for num in range(0, len(dxBest)): 251 | i = num // 3 252 | j = num % 3 253 | chartInfo = dxBest[num] 254 | pngPath = self.cover_dir + f'{get_cover_len5_id(chartInfo.idNum)}.png' 255 | if not os.path.exists(pngPath): 256 | pngPath = self.cover_dir + '01000.png' 257 | temp = Image.open(pngPath).convert('RGB') 258 | temp = self._resizePic(temp, itemW / temp.size[0]) 259 | temp = temp.crop((0, (temp.size[1] - itemH) / 2, itemW, (temp.size[1] + itemH) / 2)) 260 | temp = temp.filter(ImageFilter.GaussianBlur(3)) 261 | temp = temp.point(lambda p: int(p * 0.72)) 262 | 263 | tempDraw = ImageDraw.Draw(temp) 264 | tempDraw.polygon(levelTriagle, Color[chartInfo.diff]) 265 | font = ImageFont.truetype(titleFontName, 16, encoding='utf-8') 266 | title = chartInfo.title 267 | if self._coloumWidth(title) > 15: 268 | title = self._changeColumnWidth(title, 14) + '...' 269 | tempDraw.text((8, 8), title, 'white', font) 270 | font = ImageFont.truetype(titleFontName, 14, encoding='utf-8') 271 | 272 | tempDraw.text((7, 28), f'{"%.4f" % chartInfo.achievement}%', 'white', font) 273 | rankImg = Image.open(self.pic_dir + f'UI_GAM_Rank_{rankPic[chartInfo.scoreId]}.png').convert('RGBA') 274 | rankImg = self._resizePic(rankImg, 0.3) 275 | temp.paste(rankImg, (88, 28), rankImg.split()[3]) 276 | if chartInfo.comboId: 277 | comboImg = Image.open(self.pic_dir + f'UI_MSS_MBase_Icon_{comboPic[chartInfo.comboId]}_S.png').convert( 278 | 'RGBA') 279 | comboImg = self._resizePic(comboImg, 0.45) 280 | temp.paste(comboImg, (119, 27), comboImg.split()[3]) 281 | font = ImageFont.truetype('src/static/adobe_simhei.otf', 12, encoding='utf-8') 282 | tempDraw.text((8, 44), f'Base: {chartInfo.ds} -> {chartInfo.ra}', 'white', font) 283 | font = ImageFont.truetype('src/static/adobe_simhei.otf', 18, encoding='utf-8') 284 | tempDraw.text((8, 60), f'#{num + 1}', 'white', font) 285 | 286 | recBase = Image.new('RGBA', (itemW, itemH), 'black') 287 | recBase = recBase.point(lambda p: int(p * 0.8)) 288 | img.paste(recBase, (self.COLOUMS_IMG[j + 6] + 5, self.ROWS_IMG[i + 1] + 5)) 289 | img.paste(temp, (self.COLOUMS_IMG[j + 6] + 4, self.ROWS_IMG[i + 1] + 4)) 290 | for num in range(len(dxBest), dxBest.size): 291 | i = num // 3 292 | j = num % 3 293 | temp = Image.open(self.cover_dir + f'01000.png').convert('RGB') 294 | temp = self._resizePic(temp, itemW / temp.size[0]) 295 | temp = temp.crop((0, (temp.size[1] - itemH) / 2, itemW, (temp.size[1] + itemH) / 2)) 296 | temp = temp.filter(ImageFilter.GaussianBlur(1)) 297 | img.paste(temp, (self.COLOUMS_IMG[j + 6] + 4, self.ROWS_IMG[i + 1] + 4)) 298 | 299 | def draw(self): 300 | splashLogo = Image.open(self.pic_dir + 'UI_CMN_TabTitle_MaimaiTitle_Ver214.png').convert('RGBA') 301 | splashLogo = self._resizePic(splashLogo, 0.65) 302 | self.img.paste(splashLogo, (10, 10), mask=splashLogo.split()[3]) 303 | 304 | ratingBaseImg = Image.open(self.pic_dir + self._findRaPic()).convert('RGBA') 305 | ratingBaseImg = self._drawRating(ratingBaseImg) 306 | ratingBaseImg = self._resizePic(ratingBaseImg, 0.85) 307 | self.img.paste(ratingBaseImg, (240, 8), mask=ratingBaseImg.split()[3]) 308 | 309 | namePlateImg = Image.open(self.pic_dir + 'UI_TST_PlateMask.png').convert('RGBA') 310 | namePlateImg = namePlateImg.resize((285, 40)) 311 | namePlateDraw = ImageDraw.Draw(namePlateImg) 312 | font1 = ImageFont.truetype('src/static/msyh.ttc', 28, encoding='unic') 313 | namePlateDraw.text((12, 4), ' '.join(list(self.userName)), 'black', font1) 314 | nameDxImg = Image.open(self.pic_dir + 'UI_CMN_Name_DX.png').convert('RGBA') 315 | nameDxImg = self._resizePic(nameDxImg, 0.9) 316 | namePlateImg.paste(nameDxImg, (230, 4), mask=nameDxImg.split()[3]) 317 | self.img.paste(namePlateImg, (240, 40), mask=namePlateImg.split()[3]) 318 | 319 | shougouImg = Image.open(self.pic_dir + 'UI_CMN_Shougou_Rainbow.png').convert('RGBA') 320 | shougouDraw = ImageDraw.Draw(shougouImg) 321 | font2 = ImageFont.truetype('src/static/adobe_simhei.otf', 14, encoding='utf-8') 322 | playCountInfo = f'底分: {self.musicRating} + 段位分: {self.rankRating}' 323 | shougouImgW, shougouImgH = shougouImg.size 324 | # playCountInfoW, playCountInfoH = shougouDraw.textsize(playCountInfo, font2) # 过时的用法 325 | # textPos = ((shougouImgW - playCountInfoW - font2.getoffset(playCountInfo)[0]) / 2, 5) 326 | bbox = shougouDraw.textbbox((0, 0), playCountInfo, font2) 327 | playCountInfoW = bbox[2] - bbox[0] 328 | playCountInfoH = bbox[3] - bbox[1] 329 | textPos = ((shougouImgW - playCountInfoW - font2.getbbox(playCountInfo)) / 2, 5) 330 | shougouDraw.text((textPos[0] - 1, textPos[1]), playCountInfo, 'black', font2) 331 | shougouDraw.text((textPos[0] + 1, textPos[1]), playCountInfo, 'black', font2) 332 | shougouDraw.text((textPos[0], textPos[1] - 1), playCountInfo, 'black', font2) 333 | shougouDraw.text((textPos[0], textPos[1] + 1), playCountInfo, 'black', font2) 334 | shougouDraw.text((textPos[0] - 1, textPos[1] - 1), playCountInfo, 'black', font2) 335 | shougouDraw.text((textPos[0] + 1, textPos[1] - 1), playCountInfo, 'black', font2) 336 | shougouDraw.text((textPos[0] - 1, textPos[1] + 1), playCountInfo, 'black', font2) 337 | shougouDraw.text((textPos[0] + 1, textPos[1] + 1), playCountInfo, 'black', font2) 338 | shougouDraw.text(textPos, playCountInfo, 'white', font2) 339 | shougouImg = self._resizePic(shougouImg, 1.05) 340 | self.img.paste(shougouImg, (240, 83), mask=shougouImg.split()[3]) 341 | 342 | self._drawBestList(self.img, self.sdBest, self.dxBest) 343 | 344 | authorBoardImg = Image.open(self.pic_dir + 'UI_CMN_MiniDialog_01.png').convert('RGBA') 345 | authorBoardImg = self._resizePic(authorBoardImg, 0.35) 346 | authorBoardDraw = ImageDraw.Draw(authorBoardImg) 347 | authorBoardDraw.text((31, 28), ' Generated By\nXybBot & Chiyuki', 'black', font2) 348 | self.img.paste(authorBoardImg, (1224, 19), mask=authorBoardImg.split()[3]) 349 | 350 | dxImg = Image.open(self.pic_dir + 'UI_RSL_MBase_Parts_01.png').convert('RGBA') 351 | self.img.paste(dxImg, (890, 65), mask=dxImg.split()[3]) 352 | sdImg = Image.open(self.pic_dir + 'UI_RSL_MBase_Parts_02.png').convert('RGBA') 353 | self.img.paste(sdImg, (758, 65), mask=sdImg.split()[3]) 354 | 355 | # self.img.show() 356 | 357 | def getDir(self): 358 | return self.img 359 | 360 | 361 | def computeRa(ds: float, achievement:float) -> int: 362 | baseRa = 15.0 363 | if achievement >= 50 and achievement < 60: 364 | baseRa = 5.0 365 | elif achievement < 70: 366 | baseRa = 6.0 367 | elif achievement < 75: 368 | baseRa = 7.0 369 | elif achievement < 80: 370 | baseRa = 7.5 371 | elif achievement < 90: 372 | baseRa = 8.0 373 | elif achievement < 94: 374 | baseRa = 9.0 375 | elif achievement < 97: 376 | baseRa = 9.4 377 | elif achievement < 98: 378 | baseRa = 10.0 379 | elif achievement < 99: 380 | baseRa = 11.0 381 | elif achievement < 99.5: 382 | baseRa = 12.0 383 | elif achievement < 99.99: 384 | baseRa = 13.0 385 | elif achievement < 100: 386 | baseRa = 13.5 387 | elif achievement < 100.5: 388 | baseRa = 14.0 389 | 390 | return math.floor(ds * (min(100.5, achievement) / 100) * baseRa) 391 | 392 | 393 | async def generate(payload: Dict) -> (Optional[Image.Image], bool): 394 | async with aiohttp.request("POST", "https://www.diving-fish.com/api/maimaidxprober/query/player", json=payload) as resp: 395 | if resp.status == 400: 396 | return None, 400 397 | if resp.status == 403: 398 | return None, 403 399 | sd_best = BestList(25) 400 | dx_best = BestList(15) 401 | obj = await resp.json() 402 | dx: List[Dict] = obj["charts"]["dx"] 403 | sd: List[Dict] = obj["charts"]["sd"] 404 | for c in sd: 405 | sd_best.push(ChartInfo.from_json(c)) 406 | for c in dx: 407 | dx_best.push(ChartInfo.from_json(c)) 408 | pic = DrawBest(sd_best, dx_best, obj["nickname"], obj["rating"] + obj["additional_rating"], obj["rating"]).getDir() 409 | return pic, 0 410 | -------------------------------------------------------------------------------- /src/libraries/maimai_best_50.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import math 4 | from typing import Optional, Dict, List, Tuple 5 | 6 | import aiohttp 7 | from PIL import Image, ImageDraw, ImageFont, ImageFilter 8 | from src.libraries.maimaidx_music import total_list, get_cover_len5_id 9 | 10 | 11 | scoreRank = 'D C B BB BBB A AA AAA S S+ SS SS+ SSS SSS+'.split(' ') 12 | combo = ' FC FC+ AP AP+'.split(' ') 13 | diffs = 'Basic Advanced Expert Master Re:Master'.split(' ') 14 | 15 | 16 | class ChartInfo(object): 17 | def __init__(self, idNum:str, diff:int, tp:str, achievement:float, ra:int, comboId:int, scoreId:int, 18 | title:str, ds:float, lv:str): 19 | self.idNum = idNum 20 | self.diff = diff 21 | self.tp = tp 22 | self.achievement = achievement 23 | self.ra = computeRa(ds,achievement) 24 | self.comboId = comboId 25 | self.scoreId = scoreId 26 | self.title = title 27 | self.ds = ds 28 | self.lv = lv 29 | 30 | def __str__(self): 31 | return '%-50s' % f'{self.title} [{self.tp}]' + f'{self.ds}\t{diffs[self.diff]}\t{self.ra}' 32 | 33 | def __eq__(self, other): 34 | return self.ra == other.ra 35 | 36 | def __lt__(self, other): 37 | return self.ra < other.ra 38 | 39 | @classmethod 40 | def from_json(cls, data): 41 | rate = ['d', 'c', 'b', 'bb', 'bbb', 'a', 'aa', 'aaa', 's', 'sp', 'ss', 'ssp', 'sss', 'sssp'] 42 | ri = rate.index(data["rate"]) 43 | fc = ['', 'fc', 'fcp', 'ap', 'app'] 44 | fi = fc.index(data["fc"]) 45 | return cls( 46 | idNum=total_list.by_title(data["title"]).id, 47 | title=data["title"], 48 | diff=data["level_index"], 49 | ra=data["ra"], 50 | ds=data["ds"], 51 | comboId=fi, 52 | scoreId=ri, 53 | lv=data["level"], 54 | achievement=data["achievements"], 55 | tp=data["type"] 56 | ) 57 | 58 | 59 | 60 | class BestList(object): 61 | 62 | def __init__(self, size:int): 63 | self.data = [] 64 | self.size = size 65 | 66 | def push(self, elem:ChartInfo): 67 | if len(self.data) >= self.size and elem < self.data[-1]: 68 | return 69 | self.data.append(elem) 70 | self.data.sort() 71 | self.data.reverse() 72 | while(len(self.data) > self.size): 73 | del self.data[-1] 74 | 75 | def pop(self): 76 | del self.data[-1] 77 | 78 | def __str__(self): 79 | return '[\n\t' + ', \n\t'.join([str(ci) for ci in self.data]) + '\n]' 80 | 81 | def __len__(self): 82 | return len(self.data) 83 | 84 | def __getitem__(self, index): 85 | return self.data[index] 86 | 87 | 88 | class DrawBest(object): 89 | 90 | def __init__(self, sdBest:BestList, dxBest:BestList, userName:str): 91 | self.sdBest = sdBest 92 | self.dxBest = dxBest 93 | self.userName = self._stringQ2B(userName) 94 | self.sdRating = 0 95 | self.dxRating = 0 96 | for sd in sdBest: 97 | self.sdRating += computeRa(sd.ds, sd.achievement) 98 | for dx in dxBest: 99 | self.dxRating += computeRa(dx.ds, dx.achievement) 100 | self.playerRating = self.sdRating + self.dxRating 101 | self.pic_dir = 'src/static/mai/pic/' 102 | self.cover_dir = 'src/static/mai/cover/' 103 | self.img = Image.open(self.pic_dir + 'UI_TTR_BG_Base_Plus.png').convert('RGBA') 104 | self.ROWS_IMG = [2] 105 | for i in range(6): 106 | self.ROWS_IMG.append(116 + 96 * i) 107 | self.COLOUMS_IMG = [] 108 | for i in range(8): 109 | self.COLOUMS_IMG.append(2 + 138 * i) 110 | for i in range(4): 111 | self.COLOUMS_IMG.append(988 + 138 * i) 112 | self.draw() 113 | 114 | def _Q2B(self, uchar): 115 | """单个字符 全角转半角""" 116 | inside_code = ord(uchar) 117 | if inside_code == 0x3000: 118 | inside_code = 0x0020 119 | else: 120 | inside_code -= 0xfee0 121 | if inside_code < 0x0020 or inside_code > 0x7e: #转完之后不是半角字符返回原来的字符 122 | return uchar 123 | return chr(inside_code) 124 | 125 | def _stringQ2B(self, ustring): 126 | """把字符串全角转半角""" 127 | return "".join([self._Q2B(uchar) for uchar in ustring]) 128 | 129 | def _getCharWidth(self, o) -> int: 130 | widths = [ 131 | (126, 1), (159, 0), (687, 1), (710, 0), (711, 1), (727, 0), (733, 1), (879, 0), (1154, 1), (1161, 0), 132 | (4347, 1), (4447, 2), (7467, 1), (7521, 0), (8369, 1), (8426, 0), (9000, 1), (9002, 2), (11021, 1), 133 | (12350, 2), (12351, 1), (12438, 2), (12442, 0), (19893, 2), (19967, 1), (55203, 2), (63743, 1), 134 | (64106, 2), (65039, 1), (65059, 0), (65131, 2), (65279, 1), (65376, 2), (65500, 1), (65510, 2), 135 | (120831, 1), (262141, 2), (1114109, 1), 136 | ] 137 | if o == 0xe or o == 0xf: 138 | return 0 139 | for num, wid in widths: 140 | if o <= num: 141 | return wid 142 | return 1 143 | 144 | def _coloumWidth(self, s:str): 145 | res = 0 146 | for ch in s: 147 | res += self._getCharWidth(ord(ch)) 148 | return res 149 | 150 | def _changeColumnWidth(self, s:str, len:int) -> str: 151 | res = 0 152 | sList = [] 153 | for ch in s: 154 | res += self._getCharWidth(ord(ch)) 155 | if res <= len: 156 | sList.append(ch) 157 | return ''.join(sList) 158 | 159 | def _resizePic(self, img:Image.Image, time:float): 160 | return img.resize((int(img.size[0] * time), int(img.size[1] * time))) 161 | 162 | def _findRaPic(self) -> str: 163 | num = '10' 164 | if self.playerRating < 1000: 165 | num = '01' 166 | elif self.playerRating < 2000: 167 | num = '02' 168 | elif self.playerRating < 4000: 169 | num = '03' 170 | elif self.playerRating < 7000: 171 | num = '04' 172 | elif self.playerRating < 10000: 173 | num = '05' 174 | elif self.playerRating < 12000: 175 | num = '06' 176 | elif self.playerRating < 13000: 177 | num = '07' 178 | elif self.playerRating < 14500: 179 | num = '08' 180 | elif self.playerRating < 15000: 181 | num = '09' 182 | return f'UI_CMN_DXRating_S_{num}.png' 183 | 184 | def _drawRating(self, ratingBaseImg:Image.Image): 185 | COLOUMS_RATING = [86, 100, 115, 130, 145] 186 | theRa = self.playerRating 187 | i = 4 188 | while theRa: 189 | digit = theRa % 10 190 | theRa = theRa // 10 191 | digitImg = Image.open(self.pic_dir + f'UI_NUM_Drating_{digit}.png').convert('RGBA') 192 | digitImg = self._resizePic(digitImg, 0.6) 193 | ratingBaseImg.paste(digitImg, (COLOUMS_RATING[i] - 2, 9), mask=digitImg.split()[3]) 194 | i = i - 1 195 | return ratingBaseImg 196 | 197 | def _drawBestList(self, img:Image.Image, sdBest:BestList, dxBest:BestList): 198 | itemW = 131 199 | itemH = 88 200 | Color = [(69, 193, 36), (255, 186, 1), (255, 90, 102), (134, 49, 200), (217, 197, 233)] 201 | levelTriagle = [(itemW, 0), (itemW - 27, 0), (itemW, 27)] 202 | rankPic = 'D C B BB BBB A AA AAA S Sp SS SSp SSS SSSp'.split(' ') 203 | comboPic = ' FC FCp AP APp'.split(' ') 204 | imgDraw = ImageDraw.Draw(img) 205 | titleFontName = 'src/static/adobe_simhei.otf' 206 | for num in range(0, len(sdBest)): 207 | i = num // 7 208 | j = num % 7 209 | chartInfo = sdBest[num] 210 | pngPath = self.cover_dir + f'{get_cover_len5_id(chartInfo.idNum)}.png' 211 | if not os.path.exists(pngPath): 212 | pngPath = self.cover_dir + '01000.png' 213 | temp = Image.open(pngPath).convert('RGB') 214 | temp = self._resizePic(temp, itemW / temp.size[0]) 215 | temp = temp.crop((0, (temp.size[1] - itemH) / 2, itemW, (temp.size[1] + itemH) / 2)) 216 | temp = temp.filter(ImageFilter.GaussianBlur(3)) 217 | temp = temp.point(lambda p: int(p * 0.72)) 218 | 219 | tempDraw = ImageDraw.Draw(temp) 220 | tempDraw.polygon(levelTriagle, Color[chartInfo.diff]) 221 | font = ImageFont.truetype(titleFontName, 16, encoding='utf-8') 222 | title = chartInfo.title 223 | if self._coloumWidth(title) > 15: 224 | title = self._changeColumnWidth(title, 12) + '...' 225 | tempDraw.text((8, 8), title, 'white', font) 226 | font = ImageFont.truetype(titleFontName, 12, encoding='utf-8') 227 | 228 | tempDraw.text((7, 28), f'{"%.4f" % chartInfo.achievement}%', 'white', font) 229 | rankImg = Image.open(self.pic_dir + f'UI_GAM_Rank_{rankPic[chartInfo.scoreId]}.png').convert('RGBA') 230 | rankImg = self._resizePic(rankImg, 0.3) 231 | temp.paste(rankImg, (72, 28), rankImg.split()[3]) 232 | if chartInfo.comboId: 233 | comboImg = Image.open(self.pic_dir + f'UI_MSS_MBase_Icon_{comboPic[chartInfo.comboId]}_S.png').convert('RGBA') 234 | comboImg = self._resizePic(comboImg, 0.45) 235 | temp.paste(comboImg, (103, 27), comboImg.split()[3]) 236 | font = ImageFont.truetype('src/static/adobe_simhei.otf', 12, encoding='utf-8') 237 | tempDraw.text((8, 44), f'Base: {chartInfo.ds} -> {computeRa(chartInfo.ds, chartInfo.achievement)}', 'white', font) 238 | font = ImageFont.truetype('src/static/adobe_simhei.otf', 18, encoding='utf-8') 239 | tempDraw.text((8, 60), f'#{num + 1}', 'white', font) 240 | 241 | recBase = Image.new('RGBA', (itemW, itemH), 'black') 242 | recBase = recBase.point(lambda p: int(p * 0.8)) 243 | img.paste(recBase, (self.COLOUMS_IMG[j] + 5, self.ROWS_IMG[i + 1] + 5)) 244 | img.paste(temp, (self.COLOUMS_IMG[j] + 4, self.ROWS_IMG[i + 1] + 4)) 245 | for num in range(len(sdBest), sdBest.size): 246 | i = num // 7 247 | j = num % 7 248 | temp = Image.open(self.cover_dir + f'01000.png').convert('RGB') 249 | temp = self._resizePic(temp, itemW / temp.size[0]) 250 | temp = temp.crop((0, (temp.size[1] - itemH) / 2, itemW, (temp.size[1] + itemH) / 2)) 251 | temp = temp.filter(ImageFilter.GaussianBlur(1)) 252 | img.paste(temp, (self.COLOUMS_IMG[j] + 4, self.ROWS_IMG[i + 1] + 4)) 253 | for num in range(0, len(dxBest)): 254 | i = num // 3 255 | j = num % 3 256 | chartInfo = dxBest[num] 257 | pngPath = self.cover_dir + f'{get_cover_len5_id(chartInfo.idNum)}.png' 258 | if not os.path.exists(pngPath): 259 | pngPath = self.cover_dir + '01000.png' 260 | temp = Image.open(pngPath).convert('RGB') 261 | temp = self._resizePic(temp, itemW / temp.size[0]) 262 | temp = temp.crop((0, (temp.size[1] - itemH) / 2, itemW, (temp.size[1] + itemH) / 2)) 263 | temp = temp.filter(ImageFilter.GaussianBlur(3)) 264 | temp = temp.point(lambda p: int(p * 0.72)) 265 | 266 | tempDraw = ImageDraw.Draw(temp) 267 | tempDraw.polygon(levelTriagle, Color[chartInfo.diff]) 268 | font = ImageFont.truetype(titleFontName, 14, encoding='utf-8') 269 | title = chartInfo.title 270 | if self._coloumWidth(title) > 13: 271 | title = self._changeColumnWidth(title, 12) + '...' 272 | tempDraw.text((8, 8), title, 'white', font) 273 | font = ImageFont.truetype(titleFontName, 12, encoding='utf-8') 274 | 275 | tempDraw.text((7, 28), f'{"%.4f" % chartInfo.achievement}%', 'white', font) 276 | rankImg = Image.open(self.pic_dir + f'UI_GAM_Rank_{rankPic[chartInfo.scoreId]}.png').convert('RGBA') 277 | rankImg = self._resizePic(rankImg, 0.3) 278 | temp.paste(rankImg, (72, 28), rankImg.split()[3]) 279 | if chartInfo.comboId: 280 | comboImg = Image.open(self.pic_dir + f'UI_MSS_MBase_Icon_{comboPic[chartInfo.comboId]}_S.png').convert( 281 | 'RGBA') 282 | comboImg = self._resizePic(comboImg, 0.45) 283 | temp.paste(comboImg, (103, 27), comboImg.split()[3]) 284 | font = ImageFont.truetype('src/static/adobe_simhei.otf', 12, encoding='utf-8') 285 | tempDraw.text((8, 44), f'Base: {chartInfo.ds} -> {chartInfo.ra}', 'white', font) 286 | font = ImageFont.truetype('src/static/adobe_simhei.otf', 18, encoding='utf-8') 287 | tempDraw.text((8, 60), f'#{num + 1}', 'white', font) 288 | 289 | recBase = Image.new('RGBA', (itemW, itemH), 'black') 290 | recBase = recBase.point(lambda p: int(p * 0.8)) 291 | img.paste(recBase, (self.COLOUMS_IMG[j + 8] + 5, self.ROWS_IMG[i + 1] + 5)) 292 | img.paste(temp, (self.COLOUMS_IMG[j + 8] + 4, self.ROWS_IMG[i + 1] + 4)) 293 | for num in range(len(dxBest), dxBest.size): 294 | i = num // 3 295 | j = num % 3 296 | temp = Image.open(self.cover_dir + f'01000.png').convert('RGB') 297 | temp = self._resizePic(temp, itemW / temp.size[0]) 298 | temp = temp.crop((0, (temp.size[1] - itemH) / 2, itemW, (temp.size[1] + itemH) / 2)) 299 | temp = temp.filter(ImageFilter.GaussianBlur(1)) 300 | img.paste(temp, (self.COLOUMS_IMG[j + 8] + 4, self.ROWS_IMG[i + 1] + 4)) 301 | 302 | def draw(self): 303 | splashLogo = Image.open(self.pic_dir + 'UI_CMN_TabTitle_MaimaiTitle_Ver214.png').convert('RGBA') 304 | splashLogo = self._resizePic(splashLogo, 0.65) 305 | self.img.paste(splashLogo, (10, 10), mask=splashLogo.split()[3]) 306 | 307 | ratingBaseImg = Image.open(self.pic_dir + self._findRaPic()).convert('RGBA') 308 | ratingBaseImg = self._drawRating(ratingBaseImg) 309 | ratingBaseImg = self._resizePic(ratingBaseImg, 0.85) 310 | self.img.paste(ratingBaseImg, (240, 8), mask=ratingBaseImg.split()[3]) 311 | 312 | namePlateImg = Image.open(self.pic_dir + 'UI_TST_PlateMask.png').convert('RGBA') 313 | namePlateImg = namePlateImg.resize((285, 40)) 314 | namePlateDraw = ImageDraw.Draw(namePlateImg) 315 | font1 = ImageFont.truetype('src/static/msyh.ttc', 28, encoding='unic') 316 | namePlateDraw.text((12, 4), ' '.join(list(self.userName)), 'black', font1) 317 | nameDxImg = Image.open(self.pic_dir + 'UI_CMN_Name_DX.png').convert('RGBA') 318 | nameDxImg = self._resizePic(nameDxImg, 0.9) 319 | namePlateImg.paste(nameDxImg, (230, 4), mask=nameDxImg.split()[3]) 320 | self.img.paste(namePlateImg, (240, 40), mask=namePlateImg.split()[3]) 321 | 322 | shougouImg = Image.open(self.pic_dir + 'UI_CMN_Shougou_Rainbow.png').convert('RGBA') 323 | shougouDraw = ImageDraw.Draw(shougouImg) 324 | font2 = ImageFont.truetype('src/static/adobe_simhei.otf', 14, encoding='utf-8') 325 | playCountInfo = f'SD: {self.sdRating} + DX: {self.dxRating} = {self.playerRating}' 326 | shougouImgW, shougouImgH = shougouImg.size 327 | # playCountInfoW, playCountInfoH = shougouDraw.textsize(playCountInfo, font2) # 过时的用法textsize() 328 | # textPos = ((shougouImgW - playCountInfoW - font2.getoffset(playCountInfo)[0]) / 2, 5) # 过时的用法getoffset() 329 | bbox = shougouDraw.textbbox((0, 0), playCountInfo, font2) 330 | playCountInfoW = bbox[2] - bbox[0] 331 | playCountInfoH = bbox[3] - bbox[1] 332 | textPos = ((shougouImgW - playCountInfoW - font2.getbbox(playCountInfo)) / 2, 5) 333 | shougouDraw.text((textPos[0] - 1, textPos[1]), playCountInfo, 'black', font2) 334 | shougouDraw.text((textPos[0] + 1, textPos[1]), playCountInfo, 'black', font2) 335 | shougouDraw.text((textPos[0], textPos[1] - 1), playCountInfo, 'black', font2) 336 | shougouDraw.text((textPos[0], textPos[1] + 1), playCountInfo, 'black', font2) 337 | shougouDraw.text((textPos[0] - 1, textPos[1] - 1), playCountInfo, 'black', font2) 338 | shougouDraw.text((textPos[0] + 1, textPos[1] - 1), playCountInfo, 'black', font2) 339 | shougouDraw.text((textPos[0] - 1, textPos[1] + 1), playCountInfo, 'black', font2) 340 | shougouDraw.text((textPos[0] + 1, textPos[1] + 1), playCountInfo, 'black', font2) 341 | shougouDraw.text(textPos, playCountInfo, 'white', font2) 342 | shougouImg = self._resizePic(shougouImg, 1.05) 343 | self.img.paste(shougouImg, (240, 83), mask=shougouImg.split()[3]) 344 | 345 | self._drawBestList(self.img, self.sdBest, self.dxBest) 346 | 347 | authorBoardImg = Image.open(self.pic_dir + 'UI_CMN_MiniDialog_01.png').convert('RGBA') 348 | authorBoardImg = self._resizePic(authorBoardImg, 0.35) 349 | authorBoardDraw = ImageDraw.Draw(authorBoardImg) 350 | authorBoardDraw.text((31, 28), ' Generated By\nXybBot & Chiyuki', 'black', font2) 351 | self.img.paste(authorBoardImg, (1224, 19), mask=authorBoardImg.split()[3]) 352 | 353 | dxImg = Image.open(self.pic_dir + 'UI_RSL_MBase_Parts_01.png').convert('RGBA') 354 | self.img.paste(dxImg, (988, 65), mask=dxImg.split()[3]) 355 | sdImg = Image.open(self.pic_dir + 'UI_RSL_MBase_Parts_02.png').convert('RGBA') 356 | self.img.paste(sdImg, (865, 65), mask=sdImg.split()[3]) 357 | 358 | # self.img.show() 359 | 360 | def getDir(self): 361 | return self.img 362 | 363 | 364 | def computeRa(ds: float, achievement: float) -> int: 365 | baseRa = 22.4 366 | if achievement < 50: 367 | baseRa = 7.0 368 | elif achievement < 60: 369 | baseRa = 8.0 370 | elif achievement < 70: 371 | baseRa = 9.6 372 | elif achievement < 75: 373 | baseRa = 11.2 374 | elif achievement < 80: 375 | baseRa = 12.0 376 | elif achievement < 90: 377 | baseRa = 13.6 378 | elif achievement < 94: 379 | baseRa = 15.2 380 | elif achievement < 97: 381 | baseRa = 16.8 382 | elif achievement < 98: 383 | baseRa = 20.0 384 | elif achievement < 99: 385 | baseRa = 20.3 386 | elif achievement < 99.5: 387 | baseRa = 20.8 388 | elif achievement < 100: 389 | baseRa = 21.1 390 | elif achievement < 100.5: 391 | baseRa = 21.6 392 | 393 | return math.floor(ds * (min(100.5, achievement) / 100) * baseRa) 394 | 395 | 396 | async def generate50(payload: Dict) -> Tuple[Optional[Image.Image], bool]: 397 | async with aiohttp.request("POST", "https://www.diving-fish.com/api/maimaidxprober/query/player", json=payload) as resp: 398 | if resp.status == 400: 399 | return None, 400 400 | if resp.status == 403: 401 | return None, 403 402 | sd_best = BestList(35) 403 | dx_best = BestList(15) 404 | obj = await resp.json() 405 | dx: List[Dict] = obj["charts"]["dx"] 406 | sd: List[Dict] = obj["charts"]["sd"] 407 | for c in sd: 408 | sd_best.push(ChartInfo.from_json(c)) 409 | for c in dx: 410 | dx_best.push(ChartInfo.from_json(c)) 411 | pic = DrawBest(sd_best, dx_best, obj["nickname"]).getDir() 412 | return pic, 0 413 | -------------------------------------------------------------------------------- /src/libraries/maimaidx_music.py: -------------------------------------------------------------------------------- 1 | import json 2 | import random 3 | from typing import Dict, List, Optional, Union, Tuple, Any 4 | from copy import deepcopy 5 | 6 | import requests 7 | 8 | def get_cover_len5_id(mid) -> str: 9 | mid = int(mid) 10 | if mid > 10000 and mid <= 11000: 11 | mid -= 10000 12 | return f'{mid:05d}' 13 | 14 | def cross(checker: List[Any], elem: Optional[Union[Any, List[Any]]], diff): 15 | ret = False 16 | diff_ret = [] 17 | if not elem or elem is Ellipsis: 18 | return True, diff 19 | if isinstance(elem, List): 20 | for _j in (range(len(checker)) if diff is Ellipsis else diff): 21 | if _j >= len(checker): 22 | continue 23 | __e = checker[_j] 24 | if __e in elem: 25 | diff_ret.append(_j) 26 | ret = True 27 | elif isinstance(elem, Tuple): 28 | for _j in (range(len(checker)) if diff is Ellipsis else diff): 29 | if _j >= len(checker): 30 | continue 31 | __e = checker[_j] 32 | if elem[0] <= __e <= elem[1]: 33 | diff_ret.append(_j) 34 | ret = True 35 | else: 36 | for _j in (range(len(checker)) if diff is Ellipsis else diff): 37 | if _j >= len(checker): 38 | continue 39 | __e = checker[_j] 40 | if elem == __e: 41 | return True, [_j] 42 | return ret, diff_ret 43 | 44 | 45 | def in_or_equal(checker: Any, elem: Optional[Union[Any, List[Any]]]): 46 | if elem is Ellipsis: 47 | return True 48 | if isinstance(elem, List): 49 | return checker in elem 50 | elif isinstance(elem, Tuple): 51 | return elem[0] <= checker <= elem[1] 52 | else: 53 | return checker == elem 54 | 55 | 56 | class Chart(Dict): 57 | tap: Optional[int] = None 58 | slide: Optional[int] = None 59 | hold: Optional[int] = None 60 | touch: Optional[int] = None 61 | brk: Optional[int] = None 62 | charter: Optional[int] = None 63 | 64 | def __getattribute__(self, item): 65 | if item == 'tap': 66 | return self['notes'][0] 67 | elif item == 'hold': 68 | return self['notes'][1] 69 | elif item == 'slide': 70 | return self['notes'][2] 71 | elif item == 'touch': 72 | return self['notes'][3] if len(self['notes']) == 5 else 0 73 | elif item == 'brk': 74 | return self['notes'][-1] 75 | elif item == 'charter': 76 | return self['charter'] 77 | return super().__getattribute__(item) 78 | 79 | 80 | class Music(Dict): 81 | id: Optional[str] = None 82 | title: Optional[str] = None 83 | ds: Optional[List[float]] = None 84 | level: Optional[List[str]] = None 85 | genre: Optional[str] = None 86 | type: Optional[str] = None 87 | bpm: Optional[float] = None 88 | version: Optional[str] = None 89 | charts: Optional[Chart] = None 90 | release_date: Optional[str] = None 91 | artist: Optional[str] = None 92 | 93 | diff: List[int] = [] 94 | 95 | def __getattribute__(self, item): 96 | if item in {'genre', 'artist', 'release_date', 'bpm', 'version'}: 97 | if item == 'version': 98 | return self['basic_info']['from'] 99 | return self['basic_info'][item] 100 | elif item in self: 101 | return self[item] 102 | return super().__getattribute__(item) 103 | 104 | 105 | class MusicList(List[Music]): 106 | def by_id(self, music_id: str) -> Optional[Music]: 107 | for music in self: 108 | if music.id == music_id: 109 | return music 110 | return None 111 | 112 | def by_title(self, music_title: str) -> Optional[Music]: 113 | for music in self: 114 | if music.title == music_title: 115 | return music 116 | return None 117 | 118 | def random(self): 119 | return random.choice(self) 120 | 121 | def filter(self, 122 | *, 123 | level: Optional[Union[str, List[str]]] = ..., 124 | ds: Optional[Union[float, List[float], Tuple[float, float]]] = ..., 125 | title_search: Optional[str] = ..., 126 | genre: Optional[Union[str, List[str]]] = ..., 127 | bpm: Optional[Union[float, List[float], Tuple[float, float]]] = ..., 128 | type: Optional[Union[str, List[str]]] = ..., 129 | diff: List[int] = ..., 130 | ): 131 | new_list = MusicList() 132 | for music in self: 133 | diff2 = diff 134 | music = deepcopy(music) 135 | ret, diff2 = cross(music.level, level, diff2) 136 | if not ret: 137 | continue 138 | ret, diff2 = cross(music.ds, ds, diff2) 139 | if not ret: 140 | continue 141 | if not in_or_equal(music.genre, genre): 142 | continue 143 | if not in_or_equal(music.type, type): 144 | continue 145 | if not in_or_equal(music.bpm, bpm): 146 | continue 147 | if title_search is not Ellipsis and title_search.lower() not in music.title.lower(): 148 | continue 149 | music.diff = diff2 150 | new_list.append(music) 151 | return new_list 152 | 153 | 154 | obj = requests.get('https://www.diving-fish.com/api/maimaidxprober/music_data').json() 155 | total_list: MusicList = MusicList(obj) 156 | for __i in range(len(total_list)): 157 | total_list[__i] = Music(total_list[__i]) 158 | for __j in range(len(total_list[__i].charts)): 159 | total_list[__i].charts[__j] = Chart(total_list[__i].charts[__j]) 160 | -------------------------------------------------------------------------------- /src/libraries/tool.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | def hash(qq: int): 5 | days = int(time.strftime("%d", time.localtime(time.time()))) + 31 * int( 6 | time.strftime("%m", time.localtime(time.time()))) + 77 7 | return (days * qq) >> 8 8 | -------------------------------------------------------------------------------- /src/plugins/maimaidx.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command, on_regex 2 | from nonebot.params import CommandArg, EventMessage 3 | from nonebot.adapters import Event 4 | from nonebot.adapters.onebot.v11 import Message, MessageSegment 5 | 6 | from src.libraries.tool import hash 7 | from src.libraries.maimaidx_music import * 8 | from src.libraries.image import * 9 | from src.libraries.maimai_best_40 import generate 10 | from src.libraries.maimai_best_50 import generate50 11 | import re 12 | 13 | 14 | def song_txt(music: Music): 15 | return Message([ 16 | MessageSegment("text", { 17 | "text": f"{music.id}. {music.title}\n" 18 | }), 19 | MessageSegment("image", { 20 | "file": f"https://www.diving-fish.com/covers/{get_cover_len5_id(music.id)}.png" 21 | }), 22 | MessageSegment("text", { 23 | "text": f"\n{'/'.join(music.level)}" 24 | }) 25 | ]) 26 | 27 | 28 | def inner_level_q(ds1, ds2=None): 29 | result_set = [] 30 | diff_label = ['Bas', 'Adv', 'Exp', 'Mst', 'ReM'] 31 | if ds2 is not None: 32 | music_data = total_list.filter(ds=(ds1, ds2)) 33 | else: 34 | music_data = total_list.filter(ds=ds1) 35 | for music in sorted(music_data, key = lambda i: int(i['id'])): 36 | for i in music.diff: 37 | result_set.append((music['id'], music['title'], music['ds'][i], diff_label[i], music['level'][i])) 38 | return result_set 39 | 40 | 41 | inner_level = on_command('inner_level ', aliases={'定数查歌 '}) 42 | 43 | 44 | @inner_level.handle() 45 | async def _(event: Event, message: Message = CommandArg()): 46 | argv = str(message).strip().split(" ") 47 | if len(argv) > 2 or len(argv) == 0: 48 | await inner_level.finish("命令格式为\n定数查歌 <定数>\n定数查歌 <定数下限> <定数上限>") 49 | return 50 | if len(argv) == 1: 51 | result_set = inner_level_q(float(argv[0])) 52 | else: 53 | result_set = inner_level_q(float(argv[0]), float(argv[1])) 54 | if len(result_set) > 50: 55 | await inner_level.finish(f"结果过多({len(result_set)} 条),请缩小搜索范围。") 56 | return 57 | s = "" 58 | for elem in result_set: 59 | s += f"{elem[0]}. {elem[1]} {elem[3]} {elem[4]}({elem[2]})\n" 60 | await inner_level.finish(s.strip()) 61 | 62 | 63 | spec_rand = on_regex(r"^随个(?:dx|sd|标准)?[绿黄红紫白]?[0-9]+\+?") 64 | 65 | 66 | @spec_rand.handle() 67 | async def _(event: Event, message: Message = EventMessage()): 68 | level_labels = ['绿', '黄', '红', '紫', '白'] 69 | regex = "随个((?:dx|sd|标准))?([绿黄红紫白]?)([0-9]+\+?)" 70 | res = re.match(regex, str(message).lower()) 71 | try: 72 | if res.groups()[0] == "dx": 73 | tp = ["DX"] 74 | elif res.groups()[0] == "sd" or res.groups()[0] == "标准": 75 | tp = ["SD"] 76 | else: 77 | tp = ["SD", "DX"] 78 | level = res.groups()[2] 79 | if res.groups()[1] == "": 80 | music_data = total_list.filter(level=level, type=tp) 81 | else: 82 | music_data = total_list.filter(level=level, diff=['绿黄红紫白'.index(res.groups()[1])], type=tp) 83 | if len(music_data) == 0: 84 | rand_result = "没有这样的乐曲哦。" 85 | else: 86 | rand_result = song_txt(music_data.random()) 87 | await spec_rand.send(rand_result) 88 | except Exception as e: 89 | print(e) 90 | await spec_rand.finish("随机命令错误,请检查语法") 91 | 92 | 93 | mr = on_regex(r".*maimai.*什么") 94 | 95 | 96 | @mr.handle() 97 | async def _(): 98 | await mr.finish(song_txt(total_list.random())) 99 | 100 | 101 | search_music = on_regex(r"^查歌.+") 102 | 103 | 104 | @search_music.handle() 105 | async def _(event: Event, message: Message = EventMessage()): 106 | regex = "查歌(.+)" 107 | name = re.match(regex, str(message)).groups()[0].strip() 108 | if name == "": 109 | return 110 | res = total_list.filter(title_search=name) 111 | if len(res) == 0: 112 | await search_music.send("没有找到这样的乐曲。") 113 | elif len(res) < 50: 114 | search_result = "" 115 | for music in sorted(res, key = lambda i: int(i['id'])): 116 | search_result += f"{music['id']}. {music['title']}\n" 117 | await search_music.finish(Message([ 118 | MessageSegment("text", { 119 | "text": search_result.strip() 120 | })])) 121 | else: 122 | await search_music.send(f"结果过多({len(res)} 条),请缩小查询范围。") 123 | 124 | 125 | query_chart = on_regex(r"^([绿黄红紫白]?)id([0-9]+)") 126 | 127 | 128 | @query_chart.handle() 129 | async def _(event: Event, message: Message = EventMessage()): 130 | regex = "([绿黄红紫白]?)id([0-9]+)" 131 | groups = re.match(regex, str(message)).groups() 132 | level_labels = ['绿', '黄', '红', '紫', '白'] 133 | if groups[0] != "": 134 | try: 135 | level_index = level_labels.index(groups[0]) 136 | level_name = ['Basic', 'Advanced', 'Expert', 'Master', 'Re: MASTER'] 137 | name = groups[1] 138 | music = total_list.by_id(name) 139 | chart = music['charts'][level_index] 140 | ds = music['ds'][level_index] 141 | level = music['level'][level_index] 142 | file = f"https://www.diving-fish.com/covers/{get_cover_len5_id(music['id'])}.png" 143 | if len(chart['notes']) == 4: 144 | msg = f'''{level_name[level_index]} {level}({ds}) 145 | TAP: {chart['notes'][0]} 146 | HOLD: {chart['notes'][1]} 147 | SLIDE: {chart['notes'][2]} 148 | BREAK: {chart['notes'][3]} 149 | 谱师: {chart['charter']}''' 150 | else: 151 | msg = f'''{level_name[level_index]} {level}({ds}) 152 | TAP: {chart['notes'][0]} 153 | HOLD: {chart['notes'][1]} 154 | SLIDE: {chart['notes'][2]} 155 | TOUCH: {chart['notes'][3]} 156 | BREAK: {chart['notes'][4]} 157 | 谱师: {chart['charter']}''' 158 | await query_chart.send(Message([ 159 | MessageSegment("text", { 160 | "text": f"{music['id']}. {music['title']}\n" 161 | }), 162 | MessageSegment("image", { 163 | "file": f"{file}" 164 | }), 165 | MessageSegment("text", { 166 | "text": msg 167 | }) 168 | ])) 169 | except Exception: 170 | await query_chart.send("未找到该谱面") 171 | else: 172 | name = groups[1] 173 | music = total_list.by_id(name) 174 | try: 175 | file =f"https://www.diving-fish.com/covers/{get_cover_len5_id(music['id'])}.png" 176 | await query_chart.send(Message([ 177 | MessageSegment("text", { 178 | "text": f"{music['id']}. {music['title']}\n" 179 | }), 180 | MessageSegment("image", { 181 | "file": f"{file}" 182 | }), 183 | MessageSegment("text", { 184 | "text": f"艺术家: {music['basic_info']['artist']}\n分类: {music['basic_info']['genre']}\nBPM: {music['basic_info']['bpm']}\n版本: {music['basic_info']['from']}\n难度: {'/'.join(music['level'])}" 185 | }) 186 | ])) 187 | except Exception: 188 | await query_chart.send("未找到该乐曲") 189 | 190 | 191 | wm_list = ['拼机', '推分', '越级', '下埋', '夜勤', '练底力', '练手法', '打旧框', '干饭', '抓绝赞', '收歌'] 192 | 193 | 194 | jrwm = on_command('今日舞萌', aliases={'今日mai'}) 195 | 196 | 197 | @jrwm.handle() 198 | async def _(event: Event, message: Message = CommandArg()): 199 | qq = int(event.get_user_id()) 200 | h = hash(qq) 201 | rp = h % 100 202 | wm_value = [] 203 | for i in range(11): 204 | wm_value.append(h & 3) 205 | h >>= 2 206 | s = f"今日人品值:{rp}\n" 207 | for i in range(11): 208 | if wm_value[i] == 3: 209 | s += f'宜 {wm_list[i]}\n' 210 | elif wm_value[i] == 0: 211 | s += f'忌 {wm_list[i]}\n' 212 | s += "千雪提醒您:打机时不要大力拍打或滑动哦\n今日推荐歌曲:" 213 | music = total_list[h % len(total_list)] 214 | await jrwm.finish(Message([MessageSegment("text", {"text": s})] + song_txt(music))) 215 | 216 | query_score = on_command('分数线') 217 | 218 | 219 | @query_score.handle() 220 | async def _(event: Event, message: Message = CommandArg()): 221 | r = "([绿黄红紫白])(id)?([0-9]+)" 222 | argv = str(message).strip().split(" ") 223 | if len(argv) == 1 and argv[0] == '帮助': 224 | s = '''此功能为查找某首歌分数线设计。 225 | 命令格式:分数线 <难度+歌曲id> <分数线> 226 | 例如:分数线 紫799 100 227 | 命令将返回分数线允许的 TAP GREAT 容错以及 BREAK 50落等价的 TAP GREAT 数。 228 | 以下为 TAP GREAT 的对应表: 229 | GREAT/GOOD/MISS 230 | TAP\t1/2.5/5 231 | HOLD\t2/5/10 232 | SLIDE\t3/7.5/15 233 | TOUCH\t1/2.5/5 234 | BREAK\t5/12.5/25(外加200落)''' 235 | await query_score.send(Message([ 236 | MessageSegment("image", { 237 | "file": f"base64://{str(image_to_base64(text_to_image(s)), encoding='utf-8')}" 238 | }) 239 | ])) 240 | elif len(argv) == 2: 241 | try: 242 | grp = re.match(r, argv[0]).groups() 243 | level_labels = ['绿', '黄', '红', '紫', '白'] 244 | level_labels2 = ['Basic', 'Advanced', 'Expert', 'Master', 'Re:MASTER'] 245 | level_index = level_labels.index(grp[0]) 246 | chart_id = grp[2] 247 | line = float(argv[1]) 248 | music = total_list.by_id(chart_id) 249 | chart: Dict[Any] = music['charts'][level_index] 250 | tap = int(chart['notes'][0]) 251 | slide = int(chart['notes'][2]) 252 | hold = int(chart['notes'][1]) 253 | touch = int(chart['notes'][3]) if len(chart['notes']) == 5 else 0 254 | brk = int(chart['notes'][-1]) 255 | total_score = 500 * tap + slide * 1500 + hold * 1000 + touch * 500 + brk * 2500 256 | break_bonus = 0.01 / brk 257 | break_50_reduce = total_score * break_bonus / 4 258 | reduce = 101 - line 259 | if reduce <= 0 or reduce >= 101: 260 | raise ValueError 261 | await query_chart.send(f'''{music['title']} {level_labels2[level_index]} 262 | 分数线 {line}% 允许的最多 TAP GREAT 数量为 {(total_score * reduce / 10000):.2f}(每个-{10000 / total_score:.4f}%), 263 | BREAK 50落(一共{brk}个)等价于 {(break_50_reduce / 100):.3f} 个 TAP GREAT(-{break_50_reduce / total_score * 100:.4f}%)''') 264 | except Exception: 265 | await query_chart.send("格式错误,输入“分数线 帮助”以查看帮助信息") 266 | 267 | 268 | best_40_pic = on_command('b40') 269 | 270 | 271 | @best_40_pic.handle() 272 | async def _(event: Event, message: Message = CommandArg()): 273 | username = str(message).strip() 274 | if username == "": 275 | payload = {'qq': str(event.get_user_id())} 276 | else: 277 | payload = {'username': username} 278 | img, success = await generate(payload) 279 | if success == 400: 280 | await best_40_pic.send("未找到此玩家,请确保此玩家的用户名和查分器中的用户名相同。") 281 | elif success == 403: 282 | await best_40_pic.send("该用户禁止了其他人获取数据。") 283 | else: 284 | await best_40_pic.send(Message([ 285 | MessageSegment("image", { 286 | "file": f"base64://{str(image_to_base64(img), encoding='utf-8')}" 287 | }) 288 | ])) 289 | 290 | best_50_pic = on_command('b50') 291 | 292 | 293 | @best_50_pic.handle() 294 | async def _(event: Event, message: Message = CommandArg()): 295 | username = str(message).strip() 296 | if username == "": 297 | payload = {'qq': str(event.get_user_id()),'b50':True} 298 | else: 299 | payload = {'username': username,'b50': True} 300 | img, success = await generate50(payload) 301 | if success == 400: 302 | await best_50_pic.send("未找到此玩家,请确保此玩家的用户名和查分器中的用户名相同。") 303 | elif success == 403: 304 | await best_50_pic.send("该用户禁止了其他人获取数据。") 305 | else: 306 | await best_50_pic.send(Message([ 307 | MessageSegment("image", { 308 | "file": f"base64://{str(image_to_base64(img), encoding='utf-8')}" 309 | }) 310 | ])) 311 | -------------------------------------------------------------------------------- /src/plugins/public.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command, on_notice 2 | from nonebot.typing import T_State 3 | from nonebot.adapters.onebot.v11 import Message, Event, Bot, MessageSegment 4 | from nonebot.exception import IgnoredException 5 | from nonebot.message import event_preprocessor 6 | from src.libraries.image import * 7 | 8 | 9 | @event_preprocessor 10 | async def preprocessor(bot, event, state): 11 | if hasattr(event, 'message_type') and event.message_type == "private" and event.sub_type != "friend": 12 | raise IgnoredException("not reply group temp message") 13 | 14 | 15 | help = on_command('help') 16 | 17 | 18 | @help.handle() 19 | async def _(bot: Bot, event: Event, state: T_State): 20 | help_str = '''可用命令如下: 21 | 今日舞萌 查看今天的舞萌运势 22 | XXXmaimaiXXX什么 随机一首歌 23 | 随个[dx/标准][绿黄红紫白]<难度> 随机一首指定条件的乐曲 24 | 查歌<乐曲标题的一部分> 查询符合条件的乐曲 25 | [绿黄红紫白]id<歌曲编号> 查询乐曲信息或谱面信息 26 | <歌曲别名>是什么歌 查询乐曲别名对应的乐曲 27 | 定数查歌 <定数> 查询定数对应的乐曲 28 | 定数查歌 <定数下限> <定数上限> 29 | 分数线 <难度+歌曲id> <分数线> 详情请输入“分数线 帮助”查看''' 30 | await help.send(Message([ 31 | MessageSegment("image", { 32 | "file": f"base64://{str(image_to_base64(text_to_image(help_str)), encoding='utf-8')}" 33 | }) 34 | ])) 35 | 36 | 37 | async def _group_poke(bot: Bot, event: Event) -> bool: 38 | value = (event.notice_type == "notify" and event.sub_type == "poke" and event.target_id == int(bot.self_id)) 39 | return value 40 | 41 | 42 | poke = on_notice(rule=_group_poke, priority=10, block=True) 43 | 44 | 45 | @poke.handle() 46 | async def _(bot: Bot, event: Event, state: T_State): 47 | if event.__getattribute__('group_id') is None: 48 | event.__delattr__('group_id') 49 | await poke.send(Message([ 50 | MessageSegment("poke", { 51 | "qq": f"{event.sender_id}" 52 | }) 53 | ])) 54 | 55 | --------------------------------------------------------------------------------